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
2 changes: 1 addition & 1 deletion .changeset/some-moons-burn.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"thirdweb": minor
---

Accept arbitrary chain ids for x402 payments with new verifyPayment() backend utility
Accept arbitrary chain ids for x402 payments with new settlePayment() and verifyPayment() backend utility functions
7 changes: 2 additions & 5 deletions apps/playground-web/src/app/payments/x402/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function ServerCodeExample() {
className="h-full rounded-none border-none"
code={`// src/middleware.ts

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

const client = createThirdwebClient({ secretKey: "your-secret-key" });
Expand All @@ -71,16 +71,13 @@ export async function middleware(request: NextRequest) {
const resourceUrl = request.nextUrl.toString();
const paymentData = request.headers.get("X-PAYMENT");

const result = await verifyPayment({
const result = await settlePayment({
resourceUrl,
method,
paymentData,
payTo: "0xYourWalletAddress",
network: "eip155:11155111", // or any other chain id
price: "$0.01", // can also be a ERC20 token amount
routeConfig: {
description: "Access to paid content",
},
facilitator: thirdwebX402Facilitator,
});

Expand Down
4 changes: 2 additions & 2 deletions apps/playground-web/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type NextRequest, NextResponse } from "next/server";
import { createThirdwebClient } from "thirdweb";
import { arbitrumSepolia } from "thirdweb/chains";
import { facilitator, verifyPayment } from "thirdweb/x402";
import { facilitator, settlePayment } from "thirdweb/x402";

const client = createThirdwebClient({
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
Expand All @@ -26,7 +26,7 @@ export async function middleware(request: NextRequest) {
const resourceUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}${pathname}`;
const paymentData = request.headers.get("X-PAYMENT");

const result = await verifyPayment({
const result = await settlePayment({
resourceUrl,
method,
paymentData,
Expand Down
10 changes: 6 additions & 4 deletions apps/portal/src/app/payments/x402/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ const response = await fetchWithPay('https://api.example.com/paid-endpoint');

## Server Side

To make your API calls payable, you can use the `verifyPayment` function in a simple middleware or in your endpoint directly.
To make your API calls payable, you can use the `settlePayment` function in a middleware or in your endpoint directly.

Use the `facilitator` configuration function settle transactions with your thirdweb server wallet gaslessly and pass it to the `verifyPayment` function.
Use the `facilitator` configuration function settle transactions with your thirdweb server wallet gaslessly and pass it to the `settlePayment` function.

Here's an example with a Next.js middleware:

```typescript
import { createThirdwebClient } from "thirdweb";
import { facilitator, verifyPayment } from "thirdweb/x402";
import { facilitator, settlePayment } from "thirdweb/x402";

const client = createThirdwebClient({ secretKey: "your-secret-key" });
const thirdwebX402Facilitator = facilitator({
Expand All @@ -62,7 +62,7 @@ export async function middleware(request: NextRequest) {
const resourceUrl = request.nextUrl.toString();
const paymentData = request.headers.get("X-PAYMENT");

const result = await verifyPayment({
const result = await settlePayment({
resourceUrl,
method,
paymentData,
Expand Down Expand Up @@ -97,3 +97,5 @@ export const config = {
matcher: ["/api/paid-endpoint"],
};
```

You can also use the `verifyPayment` function to verify the payment before settling it. This lets you do the work that requires payment first and then settle the payment.
12 changes: 7 additions & 5 deletions packages/thirdweb/src/exports/x402.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ export {
type ThirdwebX402FacilitatorConfig,
} from "../x402/facilitator.js";
export { wrapFetchWithPayment } from "../x402/fetchWithPayment.js";
export {
type VerifyPaymentArgs,
type VerifyPaymentResult,
verifyPayment,
} from "../x402/verify-payment.js";
export { settlePayment } from "../x402/settle-payment.js";
export type {
PaymentArgs,
SettlePaymentResult,
VerifyPaymentResult,
} from "../x402/types.js";
export { verifyPayment } from "../x402/verify-payment.js";
242 changes: 242 additions & 0 deletions packages/thirdweb/src/x402/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import {
type ERC20TokenAmount,
type Money,
moneySchema,
type Network,
SupportedEVMNetworks,
} 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,
} from "./schemas.js";
import {
type PaymentArgs,
type PaymentRequiredResult,
x402Version,
} from "./types.js";

type GetPaymentRequirementsResult = {
status: 200;
paymentRequirements: RequestedPaymentRequirements[];
selectedPaymentRequirements: RequestedPaymentRequirements;
decodedPayment: RequestedPaymentPayload;
};

/**
* Decodes a payment request and returns the payment requirements, selected payment requirements, and decoded payment
* @param args
* @returns The payment requirements, selected payment requirements, and decoded payment
*/
export async function decodePaymentRequest(
args: PaymentArgs,
): Promise<GetPaymentRequirementsResult | PaymentRequiredResult> {
const {
price,
network,
facilitator,
resourceUrl,
routeConfig = {},
payTo,
method,
paymentData,
} = args;
const {
description,
mimeType,
maxTimeoutSeconds,
inputSchema,
outputSchema,
errorMessages,
discoverable,
} = routeConfig;
const atomicAmountForAsset = await processPriceToAtomicAmount(
price,
network,
facilitator,
);
if ("error" in atomicAmountForAsset) {
return {
status: 402,
responseHeaders: { "Content-Type": "application/json" },
responseBody: {
x402Version,
error: atomicAmountForAsset.error,
accepts: [],
},
};
}
const { maxAmountRequired, asset } = atomicAmountForAsset;

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,
},
};
}

// Check for payment header
if (!paymentData) {
return {
status: 402,
responseHeaders: {
"Content-Type": "application/json",
},
responseBody: {
x402Version,
error: errorMessages?.paymentRequired || "X-PAYMENT header is required",
accepts: paymentRequirements,
},
};
}

// Verify payment
let decodedPayment: RequestedPaymentPayload;
try {
decodedPayment = decodePayment(paymentData);
decodedPayment.x402Version = x402Version;
} catch (error) {
return {
status: 402,
responseHeaders: {
"Content-Type": "application/json",
},
responseBody: {
x402Version,
error:
errorMessages?.invalidPayment ||
(error instanceof Error ? error.message : "Invalid payment"),
accepts: paymentRequirements,
},
};
}

const selectedPaymentRequirements = paymentRequirements.find(
(value) =>
value.scheme === decodedPayment.scheme &&
value.network === decodedPayment.network,
);
if (!selectedPaymentRequirements) {
return {
status: 402,
responseHeaders: {
"Content-Type": "application/json",
},
responseBody: {
x402Version,
error:
errorMessages?.noMatchingRequirements ||
"Unable to find matching payment requirements",
accepts: paymentRequirements,
},
};
}

return {
status: 200,
paymentRequirements,
decodedPayment,
selectedPaymentRequirements,
};
}

/**
* Parses the amount from the given price
*
* @param price - The price to parse
* @param network - The network to get the default asset for
* @returns The parsed amount or an error message
*/
async function processPriceToAtomicAmount(
price: Money | ERC20TokenAmount,
network: FacilitatorNetwork,
facilitator: ReturnType<typeof facilitatorType>,
): Promise<
| { maxAmountRequired: string; asset: ERC20TokenAmount["asset"] }
| { error: string }
> {
// Handle USDC amount (string) or token amount (ERC20TokenAmount)
let maxAmountRequired: string;
let asset: ERC20TokenAmount["asset"];

if (typeof price === "string" || typeof price === "number") {
// USDC amount in dollars
const parsedAmount = moneySchema.safeParse(price);
if (!parsedAmount.success) {
return {
error: `Invalid price (price: ${price}). Must be in the form "$3.10", 0.10, "0.001", ${parsedAmount.error}`,
};
}
const parsedUsdAmount = parsedAmount.data;
const defaultAsset = await getDefaultAsset(network, facilitator);
if (!defaultAsset) {
return {
error: `Unable to get default asset on ${network}. Please specify an asset in the payment requirements.`,
};
}
asset = defaultAsset;
maxAmountRequired = (parsedUsdAmount * 10 ** asset.decimals).toString();
} else {
// Token amount in atomic units
maxAmountRequired = price.amount;
asset = price.asset;
}

return {
maxAmountRequired,
asset,
};
}

async function getDefaultAsset(
network: FacilitatorNetwork,
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}`,
);
const assetConfig = matchingAsset?.extra
?.defaultAsset as ERC20TokenAmount["asset"];
return assetConfig;
}
2 changes: 1 addition & 1 deletion packages/thirdweb/src/x402/facilitator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402";

/**
* Creates a facilitator for the x402 payment protocol.
* You can use this with `verifyPayment` or with any x402 middleware to enable settling transactions with your thirdweb server wallet.
* You can use this with `settlePayment` or 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
Expand Down
Loading
Loading