diff --git a/typescript/.changeset/funny-masks-lie.md b/typescript/.changeset/funny-masks-lie.md new file mode 100644 index 000000000..8a6181345 --- /dev/null +++ b/typescript/.changeset/funny-masks-lie.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": patch +--- + +Updated x402 actions to v2 and improved discovery and query/body parameter handling diff --git a/typescript/agentkit/package.json b/typescript/agentkit/package.json index 69ac8a251..3a9ebceb0 100644 --- a/typescript/agentkit/package.json +++ b/typescript/agentkit/package.json @@ -53,12 +53,14 @@ "@solana/spl-token": "^0.4.12", "@solana/web3.js": "^1.98.1", "@vaultsfyi/sdk": "^2.1.9", + "@x402/evm": "^2.0.0", + "@x402/svm": "^2.0.0", + "@x402/fetch": "^2.0.0", "@zerodev/ecdsa-validator": "^5.4.5", "@zerodev/intent": "^0.0.24", "@zerodev/sdk": "^5.4.28", "@zoralabs/coins-sdk": "0.2.8", "@zoralabs/protocol-deployments": "0.6.1", - "axios": "^1.9.0", "bs58": "^4.0.1", "canonicalize": "^2.1.0", "clanker-sdk": "^4.1.18", @@ -71,8 +73,6 @@ "sushi": "6.2.1", "twitter-api-v2": "^1.18.2", "viem": "2.38.3", - "x402": "^0.6.0", - "x402-axios": "^0.6.0", "zod": "^3.23.8" }, "devDependencies": { diff --git a/typescript/agentkit/src/action-providers/x402/README.md b/typescript/agentkit/src/action-providers/x402/README.md index 27ffec4a7..d44ae587f 100644 --- a/typescript/agentkit/src/action-providers/x402/README.md +++ b/typescript/agentkit/src/action-providers/x402/README.md @@ -8,6 +8,7 @@ This directory contains the **X402ActionProvider** implementation, which provide x402/ ├── x402ActionProvider.ts # Main provider with x402 payment functionality ├── schemas.ts # x402 action schemas +├── constants.ts # Network mappings and type definitions ├── index.ts # Main exports ├── utils.ts # Utility functions └── README.md # This file @@ -20,15 +21,17 @@ x402/ 1. `make_http_request`: Make initial HTTP request and handle 402 responses 2. `retry_http_request_with_x402`: Retry a request with payment after receiving payment details -### Alternative Action +### Alternative Actions - `make_http_request_with_x402`: Direct payment-enabled requests (skips confirmation flow) -- `discover_x402_services`: Discover available x402 services (optionally filter by asset and price) +- `discover_x402_services`: Discover available x402 services (optionally filter by price) ## Overview The x402 protocol enables APIs to require micropayments for access. When a client makes a request to a protected endpoint, the server responds with a `402 Payment Required` status code along with payment instructions. +This provider supports **both v1 and v2 x402 endpoints** automatically. + ### Recommended Two-Step Flow 1. Initial Request: @@ -64,21 +67,33 @@ Makes initial request and handles 402 responses: ### `retry_http_request_with_x402` Action -Retries request with payment after 402: +Retries request with payment after 402. Supports both v1 and v2 payment option formats: ```typescript +// v1 format (legacy endpoints) { url: "https://api.example.com/data", - method: "GET", // Optional, defaults to GET - headers: { "Accept": "..." }, // Optional - body: { ... }, // Optional - selectedPaymentOption: { // Payment details from 402 response + method: "GET", + selectedPaymentOption: { scheme: "exact", - network: "base-sepolia", + network: "base-sepolia", // v1 network identifier maxAmountRequired: "1000", asset: "0x..." } } + +// v2 format (CAIP-2 network identifiers) +{ + url: "https://api.example.com/data", + method: "GET", + selectedPaymentOption: { + scheme: "exact", + network: "eip155:84532", // v2 CAIP-2 identifier + amount: "1000", + asset: "0x...", + payTo: "0x..." + } +} ``` ### `make_http_request_with_x402` Action @@ -96,22 +111,33 @@ Direct payment-enabled requests (use with caution): ### `discover_x402_services` Action -Fetches available services and optionally filters them by maximum price in USDC whole units. The action defaults to USDC on the current network: +Fetches all available services from the x402 Bazaar with full pagination support. Returns simplified output with url, price, and description for each service. ```typescript { - maxUsdcPrice: 0.1 // optional (e.g., 0.1 for $0.10 USDC) + facilitator: "cdp", // Optional: 'cdp', 'payai', or custom URL + maxUsdcPrice: 0.1, // Optional, filter by max price in USDC + keyword: "weather", // Optional, filter by description/URL keyword + x402Versions: [1, 2] // Optional, filter by protocol version } ``` -Example filtering for USDC services under $0.10: - -```ts -const maxUsdcPrice = 0.1; - -const services = await discover_x402_services({ maxUsdcPrice }); - +Example response: +```json +{ + "success": true, + "walletNetworks": ["base-sepolia", "eip155:84532"], + "total": 150, + "returned": 25, + "services": [ + { + "url": "https://api.example.com/weather", + "price": "0.001 USDC on base-sepolia", + "description": "Get current weather data" + } + ] +} ``` ## Response Format @@ -132,21 +158,32 @@ Successful responses include payment proof when payment was made: ## Network Support -The x402 provider currently supports the following networks: -- `base-mainnet` -- `base-sepolia` -- `solana-mainnet` -- `solana-devnet` +The x402 provider supports the following networks: + +| Internal Network ID | v1 Identifier | v2 CAIP-2 Identifier | +|---------------------|---------------|----------------------| +| `base-mainnet` | `base` | `eip155:8453` | +| `base-sepolia` | `base-sepolia` | `eip155:84532` | +| `solana-mainnet` | `solana` | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | +| `solana-devnet` | `solana-devnet` | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | + +The provider supports both EVM and SVM (Solana) wallets for signing payment transactions. + +## v1/v2 Compatibility + +This provider automatically handles both v1 and v2 x402 endpoints: -The provider requires EVM-compatible networks where the wallet can sign payment transactions. +- **Discovery**: Filters resources matching either v1 or v2 network identifiers +- **Payment**: The `@x402/fetch` library handles protocol version detection automatically +- **Headers**: Supports both `X-PAYMENT-RESPONSE` (v1) and `PAYMENT-RESPONSE` (v2) headers ## Dependencies This action provider requires: -- `axios` - For making HTTP requests -- `x402-axios` - For handling x402 payment flows -- `x402` - For payment requirement types and validation +- `@x402/fetch` - For handling x402 payment flows +- `@x402/evm` - For EVM payment scheme support +- `@x402/svm` - For Solana payment scheme support ## Notes -For more information on the **x402 protocol**, visit the [x402 documentation](https://x402.gitbook.io/x402/). \ No newline at end of file +For more information on the **x402 protocol**, visit the [x402 documentation](https://docs.cdp.coinbase.com/x402/overview). diff --git a/typescript/agentkit/src/action-providers/x402/constants.ts b/typescript/agentkit/src/action-providers/x402/constants.ts new file mode 100644 index 000000000..961491dd3 --- /dev/null +++ b/typescript/agentkit/src/action-providers/x402/constants.ts @@ -0,0 +1,88 @@ +/** + * Known facilitator registry + */ +export const KNOWN_FACILITATORS = { + cdp: "https://api.cdp.coinbase.com/platform/v2/x402", + payai: "https://facilitator.payai.network", +} as const; + +export type KnownFacilitatorName = keyof typeof KNOWN_FACILITATORS; + +export const DEFAULT_FACILITATOR: KnownFacilitatorName = "cdp"; + +/** + * Supported networks for x402 payment protocol + */ +export const SUPPORTED_NETWORKS = [ + "base-mainnet", + "base-sepolia", + "solana-mainnet", + "solana-devnet", +] as const; + +/** + * USDC token addresses for Solana networks + */ +export const SOLANA_USDC_ADDRESSES = { + "solana-devnet": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", + "solana-mainnet": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", +} as const; + +/** + * Network mapping from internal network ID to both v1 and v2 (CAIP-2) formats. + * Used for filtering discovery results that may contain either format. + */ +export const NETWORK_MAPPINGS: Record = { + "base-mainnet": ["base", "eip155:8453"], + "base-sepolia": ["base-sepolia", "eip155:84532"], + "solana-mainnet": ["solana", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"], + "solana-devnet": ["solana-devnet", "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"], +}; + +/** + * x402 protocol version type + */ +export type X402Version = 1 | 2; + +/** + * Payment option from discovery API (supports both v1 and v2 formats) + */ +export interface PaymentOption { + scheme: string; + network: string; + asset: string; + // v1 format + maxAmountRequired?: string; + // v2 format + amount?: string; + price?: string; + payTo?: string; + description?: string; +} + +/** + * Resource from discovery API + */ +export interface DiscoveryResource { + url?: string; + resource?: string; + type?: string; + metadata?: { + [key: string]: unknown; + description?: string; + input?: Record; + output?: Record; + }; + accepts?: PaymentOption[]; + x402Version?: number; + lastUpdated?: string; +} + +/** + * Simplified resource output for LLM consumption + */ +export interface SimplifiedResource { + url: string; + price: string; + description: string; +} diff --git a/typescript/agentkit/src/action-providers/x402/schemas.ts b/typescript/agentkit/src/action-providers/x402/schemas.ts index 877cacec2..b19b0de7d 100644 --- a/typescript/agentkit/src/action-providers/x402/schemas.ts +++ b/typescript/agentkit/src/action-providers/x402/schemas.ts @@ -1,14 +1,46 @@ import { z } from "zod"; +import { KNOWN_FACILITATORS, KnownFacilitatorName, DEFAULT_FACILITATOR } from "./constants"; + +/** + * Resolves a facilitator name or URL to the actual URL. + * + * @param facilitator - Either a known facilitator name ('cdp', 'payai') or a custom URL + * @returns The facilitator URL + */ +export function resolveFacilitatorUrl(facilitator: string): string { + if (facilitator in KNOWN_FACILITATORS) { + return KNOWN_FACILITATORS[facilitator as KnownFacilitatorName]; + } + return facilitator; +} // Schema for listing x402 services export const ListX402ServicesSchema = z .object({ + facilitator: z + .union([z.enum(["cdp", "payai"]), z.string().url()]) + .default(DEFAULT_FACILITATOR) + .describe( + "Facilitator to query: 'cdp' (Coinbase CDP), 'payai' (PayAI Network), or a custom facilitator URL.", + ), maxUsdcPrice: z .number() + .positive() + .finite() .optional() .describe( "Optional maximum price in USDC whole units (e.g., 0.1 for 0.10 USDC). Only USDC payment options will be considered when this filter is applied.", ), + x402Versions: z + .array(z.union([z.literal(1), z.literal(2)])) + .default([1, 2]) + .describe("Filter by x402 protocol version (1 or 2). Defaults to accepting both versions."), + keyword: z + .string() + .optional() + .describe( + "Optional keyword to filter services by description (case-insensitive). Example: 'weather' to find weather-related services.", + ), }) .strip() .describe("Parameters for listing x402 services with optional filtering"); @@ -30,15 +62,46 @@ export const HttpRequestSchema = z .optional() .nullable() .describe("Optional headers to include in the request"), + queryParams: z + .record(z.string()) + .optional() + .nullable() + .describe( + "Query parameters to append to the URL as key-value string pairs. " + + "Use ONLY for GET/DELETE requests. " + + "For POST/PUT/PATCH, you must use the 'body' parameter instead. " + + "Example: {location: 'NYC', units: 'metric'} becomes ?location=NYC&units=metric", + ), body: z .any() .optional() .nullable() - .describe("Optional request body for POST/PUT/PATCH requests"), + .describe( + "Request body - REQUIRED for POST/PUT/PATCH requests when sending data. " + + "Always prefer 'body' over 'queryParams' for POST/PUT/PATCH. " + + "Do NOT use for GET or DELETE, use queryParams instead.", + ), }) .strip() .describe("Instructions for making a basic HTTP request"); +// Payment option schema that supports both v1 and v2 formats +const PaymentOptionSchema = z + .object({ + scheme: z.string().describe("Payment scheme (e.g., 'exact')"), + network: z + .string() + .describe("Network identifier (v1: 'base-sepolia' or v2 CAIP-2: 'eip155:84532')"), + asset: z.string().describe("Asset address or identifier"), + // v1 format + maxAmountRequired: z.string().optional().describe("Maximum amount required (v1 format)"), + // v2 format + amount: z.string().optional().describe("Amount required (v2 format)"), + price: z.string().optional().describe("Price (v2 format, e.g., '$0.01')"), + payTo: z.string().optional().describe("Payment recipient address (v2 format)"), + }) + .describe("Payment option supporting both v1 and v2 x402 formats"); + // Schema for retrying a failed request with x402 payment export const RetryWithX402Schema = z .object({ @@ -52,15 +115,27 @@ export const RetryWithX402Schema = z .default("GET") .describe("The HTTP method to use for the request"), headers: z.record(z.string()).optional().describe("Optional headers to include in the request"), - body: z.any().optional().describe("Optional request body for POST/PUT/PATCH requests"), - selectedPaymentOption: z - .object({ - scheme: z.string(), - network: z.string(), - maxAmountRequired: z.string(), - asset: z.string(), - }) - .describe("The payment option to use for this request"), + queryParams: z + .record(z.string()) + .optional() + .nullable() + .describe( + "Query parameters to append to the URL as key-value string pairs. " + + "Use ONLY for GET/DELETE requests. " + + "For POST/PUT/PATCH, you must use the 'body' parameter instead. " + + "Example: {location: 'NYC', units: 'metric'} becomes ?location=NYC&units=metric", + ), + body: z + .any() + .optional() + .describe( + "Request body - REQUIRED for POST/PUT/PATCH requests when sending data. " + + "Always prefer 'body' over 'queryParams' for POST/PUT/PATCH. " + + "Do NOT use for GET or DELETE, use queryParams instead.", + ), + selectedPaymentOption: PaymentOptionSchema.describe( + "The payment option to use for this request", + ), }) .strip() .describe("Instructions for retrying a request with x402 payment after receiving a 402 response"); @@ -82,11 +157,25 @@ export const DirectX402RequestSchema = z .optional() .nullable() .describe("Optional headers to include in the request"), + queryParams: z + .record(z.string()) + .optional() + .nullable() + .describe( + "Query parameters to append to the URL as key-value string pairs. " + + "Use ONLY for GET/DELETE requests. " + + "For POST/PUT/PATCH, use the 'body' parameter instead. " + + "Example: {location: 'NYC', units: 'metric'} becomes ?location=NYC&units=metric", + ), body: z .any() .optional() .nullable() - .describe("Optional request body for POST/PUT/PATCH requests"), + .describe( + "Request body - REQUIRED for POST/PUT/PATCH requests when sending data. " + + "Always prefer 'body' over 'queryParams' for POST/PUT/PATCH. " + + "Do NOT use for GET or DELETE, use queryParams instead.", + ), }) .strip() .describe( diff --git a/typescript/agentkit/src/action-providers/x402/utils.ts b/typescript/agentkit/src/action-providers/x402/utils.ts index 33ee9c0fe..42981d696 100644 --- a/typescript/agentkit/src/action-providers/x402/utils.ts +++ b/typescript/agentkit/src/action-providers/x402/utils.ts @@ -1,59 +1,369 @@ import { Network } from "../../network"; -import { AxiosError } from "axios"; import { getTokenDetails } from "../erc20/utils"; import { TOKEN_ADDRESSES_BY_SYMBOLS } from "../erc20/constants"; import { formatUnits, parseUnits } from "viem"; import { EvmWalletProvider, SvmWalletProvider, WalletProvider } from "../../wallet-providers"; +import { + SOLANA_USDC_ADDRESSES, + NETWORK_MAPPINGS, + type DiscoveryResource, + type SimplifiedResource, + type X402Version, +} from "./constants"; /** - * Supported network types for x402 protocol + * Returns array of matching network identifiers (both v1 and v2 CAIP-2 formats). + * Used for filtering discovery results that may contain either format. + * + * @param network - The network object + * @returns Array of network identifiers that match the wallet's network + */ +export function getX402Networks(network: Network): string[] { + const networkId = network.networkId; + if (!networkId) { + return []; + } + return NETWORK_MAPPINGS[networkId] ?? [networkId]; +} + +/** + * Gets network ID from a CAIP-2 or v1 network identifier. + * + * @param network - The x402 network identifier (e.g., "eip155:8453" for v2 or "base" for v1) + * @returns The network ID (e.g., "base-mainnet") or the original if not found + */ +export function getNetworkId(network: string): string { + for (const [agentKitId, formats] of Object.entries(NETWORK_MAPPINGS)) { + if (formats.includes(network)) { + return agentKitId; + } + } + return network; +} + +/** + * Fetches a URL with retry logic and exponential backoff for rate limiting. + * + * @param url - The URL to fetch + * @param maxRetries - Maximum number of retries (default 3) + * @param initialDelayMs - Initial delay in milliseconds (default 1000) + * @returns The fetch Response + */ +async function fetchWithRetry( + url: string, + maxRetries: number = 3, + initialDelayMs: number = 1000, +): Promise { + let lastError: Error | null = null; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + const response = await fetch(url); + + if (response.ok) { + return response; + } + + if (response.status === 429 && attempt < maxRetries) { + const delayMs = initialDelayMs * Math.pow(2, attempt); + await new Promise(resolve => setTimeout(resolve, delayMs)); + continue; + } + + lastError = new Error(`Discovery API error: ${response.status} ${response.statusText}`); + break; + } + + throw lastError ?? new Error("Failed to fetch after retries"); +} + +/** + * Fetches all resources from the discovery API with pagination. + * + * @param discoveryUrl - The base URL for discovery + * @param pageSize - Number of resources per page (default 100) + * @returns Array of all discovered resources + */ +export async function fetchAllDiscoveryResources( + discoveryUrl: string, + pageSize: number = 1000, +): Promise { + const allResources: DiscoveryResource[] = []; + let offset = 0; + let hasMore = true; + + while (hasMore) { + const url = new URL(discoveryUrl); + url.searchParams.set("limit", pageSize.toString()); + url.searchParams.set("offset", offset.toString()); + + const response = await fetchWithRetry(url.toString()); + + const data = await response.json(); + const resources = data.resources ?? data.items ?? []; + + if (resources.length === 0) { + hasMore = false; + } else { + allResources.push(...resources); + offset += resources.length; + + // Stop when getting fewer resources than requested (last page) + if (resources.length < pageSize) { + hasMore = false; + } + } + } + + return allResources; +} + +/** + * Filters resources by network compatibility. + * Matches resources that accept any of the wallet's network identifiers (v1 or v2 format). + * + * @param resources - Array of discovery resources + * @param walletNetworks - Array of network identifiers to match + * @returns Filtered array of resources + */ +export function filterByNetwork( + resources: DiscoveryResource[], + walletNetworks: string[], +): DiscoveryResource[] { + return resources.filter(resource => { + const accepts = resource.accepts ?? []; + return accepts.some(option => walletNetworks.includes(option.network)); + }); +} + +/** + * Extracts description from a resource based on its x402 version. + * - v1: description is in accepts[].description + * - v2: description is in metadata.description + * + * @param resource - The discovery resource + * @returns The description string or empty string if not found + */ +function getResourceDescription(resource: DiscoveryResource): string { + if (resource.x402Version === 2) { + const metadataDesc = resource.metadata?.description; + return typeof metadataDesc === "string" ? metadataDesc : ""; + } + + // v1: look in accepts[].description + const accepts = resource.accepts ?? []; + for (const option of accepts) { + if (option.description?.trim()) { + return option.description; + } + } + return ""; +} + +/** + * Filters resources by having a valid description. + * Removes resources with empty or default descriptions. + * Supports both v1 (accepts[].description) and v2 (metadata.description) formats. + * + * @param resources - Array of discovery resources + * @returns Filtered array of resources with valid descriptions */ -export type X402Network = "base" | "base-sepolia" | "solana" | "solana-devnet"; +export function filterByDescription(resources: DiscoveryResource[]): DiscoveryResource[] { + return resources.filter(resource => { + const desc = getResourceDescription(resource).trim(); + return desc && desc !== "" && desc !== "Access to protected content"; + }); +} + +/** + * Filters resources by x402 protocol version. + * Uses the x402Version field on the resource. + * + * @param resources - Array of discovery resources + * @param allowedVersions - Array of allowed versions (default: [1, 2]) + * @returns Filtered array of resources matching the allowed versions + */ +export function filterByX402Version( + resources: DiscoveryResource[], + allowedVersions: X402Version[] = [1, 2], +): DiscoveryResource[] { + return resources.filter(resource => { + const version = resource.x402Version; + if (version === undefined) { + return true; // Include resources without version info + } + return allowedVersions.includes(version as X402Version); + }); +} /** - * USDC token addresses for Solana networks + * Filters resources by keyword appearing in description or URL. + * Case-insensitive search. + * Supports both v1 (accepts[].description) and v2 (metadata.description) formats. + * + * @param resources - Array of discovery resources + * @param keyword - The keyword to search for in descriptions and URLs + * @returns Filtered array of resources with matching descriptions or URLs */ -const SOLANA_USDC_ADDRESSES = { - "solana-devnet": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", - "solana-mainnet": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", -} as const; +export function filterByKeyword( + resources: DiscoveryResource[], + keyword: string, +): DiscoveryResource[] { + const lowerKeyword = keyword.toLowerCase(); + return resources.filter(resource => { + // Check description (version-aware) + const desc = getResourceDescription(resource).toLowerCase(); + if (desc.includes(lowerKeyword)) { + return true; + } + + // Also check the URL for keyword matches + const url = (resource.resource ?? resource.url ?? "").toLowerCase(); + if (url.includes(lowerKeyword)) { + return true; + } + + return false; + }); +} + +/** + * Filters resources by maximum USDC price. + * + * @param resources - Array of discovery resources + * @param maxUsdcPrice - Maximum price in whole USDC units + * @param walletProvider - Wallet provider for asset identification + * @param walletNetworks - Array of network identifiers to match + * @returns Filtered array of resources within price limit + */ +export async function filterByMaxPrice( + resources: DiscoveryResource[], + maxUsdcPrice: number, + walletProvider: WalletProvider, + walletNetworks: string[], +): Promise { + const filtered: DiscoveryResource[] = []; + + for (const resource of resources) { + const accepts = resource.accepts ?? []; + let shouldInclude = false; + + for (const option of accepts) { + if (!walletNetworks.includes(option.network)) { + continue; + } + + if (!option.asset) { + continue; + } + + // Check if this is a USDC asset + if (!isUsdcAsset(option.asset, walletProvider)) { + continue; + } + + // Get the amount (supports both v1 maxAmountRequired and v2 amount/price) + const amountStr = option.maxAmountRequired ?? option.amount ?? option.price; + if (!amountStr) { + continue; + } + + try { + const maxUsdcPriceAtomic = await convertWholeUnitsToAtomic( + maxUsdcPrice, + option.asset, + walletProvider, + ); + if (maxUsdcPriceAtomic) { + const resourceAmount = BigInt(amountStr); + const maxAmount = BigInt(maxUsdcPriceAtomic); + if (resourceAmount <= maxAmount) { + shouldInclude = true; + break; + } + } + } catch { + // Skip if conversion fails + continue; + } + } + + if (shouldInclude) { + filtered.push(resource); + } + } + + return filtered; +} /** - * Converts the internal network ID to the format expected by the x402 protocol. + * Formats resources into simplified output for LLM consumption. * - * @param network - The network to convert - * @returns The network ID in x402 format - * @throws Error if the network is not supported + * @param resources - Array of discovery resources + * @param walletNetworks - Array of network identifiers to match for price extraction + * @param walletProvider - Wallet provider for formatting + * @returns Array of simplified resources with url, price, description */ -export function getX402Network(network: Network): X402Network | string | undefined { - switch (network.networkId) { - case "base-mainnet": - return "base"; - case "base-sepolia": - return "base-sepolia"; - case "solana-mainnet": - return "solana"; - case "solana-devnet": - return "solana-devnet"; - default: - return network.networkId; +export async function formatSimplifiedResources( + resources: DiscoveryResource[], + walletNetworks: string[], + walletProvider: WalletProvider, +): Promise { + const simplified: SimplifiedResource[] = []; + + for (const resource of resources) { + const accepts = resource.accepts ?? []; + const matchingOption = accepts.find(opt => walletNetworks.includes(opt.network)); + + if (!matchingOption) { + continue; + } + + // Extract URL: v1 and v2 both use resource.resource, but v2 docs show resource.url + const url = resource.resource ?? resource.url ?? ""; + + // Extract description (version-aware via helper) + const description = getResourceDescription(resource); + + let price = "Unknown"; + + // Get the amount (supports both v1 and v2 formats) + const amountStr = + matchingOption.maxAmountRequired ?? matchingOption.amount ?? matchingOption.price; + if (amountStr && matchingOption.asset) { + price = await formatPaymentOption( + { + asset: matchingOption.asset, + maxAmountRequired: amountStr, + network: matchingOption.network, + }, + walletProvider, + ); + } + + simplified.push({ + url, + price, + description, + }); } + + return simplified; } /** * Helper method to handle HTTP errors consistently. * - * @param error - The axios error to handle + * @param error - The error to handle * @param url - The URL that was being accessed when the error occurred * @returns A JSON string containing formatted error details */ -export function handleHttpError(error: AxiosError, url: string): string { - if (error.response) { +export function handleHttpError(error: unknown, url: string): string { + if (error instanceof Response) { return JSON.stringify( { error: true, - message: `HTTP ${error.response.status} error when accessing ${url}`, - details: (error.response.data as { error?: string })?.error || error.response.statusText, + message: `HTTP ${error.status} error when accessing ${url}`, + details: error.statusText, suggestion: "Check if the URL is correct and the API is available.", }, null, @@ -61,7 +371,7 @@ export function handleHttpError(error: AxiosError, url: string): string { ); } - if (error.request) { + if (error instanceof TypeError && error.message.includes("fetch")) { return JSON.stringify( { error: true, @@ -74,11 +384,12 @@ export function handleHttpError(error: AxiosError, url: string): string { ); } + const message = error instanceof Error ? error.message : String(error); return JSON.stringify( { error: true, message: `Error making request to ${url}`, - details: error.message, + details: message, suggestion: "Please check the request parameters and try again.", }, null, @@ -116,7 +427,7 @@ export async function formatPaymentOption( if (asset.toLowerCase() === address.toLowerCase()) { const decimals = symbol === "USDC" || symbol === "EURC" ? 6 : 18; const formattedAmount = formatUnits(BigInt(maxAmountRequired), decimals); - return `${formattedAmount} ${symbol} on ${network} network`; + return `${formattedAmount} ${symbol} on ${getNetworkId(network)}`; } } } @@ -126,7 +437,7 @@ export async function formatPaymentOption( const tokenDetails = await getTokenDetails(walletProvider, asset); if (tokenDetails) { const formattedAmount = formatUnits(BigInt(maxAmountRequired), tokenDetails.decimals); - return `${formattedAmount} ${tokenDetails.name} on ${network} network`; + return `${formattedAmount} ${tokenDetails.name} on ${getNetworkId(network)}`; } } catch { // If we can't get token details, fall back to raw format @@ -141,12 +452,12 @@ export async function formatPaymentOption( if (usdcAddress && asset === usdcAddress) { // USDC has 6 decimals on Solana const formattedAmount = formatUnits(BigInt(maxAmountRequired), 6); - return `${formattedAmount} USDC on ${network} network`; + return `${formattedAmount} USDC on ${getNetworkId(network)}`; } } // Fallback to original format for non-EVM/SVM networks or when token details can't be fetched - return `${asset} ${maxAmountRequired} on ${network} network`; + return `${asset} ${maxAmountRequired} on ${getNetworkId(network)}`; } /** @@ -238,3 +549,24 @@ export async function convertWholeUnitsToAtomic( // Fallback to 18 decimals for unknown tokens or non-EVM/SVM networks return parseUnits(wholeUnits.toString(), 18).toString(); } + +/** + * Builds a URL with query parameters appended. + * + * @param baseUrl - The base URL + * @param queryParams - Optional query parameters to append + * @returns URL string with query parameters + */ +export function buildUrlWithParams( + baseUrl: string, + queryParams?: Record | null, +): string { + if (!queryParams || Object.keys(queryParams).length === 0) { + return baseUrl; + } + const url = new URL(baseUrl); + Object.entries(queryParams).forEach(([key, value]) => { + url.searchParams.append(key, value); + }); + return url.toString(); +} diff --git a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.test.ts b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.test.ts index e3a8a4ec5..e3af83218 100644 --- a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.test.ts @@ -1,95 +1,73 @@ import { X402ActionProvider } from "./x402ActionProvider"; import { EvmWalletProvider } from "../../wallet-providers"; import { Network } from "../../network"; -import { AxiosError, AxiosResponse, AxiosRequestConfig, AxiosInstance } from "axios"; -import axios from "axios"; -import * as x402axios from "x402-axios"; -import * as x402Verify from "x402/verify"; -import * as utils from "./utils"; +import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; +import { registerExactEvmScheme } from "@x402/evm/exact/client"; +import { registerExactSvmScheme } from "@x402/svm/exact/client"; -// Mock external facilitator dependency -jest.mock("@coinbase/x402", () => ({ - facilitator: {}, -})); +import * as utils from "./utils"; +import { resolveFacilitatorUrl } from "./schemas"; -// Mock modules -jest.mock("axios"); -jest.mock("x402-axios"); -jest.mock("x402/verify"); +// Mock external modules +jest.mock("@x402/fetch"); +jest.mock("@x402/evm/exact/client"); +jest.mock("@x402/svm/exact/client"); jest.mock("./utils"); +jest.mock("./schemas", () => ({ + ...jest.requireActual("./schemas"), + resolveFacilitatorUrl: jest.fn(), +})); // Create mock functions -const mockRequest = jest.fn(); - -// Create a complete mock axios instance -const mockAxiosInstance = { - request: mockRequest, - get: jest.fn(), - delete: jest.fn(), - head: jest.fn(), - options: jest.fn(), - post: jest.fn(), - put: jest.fn(), - patch: jest.fn(), - getUri: jest.fn(), - defaults: {}, - interceptors: { - request: { use: jest.fn(), eject: jest.fn(), clear: jest.fn() }, - response: { use: jest.fn(), eject: jest.fn(), clear: jest.fn() }, - }, -} as unknown as AxiosInstance; - -// Create a complete mock axios static -const mockAxios = { - create: jest.fn().mockReturnValue(mockAxiosInstance), - request: mockRequest, - get: jest.fn(), - delete: jest.fn(), - head: jest.fn(), - options: jest.fn(), - post: jest.fn(), - put: jest.fn(), - patch: jest.fn(), - all: jest.fn(), - spread: jest.fn(), - isAxiosError: jest.fn(), - isCancel: jest.fn(), - CancelToken: { - source: jest.fn(), - }, - VERSION: "1.x", -} as unknown as jest.Mocked; - -const mockWithPaymentInterceptor = jest.fn().mockReturnValue(mockAxiosInstance); -const mockDecodeXPaymentResponse = jest.fn(); -const mockUseFacilitator = jest.fn(); -const mockIsUsdcAsset = jest.fn(); -const mockConvertWholeUnitsToAtomic = jest.fn(); -const mockFormatPaymentOption = jest.fn(); -const mockGetX402Network = jest.fn(); - -// Override the mocked modules -(axios as jest.Mocked).create = mockAxios.create; -(axios as jest.Mocked).request = mockRequest; -(axios as jest.Mocked).isAxiosError = mockAxios.isAxiosError; +const mockFetch = jest.fn(); +const mockFetchWithPayment = jest.fn(); -// Mock x402-axios functions -jest.mocked(x402axios.withPaymentInterceptor).mockImplementation(mockWithPaymentInterceptor); -jest.mocked(x402axios.decodeXPaymentResponse).mockImplementation(mockDecodeXPaymentResponse); -jest.mocked(x402Verify.useFacilitator).mockImplementation(mockUseFacilitator); +// Mock x402 client +const mockX402Client = { + registerScheme: jest.fn(), +}; // Mock utils functions -jest.mocked(utils.isUsdcAsset).mockImplementation(mockIsUsdcAsset); -jest.mocked(utils.convertWholeUnitsToAtomic).mockImplementation(mockConvertWholeUnitsToAtomic); +const mockGetX402Networks = jest.fn(); +const mockHandleHttpError = jest.fn(); +const mockFormatPaymentOption = jest.fn(); +const mockFetchAllDiscoveryResources = jest.fn(); +const mockFilterByNetwork = jest.fn(); +const mockFilterByDescription = jest.fn(); +const mockFilterByX402Version = jest.fn(); +const mockFilterByKeyword = jest.fn(); +const mockFilterByMaxPrice = jest.fn(); +const mockFormatSimplifiedResources = jest.fn(); +const mockBuildUrlWithParams = jest.fn(); +const mockResolveFacilitatorUrl = jest.fn(); + +// Setup mocks +jest + .mocked(x402Client) + .mockImplementation(() => mockX402Client as unknown as InstanceType); +jest.mocked(wrapFetchWithPayment).mockReturnValue(mockFetchWithPayment); +jest + .mocked(registerExactEvmScheme) + .mockImplementation(() => mockX402Client as unknown as InstanceType); +jest + .mocked(registerExactSvmScheme) + .mockImplementation(() => mockX402Client as unknown as InstanceType); + +jest.mocked(utils.getX402Networks).mockImplementation(mockGetX402Networks); +jest.mocked(utils.handleHttpError).mockImplementation(mockHandleHttpError); jest.mocked(utils.formatPaymentOption).mockImplementation(mockFormatPaymentOption); -jest.mocked(utils.getX402Network).mockImplementation(mockGetX402Network); -jest.mocked(utils.handleHttpError).mockImplementation((error, url) => { - return JSON.stringify({ - error: true, - message: error instanceof Error ? error.message : "Network error", - url: url, - }); -}); +jest.mocked(utils.fetchAllDiscoveryResources).mockImplementation(mockFetchAllDiscoveryResources); +jest.mocked(utils.filterByNetwork).mockImplementation(mockFilterByNetwork); +jest.mocked(utils.filterByDescription).mockImplementation(mockFilterByDescription); +jest.mocked(utils.filterByX402Version).mockImplementation(mockFilterByX402Version); +jest.mocked(utils.filterByKeyword).mockImplementation(mockFilterByKeyword); +jest.mocked(utils.filterByMaxPrice).mockImplementation(mockFilterByMaxPrice); +jest.mocked(utils.formatSimplifiedResources).mockImplementation(mockFormatSimplifiedResources); +jest.mocked(utils.buildUrlWithParams).mockImplementation(mockBuildUrlWithParams); +jest.mocked(resolveFacilitatorUrl).mockImplementation(mockResolveFacilitatorUrl); + +// Mock global fetch +global.fetch = mockFetch; // Mock wallet provider const makeMockWalletProvider = (networkId: string) => { @@ -101,38 +79,51 @@ const makeMockWalletProvider = (networkId: string) => { // Sample responses based on real examples const MOCK_PAYMENT_INFO_RESPONSE = { - paymentRequired: true, - url: "https://www.x402.org/protected", - status: 402, - data: { - x402Version: 1, - error: "X-PAYMENT header is required", - accepts: [ - { - scheme: "exact", - network: "base-sepolia", - maxAmountRequired: "10000", - resource: "https://www.x402.org/protected", - description: "Access to protected content", - mimeType: "application/json", - payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", - maxTimeoutSeconds: 300, - asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - extra: { - name: "USDC", - version: "2", - }, + x402Version: 1, + error: "X-PAYMENT header is required", + accepts: [ + { + scheme: "exact", + network: "base-sepolia", + maxAmountRequired: "10000", + resource: "https://www.x402.org/protected", + description: "Access to protected content", + mimeType: "application/json", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + maxTimeoutSeconds: 300, + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + extra: { + name: "USDC", + version: "2", }, - ], - }, + }, + ], +}; + +const MOCK_PAYMENT_PROOF = { + transaction: "0xcbc385789d3744b52af5106c32809534f64adcbe097e050ec03d6b53fed5d305", + network: "base-sepolia", + payer: "0xa8c1a5D3C372C65c04f91f87a43F549619A9483f", }; -const MOCK_PAYMENT_RESPONSE = { - success: true, - transaction: - "0xcbc385789d3744b52af5106c32809534f64adcbe097e050ec03d6b53fed5d305" as `0x${string}`, - network: "base-sepolia" as const, - payer: "0xa8c1a5D3C372C65c04f91f87a43F549619A9483f" as `0x${string}`, +// Helper to create mock Response +const createMockResponse = (options: { + status: number; + statusText?: string; + data?: unknown; + headers?: Record; +}): Response => { + const headersMap = new Map(Object.entries(options.headers ?? {})); + return { + status: options.status, + statusText: options.statusText ?? "OK", + ok: options.status >= 200 && options.status < 300, + headers: { + get: (name: string) => headersMap.get(name.toLowerCase()) ?? null, + }, + json: jest.fn().mockResolvedValue(options.data), + text: jest.fn().mockResolvedValue(JSON.stringify(options.data)), + } as unknown as Response; }; describe("X402ActionProvider", () => { @@ -142,26 +133,18 @@ describe("X402ActionProvider", () => { provider = new X402ActionProvider(); jest.clearAllMocks(); - // Setup mocks - mockAxios.create.mockReturnValue(mockAxiosInstance); - mockWithPaymentInterceptor.mockReturnValue(mockAxiosInstance); - - // Setup axios.isAxiosError mock - jest - .mocked(axios.isAxiosError) - .mockImplementation((error: unknown): boolean => - Boolean( - error && - typeof error === "object" && - ("isAxiosError" in error || "response" in error || "request" in error), - ), - ); - - // Reset all utility mocks to default behavior - mockGetX402Network.mockImplementation(network => network.networkId); - mockIsUsdcAsset.mockReturnValue(false); - mockConvertWholeUnitsToAtomic.mockResolvedValue("100000"); + // Setup default mock behaviors + mockGetX402Networks.mockImplementation(network => [network.networkId]); + mockBuildUrlWithParams.mockImplementation(url => url); + mockHandleHttpError.mockImplementation((error, url) => { + return JSON.stringify({ + error: true, + message: error instanceof Error ? error.message : "Network error", + url: url, + }); + }); mockFormatPaymentOption.mockResolvedValue("mocked payment option"); + mockResolveFacilitatorUrl.mockReturnValue("https://api.cdp.coinbase.com/platform/v2/x402"); }); afterEach(() => { @@ -197,12 +180,13 @@ describe("X402ActionProvider", () => { describe("makeHttpRequest", () => { it("should handle successful non-payment requests", async () => { - mockRequest.mockResolvedValue({ - status: 200, - data: { message: "Success" }, - headers: {}, - config: {} as AxiosRequestConfig, - } as AxiosResponse); + mockFetch.mockResolvedValue( + createMockResponse({ + status: 200, + data: { message: "Success" }, + headers: { "content-type": "application/json" }, + }), + ); const result = await provider.makeHttpRequest(makeMockWalletProvider("base-sepolia"), { url: "https://api.example.com/free", @@ -216,15 +200,16 @@ describe("X402ActionProvider", () => { }); it("should handle 402 responses with payment options", async () => { - mockGetX402Network.mockReturnValue("base-sepolia"); + mockGetX402Networks.mockReturnValue(["base-sepolia"]); mockFormatPaymentOption.mockResolvedValue("10000 USDC on base-sepolia network"); - mockRequest.mockResolvedValue({ - status: 402, - data: MOCK_PAYMENT_INFO_RESPONSE.data, - headers: {}, - config: {} as AxiosRequestConfig, - } as AxiosResponse); + mockFetch.mockResolvedValue( + createMockResponse({ + status: 402, + data: MOCK_PAYMENT_INFO_RESPONSE, + headers: { "content-type": "application/json" }, + }), + ); const result = await provider.makeHttpRequest(makeMockWalletProvider("base-sepolia"), { url: "https://www.x402.org/protected", @@ -233,18 +218,13 @@ describe("X402ActionProvider", () => { const parsedResult = JSON.parse(result); expect(parsedResult.status).toBe("error_402_payment_required"); - expect(parsedResult.acceptablePaymentOptions).toEqual( - MOCK_PAYMENT_INFO_RESPONSE.data.accepts, - ); + expect(parsedResult.acceptablePaymentOptions).toEqual(MOCK_PAYMENT_INFO_RESPONSE.accepts); expect(parsedResult.nextSteps).toBeDefined(); }); it("should handle network errors", async () => { - const error = new Error("Network error") as AxiosError; - error.isAxiosError = true; - error.request = {}; - - mockRequest.mockRejectedValue(error); + const error = new TypeError("fetch failed"); + mockFetch.mockRejectedValue(error); const result = await provider.makeHttpRequest(makeMockWalletProvider("base-sepolia"), { url: "https://api.example.com/endpoint", @@ -253,147 +233,143 @@ describe("X402ActionProvider", () => { const parsedResult = JSON.parse(result); expect(parsedResult.error).toBe(true); - expect(parsedResult.message).toContain("Network error"); + expect(parsedResult.message).toBeDefined(); }); }); - describe("listX402Services", () => { + describe("discoverX402Services", () => { it("should list services without filters", async () => { - const mockList = jest.fn().mockResolvedValue({ - items: [ - { - resource: "https://example.com/service1", - metadata: { category: "test" }, - accepts: [ - { - asset: "0xUSDC", - maxAmountRequired: "90000", - network: "base-sepolia", - scheme: "exact", - description: "Test service 1", - outputSchema: { type: "object" }, - extra: { name: "USDC" }, - }, - ], - }, - ], - }); + const mockResources = [ + { + resource: "https://example.com/service1", + x402Version: 1, + accepts: [ + { + asset: "0xUSDC", + maxAmountRequired: "90000", + network: "base-sepolia", + scheme: "exact", + description: "Test service 1", + }, + ], + }, + ]; - mockUseFacilitator.mockReturnValue({ list: mockList }); - mockGetX402Network.mockReturnValue("base-sepolia"); + const mockSimplified = [ + { + url: "https://example.com/service1", + price: "0.09 USDC on base-sepolia", + description: "Test service 1", + }, + ]; - const result = await provider.discoverX402Services( - makeMockWalletProvider("base-sepolia"), - {}, - ); + mockFetchAllDiscoveryResources.mockResolvedValue(mockResources); + mockFilterByNetwork.mockReturnValue(mockResources); + mockFilterByDescription.mockReturnValue(mockResources); + mockFilterByX402Version.mockReturnValue(mockResources); + mockFormatSimplifiedResources.mockResolvedValue(mockSimplified); + mockGetX402Networks.mockReturnValue(["base-sepolia"]); + + const result = await provider.discoverX402Services(makeMockWalletProvider("base-sepolia"), { + facilitator: "cdp", + x402Versions: [1, 2], + }); const parsed = JSON.parse(result); expect(parsed.success).toBe(true); expect(parsed.total).toBe(1); expect(parsed.returned).toBe(1); - expect(parsed.items.length).toBe(1); + expect(parsed.services.length).toBe(1); }); - it("should filter services by asset and maxPrice", async () => { - const mockList = jest.fn().mockResolvedValue({ - items: [ - { - resource: "https://example.com/service1", - metadata: { category: "test" }, - accepts: [ - { - asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Real USDC address for base-sepolia - maxAmountRequired: "90000", // 0.09 USDC (should pass 0.1 filter) - network: "base-sepolia", - scheme: "exact", - description: "Test service 1", - outputSchema: { type: "object" }, - extra: { name: "USDC" }, - }, - ], - }, - { - resource: "https://example.com/service2", - metadata: { category: "test" }, - accepts: [ - { - asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Real USDC address for base-sepolia - maxAmountRequired: "150000", // 0.15 USDC (should fail 0.1 filter) - network: "base-sepolia", - scheme: "exact", - description: "Test service 2", - outputSchema: { type: "object" }, - extra: { name: "USDC" }, - }, - ], - }, - { - resource: "https://example.com/service3", - metadata: { category: "test" }, - accepts: [ - { - asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Real USDC address for base-sepolia - maxAmountRequired: "50000", // 0.05 USDC (should pass 0.1 filter) - network: "base-sepolia", - scheme: "exact", - description: "Test service 3", - outputSchema: { type: "object" }, - extra: { name: "USDC" }, - }, - ], - }, - ], - }); + it("should filter services by maxPrice", async () => { + const mockResources = [ + { + resource: "https://example.com/service1", + x402Version: 1, + accepts: [ + { + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + maxAmountRequired: "90000", + network: "base-sepolia", + scheme: "exact", + description: "Test service 1", + }, + ], + }, + { + resource: "https://example.com/service2", + x402Version: 1, + accepts: [ + { + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + maxAmountRequired: "150000", + network: "base-sepolia", + scheme: "exact", + description: "Test service 2", + }, + ], + }, + ]; - mockUseFacilitator.mockReturnValue({ list: mockList }); + const filteredResources = [mockResources[0]]; + const mockSimplified = [ + { + url: "https://example.com/service1", + price: "0.09 USDC on base-sepolia", + description: "Test service 1", + }, + ]; - // Mock the utility functions for this test - mockGetX402Network.mockReturnValue("base-sepolia"); - mockIsUsdcAsset.mockReturnValue(true); // All assets are USDC - mockConvertWholeUnitsToAtomic - .mockResolvedValueOnce("100000") // 0.1 USDC in atomic units - .mockResolvedValueOnce("100000") - .mockResolvedValueOnce("100000"); - mockFormatPaymentOption.mockResolvedValue("formatted payment option"); + mockFetchAllDiscoveryResources.mockResolvedValue(mockResources); + mockFilterByNetwork.mockReturnValue(mockResources); + mockFilterByDescription.mockReturnValue(mockResources); + mockFilterByX402Version.mockReturnValue(mockResources); + mockFilterByMaxPrice.mockResolvedValue(filteredResources); + mockFormatSimplifiedResources.mockResolvedValue(mockSimplified); + mockGetX402Networks.mockReturnValue(["base-sepolia"]); const result = await provider.discoverX402Services(makeMockWalletProvider("base-sepolia"), { + facilitator: "cdp", maxUsdcPrice: 0.1, + x402Versions: [1, 2], }); + const parsed = JSON.parse(result); expect(parsed.success).toBe(true); - expect(parsed.returned).toBe(2); - expect(parsed.items.map(item => item.resource)).toEqual( - expect.arrayContaining(["https://example.com/service1", "https://example.com/service3"]), - ); + expect(parsed.returned).toBe(1); + expect(parsed.services[0].url).toBe("https://example.com/service1"); }); - it("should handle errors from facilitator", async () => { - const mockList = jest.fn().mockRejectedValue(new Error("boom")); - mockUseFacilitator.mockReturnValue({ list: mockList }); + it("should handle errors from discovery", async () => { + mockFetchAllDiscoveryResources.mockRejectedValue(new Error("boom")); - const result = await provider.discoverX402Services( - makeMockWalletProvider("base-sepolia"), - {}, - ); + const result = await provider.discoverX402Services(makeMockWalletProvider("base-sepolia"), { + facilitator: "cdp", + x402Versions: [1, 2], + }); const parsed = JSON.parse(result); expect(parsed.error).toBe(true); expect(parsed.message).toContain("Failed to list x402 services"); }); }); - describe("retryHttpRequestWithX402", () => { + describe("retryWithX402", () => { it("should successfully retry with payment", async () => { - mockDecodeXPaymentResponse.mockReturnValue(MOCK_PAYMENT_RESPONSE); - mockGetX402Network.mockReturnValue("base-sepolia"); - - mockRequest.mockResolvedValue({ - status: 200, - statusText: "OK", - data: { message: "Paid content" }, - headers: { - "x-payment-response": "encoded-payment-data", - }, - config: {} as AxiosRequestConfig, - } as AxiosResponse); + mockGetX402Networks.mockReturnValue(["base-sepolia"]); + + // Encode the payment proof as base64 + const encodedPaymentProof = btoa(JSON.stringify(MOCK_PAYMENT_PROOF)); + + mockFetchWithPayment.mockResolvedValue( + createMockResponse({ + status: 200, + data: { message: "Paid content" }, + headers: { + "content-type": "application/json", + "x-payment-response": encodedPaymentProof, + }, + }), + ); const result = await provider.retryWithX402(makeMockWalletProvider("base-sepolia"), { url: "https://www.x402.org/protected", @@ -406,29 +382,17 @@ describe("X402ActionProvider", () => { }, }); - // Update expectation to accept the payment selector function - expect(mockWithPaymentInterceptor).toHaveBeenCalledWith( - mockAxiosInstance, - "mock-signer", - expect.any(Function), - ); + expect(wrapFetchWithPayment).toHaveBeenCalledWith(fetch, mockX402Client); const parsedResult = JSON.parse(result); expect(parsedResult.status).toBe("success"); - expect(parsedResult.details.paymentProof).toEqual({ - transaction: MOCK_PAYMENT_RESPONSE.transaction, - network: MOCK_PAYMENT_RESPONSE.network, - payer: MOCK_PAYMENT_RESPONSE.payer, - }); + expect(parsedResult.details.paymentProof).toEqual(MOCK_PAYMENT_PROOF); }); it("should handle network errors during payment", async () => { - const error = new Error("Network error") as AxiosError; - error.isAxiosError = true; - error.request = {}; - - mockRequest.mockRejectedValue(error); - mockGetX402Network.mockReturnValue("base-sepolia"); + const error = new TypeError("fetch failed"); + mockGetX402Networks.mockReturnValue(["base-sepolia"]); + mockFetchWithPayment.mockRejectedValue(error); const result = await provider.retryWithX402(makeMockWalletProvider("base-sepolia"), { url: "https://www.x402.org/protected", @@ -443,23 +407,24 @@ describe("X402ActionProvider", () => { const parsedResult = JSON.parse(result); expect(parsedResult.error).toBe(true); - expect(parsedResult.message).toContain("Network error"); }); }); describe("makeHttpRequestWithX402", () => { it("should handle successful direct payment requests", async () => { - mockDecodeXPaymentResponse.mockReturnValue(MOCK_PAYMENT_RESPONSE); - - mockRequest.mockResolvedValue({ - status: 200, - statusText: "OK", - data: { message: "Paid content" }, - headers: { - "x-payment-response": "encoded-payment-data", - }, - config: {} as AxiosRequestConfig, - } as AxiosResponse); + // Encode the payment proof as base64 + const encodedPaymentProof = btoa(JSON.stringify(MOCK_PAYMENT_PROOF)); + + mockFetchWithPayment.mockResolvedValue( + createMockResponse({ + status: 200, + data: { message: "Paid content" }, + headers: { + "content-type": "application/json", + "x-payment-response": encodedPaymentProof, + }, + }), + ); const result = await provider.makeHttpRequestWithX402( makeMockWalletProvider("base-sepolia"), @@ -469,28 +434,22 @@ describe("X402ActionProvider", () => { }, ); - expect(mockWithPaymentInterceptor).toHaveBeenCalledWith(mockAxiosInstance, "mock-signer"); + expect(wrapFetchWithPayment).toHaveBeenCalledWith(fetch, mockX402Client); const parsedResult = JSON.parse(result); expect(parsedResult.success).toBe(true); expect(parsedResult.data).toEqual({ message: "Paid content" }); - expect(parsedResult.paymentProof).toEqual({ - transaction: MOCK_PAYMENT_RESPONSE.transaction, - network: MOCK_PAYMENT_RESPONSE.network, - payer: MOCK_PAYMENT_RESPONSE.payer, - }); + expect(parsedResult.paymentProof).toEqual(MOCK_PAYMENT_PROOF); }); it("should handle successful non-payment requests", async () => { - mockDecodeXPaymentResponse.mockReturnValue(null); // No payment made - - mockRequest.mockResolvedValue({ - status: 200, - statusText: "OK", - data: { message: "Free content" }, - headers: {}, - config: {} as AxiosRequestConfig, - } as AxiosResponse); + mockFetchWithPayment.mockResolvedValue( + createMockResponse({ + status: 200, + data: { message: "Free content" }, + headers: { "content-type": "application/json" }, + }), + ); const result = await provider.makeHttpRequestWithX402( makeMockWalletProvider("base-sepolia"), @@ -507,11 +466,8 @@ describe("X402ActionProvider", () => { }); it("should handle network errors", async () => { - const error = new Error("Network error") as AxiosError; - error.isAxiosError = true; - error.request = {}; - - mockRequest.mockRejectedValue(error); + const error = new TypeError("fetch failed"); + mockFetchWithPayment.mockRejectedValue(error); const result = await provider.makeHttpRequestWithX402( makeMockWalletProvider("base-sepolia"), @@ -523,7 +479,6 @@ describe("X402ActionProvider", () => { const parsedResult = JSON.parse(result); expect(parsedResult.error).toBe(true); - expect(parsedResult.message).toContain("Network error"); }); }); }); diff --git a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts index 3d9aaeaed..5ed22c17d 100644 --- a/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts +++ b/typescript/agentkit/src/action-providers/x402/x402ActionProvider.ts @@ -7,22 +7,26 @@ import { RetryWithX402Schema, DirectX402RequestSchema, ListX402ServicesSchema, + resolveFacilitatorUrl, } from "./schemas"; import { EvmWalletProvider, WalletProvider, SvmWalletProvider } from "../../wallet-providers"; -import axios, { AxiosError } from "axios"; -import { withPaymentInterceptor, decodeXPaymentResponse } from "x402-axios"; -import { PaymentRequirements } from "x402/types"; -import { useFacilitator } from "x402/verify"; -import { facilitator } from "@coinbase/x402"; +import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; +import { registerExactEvmScheme } from "@x402/evm/exact/client"; +import { registerExactSvmScheme } from "@x402/svm/exact/client"; import { - getX402Network, + getX402Networks, handleHttpError, formatPaymentOption, - convertWholeUnitsToAtomic, - isUsdcAsset, + fetchAllDiscoveryResources, + filterByNetwork, + filterByDescription, + filterByX402Version, + filterByKeyword, + filterByMaxPrice, + formatSimplifiedResources, + buildUrlWithParams, } from "./utils"; - -const SUPPORTED_NETWORKS = ["base-mainnet", "base-sepolia", "solana-mainnet", "solana-devnet"]; +import { SUPPORTED_NETWORKS } from "./constants"; /** * X402ActionProvider provides actions for making HTTP requests, with optional x402 payment handling. @@ -40,7 +44,7 @@ export class X402ActionProvider extends ActionProvider { * Discovers available x402 services with optional filtering. * * @param walletProvider - The wallet provider to use for network filtering - * @param args - Optional filters: maxUsdcPrice + * @param args - Optional filters: discoveryUrl, maxUsdcPrice * @returns JSON string with the list of services (filtered by network and description) */ @CreateAction({ @@ -54,121 +58,57 @@ export class X402ActionProvider extends ActionProvider { args: z.infer, ): Promise { try { - const { list } = useFacilitator(facilitator); - const services = await list(); - if (!services || !services.items) { + console.log("args", args); + const facilitatorUrl = resolveFacilitatorUrl(args.facilitator); + const discoveryUrl = facilitatorUrl + "/discovery/resources"; + + // Fetch all resources with pagination + const allResources = await fetchAllDiscoveryResources(discoveryUrl); + + if (allResources.length === 0) { return JSON.stringify({ error: true, message: "No services found", }); } - // Get the current wallet network - const walletNetwork = getX402Network(walletProvider.getNetwork()); - - // Filter services by network, description, and optional USDC price - const hasValidMaxUsdcPrice = - typeof args.maxUsdcPrice === "number" && - Number.isFinite(args.maxUsdcPrice) && - args.maxUsdcPrice > 0; - - const filteredServices = services.items.filter(item => { - // Filter by network - only include services that accept the current wallet network - const accepts = Array.isArray(item.accepts) ? item.accepts : []; - const hasMatchingNetwork = accepts.some(req => req.network === walletNetwork); - - // Filter out services with empty or default descriptions - const hasDescription = accepts.some( - req => - req.description && - req.description.trim() !== "" && - req.description.trim() !== "Access to protected content", - ); + // Get the wallet's network identifiers (both v1 and v2 formats) + const walletNetworks = getX402Networks(walletProvider.getNetwork()); - return hasMatchingNetwork && hasDescription; - }); + // Apply filter pipeline + let filteredResources = filterByNetwork(allResources, walletNetworks); + filteredResources = filterByDescription(filteredResources); + filteredResources = filterByX402Version(filteredResources, args.x402Versions); - // Apply USDC price filtering if maxUsdcPrice is provided (only consider USDC assets) - let priceFilteredServices = filteredServices; - if (hasValidMaxUsdcPrice) { - priceFilteredServices = []; - for (const item of filteredServices) { - const accepts = Array.isArray(item.accepts) ? item.accepts : []; - let shouldInclude = false; - - for (const req of accepts) { - if (req.network === walletNetwork && req.asset && req.maxAmountRequired) { - // Only consider USDC assets when maxUsdcPrice filter is applied - if (isUsdcAsset(req.asset, walletProvider)) { - try { - const maxUsdcPriceAtomic = await convertWholeUnitsToAtomic( - args.maxUsdcPrice as number, - req.asset, - walletProvider, - ); - if (maxUsdcPriceAtomic) { - const requirement = BigInt(req.maxAmountRequired); - const maxUsdcPriceAtomicBigInt = BigInt(maxUsdcPriceAtomic); - if (requirement <= maxUsdcPriceAtomicBigInt) { - shouldInclude = true; - break; - } - } - } catch { - // If conversion fails, skip this requirement - continue; - } - } - } - } - - if (shouldInclude) { - priceFilteredServices.push(item); - } - } + // Apply keyword filter if provided + if (args.keyword) { + filteredResources = filterByKeyword(filteredResources, args.keyword); } - // Format the filtered services - const filtered = await Promise.all( - priceFilteredServices.map(async item => { - const accepts = Array.isArray(item.accepts) ? item.accepts : []; - const matchingAccept = accepts.find(req => req.network === walletNetwork); + // Apply price filter if maxUsdcPrice is provided + if (args.maxUsdcPrice !== undefined) { + filteredResources = await filterByMaxPrice( + filteredResources, + args.maxUsdcPrice, + walletProvider, + walletNetworks, + ); + } - // Format amount if available - let formattedMaxAmount = matchingAccept?.maxAmountRequired; - if (matchingAccept?.maxAmountRequired && matchingAccept?.asset) { - formattedMaxAmount = await formatPaymentOption( - { - asset: matchingAccept.asset, - maxAmountRequired: matchingAccept.maxAmountRequired, - network: matchingAccept.network, - }, - walletProvider, - ); - } - - return { - resource: item.resource, - description: matchingAccept?.description || "", - cost: formattedMaxAmount, - ...(matchingAccept?.outputSchema?.input && { - input: matchingAccept.outputSchema.input, - }), - ...(matchingAccept?.outputSchema?.output && { - output: matchingAccept.outputSchema.output, - }), - ...(item.metadata && item.metadata.length > 0 && { metadata: item.metadata }), - }; - }), + // Format simplified output + const simplifiedResources = await formatSimplifiedResources( + filteredResources, + walletNetworks, + walletProvider, ); return JSON.stringify( { success: true, - walletNetwork, - total: services.items.length, - returned: filtered.length, - items: filtered, + services: simplifiedResources, + walletNetworks, + total: allResources.length, + returned: simplifiedResources.length, }, null, 2, @@ -203,9 +143,9 @@ it will return payment details that can be used with retry_http_request_with_x40 EXAMPLES: - Production API: make_http_request("https://api.example.com/weather") - Local development: make_http_request("http://localhost:3000/api/data") -- Testing x402: make_http_request("http://localhost:3000/protected") -If you receive a 402 Payment Required response, use retry_http_request_with_x402 to handle the payment.`, +If you receive a 402 Payment Required response, use retry_http_request_with_x402 to handle the payment. +`, schema: HttpRequestSchema, }) async makeHttpRequest( @@ -213,59 +153,124 @@ If you receive a 402 Payment Required response, use retry_http_request_with_x402 args: z.infer, ): Promise { try { - const response = await axios.request({ - url: args.url, - method: args.method ?? "GET", + const finalUrl = buildUrlWithParams(args.url, args.queryParams); + let method = args.method ?? "GET"; + let canHaveBody = ["POST", "PUT", "PATCH"].includes(method); + + let response = await fetch(finalUrl, { + method, headers: args.headers ?? undefined, - data: args.body, - validateStatus: status => status === 402 || (status >= 200 && status < 300), + body: canHaveBody && args.body ? JSON.stringify(args.body) : undefined, }); + // Retry with other http method for 404 status code + if (response.status === 404) { + method = method === "GET" ? "POST" : "GET"; + canHaveBody = ["POST", "PUT", "PATCH"].includes(method); + response = await fetch(finalUrl, { + method, + headers: args.headers ?? undefined, + body: canHaveBody && args.body ? JSON.stringify(args.body) : undefined, + }); + } + if (response.status !== 402) { + const data = await this.parseResponseData(response); return JSON.stringify( { success: true, - url: args.url, - method: args.method, + url: finalUrl, + method, status: response.status, - data: response.data, + data, }, null, 2, ); } - // Check if wallet network matches any available payment options - const walletNetwork = getX402Network(walletProvider.getNetwork()); - const availableNetworks = response.data.accepts.map(option => option.network); - const hasMatchingNetwork = availableNetworks.includes(walletNetwork); + // Handle 402 Payment Required + // v2 sends requirements in PAYMENT-REQUIRED header; v1 sends in body + const walletNetworks = getX402Networks(walletProvider.getNetwork()); + + let acceptsArray: Array<{ + scheme?: string; + network: string; + asset: string; + maxAmountRequired?: string; + amount?: string; + payTo?: string; + }> = []; + let paymentData: Record = {}; + + // Check for v2 header-based payment requirements + const paymentRequiredHeader = response.headers.get("payment-required"); + if (paymentRequiredHeader) { + try { + const decoded = JSON.parse(atob(paymentRequiredHeader)); + acceptsArray = decoded.accepts ?? []; + paymentData = decoded; + } catch { + // Header parsing failed, fall back to body + } + } + + // Fall back to v1 body-based requirements if header not present or empty + if (acceptsArray.length === 0) { + paymentData = await response.json(); + acceptsArray = (paymentData.accepts as typeof acceptsArray) ?? []; + } + + const availableNetworks = acceptsArray.map(option => option.network); + const hasMatchingNetwork = availableNetworks.some((net: string) => + walletNetworks.includes(net), + ); + + let paymentOptionsText = `The wallet networks ${walletNetworks.join(", ")} do not match any available payment options (${availableNetworks.join(", ")}).`; - let paymentOptionsText = `The wallet network ${walletNetwork} does not match any available payment options (${availableNetworks.join(", ")}).`; - // Format payment options for matching networks if (hasMatchingNetwork) { - const matchingOptions = response.data.accepts.filter( - option => option.network === walletNetwork, + const matchingOptions = acceptsArray.filter(option => + walletNetworks.includes(option.network), ); const formattedOptions = await Promise.all( - matchingOptions.map(option => formatPaymentOption(option, walletProvider)), + matchingOptions.map(option => + formatPaymentOption( + { + asset: option.asset, + maxAmountRequired: option.maxAmountRequired ?? option.amount ?? "0", + network: option.network, + }, + walletProvider, + ), + ), ); paymentOptionsText = `The payment options are: ${formattedOptions.join(", ")}`; } + // Extract discovery info from v2 response (description, mimeType, extensions) + const discoveryInfo: Record = {}; + if (paymentData.description) discoveryInfo.description = paymentData.description; + if (paymentData.mimeType) discoveryInfo.mimeType = paymentData.mimeType; + if (paymentData.extensions) discoveryInfo.extensions = paymentData.extensions; + return JSON.stringify({ status: "error_402_payment_required", - acceptablePaymentOptions: response.data.accepts, + acceptablePaymentOptions: acceptsArray, + ...(Object.keys(discoveryInfo).length > 0 && { discoveryInfo }), nextSteps: [ "Inform the user that the requested server replied with a 402 Payment Required response.", paymentOptionsText, + "Include the description of the service in the response.", + "IMPORTANT: Identify required or optional query or body parameters based on this response. If there are any, you must inform the user and request them to provide the values. Always suggest example values.", + "CRITICAL: For POST/PUT/PATCH requests, you MUST use the 'body' parameter (NOT queryParams) to send data.", hasMatchingNetwork ? "Ask the user if they want to retry the request with payment." : "", hasMatchingNetwork - ? `Use retry_http_request_with_x402 to retry the request with payment.` + ? "Use retry_http_request_with_x402 to retry the request with payment. IMPORTANT: You must retry_http_request_with_x402 with the correct Http method. " : "", ], }); } catch (error) { - return handleHttpError(error as AxiosError, args.url); + return handleHttpError(error, args.url); } } @@ -295,16 +300,18 @@ DO NOT use this action directly without first trying make_http_request!`, args: z.infer, ): Promise { try { + console.log("args", args); + // Check network compatibility before attempting payment - const walletNetwork = getX402Network(walletProvider.getNetwork()); + const walletNetworks = getX402Networks(walletProvider.getNetwork()); const selectedNetwork = args.selectedPaymentOption.network; - if (walletNetwork !== selectedNetwork) { + if (!walletNetworks.includes(selectedNetwork)) { return JSON.stringify( { error: true, message: "Network mismatch", - details: `Wallet is on ${walletNetwork} but payment requires ${selectedNetwork}`, + details: `Wallet is on ${walletNetworks.join(", ")} but payment requires ${selectedNetwork}`, }, null, 2, @@ -328,79 +335,82 @@ DO NOT use this action directly without first trying make_http_request!`, ); } - // Make the request with payment handling - const account = await walletProvider.toSigner(); + // Create x402 client with appropriate signer + const client = await this.createX402Client(walletProvider); + const fetchWithPayment = wrapFetchWithPayment(fetch, client); - const paymentSelector = (accepts: PaymentRequirements[]) => { - const { scheme, network, maxAmountRequired, asset } = args.selectedPaymentOption; + // Build URL with query params and determine if body is allowed + const finalUrl = buildUrlWithParams(args.url, args.queryParams); + const method = args.method ?? "GET"; + const canHaveBody = ["POST", "PUT", "PATCH"].includes(method); - let paymentRequirements = accepts.find( - accept => - accept.scheme === scheme && - accept.network === network && - accept.maxAmountRequired <= maxAmountRequired && - accept.asset === asset, - ); - if (paymentRequirements) { - return paymentRequirements; - } + // Build headers, adding Content-Type for JSON body + const headers: Record = { ...(args.headers ?? {}) }; + if (canHaveBody && args.body) { + headers["Content-Type"] = "application/json"; + } - paymentRequirements = accepts.find( - accept => - accept.scheme === scheme && - accept.network === network && - accept.maxAmountRequired <= maxAmountRequired && - accept.asset === asset, - ); - if (paymentRequirements) { - return paymentRequirements; - } + // Make the request with payment handling + const response = await fetchWithPayment(finalUrl, { + method, + headers, + body: canHaveBody && args.body ? JSON.stringify(args.body) : undefined, + }); - return accepts[0]; - }; + const data = await this.parseResponseData(response); - const api = withPaymentInterceptor( - axios.create({}), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - account as any, - paymentSelector as unknown as Parameters[2], - ); + // Check for payment proof in headers (v2: payment-response, v1: x-payment-response) + const paymentResponseHeader = + response.headers.get("payment-response") ?? response.headers.get("x-payment-response"); - const response = await api.request({ - url: args.url, - method: args.method ?? "GET", - headers: args.headers ?? undefined, - data: args.body, - }); + let paymentProof: Record | null = null; + if (paymentResponseHeader) { + try { + paymentProof = JSON.parse(atob(paymentResponseHeader)); + } catch { + // If parsing fails, include raw header + paymentProof = { raw: paymentResponseHeader }; + } + } + + // Get the amount used (supports both v1 and v2 formats) + const amountUsed = + args.selectedPaymentOption.maxAmountRequired ?? + args.selectedPaymentOption.amount ?? + args.selectedPaymentOption.price; - // Check for payment proof - const paymentProof = response.headers["x-payment-response"] - ? decodeXPaymentResponse(response.headers["x-payment-response"]) - : null; + // Check if the response was successful + // Payment is only settled on 200 status + if (response.status !== 200) { + return JSON.stringify({ + status: "error", + message: `Request failed with status ${response.status}. Payment was not settled.`, + httpStatus: response.status, + data, + details: { + url: finalUrl, + method, + }, + }); + } return JSON.stringify({ status: "success", - data: response.data, + data, message: "Request completed successfully with payment", details: { - url: args.url, - method: args.method, + url: finalUrl, + method, paymentUsed: { network: args.selectedPaymentOption.network, asset: args.selectedPaymentOption.asset, - amount: args.selectedPaymentOption.maxAmountRequired, + amount: amountUsed, }, - paymentProof: paymentProof - ? { - transaction: paymentProof.transaction, - network: paymentProof.network, - payer: paymentProof.payer, - } - : null, + paymentProof, }, }); } catch (error) { - return handleHttpError(error as AxiosError, args.url); + return handleHttpError(error, args.url); } } @@ -414,7 +424,7 @@ DO NOT use this action directly without first trying make_http_request!`, @CreateAction({ name: "make_http_request_with_x402", description: ` -⚠️ WARNING: This action automatically handles payments without asking for confirmation! +WARNING: This action automatically handles payments without asking for confirmation! Only use this when explicitly told to skip the confirmation flow. For most cases, you should: @@ -454,44 +464,76 @@ Unless specifically instructed otherwise, prefer the two-step approach with make 2, ); } - const account = await walletProvider.toSigner(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const api = withPaymentInterceptor(axios.create({}), account as any); + // Create x402 client with appropriate signer + const client = await this.createX402Client(walletProvider); + const fetchWithPayment = wrapFetchWithPayment(fetch, client); - const response = await api.request({ - url: args.url, - method: args.method ?? "GET", - headers: args.headers ?? undefined, - data: args.body, + // Build URL with query params and determine if body is allowed + const finalUrl = buildUrlWithParams(args.url, args.queryParams); + const method = args.method ?? "GET"; + const canHaveBody = ["POST", "PUT", "PATCH"].includes(method); + + // Build headers, adding Content-Type for JSON body + const headers: Record = { ...(args.headers ?? {}) }; + if (canHaveBody && args.body) { + headers["Content-Type"] = "application/json"; + } + + const response = await fetchWithPayment(finalUrl, { + method, + headers, + body: canHaveBody && args.body ? JSON.stringify(args.body) : undefined, }); - // Check for payment proof - const paymentProof = response.headers["x-payment-response"] - ? decodeXPaymentResponse(response.headers["x-payment-response"]) - : null; + const data = await this.parseResponseData(response); + + // Check for payment proof in headers (v2: payment-response, v1: x-payment-response) + const paymentResponseHeader = + response.headers.get("payment-response") ?? response.headers.get("x-payment-response"); + + let paymentProof: Record | null = null; + if (paymentResponseHeader) { + try { + paymentProof = JSON.parse(atob(paymentResponseHeader)); + } catch { + // If parsing fails, include raw header + paymentProof = { raw: paymentResponseHeader }; + } + } + + // Check if the response was successful + // Payment is only settled on 200 status + if (response.status !== 200) { + return JSON.stringify( + { + success: false, + message: `Request failed with status ${response.status}. Payment was not settled.`, + url: finalUrl, + method, + status: response.status, + data, + }, + null, + 2, + ); + } return JSON.stringify( { success: true, message: "Request completed successfully (payment handled automatically if required)", - url: args.url, - method: args.method, + url: finalUrl, + method, status: response.status, - data: response.data, - paymentProof: paymentProof - ? { - transaction: paymentProof.transaction, - network: paymentProof.network, - payer: paymentProof.payer, - } - : null, + data, + paymentProof, }, null, 2, ); } catch (error) { - return handleHttpError(error as AxiosError, args.url); + return handleHttpError(error, args.url); } } @@ -501,7 +543,44 @@ Unless specifically instructed otherwise, prefer the two-step approach with make * @param network - The network to check support for * @returns True if the network is supported, false otherwise */ - supportsNetwork = (network: Network) => SUPPORTED_NETWORKS.includes(network.networkId!); + supportsNetwork = (network: Network) => + (SUPPORTED_NETWORKS as readonly string[]).includes(network.networkId!); + + /** + * Creates an x402 client configured for the given wallet provider. + * + * @param walletProvider - The wallet provider to configure the client for + * @returns Configured x402Client + */ + private async createX402Client(walletProvider: WalletProvider): Promise { + const client = new x402Client(); + + if (walletProvider instanceof EvmWalletProvider) { + const signer = walletProvider.toSigner(); + registerExactEvmScheme(client, { signer }); + } else if (walletProvider instanceof SvmWalletProvider) { + const signer = await walletProvider.toSigner(); + registerExactSvmScheme(client, { signer }); + } + + return client; + } + + /** + * Parses response data based on content type. + * + * @param response - The fetch Response object + * @returns Parsed response data + */ + private async parseResponseData(response: Response): Promise { + const contentType = response.headers.get("content-type") ?? ""; + + if (contentType.includes("application/json")) { + return response.json(); + } + + return response.text(); + } } export const x402ActionProvider = () => new X402ActionProvider(); diff --git a/typescript/pnpm-lock.yaml b/typescript/pnpm-lock.yaml index 931b0ffa6..4b9bc60ad 100644 --- a/typescript/pnpm-lock.yaml +++ b/typescript/pnpm-lock.yaml @@ -79,7 +79,7 @@ importers: version: 0.20.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) '@coinbase/x402': specifier: ^0.6.3 - version: 0.6.4(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.6.4(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@ensofinance/sdk': specifier: ^2.0.6 version: 2.0.6 @@ -94,7 +94,7 @@ importers: version: 1.18.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) '@solana/kit': specifier: ^2.1.1 - version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/spl-token': specifier: ^0.4.12 version: 0.4.13(@solana/web3.js@1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(utf-8-validate@5.0.10) @@ -103,7 +103,16 @@ importers: version: 1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) '@vaultsfyi/sdk': specifier: ^2.1.9 - version: 2.1.9(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 2.1.9(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@x402/evm': + specifier: ^2.0.0 + version: 2.0.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) + '@x402/fetch': + specifier: ^2.0.0 + version: 2.0.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) + '@x402/svm': + specifier: ^2.0.0 + version: 2.0.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@zerodev/ecdsa-validator': specifier: ^5.4.5 version: 5.4.5(@zerodev/sdk@5.4.28(viem@2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)))(viem@2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) @@ -119,9 +128,6 @@ importers: '@zoralabs/protocol-deployments': specifier: 0.6.1 version: 0.6.1 - axios: - specifier: ^1.9.0 - version: 1.9.0 bs58: specifier: ^4.0.1 version: 4.0.1 @@ -158,12 +164,6 @@ importers: viem: specifier: 2.38.3 version: 2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) - x402: - specifier: ^0.6.0 - version: 0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - x402-axios: - specifier: ^0.6.0 - version: 0.6.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: specifier: ^3.23.8 version: 3.24.2 @@ -2469,6 +2469,18 @@ packages: '@walletconnect/window-metadata@1.0.1': resolution: {integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==} + '@x402/core@2.0.0': + resolution: {integrity: sha512-tgC8Ujj8Bi9zU1318RAOcagffaHRMPupqj0tlmcOm4j/a/iWGoN58SIpqFV98qkZi7z6BpFAsScCyXnmaDvIyQ==} + + '@x402/evm@2.0.0': + resolution: {integrity: sha512-BA8zC1/AWYq99tWRLNoqudJkeZJpkbPdHMEjrsEOjineVoijnz5N/0NO0klxT64oDG9kNbeczwLU/mTj7NneEw==} + + '@x402/fetch@2.0.0': + resolution: {integrity: sha512-7odbj+d9Op6Ns62YPOAw37Afvf0ldZ90bHOrAJFP9ahcV6l0iHdTtr7NcSqIZI+0r4emDmIIpGtLRhyyVCQYnA==} + + '@x402/svm@2.0.0': + resolution: {integrity: sha512-VJdxUoiNZ18/wucXToEscVKNfN+x18jK9c6p0AscS7iJaNq3z3ST/guf8asRedEWX9Rus62SilTmq3UYdNcnvw==} + '@xmtp/agent-sdk@1.1.4': resolution: {integrity: sha512-98hFtZmQln+QxA3eN6Of3ot9jxjUsRoMBvGz4yRV7gRrqJtH4iVqWTCCN1SqFpBPaQECdKPJhlxCEE7II+5jMw==} engines: {node: '>=20'} @@ -2586,6 +2598,17 @@ packages: zod: optional: true + abitype@1.2.3: + resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -4952,6 +4975,14 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + ox@0.10.5: + resolution: {integrity: sha512-mXJRiZswmX46abrzNkJpTN9sPJ/Rhevsp5Dfg0z80D55aoLNmEV4oN+/+feSNW593c2CnHavMqSVBanpJ0lUkQ==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + ox@0.6.7: resolution: {integrity: sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==} peerDependencies: @@ -6197,6 +6228,14 @@ packages: typescript: optional: true + viem@2.43.1: + resolution: {integrity: sha512-S33pBNlRvOlVv4+L94Z8ydCMDB1j0cuHFUvaC28i6OTxw3uY1P4M3h1YDFK8YC1H9/lIbeBTTvCRhi0FqU/2iw==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + wagmi@2.17.0: resolution: {integrity: sha512-NxdS/oHG3j4l3JzNIAb0zL14p3GXOS9YU4Q3WS+0eTChFIfdqBrurQMImPLNC9P5rXYym6cjC2feeqhHVMJjpg==} peerDependencies: @@ -6344,9 +6383,6 @@ packages: utf-8-validate: optional: true - x402-axios@0.6.0: - resolution: {integrity: sha512-UyKZnepVH3xz8+BsHIqAsvZohHa06yLdOOWo51ctEeazOdH57gRLVmtvEVbpBnrO2Fh0kbgcagzVlM/5v3M0Zw==} - x402-fetch@0.7.0: resolution: {integrity: sha512-HS7v6wsIVrU8TvAGBwRmA3I+ZXbanPraA3OMj90y6Hn1Mej1wAELOK4VpGh6zI8d6w5E464BnGu9o0FE+8DRAA==} @@ -6983,11 +7019,11 @@ snapshots: - utf-8-validate - zod - '@coinbase/x402@0.6.4(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coinbase/x402@0.6.4(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@coinbase/cdp-sdk': 1.38.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(utf-8-validate@5.0.10) viem: 2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56) - x402: 0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.56 transitivePeerDependencies: - '@azure/app-configuration' @@ -8643,18 +8679,18 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))': + '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) - '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)': dependencies: @@ -8828,7 +8864,7 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) @@ -8841,11 +8877,11 @@ snapshots: '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) '@solana/rpc-parsed-types': 2.3.0(typescript@5.8.2) '@solana/rpc-spec-types': 2.3.0(typescript@5.8.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) typescript: 5.8.2 @@ -8935,14 +8971,14 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.3.0(typescript@5.8.2) '@solana/functional': 2.3.0(typescript@5.8.2) '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.8.2) '@solana/subscribable': 2.3.0(typescript@5.8.2) typescript: 5.8.2 - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@solana/rpc-subscriptions-spec@2.3.0(typescript@5.8.2)': dependencies: @@ -8952,7 +8988,7 @@ snapshots: '@solana/subscribable': 2.3.0(typescript@5.8.2) typescript: 5.8.2 - '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.3.0(typescript@5.8.2) '@solana/fast-stable-stringify': 2.3.0(typescript@5.8.2) @@ -8960,7 +8996,7 @@ snapshots: '@solana/promises': 2.3.0(typescript@5.8.2) '@solana/rpc-spec-types': 2.3.0(typescript@5.8.2) '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) - '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.8.2) '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) @@ -9076,7 +9112,7 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) @@ -9084,7 +9120,7 @@ snapshots: '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) '@solana/promises': 2.3.0(typescript@5.8.2) '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) @@ -9429,9 +9465,9 @@ snapshots: '@uniswap/token-lists@1.0.0-beta.33': {} - '@vaultsfyi/sdk@2.1.9(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@vaultsfyi/sdk@2.1.9(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: - x402-fetch: 0.7.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402-fetch: 0.7.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10072,6 +10108,46 @@ snapshots: '@walletconnect/window-getters': 1.0.1 tslib: 1.14.1 + '@x402/core@2.0.0': + dependencies: + zod: 3.25.56 + + '@x402/evm@2.0.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)': + dependencies: + '@x402/core': 2.0.0 + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56) + zod: 3.25.56 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + + '@x402/fetch@2.0.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)': + dependencies: + '@x402/core': 2.0.0 + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56) + zod: 3.25.56 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + + '@x402/svm@2.0.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@scure/base': 1.2.6 + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@x402/core': 2.0.0 + zod: 3.25.56 + transitivePeerDependencies: + - '@solana/sysvars' + - fastestsmallesttextencoderdecoder + - typescript + - ws + '@xmtp/agent-sdk@1.1.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)': dependencies: '@xmtp/content-type-reaction': 2.0.2 @@ -10220,6 +10296,11 @@ snapshots: typescript: 5.8.2 zod: 3.25.56 + abitype@1.2.3(typescript@5.8.2)(zod@3.25.56): + optionalDependencies: + typescript: 5.8.2 + zod: 3.25.56 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -13258,6 +13339,21 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + ox@0.10.5(typescript@5.8.2)(zod@3.25.56): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.8.2)(zod@3.25.56) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - zod + ox@0.6.7(typescript@5.8.2)(zod@3.25.56): dependencies: '@adraffy/ens-normalize': 1.11.0 @@ -14665,6 +14761,23 @@ snapshots: - utf-8-validate - zod + viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.8.2)(zod@3.25.56) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.10.5(typescript@5.8.2)(zod@3.25.56) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + wagmi@2.17.0(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.25.56): dependencies: '@tanstack/react-query': 5.89.0(react@18.3.1) @@ -14837,51 +14950,10 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 5.0.10 - x402-axios@0.6.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): - dependencies: - axios: 1.9.0 - viem: 2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56) - x402: 0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - zod: 3.25.56 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@react-native-async-storage/async-storage' - - '@solana/sysvars' - - '@tanstack/query-core' - - '@tanstack/react-query' - - '@types/react' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - bufferutil - - db0 - - debug - - encoding - - fastestsmallesttextencoderdecoder - - immer - - ioredis - - react - - supports-color - - typescript - - uploadthing - - utf-8-validate - - ws - - x402-fetch@0.7.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402-fetch@0.7.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: viem: 2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56) - x402: 0.7.2(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.7.2(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.56 transitivePeerDependencies: - '@azure/app-configuration' @@ -14917,14 +14989,14 @@ snapshots: - utf-8-validate - ws - x402@0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402@0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@scure/base': 1.2.6 - '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)) - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) viem: 2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.25.56) wagmi: 2.17.0(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.25.56) zod: 3.25.56 @@ -14962,14 +15034,14 @@ snapshots: - utf-8-validate - ws - x402@0.7.2(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402@0.7.2(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@scure/base': 1.2.6 - '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)) - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/wallet-standard-features': 1.3.0 '@wallet-standard/app': 1.1.0 '@wallet-standard/base': 1.1.0