Skip to content

Commit 9069351

Browse files
[SDK] Support Chain objects in x402 payment utilities (#8126)
1 parent 8f6363f commit 9069351

File tree

11 files changed

+73
-62
lines changed

11 files changed

+73
-62
lines changed

.changeset/polite-banks-peel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Accept chain objects for x402 APIs

apps/playground-web/src/app/payments/x402/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function ServerCodeExample() {
5959
6060
import { facilitator, settlePayment } from "thirdweb/x402";
6161
import { createThirdwebClient } from "thirdweb";
62+
import { arbitrumSepolia } from "thirdweb/chains";
6263
6364
const client = createThirdwebClient({ secretKey: "your-secret-key" });
6465
const thirdwebX402Facilitator = facilitator({
@@ -76,7 +77,7 @@ export async function middleware(request: NextRequest) {
7677
method,
7778
paymentData,
7879
payTo: "0xYourWalletAddress",
79-
network: "eip155:11155111", // or any other chain id
80+
network: arbitrumSepolia, // or any other chain
8081
price: "$0.01", // can also be a ERC20 token amount
8182
facilitator: thirdwebX402Facilitator,
8283
});

apps/playground-web/src/middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export async function middleware(request: NextRequest) {
3131
method,
3232
paymentData,
3333
payTo: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
34-
network: `eip155:${chain.id}`,
34+
network: chain,
3535
price: "$0.01",
3636
routeConfig: {
3737
description: "Access to paid content",

apps/portal/src/app/payments/x402/page.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Here's an example with a Next.js middleware:
5050
```typescript
5151
import { createThirdwebClient } from "thirdweb";
5252
import { facilitator, settlePayment } from "thirdweb/x402";
53+
import { arbitrumSepolia } from "thirdweb/chains";
5354

5455
const client = createThirdwebClient({ secretKey: "your-secret-key" });
5556
const thirdwebX402Facilitator = facilitator({
@@ -67,7 +68,7 @@ export async function middleware(request: NextRequest) {
6768
method,
6869
paymentData,
6970
payTo: "0xYourWalletAddress",
70-
network: "eip155:1", // or any other chain id in CAIP2 format: "eip155:<chain_id>"
71+
network: arbitrumSepolia, // or any other chain
7172
price: "$0.01", // can also be a ERC20 token amount
7273
routeConfig: {
7374
description: "Access to paid content",

packages/thirdweb/src/x402/common.ts

Lines changed: 46 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
1-
import {
2-
type ERC20TokenAmount,
3-
type Money,
4-
moneySchema,
5-
type Network,
6-
SupportedEVMNetworks,
7-
} from "x402/types";
1+
import { type ERC20TokenAmount, type Money, moneySchema } from "x402/types";
82
import { getAddress } from "../utils/address.js";
93
import { decodePayment } from "./encode.js";
104
import type { facilitator as facilitatorType } from "./facilitator.js";
115
import {
12-
type FacilitatorNetwork,
136
networkToChainId,
147
type RequestedPaymentPayload,
158
type RequestedPaymentRequirements,
@@ -54,9 +47,28 @@ export async function decodePaymentRequest(
5447
errorMessages,
5548
discoverable,
5649
} = routeConfig;
50+
51+
let chainId: number;
52+
try {
53+
chainId = networkToChainId(network);
54+
} catch (error) {
55+
return {
56+
status: 402,
57+
responseHeaders: { "Content-Type": "application/json" },
58+
responseBody: {
59+
x402Version,
60+
error:
61+
error instanceof Error
62+
? error.message
63+
: `Invalid network: ${network}`,
64+
accepts: [],
65+
},
66+
};
67+
}
68+
5769
const atomicAmountForAsset = await processPriceToAtomicAmount(
5870
price,
59-
network,
71+
chainId,
6072
facilitator,
6173
);
6274
if ("error" in atomicAmountForAsset) {
@@ -74,45 +86,28 @@ export async function decodePaymentRequest(
7486

7587
const paymentRequirements: RequestedPaymentRequirements[] = [];
7688

77-
if (
78-
SupportedEVMNetworks.includes(network as Network) ||
79-
network.startsWith("eip155:")
80-
) {
81-
paymentRequirements.push({
82-
scheme: "exact",
83-
network,
84-
maxAmountRequired,
85-
resource: resourceUrl,
86-
description: description ?? "",
87-
mimeType: mimeType ?? "application/json",
88-
payTo: getAddress(payTo),
89-
maxTimeoutSeconds: maxTimeoutSeconds ?? 300,
90-
asset: getAddress(asset.address),
91-
// TODO: Rename outputSchema to requestStructure
92-
outputSchema: {
93-
input: {
94-
type: "http",
95-
method,
96-
discoverable: discoverable ?? true,
97-
...inputSchema,
98-
},
99-
output: outputSchema,
100-
},
101-
extra: (asset as ERC20TokenAmount["asset"]).eip712,
102-
});
103-
} else {
104-
return {
105-
status: 402,
106-
responseHeaders: {
107-
"Content-Type": "application/json",
108-
},
109-
responseBody: {
110-
x402Version,
111-
error: `Unsupported network: ${network}`,
112-
accepts: paymentRequirements,
89+
paymentRequirements.push({
90+
scheme: "exact",
91+
network: `eip155:${chainId}`,
92+
maxAmountRequired,
93+
resource: resourceUrl,
94+
description: description ?? "",
95+
mimeType: mimeType ?? "application/json",
96+
payTo: getAddress(payTo),
97+
maxTimeoutSeconds: maxTimeoutSeconds ?? 300,
98+
asset: getAddress(asset.address),
99+
// TODO: Rename outputSchema to requestStructure
100+
outputSchema: {
101+
input: {
102+
type: "http",
103+
method,
104+
discoverable: discoverable ?? true,
105+
...inputSchema,
113106
},
114-
};
115-
}
107+
output: outputSchema,
108+
},
109+
extra: (asset as ERC20TokenAmount["asset"]).eip712,
110+
});
116111

117112
// Check for payment header
118113
if (!paymentData) {
@@ -188,7 +183,7 @@ export async function decodePaymentRequest(
188183
*/
189184
async function processPriceToAtomicAmount(
190185
price: Money | ERC20TokenAmount,
191-
network: FacilitatorNetwork,
186+
chainId: number,
192187
facilitator: ReturnType<typeof facilitatorType>,
193188
): Promise<
194189
| { maxAmountRequired: string; asset: ERC20TokenAmount["asset"] }
@@ -207,10 +202,10 @@ async function processPriceToAtomicAmount(
207202
};
208203
}
209204
const parsedUsdAmount = parsedAmount.data;
210-
const defaultAsset = await getDefaultAsset(network, facilitator);
205+
const defaultAsset = await getDefaultAsset(chainId, facilitator);
211206
if (!defaultAsset) {
212207
return {
213-
error: `Unable to get default asset on ${network}. Please specify an asset in the payment requirements.`,
208+
error: `Unable to get default asset on chain ${chainId}. Please specify an asset in the payment requirements.`,
214209
};
215210
}
216211
asset = defaultAsset;
@@ -228,11 +223,10 @@ async function processPriceToAtomicAmount(
228223
}
229224

230225
async function getDefaultAsset(
231-
network: FacilitatorNetwork,
226+
chainId: number,
232227
facilitator: ReturnType<typeof facilitatorType>,
233228
): Promise<ERC20TokenAmount["asset"] | undefined> {
234229
const supportedAssets = await facilitator.supported();
235-
const chainId = networkToChainId(network);
236230
const matchingAsset = supportedAssets.kinds.find(
237231
(supported) => supported.network === `eip155:${chainId}`,
238232
);

packages/thirdweb/src/x402/facilitator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402";
2828
* ```ts
2929
* import { facilitator } from "thirdweb/x402";
3030
* import { createThirdwebClient } from "thirdweb";
31+
* import { paymentMiddleware } from 'x402-hono'
3132
*
3233
* const client = createThirdwebClient({
3334
* secretKey: "your-secret-key",

packages/thirdweb/src/x402/fetchWithPayment.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ export function wrapFetchWithPayment(
8484
);
8585

8686
if (BigInt(selectedPaymentRequirements.maxAmountRequired) > maxValue) {
87-
throw new Error("Payment amount exceeds maximum allowed");
87+
throw new Error(
88+
`Payment amount exceeds maximum allowed (currently set to ${maxValue} in base units)`,
89+
);
8890
}
8991

9092
const paymentChainId = networkToChainId(

packages/thirdweb/src/x402/schemas.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SettleResponseSchema,
88
} from "x402/types";
99
import { z } from "zod";
10+
import type { Chain } from "../chains/types.js";
1011

1112
const FacilitatorNetworkSchema = z.union([
1213
z.literal("base-sepolia"),
@@ -55,7 +56,10 @@ export type FacilitatorSettleResponse = z.infer<
5556
typeof FacilitatorSettleResponseSchema
5657
>;
5758

58-
export function networkToChainId(network: string): number {
59+
export function networkToChainId(network: string | Chain): number {
60+
if (typeof network === "object") {
61+
return network.id;
62+
}
5963
if (network.startsWith("eip155:")) {
6064
const chainId = parseInt(network.split(":")[1] ?? "0");
6165
if (!Number.isNaN(chainId) && chainId > 0) {

packages/thirdweb/src/x402/settle-payment.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
* // Usage in a Next.js API route
2626
* import { settlePayment, facilitator } from "thirdweb/x402";
2727
* import { createThirdwebClient } from "thirdweb";
28+
* import { arbitrumSepolia } from "thirdweb/chains";
2829
*
2930
* const client = createThirdwebClient({
3031
* secretKey: process.env.THIRDWEB_SECRET_KEY,
@@ -44,7 +45,7 @@ import {
4445
* method: "GET",
4546
* paymentData,
4647
* payTo: "0x1234567890123456789012345678901234567890",
47-
* network: "eip155:84532", // CAIP2 format: "eip155:<chain_id>"
48+
* network: arbitrumSepolia, // or any other chain
4849
* price: "$0.10", // or { amount: "100000", asset: { address: "0x...", decimals: 6 } }
4950
* facilitator: thirdwebFacilitator,
5051
* routeConfig: {
@@ -74,6 +75,7 @@ import {
7475
* import express from "express";
7576
* import { settlePayment, facilitator } from "thirdweb/x402";
7677
* import { createThirdwebClient } from "thirdweb";
78+
* import { arbitrumSepolia } from "thirdweb/chains";
7779
*
7880
* const client = createThirdwebClient({
7981
* secretKey: process.env.THIRDWEB_SECRET_KEY,
@@ -93,7 +95,7 @@ import {
9395
* method: req.method,
9496
* paymentData: req.headers["x-payment"],
9597
* payTo: "0x1234567890123456789012345678901234567890",
96-
* network: "eip155:8453", // CAIP2 format: "eip155:<chain_id>"
98+
* network: arbitrumSepolia, // or any other chain
9799
* price: "$0.05",
98100
* facilitator: thirdwebFacilitator,
99101
* });
@@ -136,7 +138,6 @@ export async function settlePayment(
136138
const { selectedPaymentRequirements, decodedPayment, paymentRequirements } =
137139
decodePaymentResult;
138140

139-
// Settle payment
140141
try {
141142
const settlement = await facilitator.settle(
142143
decodedPayment,

packages/thirdweb/src/x402/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
Money,
44
PaymentMiddlewareConfig,
55
} from "x402/types";
6+
import type { Chain } from "../chains/types.js";
67
import type { Address } from "../utils/address.js";
78
import type { Prettify } from "../utils/type-utils.js";
89
import type { facilitator as facilitatorType } from "./facilitator.js";
@@ -30,7 +31,7 @@ export type PaymentArgs = {
3031
/** The wallet address that should receive the payment */
3132
payTo: Address;
3233
/** The blockchain network where the payment should be processed */
33-
network: FacilitatorNetwork;
34+
network: FacilitatorNetwork | Chain;
3435
/** The price for accessing the resource - either a USD amount (e.g., "$0.10") or a specific token amount */
3536
price: Money | ERC20TokenAmount;
3637
/** The payment facilitator instance used to verify and settle payments */

0 commit comments

Comments
 (0)