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/polite-banks-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Accept chain objects for x402 APIs
3 changes: 2 additions & 1 deletion apps/playground-web/src/app/payments/x402/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ function ServerCodeExample() {

import { facilitator, settlePayment } from "thirdweb/x402";
import { createThirdwebClient } from "thirdweb";
import { arbitrumSepolia } from "thirdweb/chains";

const client = createThirdwebClient({ secretKey: "your-secret-key" });
const thirdwebX402Facilitator = facilitator({
Expand All @@ -76,7 +77,7 @@ export async function middleware(request: NextRequest) {
method,
paymentData,
payTo: "0xYourWalletAddress",
network: "eip155:11155111", // or any other chain id
network: arbitrumSepolia, // or any other chain
price: "$0.01", // can also be a ERC20 token amount
facilitator: thirdwebX402Facilitator,
});
Expand Down
2 changes: 1 addition & 1 deletion apps/playground-web/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function middleware(request: NextRequest) {
method,
paymentData,
payTo: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
network: `eip155:${chain.id}`,
network: chain,
price: "$0.01",
routeConfig: {
description: "Access to paid content",
Expand Down
3 changes: 2 additions & 1 deletion apps/portal/src/app/payments/x402/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Here's an example with a Next.js middleware:
```typescript
import { createThirdwebClient } from "thirdweb";
import { facilitator, settlePayment } from "thirdweb/x402";
import { arbitrumSepolia } from "thirdweb/chains";

const client = createThirdwebClient({ secretKey: "your-secret-key" });
const thirdwebX402Facilitator = facilitator({
Expand All @@ -67,7 +68,7 @@ export async function middleware(request: NextRequest) {
method,
paymentData,
payTo: "0xYourWalletAddress",
network: "eip155:1", // or any other chain id in CAIP2 format: "eip155:<chain_id>"
network: arbitrumSepolia, // or any other chain
price: "$0.01", // can also be a ERC20 token amount
routeConfig: {
description: "Access to paid content",
Expand Down
98 changes: 46 additions & 52 deletions packages/thirdweb/src/x402/common.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import {
type ERC20TokenAmount,
type Money,
moneySchema,
type Network,
SupportedEVMNetworks,
} from "x402/types";
import { type ERC20TokenAmount, type Money, moneySchema } from "x402/types";
import { getAddress } from "../utils/address.js";
import { decodePayment } from "./encode.js";
import type { facilitator as facilitatorType } from "./facilitator.js";
import {
type FacilitatorNetwork,
networkToChainId,
type RequestedPaymentPayload,
type RequestedPaymentRequirements,
Expand Down Expand Up @@ -54,9 +47,28 @@ export async function decodePaymentRequest(
errorMessages,
discoverable,
} = routeConfig;

let chainId: number;
try {
chainId = networkToChainId(network);
} catch (error) {
return {
status: 402,
responseHeaders: { "Content-Type": "application/json" },
responseBody: {
x402Version,
error:
error instanceof Error
? error.message
: `Invalid network: ${network}`,
accepts: [],
},
};
}

const atomicAmountForAsset = await processPriceToAtomicAmount(
price,
network,
chainId,
facilitator,
);
if ("error" in atomicAmountForAsset) {
Expand All @@ -74,45 +86,28 @@ export async function decodePaymentRequest(

const paymentRequirements: RequestedPaymentRequirements[] = [];

if (
SupportedEVMNetworks.includes(network as Network) ||
network.startsWith("eip155:")
) {
paymentRequirements.push({
scheme: "exact",
network,
maxAmountRequired,
resource: resourceUrl,
description: description ?? "",
mimeType: mimeType ?? "application/json",
payTo: getAddress(payTo),
maxTimeoutSeconds: maxTimeoutSeconds ?? 300,
asset: getAddress(asset.address),
// TODO: Rename outputSchema to requestStructure
outputSchema: {
input: {
type: "http",
method,
discoverable: discoverable ?? true,
...inputSchema,
},
output: outputSchema,
},
extra: (asset as ERC20TokenAmount["asset"]).eip712,
});
} else {
return {
status: 402,
responseHeaders: {
"Content-Type": "application/json",
},
responseBody: {
x402Version,
error: `Unsupported network: ${network}`,
accepts: paymentRequirements,
paymentRequirements.push({
scheme: "exact",
network: `eip155:${chainId}`,
maxAmountRequired,
resource: resourceUrl,
description: description ?? "",
mimeType: mimeType ?? "application/json",
payTo: getAddress(payTo),
maxTimeoutSeconds: maxTimeoutSeconds ?? 300,
asset: getAddress(asset.address),
// TODO: Rename outputSchema to requestStructure
outputSchema: {
input: {
type: "http",
method,
discoverable: discoverable ?? true,
...inputSchema,
},
};
}
output: outputSchema,
},
extra: (asset as ERC20TokenAmount["asset"]).eip712,
});

// Check for payment header
if (!paymentData) {
Expand Down Expand Up @@ -188,7 +183,7 @@ export async function decodePaymentRequest(
*/
async function processPriceToAtomicAmount(
price: Money | ERC20TokenAmount,
network: FacilitatorNetwork,
chainId: number,
facilitator: ReturnType<typeof facilitatorType>,
): Promise<
| { maxAmountRequired: string; asset: ERC20TokenAmount["asset"] }
Expand All @@ -207,10 +202,10 @@ async function processPriceToAtomicAmount(
};
}
const parsedUsdAmount = parsedAmount.data;
const defaultAsset = await getDefaultAsset(network, facilitator);
const defaultAsset = await getDefaultAsset(chainId, facilitator);
if (!defaultAsset) {
return {
error: `Unable to get default asset on ${network}. Please specify an asset in the payment requirements.`,
error: `Unable to get default asset on chain ${chainId}. Please specify an asset in the payment requirements.`,
};
}
asset = defaultAsset;
Expand All @@ -228,11 +223,10 @@ async function processPriceToAtomicAmount(
}

async function getDefaultAsset(
network: FacilitatorNetwork,
chainId: number,
facilitator: ReturnType<typeof facilitatorType>,
): Promise<ERC20TokenAmount["asset"] | undefined> {
const supportedAssets = await facilitator.supported();
const chainId = networkToChainId(network);
const matchingAsset = supportedAssets.kinds.find(
(supported) => supported.network === `eip155:${chainId}`,
);
Expand Down
1 change: 1 addition & 0 deletions packages/thirdweb/src/x402/facilitator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402";
* ```ts
* import { facilitator } from "thirdweb/x402";
* import { createThirdwebClient } from "thirdweb";
* import { paymentMiddleware } from 'x402-hono'
*
* const client = createThirdwebClient({
* secretKey: "your-secret-key",
Expand Down
4 changes: 3 additions & 1 deletion packages/thirdweb/src/x402/fetchWithPayment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ export function wrapFetchWithPayment(
);

if (BigInt(selectedPaymentRequirements.maxAmountRequired) > maxValue) {
throw new Error("Payment amount exceeds maximum allowed");
throw new Error(
`Payment amount exceeds maximum allowed (currently set to ${maxValue} in base units)`,
);
}

const paymentChainId = networkToChainId(
Expand Down
6 changes: 5 additions & 1 deletion packages/thirdweb/src/x402/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SettleResponseSchema,
} from "x402/types";
import { z } from "zod";
import type { Chain } from "../chains/types.js";

const FacilitatorNetworkSchema = z.union([
z.literal("base-sepolia"),
Expand Down Expand Up @@ -55,7 +56,10 @@ export type FacilitatorSettleResponse = z.infer<
typeof FacilitatorSettleResponseSchema
>;

export function networkToChainId(network: string): number {
export function networkToChainId(network: string | Chain): number {
if (typeof network === "object") {
return network.id;
}
if (network.startsWith("eip155:")) {
const chainId = parseInt(network.split(":")[1] ?? "0");
if (!Number.isNaN(chainId) && chainId > 0) {
Expand Down
7 changes: 4 additions & 3 deletions packages/thirdweb/src/x402/settle-payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
* // Usage in a Next.js API route
* import { settlePayment, facilitator } from "thirdweb/x402";
* import { createThirdwebClient } from "thirdweb";
* import { arbitrumSepolia } from "thirdweb/chains";
*
* const client = createThirdwebClient({
* secretKey: process.env.THIRDWEB_SECRET_KEY,
Expand All @@ -44,7 +45,7 @@ import {
* method: "GET",
* paymentData,
* payTo: "0x1234567890123456789012345678901234567890",
* network: "eip155:84532", // CAIP2 format: "eip155:<chain_id>"
* network: arbitrumSepolia, // or any other chain
* price: "$0.10", // or { amount: "100000", asset: { address: "0x...", decimals: 6 } }
* facilitator: thirdwebFacilitator,
* routeConfig: {
Expand Down Expand Up @@ -74,6 +75,7 @@ import {
* import express from "express";
* import { settlePayment, facilitator } from "thirdweb/x402";
* import { createThirdwebClient } from "thirdweb";
* import { arbitrumSepolia } from "thirdweb/chains";
*
* const client = createThirdwebClient({
* secretKey: process.env.THIRDWEB_SECRET_KEY,
Expand All @@ -93,7 +95,7 @@ import {
* method: req.method,
* paymentData: req.headers["x-payment"],
* payTo: "0x1234567890123456789012345678901234567890",
* network: "eip155:8453", // CAIP2 format: "eip155:<chain_id>"
* network: arbitrumSepolia, // or any other chain
* price: "$0.05",
* facilitator: thirdwebFacilitator,
* });
Expand Down Expand Up @@ -136,7 +138,6 @@ export async function settlePayment(
const { selectedPaymentRequirements, decodedPayment, paymentRequirements } =
decodePaymentResult;

// Settle payment
try {
const settlement = await facilitator.settle(
decodedPayment,
Expand Down
3 changes: 2 additions & 1 deletion packages/thirdweb/src/x402/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
Money,
PaymentMiddlewareConfig,
} from "x402/types";
import type { Chain } from "../chains/types.js";
import type { Address } from "../utils/address.js";
import type { Prettify } from "../utils/type-utils.js";
import type { facilitator as facilitatorType } from "./facilitator.js";
Expand Down Expand Up @@ -30,7 +31,7 @@ export type PaymentArgs = {
/** The wallet address that should receive the payment */
payTo: Address;
/** The blockchain network where the payment should be processed */
network: FacilitatorNetwork;
network: FacilitatorNetwork | Chain;
/** The price for accessing the resource - either a USD amount (e.g., "$0.10") or a specific token amount */
price: Money | ERC20TokenAmount;
/** The payment facilitator instance used to verify and settle payments */
Expand Down
3 changes: 2 additions & 1 deletion packages/thirdweb/src/x402/verify-payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
* // Usage in a Next.js API route
* import { verifyPayment, facilitator } from "thirdweb/x402";
* import { createThirdwebClient } from "thirdweb";
* import { arbitrumSepolia } from "thirdweb/chains";
*
* const client = createThirdwebClient({
* secretKey: process.env.THIRDWEB_SECRET_KEY,
Expand All @@ -35,7 +36,7 @@ import {
* method: "GET",
* paymentData,
* payTo: "0x1234567890123456789012345678901234567890",
* network: "eip155:84532", // CAIP2 format: "eip155:<chain_id>"
* network: arbitrumSepolia, // or any other chain
* price: "$0.10", // or { amount: "100000", asset: { address: "0x...", decimals: 6 } }
* facilitator: thirdwebFacilitator,
* routeConfig: {
Expand Down
Loading