Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/giant-suns-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Support ERC-2612 permit for x402 payments
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export function ServerWalletsTableUI({
<PaginationContent>
<PaginationItem>
<Link
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
currentPage > 1 ? currentPage - 1 : 1
}`}
legacyBehavior
Expand All @@ -232,7 +232,7 @@ export function ServerWalletsTableUI({
(pageNumber) => (
<PaginationItem key={`page-${pageNumber}`}>
<Link
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${pageNumber}`}
href={`/team/${teamSlug}/${project.slug}/transactions?page=${pageNumber}`}
passHref
>
<PaginationLink isActive={currentPage === pageNumber}>
Expand All @@ -244,7 +244,7 @@ export function ServerWalletsTableUI({
)}
<PaginationItem>
<Link
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
currentPage < totalPages ? currentPage + 1 : totalPages
}`}
passHref
Expand Down
2 changes: 1 addition & 1 deletion apps/playground-web/src/app/api/paywall/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export const maxDuration = 300;
export async function GET(_req: Request) {
return NextResponse.json({
success: true,
message: "Congratulations! You have accessed the protected route.",
message: "Payment successful. You have accessed the protected route.",
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// const chain = arbitrumSepolia;

import { arbitrumSepolia } from "thirdweb/chains";
import { getDefaultToken } from "thirdweb/react";

export const chain = arbitrumSepolia;
export const token = getDefaultToken(chain, "USDC")!;
// export const chain = base;
// export const token = {
// address: "0x0578d8A44db98B23BF096A382e016e29a5Ce0ffe",
// decimals: 18,
// name: "Higher",
// symbol: "HIGHER",
// version: "1",
// };
// export const token = {
// address: "0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798",
// decimals: 18,
// name: "Stable Coin",
// symbol: "SBC",
// version: "1",
// // primaryType: "Permit",
// }
// export const chain = defineChain(3338);
// export const token = {
// address: "0xbbA60da06c2c5424f03f7434542280FCAd453d10",
// decimals: 6,
// name: "USDC",
// symbol: "USDC",
// version: "2",
// }
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
"use client";

import { useMutation } from "@tanstack/react-query";
import { Badge } from "@workspace/ui/components/badge";
import { CodeClient } from "@workspace/ui/components/code/code.client";
import { CodeIcon, LockIcon } from "lucide-react";
import { arbitrumSepolia } from "thirdweb/chains";
import {
ConnectButton,
getDefaultToken,
useActiveAccount,
useActiveWallet,
} from "thirdweb/react";
import { wrapFetchWithPayment } from "thirdweb/x402";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { THIRDWEB_CLIENT } from "../../../../lib/client";

const chain = arbitrumSepolia;
const token = getDefaultToken(chain, "USDC");
import { chain, token } from "./constants";

export function X402ClientPreview() {
const activeWallet = useActiveWallet();
Expand All @@ -30,8 +27,21 @@ export function X402ClientPreview() {
fetch,
THIRDWEB_CLIENT,
activeWallet,
BigInt(1 * 10 ** 18),
);
const response = await fetchWithPay("/api/paywall");
const searchParams = new URLSearchParams();
searchParams.set("chainId", chain.id.toString());
searchParams.set("payTo", activeWallet.getAccount()?.address || "");
// TODO (402): dynamic from playground config
// if (token) {
// searchParams.set("amount", "0.01");
// searchParams.set("tokenAddress", token.address);
// searchParams.set("decimals", token.decimals.toString());
// }
const url =
"/api/paywall" +
(searchParams.size > 0 ? "?" + searchParams.toString() : "");
const response = await fetchWithPay(url.toString());
return response.json();
},
});
Expand All @@ -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],
}}
/>
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<LockIcon className="w-5 h-5 text-muted-foreground" />
<span className="text-lg font-medium">Paid API Call</span>
<span className="text-xl font-bold text-red-600">$0.01</span>
<Badge variant="success">
<span className="text-xl font-bold">0.1 {token.symbol}</span>
</Badge>
</div>

<Button
Expand All @@ -67,19 +79,25 @@ export function X402ClientPreview() {
size="lg"
disabled={paidApiCall.isPending || !activeAccount}
>
Pay Now
Access Premium Content
</Button>
<p className="text-sm text-muted-foreground">
{" "}
<a
className="underline"
href={"https://faucet.circle.com/"}
target="_blank"
rel="noopener noreferrer"
>
Click here to get USDC on {chain.name}
</a>
Pay for access with {token.symbol} on{" "}
{chain.name || `chain ${chain.id}`}
</p>
{chain.testnet && token.symbol.toLowerCase() === "usdc" && (
<p className="text-sm text-muted-foreground">
{" "}
<a
className="underline"
href={"https://faucet.circle.com/"}
target="_blank"
rel="noopener noreferrer"
>
Click here to get testnet {token.symbol} on {chain.name}
</a>
</p>
)}
</Card>
<Card className="p-6">
<div className="flex items-center gap-3 mb-2">
Expand Down
37 changes: 31 additions & 6 deletions apps/playground-web/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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",
},
Expand Down
3 changes: 3 additions & 0 deletions packages/thirdweb/scripts/generate/abis/erc20/USDC.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes signature)"
]
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
1 change: 1 addition & 0 deletions packages/thirdweb/src/exports/x402.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Loading
Loading