Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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