setup @thirdweb-dev/nexus package (#8332)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@thirdweb-dev/nexus": minor
|
||||
---
|
||||
|
||||
initial release
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"thirdweb": patch
|
||||
---
|
||||
|
||||
expose some useful erc20 extensions
|
||||
@@ -188,9 +188,16 @@ jobs:
|
||||
- name: Build Packages
|
||||
run: pnpm build
|
||||
|
||||
- name: Report bundle size
|
||||
- name: Report bundle size (thirdweb)
|
||||
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
package_manager: pnpm
|
||||
directory: packages/thirdweb
|
||||
directory: packages/thirdweb
|
||||
|
||||
- name: Report bundle size (nexus)
|
||||
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
package_manager: pnpm
|
||||
directory: packages/nexus
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"import": "*",
|
||||
"limit": "110 kB",
|
||||
"name": "@thirdweb-dev/nexus (esm)",
|
||||
"path": "./dist/esm/exports/nexus.js"
|
||||
},
|
||||
{
|
||||
"limit": "350 kB",
|
||||
"name": "@thirdweb-dev/nexus (cjs)",
|
||||
"path": "./dist/cjs/exports/nexus.js"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
||||
"extends": "//",
|
||||
"overrides": [
|
||||
{
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"useSortedKeys": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"includes": ["package.json"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"entry": ["src/exports/**"],
|
||||
"ignore": ["src/**/__generated__/**", "**/*.bench.ts"],
|
||||
"ignoreBinaries": ["printf"],
|
||||
"ignoreDependencies": ["tslib"],
|
||||
"project": ["src/**/*.{ts,tsx}"],
|
||||
"rules": {
|
||||
"enumMembers": "off",
|
||||
"optionalPeerDependencies": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"author": "thirdweb eng <[email protected]>",
|
||||
"browser": {
|
||||
"crypto": false
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/thirdweb-dev/js/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"x402": "0.7.0",
|
||||
"zod": "3.25.75"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.6",
|
||||
"@size-limit/preset-small-lib": "11.2.0",
|
||||
"knip": "5.60.2",
|
||||
"rimraf": "6.0.1",
|
||||
"size-limit": "11.2.0",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/exports/nexus.d.ts",
|
||||
"import": "./dist/esm/exports/nexus.js",
|
||||
"default": "./dist/cjs/exports/nexus.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/*",
|
||||
"src/*",
|
||||
"!**/*.tsbuildinfo",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.test.tsx",
|
||||
"!**/*.test.ts.snap",
|
||||
"!**/*.test-d.ts",
|
||||
"!**/*.bench.ts",
|
||||
"!tsconfig.build.json"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "./dist/cjs/exports/nexus.js",
|
||||
"module": "./dist/esm/exports/nexus.js",
|
||||
"name": "@thirdweb-dev/nexus",
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm clean && pnpm build:types && pnpm build:cjs && pnpm build:esm",
|
||||
"build:cjs": "tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false && printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json",
|
||||
"build:esm": "tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json",
|
||||
"build:types": "tsc --project ./tsconfig.build.json --module nodenext --moduleResolution nodenext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap",
|
||||
"clean": "rimraf dist",
|
||||
"dev": "tsc --project ./tsconfig.build.json --module nodenext --moduleResolution nodenext --outDir ./dist/esm --watch",
|
||||
"dev:cjs": "printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json && tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false --watch",
|
||||
"dev:esm": "printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json && tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm --watch",
|
||||
"fix": "biome check ./src --fix",
|
||||
"format": "biome format ./src --write",
|
||||
"knip": "knip",
|
||||
"lint": "knip && biome check ./src && tsc --project ./tsconfig.build.json --module nodenext --moduleResolution nodenext --noEmit",
|
||||
"size": "size-limit",
|
||||
"typecheck": "tsc --project ./tsconfig.build.json --module nodenext --moduleResolution nodenext --noEmit"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/nexus.d.ts",
|
||||
"typings": "./dist/types/exports/nexus.d.ts",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
import { ChainIdToNetwork, type Money, moneySchema } from "x402/types";
|
||||
import { decodePayment } from "./encode.js";
|
||||
import type { ThirdwebX402Facilitator } from "./facilitator.js";
|
||||
import {
|
||||
networkToChainId,
|
||||
type RequestedPaymentPayload,
|
||||
type RequestedPaymentRequirements,
|
||||
} from "./schemas.js";
|
||||
import {
|
||||
type DefaultAsset,
|
||||
type ERC20TokenAmount,
|
||||
type PaymentArgs,
|
||||
type PaymentRequiredResult,
|
||||
type SupportedSignatureType,
|
||||
x402Version,
|
||||
} from "./types.js";
|
||||
import { toUnits } from "./utils.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,
|
||||
payTo,
|
||||
resourceUrl,
|
||||
routeConfig = {},
|
||||
method,
|
||||
paymentData,
|
||||
} = args;
|
||||
const {
|
||||
description,
|
||||
mimeType,
|
||||
maxTimeoutSeconds,
|
||||
inputSchema,
|
||||
outputSchema,
|
||||
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,
|
||||
chainId,
|
||||
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[] = [];
|
||||
|
||||
const mappedNetwork = ChainIdToNetwork[chainId];
|
||||
paymentRequirements.push({
|
||||
scheme: "exact",
|
||||
network: mappedNetwork ? mappedNetwork : `eip155:${chainId}`,
|
||||
maxAmountRequired,
|
||||
resource: resourceUrl,
|
||||
description: description ?? "",
|
||||
mimeType: mimeType ?? "application/json",
|
||||
payTo: facilitator.address as `0x${string}`, // always pay to the facilitator address first
|
||||
maxTimeoutSeconds: maxTimeoutSeconds ?? 86400,
|
||||
asset: asset.address as `0x${string}`,
|
||||
outputSchema: {
|
||||
input: {
|
||||
type: "http",
|
||||
method,
|
||||
discoverable: discoverable ?? true,
|
||||
...inputSchema,
|
||||
},
|
||||
output: outputSchema,
|
||||
},
|
||||
extra: {
|
||||
recipientAddress: payTo, // input payTo is the final recipient address
|
||||
...((asset as ERC20TokenAmount["asset"]).eip712 ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
// 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// decode b64 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 &&
|
||||
networkToChainId(value.network) ===
|
||||
networkToChainId(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,
|
||||
chainId: number,
|
||||
facilitator: ThirdwebX402Facilitator,
|
||||
): Promise<
|
||||
{ maxAmountRequired: string; asset: DefaultAsset } | { error: string }
|
||||
> {
|
||||
// Handle USDC amount (string) or token amount (ERC20TokenAmount)
|
||||
let maxAmountRequired: string;
|
||||
let asset: DefaultAsset;
|
||||
|
||||
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(chainId, facilitator);
|
||||
if (!defaultAsset) {
|
||||
return {
|
||||
error: `Unable to get default asset on chain ${chainId}. Please specify an asset in the payment requirements.`,
|
||||
};
|
||||
}
|
||||
asset = defaultAsset;
|
||||
maxAmountRequired = toUnits(
|
||||
parsedUsdAmount.toString(),
|
||||
defaultAsset.decimals,
|
||||
).toString();
|
||||
} else {
|
||||
// Token amount in atomic units
|
||||
maxAmountRequired = price.amount;
|
||||
const tokenExtras = await getOrDetectTokenExtras({
|
||||
facilitator,
|
||||
partialAsset: price.asset,
|
||||
chainId,
|
||||
});
|
||||
if (!tokenExtras) {
|
||||
return {
|
||||
error: `Unable to find token information for ${price.asset.address} on chain ${chainId}. Please specify the asset decimals and eip712 information in the asset options.`,
|
||||
};
|
||||
}
|
||||
asset = {
|
||||
address: price.asset.address,
|
||||
decimals: tokenExtras.decimals,
|
||||
eip712: {
|
||||
name: tokenExtras.name,
|
||||
version: tokenExtras.version,
|
||||
primaryType: tokenExtras.primaryType,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
maxAmountRequired,
|
||||
asset,
|
||||
};
|
||||
}
|
||||
|
||||
async function getDefaultAsset(
|
||||
chainId: number,
|
||||
facilitator: ThirdwebX402Facilitator,
|
||||
): Promise<DefaultAsset | undefined> {
|
||||
const supportedAssets = await facilitator.supported();
|
||||
const matchingAsset = supportedAssets.kinds.find(
|
||||
(supported) => networkToChainId(supported.network) === chainId,
|
||||
);
|
||||
const assetConfig = matchingAsset?.extra?.defaultAsset as DefaultAsset;
|
||||
return assetConfig;
|
||||
}
|
||||
|
||||
async function getOrDetectTokenExtras(args: {
|
||||
facilitator: ThirdwebX402Facilitator;
|
||||
partialAsset: ERC20TokenAmount["asset"];
|
||||
chainId: number;
|
||||
}): Promise<
|
||||
| {
|
||||
name: string;
|
||||
version: string;
|
||||
decimals: number;
|
||||
primaryType: SupportedSignatureType;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
const { facilitator, partialAsset, chainId } = args;
|
||||
if (
|
||||
partialAsset.eip712?.name &&
|
||||
partialAsset.eip712?.version &&
|
||||
partialAsset.decimals !== undefined
|
||||
) {
|
||||
return {
|
||||
name: partialAsset.eip712.name,
|
||||
version: partialAsset.eip712.version,
|
||||
decimals: partialAsset.decimals,
|
||||
primaryType: partialAsset.eip712.primaryType,
|
||||
};
|
||||
}
|
||||
// read from facilitator
|
||||
const response = await facilitator
|
||||
.supported({
|
||||
chainId,
|
||||
tokenAddress: partialAsset.address,
|
||||
})
|
||||
.catch(() => {
|
||||
return {
|
||||
kinds: [],
|
||||
};
|
||||
});
|
||||
|
||||
const exactScheme = response.kinds?.find((kind) => kind.scheme === "exact");
|
||||
if (!exactScheme) {
|
||||
return undefined;
|
||||
}
|
||||
const supportedAsset = exactScheme.extra?.supportedAssets?.find(
|
||||
(asset) =>
|
||||
asset.address.toLowerCase() === partialAsset.address.toLowerCase(),
|
||||
);
|
||||
if (!supportedAsset) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
name: supportedAsset.eip712.name,
|
||||
version: supportedAsset.eip712.version,
|
||||
decimals: supportedAsset.decimals,
|
||||
primaryType: supportedAsset.eip712.primaryType as SupportedSignatureType,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { ExactEvmPayload } from "x402/types";
|
||||
import {
|
||||
type RequestedPaymentPayload,
|
||||
RequestedPaymentPayloadSchema,
|
||||
} from "./schemas.js";
|
||||
|
||||
/**
|
||||
* Encodes a payment payload into a base64 string, ensuring bigint values are properly stringified
|
||||
*
|
||||
* @param payment - The payment payload to encode
|
||||
* @returns A base64 encoded string representation of the payment payload
|
||||
*/
|
||||
export function encodePayment(payment: RequestedPaymentPayload): string {
|
||||
let safe: RequestedPaymentPayload;
|
||||
|
||||
// evm
|
||||
const evmPayload = payment.payload as ExactEvmPayload;
|
||||
safe = {
|
||||
...payment,
|
||||
payload: {
|
||||
...evmPayload,
|
||||
authorization: Object.fromEntries(
|
||||
Object.entries(evmPayload.authorization).map(([key, value]) => [
|
||||
key,
|
||||
typeof value === "bigint" ? (value as bigint).toString() : value,
|
||||
]),
|
||||
) as ExactEvmPayload["authorization"],
|
||||
},
|
||||
};
|
||||
return safeBase64Encode(JSON.stringify(safe));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a base64 encoded payment string back into a PaymentPayload object
|
||||
*
|
||||
* @param payment - The base64 encoded payment string to decode
|
||||
* @returns The decoded and validated PaymentPayload object
|
||||
*/
|
||||
export function decodePayment(payment: string): RequestedPaymentPayload {
|
||||
const decoded = safeBase64Decode(payment);
|
||||
const parsed = JSON.parse(decoded);
|
||||
|
||||
const obj: RequestedPaymentPayload = {
|
||||
...parsed,
|
||||
payload: parsed.payload as ExactEvmPayload,
|
||||
};
|
||||
const validated = RequestedPaymentPayloadSchema.parse(obj);
|
||||
return validated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a string to base64 format
|
||||
*
|
||||
* @param data - The string to be encoded to base64
|
||||
* @returns The base64 encoded string
|
||||
*/
|
||||
export function safeBase64Encode(data: string): string {
|
||||
if (
|
||||
typeof globalThis !== "undefined" &&
|
||||
typeof globalThis.btoa === "function"
|
||||
) {
|
||||
return globalThis.btoa(data);
|
||||
}
|
||||
return Buffer.from(data).toString("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a base64 string back to its original format
|
||||
*
|
||||
* @param data - The base64 encoded string to be decoded
|
||||
* @returns The decoded string in UTF-8 format
|
||||
*/
|
||||
function safeBase64Decode(data: string): string {
|
||||
if (
|
||||
typeof globalThis !== "undefined" &&
|
||||
typeof globalThis.atob === "function"
|
||||
) {
|
||||
return globalThis.atob(data);
|
||||
}
|
||||
return Buffer.from(data, "base64").toString("utf-8");
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export type {
|
||||
HTTPRequestStructure,
|
||||
Money,
|
||||
PaymentMiddlewareConfig,
|
||||
Resource,
|
||||
} from "x402/types";
|
||||
export { decodePayment, encodePayment } from "../encode.js";
|
||||
export {
|
||||
createFacilitator,
|
||||
type ThirdwebX402Facilitator,
|
||||
type ThirdwebX402FacilitatorConfig,
|
||||
type WaitUntil,
|
||||
} from "../facilitator.js";
|
||||
export { settlePayment } from "../settle-payment.js";
|
||||
export type {
|
||||
ERC20TokenAmount,
|
||||
PaymentArgs,
|
||||
PaymentRequiredResult,
|
||||
SettlePaymentArgs,
|
||||
SettlePaymentResult,
|
||||
SupportedSignatureType,
|
||||
VerifyPaymentResult,
|
||||
} from "../types.js";
|
||||
export { verifyPayment } from "../verify-payment.js";
|
||||
@@ -0,0 +1,233 @@
|
||||
import type { VerifyResponse } from "x402/types";
|
||||
import type {
|
||||
FacilitatorSettleResponse,
|
||||
FacilitatorSupportedResponse,
|
||||
FacilitatorVerifyResponse,
|
||||
RequestedPaymentPayload,
|
||||
RequestedPaymentRequirements,
|
||||
} from "./schemas.js";
|
||||
import { stringify } from "./utils.js";
|
||||
|
||||
export type WaitUntil = "simulated" | "submitted" | "confirmed";
|
||||
|
||||
export type ThirdwebX402FacilitatorConfig = {
|
||||
walletSecret: string;
|
||||
walletAddress: string;
|
||||
waitUntil?: WaitUntil;
|
||||
baseUrl?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* facilitator for the x402 payment protocol.
|
||||
* @public
|
||||
*/
|
||||
export type ThirdwebX402Facilitator = {
|
||||
url: `${string}://${string}`;
|
||||
address: string;
|
||||
createAuthHeaders: () => Promise<{
|
||||
verify: Record<string, string>;
|
||||
settle: Record<string, string>;
|
||||
supported: Record<string, string>;
|
||||
list: Record<string, string>;
|
||||
}>;
|
||||
verify: (
|
||||
payload: RequestedPaymentPayload,
|
||||
paymentRequirements: RequestedPaymentRequirements,
|
||||
) => Promise<FacilitatorVerifyResponse>;
|
||||
settle: (
|
||||
payload: RequestedPaymentPayload,
|
||||
paymentRequirements: RequestedPaymentRequirements,
|
||||
waitUntil?: WaitUntil,
|
||||
) => Promise<FacilitatorSettleResponse>;
|
||||
supported: (filters?: {
|
||||
chainId: number;
|
||||
tokenAddress?: string;
|
||||
}) => Promise<FacilitatorSupportedResponse>;
|
||||
};
|
||||
|
||||
const DEFAULT_BASE_URL = "https://nexus-api.thirdweb.com";
|
||||
|
||||
/**
|
||||
* Creates a facilitator for the x402 payment protocol.
|
||||
* 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
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createFacilitator } from "@thirdweb-dev/nexus";
|
||||
* import { paymentMiddleware } from 'x402-hono'
|
||||
*
|
||||
* const facilitator = createFacilitator({
|
||||
* walletSecret: <your-wallet-secret>,
|
||||
* walletAddress: <your-wallet-address>,
|
||||
* });
|
||||
*
|
||||
* // add the facilitator to any x402 payment middleware
|
||||
* const middleware = paymentMiddleware(
|
||||
* facilitator.address,
|
||||
* {
|
||||
* "/api/paywall": {
|
||||
* price: "$0.01",
|
||||
* network: "base-sepolia",
|
||||
* config: {
|
||||
* description: "Access to paid content",
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* facilitator,
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* #### Configuration Options
|
||||
*
|
||||
* ```ts
|
||||
* const thirdwebX402Facilitator = createFacilitator({
|
||||
* walletSecret: <your-wallet-secret>,
|
||||
* walletAddress: <your-wallet-address>,
|
||||
* // Optional: Wait behavior for settlements
|
||||
* // - "simulated": Only simulate the transaction (fastest)
|
||||
* // - "submitted": Wait until transaction is submitted
|
||||
* // - "confirmed": Wait for full on-chain confirmation (slowest, default)
|
||||
* waitUntil: "confirmed",
|
||||
* });
|
||||
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
export function createFacilitator(
|
||||
config: ThirdwebX402FacilitatorConfig,
|
||||
): ThirdwebX402Facilitator {
|
||||
if (!config.walletSecret) {
|
||||
throw new Error("Wallet secret is required for the x402 facilitator");
|
||||
}
|
||||
|
||||
if (!config.walletAddress) {
|
||||
throw new Error("Wallet address is required for the x402 facilitator");
|
||||
}
|
||||
|
||||
const BASE_URL = config.baseUrl ?? DEFAULT_BASE_URL;
|
||||
|
||||
const AUTH_HEADERS = {
|
||||
verify: {
|
||||
authorization: `Bearer ${config.walletSecret}`,
|
||||
},
|
||||
settle: {
|
||||
authorization: `Bearer ${config.walletSecret}`,
|
||||
},
|
||||
supported: {
|
||||
authorization: `Bearer ${config.walletSecret}`,
|
||||
},
|
||||
list: {
|
||||
authorization: `Bearer ${config.walletSecret}`,
|
||||
},
|
||||
} as const;
|
||||
|
||||
return {
|
||||
url: BASE_URL as `${string}://${string}`,
|
||||
address: config.walletAddress,
|
||||
createAuthHeaders: async () => AUTH_HEADERS,
|
||||
/**
|
||||
* Verifies a payment payload with the facilitator service
|
||||
*
|
||||
* @param payload - The payment payload to verify
|
||||
* @param paymentRequirements - The payment requirements to verify against
|
||||
* @returns A promise that resolves to the verification response
|
||||
*/
|
||||
async verify(
|
||||
payload: RequestedPaymentPayload,
|
||||
paymentRequirements: RequestedPaymentRequirements,
|
||||
): Promise<FacilitatorVerifyResponse> {
|
||||
let headers = { "Content-Type": "application/json" };
|
||||
|
||||
headers = { ...headers, ...AUTH_HEADERS.verify };
|
||||
|
||||
const res = await fetch(new URL("/verify", BASE_URL), {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: stringify({
|
||||
x402Version: payload.x402Version,
|
||||
paymentPayload: payload,
|
||||
paymentRequirements: paymentRequirements,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
const text = `${res.statusText} ${await res.text()}`;
|
||||
throw new Error(`Failed to verify payment: ${res.status} ${text}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return data as VerifyResponse;
|
||||
},
|
||||
|
||||
/**
|
||||
* Settles a payment with the facilitator service
|
||||
*
|
||||
* @param payload - The payment payload to settle
|
||||
* @param paymentRequirements - The payment requirements for the settlement
|
||||
* @returns A promise that resolves to the settlement response
|
||||
*/
|
||||
async settle(
|
||||
payload: RequestedPaymentPayload,
|
||||
paymentRequirements: RequestedPaymentRequirements,
|
||||
waitUntil?: WaitUntil,
|
||||
): Promise<FacilitatorSettleResponse> {
|
||||
let headers = { "Content-Type": "application/json" };
|
||||
headers = { ...headers, ...AUTH_HEADERS.settle };
|
||||
const waitUntilParam = waitUntil || config.waitUntil;
|
||||
|
||||
const res = await fetch(new URL("/settle", BASE_URL), {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: stringify({
|
||||
x402Version: payload.x402Version,
|
||||
paymentPayload: payload,
|
||||
paymentRequirements: paymentRequirements,
|
||||
...(waitUntilParam ? { waitUntil: waitUntilParam } : {}),
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
const text = `${res.statusText} ${await res.text()}`;
|
||||
throw new Error(`Failed to settle payment: ${res.status} ${text}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return data as FacilitatorSettleResponse;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the supported payment kinds from the facilitator service.
|
||||
*
|
||||
* @returns A promise that resolves to the supported payment kinds
|
||||
*/
|
||||
async supported(filters?: {
|
||||
chainId: number;
|
||||
tokenAddress?: string;
|
||||
}): Promise<FacilitatorSupportedResponse> {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...AUTH_HEADERS.supported,
|
||||
};
|
||||
const supportedUrl = new URL("/supported", BASE_URL);
|
||||
if (filters?.chainId) {
|
||||
supportedUrl.searchParams.set("chainId", filters.chainId.toString());
|
||||
}
|
||||
if (filters?.tokenAddress) {
|
||||
supportedUrl.searchParams.set("tokenAddress", filters.tokenAddress);
|
||||
}
|
||||
const res = await fetch(supportedUrl, { headers });
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(
|
||||
`Failed to get supported payment kinds: ${res.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return data as FacilitatorSupportedResponse;
|
||||
},
|
||||
} as const satisfies ThirdwebX402Facilitator;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
EvmNetworkToChainId,
|
||||
type Network,
|
||||
PaymentPayloadSchema,
|
||||
PaymentRequirementsSchema,
|
||||
SettleResponseSchema,
|
||||
SupportedPaymentKindsResponseSchema,
|
||||
VerifyResponseSchema,
|
||||
} from "x402/types";
|
||||
import { z } from "zod";
|
||||
|
||||
const FacilitatorNetworkSchema = z.string();
|
||||
|
||||
export type FacilitatorNetwork = z.infer<typeof FacilitatorNetworkSchema>;
|
||||
|
||||
export const RequestedPaymentPayloadSchema = PaymentPayloadSchema.extend({
|
||||
network: FacilitatorNetworkSchema,
|
||||
});
|
||||
|
||||
export type RequestedPaymentPayload = z.infer<
|
||||
typeof RequestedPaymentPayloadSchema
|
||||
>;
|
||||
|
||||
const RequestedPaymentRequirementsSchema = PaymentRequirementsSchema.extend({
|
||||
network: FacilitatorNetworkSchema,
|
||||
});
|
||||
|
||||
export type RequestedPaymentRequirements = z.infer<
|
||||
typeof RequestedPaymentRequirementsSchema
|
||||
>;
|
||||
|
||||
const FacilitatorSettleResponseSchema = SettleResponseSchema.extend({
|
||||
network: FacilitatorNetworkSchema,
|
||||
errorMessage: z.string().optional(),
|
||||
});
|
||||
export type FacilitatorSettleResponse = z.infer<
|
||||
typeof FacilitatorSettleResponseSchema
|
||||
>;
|
||||
|
||||
const FacilitatorVerifyResponseSchema = VerifyResponseSchema.extend({
|
||||
errorMessage: z.string().optional(),
|
||||
});
|
||||
|
||||
export type FacilitatorVerifyResponse = z.infer<
|
||||
typeof FacilitatorVerifyResponseSchema
|
||||
>;
|
||||
|
||||
export const SupportedSignatureTypeSchema = z.enum([
|
||||
"TransferWithAuthorization",
|
||||
"Permit",
|
||||
]);
|
||||
|
||||
export const FacilitatorSupportedAssetSchema = z.object({
|
||||
address: z.string(),
|
||||
decimals: z.number(),
|
||||
eip712: z.object({
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
primaryType: SupportedSignatureTypeSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
const FacilitatorSupportedResponseSchema =
|
||||
SupportedPaymentKindsResponseSchema.extend({
|
||||
kinds: z.array(
|
||||
z.object({
|
||||
x402Version: z.literal(1),
|
||||
scheme: z.literal("exact"),
|
||||
network: FacilitatorNetworkSchema,
|
||||
extra: z
|
||||
.object({
|
||||
defaultAsset: FacilitatorSupportedAssetSchema.optional(),
|
||||
supportedAssets: z
|
||||
.array(FacilitatorSupportedAssetSchema)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
),
|
||||
}).describe("Supported payment kinds for this facilitator");
|
||||
|
||||
export type FacilitatorSupportedResponse = z.infer<
|
||||
typeof FacilitatorSupportedResponseSchema
|
||||
>;
|
||||
|
||||
export function networkToChainId(network: string): number {
|
||||
if (network.startsWith("eip155:")) {
|
||||
const chainId = parseInt(network.split(":")[1] ?? "0");
|
||||
if (!Number.isNaN(chainId) && chainId > 0) {
|
||||
return chainId;
|
||||
} else {
|
||||
throw new Error(`Invalid network: ${network}`);
|
||||
}
|
||||
}
|
||||
// attempt to parse it as just an integer
|
||||
const maybeChainId = parseInt(network);
|
||||
if (!Number.isNaN(maybeChainId) && maybeChainId > 0) {
|
||||
return maybeChainId;
|
||||
}
|
||||
const mappedChainId = EvmNetworkToChainId.get(network as Network);
|
||||
if (!mappedChainId) {
|
||||
throw new Error(`Invalid network: ${network}`);
|
||||
}
|
||||
// TODO (402): support solana networks
|
||||
if (mappedChainId === 101 || mappedChainId === 103) {
|
||||
throw new Error("Solana networks not supported yet.");
|
||||
}
|
||||
return mappedChainId;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { decodePaymentRequest } from "./common.js";
|
||||
import { safeBase64Encode } from "./encode.js";
|
||||
import {
|
||||
type SettlePaymentArgs,
|
||||
type SettlePaymentResult,
|
||||
x402Version,
|
||||
} from "./types.js";
|
||||
import { stringify } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Verifies and processes X402 payments for protected resources.
|
||||
*
|
||||
* This function implements the X402 payment protocol, verifying payment proofs
|
||||
* and settling payments through a facilitator service. It handles the complete
|
||||
* payment flow from validation to settlement.
|
||||
*
|
||||
* @param args - Configuration object containing payment verification parameters
|
||||
* @returns A promise that resolves to either a successful payment result (200) or payment required error (402)
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ### Next.js API route example
|
||||
*
|
||||
* ```ts
|
||||
* // Usage in a Next.js API route
|
||||
* import { settlePayment, createFacilitator } from "@thirdweb-dev/nexus";
|
||||
* import { arbitrumSepolia } from "thirdweb/chains";
|
||||
*
|
||||
* const facilitator = createFacilitator({
|
||||
* walletSecret: <your-wallet-secret>,
|
||||
* walletAddress: <your-wallet-address>,
|
||||
* });
|
||||
*
|
||||
* export async function GET(request: Request) {
|
||||
* const paymentData = request.headers.get("x-payment");
|
||||
*
|
||||
* // verify and process the payment
|
||||
* const result = await settlePayment({
|
||||
* resourceUrl: "https://api.example.com/premium-content",
|
||||
* method: "GET",
|
||||
* paymentData,
|
||||
* network: arbitrumSepolia, // or any other chain
|
||||
* price: "$0.10", // or { amount: "100000", asset: { address: "0x...", decimals: 6 } }
|
||||
* facilitator,
|
||||
* routeConfig: {
|
||||
* description: "Access to premium API content",
|
||||
* mimeType: "application/json",
|
||||
* maxTimeoutSeconds: 300,
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* if (result.status === 200) {
|
||||
* // Payment verified and settled successfully
|
||||
* return Response.json({ data: "premium content" });
|
||||
* } else {
|
||||
* // Payment required
|
||||
* return Response.json(result.responseBody, {
|
||||
* status: result.status,
|
||||
* headers: result.responseHeaders,
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Express middleware example
|
||||
*
|
||||
* ```ts
|
||||
* // Usage in Express middleware
|
||||
* import express from "express";
|
||||
* import { settlePayment, createFacilitator } from "@thirdweb-dev/nexus";
|
||||
* import { arbitrumSepolia } from "thirdweb/chains";
|
||||
*
|
||||
* const facilitator = createFacilitator({
|
||||
* walletSecret: <your-wallet-secret>,
|
||||
* walletAddress: <your-wallet-address>,
|
||||
* });
|
||||
*
|
||||
* const app = express();
|
||||
*
|
||||
* async function paymentMiddleware(req, res, next) {
|
||||
* // verify and process the payment
|
||||
* const result = await settlePayment({
|
||||
* resourceUrl: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
|
||||
* method: req.method,
|
||||
* paymentData: req.headers["x-payment"],
|
||||
* network: arbitrumSepolia, // or any other chain
|
||||
* price: "$0.05",
|
||||
* waitUntil: "submitted",
|
||||
* facilitator,
|
||||
* });
|
||||
*
|
||||
* if (result.status === 200) {
|
||||
* // Set payment receipt headers and continue
|
||||
* Object.entries(result.responseHeaders).forEach(([key, value]) => {
|
||||
* res.setHeader(key, value);
|
||||
* });
|
||||
* next();
|
||||
* } else {
|
||||
* // Return payment required response
|
||||
* res.status(result.status)
|
||||
* .set(result.responseHeaders)
|
||||
* .json(result.responseBody);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* app.get("/api/premium", paymentMiddleware, (req, res) => {
|
||||
* res.json({ message: "This is premium content!" });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
* @beta
|
||||
*/
|
||||
export async function settlePayment(
|
||||
args: SettlePaymentArgs,
|
||||
): Promise<SettlePaymentResult> {
|
||||
const { routeConfig = {}, facilitator } = args;
|
||||
const { errorMessages } = routeConfig;
|
||||
|
||||
const decodePaymentResult = await decodePaymentRequest(args);
|
||||
|
||||
if (decodePaymentResult.status !== 200) {
|
||||
return decodePaymentResult;
|
||||
}
|
||||
|
||||
const { selectedPaymentRequirements, decodedPayment, paymentRequirements } =
|
||||
decodePaymentResult;
|
||||
|
||||
try {
|
||||
const settlement = await facilitator.settle(
|
||||
decodedPayment,
|
||||
selectedPaymentRequirements,
|
||||
args.waitUntil,
|
||||
);
|
||||
|
||||
if (settlement.success) {
|
||||
return {
|
||||
status: 200,
|
||||
paymentReceipt: settlement,
|
||||
responseHeaders: {
|
||||
"Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE",
|
||||
"X-PAYMENT-RESPONSE": safeBase64Encode(stringify(settlement)),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const error = settlement.errorReason || "Settlement error";
|
||||
return {
|
||||
status: 402,
|
||||
responseHeaders: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
responseBody: {
|
||||
x402Version,
|
||||
error,
|
||||
errorMessage:
|
||||
errorMessages?.settlementFailed || settlement.errorMessage,
|
||||
accepts: paymentRequirements,
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 402,
|
||||
responseHeaders: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
responseBody: {
|
||||
x402Version,
|
||||
error: "Settlement error",
|
||||
errorMessage:
|
||||
errorMessages?.settlementFailed ||
|
||||
(error instanceof Error ? error.message : undefined),
|
||||
accepts: paymentRequirements,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { Money, PaymentMiddlewareConfig } from "x402/types";
|
||||
import type z from "zod";
|
||||
import type { ThirdwebX402Facilitator, WaitUntil } from "./facilitator.js";
|
||||
import type {
|
||||
FacilitatorNetwork,
|
||||
FacilitatorSettleResponse,
|
||||
FacilitatorSupportedAssetSchema,
|
||||
RequestedPaymentPayload,
|
||||
RequestedPaymentRequirements,
|
||||
SupportedSignatureTypeSchema,
|
||||
} from "./schemas.js";
|
||||
|
||||
export const x402Version = 1;
|
||||
|
||||
/**
|
||||
* Configuration object for verifying or processing X402 payments.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PaymentArgs = {
|
||||
/** The URL of the resource being protected by the payment */
|
||||
resourceUrl: string;
|
||||
/** The HTTP method used to access the resource */
|
||||
method: "GET" | "POST" | ({} & string);
|
||||
/** The payment data/proof provided by the client, typically from the X-PAYMENT header */
|
||||
paymentData?: string | null;
|
||||
/** The blockchain network where the payment should be processed */
|
||||
network: FacilitatorNetwork;
|
||||
/** 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 */
|
||||
facilitator: ThirdwebX402Facilitator;
|
||||
/** Optional configuration for the payment middleware route */
|
||||
routeConfig?: PaymentMiddlewareConfig;
|
||||
/** Optional recipient address to receive the payment if different from your facilitator address */
|
||||
payTo?: string;
|
||||
};
|
||||
|
||||
export type SettlePaymentArgs = PaymentArgs & {
|
||||
waitUntil?: WaitUntil;
|
||||
};
|
||||
|
||||
export type PaymentRequiredResult = {
|
||||
/** HTTP 402 - Payment Required, verification or processing failed or payment missing */
|
||||
status: 402;
|
||||
/** The error response body containing payment requirements */
|
||||
responseBody: {
|
||||
/** The X402 protocol version */
|
||||
x402Version: number;
|
||||
/** error code */
|
||||
error: string;
|
||||
/** Human-readable error message */
|
||||
errorMessage?: string;
|
||||
/** Array of acceptable payment methods and requirements */
|
||||
accepts: RequestedPaymentRequirements[];
|
||||
/** Optional payer address if verification partially succeeded */
|
||||
payer?: string;
|
||||
};
|
||||
/** Response headers for the error response */
|
||||
responseHeaders: Record<string, string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The result of a payment settlement operation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SettlePaymentResult =
|
||||
| {
|
||||
/** HTTP 200 - Payment was successfully processed */
|
||||
status: 200;
|
||||
/** Response headers including payment receipt information */
|
||||
responseHeaders: Record<string, string>;
|
||||
/** The payment receipt from the payment facilitator */
|
||||
paymentReceipt: FacilitatorSettleResponse;
|
||||
}
|
||||
| PaymentRequiredResult;
|
||||
|
||||
/**
|
||||
* The result of a payment verification operation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type VerifyPaymentResult =
|
||||
| {
|
||||
/** HTTP 200 - Payment was successfully verified */
|
||||
status: 200;
|
||||
decodedPayment: RequestedPaymentPayload;
|
||||
selectedPaymentRequirements: RequestedPaymentRequirements;
|
||||
}
|
||||
| PaymentRequiredResult;
|
||||
|
||||
export type SupportedSignatureType = z.infer<
|
||||
typeof SupportedSignatureTypeSchema
|
||||
>;
|
||||
|
||||
export type ERC20TokenAmount = {
|
||||
amount: string;
|
||||
asset: {
|
||||
address: `0x${string}`;
|
||||
decimals?: number;
|
||||
eip712?: {
|
||||
name: string;
|
||||
version: string;
|
||||
primaryType: SupportedSignatureType;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type DefaultAsset = z.infer<typeof FacilitatorSupportedAssetSchema>;
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Stringify a JSON object and convert all bigint values to string
|
||||
*
|
||||
* If you are getting this error: "Exception: Do not know how to serialize a BigInt",
|
||||
* you probably can use this function to parse the data.
|
||||
* Because bigint is not an accepted value of the JSON format.
|
||||
*
|
||||
* @returns An object with all bigint values converted to string
|
||||
* @example
|
||||
* ```ts
|
||||
* import { stringify } from "thirdweb/utils";
|
||||
* const obj = { tokenId: 0n };
|
||||
* const str = stringify(obj); // "{"tokenId":"0"}"
|
||||
* ```
|
||||
* @utils
|
||||
*/
|
||||
export function stringify(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: JSON.stringify signature
|
||||
value: any,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: JSON.stringify signature
|
||||
replacer?: ((this: any, key: string, value: any) => any) | null,
|
||||
space?: string | number,
|
||||
) {
|
||||
const res = JSON.stringify(
|
||||
value,
|
||||
(key, value_) => {
|
||||
const value__ = typeof value_ === "bigint" ? value_.toString() : value_;
|
||||
return typeof replacer === "function" ? replacer(key, value__) : value__;
|
||||
},
|
||||
space,
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string representation of a number with decimal places to a BigInt representation.
|
||||
* @param tokens - The string representation of the number, including the integer and fraction parts.
|
||||
* @param decimals - The number of decimal places to include in the BigInt representation.
|
||||
* @returns The BigInt representation of the number.
|
||||
* @example
|
||||
* ```ts
|
||||
* import { toUnits } from "thirdweb/utils";
|
||||
* toUnits('1', 18)
|
||||
* // 1000000000000000000n
|
||||
* ```
|
||||
* @utils
|
||||
*/
|
||||
export function toUnits(tokens: string, decimals: number): bigint {
|
||||
if (tokens.includes("e")) {
|
||||
tokens = Number(tokens).toFixed(decimals);
|
||||
}
|
||||
|
||||
let [integerPart, fractionPart = ""] = tokens.split(".") as [string, string];
|
||||
const prefix = integerPart.startsWith("-") ? "-" : "";
|
||||
if (prefix) {
|
||||
integerPart = integerPart.slice(1);
|
||||
}
|
||||
|
||||
fractionPart = fractionPart.padEnd(decimals, "0"); // Ensure fraction part is at least 'decimals' long.
|
||||
|
||||
if (decimals === 0) {
|
||||
// Check if there's any fraction part that would necessitate rounding up the integer part.
|
||||
if (fractionPart[0] && Number.parseInt(fractionPart[0]) >= 5) {
|
||||
integerPart = (BigInt(integerPart) + 1n).toString();
|
||||
}
|
||||
fractionPart = ""; // No fraction part is needed when decimals === 0.
|
||||
} else {
|
||||
// When decimals > 0, handle potential rounding based on the digit right after the specified decimal places.
|
||||
if (fractionPart.length > decimals) {
|
||||
const roundingDigit = fractionPart[decimals];
|
||||
if (roundingDigit && Number.parseInt(roundingDigit, 10) >= 5) {
|
||||
// If rounding is needed, add 1 to the last included digit of the fraction part.
|
||||
const roundedFraction =
|
||||
BigInt(fractionPart.substring(0, decimals)) + 1n;
|
||||
fractionPart = roundedFraction.toString().padStart(decimals, "0");
|
||||
|
||||
if (fractionPart.length > decimals) {
|
||||
// If rounding the fraction results in a length increase (e.g., .999 -> 1.000), increment the integer part.
|
||||
integerPart = (BigInt(integerPart) + 1n).toString();
|
||||
// Adjust the fraction part if it's longer than the specified decimals due to rounding up.
|
||||
fractionPart = fractionPart.substring(fractionPart.length - decimals);
|
||||
}
|
||||
} else {
|
||||
// If no rounding is necessary, just truncate the fraction part to the specified number of decimals.
|
||||
fractionPart = fractionPart.substring(0, decimals);
|
||||
}
|
||||
}
|
||||
// If the fraction part is shorter than the specified decimals, it's already handled by padEnd() above.
|
||||
}
|
||||
|
||||
// Combine the integer and fraction parts into the final BigInt representation.
|
||||
return BigInt(`${prefix}${integerPart}${fractionPart}`);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { decodePaymentRequest } from "./common.js";
|
||||
import {
|
||||
type PaymentArgs,
|
||||
type VerifyPaymentResult,
|
||||
x402Version,
|
||||
} from "./types.js";
|
||||
|
||||
/**
|
||||
* Verifies X402 payments for protected resources. This function only verifies the payment,
|
||||
* you should use `settlePayment` to settle the payment.
|
||||
*
|
||||
* @param args - Configuration object containing payment verification parameters
|
||||
* @returns A promise that resolves to either a successful verification result (200) or payment required error (402)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Usage in a Next.js API route
|
||||
* import { verifyPayment, createFacilitator } from "@thirdweb-dev/nexus";
|
||||
* import { arbitrumSepolia } from "thirdweb/chains";
|
||||
*
|
||||
* const facilitator = createFacilitator({
|
||||
* walletSecret: <your-wallet-secret>,
|
||||
* walletAddress: <your-wallet-address>,
|
||||
* });
|
||||
*
|
||||
* export async function GET(request: Request) {
|
||||
* const paymentData = request.headers.get("x-payment");
|
||||
*
|
||||
* const paymentArgs = {
|
||||
* resourceUrl: "https://api.example.com/premium-content",
|
||||
* method: "GET",
|
||||
* paymentData,
|
||||
* network: arbitrumSepolia, // or any other chain
|
||||
* price: "$0.10", // or { amount: "100000", asset: { address: "0x...", decimals: 6 } }
|
||||
* facilitator,
|
||||
* routeConfig: {
|
||||
* description: "Access to premium API content",
|
||||
* mimeType: "application/json",
|
||||
* maxTimeoutSeconds: 300,
|
||||
* },
|
||||
* };
|
||||
*
|
||||
* // verify the payment
|
||||
* const result = await verifyPayment(paymentArgs);
|
||||
*
|
||||
* if (result.status === 200) {
|
||||
* // Payment verified, but not settled yet
|
||||
* // you can do the work that requires payment first
|
||||
* const result = await doSomething();
|
||||
* // then settle the payment
|
||||
* const settleResult = await settlePayment(paymentArgs);
|
||||
*
|
||||
* // then return the result
|
||||
* return Response.json(result);
|
||||
* } else {
|
||||
* // verification failed, return payment required
|
||||
* return Response.json(result.responseBody, {
|
||||
* status: result.status,
|
||||
* headers: result.responseHeaders,
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
* @beta
|
||||
*/
|
||||
export async function verifyPayment(
|
||||
args: PaymentArgs,
|
||||
): Promise<VerifyPaymentResult> {
|
||||
const { routeConfig = {}, facilitator } = args;
|
||||
const { errorMessages } = routeConfig;
|
||||
|
||||
const decodePaymentResult = await decodePaymentRequest(args);
|
||||
|
||||
if (decodePaymentResult.status !== 200) {
|
||||
return decodePaymentResult;
|
||||
}
|
||||
|
||||
const { selectedPaymentRequirements, decodedPayment, paymentRequirements } =
|
||||
decodePaymentResult;
|
||||
|
||||
// Verify payment
|
||||
try {
|
||||
const verification = await facilitator.verify(
|
||||
decodedPayment,
|
||||
selectedPaymentRequirements,
|
||||
);
|
||||
|
||||
if (verification.isValid) {
|
||||
return {
|
||||
status: 200,
|
||||
decodedPayment,
|
||||
selectedPaymentRequirements,
|
||||
};
|
||||
} else {
|
||||
const error = verification.invalidReason || "Verification failed";
|
||||
return {
|
||||
status: 402,
|
||||
responseHeaders: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
responseBody: {
|
||||
x402Version,
|
||||
error: error,
|
||||
errorMessage:
|
||||
errorMessages?.verificationFailed || verification.errorMessage,
|
||||
accepts: paymentRequirements,
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 402,
|
||||
responseHeaders: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
responseBody: {
|
||||
x402Version,
|
||||
error: "Verification error",
|
||||
errorMessage:
|
||||
errorMessages?.verificationFailed ||
|
||||
(error instanceof Error ? error.message : undefined),
|
||||
accepts: paymentRequirements,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
// This tsconfig file contains the shared config for the build (tsconfig.build.json) and type checking (tsconfig.json) config.
|
||||
"compilerOptions": {
|
||||
// Incremental builds
|
||||
// NOTE: Enabling incremental builds speeds up `tsc`. Keep in mind though that it does not reliably bust the cache when the `tsconfig.json` file changes.
|
||||
"allowJs": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"checkJs": false,
|
||||
|
||||
// Interop constraints
|
||||
"esModuleInterop": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"importHelpers": true,
|
||||
// Incremental builds
|
||||
// NOTE: Enabling incremental builds speeds up `tsc`. Keep in mind though that it does not reliably bust the cache when the `tsconfig.json` file changes.
|
||||
"incremental": false,
|
||||
|
||||
// jsx for "/react" portion
|
||||
"jsx": "react-jsx",
|
||||
"lib": [
|
||||
"ES2022", // By using ES2022 we get access to the `.cause` property on `Error` instances.
|
||||
"DOM" // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped.
|
||||
],
|
||||
"module": "NodeNext",
|
||||
|
||||
// Language and environment
|
||||
"moduleResolution": "NodeNext",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
|
||||
// Skip type checking for node modules
|
||||
"skipLibCheck": true,
|
||||
|
||||
// Type checking
|
||||
"strict": true,
|
||||
"target": "ES2021",
|
||||
"useDefineForClassFields": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
// This tsconfig file contains the shared config for the build (tsconfig.build.json) and type checking (tsconfig.json) config.
|
||||
"include": []
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"rootDir": "./src",
|
||||
"sourceMap": true
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.test-d.ts",
|
||||
"src/**/*.bench.ts",
|
||||
"src/**/*.macro.ts"
|
||||
],
|
||||
"extends": "./tsconfig.base.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// This configuration is used for local development and type checking.
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "./dist",
|
||||
"paths": {
|
||||
"~test/*": ["./test/src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": [],
|
||||
"extends": "./tsconfig.base.json",
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
"path": "./dist/esm/exports/thirdweb.js"
|
||||
},
|
||||
{
|
||||
"limit": "375 kB",
|
||||
"limit": "380 kB",
|
||||
"name": "thirdweb (cjs)",
|
||||
"path": "./dist/cjs/exports/thirdweb.js"
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type { Abi } from "abitype";
|
||||
|
||||
export { formatCompilerMetadata } from "../contract/actions/compiler-metadata.js";
|
||||
export { getBytecode } from "../contract/actions/get-bytecode.js";
|
||||
export { getCompilerMetadata } from "../contract/actions/get-compiler-metadata.js";
|
||||
|
||||
@@ -17,10 +17,6 @@ export {
|
||||
getActiveClaimConditionId,
|
||||
isGetActiveClaimConditionIdSupported,
|
||||
} from "../../extensions/erc20/__generated__/IDropERC20/read/getActiveClaimConditionId.js";
|
||||
/**
|
||||
* DROPS extension for ERC20
|
||||
*/
|
||||
// READ
|
||||
export {
|
||||
getClaimConditionById,
|
||||
isGetClaimConditionByIdSupported,
|
||||
@@ -43,6 +39,15 @@ export {
|
||||
balanceOf,
|
||||
} from "../../extensions/erc20/__generated__/IERC20/read/balanceOf.js";
|
||||
export { totalSupply } from "../../extensions/erc20/__generated__/IERC20/read/totalSupply.js";
|
||||
/**
|
||||
* PERMIT extension for ERC20
|
||||
*/
|
||||
export { nonces } from "../../extensions/erc20/__generated__/IERC20Permit/read/nonces.js";
|
||||
export { isPermitSupported } from "../../extensions/erc20/__generated__/IERC20Permit/write/permit.js";
|
||||
/**
|
||||
* DROPS extension for ERC20
|
||||
*/
|
||||
// READ
|
||||
export {
|
||||
type TokensMintedEventFilters,
|
||||
tokensMintedEvent,
|
||||
@@ -63,6 +68,7 @@ export {
|
||||
type WithdrawParams,
|
||||
withdraw,
|
||||
} from "../../extensions/erc20/__generated__/IWETH/write/withdraw.js";
|
||||
export { isTransferWithAuthorizationSupported } from "../../extensions/erc20/__generated__/USDC/write/transferWithAuthorization.js";
|
||||
export {
|
||||
type CanClaimParams,
|
||||
type CanClaimResult,
|
||||
|
||||
Generated
+1504
-1320
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -96,7 +96,7 @@
|
||||
"thirdweb-dashboard#dev": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"thirdweb#update-version": {
|
||||
"update-version": {
|
||||
"inputs": ["$TURBO_DEFAULT$", "package.json"],
|
||||
"outputs": ["src/version.ts"]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user