diff --git a/src/components/daos/dao-buy-input.tsx b/src/components/daos/dao-buy-input.tsx index ce25f302..bea08648 100644 --- a/src/components/daos/dao-buy-input.tsx +++ b/src/components/daos/dao-buy-input.tsx @@ -2,7 +2,7 @@ import type React from "react"; -import { useState, useCallback, useRef } from "react"; +import { useState, useCallback, useRef, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Wallet } from "lucide-react"; @@ -15,6 +15,8 @@ interface TokenBuyInputProps { contractPrincipal: string; disabled?: boolean; onSend: () => void; + initialAmount?: string; + onAmountChange?: (amount: string) => void; } export function TokenBuyInput({ @@ -22,19 +24,28 @@ export function TokenBuyInput({ contractPrincipal, disabled = false, onSend, + initialAmount = "", + onAmountChange, }: TokenBuyInputProps) { - const [amount, setAmount] = useState(""); + const [amount, setAmount] = useState(initialAmount); const inputRef = useRef(null); const containerRef = useRef(null); const { sendMessage, activeThreadId } = useChatStore(); const { accessToken } = useSessionStore(); + useEffect(() => { + // Update amount when initialAmount prop changes + if (initialAmount) { + setAmount(initialAmount); + } + }, [initialAmount]); + const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); if (!amount.trim() || !accessToken || !activeThreadId) return; - const message = `Buy ${amount} satoshis of ${tokenName}.\nToken DEX: ${contractPrincipal}`; + const message = `Buy ${tokenName} tokens worth ${amount} satoshis.\nToken DEX: ${contractPrincipal}`; try { await sendMessage(activeThreadId, message); @@ -55,11 +66,19 @@ export function TokenBuyInput({ ] ); - const handleChange = useCallback((e: React.ChangeEvent) => { - // Only allow numbers and decimal points - const value = e.target.value.replace(/[^\d.]/g, ""); - setAmount(value); - }, []); + const handleChange = useCallback( + (e: React.ChangeEvent) => { + // Only allow numbers and decimal points + const value = e.target.value.replace(/[^\d.]/g, ""); + setAmount(value); + + // Notify parent component about the amount change + if (onAmountChange) { + onAmountChange(value); + } + }, + [onAmountChange] + ); const handleFocus = () => { // Mobile scroll fix @@ -70,34 +89,60 @@ export function TokenBuyInput({ if (!accessToken) return null; + // Convert satoshis to BTC for display + const satoshiToBTC = (satoshis: string) => { + if (!satoshis || isNaN(Number(satoshis))) return "0.00000000"; + return (Number(satoshis) / 100000000).toFixed(8); + }; + + const btcValue = satoshiToBTC(amount); + return ( -
-
-
-
+
+
+ +
+ {amount ? `${btcValue} BTC` : "0 BTC"} +
+
- +
+ +
+ + sats + +
+
-
diff --git a/src/components/daos/dao-buy-modal.tsx b/src/components/daos/dao-buy-modal.tsx index 7149cd9b..eb276eac 100644 --- a/src/components/daos/dao-buy-modal.tsx +++ b/src/components/daos/dao-buy-modal.tsx @@ -10,15 +10,17 @@ import { DialogTrigger, DialogDescription, } from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Info, Loader2 } from "lucide-react"; +import { Info, Loader2, Wallet } from "lucide-react"; import { TokenBuyInput } from "./dao-buy-input"; import AgentWalletSelector from "@/components/chat/agent-selector"; import { useChatStore } from "@/store/chat"; import { useSessionStore } from "@/store/session"; +import { useWalletStore } from "@/store/wallet"; import { fetchDAOExtensions, fetchToken } from "@/queries/daoQueries"; import type { DAO, Token, Extension } from "@/types/supabase"; import { useToast } from "@/hooks/use-toast"; +import { Button } from "@/components/ui/button"; +import { WalletBalance, WalletWithAgent } from "@/store/wallet"; interface DAOChatModalProps { daoId: string; @@ -27,6 +29,7 @@ interface DAOChatModalProps { trigger?: React.ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void; + presetAmount?: string; } export function DAOBuyModal({ @@ -35,8 +38,10 @@ export function DAOBuyModal({ open: controlledOpen, onOpenChange: setControlledOpen, token, + presetAmount = "", }: DAOChatModalProps) { const [uncontrolledOpen, setUncontrolledOpen] = useState(false); + const [currentAmount, setCurrentAmount] = useState(presetAmount); const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen; const setOpen = setControlledOpen || setUncontrolledOpen; @@ -51,6 +56,14 @@ export function DAOBuyModal({ } = useChatStore(); const { accessToken } = useSessionStore(); + const { balances, userWallet, agentWallets } = useWalletStore(); + + useEffect(() => { + // Update current amount when presetAmount changes + if (presetAmount) { + setCurrentAmount(presetAmount); + } + }, [presetAmount]); const { data: daoExtensions, isLoading: isExtensionsLoading } = useQuery({ queryKey: ["daoExtensions", daoId], @@ -92,18 +105,81 @@ export function DAOBuyModal({ }; }, [accessToken, memoizedConnect, isConnected, open]); + const handleAmountChange = (newAmount: string) => { + setCurrentAmount(newAmount); + }; + const handleSendMessage = () => { toast({ title: "Message sent successfully", description: "The agent will receive funds shortly.", }); + setOpen(false); + }; + + // Format the balance from microSTX to STX with 6 decimal places + const formatBalance = (balance: string) => { + if (!balance) return "0.000000"; + return (Number(balance) / 1_000_000).toFixed(6); + }; + + // Convert satoshis to BTC + const satoshiToBTC = (satoshis: string) => { + if (!satoshis || isNaN(Number(satoshis))) return "0.00000000"; + return (Number(satoshis) / 100000000).toFixed(8); + }; + + // Get the current agent's wallet and balance + const getCurrentAgentWallet = () => { + if (!selectedAgentId && !userWallet) return null; + + const isMainnet = process.env.NEXT_PUBLIC_STACKS_NETWORK === "mainnet"; + + if (!selectedAgentId) { + // User wallet selected + if (!userWallet) return null; + + const address = isMainnet + ? userWallet.mainnet_address + : userWallet.testnet_address; + if (!address) return null; + + return { + address, + walletBalance: balances[address] as WalletBalance | undefined, + }; + } else { + // Agent wallet selected + const agentWallet = agentWallets.find( + (w) => w.agent_id === selectedAgentId + ) as WalletWithAgent | undefined; + if (!agentWallet) return null; + + const address = isMainnet + ? agentWallet.mainnet_address + : agentWallet.testnet_address; + if (!address) return null; + + return { + address, + walletBalance: balances[address] as WalletBalance | undefined, + }; + } }; + const agentWalletData = getCurrentAgentWallet(); + const btcValue = satoshiToBTC(currentAmount); + const renderBuySection = () => { if (!accessToken) { return ( -
- Please sign in to buy tokens +
+
+

Please sign in to buy tokens

+ +
); } @@ -111,8 +187,8 @@ export function DAOBuyModal({ if (isExtensionsLoading || isTokenLoading) { return (
- - Loading... + + Loading...
); } @@ -120,10 +196,11 @@ export function DAOBuyModal({ const tokenDexExtension = daoExtensions?.find( (ext: Extension) => ext.type === "TOKEN_DEX" ); + const tokenName = tokenData?.symbol || "DAO"; return (
-
+
-
-
- -

- The selected agent's address will receive the funds from this - transaction. -

+
+
+ +
+

+ The selected agent will receive{" "} + {tokenName} tokens worth: +

+
+
+ {btcValue} BTC +
+
+
+ + {/* Agent Wallet Balance Display - Simplified */} + {agentWalletData && agentWalletData.walletBalance && ( +
+

+ + Available Balance +

+ +
+ {/* STX Balance */} + {agentWalletData.walletBalance.stx && ( +
+ STX Balance + + {formatBalance(agentWalletData.walletBalance.stx.balance)}{" "} + STX + +
+ )} + + {/* Fungible tokens - simplified display */} + {agentWalletData.walletBalance.fungible_tokens && + Object.entries( + agentWalletData.walletBalance.fungible_tokens + ).map(([tokenId, token], index, arr) => { + const tokenSymbol = tokenId.split("::")[1] || "Token"; + const isLast = index === arr.length - 1; + return ( +
+ {tokenSymbol} + + {formatBalance(token.balance)} + +
+ ); + })} + + {/* Show message if no tokens found */} + {(!agentWalletData.walletBalance.stx || + Object.keys( + agentWalletData.walletBalance.fungible_tokens || {} + ).length === 0) && ( +
+ No tokens found +
+ )} +
+
+ )}
-
+
{tokenDexExtension ? ( ) : ( -
+
Unavailable to buy tokens
)} @@ -165,17 +306,12 @@ export function DAOBuyModal({ return ( - - {trigger || ( - - )} - - + {trigger && {trigger}} + Buy {tokenName} Tokens - Purchase {tokenName} tokens through your selected agent + Purchase {tokenName} tokens with LucideBitcoin through your selected + agent
{renderBuySection()}
diff --git a/src/components/daos/dao-buy-token.tsx b/src/components/daos/dao-buy-token.tsx index 25d6bcc9..3e66ddd8 100644 --- a/src/components/daos/dao-buy-token.tsx +++ b/src/components/daos/dao-buy-token.tsx @@ -1,3 +1,7 @@ +"use client"; + +import type React from "react"; +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { DAOBuyModal } from "./dao-buy-modal"; @@ -6,10 +10,29 @@ interface DAOBuyTokenProps { } export function DAOBuyToken({ daoId }: DAOBuyTokenProps) { + const [presetAmount, setPresetAmount] = useState(""); + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleQuickBuy = (amount: string) => { + setPresetAmount(amount); + setIsModalOpen(true); + }; + return ( - Buy} - /> +
+ + +
); } diff --git a/src/components/daos/dao-extensions.tsx b/src/components/daos/dao-extensions.tsx index 3bb737a1..02a5025e 100644 --- a/src/components/daos/dao-extensions.tsx +++ b/src/components/daos/dao-extensions.tsx @@ -1,8 +1,8 @@ import React, { useState } from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { ArrowUpRight } from "lucide-react"; import type { Extension } from "@/types/supabase"; +import { CopyButton } from "./dao-proposals"; interface DAOExtensionsProps { extensions: Extension[]; @@ -32,6 +32,8 @@ const getStatusColor = (status: Extension["status"]) => { return "bg-emerald-500/10 text-emerald-500 border-emerald-500/20"; case "pending": return "bg-amber-500/10 text-amber-500 border-amber-500/20"; + default: + return ""; } }; @@ -135,15 +137,17 @@ export default function DAOExtensions({ extensions }: DAOExtensionsProps) {
)} {extension.tx_id && ( - - TX: {truncateString(extension.tx_id)} - - + )}
diff --git a/src/components/daos/dao-proposals.tsx b/src/components/daos/dao-proposals.tsx index b0feee72..a5271d36 100644 --- a/src/components/daos/dao-proposals.tsx +++ b/src/components/daos/dao-proposals.tsx @@ -1,5 +1,9 @@ +"use client"; + +import type React from "react"; import { useState } from "react"; import { deserializeCV, cvToString } from "@stacks/transactions"; +import { format } from "date-fns"; import { Card, CardHeader, @@ -27,8 +31,6 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { format } from "date-fns"; -import type { Proposal } from "@/types/supabase"; import { Timer, CheckCircle2, @@ -44,39 +46,66 @@ import { ArrowRight, Copy, CheckIcon, - MessageSquare, Info, } from "lucide-react"; +export interface Proposal { + id: string; + created_at: string; + title: string; + description: string; + status: "DRAFT" | "PENDING" | "DEPLOYED" | "FAILED"; + contract_principal: string; + tx_id: string; + dao_id: string; + proposal_id: string; + action: string; + caller: string; + creator: string; + created_at_block: number; + end_block: number; + start_block: number; + liquid_tokens: number | null; + parameters: string; + concluded_by: string; + executed: boolean; + met_quorum: boolean; + met_threshold: boolean; + passed: boolean; + votes_against: string; + votes_for: string; +} + interface DAOProposalsProps { proposals: Proposal[]; } +/** StatusBadge highlights the proposal state with a badge */ const StatusBadge = ({ status }: { status: Proposal["status"] }) => { const config = { DRAFT: { - color: "bg-gray-500/10 text-gray-500", + color: "bg-secondary/10 text-secondary", icon: FileEdit, label: "Draft", - tooltip: "This proposal is in draft state and not yet active", + tooltip: "This proposal is in draft state and not yet active.", }, PENDING: { - color: "bg-blue-500/10 text-blue-500", + color: "bg-primary/10 text-primary", icon: Timer, label: "Pending", - tooltip: "This proposal is waiting for AI agents to vote", + tooltip: "This proposal is awaiting votes.", }, DEPLOYED: { - color: "bg-green-500/10 text-green-500", + color: "bg-primary/10 text-primary", icon: CheckCircle2, label: "Deployed", - tooltip: "This proposal has been approved and deployed by AI agents", + tooltip: "This proposal has been approved and executed.", }, FAILED: { - color: "bg-red-500/10 text-red-500", + color: "bg-secondary/10 text-secondary", icon: XCircle, label: "Failed", - tooltip: "This proposal failed to receive enough support from AI agents", + tooltip: "This proposal did not receive enough support.", }, }; @@ -101,27 +130,30 @@ const StatusBadge = ({ status }: { status: Proposal["status"] }) => { ); }; +/** Utility: Truncate a string with fallback text */ const truncateString = ( str: string, startLength: number, endLength: number ) => { - if (!str) return ""; + if (!str) return "No data available"; if (str.length <= startLength + endLength) return str; return `${str.slice(0, startLength)}...${str.slice(-endLength)}`; }; +/** Format the action string */ const formatAction = (action: string) => { - if (!action) return ""; + if (!action) return "No data available"; const parts = action.split("."); - if (parts.length <= 1) return action.toUpperCase(); - return parts[parts.length - 1].toUpperCase(); + return parts.length <= 1 + ? action.toUpperCase() + : parts[parts.length - 1].toUpperCase(); }; +/** Generate explorer links */ const getExplorerLink = (type: string, value: string) => { const isTestnet = process.env.NEXT_PUBLIC_STACKS_NETWORK === "testnet"; const testnetParam = isTestnet ? "?chain=testnet" : ""; - switch (type) { case "tx": return `http://explorer.hiro.so/txid/${value}${testnetParam}`; @@ -134,6 +166,7 @@ const getExplorerLink = (type: string, value: string) => { } }; +/** BlockVisual displays block information */ const BlockVisual = ({ value, type = "stacks", @@ -141,16 +174,14 @@ const BlockVisual = ({ value: number | null; type?: "stacks" | "bitcoin"; }) => { - if (value === null) return N/A; - - const color = type === "stacks" ? "bg-orange-500" : "bg-yellow-500"; + if (value === null) return No data available; + const color = type === "stacks" ? "bg-primary" : "bg-secondary"; const icon = type === "stacks" ? (
) : (
); - const label = type === "stacks" ? "STX" : "BTC"; const tooltip = type === "stacks" @@ -164,7 +195,7 @@ const BlockVisual = ({
{icon} {value.toLocaleString()} - {label} + {label}
@@ -175,83 +206,76 @@ const BlockVisual = ({ ); }; +/** LabeledField displays a label with its value and an optional copy button */ const LabeledField = ({ icon: Icon, label, value, copy, - tooltip, link, - showTooltip = true, - showCopy = true, }: { icon: React.ElementType; label: string; value: string | React.ReactNode; copy?: string; - tooltip?: string; link?: string; - showTooltip?: boolean; - showCopy?: boolean; }) => { - const content = ( -
+ const displayValue = + (typeof value === "string" && value.trim() === "") || !value + ? "No data available" + : value; + return ( +
- - {label}: - + {label}: {link ? ( - {value} + {displayValue} ) : ( - value + displayValue )} - {copy && showCopy && } - {tooltip && showTooltip && ( - - - - - - -

{tooltip}

-
-
-
+ {copy && ( + )}
); - - return content; }; -const CopyButton = ({ text }: { text: string }) => { +/** CopyButton (if needed) */ +export const CopyButton = ({ text }: { text: string }) => { const [copied, setCopied] = useState(false); - const handleCopy = () => { navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); }; - return ( )}
diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 00000000..15caaa2e --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +interface ProgressProps extends React.HTMLAttributes { + value?: number; +} + +const Progress = React.forwardRef( + ({ className, value = 0, ...props }, ref) => { + const percentage = Math.min(Math.max(value, 0), 100); + + return ( +
+
+
+ ); + } +); + +Progress.displayName = "Progress"; + +export { Progress }; diff --git a/src/store/wallet.ts b/src/store/wallet.ts index ca126b46..a3f41d48 100644 --- a/src/store/wallet.ts +++ b/src/store/wallet.ts @@ -2,7 +2,7 @@ import { create } from 'zustand'; import { supabase } from '@/utils/supabase/client'; import { Wallet, Agent } from '@/types/supabase'; -interface TokenBalance { +export interface TokenBalance { balance: string; total_sent: string; total_received: string; @@ -24,7 +24,7 @@ export interface WalletBalance { }; } -interface WalletWithAgent extends Wallet { +export interface WalletWithAgent extends Wallet { agent?: Agent; } diff --git a/src/types/supabase.ts b/src/types/supabase.ts index ab2c06f7..2b8c6a20 100644 --- a/src/types/supabase.ts +++ b/src/types/supabase.ts @@ -91,8 +91,6 @@ export interface Proposal { created_at: string; title: string; description: string; - link: string | null; - monetary_ask: null; status: "DRAFT" | "PENDING" | "DEPLOYED" | "FAILED"; contract_principal: string; tx_id: string; @@ -106,6 +104,13 @@ export interface Proposal { start_block: number; liquid_tokens: number | null; parameters: string; + concluded_by: string; + executed: boolean; + met_quorum: boolean; + met_threshold: boolean; + passed: boolean; + votes_against: string; + votes_for: string; } export interface CronEntry {