From 31801614c5e9f823647dcec860c687561e847e64 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Thu, 25 Sep 2025 14:44:06 +1200 Subject: [PATCH] [SDK] Support Chain objects in x402 payment utilities --- .changeset/polite-banks-peel.md | 5 + .../src/app/payments/x402/page.tsx | 3 +- apps/playground-web/src/middleware.ts | 2 +- apps/portal/src/app/payments/x402/page.mdx | 3 +- packages/thirdweb/src/x402/common.ts | 98 +++++++++---------- packages/thirdweb/src/x402/facilitator.ts | 1 + .../thirdweb/src/x402/fetchWithPayment.ts | 4 +- packages/thirdweb/src/x402/schemas.ts | 6 +- packages/thirdweb/src/x402/settle-payment.ts | 7 +- packages/thirdweb/src/x402/types.ts | 3 +- packages/thirdweb/src/x402/verify-payment.ts | 3 +- 11 files changed, 73 insertions(+), 62 deletions(-) create mode 100644 .changeset/polite-banks-peel.md diff --git a/.changeset/polite-banks-peel.md b/.changeset/polite-banks-peel.md new file mode 100644 index 00000000000..93a9d3d51e3 --- /dev/null +++ b/.changeset/polite-banks-peel.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Accept chain objects for x402 APIs diff --git a/apps/playground-web/src/app/payments/x402/page.tsx b/apps/playground-web/src/app/payments/x402/page.tsx index 51e38809c34..3481ccacbfb 100644 --- a/apps/playground-web/src/app/payments/x402/page.tsx +++ b/apps/playground-web/src/app/payments/x402/page.tsx @@ -59,6 +59,7 @@ function ServerCodeExample() { import { facilitator, settlePayment } from "thirdweb/x402"; import { createThirdwebClient } from "thirdweb"; +import { arbitrumSepolia } from "thirdweb/chains"; const client = createThirdwebClient({ secretKey: "your-secret-key" }); const thirdwebX402Facilitator = facilitator({ @@ -76,7 +77,7 @@ export async function middleware(request: NextRequest) { method, paymentData, payTo: "0xYourWalletAddress", - network: "eip155:11155111", // or any other chain id + network: arbitrumSepolia, // or any other chain price: "$0.01", // can also be a ERC20 token amount facilitator: thirdwebX402Facilitator, }); diff --git a/apps/playground-web/src/middleware.ts b/apps/playground-web/src/middleware.ts index eb74b1046ca..1b556d07ce0 100644 --- a/apps/playground-web/src/middleware.ts +++ b/apps/playground-web/src/middleware.ts @@ -31,7 +31,7 @@ export async function middleware(request: NextRequest) { method, paymentData, payTo: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024", - network: `eip155:${chain.id}`, + network: chain, price: "$0.01", routeConfig: { description: "Access to paid content", diff --git a/apps/portal/src/app/payments/x402/page.mdx b/apps/portal/src/app/payments/x402/page.mdx index f3b008d029f..4b60ca9dd11 100644 --- a/apps/portal/src/app/payments/x402/page.mdx +++ b/apps/portal/src/app/payments/x402/page.mdx @@ -50,6 +50,7 @@ Here's an example with a Next.js middleware: ```typescript import { createThirdwebClient } from "thirdweb"; import { facilitator, settlePayment } from "thirdweb/x402"; +import { arbitrumSepolia } from "thirdweb/chains"; const client = createThirdwebClient({ secretKey: "your-secret-key" }); const thirdwebX402Facilitator = facilitator({ @@ -67,7 +68,7 @@ export async function middleware(request: NextRequest) { method, paymentData, payTo: "0xYourWalletAddress", - network: "eip155:1", // or any other chain id in CAIP2 format: "eip155:" + network: arbitrumSepolia, // or any other chain price: "$0.01", // can also be a ERC20 token amount routeConfig: { description: "Access to paid content", diff --git a/packages/thirdweb/src/x402/common.ts b/packages/thirdweb/src/x402/common.ts index 4057770d711..a661dee2f7b 100644 --- a/packages/thirdweb/src/x402/common.ts +++ b/packages/thirdweb/src/x402/common.ts @@ -1,15 +1,8 @@ -import { - type ERC20TokenAmount, - type Money, - moneySchema, - type Network, - SupportedEVMNetworks, -} from "x402/types"; +import { type ERC20TokenAmount, type Money, moneySchema } from "x402/types"; import { getAddress } from "../utils/address.js"; import { decodePayment } from "./encode.js"; import type { facilitator as facilitatorType } from "./facilitator.js"; import { - type FacilitatorNetwork, networkToChainId, type RequestedPaymentPayload, type RequestedPaymentRequirements, @@ -54,9 +47,28 @@ export async function decodePaymentRequest( errorMessages, discoverable, } = routeConfig; + + let chainId: number; + try { + chainId = networkToChainId(network); + } catch (error) { + return { + status: 402, + responseHeaders: { "Content-Type": "application/json" }, + responseBody: { + x402Version, + error: + error instanceof Error + ? error.message + : `Invalid network: ${network}`, + accepts: [], + }, + }; + } + const atomicAmountForAsset = await processPriceToAtomicAmount( price, - network, + chainId, facilitator, ); if ("error" in atomicAmountForAsset) { @@ -74,45 +86,28 @@ export async function decodePaymentRequest( const paymentRequirements: RequestedPaymentRequirements[] = []; - if ( - SupportedEVMNetworks.includes(network as Network) || - network.startsWith("eip155:") - ) { - paymentRequirements.push({ - scheme: "exact", - network, - maxAmountRequired, - resource: resourceUrl, - description: description ?? "", - mimeType: mimeType ?? "application/json", - payTo: getAddress(payTo), - maxTimeoutSeconds: maxTimeoutSeconds ?? 300, - asset: getAddress(asset.address), - // TODO: Rename outputSchema to requestStructure - outputSchema: { - input: { - type: "http", - method, - discoverable: discoverable ?? true, - ...inputSchema, - }, - output: outputSchema, - }, - extra: (asset as ERC20TokenAmount["asset"]).eip712, - }); - } else { - return { - status: 402, - responseHeaders: { - "Content-Type": "application/json", - }, - responseBody: { - x402Version, - error: `Unsupported network: ${network}`, - accepts: paymentRequirements, + paymentRequirements.push({ + scheme: "exact", + network: `eip155:${chainId}`, + maxAmountRequired, + resource: resourceUrl, + description: description ?? "", + mimeType: mimeType ?? "application/json", + payTo: getAddress(payTo), + maxTimeoutSeconds: maxTimeoutSeconds ?? 300, + asset: getAddress(asset.address), + // TODO: Rename outputSchema to requestStructure + outputSchema: { + input: { + type: "http", + method, + discoverable: discoverable ?? true, + ...inputSchema, }, - }; - } + output: outputSchema, + }, + extra: (asset as ERC20TokenAmount["asset"]).eip712, + }); // Check for payment header if (!paymentData) { @@ -188,7 +183,7 @@ export async function decodePaymentRequest( */ async function processPriceToAtomicAmount( price: Money | ERC20TokenAmount, - network: FacilitatorNetwork, + chainId: number, facilitator: ReturnType, ): Promise< | { maxAmountRequired: string; asset: ERC20TokenAmount["asset"] } @@ -207,10 +202,10 @@ async function processPriceToAtomicAmount( }; } const parsedUsdAmount = parsedAmount.data; - const defaultAsset = await getDefaultAsset(network, facilitator); + const defaultAsset = await getDefaultAsset(chainId, facilitator); if (!defaultAsset) { return { - error: `Unable to get default asset on ${network}. Please specify an asset in the payment requirements.`, + error: `Unable to get default asset on chain ${chainId}. Please specify an asset in the payment requirements.`, }; } asset = defaultAsset; @@ -228,11 +223,10 @@ async function processPriceToAtomicAmount( } async function getDefaultAsset( - network: FacilitatorNetwork, + chainId: number, facilitator: ReturnType, ): Promise { const supportedAssets = await facilitator.supported(); - const chainId = networkToChainId(network); const matchingAsset = supportedAssets.kinds.find( (supported) => supported.network === `eip155:${chainId}`, ); diff --git a/packages/thirdweb/src/x402/facilitator.ts b/packages/thirdweb/src/x402/facilitator.ts index 9588ef00b56..cdb3c35c53f 100644 --- a/packages/thirdweb/src/x402/facilitator.ts +++ b/packages/thirdweb/src/x402/facilitator.ts @@ -28,6 +28,7 @@ const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402"; * ```ts * import { facilitator } from "thirdweb/x402"; * import { createThirdwebClient } from "thirdweb"; + * import { paymentMiddleware } from 'x402-hono' * * const client = createThirdwebClient({ * secretKey: "your-secret-key", diff --git a/packages/thirdweb/src/x402/fetchWithPayment.ts b/packages/thirdweb/src/x402/fetchWithPayment.ts index d978e837e63..0ecd77843e0 100644 --- a/packages/thirdweb/src/x402/fetchWithPayment.ts +++ b/packages/thirdweb/src/x402/fetchWithPayment.ts @@ -84,7 +84,9 @@ export function wrapFetchWithPayment( ); if (BigInt(selectedPaymentRequirements.maxAmountRequired) > maxValue) { - throw new Error("Payment amount exceeds maximum allowed"); + throw new Error( + `Payment amount exceeds maximum allowed (currently set to ${maxValue} in base units)`, + ); } const paymentChainId = networkToChainId( diff --git a/packages/thirdweb/src/x402/schemas.ts b/packages/thirdweb/src/x402/schemas.ts index f4fd5a0c66c..065e7a9b618 100644 --- a/packages/thirdweb/src/x402/schemas.ts +++ b/packages/thirdweb/src/x402/schemas.ts @@ -7,6 +7,7 @@ import { SettleResponseSchema, } from "x402/types"; import { z } from "zod"; +import type { Chain } from "../chains/types.js"; const FacilitatorNetworkSchema = z.union([ z.literal("base-sepolia"), @@ -55,7 +56,10 @@ export type FacilitatorSettleResponse = z.infer< typeof FacilitatorSettleResponseSchema >; -export function networkToChainId(network: string): number { +export function networkToChainId(network: string | Chain): number { + if (typeof network === "object") { + return network.id; + } if (network.startsWith("eip155:")) { const chainId = parseInt(network.split(":")[1] ?? "0"); if (!Number.isNaN(chainId) && chainId > 0) { diff --git a/packages/thirdweb/src/x402/settle-payment.ts b/packages/thirdweb/src/x402/settle-payment.ts index 5ea9286e778..73f88f9b420 100644 --- a/packages/thirdweb/src/x402/settle-payment.ts +++ b/packages/thirdweb/src/x402/settle-payment.ts @@ -25,6 +25,7 @@ import { * // Usage in a Next.js API route * import { settlePayment, facilitator } from "thirdweb/x402"; * import { createThirdwebClient } from "thirdweb"; + * import { arbitrumSepolia } from "thirdweb/chains"; * * const client = createThirdwebClient({ * secretKey: process.env.THIRDWEB_SECRET_KEY, @@ -44,7 +45,7 @@ import { * method: "GET", * paymentData, * payTo: "0x1234567890123456789012345678901234567890", - * network: "eip155:84532", // CAIP2 format: "eip155:" + * network: arbitrumSepolia, // or any other chain * price: "$0.10", // or { amount: "100000", asset: { address: "0x...", decimals: 6 } } * facilitator: thirdwebFacilitator, * routeConfig: { @@ -74,6 +75,7 @@ import { * import express from "express"; * import { settlePayment, facilitator } from "thirdweb/x402"; * import { createThirdwebClient } from "thirdweb"; + * import { arbitrumSepolia } from "thirdweb/chains"; * * const client = createThirdwebClient({ * secretKey: process.env.THIRDWEB_SECRET_KEY, @@ -93,7 +95,7 @@ import { * method: req.method, * paymentData: req.headers["x-payment"], * payTo: "0x1234567890123456789012345678901234567890", - * network: "eip155:8453", // CAIP2 format: "eip155:" + * network: arbitrumSepolia, // or any other chain * price: "$0.05", * facilitator: thirdwebFacilitator, * }); @@ -136,7 +138,6 @@ export async function settlePayment( const { selectedPaymentRequirements, decodedPayment, paymentRequirements } = decodePaymentResult; - // Settle payment try { const settlement = await facilitator.settle( decodedPayment, diff --git a/packages/thirdweb/src/x402/types.ts b/packages/thirdweb/src/x402/types.ts index db28b38bce6..ad29f6c964c 100644 --- a/packages/thirdweb/src/x402/types.ts +++ b/packages/thirdweb/src/x402/types.ts @@ -3,6 +3,7 @@ import type { Money, PaymentMiddlewareConfig, } from "x402/types"; +import type { Chain } from "../chains/types.js"; import type { Address } from "../utils/address.js"; import type { Prettify } from "../utils/type-utils.js"; import type { facilitator as facilitatorType } from "./facilitator.js"; @@ -30,7 +31,7 @@ export type PaymentArgs = { /** The wallet address that should receive the payment */ payTo: Address; /** The blockchain network where the payment should be processed */ - network: FacilitatorNetwork; + network: FacilitatorNetwork | Chain; /** The price for accessing the resource - either a USD amount (e.g., "$0.10") or a specific token amount */ price: Money | ERC20TokenAmount; /** The payment facilitator instance used to verify and settle payments */ diff --git a/packages/thirdweb/src/x402/verify-payment.ts b/packages/thirdweb/src/x402/verify-payment.ts index 067c1ce6c4c..09927775994 100644 --- a/packages/thirdweb/src/x402/verify-payment.ts +++ b/packages/thirdweb/src/x402/verify-payment.ts @@ -17,6 +17,7 @@ import { * // Usage in a Next.js API route * import { verifyPayment, facilitator } from "thirdweb/x402"; * import { createThirdwebClient } from "thirdweb"; + * import { arbitrumSepolia } from "thirdweb/chains"; * * const client = createThirdwebClient({ * secretKey: process.env.THIRDWEB_SECRET_KEY, @@ -35,7 +36,7 @@ import { * method: "GET", * paymentData, * payTo: "0x1234567890123456789012345678901234567890", - * network: "eip155:84532", // CAIP2 format: "eip155:" + * network: arbitrumSepolia, // or any other chain * price: "$0.10", // or { amount: "100000", asset: { address: "0x...", decimals: 6 } } * facilitator: thirdwebFacilitator, * routeConfig: {