From a2aef61d7ddfe807fd089e4ca56d67ec34cf635b Mon Sep 17 00:00:00 2001 From: Tuy Lv Date: Wed, 27 May 2026 10:15:35 +0700 Subject: [PATCH] Update README and code for Supabase project reference and payment verification improvements --- README.md | 2 +- agent.mts | 29 +++++++++++++++++--- lib/x402.ts | 69 ++++++++++++++++++++++++++++++++++++++++++----- package-lock.json | 2 +- 4 files changed, 91 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b8b8cb9..b526eb2 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Circle Gateway batches many signed offchain authorizations into a single onchain Requires a [Supabase](https://supabase.com/) account and project. ```bash - npx supabase link --project-ref + npx supabase link --project-ref tjjptretakkntnqyooal npx supabase db push ``` diff --git a/agent.mts b/agent.mts index 73cb653..29c68a4 100644 --- a/agent.mts +++ b/agent.mts @@ -35,6 +35,27 @@ let { spendingLimit } = parseArgs(); let totalSpent = 0; let paused = false; +function formatPaymentError(err: unknown) { + if (err instanceof Error) { + const cause = err.cause as + | { shortMessage?: string; details?: string } + | undefined; + const parts = [err.message]; + + if (cause?.shortMessage && !parts.includes(cause.shortMessage)) { + parts.push(cause.shortMessage); + } + + if (cause?.details && !parts.includes(cause.details)) { + parts.push(cause.details); + } + + return parts.join(" | "); + } + + return String(err); +} + if (spendingLimit !== null) { console.log(`Spending limit: ${spendingLimit} USDC`); } @@ -260,7 +281,8 @@ function startPaymentLoop() { if (paused) return; const ep = endpoints[index % endpoints.length]; - index++; + const requestId = ++index; + const endpointName = ep.url.split("/").pop(); inFlight++; const start = Date.now(); @@ -276,7 +298,7 @@ function startPaymentLoop() { ? ` [spent: ${totalSpent.toFixed(6)}/${spendingLimit.toFixed(6)} USDC]` : ""; console.log( - `#${index} ${ep.method} ${ep.url.split("/").pop()} -> ${result.formattedAmount} USDC (${ms}ms) [in-flight: ${inFlight}]${limitInfo}`, + `#${requestId} ${ep.method} ${endpointName} -> ${result.formattedAmount} USDC (${ms}ms) [in-flight: ${inFlight}]${limitInfo}`, ); if (spendingLimit !== null && totalSpent >= spendingLimit) { @@ -286,8 +308,9 @@ function startPaymentLoop() { .catch((err) => { inFlight--; const ms = Date.now() - start; + const errorDetails = formatPaymentError(err); console.error( - `#${index} ${ep.url.split("/").pop()} FAILED (${ms}ms): ${err.message} [in-flight: ${inFlight}]`, + `#${requestId} ${ep.method} ${endpointName} FAILED (${ms}ms): ${errorDetails} [in-flight: ${inFlight}]`, ); }); }, 1000); diff --git a/lib/x402.ts b/lib/x402.ts index d1c1473..8b9b025 100644 --- a/lib/x402.ts +++ b/lib/x402.ts @@ -42,8 +42,45 @@ interface PaymentPayload { extensions?: Record; } -function buildPaymentRequirements(price: string) { - // Parse dollar amount to USDC atomic units (6 decimals) +interface SupportedKind { + scheme: string; + network: string; + extra?: { + verifyingContract?: string; + }; +} + +let cachedArcVerifyingContract: string | null = null; + +async function getArcVerifyingContract() { + if (cachedArcVerifyingContract) { + return cachedArcVerifyingContract; + } + + try { + const supported = await facilitator.getSupported(); + const arcKind = (supported.kinds as SupportedKind[]).find( + (kind) => + kind.scheme === "exact" && + kind.network === ARC_TESTNET_NETWORK && + typeof kind.extra?.verifyingContract === "string", + ); + + if (arcKind?.extra?.verifyingContract) { + cachedArcVerifyingContract = arcKind.extra.verifyingContract; + return cachedArcVerifyingContract; + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error("[x402] Failed to fetch supported kinds:", message); + } + + // Fallback keeps the route operational if supported lookup is transiently unavailable. + return ARC_TESTNET_GATEWAY_WALLET; +} + +async function buildPaymentRequirements(price: string) { + const verifyingContract = await getArcVerifyingContract(); const amount = Math.round(parseFloat(price.replace("$", "")) * 1_000_000); return { @@ -52,11 +89,13 @@ function buildPaymentRequirements(price: string) { asset: ARC_TESTNET_USDC, amount: amount.toString(), payTo: sellerAddress, - maxTimeoutSeconds: 345600, + // Gateway currently rejects short authorization windows. + // Use a longer validity period to avoid authorization_validity_too_short. + maxTimeoutSeconds: 31536000, extra: { name: "GatewayWalletBatched", version: "1", - verifyingContract: ARC_TESTNET_GATEWAY_WALLET, + verifyingContract, }, }; } @@ -72,9 +111,8 @@ export function withGateway( price: string, endpoint: string, ) { - const requirements = buildPaymentRequirements(price); - return async (req: NextRequest) => { + const requirements = await buildPaymentRequirements(price); const paymentSignature = req.headers.get("payment-signature"); // No payment — return 402 with Gateway batching payment requirements @@ -108,12 +146,31 @@ export function withGateway( Buffer.from(paymentSignature, "base64").toString("utf-8"), ); + const acceptedNetwork = + typeof paymentPayload.accepted?.network === "string" + ? paymentPayload.accepted.network + : null; + + if (acceptedNetwork && acceptedNetwork !== requirements.network) { + return NextResponse.json( + { + error: "Unsupported payment network", + expected: requirements.network, + received: acceptedNetwork, + }, + { status: 400 }, + ); + } + const verifyResult = await facilitator.verify( paymentPayload, requirements, ); if (!verifyResult.isValid) { + console.error( + `[x402] Verification failed for ${endpoint}: ${verifyResult.invalidReason ?? "unknown"}`, + ); return NextResponse.json( { error: "Payment verification failed", diff --git a/package-lock.json b/package-lock.json index befdfca..5b6c69d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "arc-nanopayments-demo", + "name": "arc-nanopayments", "lockfileVersion": 3, "requires": true, "packages": {