[SDK] Add x402 payment protocol utilities (#8076)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"thirdweb": minor
|
||||
---
|
||||
|
||||
x402 utilities
|
||||
@@ -48,6 +48,7 @@
|
||||
"thirdweb": "workspace:*",
|
||||
"use-debounce": "^10.0.5",
|
||||
"use-stick-to-bottom": "^1.1.1",
|
||||
"x402-next": "^0.6.1",
|
||||
"zod": "3.25.75"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NextResponse } from "next/server";
|
||||
// Allow streaming responses up to 5 minutes
|
||||
export const maxDuration = 300;
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Congratulations! You have accessed the protected route.",
|
||||
});
|
||||
}
|
||||
@@ -206,6 +206,10 @@ const payments: ShadcnSidebarLink = {
|
||||
href: "/payments/transactions",
|
||||
label: "Onchain Transaction",
|
||||
},
|
||||
{
|
||||
href: "/payments/x402",
|
||||
label: "x402",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { OverviewPage } from "@/components/blocks/OverviewPage";
|
||||
import { PayIcon } from "../../icons/PayIcon";
|
||||
import { paymentsFeatureCards } from "../data/pages-metadata";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<OverviewPage
|
||||
icon={PayIcon}
|
||||
title="Payments"
|
||||
description={
|
||||
<>
|
||||
Allow developers and users to receive and spend any token on any EVM
|
||||
chain
|
||||
</>
|
||||
}
|
||||
featureCards={paymentsFeatureCards}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { CodeClient } from "@workspace/ui/components/code/code.client";
|
||||
import { CodeIcon, LockIcon } from "lucide-react";
|
||||
import { baseSepolia } 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 = baseSepolia;
|
||||
const token = getDefaultToken(chain, "USDC");
|
||||
|
||||
export function X402ClientPreview() {
|
||||
const activeWallet = useActiveWallet();
|
||||
const activeAccount = useActiveAccount();
|
||||
const paidApiCall = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!activeWallet) {
|
||||
throw new Error("No active wallet");
|
||||
}
|
||||
const fetchWithPay = wrapFetchWithPayment(
|
||||
fetch,
|
||||
THIRDWEB_CLIENT,
|
||||
activeWallet,
|
||||
);
|
||||
const response = await fetchWithPay("/api/paywall");
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
|
||||
const handlePayClick = async () => {
|
||||
paidApiCall.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full p-4 md:p-12 max-w-lg mx-auto">
|
||||
<ConnectButton
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={chain}
|
||||
detailsButton={{
|
||||
displayBalanceToken: {
|
||||
[chain.id]: token!.address,
|
||||
},
|
||||
}}
|
||||
supportedTokens={{
|
||||
[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>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handlePayClick}
|
||||
className="w-full mb-4"
|
||||
size="lg"
|
||||
disabled={paidApiCall.isPending || !activeAccount}
|
||||
>
|
||||
Pay Now
|
||||
</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>
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<CodeIcon className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-lg font-medium">API Call Response</span>
|
||||
</div>
|
||||
{paidApiCall.isPending && <div className="text-center">Loading...</div>}
|
||||
{paidApiCall.isError && (
|
||||
<div className="text-center">Error: {paidApiCall.error.message}</div>
|
||||
)}
|
||||
{paidApiCall.data && (
|
||||
<CodeClient
|
||||
code={JSON.stringify(paidApiCall.data, null, 2)}
|
||||
lang="json"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { CodeServer } from "@workspace/ui/components/code/code.server";
|
||||
import { CircleDollarSignIcon, Code2Icon } from "lucide-react";
|
||||
import { CodeExample, TabName } from "@/components/code/code-example";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { createMetadata } from "../../../lib/metadata";
|
||||
import { X402ClientPreview } from "./components/x402-client-preview";
|
||||
|
||||
const title = "x402 Payments";
|
||||
const description =
|
||||
"Use the x402 payment protocol to pay for API calls using any web3 wallet.";
|
||||
const ogDescription =
|
||||
"Use the x402 payment protocol to pay for API calls using any web3 wallet.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title,
|
||||
description: ogDescription,
|
||||
image: {
|
||||
icon: "payments",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
icon={CircleDollarSignIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/payments/x402?utm_source=playground"
|
||||
>
|
||||
<X402Example />
|
||||
<div className="h-8" />
|
||||
<ServerCodeExample />
|
||||
</PageLayout>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ServerCodeExample() {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<h2 className="font-semibold text-xl tracking-tight">
|
||||
Next.js Server Code Example
|
||||
</h2>
|
||||
<p className="max-w-4xl text-muted-foreground text-balance text-sm md:text-base">
|
||||
Use any x402 middleware + the thirdweb facilitator to settle
|
||||
transactions with our server wallet.
|
||||
</p>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-lg border bg-card">
|
||||
<div className="flex grow flex-col border-b md:border-r md:border-b-0">
|
||||
<TabName icon={Code2Icon} name="Server Code" />
|
||||
<CodeServer
|
||||
className="h-full rounded-none border-none"
|
||||
code={`// src/middleware.ts
|
||||
|
||||
import { facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { paymentMiddleware } from "x402-next";
|
||||
|
||||
const client = createThirdwebClient({ secretKey: "your-secret-key" });
|
||||
|
||||
export const middleware = paymentMiddleware(
|
||||
"0xYourWalletAddress",
|
||||
{
|
||||
"/api/paid-endpoint": {
|
||||
price: "$0.01",
|
||||
network: "base-sepolia",
|
||||
config: {
|
||||
description: "Access to paid content",
|
||||
},
|
||||
},
|
||||
},
|
||||
facilitator({
|
||||
client,
|
||||
serverWalletAddress: "0xYourServerWalletAddress",
|
||||
}),
|
||||
);
|
||||
|
||||
// Configure which paths the middleware should run on
|
||||
export const config = {
|
||||
matcher: ["/api/paid-endpoint"],
|
||||
};
|
||||
|
||||
`}
|
||||
lang="tsx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function X402Example() {
|
||||
return (
|
||||
<CodeExample
|
||||
header={{
|
||||
title: "Client Code Example",
|
||||
description:
|
||||
"Wrap your fetch requests with the `wrapFetchWithPayment` function to enable x402 payments.",
|
||||
}}
|
||||
code={`import { createThirdwebClient } from "thirdweb";
|
||||
import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
import { useActiveWallet } from "thirdweb/react";
|
||||
|
||||
const client = createThirdwebClient({ clientId: "your-client-id" });
|
||||
|
||||
export default function Page() {
|
||||
const wallet = useActiveWallet();
|
||||
|
||||
const onClick = async () => {
|
||||
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
|
||||
const response = await fetchWithPay('/api/paid-endpoint');
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={onClick}>Pay Now</Button>
|
||||
);
|
||||
}`}
|
||||
lang="tsx"
|
||||
preview={<X402ClientPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { facilitator } from "thirdweb/x402";
|
||||
import { paymentMiddleware } from "x402-next";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
|
||||
});
|
||||
|
||||
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_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"}`;
|
||||
|
||||
export const middleware = paymentMiddleware(
|
||||
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
|
||||
{
|
||||
"/api/paywall": {
|
||||
price: "$0.01",
|
||||
network: "base-sepolia",
|
||||
config: {
|
||||
description: "Access to paid content",
|
||||
},
|
||||
},
|
||||
},
|
||||
facilitator({
|
||||
baseUrl: `${API_URL}/v1/payments/x402`,
|
||||
client,
|
||||
serverWalletAddress: BACKEND_WALLET_ADDRESS,
|
||||
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
|
||||
}),
|
||||
);
|
||||
|
||||
// Configure which paths the middleware should run on
|
||||
export const config = {
|
||||
matcher: ["/api/paywall"],
|
||||
};
|
||||
@@ -61,6 +61,10 @@ export const sidebar: SideBar = {
|
||||
href: `${paymentsSlug}/custom-data`,
|
||||
name: "Custom Data",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/x402`,
|
||||
name: "x402",
|
||||
},
|
||||
],
|
||||
name: "Guides",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ArticleIconCard } from "@doc";
|
||||
import { ReactIcon } from "@/icons";
|
||||
|
||||
# x402 payments
|
||||
|
||||
Implement paid API calls using the x402 protocol. Every request is paid for by the user with a micro payment onchain.
|
||||
|
||||
<ArticleIconCard
|
||||
title="x402 Playground"
|
||||
description="Try out a x402 payment in our live playground"
|
||||
icon={ReactIcon}
|
||||
href="https://playground.thirdweb.com/payments/x402"
|
||||
/>
|
||||
|
||||
## Client Side
|
||||
|
||||
`wrapFetchWithPayment` wraps the native fetch API to automatically handle `402 Payment Required` responses from any API call. It will:
|
||||
1. Make the initial request
|
||||
2. If a 402 response is received, parse the payment requirements
|
||||
3. Verify the payment amount is within the allowed maximum
|
||||
4. Sign a payment authorization
|
||||
5. Create a payment header using the provided wallet signature
|
||||
6. Retry the request with the payment header
|
||||
|
||||
Here's an example:
|
||||
|
||||
```typescript
|
||||
import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { createWallet } from "thirdweb/wallets";
|
||||
|
||||
const client = createThirdwebClient({ clientId: "your-client-id" });
|
||||
const wallet = createWallet("io.metamask"); // or any other wallet
|
||||
await wallet.connect({ client })
|
||||
|
||||
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
|
||||
|
||||
// Make a request that may require payment
|
||||
const response = await fetchWithPay('https://api.example.com/paid-endpoint');
|
||||
```
|
||||
|
||||
## Server Side
|
||||
|
||||
To make your API calls payable, you can use any x402 middleware library like x402-hono, x402-next, x402-express, etc.
|
||||
|
||||
Then, use the `facilitator` configuratino function settle transactions with your thirdweb server wallet gaslessly and pass it to the middleware.
|
||||
|
||||
Here's an example with Next.js:
|
||||
|
||||
```typescript
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { facilitator } from "thirdweb/x402";
|
||||
import { paymentMiddleware } from "x402-next";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
|
||||
});
|
||||
|
||||
export const middleware = paymentMiddleware(
|
||||
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
|
||||
{
|
||||
"/api/paid-endpoint": {
|
||||
price: "$0.01",
|
||||
network: "base-sepolia",
|
||||
config: {
|
||||
description: "Access to paid content",
|
||||
},
|
||||
},
|
||||
},
|
||||
facilitator({
|
||||
client,
|
||||
serverWalletAddress: "0x1234567890123456789012345678901234567890",
|
||||
}),
|
||||
);
|
||||
|
||||
// Configure which paths the middleware should run on
|
||||
export const config = {
|
||||
matcher: ["/api/paid-endpoint"],
|
||||
};
|
||||
```
|
||||
@@ -40,6 +40,7 @@
|
||||
"toml": "3.0.0",
|
||||
"uqr": "0.1.2",
|
||||
"viem": "2.33.2",
|
||||
"x402": "0.6.1",
|
||||
"zod": "3.25.75"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -226,6 +227,11 @@
|
||||
"react-native": "./dist/esm/exports/wallets/in-app.native.js",
|
||||
"import": "./dist/esm/exports/wallets/in-app.js",
|
||||
"default": "./dist/cjs/exports/wallets/in-app.js"
|
||||
},
|
||||
"./x402": {
|
||||
"types": "./dist/types/exports/x402.d.ts",
|
||||
"import": "./dist/esm/exports/x402.js",
|
||||
"default": "./dist/cjs/exports/x402.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -414,6 +420,9 @@
|
||||
],
|
||||
"insight": [
|
||||
"./dist/types/exports/insight.d.ts"
|
||||
],
|
||||
"x402": [
|
||||
"./dist/types/exports/x402.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
facilitator,
|
||||
type ThirdwebX402FacilitatorConfig,
|
||||
} from "../x402/facilitator.js";
|
||||
export { wrapFetchWithPayment } from "../x402/fetchWithPayment.js";
|
||||
@@ -322,7 +322,7 @@ type UIOptionsResult =
|
||||
*
|
||||
* Refer to the [`BuyWidgetConnectOptions`](https://portal.thirdweb.com/references/typescript/v5/BuyWidgetConnectOptions) type for more details.
|
||||
*
|
||||
* @bridge Widgets
|
||||
* @bridge
|
||||
*/
|
||||
export function BuyWidget(props: BuyWidgetProps) {
|
||||
const localeQuery = useConnectLocale(props.locale || "en_US");
|
||||
|
||||
@@ -318,7 +318,7 @@ type UIOptionsResult =
|
||||
*
|
||||
* Refer to the [`CheckoutWidgetConnectOptions`](https://portal.thirdweb.com/references/typescript/v5/CheckoutWidgetConnectOptions) type for more details.
|
||||
*
|
||||
* @bridge Widgets
|
||||
* @bridge
|
||||
*/
|
||||
export function CheckoutWidget(props: CheckoutWidgetProps) {
|
||||
const localeQuery = useConnectLocale(props.locale || "en_US");
|
||||
|
||||
@@ -326,7 +326,7 @@ type UIOptionsResult =
|
||||
*
|
||||
* Refer to the [`TransactionWidgetConnectOptions`](https://portal.thirdweb.com/references/typescript/v5/TransactionWidgetConnectOptions) type for more details.
|
||||
*
|
||||
* @bridge Widgets
|
||||
* @bridge
|
||||
*/
|
||||
export function TransactionWidget(props: TransactionWidgetProps) {
|
||||
const localeQuery = useConnectLocale(props.locale || "en_US");
|
||||
|
||||
@@ -234,6 +234,7 @@ export type SwapWidgetProps = {
|
||||
* }} />
|
||||
* ```
|
||||
*
|
||||
* @bridge
|
||||
*/
|
||||
export function SwapWidget(props: SwapWidgetProps) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { FacilitatorConfig } from "x402/types";
|
||||
import type { ThirdwebClient } from "../client/client.js";
|
||||
|
||||
export type ThirdwebX402FacilitatorConfig = {
|
||||
client: ThirdwebClient;
|
||||
serverWalletAddress: string;
|
||||
vaultAccessToken?: string;
|
||||
baseUrl?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402";
|
||||
|
||||
/**
|
||||
* Creates a facilitator for the x402 payment protocol.
|
||||
* Use this with any x402 middleware to enable settling transactions with your thirdweb server wallet.
|
||||
*
|
||||
* @param config - The configuration for the facilitator
|
||||
* @returns a x402 compatible FacilitatorConfig
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { facilitator } from "thirdweb/x402";
|
||||
* import { createThirdwebClient } from "thirdweb";
|
||||
*
|
||||
* const client = createThirdwebClient({
|
||||
* secretKey: "your-secret-key",
|
||||
* });
|
||||
* const thirdwebX402Facilitator = facilitator({
|
||||
* client: client,
|
||||
* serverWalletAddress: "0x1234567890123456789012345678901234567890",
|
||||
* });
|
||||
*
|
||||
* // add the facilitator to any x402 payment middleware
|
||||
* const middleware = paymentMiddleware(
|
||||
* "0x1234567890123456789012345678901234567890",
|
||||
* {
|
||||
* "/api/paywall": {
|
||||
* price: "$0.01",
|
||||
* network: "base-sepolia",
|
||||
* config: {
|
||||
* description: "Access to paid content",
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* thirdwebX402Facilitator,
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @bridge x402
|
||||
*/
|
||||
export function facilitator(
|
||||
config: ThirdwebX402FacilitatorConfig,
|
||||
): FacilitatorConfig {
|
||||
const secretKey = config.client.secretKey;
|
||||
if (!secretKey) {
|
||||
throw new Error("Client secret key is required for the x402 facilitator");
|
||||
}
|
||||
const serverWalletAddress = config.serverWalletAddress;
|
||||
if (!serverWalletAddress) {
|
||||
throw new Error(
|
||||
"Server wallet address is required for the x402 facilitator",
|
||||
);
|
||||
}
|
||||
return {
|
||||
url: (config.baseUrl ?? DEFAULT_BASE_URL) as `${string}://${string}`,
|
||||
createAuthHeaders: async () => {
|
||||
return {
|
||||
verify: {
|
||||
"x-secret-key": secretKey,
|
||||
},
|
||||
settle: {
|
||||
"x-secret-key": secretKey,
|
||||
"x-settlement-wallet-address": serverWalletAddress,
|
||||
...(config.vaultAccessToken
|
||||
? { "x-vault-access-token": config.vaultAccessToken }
|
||||
: {}),
|
||||
},
|
||||
supported: {
|
||||
"x-secret-key": secretKey,
|
||||
},
|
||||
list: {
|
||||
"x-secret-key": secretKey,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { createPaymentHeader } from "x402/client";
|
||||
import {
|
||||
ChainIdToNetwork,
|
||||
EvmNetworkToChainId,
|
||||
type PaymentRequirements,
|
||||
PaymentRequirementsSchema,
|
||||
type Signer,
|
||||
} from "x402/types";
|
||||
import { viemAdapter } from "../adapters/viem.js";
|
||||
import { getCachedChain } from "../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../client/client.js";
|
||||
import type { Wallet } from "../wallets/interfaces/wallet.js";
|
||||
|
||||
/**
|
||||
* Enables the payment of APIs using the x402 payment protocol.
|
||||
*
|
||||
* This function wraps the native fetch API to automatically handle 402 Payment Required responses
|
||||
* by creating and sending a payment header. It will:
|
||||
* 1. Make the initial request
|
||||
* 2. If a 402 response is received, parse the payment requirements
|
||||
* 3. Verify the payment amount is within the allowed maximum
|
||||
* 4. Create a payment header using the provided wallet client
|
||||
* 5. Retry the request with the payment header
|
||||
*
|
||||
* @param fetch - The fetch function to wrap (typically globalThis.fetch)
|
||||
* @param client - The thirdweb client used to access RPC infrastructure
|
||||
* @param wallet - The wallet used to sign payment messages
|
||||
* @param maxValue - The maximum allowed payment amount in base units (defaults to 1 USDC)
|
||||
* @returns A wrapped fetch function that handles 402 responses automatically
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
* import { createThirdwebClient } from "thirdweb";
|
||||
* import { createWallet } from "thirdweb/wallets";
|
||||
*
|
||||
* const client = createThirdwebClient({ clientId: "your-client-id" });
|
||||
* const wallet = createWallet("io.metamask");
|
||||
* await wallet.connect({ client })
|
||||
*
|
||||
* const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
|
||||
*
|
||||
* // Make a request that may require payment
|
||||
* const response = await fetchWithPay('https://api.example.com/paid-endpoint');
|
||||
* ```
|
||||
*
|
||||
* @throws {Error} If the payment amount exceeds the maximum allowed value
|
||||
* @throws {Error} If a payment has already been attempted for this request
|
||||
* @throws {Error} If there's an error creating the payment header
|
||||
*
|
||||
* @bridge x402
|
||||
*/
|
||||
export function wrapFetchWithPayment(
|
||||
fetch: typeof globalThis.fetch,
|
||||
client: ThirdwebClient,
|
||||
wallet: Wallet,
|
||||
maxValue: bigint = BigInt(1 * 10 ** 6), // Default to 1 USDC
|
||||
) {
|
||||
return async (input: RequestInfo, init?: RequestInit) => {
|
||||
const response = await fetch(input, init);
|
||||
|
||||
if (response.status !== 402) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const { x402Version, accepts } = (await response.json()) as {
|
||||
x402Version: number;
|
||||
accepts: unknown[];
|
||||
};
|
||||
const parsedPaymentRequirements = accepts
|
||||
.map((x) => PaymentRequirementsSchema.parse(x))
|
||||
.filter((x) => x.scheme === "exact"); // TODO (402): accept other schemes
|
||||
|
||||
const account = wallet.getAccount();
|
||||
let chain = wallet.getChain();
|
||||
|
||||
if (!account || !chain) {
|
||||
throw new Error(
|
||||
"Wallet not connected. Please connect your wallet to continue.",
|
||||
);
|
||||
}
|
||||
const selectedPaymentRequirements = defaultPaymentRequirementsSelector(
|
||||
parsedPaymentRequirements,
|
||||
chain.id,
|
||||
"exact",
|
||||
);
|
||||
|
||||
if (BigInt(selectedPaymentRequirements.maxAmountRequired) > maxValue) {
|
||||
throw new Error("Payment amount exceeds maximum allowed");
|
||||
}
|
||||
|
||||
const paymentChainId = EvmNetworkToChainId.get(
|
||||
selectedPaymentRequirements.network,
|
||||
);
|
||||
|
||||
if (!paymentChainId) {
|
||||
throw new Error(
|
||||
`No chain found for the selected payment requirement: ${selectedPaymentRequirements.network}`,
|
||||
);
|
||||
}
|
||||
|
||||
// switch to the payment chain if it's not the current chain
|
||||
if (paymentChainId !== chain.id) {
|
||||
await wallet.switchChain(getCachedChain(paymentChainId));
|
||||
chain = wallet.getChain();
|
||||
if (!chain) {
|
||||
throw new Error(`Failed to switch chain (${paymentChainId})`);
|
||||
}
|
||||
}
|
||||
|
||||
const walletClient = viemAdapter.wallet.toViem({
|
||||
wallet: wallet,
|
||||
chain,
|
||||
client,
|
||||
}) as Signer;
|
||||
|
||||
const paymentHeader = await createPaymentHeader(
|
||||
walletClient,
|
||||
x402Version,
|
||||
selectedPaymentRequirements,
|
||||
);
|
||||
|
||||
const initParams = init || {};
|
||||
|
||||
if ((initParams as { __is402Retry?: boolean }).__is402Retry) {
|
||||
throw new Error("Payment already attempted");
|
||||
}
|
||||
|
||||
const newInit = {
|
||||
...initParams,
|
||||
headers: {
|
||||
...(initParams.headers || {}),
|
||||
"X-PAYMENT": paymentHeader,
|
||||
"Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE",
|
||||
},
|
||||
__is402Retry: true,
|
||||
};
|
||||
|
||||
const secondResponse = await fetch(input, newInit);
|
||||
return secondResponse;
|
||||
};
|
||||
}
|
||||
|
||||
function defaultPaymentRequirementsSelector(
|
||||
paymentRequirements: PaymentRequirements[],
|
||||
chainId: number,
|
||||
scheme: "exact",
|
||||
) {
|
||||
if (!paymentRequirements.length) {
|
||||
throw new Error(
|
||||
"No valid payment requirements found in server 402 response",
|
||||
);
|
||||
}
|
||||
const currentWalletNetwork = ChainIdToNetwork[chainId];
|
||||
// find the payment requirements matching the connected wallet chain
|
||||
const matchingPaymentRequirements = paymentRequirements.find(
|
||||
(x) => x.network === currentWalletNetwork && x.scheme === scheme,
|
||||
);
|
||||
|
||||
if (matchingPaymentRequirements) {
|
||||
return matchingPaymentRequirements;
|
||||
} else {
|
||||
// if no matching payment requirements, use the first payment requirement
|
||||
// and switch the wallet to that chain
|
||||
const firstPaymentRequirement = paymentRequirements.find(
|
||||
(x) => x.scheme === scheme,
|
||||
);
|
||||
if (!firstPaymentRequirement) {
|
||||
throw new Error("No suitable payment requirements found");
|
||||
}
|
||||
return firstPaymentRequirement;
|
||||
}
|
||||
}
|
||||
Generated
+1324
-144
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user