From 3ba60c41e0adca04be0ba60d060908c4f244eda4 Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 11 Jun 2026 22:17:14 +0800 Subject: [PATCH] feat: prioritize wallet payments and live balance --- apps/web/src/app/admin/treasury/page.tsx | 37 ++++++++++ .../api/admin/treasury/usdc-balance/route.ts | 68 +++++++++++++++++++ apps/web/src/app/propose/actions.ts | 8 +++ apps/web/src/app/propose/page.tsx | 28 ++++++-- apps/web/src/app/propose/success/page.tsx | 34 +++++++++- apps/web/src/lib/a2a-growth.ts | 24 ++++++- 6 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 apps/web/src/app/api/admin/treasury/usdc-balance/route.ts diff --git a/apps/web/src/app/admin/treasury/page.tsx b/apps/web/src/app/admin/treasury/page.tsx index 2125a1c..a95fc2f 100644 --- a/apps/web/src/app/admin/treasury/page.tsx +++ b/apps/web/src/app/admin/treasury/page.tsx @@ -53,8 +53,19 @@ type TreasuryStats = { }>; }; +type LiveUsdcBalance = { + wallet: string; + network: string; + chain_id: string; + token_address: string; + balance_usdc: string; + checked_at: string; +}; + export default function TreasuryDashboard() { const [stats, setStats] = useState(null); + const [liveUsdcBalance, setLiveUsdcBalance] = useState(null); + const [liveUsdcBalanceError, setLiveUsdcBalanceError] = useState(""); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(""); const [withdrawing, setWithdrawing] = useState(false); @@ -86,6 +97,16 @@ export default function TreasuryDashboard() { } setStats(data); setLoadError(""); + + const balanceRes = await fetch("/api/admin/treasury/usdc-balance"); + const balanceData = await balanceRes.json().catch(() => ({})); + if (balanceRes.ok) { + setLiveUsdcBalance(balanceData); + setLiveUsdcBalanceError(""); + } else { + setLiveUsdcBalance(null); + setLiveUsdcBalanceError(balanceData.error || `USDC balance request failed (${balanceRes.status})`); + } } catch (error) { setLoadError(error instanceof Error ? error.message : "Unable to load treasury stats."); } finally { @@ -324,6 +345,22 @@ export default function TreasuryDashboard() { Lifetime Revenue: ${(cents(stats.revenue?.usdc) / 100).toLocaleString()} Total GMV: ${(cents(stats.gmv?.usdc) / 100).toLocaleString()} +
+ {liveUsdcBalance ? ( + <> +
+ Live wallet balance + {Number(liveUsdcBalance.balance_usdc).toFixed(2)} USDC +
+
+ {liveUsdcBalance.network} · chain {liveUsdcBalance.chain_id} +
+
{liveUsdcBalance.wallet}
+ + ) : ( + {liveUsdcBalanceError || "Live wallet balance is loading."} + )} +
{/* Fiat Treasury */} diff --git a/apps/web/src/app/api/admin/treasury/usdc-balance/route.ts b/apps/web/src/app/api/admin/treasury/usdc-balance/route.ts new file mode 100644 index 0000000..acc71dc --- /dev/null +++ b/apps/web/src/app/api/admin/treasury/usdc-balance/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from "next/server"; +import { adminUnauthorizedResponse, isAdminRequestAuthorized } from "@/lib/admin-auth"; +import { + TREASURY_USDC_ADDRESS, + TREASURY_USDC_CHAIN_ID, + TREASURY_USDC_NETWORK, + TREASURY_USDC_RPC_URL, + TREASURY_USDC_TOKEN_ADDRESS, +} from "@/lib/a2a-growth"; + +function encodeBalanceOf(address: string) { + return `0x70a08231${address.toLowerCase().replace(/^0x/, "").padStart(64, "0")}`; +} + +function formatUsdc(raw: bigint) { + const decimals = BigInt(1_000_000); + return `${raw / decimals}.${String(raw % decimals).padStart(6, "0")}`; +} + +export async function GET(request: NextRequest) { + if (!isAdminRequestAuthorized(request)) { + return adminUnauthorizedResponse(); + } + + if (!TREASURY_USDC_ADDRESS || !TREASURY_USDC_TOKEN_ADDRESS || !TREASURY_USDC_RPC_URL) { + return NextResponse.json({ error: "Treasury USDC wallet is not configured" }, { status: 503 }); + } + + try { + const response = await fetch(TREASURY_USDC_RPC_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "eth_call", + params: [ + { + to: TREASURY_USDC_TOKEN_ADDRESS, + data: encodeBalanceOf(TREASURY_USDC_ADDRESS), + }, + "latest", + ], + }), + cache: "no-store", + }); + const data = await response.json().catch(() => ({})); + if (!response.ok || data.error) { + return NextResponse.json({ + error: data.error?.message || `USDC balance RPC failed (${response.status})`, + }, { status: 502 }); + } + + const raw = BigInt(typeof data.result === "string" ? data.result : "0x0"); + return NextResponse.json({ + wallet: TREASURY_USDC_ADDRESS, + network: TREASURY_USDC_NETWORK, + chain_id: TREASURY_USDC_CHAIN_ID, + token_address: TREASURY_USDC_TOKEN_ADDRESS, + balance_raw: raw.toString(), + balance_usdc: formatUsdc(raw), + checked_at: new Date().toISOString(), + }); + } catch (error: unknown) { + console.error("[Treasury USDC Balance Error]", error); + return NextResponse.json({ error: "Unable to read treasury USDC balance" }, { status: 502 }); + } +} diff --git a/apps/web/src/app/propose/actions.ts b/apps/web/src/app/propose/actions.ts index 9a9ab50..d2aa609 100644 --- a/apps/web/src/app/propose/actions.ts +++ b/apps/web/src/app/propose/actions.ts @@ -6,6 +6,9 @@ import { getProposalPackage, sanitizeAgentId, TREASURY_USDC_ADDRESS, + TREASURY_USDC_CHAIN_ID, + TREASURY_USDC_NETWORK, + TREASURY_USDC_TOKEN_ADDRESS, TREASURY_WALLET_LABEL, VIBEWORK_SITE_URL, } from "@/lib/a2a-growth"; @@ -217,6 +220,9 @@ export async function createDemandProposal(formData: FormData) { amount_cents: proposalPackage.feeCents, treasury_wallet_label: TREASURY_WALLET_LABEL, treasury_usdc_address_present: Boolean(TREASURY_USDC_ADDRESS), + treasury_usdc_network: TREASURY_USDC_NETWORK, + treasury_usdc_chain_id: TREASURY_USDC_CHAIN_ID, + treasury_usdc_token_address: TREASURY_USDC_TOKEN_ADDRESS, }, }, }); @@ -234,6 +240,8 @@ export async function createDemandProposal(formData: FormData) { package_id: proposalPackage.id, amount_cents: proposalPackage.feeCents, referral_agent: referralAgent, + treasury_usdc_network: TREASURY_USDC_NETWORK, + treasury_usdc_chain_id: TREASURY_USDC_CHAIN_ID, response_status: 200, response_summary: "wallet_payment_instructions_issued", }, diff --git a/apps/web/src/app/propose/page.tsx b/apps/web/src/app/propose/page.tsx index 05d88d7..cf0ddc3 100644 --- a/apps/web/src/app/propose/page.tsx +++ b/apps/web/src/app/propose/page.tsx @@ -1,5 +1,12 @@ import { createDemandProposal } from "@/app/propose/actions"; -import { buildAgentGrowthKit, getProposalPackage, PROPOSAL_PACKAGES, sanitizeAgentId } from "@/lib/a2a-growth"; +import { + buildAgentGrowthKit, + getProposalPackage, + PROPOSAL_PACKAGES, + sanitizeAgentId, + TREASURY_USDC_ADDRESS, + TREASURY_USDC_NETWORK, +} from "@/lib/a2a-growth"; import { logA2aTrafficEvent } from "@/lib/a2a-traffic"; import { Activity, ArrowRight, Bot, CreditCard, Gauge, Network, Users, Wallet } from "lucide-react"; import { headers } from "next/headers"; @@ -77,6 +84,7 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea const growthKit = referralAgent ? buildAgentGrowthKit({ agentId: referralAgent, campaign, source }) : null; + const walletPaymentAvailable = Boolean(TREASURY_USDC_ADDRESS); if (referralAgent) { const requestHeaders = await headers(); @@ -303,14 +311,20 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea 付款方式
diff --git a/apps/web/src/app/propose/success/page.tsx b/apps/web/src/app/propose/success/page.tsx index b3cf1c4..853ffde 100644 --- a/apps/web/src/app/propose/success/page.tsx +++ b/apps/web/src/app/propose/success/page.tsx @@ -1,12 +1,16 @@ import { prisma } from "@/lib/prisma"; import { submitWalletReceipt } from "@/app/propose/wallet/actions"; import { + buildUsdcPaymentUri, getProposalPackage, TREASURY_USDC_ADDRESS, + TREASURY_USDC_CHAIN_ID, TREASURY_USDC_NETWORK, + TREASURY_USDC_TOKEN_ADDRESS, TREASURY_WALLET_LABEL, + usdcAtomicUnitsFromCents, } from "@/lib/a2a-growth"; -import { CheckCircle2, Copy, Wallet } from "lucide-react"; +import { CheckCircle2, Copy, ExternalLink, Wallet } from "lucide-react"; import Link from "next/link"; export const dynamic = "force-dynamic"; @@ -28,6 +32,7 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara const payment = getParam(params, "payment") || "stripe"; const receiptStatus = getParam(params, "receipt"); const proposalPackage = getProposalPackage(getParam(params, "package")); + const walletPaymentUri = buildUsdcPaymentUri(proposalPackage.feeCents); const task = taskId ? await prisma.task.findUnique({ where: { id: taskId }, @@ -145,7 +150,7 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
Network
-
{TREASURY_USDC_NETWORK || "Confirm network with VibeWork before transfer"}
+
{TREASURY_USDC_NETWORK}
Wallet
@@ -153,6 +158,20 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara {TREASURY_USDC_ADDRESS || `${TREASURY_WALLET_LABEL} is not configured yet`}
+ {TREASURY_USDC_ADDRESS ? ( +
+
Chain / token
+
+ Chain ID {TREASURY_USDC_CHAIN_ID} · {TREASURY_USDC_TOKEN_ADDRESS} +
+
+ ) : null} + {TREASURY_USDC_ADDRESS ? ( +
+
USDC atomic amount
+
{usdcAtomicUnitsFromCents(proposalPackage.feeCents)}
+
+ ) : null} {walletCaptured ? (
Receipt
@@ -166,6 +185,15 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara 轉帳備註請包含 Proposal ID;入帳確認前 referral ledger 不會進入可出金狀態。
) : null} + {walletPaymentUri && !walletCaptured ? ( + + + 開啟錢包付款 + + ) : null} {!walletCaptured ? (
@@ -213,7 +241,7 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara Network diff --git a/apps/web/src/lib/a2a-growth.ts b/apps/web/src/lib/a2a-growth.ts index c9ec748..dc83930 100644 --- a/apps/web/src/lib/a2a-growth.ts +++ b/apps/web/src/lib/a2a-growth.ts @@ -13,9 +13,31 @@ export const AGENT_GATEWAY_URL = ( "https://agent.wooo.work" ).replace(/\/$/, ""); +export const DEFAULT_TREASURY_USDC_NETWORK = "Base USDC (native)"; +export const DEFAULT_TREASURY_USDC_CHAIN_ID = "8453"; +export const DEFAULT_TREASURY_USDC_TOKEN_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; + export const TREASURY_USDC_ADDRESS = (process.env.VIBEWORK_TREASURY_USDC_ADDRESS || "").trim(); export const TREASURY_WALLET_LABEL = (process.env.VIBEWORK_TREASURY_WALLET_LABEL || "USDC Treasury").trim(); -export const TREASURY_USDC_NETWORK = (process.env.VIBEWORK_TREASURY_USDC_NETWORK || "").trim(); +export const TREASURY_USDC_NETWORK = (process.env.VIBEWORK_TREASURY_USDC_NETWORK || DEFAULT_TREASURY_USDC_NETWORK).trim(); +export const TREASURY_USDC_CHAIN_ID = (process.env.VIBEWORK_TREASURY_USDC_CHAIN_ID || DEFAULT_TREASURY_USDC_CHAIN_ID).trim(); +export const TREASURY_USDC_TOKEN_ADDRESS = ( + process.env.VIBEWORK_TREASURY_USDC_TOKEN_ADDRESS || DEFAULT_TREASURY_USDC_TOKEN_ADDRESS +).trim(); +export const TREASURY_USDC_RPC_URL = (process.env.VIBEWORK_TREASURY_USDC_RPC_URL || "https://mainnet.base.org").trim(); + +export function usdcAtomicUnitsFromCents(amountCents: number) { + return (BigInt(Math.max(0, Math.floor(amountCents))) * BigInt(10_000)).toString(); +} + +export function buildUsdcPaymentUri(amountCents: number) { + if (!TREASURY_USDC_ADDRESS || !TREASURY_USDC_TOKEN_ADDRESS || !TREASURY_USDC_CHAIN_ID) return ""; + const params = new URLSearchParams({ + address: TREASURY_USDC_ADDRESS, + uint256: usdcAtomicUnitsFromCents(amountCents), + }); + return `ethereum:${TREASURY_USDC_TOKEN_ADDRESS}@${TREASURY_USDC_CHAIN_ID}/transfer?${params.toString()}`; +} export const PROPOSAL_PACKAGES = [ {