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/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/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/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/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..b4eb3b834d7 100644 --- a/packages/thirdweb/src/x402/common.ts +++ b/packages/thirdweb/src/x402/common.ts @@ -1,15 +1,25 @@ -import { type ERC20TokenAmount, type Money, moneySchema } from "x402/types"; +import type { Abi } from "abitype"; +import { toFunctionSelector } from "viem/utils"; +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"; +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"; +import type { ThirdwebX402Facilitator } from "./facilitator.js"; import { networkToChainId, type RequestedPaymentPayload, type RequestedPaymentRequirements, } from "./schemas.js"; import { + type ERC20TokenAmount, type PaymentArgs, type PaymentRequiredResult, + type SupportedSignatureType, x402Version, } from "./types.js"; @@ -106,7 +116,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 @@ -184,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 } @@ -224,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( @@ -234,3 +247,43 @@ async function getDefaultAsset( ?.defaultAsset as ERC20TokenAmount["asset"]; return assetConfig; } + +export async function getSupportedSignatureType(args: { + client: ThirdwebClient; + asset: string; + chainId: number; + eip712Extras: ERC20TokenAmount["asset"]["eip712"] | undefined; +}): Promise { + const primaryType = args.eip712Extras?.primaryType; + + if (primaryType === "Permit" || primaryType === "TransferWithAuthorization") { + return primaryType; + } + + // not specified, so we need to detect it + 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); + + // prefer transferWithAuthorization over permit + if (hasTransferWithAuthorization) { + return "TransferWithAuthorization"; + } + if (hasPermit) { + return "Permit"; + } + return undefined; +} diff --git a/packages/thirdweb/src/x402/facilitator.ts b/packages/thirdweb/src/x402/facilitator.ts index cdb3c35c53f..b9047e162f8 100644 --- a/packages/thirdweb/src/x402/facilitator.ts +++ b/packages/thirdweb/src/x402/facilitator.ts @@ -15,6 +15,30 @@ export type ThirdwebX402FacilitatorConfig = { baseUrl?: string; }; +/** + * facilitator for the x402 payment protocol. + * @public + */ +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 +80,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"); @@ -69,6 +95,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..6c0b2bc2ab7 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,9 +103,10 @@ export function wrapFetchWithPayment( } const paymentHeader = await createPaymentHeader( + client, account, - x402Version, selectedPaymentRequirements, + x402Version, ); const initParams = init || {}; diff --git a/packages/thirdweb/src/x402/sign.ts b/packages/thirdweb/src/x402/sign.ts index dc414ca7f45..5cc145e12e8 100644 --- a/packages/thirdweb/src/x402/sign.ts +++ b/packages/thirdweb/src/x402/sign.ts @@ -1,7 +1,13 @@ +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 { 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 { getSupportedSignatureType } from "./common.js"; import { encodePayment } from "./encode.js"; import { networkToChainId, @@ -9,6 +15,7 @@ import { type RequestedPaymentRequirements, type UnsignedPaymentPayload, } from "./schemas.js"; +import type { ERC20TokenAmount } from "./types.js"; /** * Prepares an unsigned payment header with the given sender address and payment requirements. @@ -22,9 +29,8 @@ 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 ).toString(); @@ -44,7 +50,7 @@ function preparePaymentHeader( value: paymentRequirements.maxAmountRequired, validAfter: validAfter.toString(), validBefore: validBefore.toString(), - nonce, + nonce: nonce, }, }, }; @@ -59,45 +65,78 @@ 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, ): Promise { const from = getAddress(account.address); - const unsignedPaymentHeader = preparePaymentHeader( - from, - x402Version, - paymentRequirements, - ); - return signPaymentHeader(account, paymentRequirements, unsignedPaymentHeader); + const chainId = networkToChainId(paymentRequirements.network); + const supportedSignatureType = await getSupportedSignatureType({ + client, + asset: paymentRequirements.asset, + chainId: chainId, + eip712Extras: paymentRequirements.extra as + | ERC20TokenAmount["asset"]["eip712"] + | undefined, + }); + + 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}`, + ); + } } /** @@ -109,14 +148,16 @@ 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, + x402Version: number, ): Promise { - const payment = await createPayment( + const payment = await signPaymentHeader( + client, account, - x402Version, paymentRequirements, + x402Version, ); return encodePayment(payment); } @@ -138,7 +179,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, @@ -154,8 +195,7 @@ async function signAuthorization( 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 +216,65 @@ 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", + ); + } + 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({ + 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 +285,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" diff --git a/packages/thirdweb/src/x402/types.ts b/packages/thirdweb/src/x402/types.ts index ad29f6c964c..51260a0e6d3 100644 --- a/packages/thirdweb/src/x402/types.ts +++ b/packages/thirdweb/src/x402/types.ts @@ -1,12 +1,8 @@ -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"; -import type { facilitator as facilitatorType } from "./facilitator.js"; +import type { ThirdwebX402Facilitator } from "./facilitator.js"; import type { FacilitatorNetwork, FacilitatorSettleResponse, @@ -35,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; }; @@ -89,3 +85,18 @@ export type VerifyPaymentResult = Prettify< } | PaymentRequiredResult >; + +export type SupportedSignatureType = "TransferWithAuthorization" | "Permit"; + +export type ERC20TokenAmount = { + amount: string; + asset: { + address: `0x${string}`; + decimals: number; + eip712: { + name: string; + version: string; + primaryType: SupportedSignatureType; + }; + }; +};