Skip to content

Commit f152139

Browse files
[SDK] Support ERC-2612 permit for x402 payments (#8128)
1 parent 1630701 commit f152139

File tree

15 files changed

+574
-98
lines changed

15 files changed

+574
-98
lines changed

.changeset/giant-suns-drive.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+
Support ERC-2612 permit for x402 payments

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/wallet-table/wallet-table-ui.client.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ export function ServerWalletsTableUI({
215215
<PaginationContent>
216216
<PaginationItem>
217217
<Link
218-
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
218+
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
219219
currentPage > 1 ? currentPage - 1 : 1
220220
}`}
221221
legacyBehavior
@@ -232,7 +232,7 @@ export function ServerWalletsTableUI({
232232
(pageNumber) => (
233233
<PaginationItem key={`page-${pageNumber}`}>
234234
<Link
235-
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${pageNumber}`}
235+
href={`/team/${teamSlug}/${project.slug}/transactions?page=${pageNumber}`}
236236
passHref
237237
>
238238
<PaginationLink isActive={currentPage === pageNumber}>
@@ -244,7 +244,7 @@ export function ServerWalletsTableUI({
244244
)}
245245
<PaginationItem>
246246
<Link
247-
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
247+
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
248248
currentPage < totalPages ? currentPage + 1 : totalPages
249249
}`}
250250
passHref

apps/playground-web/src/app/api/paywall/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ export const maxDuration = 300;
55
export async function GET(_req: Request) {
66
return NextResponse.json({
77
success: true,
8-
message: "Congratulations! You have accessed the protected route.",
8+
message: "Payment successful. You have accessed the protected route.",
99
});
1010
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// const chain = arbitrumSepolia;
2+
3+
import { arbitrumSepolia } from "thirdweb/chains";
4+
import { getDefaultToken } from "thirdweb/react";
5+
6+
export const chain = arbitrumSepolia;
7+
export const token = getDefaultToken(chain, "USDC")!;
8+
// export const chain = base;
9+
// export const token = {
10+
// address: "0x0578d8A44db98B23BF096A382e016e29a5Ce0ffe",
11+
// decimals: 18,
12+
// name: "Higher",
13+
// symbol: "HIGHER",
14+
// version: "1",
15+
// };
16+
// export const token = {
17+
// address: "0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798",
18+
// decimals: 18,
19+
// name: "Stable Coin",
20+
// symbol: "SBC",
21+
// version: "1",
22+
// // primaryType: "Permit",
23+
// }
24+
// export const chain = defineChain(3338);
25+
// export const token = {
26+
// address: "0xbbA60da06c2c5424f03f7434542280FCAd453d10",
27+
// decimals: 6,
28+
// name: "USDC",
29+
// symbol: "USDC",
30+
// version: "2",
31+
// }

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

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
"use client";
22

33
import { useMutation } from "@tanstack/react-query";
4+
import { Badge } from "@workspace/ui/components/badge";
45
import { CodeClient } from "@workspace/ui/components/code/code.client";
56
import { CodeIcon, LockIcon } from "lucide-react";
6-
import { arbitrumSepolia } from "thirdweb/chains";
77
import {
88
ConnectButton,
9-
getDefaultToken,
109
useActiveAccount,
1110
useActiveWallet,
1211
} from "thirdweb/react";
1312
import { wrapFetchWithPayment } from "thirdweb/x402";
1413
import { Button } from "@/components/ui/button";
1514
import { Card } from "@/components/ui/card";
1615
import { THIRDWEB_CLIENT } from "../../../../lib/client";
17-
18-
const chain = arbitrumSepolia;
19-
const token = getDefaultToken(chain, "USDC");
16+
import { chain, token } from "./constants";
2017

2118
export function X402ClientPreview() {
2219
const activeWallet = useActiveWallet();
@@ -30,8 +27,21 @@ export function X402ClientPreview() {
3027
fetch,
3128
THIRDWEB_CLIENT,
3229
activeWallet,
30+
BigInt(1 * 10 ** 18),
3331
);
34-
const response = await fetchWithPay("/api/paywall");
32+
const searchParams = new URLSearchParams();
33+
searchParams.set("chainId", chain.id.toString());
34+
searchParams.set("payTo", activeWallet.getAccount()?.address || "");
35+
// TODO (402): dynamic from playground config
36+
// if (token) {
37+
// searchParams.set("amount", "0.01");
38+
// searchParams.set("tokenAddress", token.address);
39+
// searchParams.set("decimals", token.decimals.toString());
40+
// }
41+
const url =
42+
"/api/paywall" +
43+
(searchParams.size > 0 ? "?" + searchParams.toString() : "");
44+
const response = await fetchWithPay(url.toString());
3545
return response.json();
3646
},
3747
});
@@ -47,18 +57,20 @@ export function X402ClientPreview() {
4757
chain={chain}
4858
detailsButton={{
4959
displayBalanceToken: {
50-
[chain.id]: token!.address,
60+
[chain.id]: token.address,
5161
},
5262
}}
5363
supportedTokens={{
54-
[chain.id]: [token!],
64+
[chain.id]: [token],
5565
}}
5666
/>
5767
<Card className="p-6">
5868
<div className="flex items-center gap-3 mb-4">
5969
<LockIcon className="w-5 h-5 text-muted-foreground" />
6070
<span className="text-lg font-medium">Paid API Call</span>
61-
<span className="text-xl font-bold text-red-600">$0.01</span>
71+
<Badge variant="success">
72+
<span className="text-xl font-bold">0.1 {token.symbol}</span>
73+
</Badge>
6274
</div>
6375

6476
<Button
@@ -67,19 +79,25 @@ export function X402ClientPreview() {
6779
size="lg"
6880
disabled={paidApiCall.isPending || !activeAccount}
6981
>
70-
Pay Now
82+
Access Premium Content
7183
</Button>
7284
<p className="text-sm text-muted-foreground">
73-
{" "}
74-
<a
75-
className="underline"
76-
href={"https://faucet.circle.com/"}
77-
target="_blank"
78-
rel="noopener noreferrer"
79-
>
80-
Click here to get USDC on {chain.name}
81-
</a>
85+
Pay for access with {token.symbol} on{" "}
86+
{chain.name || `chain ${chain.id}`}
8287
</p>
88+
{chain.testnet && token.symbol.toLowerCase() === "usdc" && (
89+
<p className="text-sm text-muted-foreground">
90+
{" "}
91+
<a
92+
className="underline"
93+
href={"https://faucet.circle.com/"}
94+
target="_blank"
95+
rel="noopener noreferrer"
96+
>
97+
Click here to get testnet {token.symbol} on {chain.name}
98+
</a>
99+
</p>
100+
)}
83101
</Card>
84102
<Card className="p-6">
85103
<div className="flex items-center gap-3 mb-2">

apps/playground-web/src/middleware.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import { type NextRequest, NextResponse } from "next/server";
2-
import { createThirdwebClient } from "thirdweb";
3-
import { arbitrumSepolia } from "thirdweb/chains";
2+
import { createThirdwebClient, defineChain } from "thirdweb";
43
import { facilitator, settlePayment } from "thirdweb/x402";
54

65
const client = createThirdwebClient({
76
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
87
});
98

10-
const chain = arbitrumSepolia;
119
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;
10+
// const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_SMART_WALLET as string;
1211
const ENGINE_VAULT_ACCESS_TOKEN = process.env
1312
.ENGINE_VAULT_ACCESS_TOKEN as string;
1413
const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
15-
1614
const twFacilitator = facilitator({
1715
baseUrl: `${API_URL}/v1/payments/x402`,
1816
client,
@@ -25,14 +23,41 @@ export async function middleware(request: NextRequest) {
2523
const method = request.method.toUpperCase();
2624
const resourceUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}${pathname}`;
2725
const paymentData = request.headers.get("X-PAYMENT");
26+
const queryParams = request.nextUrl.searchParams;
27+
28+
const chainId = queryParams.get("chainId");
29+
const payTo = queryParams.get("payTo");
30+
31+
if (!chainId || !payTo) {
32+
return NextResponse.json(
33+
{ error: "Missing required parameters" },
34+
{ status: 400 },
35+
);
36+
}
37+
38+
// TODO (402): dynamic from playground config
39+
// const amount = queryParams.get("amount");
40+
// const tokenAddress = queryParams.get("tokenAddress");
41+
// const decimals = queryParams.get("decimals");
2842

