From 97da9463a9987a3de7c553885e5abebccd2ad2f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:07:19 +0000 Subject: [PATCH 1/9] Initial plan From f96d63f89822615d1e9ab93c4766fdc9e2955646 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:13:44 +0000 Subject: [PATCH 2/9] feat: add animated 0x402 Payment Executor section to workflow page - Add AnimatePresence to framer-motion import - Add PaymentExecutorSection component with agent selection, task input, Freighter signing flow, and animated invoice panel - Add AgentInfo, InvoiceData, ExecutorStep types and helpers - Render PaymentExecutorSection at bottom of WorkflowPage return Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: mesayanroy <169074736+mesayanroy@users.noreply.github.com> --- app/workflow/page.tsx | 543 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 542 insertions(+), 1 deletion(-) diff --git a/app/workflow/page.tsx b/app/workflow/page.tsx index a1ce0e0..2cdf89d 100644 --- a/app/workflow/page.tsx +++ b/app/workflow/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useRef, useEffect, useCallback } from 'react'; -import { motion } from 'framer-motion'; +import { motion, AnimatePresence } from 'framer-motion'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -206,6 +206,537 @@ function ToolBtn({ ); } +// ─── Payment Executor Types ──────────────────────────────────────────────────── + +interface AgentInfo { + id: string; + name: string; + description?: string; + priceXlm: number; + ownerAddress: string; +} + +interface InvoiceData { + invoiceNumber: string; + agentId: string; + agentName: string; + task: string; + priceXlm: number; + txHash: string; + fromWallet: string; + timestamp: string; + explorerUrl: string; +} + +type ExecutorStep = + | 'idle' + | 'checking_wallet' + | 'building_tx' + | 'signing' + | 'submitting' + | 'confirming' + | 'running_agent' + | 'done' + | 'error'; + +const EXECUTOR_STEP_LABELS: Record = { + idle: 'Execute Task', + checking_wallet: 'Checking wallet…', + building_tx: 'Building transaction…', + signing: 'Waiting for Freighter…', + submitting: 'Submitting to Stellar…', + confirming: 'Confirming on ledger…', + running_agent: 'Running agent…', + done: 'Done', + error: 'Retry', +}; + +function extractStellarError(err: unknown): string { + if (!err) return 'Unknown error'; + if (typeof err === 'object' && err !== null) { + const e = err as Record; + try { + const resultCodes = ( + (e.response as Record)?.data as Record + )?.extras as Record; + if (resultCodes?.result_codes) { + const rc = resultCodes.result_codes as Record; + return `Transaction failed: ${rc.transaction || ''} ops: ${JSON.stringify(rc.operations || [])}`; + } + } catch { /* fall through */ } + } + const msg = String(err); + if (msg.includes('Resource Missing') || msg.includes('404')) + return 'Account not found on Stellar network. Make sure Freighter is funded on the correct network.'; + if (msg.includes('403') || msg.includes('Forbidden')) + return 'Access denied. Please unlock Freighter and try again.'; + return msg.startsWith('Error:') ? msg.slice(7).trim() : msg; +} + +async function waitForLedger( + horizonServer: import('stellar-sdk').Horizon.Server, + txHash: string, + timeoutMs = 30_000 +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + try { + await horizonServer.transactions().transaction(txHash).call(); + return; + } catch { /* not yet */ } + await new Promise((r) => setTimeout(r, 2_000)); + } +} + +// ─── Payment Executor Section ────────────────────────────────────────────────── + +function PaymentExecutorSection({ walletAddress }: { walletAddress: string }) { + const [agents, setAgents] = useState([]); + const [loadingAgents, setLoadingAgents] = useState(false); + const [agentsError, setAgentsError] = useState(null); + + const [selectedAgent, setSelectedAgent] = useState(null); + const [taskPrompt, setTaskPrompt] = useState(''); + + const [showConfirmModal, setShowConfirmModal] = useState(false); + const [step, setStep] = useState('idle'); + const [stepError, setStepError] = useState(null); + const [invoice, setInvoice] = useState(null); + + useEffect(() => { + if (!walletAddress) return; + setLoadingAgents(true); + setAgentsError(null); + fetch(`/api/agents/list?owner=${walletAddress}`) + .then((r) => r.json()) + .then((data) => { + const list: AgentInfo[] = (Array.isArray(data) ? data : data?.agents ?? []).map( + (a: Record) => ({ + id: String(a.id ?? a.agent_id ?? ''), + name: String(a.name ?? a.agent_name ?? 'Unnamed Agent'), + description: a.description ? String(a.description) : undefined, + priceXlm: Number(a.price_xlm ?? a.priceXlm ?? 0.1), + ownerAddress: String(a.owner_address ?? a.ownerAddress ?? walletAddress), + }) + ); + setAgents(list); + }) + .catch((e) => setAgentsError(String(e))) + .finally(() => setLoadingAgents(false)); + }, [walletAddress]); + + const handleExecute = async () => { + if (!selectedAgent || !taskPrompt.trim()) return; + setShowConfirmModal(false); + setStep('checking_wallet'); + setStepError(null); + setInvoice(null); + + try { + const StellarSdk = await import('stellar-sdk'); + const freighter = await import('@stellar/freighter-api'); + + const connResult = await freighter.isConnected(); + if (!connResult.isConnected) + throw new Error('Freighter wallet is not installed. Visit https://www.freighter.app'); + + const accessResult = await freighter.requestAccess(); + if (accessResult && 'error' in accessResult && accessResult.error) + throw new Error('Freighter access denied. Please allow this site in Freighter.'); + + const { address: senderKey, error: addrErr } = await freighter.getAddress(); + if (addrErr || !senderKey) + throw new Error('Could not get wallet address. Ensure Freighter is unlocked.'); + + setStep('building_tx'); + + const isMainnet = process.env.NEXT_PUBLIC_STELLAR_NETWORK === 'mainnet'; + const horizonUrl = + process.env.NEXT_PUBLIC_HORIZON_URL ?? + (isMainnet ? 'https://horizon.stellar.org' : 'https://horizon-testnet.stellar.org'); + const networkPassphrase = isMainnet ? StellarSdk.Networks.PUBLIC : StellarSdk.Networks.TESTNET; + const horizonServer = new StellarSdk.Horizon.Server(horizonUrl); + + const senderAccount = await horizonServer.loadAccount(senderKey); + const memo = `agent:${selectedAgent.id}`.slice(0, 28); + + const tx = new StellarSdk.TransactionBuilder(senderAccount, { + fee: StellarSdk.BASE_FEE, + networkPassphrase, + }) + .addOperation( + StellarSdk.Operation.payment({ + destination: selectedAgent.ownerAddress, + asset: StellarSdk.Asset.native(), + amount: selectedAgent.priceXlm.toFixed(7), + }) + ) + .addMemo(StellarSdk.Memo.text(memo)) + .setTimeout(60) + .build(); + + setStep('signing'); + + const signedResult = await freighter.signTransaction(tx.toXDR(), { networkPassphrase }); + if (signedResult.error) throw new Error(String(signedResult.error)); + + const signedTx = StellarSdk.TransactionBuilder.fromXDR(signedResult.signedTxXdr, networkPassphrase); + + setStep('submitting'); + const submitResult = await horizonServer.submitTransaction(signedTx); + const txHash = submitResult.hash; + + setStep('confirming'); + await waitForLedger(horizonServer, txHash); + + setStep('running_agent'); + + const explorerNet = isMainnet ? 'public' : 'testnet'; + const explorerUrl = `https://stellar.expert/explorer/${explorerNet}/tx/${txHash}`; + + const runRes = await fetch(`/api/agents/${selectedAgent.id}/run`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-payment-tx-hash': txHash, + 'x-payment-from': senderKey, + }, + body: JSON.stringify({ prompt: taskPrompt, task: taskPrompt }), + }); + + const runData = await runRes.json().catch(() => ({})); + if (!runRes.ok && runRes.status !== 200) { + // Payment succeeded; agent run had an issue — still show invoice + console.warn('Agent run error:', runData); + } + + setInvoice({ + invoiceNumber: `INV-${Date.now().toString(36).toUpperCase()}`, + agentId: selectedAgent.id, + agentName: selectedAgent.name, + task: taskPrompt, + priceXlm: selectedAgent.priceXlm, + txHash, + fromWallet: senderKey, + timestamp: new Date().toISOString(), + explorerUrl, + }); + + setStep('done'); + } catch (err) { + setStepError(extractStellarError(err)); + setStep('error'); + } + }; + + const busy = step !== 'idle' && step !== 'done' && step !== 'error'; + const isMainnet = process.env.NEXT_PUBLIC_STELLAR_NETWORK === 'mainnet'; + + return ( + + {/* Heading */} +
+
+ + + +
+
+

