feat: prioritize wallet payments and live balance
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s

This commit is contained in:
OG T
2026-06-11 22:17:14 +08:00
parent eea12f633f
commit 3ba60c41e0
6 changed files with 188 additions and 11 deletions

View File

@@ -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<TreasuryStats | null>(null);
const [liveUsdcBalance, setLiveUsdcBalance] = useState<LiveUsdcBalance | null>(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() {
<span>Lifetime Revenue: ${(cents(stats.revenue?.usdc) / 100).toLocaleString()}</span>
<span>Total GMV: ${(cents(stats.gmv?.usdc) / 100).toLocaleString()}</span>
</div>
<div className="mt-4 rounded-xl border border-emerald-500/20 bg-black/30 p-3 text-xs leading-5 text-emerald-100">
{liveUsdcBalance ? (
<>
<div className="flex items-center justify-between gap-3">
<span className="text-emerald-100/70">Live wallet balance</span>
<span className="font-mono text-emerald-200">{Number(liveUsdcBalance.balance_usdc).toFixed(2)} USDC</span>
</div>
<div className="mt-2 break-all text-emerald-100/60">
{liveUsdcBalance.network} · chain {liveUsdcBalance.chain_id}
</div>
<div className="mt-1 break-all text-emerald-100/60">{liveUsdcBalance.wallet}</div>
</>
) : (
<span className="text-amber-200">{liveUsdcBalanceError || "Live wallet balance is loading."}</span>
)}
</div>
</div>
{/* Fiat Treasury */}

View File

@@ -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 });
}
}

View File

@@ -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",
},

View File

@@ -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
<legend className="mb-3 text-sm font-semibold text-zinc-100"></legend>
<div className="grid gap-3 md:grid-cols-2">
<label className="flex cursor-pointer items-center gap-3 rounded-md border border-zinc-700 bg-zinc-950 p-4 hover:border-emerald-400">
<input type="radio" name="paymentMethod" value="stripe" defaultChecked className="h-4 w-4 accent-emerald-400" />
<CreditCard className="h-5 w-5 text-emerald-300" />
<span className="text-sm font-medium text-white"></span>
<input type="radio" name="paymentMethod" value="wallet" defaultChecked={walletPaymentAvailable} className="h-4 w-4 accent-emerald-400" />
<Wallet className="h-5 w-5 text-emerald-300" />
<span>
<span className="block text-sm font-medium text-white">USDC Treasury</span>
<span className="mt-1 block text-xs text-zinc-400">{TREASURY_USDC_NETWORK}</span>
</span>
</label>
<label className="flex cursor-pointer items-center gap-3 rounded-md border border-zinc-700 bg-zinc-950 p-4 hover:border-emerald-400">
<input type="radio" name="paymentMethod" value="wallet" className="h-4 w-4 accent-emerald-400" />
<Wallet className="h-5 w-5 text-emerald-300" />
<span className="text-sm font-medium text-white">USDC </span>
<input type="radio" name="paymentMethod" value="stripe" defaultChecked={!walletPaymentAvailable} className="h-4 w-4 accent-emerald-400" />
<CreditCard className="h-5 w-5 text-emerald-300" />
<span>
<span className="block text-sm font-medium text-white"></span>
<span className="mt-1 block text-xs text-zinc-400">Stripe </span>
</span>
</label>
</div>
</fieldset>

View File

@@ -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
</div>
<div>
<dt className="text-emerald-100/70">Network</dt>
<dd className="mt-1 text-emerald-50">{TREASURY_USDC_NETWORK || "Confirm network with VibeWork before transfer"}</dd>
<dd className="mt-1 text-emerald-50">{TREASURY_USDC_NETWORK}</dd>
</div>
<div>
<dt className="text-emerald-100/70">Wallet</dt>
@@ -153,6 +158,20 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
{TREASURY_USDC_ADDRESS || `${TREASURY_WALLET_LABEL} is not configured yet`}
</dd>
</div>
{TREASURY_USDC_ADDRESS ? (
<div>
<dt className="text-emerald-100/70">Chain / token</dt>
<dd className="mt-1 break-all text-emerald-50">
Chain ID {TREASURY_USDC_CHAIN_ID} · {TREASURY_USDC_TOKEN_ADDRESS}
</dd>
</div>
) : null}
{TREASURY_USDC_ADDRESS ? (
<div>
<dt className="text-emerald-100/70">USDC atomic amount</dt>
<dd className="mt-1 break-all text-emerald-50">{usdcAtomicUnitsFromCents(proposalPackage.feeCents)}</dd>
</div>
) : null}
{walletCaptured ? (
<div>
<dt className="text-emerald-100/70">Receipt</dt>
@@ -166,6 +185,15 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
Proposal ID referral ledger
</div>
) : null}
{walletPaymentUri && !walletCaptured ? (
<a
href={walletPaymentUri}
className="mt-3 inline-flex h-11 items-center justify-center gap-2 rounded-md bg-emerald-300 px-4 text-sm font-semibold text-zinc-950 hover:bg-emerald-200"
>
<ExternalLink className="h-4 w-4" />
</a>
) : null}
{!walletCaptured ? (
<div className="mt-4 rounded-md border border-zinc-700 bg-zinc-950/80 p-4">
@@ -213,7 +241,7 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
Network
<input
name="network"
defaultValue={TREASURY_USDC_NETWORK || ""}
defaultValue={TREASURY_USDC_NETWORK}
className="h-10 rounded-md border border-zinc-700 bg-black px-3 text-sm text-white outline-none focus:border-emerald-400"
placeholder="Base / Ethereum / Polygon"
/>

View File

@@ -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 = [
{