2943
const result = await settlePayment({
3044
resourceUrl,
3145
method,
3246
paymentData,
33-
payTo: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
34-
network: chain,
47+
payTo: payTo as `0x${string}`,
48+
network: defineChain(Number(chainId)),
3549
price: "$0.01",
50+
// price: {
51+
// amount: toUnits(amount as string, parseInt(decimals as string)).toString(),
52+
// asset: {
53+
// address: tokenAddress as `0x${string}`,
54+
// decimals: decimals ? parseInt(decimals) : token.decimals,
55+
// eip712: {
56+
// name: token.name,
57+
// version: token.version,
58+
// },
59+
// },
60+
// },
3661
routeConfig: {
3762
description: "Access to paid content",
3863
},
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[
2+
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes signature)"
3+
]

packages/thirdweb/scripts/generate/abis/erc7702/MinimalAccount.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,4 @@
2424
"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))",
2525
"function getTransferPoliciesForSigner(address signer) view returns ((address target, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit)[])",
2626
"function isWildcardSigner(address signer) view returns (bool)",
27-
"function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) returns (bytes4)",
28-
"function onERC1155Received(address, address, uint256, uint256, bytes) returns (bytes4)",
29-
"function onERC721Received(address, address, uint256, bytes) returns (bytes4)",
30-
"function supportsInterface(bytes4 interfaceId) view returns (bool)",
31-
"receive() external payable"
3227
]

packages/thirdweb/src/exports/x402.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { decodePayment, encodePayment } from "../x402/encode.js";
22
export {
33
facilitator,
4+
type ThirdwebX402Facilitator,
45
type ThirdwebX402FacilitatorConfig,
56
} from "../x402/facilitator.js";
67
export { wrapFetchWithPayment } from "../x402/fetchWithPayment.js";

0 commit comments

Comments
 (0)