0x402 Payment Executor

+

Select an agent, enter a task, pay & execute via Stellar.

+
+
+ +
+ {/* ── Agent Selection ── */} +
+

Your Agents

+ + {loadingAgents && ( +
+ + Loading agents… +
+ )} + + {agentsError && ( +
+ {agentsError} +
+ )} + + {!loadingAgents && !agentsError && agents.length === 0 && ( +
+

No agents found for this wallet.

+

Deploy an agent from the dashboard first.

+
+ )} + +
+ {agents.map((agent, i) => ( + { setSelectedAgent(agent); setStep('idle'); setStepError(null); setInvoice(null); }} + className={`w-full text-left p-3 rounded-xl border transition-all ${ + selectedAgent?.id === agent.id + ? 'border-[rgba(0,255,229,0.4)] bg-[rgba(0,255,229,0.06)]' + : 'border-[rgba(255,255,255,0.06)] bg-[rgba(255,255,255,0.02)] hover:border-[rgba(0,255,229,0.2)] hover:bg-[rgba(0,255,229,0.03)]' + }`} + > +
+ + {agent.name} + + {agent.priceXlm} XLM +
+ {agent.description && ( +

{agent.description}

+ )} +

{agent.id}

+
+ ))} +
+
+ + {/* ── Task Input + Execute ── */} +
+

Task / Prompt

+ +