Skip to content
Closed
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
1 change: 0 additions & 1 deletion packages/nexus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"url": "https://github.com/thirdweb-dev/js/issues"
},
"dependencies": {
"thirdweb": "workspace:*",
"x402": "0.7.0",
"zod": "3.25.75"
},
Expand Down
53 changes: 3 additions & 50 deletions packages/nexus/src/common.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import type { ThirdwebClient } from "thirdweb";
import { type Abi, getContract, resolveContractAbi } from "thirdweb/contract";
import {
isPermitSupported,
isTransferWithAuthorizationSupported,
} from "thirdweb/extensions/erc20";
import { getAddress, toFunctionSelector, toUnits } from "thirdweb/utils";
import { ChainIdToNetwork, type Money, moneySchema } from "x402/types";
import { getCachedChain } from "../../thirdweb/dist/types/chains/utils.js";
import { decodePayment } from "./encode.js";
import type { ThirdwebX402Facilitator } from "./facilitator.js";
import {
Expand All @@ -22,6 +14,7 @@ import {
type SupportedSignatureType,
x402Version,
} from "./types.js";
import { toUnits } from "./utils.js";

type GetPaymentRequirementsResult = {
status: 200;
Expand Down Expand Up @@ -104,9 +97,9 @@ export async function decodePaymentRequest(
resource: resourceUrl,
description: description ?? "",
mimeType: mimeType ?? "application/json",
payTo: getAddress(facilitator.address), // always pay to the facilitator address first
payTo: facilitator.address as `0x${string}`, // always pay to the facilitator address first
maxTimeoutSeconds: maxTimeoutSeconds ?? 86400,
asset: getAddress(asset.address),
asset: asset.address as `0x${string}`,
outputSchema: {
input: {
type: "http",
Expand Down Expand Up @@ -268,46 +261,6 @@ async function getDefaultAsset(
return assetConfig;
}

export async function getSupportedSignatureType(args: {
client: ThirdwebClient;
asset: string;
chainId: number;
eip712Extras: ERC20TokenAmount["asset"]["eip712"] | undefined;
}): Promise<SupportedSignatureType | undefined> {
const primaryType = args.eip712Extras?.primaryType;

if (primaryType === "Permit" || primaryType === "TransferWithAuthorization") {
return primaryType;
}

// not specified, so we need to detect it
const abi: Abi = await resolveContractAbi(
getContract({
client: args.client,
address: args.asset,
chain: getCachedChain(args.chainId),
}),
).catch((error) => {
console.error("Error resolving contract ABI", error);
return [];
});
const selectors = abi
.filter((f) => f.type === "function")
.map((f) => toFunctionSelector(f));
const hasPermit = isPermitSupported(selectors);
const hasTransferWithAuthorization =
isTransferWithAuthorizationSupported(selectors);

// prefer transferWithAuthorization over permit
if (hasTransferWithAuthorization) {
return "TransferWithAuthorization";
}
if (hasPermit) {
return "Permit";
}
return undefined;
}

async function getOrDetectTokenExtras(args: {
facilitator: ThirdwebX402Facilitator;
partialAsset: ERC20TokenAmount["asset"];
Expand Down
31 changes: 15 additions & 16 deletions packages/nexus/src/exports/nexus.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
export type {
HTTPRequestStructure,
Money,
PaymentMiddlewareConfig,
Resource,
HTTPRequestStructure,
Money,
PaymentMiddlewareConfig,
Resource,
} from "x402/types";
export { decodePayment, encodePayment } from "../encode.js";
export {
facilitator,
type ThirdwebX402Facilitator,
type ThirdwebX402FacilitatorConfig,
type WaitUntil,
createFacilitator,
type ThirdwebX402Facilitator,
type ThirdwebX402FacilitatorConfig,
type WaitUntil,
} from "../facilitator.js";
export { wrapFetchWithPayment } from "../fetchWithPayment.js";
export { settlePayment } from "../settle-payment.js";
export type {
ERC20TokenAmount,
PaymentArgs,
PaymentRequiredResult,
SettlePaymentArgs,
SettlePaymentResult,
SupportedSignatureType,
VerifyPaymentResult,
ERC20TokenAmount,
PaymentArgs,
PaymentRequiredResult,
SettlePaymentArgs,
SettlePaymentResult,
SupportedSignatureType,
VerifyPaymentResult,
} from "../types.js";
export { verifyPayment } from "../verify-payment.js";
119 changes: 49 additions & 70 deletions packages/nexus/src/facilitator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { ThirdwebClient } from "thirdweb";
import { stringify } from "thirdweb/utils";
import type { VerifyResponse } from "x402/types";
import type {
FacilitatorSettleResponse,
Expand All @@ -8,14 +6,14 @@ import type {
RequestedPaymentPayload,
RequestedPaymentRequirements,
} from "./schemas.js";
import { stringify } from "./utils.js";

export type WaitUntil = "simulated" | "submitted" | "confirmed";

export type ThirdwebX402FacilitatorConfig = {
client: ThirdwebClient;
serverWalletAddress: string;
walletSecret: string;
walletAddress: string;
waitUntil?: WaitUntil;
vaultAccessToken?: string;
baseUrl?: string;
};

Expand Down Expand Up @@ -47,7 +45,7 @@ export type ThirdwebX402Facilitator = {
}) => Promise<FacilitatorSupportedResponse>;
};

const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402";
const DEFAULT_BASE_URL = "https://nexus-api.thirdweb.com";

/**
* Creates a facilitator for the x402 payment protocol.
Expand All @@ -58,21 +56,17 @@ const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402";
*
* @example
* ```ts
* import { facilitator } from "thirdweb/x402";
* import { createThirdwebClient } from "thirdweb";
* import { createFacilitator } from "@thirdweb-dev/nexus";
* import { paymentMiddleware } from 'x402-hono'
*
* const client = createThirdwebClient({
* secretKey: "your-secret-key",
* });
* const thirdwebX402Facilitator = facilitator({
* client: client,
* serverWalletAddress: "0x1234567890123456789012345678901234567890",
* const facilitator = createFacilitator({
* walletSecret: <your-wallet-secret>,
* walletAddress: <your-wallet-address>,
* });
*
* // add the facilitator to any x402 payment middleware
* const middleware = paymentMiddleware(
* "0x1234567890123456789012345678901234567890",
* facilitator.address,
* {
* "/api/paywall": {
* price: "$0.01",
Expand All @@ -82,16 +76,16 @@ const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402";
* },
* },
* },
* thirdwebX402Facilitator,
* facilitator,
* );
* ```
*
* #### Configuration Options
*
* ```ts
* const thirdwebX402Facilitator = facilitator({
* client: client,
* serverWalletAddress: "0x1234567890123456789012345678901234567890",
* 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
Expand All @@ -101,43 +95,39 @@ const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402";
* ```
*
* @bridge x402
*/
export function facilitator(
export function createFacilitator(
config: ThirdwebX402FacilitatorConfig,
): ThirdwebX402Facilitator {
const secretKey = config.client.secretKey;
if (!secretKey) {
throw new Error("Client secret key is required for the x402 facilitator");
if (!config.walletSecret) {
throw new Error("Wallet secret is required for the x402 facilitator");
}
const serverWalletAddress = config.serverWalletAddress;
if (!serverWalletAddress) {
throw new Error(
"Server wallet address is required for the x402 facilitator",
);

if (!config.walletAddress) {
throw new Error("Wallet address is required for the x402 facilitator");
}
const facilitator = {
url: (config.baseUrl ?? DEFAULT_BASE_URL) as `${string}://${string}`,
address: serverWalletAddress,
createAuthHeaders: async () => {
return {
verify: {
"x-secret-key": secretKey,
},
settle: {
"x-secret-key": secretKey,
...(config.vaultAccessToken
? { "x-vault-access-token": config.vaultAccessToken }
: {}),
},
supported: {
"x-secret-key": secretKey,
},
list: {
"x-secret-key": secretKey,
},
};

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
*
Expand All @@ -149,13 +139,11 @@ export function facilitator(
payload: RequestedPaymentPayload,
paymentRequirements: RequestedPaymentRequirements,
): Promise<FacilitatorVerifyResponse> {
const url = config.baseUrl ?? DEFAULT_BASE_URL;

let headers = { "Content-Type": "application/json" };
const authHeaders = await facilitator.createAuthHeaders();
headers = { ...headers, ...authHeaders.verify };

const res = await fetch(`${url}/verify`, {
headers = { ...headers, ...AUTH_HEADERS.verify };

const res = await fetch(new URL("/verify", BASE_URL), {
method: "POST",
headers,
body: stringify({
Expand Down Expand Up @@ -186,14 +174,11 @@ export function facilitator(
paymentRequirements: RequestedPaymentRequirements,
waitUntil?: WaitUntil,
): Promise<FacilitatorSettleResponse> {
const url = config.baseUrl ?? DEFAULT_BASE_URL;

let headers = { "Content-Type": "application/json" };
const authHeaders = await facilitator.createAuthHeaders();
headers = { ...headers, ...authHeaders.settle };
headers = { ...headers, ...AUTH_HEADERS.settle };
const waitUntilParam = waitUntil || config.waitUntil;

const res = await fetch(`${url}/settle`, {
const res = await fetch(new URL("/settle", BASE_URL), {
method: "POST",
headers,
body: JSON.stringify({
Expand Down Expand Up @@ -222,22 +207,18 @@ export function facilitator(
chainId: number;
tokenAddress?: string;
}): Promise<FacilitatorSupportedResponse> {
const url = config.baseUrl ?? DEFAULT_BASE_URL;

// TODO: re-add caching? (see thirdweb/x402/facilitator.ts)
const authHeaders = await facilitator.createAuthHeaders();
const headers = {
"Content-Type": "application/json",
...authHeaders.supported,
...AUTH_HEADERS.supported,
};
const supportedUrl = new URL(`${url}/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.toString(), { headers });
const res = await fetch(supportedUrl, { headers });

if (res.status !== 200) {
throw new Error(
Expand All @@ -248,7 +229,5 @@ export function facilitator(
const data = await res.json();
return data as FacilitatorSupportedResponse;
},
};

return facilitator;
} as const satisfies ThirdwebX402Facilitator;
}
Loading
Loading