From 5bc4c74671cfdcc91bee1b3174dbca3e4b547b83 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Fri, 26 Sep 2025 00:21:15 +1200 Subject: [PATCH 1/5] [SDK] Support ERC-2612 permit for x402 payments --- .changeset/giant-suns-drive.md | 5 + .../scripts/generate/abis/erc20/USDC.json | 3 + .../generate/abis/erc7702/MinimalAccount.json | 5 - .../USDC/write/transferWithAuthorization.ts | 215 ++++++++++++++++++ packages/thirdweb/src/x402/common.ts | 46 +++- packages/thirdweb/src/x402/facilitator.ts | 1 + .../thirdweb/src/x402/fetchWithPayment.ts | 4 +- packages/thirdweb/src/x402/sign.ts | 190 +++++++++++----- 8 files changed, 403 insertions(+), 66 deletions(-) create mode 100644 .changeset/giant-suns-drive.md create mode 100644 packages/thirdweb/scripts/generate/abis/erc20/USDC.json create mode 100644 packages/thirdweb/src/extensions/erc20/__generated__/USDC/write/transferWithAuthorization.ts diff --git a/.changeset/giant-suns-drive.md b/.changeset/giant-suns-drive.md new file mode 100644 index 00000000000..72b21a306f9 --- /dev/null +++ b/.changeset/giant-suns-drive.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Support ERC-2612 permit for x402 payments diff --git a/packages/thirdweb/scripts/generate/abis/erc20/USDC.json b/packages/thirdweb/scripts/generate/abis/erc20/USDC.json new file mode 100644 index 00000000000..7d5f7a796d0 --- /dev/null +++ b/packages/thirdweb/scripts/generate/abis/erc20/USDC.json @@ -0,0 +1,3 @@ +[ + "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes signature)" +] \ No newline at end of file diff --git a/packages/thirdweb/scripts/generate/abis/erc7702/MinimalAccount.json b/packages/thirdweb/scripts/generate/abis/erc7702/MinimalAccount.json index 7065f761222..f4a3cf1548e 100644 --- a/packages/thirdweb/scripts/generate/abis/erc7702/MinimalAccount.json +++ b/packages/thirdweb/scripts/generate/abis/erc7702/MinimalAccount.json @@ -24,9 +24,4 @@ "function getSessionStateForSigner(address signer) view returns (((uint256 remaining, address target, bytes4 selector, uint256 index)[] transferValue, (uint256 remaining, address target, bytes4 selector, uint256 index)[] callValue, (uint256 remaining, address target, bytes4 selector, uint256 index)[] callParams))", "function getTransferPoliciesForSigner(address signer) view returns ((address target, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit)[])", "function isWildcardSigner(address signer) view returns (bool)", - "function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) returns (bytes4)", - "function onERC1155Received(address, address, uint256, uint256, bytes) returns (bytes4)", - "function onERC721Received(address, address, uint256, bytes) returns (bytes4)", - "function supportsInterface(bytes4 interfaceId) view returns (bool)", - "receive() external payable" ] \ No newline at end of file diff --git a/packages/thirdweb/src/extensions/erc20/__generated__/USDC/write/transferWithAuthorization.ts b/packages/thirdweb/src/extensions/erc20/__generated__/USDC/write/transferWithAuthorization.ts new file mode 100644 index 00000000000..84c8180025c --- /dev/null +++ b/packages/thirdweb/src/extensions/erc20/__generated__/USDC/write/transferWithAuthorization.ts @@ -0,0 +1,215 @@ +import type { AbiParameterToPrimitiveType } from "abitype"; +import { prepareContractCall } from "../../../../../transaction/prepare-contract-call.js"; +import type { + BaseTransactionOptions, + WithOverrides, +} from "../../../../../transaction/types.js"; +import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js"; +import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js"; +import { once } from "../../../../../utils/promise/once.js"; + +/** + * Represents the parameters for the "transferWithAuthorization" function. + */ +export type TransferWithAuthorizationParams = WithOverrides<{ + from: AbiParameterToPrimitiveType<{ type: "address"; name: "from" }>; + to: AbiParameterToPrimitiveType<{ type: "address"; name: "to" }>; + value: AbiParameterToPrimitiveType<{ type: "uint256"; name: "value" }>; + validAfter: AbiParameterToPrimitiveType<{ + type: "uint256"; + name: "validAfter"; + }>; + validBefore: AbiParameterToPrimitiveType<{ + type: "uint256"; + name: "validBefore"; + }>; + nonce: AbiParameterToPrimitiveType<{ type: "bytes32"; name: "nonce" }>; + signature: AbiParameterToPrimitiveType<{ type: "bytes"; name: "signature" }>; +}>; + +export const FN_SELECTOR = "0xcf092995" as const; +const FN_INPUTS = [ + { + type: "address", + name: "from", + }, + { + type: "address", + name: "to", + }, + { + type: "uint256", + name: "value", + }, + { + type: "uint256", + name: "validAfter", + }, + { + type: "uint256", + name: "validBefore", + }, + { + type: "bytes32", + name: "nonce", + }, + { + type: "bytes", + name: "signature", + }, +] as const; +const FN_OUTPUTS = [] as const; + +/** + * Checks if the `transferWithAuthorization` method is supported by the given contract. + * @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors. + * @returns A boolean indicating if the `transferWithAuthorization` method is supported. + * @extension ERC20 + * @example + * ```ts + * import { isTransferWithAuthorizationSupported } from "thirdweb/extensions/erc20"; + * + * const supported = isTransferWithAuthorizationSupported(["0x..."]); + * ``` + */ +export function isTransferWithAuthorizationSupported( + availableSelectors: string[], +) { + return detectMethod({ + availableSelectors, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + }); +} + +/** + * Encodes the parameters for the "transferWithAuthorization" function. + * @param options - The options for the transferWithAuthorization function. + * @returns The encoded ABI parameters. + * @extension ERC20 + * @example + * ```ts + * import { encodeTransferWithAuthorizationParams } from "thirdweb/extensions/erc20"; + * const result = encodeTransferWithAuthorizationParams({ + * from: ..., + * to: ..., + * value: ..., + * validAfter: ..., + * validBefore: ..., + * nonce: ..., + * signature: ..., + * }); + * ``` + */ +export function encodeTransferWithAuthorizationParams( + options: TransferWithAuthorizationParams, +) { + return encodeAbiParameters(FN_INPUTS, [ + options.from, + options.to, + options.value, + options.validAfter, + options.validBefore, + options.nonce, + options.signature, + ]); +} + +/** + * Encodes the "transferWithAuthorization" function into a Hex string with its parameters. + * @param options - The options for the transferWithAuthorization function. + * @returns The encoded hexadecimal string. + * @extension ERC20 + * @example + * ```ts + * import { encodeTransferWithAuthorization } from "thirdweb/extensions/erc20"; + * const result = encodeTransferWithAuthorization({ + * from: ..., + * to: ..., + * value: ..., + * validAfter: ..., + * validBefore: ..., + * nonce: ..., + * signature: ..., + * }); + * ``` + */ +export function encodeTransferWithAuthorization( + options: TransferWithAuthorizationParams, +) { + // we do a "manual" concat here to avoid the overhead of the "concatHex" function + // we can do this because we know the specific formats of the values + return (FN_SELECTOR + + encodeTransferWithAuthorizationParams(options).slice( + 2, + )) as `${typeof FN_SELECTOR}${string}`; +} + +/** + * Prepares a transaction to call the "transferWithAuthorization" function on the contract. + * @param options - The options for the "transferWithAuthorization" function. + * @returns A prepared transaction object. + * @extension ERC20 + * @example + * ```ts + * import { sendTransaction } from "thirdweb"; + * import { transferWithAuthorization } from "thirdweb/extensions/erc20"; + * + * const transaction = transferWithAuthorization({ + * contract, + * from: ..., + * to: ..., + * value: ..., + * validAfter: ..., + * validBefore: ..., + * nonce: ..., + * signature: ..., + * overrides: { + * ... + * } + * }); + * + * // Send the transaction + * await sendTransaction({ transaction, account }); + * ``` + */ +export function transferWithAuthorization( + options: BaseTransactionOptions< + | TransferWithAuthorizationParams + | { + asyncParams: () => Promise; + } + >, +) { + const asyncOptions = once(async () => { + return "asyncParams" in options ? await options.asyncParams() : options; + }); + + return prepareContractCall({ + contract: options.contract, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + params: async () => { + const resolvedOptions = await asyncOptions(); + return [ + resolvedOptions.from, + resolvedOptions.to, + resolvedOptions.value, + resolvedOptions.validAfter, + resolvedOptions.validBefore, + resolvedOptions.nonce, + resolvedOptions.signature, + ] as const; + }, + value: async () => (await asyncOptions()).overrides?.value, + accessList: async () => (await asyncOptions()).overrides?.accessList, + gas: async () => (await asyncOptions()).overrides?.gas, + gasPrice: async () => (await asyncOptions()).overrides?.gasPrice, + maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas, + maxPriorityFeePerGas: async () => + (await asyncOptions()).overrides?.maxPriorityFeePerGas, + nonce: async () => (await asyncOptions()).overrides?.nonce, + extraGas: async () => (await asyncOptions()).overrides?.extraGas, + erc20Value: async () => (await asyncOptions()).overrides?.erc20Value, + authorizationList: async () => + (await asyncOptions()).overrides?.authorizationList, + }); +} diff --git a/packages/thirdweb/src/x402/common.ts b/packages/thirdweb/src/x402/common.ts index a661dee2f7b..26e100c5d6e 100644 --- a/packages/thirdweb/src/x402/common.ts +++ b/packages/thirdweb/src/x402/common.ts @@ -12,6 +12,14 @@ import { type PaymentRequiredResult, x402Version, } from "./types.js"; +import { isTransferWithAuthorizationSupported } from "../extensions/erc20/__generated__/USDC/write/transferWithAuthorization.js"; +import { getContract } from "../contract/contract.js"; +import { resolveContractAbi } from "../contract/actions/resolve-abi.js"; +import type { Abi } from "abitype"; +import { getCachedChain } from "../chains/utils.js"; +import type { ThirdwebClient } from "../client/client.js"; +import { toFunctionSelector } from "viem/utils"; +import { isPermitSupported } from "../extensions/erc20/__generated__/IERC20Permit/write/permit.js"; type GetPaymentRequirementsResult = { status: 200; @@ -106,7 +114,10 @@ export async function decodePaymentRequest( }, output: outputSchema, }, - extra: (asset as ERC20TokenAmount["asset"]).eip712, + extra: { + facilitatorAddress: facilitator.address, + ...(asset as ERC20TokenAmount["asset"]).eip712, + }, }); // Check for payment header @@ -234,3 +245,36 @@ async function getDefaultAsset( ?.defaultAsset as ERC20TokenAmount["asset"]; return assetConfig; } + +export type SupportedAuthorizationMethods = { + hasPermit: boolean, + hasTransferWithAuthorization: boolean, +} + +export async function detectSupportedAuthorizationMethods(args: { + client: ThirdwebClient, + asset: string, + chainId: number, +}): Promise { + const abi = await resolveContractAbi( + getContract({ + client: args.client, + address: args.asset, + chain: getCachedChain(args.chainId), + }), + ).catch((error) => { + console.error("Error resolving contract ABI", error); + return [] as Abi; + }); + const selectors = abi + .filter((f) => f.type === "function") + .map((f) => toFunctionSelector(f)); + const hasPermit = isPermitSupported(selectors); + const hasTransferWithAuthorization = + isTransferWithAuthorizationSupported(selectors); + + return { + hasPermit, + hasTransferWithAuthorization, + }; +} diff --git a/packages/thirdweb/src/x402/facilitator.ts b/packages/thirdweb/src/x402/facilitator.ts index cdb3c35c53f..9287bfd951f 100644 --- a/packages/thirdweb/src/x402/facilitator.ts +++ b/packages/thirdweb/src/x402/facilitator.ts @@ -69,6 +69,7 @@ export function facilitator(config: ThirdwebX402FacilitatorConfig) { } const facilitator = { url: (config.baseUrl ?? DEFAULT_BASE_URL) as `${string}://${string}`, + address: serverWalletAddress, createAuthHeaders: async () => { return { verify: { diff --git a/packages/thirdweb/src/x402/fetchWithPayment.ts b/packages/thirdweb/src/x402/fetchWithPayment.ts index 0ecd77843e0..1fc340c5628 100644 --- a/packages/thirdweb/src/x402/fetchWithPayment.ts +++ b/packages/thirdweb/src/x402/fetchWithPayment.ts @@ -50,7 +50,7 @@ import { createPaymentHeader } from "./sign.js"; */ export function wrapFetchWithPayment( fetch: typeof globalThis.fetch, - _client: ThirdwebClient, + client: ThirdwebClient, wallet: Wallet, maxValue: bigint = BigInt(1 * 10 ** 6), // Default to 1 USDC ) { @@ -103,8 +103,8 @@ export function wrapFetchWithPayment( } const paymentHeader = await createPaymentHeader( + client, account, - x402Version, selectedPaymentRequirements, ); diff --git a/packages/thirdweb/src/x402/sign.ts b/packages/thirdweb/src/x402/sign.ts index dc414ca7f45..550201cd38e 100644 --- a/packages/thirdweb/src/x402/sign.ts +++ b/packages/thirdweb/src/x402/sign.ts @@ -1,4 +1,8 @@ +import { hexToBigInt } from "viem"; import type { ExactEvmPayloadAuthorization } from "x402/types"; +import { getCachedChain } from "../chains/utils.js"; +import type { ThirdwebClient } from "../client/client.js"; +import { getContract } from "../contract/contract.js"; import { type Address, getAddress } from "../utils/address.js"; import { type Hex, toHex } from "../utils/encoding/hex.js"; import type { Account } from "../wallets/interfaces/wallet.js"; @@ -9,6 +13,11 @@ import { type RequestedPaymentRequirements, type UnsignedPaymentPayload, } from "./schemas.js"; +import { + detectSupportedAuthorizationMethods, +} from "./common.js"; +import { nonces } from "../extensions/erc20/__generated__/IERC20Permit/read/nonces.js"; +import { x402Version } from "./types.js"; /** * Prepares an unsigned payment header with the given sender address and payment requirements. @@ -22,14 +31,13 @@ function preparePaymentHeader( from: Address, x402Version: number, paymentRequirements: RequestedPaymentRequirements, + nonce: Hex ): UnsignedPaymentPayload { - const nonce = createNonce(); - const validAfter = BigInt( - Math.floor(Date.now() / 1000) - 600, // 10 minutes before + Math.floor(Date.now() / 1000) - 600 // 10 minutes before ).toString(); const validBefore = BigInt( - Math.floor(Date.now() / 1000 + paymentRequirements.maxTimeoutSeconds), + Math.floor(Date.now() / 1000 + paymentRequirements.maxTimeoutSeconds) ).toString(); return { @@ -44,7 +52,7 @@ function preparePaymentHeader( value: paymentRequirements.maxAmountRequired, validAfter: validAfter.toString(), validBefore: validBefore.toString(), - nonce, + nonce: nonce, }, }, }; @@ -59,45 +67,69 @@ function preparePaymentHeader( * @returns A promise that resolves to the signed payment payload */ async function signPaymentHeader( + client: ThirdwebClient, account: Account, - paymentRequirements: RequestedPaymentRequirements, - unsignedPaymentHeader: UnsignedPaymentPayload, -): Promise { - const { signature } = await signAuthorization( - account, - unsignedPaymentHeader.payload.authorization, - paymentRequirements, - ); - - return { - ...unsignedPaymentHeader, - payload: { - ...unsignedPaymentHeader.payload, - signature, - }, - }; -} - -/** - * Creates a complete payment payload by preparing and signing a payment header. - * - * @param client - The signer wallet instance used to create and sign the payment - * @param x402Version - The version of the X402 protocol to use - * @param paymentRequirements - The payment requirements containing scheme and network information - * @returns A promise that resolves to the complete signed payment payload - */ -async function createPayment( - account: Account, - x402Version: number, - paymentRequirements: RequestedPaymentRequirements, + paymentRequirements: RequestedPaymentRequirements ): Promise { const from = getAddress(account.address); - const unsignedPaymentHeader = preparePaymentHeader( - from, - x402Version, - paymentRequirements, - ); - return signPaymentHeader(account, paymentRequirements, unsignedPaymentHeader); + const chainId = networkToChainId(paymentRequirements.network); + const { hasPermit, hasTransferWithAuthorization } = + await detectSupportedAuthorizationMethods({ + client, + asset: paymentRequirements.asset, + chainId: chainId, + }); + + // only use permit if no transfer with authorization is supported + if (hasPermit && !hasTransferWithAuthorization) { + const nonce = await nonces({ + contract: getContract({ + address: paymentRequirements.asset, + chain: getCachedChain(chainId), + client: client, + }), + owner: from, + }); + const unsignedPaymentHeader = preparePaymentHeader( + from, + x402Version, + paymentRequirements, + toHex(nonce, { size: 32 }) // permit nonce + ); + const { signature } = await signERC2612Permit( + account, + unsignedPaymentHeader.payload.authorization, + paymentRequirements + ); + return { + ...unsignedPaymentHeader, + payload: { + ...unsignedPaymentHeader.payload, + signature, + }, + }; + } else { + // default to transfer with authorization + const nonce = await createNonce(); + const unsignedPaymentHeader = preparePaymentHeader( + from, + x402Version, + paymentRequirements, + nonce // random nonce + ); + const { signature } = await signERC3009Authorization( + account, + unsignedPaymentHeader.payload.authorization, + paymentRequirements + ); + return { + ...unsignedPaymentHeader, + payload: { + ...unsignedPaymentHeader.payload, + signature, + }, + }; + } } /** @@ -109,15 +141,11 @@ async function createPayment( * @returns A promise that resolves to the encoded payment header string */ export async function createPaymentHeader( + client: ThirdwebClient, account: Account, - x402Version: number, - paymentRequirements: RequestedPaymentRequirements, + paymentRequirements: RequestedPaymentRequirements ): Promise { - const payment = await createPayment( - account, - x402Version, - paymentRequirements, - ); + const payment = await signPaymentHeader(client, account, paymentRequirements); return encodePayment(payment); } @@ -138,7 +166,7 @@ export async function createPaymentHeader( * @param paymentRequirements.extra - The extra information containing the name and version of the ERC20 contract * @returns The signature for the authorization */ -async function signAuthorization( +async function signERC3009Authorization( account: Account, { from, @@ -148,14 +176,13 @@ async function signAuthorization( validBefore, nonce, }: ExactEvmPayloadAuthorization, - { asset, network, extra }: RequestedPaymentRequirements, + { asset, network, extra }: RequestedPaymentRequirements ): Promise<{ signature: Hex }> { const chainId = networkToChainId(network); const name = extra?.name; const version = extra?.version; - // TODO (402): detect permit vs transfer on asset contract - const data = { + const signature = await account.signTypedData({ types: { TransferWithAuthorization: [ { name: "from", type: "address" }, @@ -176,14 +203,61 @@ async function signAuthorization( message: { from: getAddress(from), to: getAddress(to), - value, - validAfter, - validBefore, - nonce: nonce, + value: BigInt(value), + validAfter: BigInt(validAfter), + validBefore: BigInt(validBefore), + nonce: nonce as Hex, }, + }); + + return { + signature, }; +} + +async function signERC2612Permit( + account: Account, + { from, value, validBefore, nonce }: ExactEvmPayloadAuthorization, + { asset, network, extra }: RequestedPaymentRequirements +): Promise<{ signature: Hex }> { + const chainId = networkToChainId(network); + const name = extra?.name; + const version = extra?.version; + + const facilitatorAddress = extra?.facilitatorAddress; + if (!facilitatorAddress) { + throw new Error( + "facilitatorAddress is required in PaymentRequirements extra to pay with permit-based assets" + ); + } + + //Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline + const signature = await account.signTypedData({ + types: { + Permit: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], + }, + domain: { + name, + version, + chainId, + verifyingContract: getAddress(asset), + }, + primaryType: "Permit" as const, + message: { + owner: getAddress(from), + spender: getAddress(facilitatorAddress), // approve the facilitator + value: BigInt(value), + nonce: hexToBigInt(nonce as Hex), + deadline: BigInt(validBefore), + }, + }); - const signature = await account.signTypedData(data); return { signature, }; @@ -194,7 +268,7 @@ async function signAuthorization( * * @returns A random 32-byte nonce as a hex string */ -function createNonce(): Hex { +async function createNonce(): Promise { const cryptoObj = typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.getRandomValues === "function" From 2461aa461e1361f06cb83c7f5b37597bf7599f0f Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Fri, 26 Sep 2025 00:28:29 +1200 Subject: [PATCH 2/5] lint --- packages/thirdweb/src/x402/common.ts | 70 ++++++++++++++-------------- packages/thirdweb/src/x402/sign.ts | 30 ++++++------ 2 files changed, 49 insertions(+), 51 deletions(-) diff --git a/packages/thirdweb/src/x402/common.ts b/packages/thirdweb/src/x402/common.ts index 26e100c5d6e..7268ed5a869 100644 --- a/packages/thirdweb/src/x402/common.ts +++ b/packages/thirdweb/src/x402/common.ts @@ -1,4 +1,12 @@ +import type { Abi } from "abitype"; +import { toFunctionSelector } from "viem/utils"; import { type ERC20TokenAmount, type Money, moneySchema } from "x402/types"; +import { getCachedChain } from "../chains/utils.js"; +import type { ThirdwebClient } from "../client/client.js"; +import { resolveContractAbi } from "../contract/actions/resolve-abi.js"; +import { getContract } from "../contract/contract.js"; +import { isPermitSupported } from "../extensions/erc20/__generated__/IERC20Permit/write/permit.js"; +import { isTransferWithAuthorizationSupported } from "../extensions/erc20/__generated__/USDC/write/transferWithAuthorization.js"; import { getAddress } from "../utils/address.js"; import { decodePayment } from "./encode.js"; import type { facilitator as facilitatorType } from "./facilitator.js"; @@ -12,14 +20,6 @@ import { type PaymentRequiredResult, x402Version, } from "./types.js"; -import { isTransferWithAuthorizationSupported } from "../extensions/erc20/__generated__/USDC/write/transferWithAuthorization.js"; -import { getContract } from "../contract/contract.js"; -import { resolveContractAbi } from "../contract/actions/resolve-abi.js"; -import type { Abi } from "abitype"; -import { getCachedChain } from "../chains/utils.js"; -import type { ThirdwebClient } from "../client/client.js"; -import { toFunctionSelector } from "viem/utils"; -import { isPermitSupported } from "../extensions/erc20/__generated__/IERC20Permit/write/permit.js"; type GetPaymentRequirementsResult = { status: 200; @@ -247,34 +247,34 @@ async function getDefaultAsset( } export type SupportedAuthorizationMethods = { - hasPermit: boolean, - hasTransferWithAuthorization: boolean, -} + hasPermit: boolean; + hasTransferWithAuthorization: boolean; +}; export async function detectSupportedAuthorizationMethods(args: { - client: ThirdwebClient, - asset: string, - chainId: number, -}): Promise { - const abi = await resolveContractAbi( - getContract({ - client: args.client, - address: args.asset, - chain: getCachedChain(args.chainId), - }), - ).catch((error) => { - console.error("Error resolving contract ABI", error); - return [] as Abi; - }); - const selectors = abi - .filter((f) => f.type === "function") - .map((f) => toFunctionSelector(f)); - const hasPermit = isPermitSupported(selectors); - const hasTransferWithAuthorization = - isTransferWithAuthorizationSupported(selectors); + client: ThirdwebClient; + asset: string; + chainId: number; +}): Promise { + const abi = await resolveContractAbi( + getContract({ + client: args.client, + address: args.asset, + chain: getCachedChain(args.chainId), + }), + ).catch((error) => { + console.error("Error resolving contract ABI", error); + return [] as Abi; + }); + const selectors = abi + .filter((f) => f.type === "function") + .map((f) => toFunctionSelector(f)); + const hasPermit = isPermitSupported(selectors); + const hasTransferWithAuthorization = + isTransferWithAuthorizationSupported(selectors); - return { - hasPermit, - hasTransferWithAuthorization, - }; + return { + hasPermit, + hasTransferWithAuthorization, + }; } diff --git a/packages/thirdweb/src/x402/sign.ts b/packages/thirdweb/src/x402/sign.ts index 550201cd38e..ec4917d07b1 100644 --- a/packages/thirdweb/src/x402/sign.ts +++ b/packages/thirdweb/src/x402/sign.ts @@ -3,9 +3,11 @@ import type { ExactEvmPayloadAuthorization } from "x402/types"; import { getCachedChain } from "../chains/utils.js"; import type { ThirdwebClient } from "../client/client.js"; import { getContract } from "../contract/contract.js"; +import { nonces } from "../extensions/erc20/__generated__/IERC20Permit/read/nonces.js"; import { type Address, getAddress } from "../utils/address.js"; import { type Hex, toHex } from "../utils/encoding/hex.js"; import type { Account } from "../wallets/interfaces/wallet.js"; +import { detectSupportedAuthorizationMethods } from "./common.js"; import { encodePayment } from "./encode.js"; import { networkToChainId, @@ -13,10 +15,6 @@ import { type RequestedPaymentRequirements, type UnsignedPaymentPayload, } from "./schemas.js"; -import { - detectSupportedAuthorizationMethods, -} from "./common.js"; -import { nonces } from "../extensions/erc20/__generated__/IERC20Permit/read/nonces.js"; import { x402Version } from "./types.js"; /** @@ -31,13 +29,13 @@ function preparePaymentHeader( from: Address, x402Version: number, paymentRequirements: RequestedPaymentRequirements, - nonce: Hex + nonce: Hex, ): UnsignedPaymentPayload { const validAfter = BigInt( - Math.floor(Date.now() / 1000) - 600 // 10 minutes before + Math.floor(Date.now() / 1000) - 600, // 10 minutes before ).toString(); const validBefore = BigInt( - Math.floor(Date.now() / 1000 + paymentRequirements.maxTimeoutSeconds) + Math.floor(Date.now() / 1000 + paymentRequirements.maxTimeoutSeconds), ).toString(); return { @@ -69,7 +67,7 @@ function preparePaymentHeader( async function signPaymentHeader( client: ThirdwebClient, account: Account, - paymentRequirements: RequestedPaymentRequirements + paymentRequirements: RequestedPaymentRequirements, ): Promise { const from = getAddress(account.address); const chainId = networkToChainId(paymentRequirements.network); @@ -94,12 +92,12 @@ async function signPaymentHeader( from, x402Version, paymentRequirements, - toHex(nonce, { size: 32 }) // permit nonce + toHex(nonce, { size: 32 }), // permit nonce ); const { signature } = await signERC2612Permit( account, unsignedPaymentHeader.payload.authorization, - paymentRequirements + paymentRequirements, ); return { ...unsignedPaymentHeader, @@ -115,12 +113,12 @@ async function signPaymentHeader( from, x402Version, paymentRequirements, - nonce // random nonce + nonce, // random nonce ); const { signature } = await signERC3009Authorization( account, unsignedPaymentHeader.payload.authorization, - paymentRequirements + paymentRequirements, ); return { ...unsignedPaymentHeader, @@ -143,7 +141,7 @@ async function signPaymentHeader( export async function createPaymentHeader( client: ThirdwebClient, account: Account, - paymentRequirements: RequestedPaymentRequirements + paymentRequirements: RequestedPaymentRequirements, ): Promise { const payment = await signPaymentHeader(client, account, paymentRequirements); return encodePayment(payment); @@ -176,7 +174,7 @@ async function signERC3009Authorization( validBefore, nonce, }: ExactEvmPayloadAuthorization, - { asset, network, extra }: RequestedPaymentRequirements + { asset, network, extra }: RequestedPaymentRequirements, ): Promise<{ signature: Hex }> { const chainId = networkToChainId(network); const name = extra?.name; @@ -218,7 +216,7 @@ async function signERC3009Authorization( async function signERC2612Permit( account: Account, { from, value, validBefore, nonce }: ExactEvmPayloadAuthorization, - { asset, network, extra }: RequestedPaymentRequirements + { asset, network, extra }: RequestedPaymentRequirements, ): Promise<{ signature: Hex }> { const chainId = networkToChainId(network); const name = extra?.name; @@ -227,7 +225,7 @@ async function signERC2612Permit( const facilitatorAddress = extra?.facilitatorAddress; if (!facilitatorAddress) { throw new Error( - "facilitatorAddress is required in PaymentRequirements extra to pay with permit-based assets" + "facilitatorAddress is required in PaymentRequirements extra to pay with permit-based assets", ); } From a1e06b21df2f323ca05e5c7792d9151d8e79cfd6 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Fri, 26 Sep 2025 12:28:54 +1200 Subject: [PATCH 3/5] fixes --- .../wallet-table/wallet-table-ui.client.tsx | 6 +- .../src/app/api/paywall/route.ts | 2 +- .../app/payments/x402/components/constants.ts | 31 ++++++++++ .../x402/components/x402-client-preview.tsx | 56 ++++++++++++------- apps/playground-web/src/middleware.ts | 37 ++++++++++-- packages/thirdweb/src/exports/x402.ts | 1 + packages/thirdweb/src/x402/common.ts | 31 ++++++++-- packages/thirdweb/src/x402/facilitator.ts | 24 +++++++- .../thirdweb/src/x402/fetchWithPayment.ts | 1 + packages/thirdweb/src/x402/sign.ts | 30 +++++++--- packages/thirdweb/src/x402/types.ts | 19 +++++-- 11 files changed, 190 insertions(+), 48 deletions(-) create mode 100644 apps/playground-web/src/app/payments/x402/components/constants.ts diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/wallet-table/wallet-table-ui.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/wallet-table/wallet-table-ui.client.tsx index c778d8ed948..7893ccd7833 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/wallet-table/wallet-table-ui.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/wallet-table/wallet-table-ui.client.tsx @@ -215,7 +215,7 @@ export function ServerWalletsTableUI({ 1 ? currentPage - 1 : 1 }`} legacyBehavior @@ -232,7 +232,7 @@ export function ServerWalletsTableUI({ (pageNumber) => ( @@ -244,7 +244,7 @@ export function ServerWalletsTableUI({ )} 0 ? "?" + searchParams.toString() : ""); + const response = await fetchWithPay(url.toString()); return response.json(); }, }); @@ -47,18 +57,20 @@ export function X402ClientPreview() { chain={chain} detailsButton={{ displayBalanceToken: { - [chain.id]: token!.address, + [chain.id]: token.address, }, }} supportedTokens={{ - [chain.id]: [token!], + [chain.id]: [token], }} />
Paid API Call - $0.01 + + 0.1 {token.symbol} +

- {" "} - - Click here to get USDC on {chain.name} - + Pay for access with {token.symbol} on{" "} + {chain.name || `chain ${chain.id}`}

+ {chain.testnet && token.symbol.toLowerCase() === "usdc" && ( +

+ {" "} + + Click here to get testnet {token.symbol} on {chain.name} + +

+ )}
diff --git a/apps/playground-web/src/middleware.ts b/apps/playground-web/src/middleware.ts index 1b556d07ce0..25d7e5bdb65 100644 --- a/apps/playground-web/src/middleware.ts +++ b/apps/playground-web/src/middleware.ts @@ -1,18 +1,16 @@ import { type NextRequest, NextResponse } from "next/server"; -import { createThirdwebClient } from "thirdweb"; -import { arbitrumSepolia } from "thirdweb/chains"; +import { createThirdwebClient, defineChain } from "thirdweb"; import { facilitator, settlePayment } from "thirdweb/x402"; const client = createThirdwebClient({ secretKey: process.env.THIRDWEB_SECRET_KEY as string, }); -const chain = arbitrumSepolia; const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string; +// const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_SMART_WALLET as string; const ENGINE_VAULT_ACCESS_TOKEN = process.env .ENGINE_VAULT_ACCESS_TOKEN as string; const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`; - const twFacilitator = facilitator({ baseUrl: `${API_URL}/v1/payments/x402`, client, @@ -25,14 +23,41 @@ export async function middleware(request: NextRequest) { const method = request.method.toUpperCase(); const resourceUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}${pathname}`; const paymentData = request.headers.get("X-PAYMENT"); + const queryParams = request.nextUrl.searchParams; + + const chainId = queryParams.get("chainId"); + const payTo = queryParams.get("payTo"); + + if (!chainId || !payTo) { + return NextResponse.json( + { error: "Missing required parameters" }, + { status: 400 }, + ); + } + + // TODO (402): dynamic from playground config + // const amount = queryParams.get("amount"); + // const tokenAddress = queryParams.get("tokenAddress"); + // const decimals = queryParams.get("decimals"); const result = await settlePayment({ resourceUrl, method, paymentData, - payTo: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024", - network: chain, + payTo: payTo as `0x${string}`, + network: defineChain(Number(chainId)), price: "$0.01", + // price: { + // amount: toUnits(amount as string, parseInt(decimals as string)).toString(), + // asset: { + // address: tokenAddress as `0x${string}`, + // decimals: decimals ? parseInt(decimals) : token.decimals, + // eip712: { + // name: token.name, + // version: token.version, + // }, + // }, + // }, routeConfig: { description: "Access to paid content", }, diff --git a/packages/thirdweb/src/exports/x402.ts b/packages/thirdweb/src/exports/x402.ts index 7b16b43f5f1..b1c37052133 100644 --- a/packages/thirdweb/src/exports/x402.ts +++ b/packages/thirdweb/src/exports/x402.ts @@ -1,6 +1,7 @@ export { decodePayment, encodePayment } from "../x402/encode.js"; export { facilitator, + type ThirdwebX402Facilitator, type ThirdwebX402FacilitatorConfig, } from "../x402/facilitator.js"; export { wrapFetchWithPayment } from "../x402/fetchWithPayment.js"; diff --git a/packages/thirdweb/src/x402/common.ts b/packages/thirdweb/src/x402/common.ts index 7268ed5a869..d975fa8fc37 100644 --- a/packages/thirdweb/src/x402/common.ts +++ b/packages/thirdweb/src/x402/common.ts @@ -1,6 +1,6 @@ import type { Abi } from "abitype"; import { toFunctionSelector } from "viem/utils"; -import { type ERC20TokenAmount, type Money, moneySchema } from "x402/types"; +import { type Money, moneySchema } from "x402/types"; import { getCachedChain } from "../chains/utils.js"; import type { ThirdwebClient } from "../client/client.js"; import { resolveContractAbi } from "../contract/actions/resolve-abi.js"; @@ -16,6 +16,7 @@ import { type RequestedPaymentRequirements, } from "./schemas.js"; import { + type ERC20TokenAmount, type PaymentArgs, type PaymentRequiredResult, x402Version, @@ -116,7 +117,7 @@ export async function decodePaymentRequest( }, extra: { facilitatorAddress: facilitator.address, - ...(asset as ERC20TokenAmount["asset"]).eip712, + ...((asset as ERC20TokenAmount["asset"]).eip712 ?? {}), }, }); @@ -247,15 +248,33 @@ async function getDefaultAsset( } export type SupportedAuthorizationMethods = { - hasPermit: boolean; - hasTransferWithAuthorization: boolean; + usePermit: boolean; + useTransferWithAuthorization: boolean; }; export async function detectSupportedAuthorizationMethods(args: { client: ThirdwebClient; asset: string; chainId: number; + eip712Extras: ERC20TokenAmount["asset"]["eip712"] | undefined; }): Promise { + const primaryType = args.eip712Extras?.primaryType; + + if (primaryType === "Permit") { + return { + usePermit: true, + useTransferWithAuthorization: false, + }; + } + + if (primaryType === "TransferWithAuthorization") { + return { + usePermit: false, + useTransferWithAuthorization: true, + }; + } + + // not specified, so we need to detect it const abi = await resolveContractAbi( getContract({ client: args.client, @@ -274,7 +293,7 @@ export async function detectSupportedAuthorizationMethods(args: { isTransferWithAuthorizationSupported(selectors); return { - hasPermit, - hasTransferWithAuthorization, + usePermit: hasPermit && !hasTransferWithAuthorization, // only use permit if transfer with authorization is unsupported + useTransferWithAuthorization: hasTransferWithAuthorization, }; } diff --git a/packages/thirdweb/src/x402/facilitator.ts b/packages/thirdweb/src/x402/facilitator.ts index 9287bfd951f..2194487f715 100644 --- a/packages/thirdweb/src/x402/facilitator.ts +++ b/packages/thirdweb/src/x402/facilitator.ts @@ -15,6 +15,26 @@ export type ThirdwebX402FacilitatorConfig = { baseUrl?: string; }; +export type ThirdwebX402Facilitator = { + url: `${string}://${string}`; + address: string; + createAuthHeaders: () => Promise<{ + verify: Record; + settle: Record; + supported: Record; + list: Record; + }>; + verify: ( + payload: RequestedPaymentPayload, + paymentRequirements: RequestedPaymentRequirements, + ) => Promise; + settle: ( + payload: RequestedPaymentPayload, + paymentRequirements: RequestedPaymentRequirements, + ) => Promise; + supported: () => Promise; +}; + const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402"; /** @@ -56,7 +76,9 @@ const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402"; * * @bridge x402 */ -export function facilitator(config: ThirdwebX402FacilitatorConfig) { +export function facilitator( + config: ThirdwebX402FacilitatorConfig, +): ThirdwebX402Facilitator { const secretKey = config.client.secretKey; if (!secretKey) { throw new Error("Client secret key is required for the x402 facilitator"); diff --git a/packages/thirdweb/src/x402/fetchWithPayment.ts b/packages/thirdweb/src/x402/fetchWithPayment.ts index 1fc340c5628..6c0b2bc2ab7 100644 --- a/packages/thirdweb/src/x402/fetchWithPayment.ts +++ b/packages/thirdweb/src/x402/fetchWithPayment.ts @@ -106,6 +106,7 @@ export function wrapFetchWithPayment( client, account, selectedPaymentRequirements, + x402Version, ); const initParams = init || {}; diff --git a/packages/thirdweb/src/x402/sign.ts b/packages/thirdweb/src/x402/sign.ts index ec4917d07b1..2bdd277243c 100644 --- a/packages/thirdweb/src/x402/sign.ts +++ b/packages/thirdweb/src/x402/sign.ts @@ -15,7 +15,7 @@ import { type RequestedPaymentRequirements, type UnsignedPaymentPayload, } from "./schemas.js"; -import { x402Version } from "./types.js"; +import type { ERC20TokenAmount } from "./types.js"; /** * Prepares an unsigned payment header with the given sender address and payment requirements. @@ -68,18 +68,21 @@ async function signPaymentHeader( client: ThirdwebClient, account: Account, paymentRequirements: RequestedPaymentRequirements, + x402Version: number, ): Promise { const from = getAddress(account.address); const chainId = networkToChainId(paymentRequirements.network); - const { hasPermit, hasTransferWithAuthorization } = + const { usePermit, useTransferWithAuthorization } = await detectSupportedAuthorizationMethods({ client, asset: paymentRequirements.asset, chainId: chainId, + eip712Extras: paymentRequirements.extra as + | ERC20TokenAmount["asset"]["eip712"] + | undefined, }); - // only use permit if no transfer with authorization is supported - if (hasPermit && !hasTransferWithAuthorization) { + if (usePermit) { const nonce = await nonces({ contract: getContract({ address: paymentRequirements.asset, @@ -106,7 +109,7 @@ async function signPaymentHeader( signature, }, }; - } else { + } else if (useTransferWithAuthorization) { // default to transfer with authorization const nonce = await createNonce(); const unsignedPaymentHeader = preparePaymentHeader( @@ -128,6 +131,9 @@ async function signPaymentHeader( }, }; } + throw new Error( + `No supported payment authorization methods found on ${paymentRequirements.asset} on chain ${paymentRequirements.network}`, + ); } /** @@ -142,8 +148,14 @@ export async function createPaymentHeader( client: ThirdwebClient, account: Account, paymentRequirements: RequestedPaymentRequirements, + x402Version: number, ): Promise { - const payment = await signPaymentHeader(client, account, paymentRequirements); + const payment = await signPaymentHeader( + client, + account, + paymentRequirements, + x402Version, + ); return encodePayment(payment); } @@ -221,13 +233,17 @@ async function signERC2612Permit( const chainId = networkToChainId(network); const name = extra?.name; const version = extra?.version; - const facilitatorAddress = extra?.facilitatorAddress; if (!facilitatorAddress) { throw new Error( "facilitatorAddress is required in PaymentRequirements extra to pay with permit-based assets", ); } + if (!name || !version) { + throw new Error( + "name and version are required in PaymentRequirements extra to pay with permit-based assets", + ); + } //Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline const signature = await account.signTypedData({ diff --git a/packages/thirdweb/src/x402/types.ts b/packages/thirdweb/src/x402/types.ts index ad29f6c964c..cd9315b33d1 100644 --- a/packages/thirdweb/src/x402/types.ts +++ b/packages/thirdweb/src/x402/types.ts @@ -1,8 +1,4 @@ -import type { - ERC20TokenAmount, - Money, - PaymentMiddlewareConfig, -} from "x402/types"; +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"; @@ -89,3 +85,16 @@ export type VerifyPaymentResult = Prettify< } | PaymentRequiredResult >; + +export type ERC20TokenAmount = { + amount: string; + asset: { + address: `0x${string}`; + decimals: number; + eip712: { + name: string; + version: string; + primaryType: "TransferWithAuthorization" | "Permit"; + }; + }; +}; From da2b065b792e9dcce4f430de86b7d61db3e9e26e Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Fri, 26 Sep 2025 13:00:10 +1200 Subject: [PATCH 4/5] cleanup --- packages/thirdweb/src/x402/common.ts | 36 +++----- packages/thirdweb/src/x402/sign.ts | 125 ++++++++++++++------------- packages/thirdweb/src/x402/types.ts | 4 +- 3 files changed, 80 insertions(+), 85 deletions(-) diff --git a/packages/thirdweb/src/x402/common.ts b/packages/thirdweb/src/x402/common.ts index d975fa8fc37..a6258073633 100644 --- a/packages/thirdweb/src/x402/common.ts +++ b/packages/thirdweb/src/x402/common.ts @@ -19,6 +19,7 @@ import { type ERC20TokenAmount, type PaymentArgs, type PaymentRequiredResult, + type SupportedSignatureType, x402Version, } from "./types.js"; @@ -247,31 +248,16 @@ async function getDefaultAsset( return assetConfig; } -export type SupportedAuthorizationMethods = { - usePermit: boolean; - useTransferWithAuthorization: boolean; -}; - -export async function detectSupportedAuthorizationMethods(args: { +export async function getSupportedSignatureType(args: { client: ThirdwebClient; asset: string; chainId: number; eip712Extras: ERC20TokenAmount["asset"]["eip712"] | undefined; -}): Promise { +}): Promise { const primaryType = args.eip712Extras?.primaryType; - if (primaryType === "Permit") { - return { - usePermit: true, - useTransferWithAuthorization: false, - }; - } - - if (primaryType === "TransferWithAuthorization") { - return { - usePermit: false, - useTransferWithAuthorization: true, - }; + if (primaryType === "Permit" || primaryType === "TransferWithAuthorization") { + return primaryType; } // not specified, so we need to detect it @@ -292,8 +278,12 @@ export async function detectSupportedAuthorizationMethods(args: { const hasTransferWithAuthorization = isTransferWithAuthorizationSupported(selectors); - return { - usePermit: hasPermit && !hasTransferWithAuthorization, // only use permit if transfer with authorization is unsupported - useTransferWithAuthorization: hasTransferWithAuthorization, - }; + // prefer transferWithAuthorization over permit + if (hasTransferWithAuthorization) { + return "TransferWithAuthorization"; + } + if (hasPermit) { + return "Permit"; + } + return undefined; } diff --git a/packages/thirdweb/src/x402/sign.ts b/packages/thirdweb/src/x402/sign.ts index 2bdd277243c..5cc145e12e8 100644 --- a/packages/thirdweb/src/x402/sign.ts +++ b/packages/thirdweb/src/x402/sign.ts @@ -7,7 +7,7 @@ import { nonces } from "../extensions/erc20/__generated__/IERC20Permit/read/nonc import { type Address, getAddress } from "../utils/address.js"; import { type Hex, toHex } from "../utils/encoding/hex.js"; import type { Account } from "../wallets/interfaces/wallet.js"; -import { detectSupportedAuthorizationMethods } from "./common.js"; +import { getSupportedSignatureType } from "./common.js"; import { encodePayment } from "./encode.js"; import { networkToChainId, @@ -72,68 +72,71 @@ async function signPaymentHeader( ): Promise { const from = getAddress(account.address); const chainId = networkToChainId(paymentRequirements.network); - const { usePermit, useTransferWithAuthorization } = - await detectSupportedAuthorizationMethods({ - client, - asset: paymentRequirements.asset, - chainId: chainId, - eip712Extras: paymentRequirements.extra as - | ERC20TokenAmount["asset"]["eip712"] - | undefined, - }); + const supportedSignatureType = await getSupportedSignatureType({ + client, + asset: paymentRequirements.asset, + chainId: chainId, + eip712Extras: paymentRequirements.extra as + | ERC20TokenAmount["asset"]["eip712"] + | undefined, + }); - if (usePermit) { - const nonce = await nonces({ - contract: getContract({ - address: paymentRequirements.asset, - chain: getCachedChain(chainId), - client: client, - }), - owner: from, - }); - const unsignedPaymentHeader = preparePaymentHeader( - from, - x402Version, - paymentRequirements, - toHex(nonce, { size: 32 }), // permit nonce - ); - const { signature } = await signERC2612Permit( - account, - unsignedPaymentHeader.payload.authorization, - paymentRequirements, - ); - return { - ...unsignedPaymentHeader, - payload: { - ...unsignedPaymentHeader.payload, - signature, - }, - }; - } else if (useTransferWithAuthorization) { - // default to transfer with authorization - const nonce = await createNonce(); - const unsignedPaymentHeader = preparePaymentHeader( - from, - x402Version, - paymentRequirements, - nonce, // random nonce - ); - const { signature } = await signERC3009Authorization( - account, - unsignedPaymentHeader.payload.authorization, - paymentRequirements, - ); - return { - ...unsignedPaymentHeader, - payload: { - ...unsignedPaymentHeader.payload, - signature, - }, - }; + switch (supportedSignatureType) { + case "Permit": { + const nonce = await nonces({ + contract: getContract({ + address: paymentRequirements.asset, + chain: getCachedChain(chainId), + client: client, + }), + owner: from, + }); + const unsignedPaymentHeader = preparePaymentHeader( + from, + x402Version, + paymentRequirements, + toHex(nonce, { size: 32 }), // permit nonce + ); + const { signature } = await signERC2612Permit( + account, + unsignedPaymentHeader.payload.authorization, + paymentRequirements, + ); + return { + ...unsignedPaymentHeader, + payload: { + ...unsignedPaymentHeader.payload, + signature, + }, + }; + } + case "TransferWithAuthorization": { + // default to transfer with authorization + const nonce = await createNonce(); + const unsignedPaymentHeader = preparePaymentHeader( + from, + x402Version, + paymentRequirements, + nonce, // random nonce + ); + const { signature } = await signERC3009Authorization( + account, + unsignedPaymentHeader.payload.authorization, + paymentRequirements, + ); + return { + ...unsignedPaymentHeader, + payload: { + ...unsignedPaymentHeader.payload, + signature, + }, + }; + } + default: + throw new Error( + `No supported payment authorization methods found on ${paymentRequirements.asset} on chain ${paymentRequirements.network}`, + ); } - throw new Error( - `No supported payment authorization methods found on ${paymentRequirements.asset} on chain ${paymentRequirements.network}`, - ); } /** diff --git a/packages/thirdweb/src/x402/types.ts b/packages/thirdweb/src/x402/types.ts index cd9315b33d1..a7e5bfaaae8 100644 --- a/packages/thirdweb/src/x402/types.ts +++ b/packages/thirdweb/src/x402/types.ts @@ -86,6 +86,8 @@ export type VerifyPaymentResult = Prettify< | PaymentRequiredResult >; +export type SupportedSignatureType = "TransferWithAuthorization" | "Permit"; + export type ERC20TokenAmount = { amount: string; asset: { @@ -94,7 +96,7 @@ export type ERC20TokenAmount = { eip712: { name: string; version: string; - primaryType: "TransferWithAuthorization" | "Permit"; + primaryType: SupportedSignatureType; }; }; }; From 536f5207f4249b96d14dd598421d94e7bb94cf6e Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Fri, 26 Sep 2025 13:09:02 +1200 Subject: [PATCH 5/5] types --- packages/thirdweb/src/x402/common.ts | 6 +++--- packages/thirdweb/src/x402/facilitator.ts | 4 ++++ packages/thirdweb/src/x402/types.ts | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/thirdweb/src/x402/common.ts b/packages/thirdweb/src/x402/common.ts index a6258073633..b4eb3b834d7 100644 --- a/packages/thirdweb/src/x402/common.ts +++ b/packages/thirdweb/src/x402/common.ts @@ -9,7 +9,7 @@ import { isPermitSupported } from "../extensions/erc20/__generated__/IERC20Permi import { isTransferWithAuthorizationSupported } from "../extensions/erc20/__generated__/USDC/write/transferWithAuthorization.js"; import { getAddress } from "../utils/address.js"; import { decodePayment } from "./encode.js"; -import type { facilitator as facilitatorType } from "./facilitator.js"; +import type { ThirdwebX402Facilitator } from "./facilitator.js"; import { networkToChainId, type RequestedPaymentPayload, @@ -197,7 +197,7 @@ export async function decodePaymentRequest( async function processPriceToAtomicAmount( price: Money | ERC20TokenAmount, chainId: number, - facilitator: ReturnType, + facilitator: ThirdwebX402Facilitator, ): Promise< | { maxAmountRequired: string; asset: ERC20TokenAmount["asset"] } | { error: string } @@ -237,7 +237,7 @@ async function processPriceToAtomicAmount( async function getDefaultAsset( chainId: number, - facilitator: ReturnType, + facilitator: ThirdwebX402Facilitator, ): Promise { const supportedAssets = await facilitator.supported(); const matchingAsset = supportedAssets.kinds.find( diff --git a/packages/thirdweb/src/x402/facilitator.ts b/packages/thirdweb/src/x402/facilitator.ts index 2194487f715..b9047e162f8 100644 --- a/packages/thirdweb/src/x402/facilitator.ts +++ b/packages/thirdweb/src/x402/facilitator.ts @@ -15,6 +15,10 @@ export type ThirdwebX402FacilitatorConfig = { baseUrl?: string; }; +/** + * facilitator for the x402 payment protocol. + * @public + */ export type ThirdwebX402Facilitator = { url: `${string}://${string}`; address: string; diff --git a/packages/thirdweb/src/x402/types.ts b/packages/thirdweb/src/x402/types.ts index a7e5bfaaae8..51260a0e6d3 100644 --- a/packages/thirdweb/src/x402/types.ts +++ b/packages/thirdweb/src/x402/types.ts @@ -2,7 +2,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"; +import type { ThirdwebX402Facilitator } from "./facilitator.js"; import type { FacilitatorNetwork, FacilitatorSettleResponse, @@ -31,7 +31,7 @@ export type PaymentArgs = { /** 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 */ - facilitator: ReturnType; + facilitator: ThirdwebX402Facilitator; /** Optional configuration for the payment middleware route */ routeConfig?: PaymentMiddlewareConfig; };