Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5249cb7

Browse files
[SDK] Add verifyPayment() backend utility for arbitrary chain x402 payments (#8091)
1 parent b37ed57 commit 5249cb7

File tree

15 files changed

+1136
-470
lines changed

15 files changed

+1136
-470
lines changed

.changeset/some-moons-burn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Accept arbitrary chain ids for x402 payments with new verifyPayment() backend utility

apps/playground-web/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
"thirdweb": "workspace:*",
4949
"use-debounce": "^10.0.5",
5050
"use-stick-to-bottom": "^1.1.1",
51-
"x402-next": "^0.6.1",
5251
"zod": "3.25.75"
5352
},
5453
"devDependencies": {

apps/playground-web/src/app/payments/x402/components/x402-client-preview.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useMutation } from "@tanstack/react-query";
44
import { CodeClient } from "@workspace/ui/components/code/code.client";
55
import { CodeIcon, LockIcon } from "lucide-react";
6-
import { baseSepolia } from "thirdweb/chains";
6+
import { arbitrumSepolia } from "thirdweb/chains";
77
import {
88
ConnectButton,
99
getDefaultToken,
@@ -15,7 +15,7 @@ import { Button } from "@/components/ui/button";
1515
import { Card } from "@/components/ui/card";
1616
import { THIRDWEB_CLIENT } from "../../../../lib/client";
1717

18-
const chain = baseSepolia;
18+
const chain = arbitrumSepolia;
1919
const token = getDefaultToken(chain, "USDC");
2020

2121
export function X402ClientPreview() {

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

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ function ServerCodeExample() {
4646
Next.js Server Code Example
4747
</h2>
4848
<p className="max-w-4xl text-muted-foreground text-balance text-sm md:text-base">
49-
Use any x402 middleware + the thirdweb facilitator to settle
50-
transactions with our server wallet.
49+
Create middleware with the thirdweb facilitator to settle transactions
50+
with your server wallet.
5151
</p>
5252
</div>
5353
<div className="overflow-hidden rounded-lg border bg-card">
@@ -57,28 +57,45 @@ function ServerCodeExample() {
5757
className="h-full rounded-none border-none"
5858
code={`// src/middleware.ts
5959
60-
import { facilitator } from "thirdweb/x402";
60+
import { facilitator, verifyPayment } from "thirdweb/x402";
6161
import { createThirdwebClient } from "thirdweb";
6262
import { paymentMiddleware } from "x402-next";
6363
6464
const client = createThirdwebClient({ secretKey: "your-secret-key" });
65+
const thirdwebX402Facilitator = facilitator({
66+
client,
67+
serverWalletAddress: "0xYourWalletAddress",
68+
});
69+
70+
export async function middleware(request: NextRequest) {
71+
const method = request.method.toUpperCase();
72+
const resourceUrl = request.nextUrl.toString();
73+
const paymentData = request.headers.get("X-PAYMENT");
6574
66-
export const middleware = paymentMiddleware(
67-
"0xYourWalletAddress",
68-
{
69-
"/api/paid-endpoint": {
70-
price: "$0.01",
71-
network: "base-sepolia",
72-
config: {
73-
description: "Access to paid content",
74-
},
75+
const result = await verifyPayment({
76+
resourceUrl,
77+
method,
78+
paymentData,
79+
payTo: "0xYourWalletAddress",
80+
network: "eip155:11155111", // or any other chain id
81+
price: "$0.01", // can also be a ERC20 token amount
82+
routeConfig: {
83+
description: "Access to paid content",
7584
},
76-
},
77-
facilitator({
78-
client,
79-
serverWalletAddress: "0xYourServerWalletAddress",
80-
}),
81-
);
85+
facilitator: thirdwebX402Facilitator,
86+
});
87+
88+
if (result.status === 200) {
89+
// payment successful, execute the request
90+
return NextResponse.next();
91+
}
92+
93+
// otherwise, request payment
94+
return NextResponse.json(result.responseBody, {
95+
status: result.status,
96+
headers: result.responseHeaders,
97+
});
98+
}
8299
83100
// Configure which paths the middleware should run on
84101
export const config = {

apps/playground-web/src/middleware.ts

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,59 @@
1+
import { type NextRequest, NextResponse } from "next/server";
12
import { createThirdwebClient } from "thirdweb";
2-
import { facilitator } from "thirdweb/x402";
3-
import { paymentMiddleware } from "x402-next";
3+
import { arbitrumSepolia } from "thirdweb/chains";
4+
import { facilitator, verifyPayment } from "thirdweb/x402";
45

56
const client = createThirdwebClient({
67
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
78
});
89

10+
const chain = arbitrumSepolia;
911
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;
1012
const ENGINE_VAULT_ACCESS_TOKEN = process.env
1113
.ENGINE_VAULT_ACCESS_TOKEN as string;
1214
const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
1315

14-
export const middleware = paymentMiddleware(
15-
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
16-
{
17-
"/api/paywall": {
18-
price: "$0.01",
19-
network: "base-sepolia",
20-
config: {
21-
description: "Access to paid content",
22-
},
16+
const twFacilitator = facilitator({
17+
baseUrl: `${API_URL}/v1/payments/x402`,
18+
client,
19+
serverWalletAddress: BACKEND_WALLET_ADDRESS,
20+
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
21+
});
22+
23+
export async function middleware(request: NextRequest) {
24+
const pathname = request.nextUrl.pathname;
25+
const method = request.method.toUpperCase();
26+
const resourceUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}${pathname}`;
27+
const paymentData = request.headers.get("X-PAYMENT");
28+
29+
const result = await verifyPayment({
30+
resourceUrl,
31+
method,
32+
paymentData,
33+
payTo: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
34+
network: `eip155:${chain.id}`,
35+
price: "$0.01",
36+
routeConfig: {
37+
description: "Access to paid content",
2338
},
24-
},
25-
facilitator({
26-
baseUrl: `${API_URL}/v1/payments/x402`,
27-
client,
28-
serverWalletAddress: BACKEND_WALLET_ADDRESS,
29-
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
30-
}),
31-
);
39+
facilitator: twFacilitator,
40+
});
41+
42+
if (result.status === 200) {
43+
// payment successful, execute the request
44+
const response = NextResponse.next();
45+
for (const [key, value] of Object.entries(result.responseHeaders)) {
46+
response.headers.set(key, value);
47+
}
48+
return response;
49+
}
50+
51+
// otherwise, request payment
52+
return NextResponse.json(result.responseBody, {
53+
status: result.status,
54+
headers: result.responseHeaders,
55+
});
56+
}
3257

3358
// Configure which paths the middleware should run on
3459
export const config = {

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

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -41,37 +41,57 @@ const response = await fetchWithPay('https://api.example.com/paid-endpoint');
4141

4242
## Server Side
4343

44-
To make your API calls payable, you can use any x402 middleware library like x402-hono, x402-next, x402-express, etc.
44+
To make your API calls payable, you can use the `verifyPayment` function in a simple middleware or in your endpoint directly.
4545

46-
Then, use the `facilitator` configuratino function settle transactions with your thirdweb server wallet gaslessly and pass it to the middleware.
46+
Use the `facilitator` configuration function settle transactions with your thirdweb server wallet gaslessly and pass it to the `verifyPayment` function.
4747

48-
Here's an example with Next.js:
48+
Here's an example with a Next.js middleware:
4949

5050
```typescript
5151
import { createThirdwebClient } from "thirdweb";
52-
import { facilitator } from "thirdweb/x402";
52+
import { facilitator, verifyPayment } from "thirdweb/x402";
5353
import { paymentMiddleware } from "x402-next";
5454

55-
const client = createThirdwebClient({
56-
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
55+
const client = createThirdwebClient({ secretKey: "your-secret-key" });
56+
const thirdwebX402Facilitator = facilitator({
57+
client,
58+
serverWalletAddress: "0xYourWalletAddress",
5759
});
5860

59-
export const middleware = paymentMiddleware(
60-
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
61-
{
62-
"/api/paid-endpoint": {
63-
price: "$0.01",
64-
network: "base-sepolia",
65-
config: {
66-
description: "Access to paid content",
67-
},
61+
export async function middleware(request: NextRequest) {
62+
const method = request.method.toUpperCase();
63+
const resourceUrl = request.nextUrl.toString();
64+
const paymentData = request.headers.get("X-PAYMENT");
65+
66+
const result = await verifyPayment({
67+
resourceUrl,
68+
method,
69+
paymentData,
70+
payTo: "0xYourWalletAddress",
71+
network: "eip155:1", // or any other chain id in CAIP2 format: "eip155:<chain_id>"
72+
price: "$0.01", // can also be a ERC20 token amount
73+
routeConfig: {
74+
description: "Access to paid content",
6875
},
69-
},
70-
facilitator({
71-
client,
72-
serverWalletAddress: "0x1234567890123456789012345678901234567890",
73-
}),
74-
);
76+
facilitator: thirdwebX402Facilitator,
77+
});
78+
79+
if (result.status === 200) {
80+
// payment successful, execute the request
81+
const response = NextResponse.next();
82+
// optionally set the response headers back to the client
83+
for (const [key, value] of Object.entries(result.responseHeaders)) {
84+
response.headers.set(key, value);
85+
}
86+
return response;
87+
}
88+
89+
// otherwise, request payment
90+
return NextResponse.json(result.responseBody, {
91+
status: result.status,
92+
headers: result.responseHeaders,
93+
});
94+
}
7595

7696
// Configure which paths the middleware should run on
7797
export const config = {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
export { decodePayment, encodePayment } from "../x402/encode.js";
12
export {
23
facilitator,
34
type ThirdwebX402FacilitatorConfig,
45
} from "../x402/facilitator.js";
56
export { wrapFetchWithPayment } from "../x402/fetchWithPayment.js";
7+
export {
8+
type VerifyPaymentArgs,
9+
type VerifyPaymentResult,
10+
verifyPayment,
11+
} from "../x402/verify-payment.js";

packages/thirdweb/src/react/core/utils/defaultTokens.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -289,15 +289,9 @@ const DEFAULT_TOKENS = {
289289
symbol: "USDC",
290290
},
291291
],
292-
"421613": [
292+
"421614": [
293293
{
294-
address: "0xe39Ab88f8A4777030A534146A9Ca3B52bd5D43A3",
295-
icon: wrappedEthIcon,
296-
name: "Wrapped Ether",
297-
symbol: "WETH",
298-
},
299-
{
300-
address: "0xfd064A18f3BF249cf1f87FC203E90D8f650f2d63",
294+
address: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
301295
icon: usdcIcon,
302296
name: "USD Coin",
303297
symbol: "USDC",
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { ExactEvmPayload } from "x402/types";
2+
import {
3+
type RequestedPaymentPayload,
4+
RequestedPaymentPayloadSchema,
5+
} from "./schemas.js";
6+
7+
/**
8+
* Encodes a payment payload into a base64 string, ensuring bigint values are properly stringified
9+
*
10+
* @param payment - The payment payload to encode
11+
* @returns A base64 encoded string representation of the payment payload
12+
*/
13+
export function encodePayment(payment: RequestedPaymentPayload): string {
14+
let safe: RequestedPaymentPayload;
15+
16+
// evm
17+
const evmPayload = payment.payload as ExactEvmPayload;
18+
safe = {
19+
...payment,
20+
payload: {
21+
...evmPayload,
22+
authorization: Object.fromEntries(
23+
Object.entries(evmPayload.authorization).map(([key, value]) => [
24+
key,
25+
typeof value === "bigint" ? (value as bigint).toString() : value,
26+
]),
27+
) as ExactEvmPayload["authorization"],
28+
},
29+
};
30+
return safeBase64Encode(JSON.stringify(safe));
31+
}
32+
33+
/**
34+
* Decodes a base64 encoded payment string back into a PaymentPayload object
35+
*
36+
* @param payment - The base64 encoded payment string to decode
37+
* @returns The decoded and validated PaymentPayload object
38+
*/
39+
export function decodePayment(payment: string): RequestedPaymentPayload {
40+
const decoded = safeBase64Decode(payment);
41+
const parsed = JSON.parse(decoded);
42+
43+
const obj: RequestedPaymentPayload = {
44+
...parsed,
45+
payload: parsed.payload as ExactEvmPayload,
46+
};
47+
const validated = RequestedPaymentPayloadSchema.parse(obj);
48+
return validated;
49+
}
50+
51+
/**
52+
* Encodes a string to base64 format
53+
*
54+
* @param data - The string to be encoded to base64
55+
* @returns The base64 encoded string
56+
*/
57+
export function safeBase64Encode(data: string): string {
58+
if (
59+
typeof globalThis !== "undefined" &&
60+
typeof globalThis.btoa === "function"
61+
) {
62+
return globalThis.btoa(data);
63+
}
64+
return Buffer.from(data).toString("base64");
65+
}
66+
67+
/**
68+
* Decodes a base64 string back to its original format
69+
*
70+
* @param data - The base64 encoded string to be decoded
71+
* @returns The decoded string in UTF-8 format
72+
*/
73+
function safeBase64Decode(data: string): string {
74+
if (
75+
typeof globalThis !== "undefined" &&
76+
typeof globalThis.atob === "function"
77+
) {
78+
return globalThis.atob(data);
79+
}
80+
return Buffer.from(data, "base64").toString("utf-8");
81+
}

0 commit comments

Comments
 (0)