diff --git a/.github/workflows/deploy-server-og-evm.yml b/.github/workflows/deploy-server-og-evm.yml index 41bda4a..4dc88c3 100644 --- a/.github/workflows/deploy-server-og-evm.yml +++ b/.github/workflows/deploy-server-og-evm.yml @@ -14,7 +14,7 @@ jobs: env: ECS_CLUSTER: MemChat ECS_SERVICE: memchat-facilitator-x402-og-evm - ECR_REPOSITORY: memchat/facilitator-x402-og-evm + ECR_REPOSITORY: memchat/facilitator-x402 IMAGE_TAG: latest AWS_REGION: us-east-2 diff --git a/Dockerfile b/Dockerfile index 308c687..fcf99fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,9 +14,7 @@ COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ COPY typescript/package.json ./typescript/ COPY typescript/packages/core/package.json ./typescript/packages/core/ COPY typescript/packages/extensions/package.json ./typescript/packages/extensions/ -COPY typescript/packages/mcp/package.json ./typescript/packages/mcp/ COPY typescript/packages/mechanisms/evm/package.json ./typescript/packages/mechanisms/evm/ -COPY typescript/packages/mechanisms/svm/package.json ./typescript/packages/mechanisms/svm/ COPY typescript/packages/http/next/package.json ./typescript/packages/http/next/ COPY typescript/packages/http/express/package.json ./typescript/packages/http/express/ COPY typescript/packages/http/fetch/package.json ./typescript/packages/http/fetch/ @@ -30,13 +28,10 @@ RUN pnpm install --frozen-lockfile # Copy source code COPY . . -# Build all packages -# We assume 'pnpm build' at root builds all workspace packages RUN pnpm --filter @x402/core build && \ pnpm --filter @x402/evm build && \ - pnpm --filter @x402/svm build && \ pnpm --filter @x402/extensions build && \ - pnpm build + pnpm exec tsc --noCheck # Remove development dependencies RUN pnpm prune --prod @@ -58,4 +53,3 @@ COPY --from=builder /app/typescript/packages ./typescript/packages EXPOSE 3002 CMD ["node", "dist/all_networks.js"] - diff --git a/all_networks.ts b/all_networks.ts index a18cc4c..153fbb3 100644 --- a/all_networks.ts +++ b/all_networks.ts @@ -3,6 +3,14 @@ import express from "express"; import { randomUUID } from "node:crypto"; import { type Server } from "node:http"; import { getAddress, isAddress } from "viem"; +import { + debugLog, + summarizeDataSettlementJob, + summarizeError, + summarizePaymentPayload, + summarizePaymentRequirements, + summarizeVerifyResponse, +} from "./logging.js"; import { incrementMetric } from "./metrics.js"; import { createBullMqConnection, @@ -29,7 +37,11 @@ import { type PaymentSettlementJobData, type SettlementApiJobResponse, } from "./all_networks_types_helpers.js"; -import { type PaymentPayload, type PaymentRequirements, type VerifyResponse } from "@x402/core/types"; +import { + type PaymentPayload, + type PaymentRequirements, + type VerifyResponse, +} from "@x402/core/types"; const app = express(); app.use(express.json()); @@ -52,7 +64,6 @@ const heartbeatRelayContext = (() => { } })(); - const paymentQueue = new Queue(PAYMENT_QUEUE_NAME, { connection, defaultJobOptions: { @@ -154,6 +165,12 @@ async function enqueuePaymentSettlementJob(args: { }, ); + console.log("[api] Payment settlement job enqueued", { + jobId: paymentJobId, + ...summarizePaymentRequirements(args.paymentRequirements), + ...summarizePaymentPayload(args.paymentPayload), + }); + return toJobResponse(PAYMENT_QUEUE_NAME, paymentJob); } @@ -175,14 +192,17 @@ async function enqueueDataSettlementJob(args: { throw new Error("Missing x-settlement-type header"); } + debugLog("[api][debug] Parsed data settlement job", parsedSettlementHeader); + const settlementJobId = `settlement-${randomUUID()}`; - const settlementJob = await dataSettlementQueue.add( - "data-settlement", - parsedSettlementHeader, - { - jobId: settlementJobId, - }, - ); + const settlementJob = await dataSettlementQueue.add("data-settlement", parsedSettlementHeader, { + jobId: settlementJobId, + }); + + console.log("[api] Data settlement job enqueued", { + jobId: settlementJobId, + ...summarizeDataSettlementJob(parsedSettlementHeader), + }); return toJobResponse(DATA_SETTLEMENT_QUEUE_NAME, settlementJob); } @@ -201,17 +221,27 @@ app.post("/verify", async (req, res) => { }); } + console.log("[api] /verify request received", { + ...summarizePaymentRequirements(paymentRequirements), + ...summarizePaymentPayload(paymentPayload), + }); + debugLog("[api][debug] /verify request body", req.body); + const response: VerifyResponse = await facilitator.verify(paymentPayload, paymentRequirements); + console.log("[api] /verify request succeeded", { + ...summarizePaymentRequirements(paymentRequirements), + ...summarizeVerifyResponse(response), + }); + debugLog("[api][debug] /verify response", response); res.json(response); } catch (error) { - console.error("Verify error:", error); + console.error("[api] /verify request failed", summarizeError(error)); res.status(500).json({ error: error instanceof Error ? error.message : "Unknown error", }); } }); - app.post("/settle", async (req, res) => { incrementMetric("api.request.count", ["route:/settle", "method:POST"]); try { @@ -226,13 +256,19 @@ app.post("/settle", async (req, res) => { }); } + console.log("[api] /settle request received", { + ...summarizePaymentRequirements(paymentRequirements), + ...summarizePaymentPayload(paymentPayload), + }); + debugLog("[api][debug] /settle request body", req.body); + const paymentJob = await enqueuePaymentSettlementJob({ paymentPayload, paymentRequirements, }); return res.status(202).json({ paymentJob }); } catch (error) { - console.error("Settle enqueue error:", error); + console.error("[api] /settle request failed", summarizeError(error)); if (isSettlementError(error)) { return res.status(400).json({ error: error.message, @@ -256,6 +292,14 @@ app.post("/settle_data", async (req, res) => { } const settlementDataHeader = normalizeHeaderValue(req.get("x-settlement-data") || undefined); + console.log("[api] /settle_data request received", { + settlementType: settlementTypeHeader, + hasSettlementDataHeader: Boolean(settlementDataHeader), + }); + debugLog("[api][debug] /settle_data headers", { + settlementTypeHeader, + settlementDataHeader, + }); const settlementJob = await enqueueDataSettlementJob({ settlementTypeHeader, settlementDataHeader, @@ -270,7 +314,7 @@ app.post("/settle_data", async (req, res) => { return res.status(202).json({ settlementJob }); } catch (error) { - console.error("Settle data enqueue error:", error); + console.error("[api] /settle_data request failed", summarizeError(error)); if (isSettlementError(error)) { return res.status(400).json({ error: error.message, @@ -401,7 +445,13 @@ app.get("/supported", async (_req, res) => { }); app.get("/health", (_req, res) => { - res.json({ status: "ok" }); + const supported = facilitator.getSupported(); + res.json({ + status: "ok", + supportedKinds: supported.kinds, + extensions: supported.extensions, + signers: supported.signers, + }); }); let httpServer: Server | null = null; @@ -441,8 +491,10 @@ process.on("SIGTERM", () => { }); httpServer = app.listen(parseInt(PORT, 10), () => { + const supported = facilitator.getSupported(); console.log(`🚀 All Networks API listening on http://localhost:${PORT}`); - console.log(` Supported networks: ${facilitator.getSupported().kinds.map(k => k.network).join(", ")}`); + console.log(` Supported networks: ${supported.kinds.map(k => k.network).join(", ")}`); + console.log(` Supported extensions: ${supported.extensions.join(", ") || "(none)"}`); console.log(` Payment queue: ${PAYMENT_QUEUE_NAME}`); console.log(` Data settlement queue: ${DATA_SETTLEMENT_QUEUE_NAME}`); console.log(); diff --git a/all_networks_shared.ts b/all_networks_shared.ts index edcf824..de62f20 100644 --- a/all_networks_shared.ts +++ b/all_networks_shared.ts @@ -1,18 +1,38 @@ -import { base58 } from "@scure/base"; -import { createKeyPairSignerFromBytes } from "@solana/kit"; import { x402Facilitator } from "@x402/core/facilitator"; -import { toFacilitatorEvmSigner } from "@x402/evm"; +import { + EIP2612_GAS_SPONSORING, + createErc20ApprovalGasSponsoringExtension, + type Erc20ApprovalGasSponsoringSigner, +} from "@x402/extensions"; +import { toFacilitatorEvmSigner, type FacilitatorEvmSigner } from "@x402/evm"; import { ExactEvmScheme } from "@x402/evm/exact/facilitator"; import { UptoEvmScheme } from "@x402/evm/upto/facilitator"; -import { toFacilitatorSvmSigner } from "@x402/svm"; -import { ExactSvmScheme } from "@x402/svm/exact/facilitator"; import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; import type { RedisOptions } from "ioredis"; -import { createWalletClient, defineChain, http, parseGwei, publicActions, toHex } from "viem"; +import { + createWalletClient, + defineChain, + http, + parseGwei, + parseTransaction, + publicActions, + recoverTransactionAddress, + toHex, +} from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { baseSepolia } from "viem/chains"; +import { base } from "viem/chains"; +import { + debugLog, + summarizeDataSettlementJob, + summarizeError, + summarizePaymentPayload, + summarizePaymentRequirements, + summarizeSettleResponse, + summarizeVerifyResponse, +} from "./logging.js"; import { gaugeMetric, histogramMetric, incrementMetric } from "./metrics.js"; import { + BASE_MAINNET_NETWORK, base64ToBytesCalldata, DATA_SETTLEMENT_BATCH_BUFFER_SIZE, DATA_SETTLEMENT_BATCH_IDLE_TIMEOUT_MS, @@ -21,6 +41,7 @@ import { DATA_WORKER_SETTLEMENT_CONTRACT_ENV, HEARTBEAT_RELAY_EVM_PRIVATE_KEY_ENV, HEARTBEAT_RELAY_REGISTRY_CONTRACT_ENV, + OG_EVM_NETWORK, REDIS_URL, toBytesCalldata, toStrictBytes32, @@ -63,6 +84,7 @@ const DATA_WORKER_SETTLEMENT_GAS_LIMIT = BigInt( const DATA_WORKER_TX_RECEIPT_TIMEOUT_MS = Number( process.env.DATA_WORKER_TX_RECEIPT_TIMEOUT_MS || 120_000, ); +const BASE_MAINNET_RPC_URL = process.env.BASE_MAINNET_RPC_URL; const HEARTBEAT_RELAY_GAS_LIMIT = BigInt(process.env.HEARTBEAT_RELAY_GAS_LIMIT || "500000"); const HEARTBEAT_RELAY_TX_RECEIPT_TIMEOUT_MS = Number( process.env.HEARTBEAT_RELAY_TX_RECEIPT_TIMEOUT_MS || 120_000, @@ -89,6 +111,21 @@ type WalrusUploadResponse = { }; }; +const DEFAULT_SPONSORED_RAW_TX_GAS = 70_000n; +const DEFAULT_SPONSORED_RAW_TX_MAX_FEE_PER_GAS = 1_000_000_000n; + +type SponsoredGasWalletClient = { + getBalance(args: { address: `0x${string}` }): Promise; + sendTransaction(args: { + to: `0x${string}`; + data?: `0x${string}`; + gas?: bigint; + value?: bigint; + }): Promise<`0x${string}`>; + waitForTransactionReceipt(args: { hash: `0x${string}` }): Promise<{ status: string }>; + sendRawTransaction(args: { serializedTransaction: `0x${string}` }): Promise<`0x${string}`>; +}; + async function withTimeout(promise: Promise, timeoutMs: number, label: string): Promise { let timeoutHandle: ReturnType | null = null; const timeoutPromise = new Promise((_, reject) => { @@ -369,7 +406,7 @@ export async function uploadToWalrus( const publisherUrl = process.env.WALRUS_PUBLISHER_URL || "http://localhost:9002/v1/blobs"; const url = `${publisherUrl}?epochs=10`; const walrusUploadTimeoutMs = Number(process.env.WALRUS_UPLOAD_TIMEOUT_MS || 30_000); - console.log(`Uploading individual settlement payload to Walrus: ${publisherUrl}`); + console.log(`[settlement] Uploading ${uploadKind} to Walrus via ${publisherUrl}`); const abortController = new AbortController(); const timeout = setTimeout(() => abortController.abort(), walrusUploadTimeoutMs); @@ -411,7 +448,7 @@ export async function uploadToWalrus( } if (result.alreadyCertified?.blobId) { - console.log("Blob already exists on Walrus (deduplicated)."); + console.log(`[settlement] ${uploadKind} already exists on Walrus (deduplicated).`); return result.alreadyCertified.blobId; } @@ -433,8 +470,9 @@ export async function processBatchSettlement( chainId: context.chainId, chainName: context.chainName, bufferedItems: batchSettlementBuffer.length, - data, + ...summarizeDataSettlementJob({ settlementType: "batch", data }), }); + debugLog("[settlement][debug] Raw batch settlement data", data); scheduleBatchFlush(context); @@ -486,8 +524,10 @@ export async function processIndividualSettlement( chainName: context.chainName, walrusBlobId: blobId, txHash, - data, + ...summarizeDataSettlementJob({ settlementType: "individual", data }), }); + debugLog("[settlement][debug] Raw individual settlement data", data); + debugLog("[settlement][debug] Walrus payload", walrusPayload); console.log( `[settlement] Individual settlement uploaded to Walrus with blob id: ${blobId}, txHash: ${txHash}`, @@ -506,8 +546,8 @@ export async function processIndividualSettlement( chainId: context.chainId, chainName: context.chainName, settlementContractAddress: context.settlementContractAddress, - data, - error, + ...summarizeDataSettlementJob({ settlementType: "individual", data }), + ...summarizeError(error), }); throw error; } @@ -731,42 +771,129 @@ export function createBullMqConnection(): RedisOptions { return options; } +function createErc20ApprovalGasSponsorSigner(args: { + signer: FacilitatorEvmSigner; + walletClient: SponsoredGasWalletClient; +}): Erc20ApprovalGasSponsoringSigner { + return { + ...args.signer, + sendTransactions: async transactions => { + const hashes: `0x${string}`[] = []; + + for (const transaction of transactions) { + let hash: `0x${string}`; + + if (typeof transaction === "string") { + const parsed = parseTransaction(transaction); + const payerAddress = await recoverTransactionAddress({ + serializedTransaction: transaction, + }); + const gas = parsed.gas ?? DEFAULT_SPONSORED_RAW_TX_GAS; + const maxFeePerGas = + parsed.maxFeePerGas ?? parsed.gasPrice ?? DEFAULT_SPONSORED_RAW_TX_MAX_FEE_PER_GAS; + const gasCost = gas * maxFeePerGas; + const payerBalance = await args.walletClient.getBalance({ address: payerAddress }); + + if (payerBalance < gasCost) { + const deficit = gasCost - payerBalance; + console.log( + `[payment-worker] funding ${payerAddress} with ${deficit.toString()} wei for sponsored approval gas`, + ); + + const fundingHash = await args.walletClient.sendTransaction({ + to: payerAddress, + value: deficit, + }); + const fundingReceipt = await args.walletClient.waitForTransactionReceipt({ + hash: fundingHash, + }); + + if (fundingReceipt.status !== "success") { + throw new Error(`gas_funding_failed: ${fundingHash}`); + } + } + + hash = await args.walletClient.sendRawTransaction({ + serializedTransaction: transaction, + }); + } else { + hash = await args.walletClient.sendTransaction({ + to: transaction.to, + data: transaction.data, + gas: transaction.gas, + }); + } + + const receipt = await args.walletClient.waitForTransactionReceipt({ hash }); + if (receipt.status !== "success") { + throw new Error(`transaction_failed: ${hash}`); + } + + hashes.push(hash); + } + + return hashes; + }, + }; +} + export async function createFacilitator(): Promise { const evmPrivateKey = (process.env.PAYMENT_WORKER_EVM_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY) as `0x${string}` | undefined; - const svmPrivateKey = (process.env.PAYMENT_WORKER_SVM_PRIVATE_KEY || - process.env.SVM_PRIVATE_KEY) as string | undefined; - if (!evmPrivateKey && !svmPrivateKey) { - throw new Error( - "At least one of PAYMENT_WORKER_EVM_PRIVATE_KEY/EVM_PRIVATE_KEY or PAYMENT_WORKER_SVM_PRIVATE_KEY/SVM_PRIVATE_KEY is required", - ); + if (!evmPrivateKey) { + throw new Error("PAYMENT_WORKER_EVM_PRIVATE_KEY or EVM_PRIVATE_KEY is required"); } const facilitator = new x402Facilitator() .onBeforeVerify(async context => { - console.log("Before verify", context); + console.log("[verify] Starting payment verification", { + ...summarizePaymentRequirements(context.requirements), + ...summarizePaymentPayload(context.paymentPayload), + }); + debugLog("[verify][debug] Full verify context", context); }) .onAfterVerify(async context => { - console.log("After verify", context); + console.log("[verify] Payment verification completed", { + ...summarizePaymentRequirements(context.requirements), + ...summarizeVerifyResponse(context.result), + }); + debugLog("[verify][debug] Verify result context", context); }) .onVerifyFailure(async context => { - console.log("Verify failure", context); + console.warn("[verify] Payment verification failed", { + ...summarizePaymentRequirements(context.requirements), + ...summarizePaymentPayload(context.paymentPayload), + ...summarizeError(context.error), + }); + debugLog("[verify][debug] Verify failure context", context); }) .onBeforeSettle(async context => { - console.log("Before settle", context); + console.log("[payment-settlement] Starting payment settlement", { + ...summarizePaymentRequirements(context.requirements), + ...summarizePaymentPayload(context.paymentPayload), + }); + debugLog("[payment-settlement][debug] Full settle context", context); }) .onAfterSettle(async context => { - console.log("After settle", context); + const settlementSummary = { + ...summarizePaymentRequirements(context.requirements), + ...summarizeSettleResponse(context.result), + ...(context.result.success ? {} : summarizePaymentPayload(context.paymentPayload)), + }; + + console.log("[payment-settlement] Payment settlement completed", settlementSummary); + debugLog("[payment-settlement][debug] Settle result context", context); }) .onSettleFailure(async context => { - console.log("Settle failure", context); + console.error("[payment-settlement] Payment settlement failed", { + ...summarizePaymentRequirements(context.requirements), + ...summarizePaymentPayload(context.paymentPayload), + ...summarizeError(context.error), + }); + debugLog("[payment-settlement][debug] Settle failure context", context); }); - const EVM_NETWORK = "eip155:10740"; - const BASE_TESTNET_NETWORK = "eip155:84532"; - const SVM_NETWORK = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"; - if (evmPrivateKey) { const evmAccount = privateKeyToAccount(evmPrivateKey); console.info(`EVM Facilitator account: ${evmAccount.address}`); @@ -779,8 +906,8 @@ export async function createFacilitator(): Promise { const baseViemClient = createWalletClient({ account: evmAccount, - chain: baseSepolia, - transport: http(), + chain: base, + transport: http(BASE_MAINNET_RPC_URL), }).extend(publicActions); const evmSigner = toFacilitatorEvmSigner({ @@ -869,30 +996,43 @@ export async function createFacilitator(): Promise { baseViemClient.waitForTransactionReceipt(args), }); + const erc20ApprovalSigners = new Map([ + [ + OG_EVM_NETWORK, + createErc20ApprovalGasSponsorSigner({ + signer: evmSigner, + walletClient: viemClient, + }), + ], + [ + BASE_MAINNET_NETWORK, + createErc20ApprovalGasSponsorSigner({ + signer: baseEvmSigner, + walletClient: baseViemClient, + }), + ], + ]); + facilitator.register( - EVM_NETWORK, + OG_EVM_NETWORK, new ExactEvmScheme(evmSigner, { deployERC4337WithEIP6492: true }), ); - facilitator.register( - EVM_NETWORK, - new UptoEvmScheme(evmSigner, { deployERC4337WithEIP6492: true }), - ); + facilitator.register(OG_EVM_NETWORK, new UptoEvmScheme(evmSigner)); facilitator.register( - BASE_TESTNET_NETWORK, + BASE_MAINNET_NETWORK, new ExactEvmScheme(baseEvmSigner, { deployERC4337WithEIP6492: true }), ); - facilitator.register( - BASE_TESTNET_NETWORK, - new UptoEvmScheme(baseEvmSigner, { deployERC4337WithEIP6492: true }), - ); - } - - if (svmPrivateKey) { - const svmAccount = await createKeyPairSignerFromBytes(base58.decode(svmPrivateKey)); - console.info(`SVM Facilitator account: ${svmAccount.address}`); - const svmSigner = toFacilitatorSvmSigner(svmAccount); - facilitator.register(SVM_NETWORK, new ExactSvmScheme(svmSigner)); + facilitator.register(BASE_MAINNET_NETWORK, new UptoEvmScheme(baseEvmSigner)); + + facilitator + .registerExtension(EIP2612_GAS_SPONSORING) + .registerExtension( + createErc20ApprovalGasSponsoringExtension( + erc20ApprovalSigners.get(OG_EVM_NETWORK)!, + network => erc20ApprovalSigners.get(network), + ), + ); } return facilitator; diff --git a/all_networks_types_helpers.ts b/all_networks_types_helpers.ts index 37c6502..f0840a3 100644 --- a/all_networks_types_helpers.ts +++ b/all_networks_types_helpers.ts @@ -38,6 +38,8 @@ export const DATA_WORKER_EVM_PRIVATE_KEY_ENV = "DATA_WORKER_EVM_PRIVATE_KEY"; export const DATA_WORKER_SETTLEMENT_CONTRACT_ENV = "DATA_WORKER_SETTLEMENT_CONTRACT"; export const HEARTBEAT_RELAY_EVM_PRIVATE_KEY_ENV = "HEARTBEAT_RELAY_EVM_PRIVATE_KEY"; export const HEARTBEAT_RELAY_REGISTRY_CONTRACT_ENV = "HEARTBEAT_RELAY_REGISTRY_CONTRACT"; +export const OG_EVM_NETWORK = "eip155:10740" as const; +export const BASE_MAINNET_NETWORK = "eip155:8453" as const; export const DATA_SETTLEMENT_BATCH_BUFFER_SIZE = Number( process.env.DATA_SETTLEMENT_BATCH_BUFFER_SIZE || 20, ); diff --git a/bazaar.ts b/bazaar.ts index d0637f4..5f7cc7a 100644 --- a/bazaar.ts +++ b/bazaar.ts @@ -5,8 +5,6 @@ * catalogs discovered x402 resources. */ -import { base58 } from "@scure/base"; -import { createKeyPairSignerFromBytes } from "@solana/kit"; import { x402Facilitator } from "@x402/core/facilitator"; import { PaymentPayload, @@ -16,8 +14,6 @@ import { } from "@x402/core/types"; import { toFacilitatorEvmSigner } from "@x402/evm"; import { ExactEvmScheme } from "@x402/evm/exact/facilitator"; -import { toFacilitatorSvmSigner } from "@x402/svm"; -import { ExactSvmScheme } from "@x402/svm/exact/facilitator"; import { extractDiscoveryInfo, DiscoveryInfo } from "@x402/extensions/bazaar"; import dotenv from "dotenv"; import express from "express"; @@ -32,19 +28,14 @@ const PORT = process.env.PORT || "4022"; // Configuration - optional per network const evmPrivateKey = process.env.EVM_PRIVATE_KEY as `0x${string}` | undefined; -const svmPrivateKey = process.env.SVM_PRIVATE_KEY as string | undefined; -// Validate at least one private key is provided -if (!evmPrivateKey && !svmPrivateKey) { - console.error( - "❌ At least one of EVM_PRIVATE_KEY or SVM_PRIVATE_KEY is required", - ); +if (!evmPrivateKey) { + console.error("❌ EVM_PRIVATE_KEY is required"); process.exit(1); } // Network configuration const EVM_NETWORK = "eip155:84532"; // Base Sepolia -const SVM_NETWORK = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"; // Solana Devnet // DiscoveredResource represents a discovered x402 resource for the bazaar catalog interface DiscoveredResource { @@ -181,18 +172,6 @@ if (evmPrivateKey) { ); } -// Register SVM scheme if private key is provided -if (svmPrivateKey) { - const svmAccount = await createKeyPairSignerFromBytes( - base58.decode(svmPrivateKey), - ); - console.info(`SVM Facilitator account: ${svmAccount.address}`); - - const svmSigner = toFacilitatorSvmSigner(svmAccount); - - facilitator.register(SVM_NETWORK, new ExactSvmScheme(svmSigner)); -} - // Initialize Express app const app = express(); app.use(express.json()); diff --git a/data_worker.ts b/data_worker.ts index 8984de2..2c74661 100644 --- a/data_worker.ts +++ b/data_worker.ts @@ -1,4 +1,5 @@ import { Worker } from "bullmq"; +import { summarizeDataSettlementJob, summarizeError } from "./logging.js"; import { incrementMetric } from "./metrics.js"; import { createDataWorkerContext, @@ -16,7 +17,11 @@ const dataWorkerContext = createDataWorkerContext(); const worker = new Worker( DATA_SETTLEMENT_QUEUE_NAME, - async (job: { data: DataSettlementJobData }) => { + async (job: { id?: string; data: DataSettlementJobData }) => { + console.log("[data-worker] Processing data settlement job", { + jobId: job.id, + ...summarizeDataSettlementJob(job.data), + }); return processDataSettlementJob(job.data, dataWorkerContext); }, { @@ -32,7 +37,10 @@ worker.on("completed", (job: { id?: string }) => { worker.on("failed", (job: { id?: string } | undefined, err: unknown) => { incrementMetric("worker.job.failed.count", ["worker:data"]); - console.error(`[data-worker] Failed job ${job?.id ?? "unknown"}:`, err); + console.error("[data-worker] Failed job", { + jobId: job?.id ?? "unknown", + ...summarizeError(err), + }); }); let isShuttingDown = false; diff --git a/logging.ts b/logging.ts new file mode 100644 index 0000000..e00a159 --- /dev/null +++ b/logging.ts @@ -0,0 +1,242 @@ +import { inspect } from "node:util"; +import type { + PaymentPayload, + PaymentRequirements, + SettleResponse, + VerifyResponse, +} from "@x402/core/types"; +import { + EIP2612_GAS_SPONSORING, + ERC20_APPROVAL_GAS_SPONSORING, + extractEip2612GasSponsoringInfo, + extractErc20ApprovalGasSponsoringInfo, + validateEip2612GasSponsoringInfo, + validateErc20ApprovalGasSponsoringInfo, +} from "@x402/extensions"; +import type { + DataSettlementJobData, + SettlementBatchData, + SettlementIndividualData, +} from "./all_networks_types_helpers.js"; + +type LogSummaryValue = boolean | number | string | undefined; +type LogSummary = Record; + +export const DEBUG_LOGGING_ENABLED = process.env.FACILITATOR_DEBUG === "true"; + +function shortenValue( + value: string | undefined, + prefixLength = 10, + suffixLength = 6, +): string | undefined { + if (!value) { + return undefined; + } + + if (value.length <= prefixLength + suffixLength + 3) { + return value; + } + + return `${value.slice(0, prefixLength)}...${value.slice(-suffixLength)}`; +} + +function summarizeObjectShape(value: unknown): string { + if (value === null) { + return "null"; + } + + if (Array.isArray(value)) { + return `array(${value.length})`; + } + + if (typeof value === "object") { + const keys = Object.keys(value as Record); + return keys.length > 0 ? `object(${keys.slice(0, 5).join(",")})` : "object(empty)"; + } + + if (typeof value === "string") { + return `string(${value.length})`; + } + + return typeof value; +} + +function summarizeSettlementBatchData(data: SettlementBatchData): LogSummary { + return { + teeId: shortenValue(data.teeId), + inputHash: shortenValue(data.inputHash), + outputHash: shortenValue(data.outputHash), + timestamp: data.timestamp, + }; +} + +function summarizeSettlementIndividualData(data: SettlementIndividualData): LogSummary { + return { + ...summarizeSettlementBatchData(data), + ethAddress: shortenValue(data.ethAddress), + inputShape: summarizeObjectShape(data.input), + outputShape: summarizeObjectShape(data.output), + }; +} + +function asRecord(value: unknown): Record | null { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return null; + } + + return value as Record; +} + +function summarizeExtensionInfoShape(extension: unknown): string | undefined { + const extensionRecord = asRecord(extension); + if (!extensionRecord) { + return undefined; + } + + const infoRecord = asRecord(extensionRecord.info); + if (!infoRecord) { + return undefined; + } + + const keys = Object.keys(infoRecord); + return keys.length > 0 ? keys.join(",") : undefined; +} + +function diagnoseEip2612Extension(paymentPayload: PaymentPayload): LogSummary { + const extension = paymentPayload.extensions?.[EIP2612_GAS_SPONSORING.key]; + if (!extension) { + return {}; + } + + const infoShape = summarizeExtensionInfoShape(extension); + const info = extractEip2612GasSponsoringInfo(paymentPayload); + if (info) { + return { + eip2612State: validateEip2612GasSponsoringInfo(info) ? "client-signed" : "client-signed-invalid", + eip2612InfoShape: infoShape, + }; + } + + return { + eip2612State: infoShape === "description,version" ? "server-declared-only" : "missing-required-fields", + eip2612InfoShape: infoShape, + probableIssue: + "eip2612 declared by server but signed permit fields are missing from paymentPayload.extensions", + }; +} + +function diagnoseErc20ApprovalExtension(paymentPayload: PaymentPayload): LogSummary { + const extension = paymentPayload.extensions?.[ERC20_APPROVAL_GAS_SPONSORING.key]; + if (!extension) { + return {}; + } + + const infoShape = summarizeExtensionInfoShape(extension); + const info = extractErc20ApprovalGasSponsoringInfo(paymentPayload); + if (info) { + return { + erc20ApprovalState: validateErc20ApprovalGasSponsoringInfo(info) + ? "client-signed" + : "client-signed-invalid", + erc20ApprovalInfoShape: infoShape, + }; + } + + return { + erc20ApprovalState: + infoShape === "description,version" ? "server-declared-only" : "missing-required-fields", + erc20ApprovalInfoShape: infoShape, + }; +} + +export function summarizePaymentRequirements(requirements: PaymentRequirements): LogSummary { + return { + scheme: requirements.scheme, + network: requirements.network, + asset: shortenValue(requirements.asset), + amount: requirements.amount, + payTo: shortenValue(requirements.payTo), + maxTimeoutSeconds: requirements.maxTimeoutSeconds, + }; +} + +export function summarizePaymentPayload(paymentPayload: PaymentPayload): LogSummary { + return { + x402Version: paymentPayload.x402Version, + resourceUrl: paymentPayload.resource?.url, + resourceMimeType: paymentPayload.resource?.mimeType, + acceptedScheme: paymentPayload.accepted.scheme, + acceptedNetwork: paymentPayload.accepted.network, + acceptedAsset: shortenValue(paymentPayload.accepted.asset), + acceptedAmount: paymentPayload.accepted.amount, + acceptedPayTo: shortenValue(paymentPayload.accepted.payTo), + payloadKeys: Object.keys(paymentPayload.payload).join(",") || undefined, + extensionKeys: paymentPayload.extensions + ? Object.keys(paymentPayload.extensions).join(",") || undefined + : undefined, + ...diagnoseEip2612Extension(paymentPayload), + ...diagnoseErc20ApprovalExtension(paymentPayload), + }; +} + +export function summarizeVerifyResponse(result: VerifyResponse): LogSummary { + return { + isValid: result.isValid, + payer: shortenValue(result.payer), + invalidReason: result.invalidReason, + invalidMessage: result.invalidMessage, + extensionKeys: result.extensions + ? Object.keys(result.extensions).join(",") || undefined + : undefined, + }; +} + +export function summarizeSettleResponse(result: SettleResponse): LogSummary { + return { + success: result.success, + payer: shortenValue(result.payer), + transaction: shortenValue(result.transaction), + network: result.network, + settledAmount: result.amount, + errorReason: result.errorReason, + errorMessage: result.errorMessage, + extensionKeys: result.extensions + ? Object.keys(result.extensions).join(",") || undefined + : undefined, + }; +} + +export function summarizeError(error: unknown): LogSummary { + if (error instanceof Error) { + return { + errorName: error.name, + errorMessage: error.message, + }; + } + + return { + errorMessage: String(error), + }; +} + +export function summarizeDataSettlementJob(jobData: DataSettlementJobData): LogSummary { + if (jobData.settlementType === "batch") { + return { + settlementType: jobData.settlementType, + ...summarizeSettlementBatchData(jobData.data), + }; + } + + return { + settlementType: jobData.settlementType, + ...summarizeSettlementIndividualData(jobData.data), + }; +} + +export function debugLog(label: string, value: unknown): void { + if (!DEBUG_LOGGING_ENABLED) { + return; + } + + console.log(`${label}\n${inspect(value, { depth: null, colors: false, compact: false })}`); +} diff --git a/package.json b/package.json index 95ff371..592c5da 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,9 @@ }, "dependencies": { "@openzeppelin/merkle-tree": "^1.0.8", - "@scure/base": "^1.2.6", - "@solana/kit": "^2.1.1", "@x402/core": "workspace:*", "@x402/evm": "workspace:*", "@x402/extensions": "workspace:*", - "@x402/svm": "workspace:*", "bullmq": "^5.23.0", "dotenv": "^16.4.5", "express": "^4.19.2", diff --git a/payment_worker.ts b/payment_worker.ts index 1831885..d62c832 100644 --- a/payment_worker.ts +++ b/payment_worker.ts @@ -1,9 +1,11 @@ import { Worker } from "bullmq"; -import { incrementMetric } from "./metrics.js"; import { - createBullMqConnection, - createFacilitator, -} from "./all_networks_shared.js"; + summarizeError, + summarizePaymentPayload, + summarizePaymentRequirements, +} from "./logging.js"; +import { incrementMetric } from "./metrics.js"; +import { createBullMqConnection, createFacilitator } from "./all_networks_shared.js"; import { PAYMENT_QUEUE_NAME, SHUTDOWN_TIMEOUT_MS, @@ -19,8 +21,13 @@ function paymentAmountToMetricValue(amount: string): number | null { const worker = new Worker( PAYMENT_QUEUE_NAME, - async (job: { data: PaymentSettlementJobData }) => { + async (job: { id?: string; data: PaymentSettlementJobData }) => { const { paymentPayload, paymentRequirements } = job.data; + console.log("[payment-worker] Processing payment settlement job", { + jobId: job.id, + ...summarizePaymentRequirements(paymentRequirements), + ...summarizePaymentPayload(paymentPayload), + }); const result = await facilitator.settle(paymentPayload, paymentRequirements); if (result.success) { @@ -54,7 +61,10 @@ worker.on("completed", (job: { id?: string }) => { worker.on("failed", (job: { id?: string } | undefined, err: unknown) => { incrementMetric("worker.job.failed.count", ["worker:payment"]); - console.error(`[payment-worker] Failed job ${job?.id ?? "unknown"}:`, err); + console.error("[payment-worker] Failed job", { + jobId: job?.id ?? "unknown", + ...summarizeError(err), + }); }); let isShuttingDown = false; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 178bb53..1800ce4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,12 +11,6 @@ importers: '@openzeppelin/merkle-tree': specifier: ^1.0.8 version: 1.0.8 - '@scure/base': - specifier: ^1.2.6 - version: 1.2.6 - '@solana/kit': - specifier: ^2.1.1 - version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@x402/core': specifier: workspace:* version: link:typescript/packages/core @@ -26,9 +20,6 @@ importers: '@x402/extensions': specifier: workspace:* version: link:typescript/packages/extensions - '@x402/svm': - specifier: workspace:* - version: link:typescript/packages/mechanisms/svm bullmq: specifier: ^5.23.0 version: 5.69.3 @@ -142,6 +133,9 @@ importers: typescript/packages/extensions: dependencies: + '@noble/curves': + specifier: ^1.9.0 + version: 1.9.7 '@scure/base': specifier: ^1.2.6 version: 1.2.6 @@ -151,6 +145,9 @@ importers: ajv: specifier: ^8.17.1 version: 8.17.1 + jose: + specifier: ^5.9.6 + version: 5.10.0 siwe: specifier: ^2.3.2 version: 2.3.2(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -270,12 +267,6 @@ importers: typescript/packages/http/express: dependencies: - '@coinbase/cdp-sdk': - specifier: ^1.22.0 - version: 1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/kit': - specifier: ^2.1.1 - version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@x402/core': specifier: workspace:~ version: link:../../core @@ -283,7 +274,7 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall viem: specifier: ^2.39.3 @@ -344,6 +335,67 @@ importers: specifier: ^3.0.5 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) + typescript/packages/http/fastify: + dependencies: + '@x402/core': + specifier: workspace:~ + version: link:../../core + '@x402/extensions': + specifier: workspace:~ + version: link:../../extensions + '@x402/paywall': + specifier: workspace:* + version: link:../paywall + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.39.1 + '@types/node': + specifier: ^22.13.4 + version: 22.19.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) + eslint: + specifier: ^9.24.0 + version: 9.39.1 + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.39.1) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) + fastify: + specifier: ^5.0.0 + version: 5.8.4 + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) + tsx: + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.19.0)(tsx@4.20.6) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(tsx@4.20.6)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) + typescript/packages/http/fetch: dependencies: '@x402/core': @@ -411,7 +463,7 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall zod: specifier: ^3.24.2 @@ -468,9 +520,6 @@ importers: typescript/packages/http/next: dependencies: - '@coinbase/cdp-sdk': - specifier: ^1.22.0 - version: 1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@x402/core': specifier: workspace:~ version: link:../../core @@ -478,7 +527,7 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall next: specifier: ^16.0.10 @@ -535,33 +584,12 @@ importers: typescript/packages/http/paywall: dependencies: - '@scure/base': - specifier: ^1.2.6 - version: 1.2.6 - '@solana-program/compute-budget': - specifier: ^0.8.0 - version: 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': - specifier: ^0.5.1 - version: 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token-2022': - specifier: ^0.4.2 - version: 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) - '@solana/kit': - specifier: ^2.1.1 - version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-confirmation': - specifier: ^2.1.1 - version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/wallet-standard-features': - specifier: ^1.3.0 - version: 1.3.0 '@tanstack/react-query': specifier: ^5.90.7 version: 5.90.7(react@19.2.0) '@wagmi/connectors': specifier: ^5.8.1 - version: 5.11.2(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(zod@3.25.76) + version: 5.11.2(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(zod@3.25.76) '@wagmi/core': specifier: ^2.17.1 version: 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) @@ -582,7 +610,7 @@ importers: version: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: ^2.17.1 - version: 2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + version: 2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) zod: specifier: ^3.24.2 version: 3.25.76 @@ -611,9 +639,6 @@ importers: '@x402/evm': specifier: workspace:~ version: link:../../mechanisms/evm - '@x402/svm': - specifier: workspace:~ - version: link:../../mechanisms/svm buffer: specifier: ^6.0.3 version: 6.0.3 @@ -660,17 +685,14 @@ importers: specifier: ^3.0.5 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) - typescript/packages/mcp: + typescript/packages/mechanisms/aptos: dependencies: - '@modelcontextprotocol/sdk': - specifier: ^1.12.1 - version: 1.26.0(zod@3.25.76) + '@aptos-labs/ts-sdk': + specifier: ^5.2.1 + version: 5.2.1(got@11.8.6) '@x402/core': - specifier: workspace:~ - version: link:../core - zod: - specifier: ^3.24.2 - version: 3.25.76 + specifier: workspace:* + version: link:../../core devDependencies: '@eslint/js': specifier: ^9.24.0 @@ -684,9 +706,6 @@ importers: '@typescript-eslint/parser': specifier: ^8.29.1 version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) - '@x402/evm': - specifier: workspace:~ - version: link:../mechanisms/evm eslint: specifier: ^9.24.0 version: 9.39.1 @@ -699,9 +718,6 @@ importers: eslint-plugin-prettier: specifier: ^5.2.6 version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) - express: - specifier: ^4.21.2 - version: 4.21.2 prettier: specifier: 3.5.2 version: 3.5.2 @@ -714,9 +730,6 @@ importers: typescript: specifier: ^5.7.3 version: 5.9.3 - viem: - specifier: ^2.27.2 - version: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) vite: specifier: ^6.2.6 version: 6.4.1(@types/node@22.19.0)(tsx@4.20.6) @@ -785,30 +798,18 @@ importers: specifier: ^3.0.5 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) - typescript/packages/mechanisms/svm: + typescript/packages/mechanisms/stellar: dependencies: - '@solana-program/compute-budget': - specifier: ^0.11.0 - version: 0.11.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)) - '@solana-program/token': - specifier: ^0.9.0 - version: 0.9.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)) - '@solana-program/token-2022': - specifier: ^0.6.1 - version: 0.6.1(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) - '@solana/kit': - specifier: ^5.1.0 - version: 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@stellar/stellar-sdk': + specifier: ^14.6.1 + version: 14.6.1 '@x402/core': - specifier: workspace:~ + specifier: workspace:* version: link:../../core devDependencies: '@eslint/js': specifier: ^9.24.0 version: 9.39.1 - '@scure/base': - specifier: ^1.2.6 - version: 1.2.6 '@types/node': specifier: ^22.13.4 version: 22.19.0 @@ -860,6 +861,20 @@ packages: '@adraffy/ens-normalize@1.11.1': resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + '@aptos-labs/aptos-cli@1.1.1': + resolution: {integrity: sha512-sB7CokCM6s76SLJmccysbnFR+MDik6udKfj2+9ZsmTLV0/t73veIeCDKbvWJmbW267ibx4HiGbPI7L+1+yjEbQ==} + hasBin: true + + '@aptos-labs/aptos-client@2.2.0': + resolution: {integrity: sha512-lYgHI8ehgD+Ykhix0IwzLaTCknHp1KNmExbq2bPZk8IeTwQg79D5BOkD46MjW0jGbJbl+J/RBtVF9vM7Te/hWA==} + engines: {node: '>=20.0.0'} + peerDependencies: + got: ^11.8.6 + + '@aptos-labs/ts-sdk@5.2.1': + resolution: {integrity: sha512-kazYjqfsPCBx2UJI+nYUOb6Ov7q7brSgYEfxp2sP27IeJWdDNa50lfs0WIpDJ92kQxdtlm9q3ZWw7Toh9f1gxQ==} + engines: {node: '>=20.0.0'} + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -1139,6 +1154,24 @@ packages: resolution: {integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==} engines: {node: '>=14'} + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@gemini-wallet/core@0.2.0': resolution: {integrity: sha512-vv9aozWnKrrPWQ3vIFcWk7yta4hQW1Ie0fsNNPeXnjAxkbXr2hqMagEptLuMxpEP2W3mnRu05VDNKzcvAuuZDw==} peerDependencies: @@ -1149,12 +1182,6 @@ packages: peerDependencies: viem: '>=2.0.0' - '@hono/node-server@1.19.9': - resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4 - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1421,16 +1448,6 @@ packages: resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} engines: {node: '>=16.0.0'} - '@modelcontextprotocol/sdk@1.26.0': - resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} cpu: [arm64] @@ -1585,6 +1602,9 @@ packages: resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} deprecated: 'The package is now available as "qr": npm install qr' + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1798,114 +1818,41 @@ packages: '@scure/bip39@1.6.0': resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} - '@solana-program/compute-budget@0.11.0': - resolution: {integrity: sha512-7f1ePqB/eURkTwTOO9TNIdUXZcyrZoX3Uy2hNo7cXMfNhPFWp9AVgIyRNBc2jf15sdUa9gNpW+PfP2iV8AYAaw==} - peerDependencies: - '@solana/kit': ^5.0 - - '@solana-program/compute-budget@0.8.0': - resolution: {integrity: sha512-qPKxdxaEsFxebZ4K5RPuy7VQIm/tfJLa1+Nlt3KNA8EYQkz9Xm8htdoEaXVrer9kpgzzp9R3I3Bh6omwCM06tQ==} - peerDependencies: - '@solana/kit': ^2.1.0 - '@solana-program/system@0.8.1': resolution: {integrity: sha512-71U9Mzdpw8HQtfgfJSL5xKZbLMRnza2Llsfk7gGnmg2waqK+o8MMH4YNma8xXS1UmOBptXIiNvoZ3p7cmOVktg==} peerDependencies: '@solana/kit': ^3.0 - '@solana-program/token-2022@0.4.2': - resolution: {integrity: sha512-zIpR5t4s9qEU3hZKupzIBxJ6nUV5/UVyIT400tu9vT1HMs5JHxaTTsb5GUhYjiiTvNwU0MQavbwc4Dl29L0Xvw==} - peerDependencies: - '@solana/kit': ^2.1.0 - '@solana/sysvars': ^2.1.0 - - '@solana-program/token-2022@0.6.1': - resolution: {integrity: sha512-Ex02cruDMGfBMvZZCrggVR45vdQQSI/unHVpt/7HPt/IwFYB4eTlXtO8otYZyqV/ce5GqZ8S6uwyRf0zy6fdbA==} - peerDependencies: - '@solana/kit': ^5.0 - '@solana/sysvars': ^5.0 - - '@solana-program/token@0.5.1': - resolution: {integrity: sha512-bJvynW5q9SFuVOZ5vqGVkmaPGA0MCC+m9jgJj1nk5m20I389/ms69ASnhWGoOPNcie7S9OwBX0gTj2fiyWpfag==} - peerDependencies: - '@solana/kit': ^2.1.0 - '@solana-program/token@0.6.0': resolution: {integrity: sha512-omkZh4Tt9rre4wzWHNOhOEHyenXQku3xyc/UrKvShexA/Qlhza67q7uRwmwEDUs4QqoDBidSZPooOmepnA/jig==} peerDependencies: '@solana/kit': ^3.0 - '@solana-program/token@0.9.0': - resolution: {integrity: sha512-vnZxndd4ED4Fc56sw93cWZ2djEeeOFxtaPS8SPf5+a+JZjKA/EnKqzbE1y04FuMhIVrLERQ8uR8H2h72eZzlsA==} - peerDependencies: - '@solana/kit': ^5.0 - - '@solana/accounts@2.3.0': - resolution: {integrity: sha512-QgQTj404Z6PXNOyzaOpSzjgMOuGwG8vC66jSDB+3zHaRcEPRVRd2sVSrd1U6sHtnV3aiaS6YyDuPQMheg4K2jw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/accounts@3.0.3': resolution: {integrity: sha512-KqlePrlZaHXfu8YQTCxN204ZuVm9o68CCcUr6l27MG2cuRUtEM1Ta0iR8JFkRUAEfZJC4Cu0ZDjK/v49loXjZQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/accounts@5.5.1': - resolution: {integrity: sha512-TfOY9xixg5rizABuLVuZ9XI2x2tmWUC/OoN556xwfDlhBHBjKfszicYYOyD6nbFmwTGYarCmyGIdteXxTXIdhQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/addresses@2.3.0': - resolution: {integrity: sha512-ypTNkY2ZaRFpHLnHAgaW8a83N0/WoqdFvCqf4CQmnMdFsZSdC7qOwcbd7YzdaQn9dy+P2hybewzB+KP7LutxGA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/addresses@3.0.3': resolution: {integrity: sha512-AuMwKhJI89ANqiuJ/fawcwxNKkSeHH9CApZd2xelQQLS7X8uxAOovpcmEgiObQuiVP944s9ScGUT62Bdul9qYg==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/addresses@5.5.1': - resolution: {integrity: sha512-5xoah3Q9G30HQghu/9BiHLb5pzlPKRC3zydQDmE3O9H//WfayxTFppsUDCL6FjYUHqj/wzK6CWHySglc2RkpdA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/assertions@2.3.0': - resolution: {integrity: sha512-Ekoet3khNg3XFLN7MIz8W31wPQISpKUGDGTylLptI+JjCDWx3PIa88xjEMqFo02WJ8sBj2NLV64Xg1sBcsHjZQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/assertions@3.0.3': resolution: {integrity: sha512-2qspxdbWp2y62dfCIlqeWQr4g+hE8FYSSwcaP6itwMwGRb8393yDGCJfI/znuzJh6m/XVWhMHIgFgsBwnevCmg==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/assertions@5.5.1': - resolution: {integrity: sha512-YTCSWAlGwSlVPnWtWLm3ukz81wH4j2YaCveK+TjpvUU88hTy6fmUqxi0+hvAMAe4zKXpJyj3Az7BrLJRxbIm4Q==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - '@solana/buffer-layout@4.0.1': resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} engines: {node: '>=5.10'} @@ -1922,36 +1869,12 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/codecs-core@5.5.1': - resolution: {integrity: sha512-TgBt//bbKBct0t6/MpA8ElaOA3sa8eYVvR7LGslCZ84WiAwwjCY0lW/lOYsFHJQzwREMdUyuEyy5YWBKtdh8Rw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/codecs-data-structures@2.3.0': - resolution: {integrity: sha512-qvU5LE5DqEdYMYgELRHv+HMOx73sSoV1ZZkwIrclwUmwTbTaH8QAJURBj0RhQ/zCne7VuLLOZFFGv6jGigWhSw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/codecs-data-structures@3.0.3': resolution: {integrity: sha512-R15cLp8riJvToXziW8lP6AMSwsztGhEnwgyGmll32Mo0Yjq+hduW2/fJrA/TJs6tA/OgTzMQjlxgk009EqZHCw==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/codecs-data-structures@5.5.1': - resolution: {integrity: sha512-97bJWGyUY9WvBz3mX1UV3YPWGDTez6btCfD0ip3UVEXJbItVuUiOkzcO5iFDUtQT5riKT6xC+Mzl+0nO76gd0w==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - '@solana/codecs-numbers@2.3.0': resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==} engines: {node: '>=20.18.0'} @@ -1964,22 +1887,6 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/codecs-numbers@5.5.1': - resolution: {integrity: sha512-rllMIZAHqmtvC0HO/dc/21wDuWaD0B8Ryv8o+YtsICQBuiL/0U4AGwH7Pi5GNFySYk0/crSuwfIqQFtmxNSPFw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/codecs-strings@2.3.0': - resolution: {integrity: sha512-y5pSBYwzVziXu521hh+VxqUtp0hYGTl1eWGoc1W+8mdvBdC1kTqm/X7aYQw33J42hw03JjryvYOvmGgk3Qz/Ug==} - engines: {node: '>=20.18.0'} - peerDependencies: - fastestsmallesttextencoderdecoder: ^1.0.22 - typescript: '>=5.3.3' - '@solana/codecs-strings@3.0.3': resolution: {integrity: sha512-VHBXnnTVtcQ1j+7Vrz+qSYo38no+jiHRdGnhFspRXEHNJbllzwKqgBE7YN3qoIXH+MKxgJUcwO5KHmdzf8Wn2A==} engines: {node: '>=20.18.0'} @@ -1987,39 +1894,12 @@ packages: fastestsmallesttextencoderdecoder: ^1.0.22 typescript: '>=5.3.3' - '@solana/codecs-strings@5.5.1': - resolution: {integrity: sha512-7klX4AhfHYA+uKKC/nxRGP2MntbYQCR3N6+v7bk1W/rSxYuhNmt+FN8aoThSZtWIKwN6BEyR1167ka8Co1+E7A==} - engines: {node: '>=20.18.0'} - peerDependencies: - fastestsmallesttextencoderdecoder: ^1.0.22 - typescript: ^5.0.0 - peerDependenciesMeta: - fastestsmallesttextencoderdecoder: - optional: true - typescript: - optional: true - - '@solana/codecs@2.3.0': - resolution: {integrity: sha512-JVqGPkzoeyU262hJGdH64kNLH0M+Oew2CIPOa/9tR3++q2pEd4jU2Rxdfye9sd0Ce3XJrR5AIa8ZfbyQXzjh+g==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/codecs@3.0.3': resolution: {integrity: sha512-GOHwTlIQsCoJx9Ryr6cEf0FHKAQ7pY4aO4xgncAftrv0lveTQ1rPP2inQ1QT0gJllsIa8nwbfXAADs9nNJxQDA==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/codecs@5.5.1': - resolution: {integrity: sha512-Vea29nJub/bXjfzEV7ZZQ/PWr1pYLZo3z0qW0LQL37uKKVzVFRQlwetd7INk3YtTD3xm9WUYr7bCvYUk3uKy2g==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - '@solana/errors@2.3.0': resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==} engines: {node: '>=20.18.0'} @@ -2034,683 +1914,258 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/errors@5.5.1': - resolution: {integrity: sha512-vFO3p+S7HoyyrcAectnXbdsMfwUzY2zYFUc2DEe5BwpiE9J1IAxPBGjOWO6hL1bbYdBrlmjNx8DXCslqS+Kcmg==} + '@solana/fast-stable-stringify@3.0.3': + resolution: {integrity: sha512-ED0pxB6lSEYvg+vOd5hcuQrgzEDnOrURFgp1ZOY+lQhJkQU6xo+P829NcJZQVP1rdU2/YQPAKJKEseyfe9VMIw==} engines: {node: '>=20.18.0'} - hasBin: true peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' - '@solana/fast-stable-stringify@2.3.0': - resolution: {integrity: sha512-KfJPrMEieUg6D3hfQACoPy0ukrAV8Kio883llt/8chPEG3FVTX9z/Zuf4O01a15xZmBbmQ7toil2Dp0sxMJSxw==} + '@solana/functional@3.0.3': + resolution: {integrity: sha512-2qX1kKANn8995vOOh5S9AmF4ItGZcfbny0w28Eqy8AFh+GMnSDN4gqpmV2LvxBI9HibXZptGH3RVOMk82h1Mpw==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/fast-stable-stringify@3.0.3': - resolution: {integrity: sha512-ED0pxB6lSEYvg+vOd5hcuQrgzEDnOrURFgp1ZOY+lQhJkQU6xo+P829NcJZQVP1rdU2/YQPAKJKEseyfe9VMIw==} + '@solana/instruction-plans@3.0.3': + resolution: {integrity: sha512-eqoaPtWtmLTTpdvbt4BZF5H6FIlJtXi9H7qLOM1dLYonkOX2Ncezx5NDCZ9tMb2qxVMF4IocYsQnNSnMfjQF1w==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/fast-stable-stringify@5.5.1': - resolution: {integrity: sha512-Ni7s2FN33zTzhTFgRjEbOVFO+UAmK8qi3Iu0/GRFYK4jN696OjKHnboSQH/EacQ+yGqS54bfxf409wU5dsLLCw==} + '@solana/instructions@3.0.3': + resolution: {integrity: sha512-4csIi8YUDb5j/J+gDzmYtOvq7ZWLbCxj4t0xKn+fPrBk/FD2pK29KVT3Fu7j4Lh1/ojunQUP9X4NHwUexY3PnA==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' - '@solana/functional@2.3.0': - resolution: {integrity: sha512-AgsPh3W3tE+nK3eEw/W9qiSfTGwLYEvl0rWaxHht/lRcuDVwfKRzeSa5G79eioWFFqr+pTtoCr3D3OLkwKz02Q==} + '@solana/keys@3.0.3': + resolution: {integrity: sha512-tp8oK9tMadtSIc4vF4aXXWkPd4oU5XPW8nf28NgrGDWGt25fUHIydKjkf2hPtMt9i1WfRyQZ33B5P3dnsNqcPQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/functional@3.0.3': - resolution: {integrity: sha512-2qX1kKANn8995vOOh5S9AmF4ItGZcfbny0w28Eqy8AFh+GMnSDN4gqpmV2LvxBI9HibXZptGH3RVOMk82h1Mpw==} + '@solana/kit@3.0.3': + resolution: {integrity: sha512-CEEhCDmkvztd1zbgADsEQhmj9GyWOOGeW1hZD+gtwbBSF5YN1uofS/pex5MIh/VIqKRj+A2UnYWI1V+9+q/lyQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/functional@5.5.1': - resolution: {integrity: sha512-tTHoJcEQq3gQx5qsdsDJ0LEJeFzwNpXD80xApW9o/PPoCNimI3SALkZl+zNW8VnxRrV3l3yYvfHWBKe/X3WG3w==} + '@solana/nominal-types@3.0.3': + resolution: {integrity: sha512-aZavCiexeUAoMHRQg4s1AHkH3wscbOb70diyfjhwZVgFz1uUsFez7csPp9tNFkNolnadVb2gky7yBk3IImQJ6A==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' - '@solana/instruction-plans@3.0.3': - resolution: {integrity: sha512-eqoaPtWtmLTTpdvbt4BZF5H6FIlJtXi9H7qLOM1dLYonkOX2Ncezx5NDCZ9tMb2qxVMF4IocYsQnNSnMfjQF1w==} + '@solana/options@3.0.3': + resolution: {integrity: sha512-jarsmnQ63RN0JPC5j9sgUat07NrL9PC71XU7pUItd6LOHtu4+wJMio3l5mT0DHVfkfbFLL6iI6+QmXSVhTNF3g==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/instruction-plans@5.5.1': - resolution: {integrity: sha512-7z3CB7YMcFKuVvgcnNY8bY6IsZ8LG61Iytbz7HpNVGX2u1RthOs1tRW8luTzSG1MPL0Ox7afyAVMYeFqSPHnaQ==} + '@solana/programs@3.0.3': + resolution: {integrity: sha512-JZlVE3/AeSNDuH3aEzCZoDu8GTXkMpGXxf93zXLzbxfxhiQ/kHrReN4XE/JWZ/uGWbaFZGR5B3UtdN2QsoZL7w==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' - '@solana/instructions@2.3.0': - resolution: {integrity: sha512-PLMsmaIKu7hEAzyElrk2T7JJx4D+9eRwebhFZpy2PXziNSmFF929eRHKUsKqBFM3cYR1Yy3m6roBZfA+bGE/oQ==} + '@solana/promises@3.0.3': + resolution: {integrity: sha512-K+UflGBVxj30XQMHTylHHZJdKH5QG3oj5k2s42GrZ/Wbu72oapVJySMBgpK45+p90t8/LEqV6rRPyTXlet9J+Q==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/instructions@3.0.3': - resolution: {integrity: sha512-4csIi8YUDb5j/J+gDzmYtOvq7ZWLbCxj4t0xKn+fPrBk/FD2pK29KVT3Fu7j4Lh1/ojunQUP9X4NHwUexY3PnA==} + '@solana/rpc-api@3.0.3': + resolution: {integrity: sha512-Yym9/Ama62OY69rAZgbOCAy1QlqaWAyb0VlqFuwSaZV1pkFCCFSwWEJEsiN1n8pb2ZP+RtwNvmYixvWizx9yvA==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/instructions@5.5.1': - resolution: {integrity: sha512-h0G1CG6S+gUUSt0eo6rOtsaXRBwCq1+Js2a+Ps9Bzk9q7YHNFA75/X0NWugWLgC92waRp66hrjMTiYYnLBoWOQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/keys@2.3.0': - resolution: {integrity: sha512-ZVVdga79pNH+2pVcm6fr2sWz9HTwfopDVhYb0Lh3dh+WBmJjwkabXEIHey2rUES7NjFa/G7sV8lrUn/v8LDCCQ==} + '@solana/rpc-parsed-types@3.0.3': + resolution: {integrity: sha512-/koM05IM2fU91kYDQxXil3VBNlOfcP+gXE0js1sdGz8KonGuLsF61CiKB5xt6u1KEXhRyDdXYLjf63JarL4Ozg==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/keys@3.0.3': - resolution: {integrity: sha512-tp8oK9tMadtSIc4vF4aXXWkPd4oU5XPW8nf28NgrGDWGt25fUHIydKjkf2hPtMt9i1WfRyQZ33B5P3dnsNqcPQ==} + '@solana/rpc-spec-types@3.0.3': + resolution: {integrity: sha512-A6Jt8SRRetnN3CeGAvGJxigA9zYRslGgWcSjueAZGvPX+MesFxEUjSWZCfl+FogVFvwkqfkgQZQbPAGZQFJQ6Q==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/keys@5.5.1': - resolution: {integrity: sha512-KRD61cL7CRL+b4r/eB9dEoVxIf/2EJ1Pm1DmRYhtSUAJD2dJ5Xw8QFuehobOGm9URqQ7gaQl+Fkc1qvDlsWqKg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/kit@2.3.0': - resolution: {integrity: sha512-sb6PgwoW2LjE5oTFu4lhlS/cGt/NB3YrShEyx7JgWFWysfgLdJnhwWThgwy/4HjNsmtMrQGWVls0yVBHcMvlMQ==} + '@solana/rpc-spec@3.0.3': + resolution: {integrity: sha512-MZn5/8BebB6MQ4Gstw6zyfWsFAZYAyLzMK+AUf/rSfT8tPmWiJ/mcxnxqOXvFup/l6D67U8pyGpIoFqwCeZqqA==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/kit@3.0.3': - resolution: {integrity: sha512-CEEhCDmkvztd1zbgADsEQhmj9GyWOOGeW1hZD+gtwbBSF5YN1uofS/pex5MIh/VIqKRj+A2UnYWI1V+9+q/lyQ==} + '@solana/rpc-subscriptions-api@3.0.3': + resolution: {integrity: sha512-MGgVK3PUS15qsjuhimpzGZrKD/CTTvS0mAlQ0Jw84zsr1RJVdQJK/F0igu07BVd172eTZL8d90NoAQ3dahW5pA==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/kit@5.5.1': - resolution: {integrity: sha512-irKUGiV2yRoyf+4eGQ/ZeCRxa43yjFEL1DUI5B0DkcfZw3cr0VJtVJnrG8OtVF01vT0OUfYOcUn6zJW5TROHvQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/nominal-types@2.3.0': - resolution: {integrity: sha512-uKlMnlP4PWW5UTXlhKM8lcgIaNj8dvd8xO4Y9l+FVvh9RvW2TO0GwUO6JCo7JBzCB0PSqRJdWWaQ8pu1Ti/OkA==} + '@solana/rpc-subscriptions-channel-websocket@3.0.3': + resolution: {integrity: sha512-zUzUlb8Cwnw+SHlsLrSqyBRtOJKGc+FvSNJo/vWAkLShoV0wUDMPv7VvhTngJx3B/3ANfrOZ4i08i9QfYPAvpQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + ws: ^8.18.0 - '@solana/nominal-types@3.0.3': - resolution: {integrity: sha512-aZavCiexeUAoMHRQg4s1AHkH3wscbOb70diyfjhwZVgFz1uUsFez7csPp9tNFkNolnadVb2gky7yBk3IImQJ6A==} + '@solana/rpc-subscriptions-spec@3.0.3': + resolution: {integrity: sha512-9KpQ32OBJWS85mn6q3gkM0AjQe1LKYlMU7gpJRrla/lvXxNLhI95tz5K6StctpUreVmRWTVkNamHE69uUQyY8A==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/nominal-types@5.5.1': - resolution: {integrity: sha512-I1ImR+kfrLFxN5z22UDiTWLdRZeKtU0J/pkWkO8qm/8WxveiwdIv4hooi8pb6JnlR4mSrWhq0pCIOxDYrL9GIQ==} + '@solana/rpc-subscriptions@3.0.3': + resolution: {integrity: sha512-LRvz6NaqvtsYFd32KwZ+rwYQ9XCs+DWjV8BvBLsJpt9/NWSuHf/7Sy/vvP6qtKxut692H/TMvHnC4iulg0WmiQ==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' - '@solana/offchain-messages@5.5.1': - resolution: {integrity: sha512-g+xHH95prTU+KujtbOzj8wn+C7ZNoiLhf3hj6nYq3MTyxOXtBEysguc97jJveUZG0K97aIKG6xVUlMutg5yxhw==} + '@solana/rpc-transformers@3.0.3': + resolution: {integrity: sha512-lzdaZM/dG3s19Tsk4mkJA5JBoS1eX9DnD7z62gkDwrwJDkDBzkAJT9aLcsYFfTmwTfIp6uU2UPgGYc97i1wezw==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' - '@solana/options@2.3.0': - resolution: {integrity: sha512-PPnnZBRCWWoZQ11exPxf//DRzN2C6AoFsDI/u2AsQfYih434/7Kp4XLpfOMT/XESi+gdBMFNNfbES5zg3wAIkw==} + '@solana/rpc-transport-http@3.0.3': + resolution: {integrity: sha512-bIXFwr2LR5A97Z46dI661MJPbHnPfcShBjFzOS/8Rnr8P4ho3j/9EUtjDrsqoxGJT3SLWj5OlyXAlaDAvVTOUQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/options@3.0.3': - resolution: {integrity: sha512-jarsmnQ63RN0JPC5j9sgUat07NrL9PC71XU7pUItd6LOHtu4+wJMio3l5mT0DHVfkfbFLL6iI6+QmXSVhTNF3g==} + '@solana/rpc-types@3.0.3': + resolution: {integrity: sha512-petWQ5xSny9UfmC3Qp2owyhNU0w9SyBww4+v7tSVyXMcCC9v6j/XsqTeimH1S0qQUllnv0/FY83ohFaxofmZ6Q==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/options@5.5.1': - resolution: {integrity: sha512-eo971c9iLNLmk+yOFyo7yKIJzJ/zou6uKpy6mBuyb/thKtS/haiKIc3VLhyTXty3OH2PW8yOlORJnv4DexJB8A==} + '@solana/rpc@3.0.3': + resolution: {integrity: sha512-3oukAaLK78GegkKcm6iNmRnO4mFeNz+BMvA8T56oizoBNKiRVEq/6DFzVX/LkmZ+wvD601pAB3uCdrTPcC0YKQ==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' - '@solana/plugin-core@5.5.1': - resolution: {integrity: sha512-VUZl30lDQFJeiSyNfzU1EjYt2QZvoBFKEwjn1lilUJw7KgqD5z7mbV7diJhT+dLFs36i0OsjXvq5kSygn8YJ3A==} + '@solana/signers@3.0.3': + resolution: {integrity: sha512-UwCd/uPYTZiwd283JKVyOWLLN5sIgMBqGDyUmNU3vo9hcmXKv5ZGm/9TvwMY2z35sXWuIOcj7etxJ8OoWc/ObQ==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' - '@solana/programs@2.3.0': - resolution: {integrity: sha512-UXKujV71VCI5uPs+cFdwxybtHZAIZyQkqDiDnmK+DawtOO9mBn4Nimdb/6RjR2CXT78mzO9ZCZ3qfyX+ydcB7w==} + '@solana/subscribable@3.0.3': + resolution: {integrity: sha512-FJ27LKGHLQ5GGttPvTOLQDLrrOZEgvaJhB7yYaHAhPk25+p+erBaQpjePhfkMyUbL1FQbxn1SUJmS6jUuaPjlQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/programs@3.0.3': - resolution: {integrity: sha512-JZlVE3/AeSNDuH3aEzCZoDu8GTXkMpGXxf93zXLzbxfxhiQ/kHrReN4XE/JWZ/uGWbaFZGR5B3UtdN2QsoZL7w==} + '@solana/sysvars@3.0.3': + resolution: {integrity: sha512-GnHew+QeKCs2f9ow+20swEJMH4mDfJA/QhtPgOPTYQx/z69J4IieYJ7fZenSHnA//lJ45fVdNdmy1trypvPLBQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/programs@5.5.1': - resolution: {integrity: sha512-7U9kn0Jsx1NuBLn5HRTFYh78MV4XN145Yc3WP/q5BlqAVNlMoU9coG5IUTJIG847TUqC1lRto3Dnpwm6T4YRpA==} + '@solana/transaction-confirmation@3.0.3': + resolution: {integrity: sha512-dXx0OLtR95LMuARgi2dDQlL1QYmk56DOou5q9wKymmeV3JTvfDExeWXnOgjRBBq/dEfj4ugN1aZuTaS18UirFw==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' - '@solana/promises@2.3.0': - resolution: {integrity: sha512-GjVgutZKXVuojd9rWy1PuLnfcRfqsaCm7InCiZc8bqmJpoghlyluweNc7ml9Y5yQn1P2IOyzh9+p/77vIyNybQ==} + '@solana/transaction-messages@3.0.3': + resolution: {integrity: sha512-s+6NWRnBhnnjFWV4x2tzBzoWa6e5LiIxIvJlWwVQBFkc8fMGY04w7jkFh0PM08t/QFKeXBEWkyBDa/TFYdkWug==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/promises@3.0.3': - resolution: {integrity: sha512-K+UflGBVxj30XQMHTylHHZJdKH5QG3oj5k2s42GrZ/Wbu72oapVJySMBgpK45+p90t8/LEqV6rRPyTXlet9J+Q==} + '@solana/transactions@3.0.3': + resolution: {integrity: sha512-iMX+n9j4ON7H1nKlWEbMqMOpKYC6yVGxKKmWHT1KdLRG7v+03I4DnDeFoI+Zmw56FA+7Bbne8jwwX60Q1vk/MQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/promises@5.5.1': - resolution: {integrity: sha512-T9lfuUYkGykJmppEcssNiCf6yiYQxJkhiLPP+pyAc2z84/7r3UVIb2tNJk4A9sucS66pzJnVHZKcZVGUUp6wzA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + '@solana/web3.js@1.98.4': + resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} - '@solana/rpc-api@2.3.0': - resolution: {integrity: sha512-UUdiRfWoyYhJL9PPvFeJr4aJ554ob2jXcpn4vKmRVn9ire0sCbpQKYx6K8eEKHZWXKrDW8IDspgTl0gT/aJWVg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' + '@spruceid/siwe-parser@2.1.2': + resolution: {integrity: sha512-d/r3S1LwJyMaRAKQ0awmo9whfXeE88Qt00vRj91q5uv5ATtWIQEGJ67Yr5eSZw5zp1/fZCXZYuEckt8lSkereQ==} - '@solana/rpc-api@3.0.3': - resolution: {integrity: sha512-Yym9/Ama62OY69rAZgbOCAy1QlqaWAyb0VlqFuwSaZV1pkFCCFSwWEJEsiN1n8pb2ZP+RtwNvmYixvWizx9yvA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' + '@stablelib/binary@1.0.1': + resolution: {integrity: sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==} - '@solana/rpc-api@5.5.1': - resolution: {integrity: sha512-XWOQQPhKl06Vj0xi3RYHAc6oEQd8B82okYJ04K7N0Vvy3J4PN2cxeK7klwkjgavdcN9EVkYCChm2ADAtnztKnA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + '@stablelib/int@1.0.1': + resolution: {integrity: sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==} - '@solana/rpc-parsed-types@2.3.0': - resolution: {integrity: sha512-B5pHzyEIbBJf9KHej+zdr5ZNAdSvu7WLU2lOUPh81KHdHQs6dEb310LGxcpCc7HVE8IEdO20AbckewDiAN6OCg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' + '@stablelib/random@1.0.2': + resolution: {integrity: sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==} - '@solana/rpc-parsed-types@3.0.3': - resolution: {integrity: sha512-/koM05IM2fU91kYDQxXil3VBNlOfcP+gXE0js1sdGz8KonGuLsF61CiKB5xt6u1KEXhRyDdXYLjf63JarL4Ozg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' + '@stablelib/wipe@1.0.1': + resolution: {integrity: sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==} - '@solana/rpc-parsed-types@5.5.1': - resolution: {integrity: sha512-HEi3G2nZqGEsa3vX6U0FrXLaqnUCg4SKIUrOe8CezD+cSFbRTOn3rCLrUmJrhVyXlHoQVaRO9mmeovk31jWxJg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + '@stellar/js-xdr@3.1.2': + resolution: {integrity: sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==} - '@solana/rpc-spec-types@2.3.0': - resolution: {integrity: sha512-xQsb65lahjr8Wc9dMtP7xa0ZmDS8dOE2ncYjlvfyw/h4mpdXTUdrSMi6RtFwX33/rGuztQ7Hwaid5xLNSLvsFQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' + '@stellar/stellar-base@14.1.0': + resolution: {integrity: sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==} + engines: {node: '>=20.0.0'} - '@solana/rpc-spec-types@3.0.3': - resolution: {integrity: sha512-A6Jt8SRRetnN3CeGAvGJxigA9zYRslGgWcSjueAZGvPX+MesFxEUjSWZCfl+FogVFvwkqfkgQZQbPAGZQFJQ6Q==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' + '@stellar/stellar-sdk@14.6.1': + resolution: {integrity: sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==} + engines: {node: '>=20.0.0'} + hasBin: true - '@solana/rpc-spec-types@5.5.1': - resolution: {integrity: sha512-6OFKtRpIEJQs8Jb2C4OO8KyP2h2Hy1MFhatMAoXA+0Ik8S3H+CicIuMZvGZ91mIu/tXicuOOsNNLu3HAkrakrw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@solana/rpc-spec@2.3.0': - resolution: {integrity: sha512-fA2LMX4BMixCrNB2n6T83AvjZ3oUQTu7qyPLyt8gHQaoEAXs8k6GZmu6iYcr+FboQCjUmRPgMaABbcr9j2J9Sw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - '@solana/rpc-spec@3.0.3': - resolution: {integrity: sha512-MZn5/8BebB6MQ4Gstw6zyfWsFAZYAyLzMK+AUf/rSfT8tPmWiJ/mcxnxqOXvFup/l6D67U8pyGpIoFqwCeZqqA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} - '@solana/rpc-spec@5.5.1': - resolution: {integrity: sha512-m3LX2bChm3E3by4mQrH4YwCAFY57QBzuUSWqlUw7ChuZ+oLLOq7b2czi4i6L4Vna67j3eCmB3e+4tqy1j5wy7Q==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + '@tanstack/query-core@5.90.7': + resolution: {integrity: sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ==} - '@solana/rpc-subscriptions-api@2.3.0': - resolution: {integrity: sha512-9mCjVbum2Hg9KGX3LKsrI5Xs0KX390lS+Z8qB80bxhar6MJPugqIPH8uRgLhCW9GN3JprAfjRNl7our8CPvsPQ==} - engines: {node: '>=20.18.0'} + '@tanstack/react-query@5.90.7': + resolution: {integrity: sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ==} peerDependencies: - typescript: '>=5.3.3' + react: ^18 || ^19 - '@solana/rpc-subscriptions-api@3.0.3': - resolution: {integrity: sha512-MGgVK3PUS15qsjuhimpzGZrKD/CTTvS0mAlQ0Jw84zsr1RJVdQJK/F0igu07BVd172eTZL8d90NoAQ3dahW5pA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - '@solana/rpc-subscriptions-api@5.5.1': - resolution: {integrity: sha512-5Oi7k+GdeS8xR2ly1iuSFkAv6CZqwG0Z6b1QZKbEgxadE1XGSDrhM2cn59l+bqCozUWCqh4c/A2znU/qQjROlw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} - '@solana/rpc-subscriptions-channel-websocket@2.3.0': - resolution: {integrity: sha512-2oL6ceFwejIgeWzbNiUHI2tZZnaOxNTSerszcin7wYQwijxtpVgUHiuItM/Y70DQmH9sKhmikQp+dqeGalaJxw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - ws: ^8.18.0 + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@solana/rpc-subscriptions-channel-websocket@3.0.3': - resolution: {integrity: sha512-zUzUlb8Cwnw+SHlsLrSqyBRtOJKGc+FvSNJo/vWAkLShoV0wUDMPv7VvhTngJx3B/3ANfrOZ4i08i9QfYPAvpQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - ws: ^8.18.0 + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@solana/rpc-subscriptions-channel-websocket@5.5.1': - resolution: {integrity: sha512-7tGfBBrYY8TrngOyxSHoCU5shy86iA9SRMRrPSyBhEaZRAk6dnbdpmUTez7gtdVo0BCvh9nzQtUycKWSS7PnFQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - '@solana/rpc-subscriptions-spec@2.3.0': - resolution: {integrity: sha512-rdmVcl4PvNKQeA2l8DorIeALCgJEMSu7U8AXJS1PICeb2lQuMeaR+6cs/iowjvIB0lMVjYN2sFf6Q3dJPu6wWg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@solana/rpc-subscriptions-spec@3.0.3': - resolution: {integrity: sha512-9KpQ32OBJWS85mn6q3gkM0AjQe1LKYlMU7gpJRrla/lvXxNLhI95tz5K6StctpUreVmRWTVkNamHE69uUQyY8A==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@solana/rpc-subscriptions-spec@5.5.1': - resolution: {integrity: sha512-iq+rGq5fMKP3/mKHPNB6MC8IbVW41KGZg83Us/+LE3AWOTWV1WT20KT2iH1F1ik9roi42COv/TpoZZvhKj45XQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + '@types/express-serve-static-core@4.19.8': + resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} - '@solana/rpc-subscriptions@2.3.0': - resolution: {integrity: sha512-Uyr10nZKGVzvCOqwCZgwYrzuoDyUdwtgQRefh13pXIrdo4wYjVmoLykH49Omt6abwStB0a4UL5gX9V4mFdDJZg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' + '@types/express-serve-static-core@5.1.0': + resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} - '@solana/rpc-subscriptions@3.0.3': - resolution: {integrity: sha512-LRvz6NaqvtsYFd32KwZ+rwYQ9XCs+DWjV8BvBLsJpt9/NWSuHf/7Sy/vvP6qtKxut692H/TMvHnC4iulg0WmiQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} - '@solana/rpc-subscriptions@5.5.1': - resolution: {integrity: sha512-CTMy5bt/6mDh4tc6vUJms9EcuZj3xvK0/xq8IQ90rhkpYvate91RjBP+egvjgSayUg9yucU9vNuUpEjz4spM7w==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + '@types/express@5.0.5': + resolution: {integrity: sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==} - '@solana/rpc-transformers@2.3.0': - resolution: {integrity: sha512-UuHYK3XEpo9nMXdjyGKkPCOr7WsZsxs7zLYDO1A5ELH3P3JoehvrDegYRAGzBS2VKsfApZ86ZpJToP0K3PhmMA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-transformers@3.0.3': - resolution: {integrity: sha512-lzdaZM/dG3s19Tsk4mkJA5JBoS1eX9DnD7z62gkDwrwJDkDBzkAJT9aLcsYFfTmwTfIp6uU2UPgGYc97i1wezw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-transformers@5.5.1': - resolution: {integrity: sha512-OsWqLCQdcrRJKvHiMmwFhp9noNZ4FARuMkHT5us3ustDLXaxOjF0gfqZLnMkulSLcKt7TGXqMhBV+HCo7z5M8Q==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/rpc-transport-http@2.3.0': - resolution: {integrity: sha512-HFKydmxGw8nAF5N+S0NLnPBDCe5oMDtI2RAmW8DMqP4U3Zxt2XWhvV1SNkAldT5tF0U1vP+is6fHxyhk4xqEvg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-transport-http@3.0.3': - resolution: {integrity: sha512-bIXFwr2LR5A97Z46dI661MJPbHnPfcShBjFzOS/8Rnr8P4ho3j/9EUtjDrsqoxGJT3SLWj5OlyXAlaDAvVTOUQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-transport-http@5.5.1': - resolution: {integrity: sha512-yv8GoVSHqEV0kUJEIhkdOVkR2SvJ6yoWC51cJn2rSV7plr6huLGe0JgujCmB7uZhhaLbcbP3zxXxu9sOjsi7Fg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/rpc-types@2.3.0': - resolution: {integrity: sha512-O09YX2hED2QUyGxrMOxQ9GzH1LlEwwZWu69QbL4oYmIf6P5dzEEHcqRY6L1LsDVqc/dzAdEs/E1FaPrcIaIIPw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-types@3.0.3': - resolution: {integrity: sha512-petWQ5xSny9UfmC3Qp2owyhNU0w9SyBww4+v7tSVyXMcCC9v6j/XsqTeimH1S0qQUllnv0/FY83ohFaxofmZ6Q==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-types@5.5.1': - resolution: {integrity: sha512-bibTFQ7PbHJJjGJPmfYC2I+/5CRFS4O2p9WwbFraX1Keeel+nRrt/NBXIy8veP5AEn2sVJIyJPpWBRpCx1oATA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/rpc@2.3.0': - resolution: {integrity: sha512-ZWN76iNQAOCpYC7yKfb3UNLIMZf603JckLKOOLTHuy9MZnTN8XV6uwvDFhf42XvhglgUjGCEnbUqWtxQ9pa/pQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc@3.0.3': - resolution: {integrity: sha512-3oukAaLK78GegkKcm6iNmRnO4mFeNz+BMvA8T56oizoBNKiRVEq/6DFzVX/LkmZ+wvD601pAB3uCdrTPcC0YKQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc@5.5.1': - resolution: {integrity: sha512-ku8zTUMrkCWci66PRIBC+1mXepEnZH/q1f3ck0kJZ95a06bOTl5KU7HeXWtskkyefzARJ5zvCs54AD5nxjQJ+A==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/signers@2.3.0': - resolution: {integrity: sha512-OSv6fGr/MFRx6J+ZChQMRqKNPGGmdjkqarKkRzkwmv7v8quWsIRnJT5EV8tBy3LI4DLO/A8vKiNSPzvm1TdaiQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/signers@3.0.3': - resolution: {integrity: sha512-UwCd/uPYTZiwd283JKVyOWLLN5sIgMBqGDyUmNU3vo9hcmXKv5ZGm/9TvwMY2z35sXWuIOcj7etxJ8OoWc/ObQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/signers@5.5.1': - resolution: {integrity: sha512-FY0IVaBT2kCAze55vEieR6hag4coqcuJ31Aw3hqRH7mv6sV8oqwuJmUrx+uFwOp1gwd5OEAzlv6N4hOOple4sQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/subscribable@2.3.0': - resolution: {integrity: sha512-DkgohEDbMkdTWiKAoatY02Njr56WXx9e/dKKfmne8/Ad6/2llUIrax78nCdlvZW9quXMaXPTxZvdQqo9N669Og==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/subscribable@3.0.3': - resolution: {integrity: sha512-FJ27LKGHLQ5GGttPvTOLQDLrrOZEgvaJhB7yYaHAhPk25+p+erBaQpjePhfkMyUbL1FQbxn1SUJmS6jUuaPjlQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/subscribable@5.5.1': - resolution: {integrity: sha512-9K0PsynFq0CsmK1CDi5Y2vUIJpCqkgSS5yfDN0eKPgHqEptLEaia09Kaxc90cSZDZU5mKY/zv1NBmB6Aro9zQQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/sysvars@2.3.0': - resolution: {integrity: sha512-LvjADZrpZ+CnhlHqfI5cmsRzX9Rpyb1Ox2dMHnbsRNzeKAMhu9w4ZBIaeTdO322zsTr509G1B+k2ABD3whvUBA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/sysvars@3.0.3': - resolution: {integrity: sha512-GnHew+QeKCs2f9ow+20swEJMH4mDfJA/QhtPgOPTYQx/z69J4IieYJ7fZenSHnA//lJ45fVdNdmy1trypvPLBQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/sysvars@5.5.1': - resolution: {integrity: sha512-k3Quq87Mm+geGUu1GWv6knPk0ALsfY6EKSJGw9xUJDHzY/RkYSBnh0RiOrUhtFm2TDNjOailg8/m0VHmi3reFA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/transaction-confirmation@2.3.0': - resolution: {integrity: sha512-UiEuiHCfAAZEKdfne/XljFNJbsKAe701UQHKXEInYzIgBjRbvaeYZlBmkkqtxwcasgBTOmEaEKT44J14N9VZDw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/transaction-confirmation@3.0.3': - resolution: {integrity: sha512-dXx0OLtR95LMuARgi2dDQlL1QYmk56DOou5q9wKymmeV3JTvfDExeWXnOgjRBBq/dEfj4ugN1aZuTaS18UirFw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/transaction-confirmation@5.5.1': - resolution: {integrity: sha512-j4mKlYPHEyu+OD7MBt3jRoX4ScFgkhZC6H65on4Fux6LMScgivPJlwnKoZMnsgxFgWds0pl+BYzSiALDsXlYtw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/transaction-messages@2.3.0': - resolution: {integrity: sha512-bgqvWuy3MqKS5JdNLH649q+ngiyOu5rGS3DizSnWwYUd76RxZl1kN6CoqHSrrMzFMvis6sck/yPGG3wqrMlAww==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/transaction-messages@3.0.3': - resolution: {integrity: sha512-s+6NWRnBhnnjFWV4x2tzBzoWa6e5LiIxIvJlWwVQBFkc8fMGY04w7jkFh0PM08t/QFKeXBEWkyBDa/TFYdkWug==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/transaction-messages@5.5.1': - resolution: {integrity: sha512-aXyhMCEaAp3M/4fP0akwBBQkFPr4pfwoC5CLDq999r/FUwDax2RE/h4Ic7h2Xk+JdcUwsb+rLq85Y52hq84XvQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/transactions@2.3.0': - resolution: {integrity: sha512-LnTvdi8QnrQtuEZor5Msje61sDpPstTVwKg4y81tNxDhiyomjuvnSNLAq6QsB9gIxUqbNzPZgOG9IU4I4/Uaug==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/transactions@3.0.3': - resolution: {integrity: sha512-iMX+n9j4ON7H1nKlWEbMqMOpKYC6yVGxKKmWHT1KdLRG7v+03I4DnDeFoI+Zmw56FA+7Bbne8jwwX60Q1vk/MQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/transactions@5.5.1': - resolution: {integrity: sha512-8hHtDxtqalZ157pnx6p8k10D7J/KY/biLzfgh9R09VNLLY3Fqi7kJvJCr7M2ik3oRll56pxhraAGCC9yIT6eOA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@solana/wallet-standard-features@1.3.0': - resolution: {integrity: sha512-ZhpZtD+4VArf6RPitsVExvgkF+nGghd1rzPjd97GmBximpnt1rsUxMOEyoIEuH3XBxPyNB6Us7ha7RHWQR+abg==} - engines: {node: '>=16'} - - '@solana/web3.js@1.98.4': - resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} - - '@spruceid/siwe-parser@2.1.2': - resolution: {integrity: sha512-d/r3S1LwJyMaRAKQ0awmo9whfXeE88Qt00vRj91q5uv5ATtWIQEGJ67Yr5eSZw5zp1/fZCXZYuEckt8lSkereQ==} - - '@stablelib/binary@1.0.1': - resolution: {integrity: sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==} - - '@stablelib/int@1.0.1': - resolution: {integrity: sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==} - - '@stablelib/random@1.0.2': - resolution: {integrity: sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==} - - '@stablelib/wipe@1.0.1': - resolution: {integrity: sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==} - - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - - '@swc/helpers@0.5.17': - resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - - '@tanstack/query-core@5.90.7': - resolution: {integrity: sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ==} - - '@tanstack/react-query@5.90.7': - resolution: {integrity: sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ==} - peerDependencies: - react: ^18 || ^19 - - '@types/body-parser@1.19.6': - resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/express-serve-static-core@4.19.8': - resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} - - '@types/express-serve-static-core@5.1.0': - resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} - - '@types/express@4.17.25': - resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} - - '@types/express@5.0.5': - resolution: {integrity: sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==} + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} @@ -2721,6 +2176,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/lodash@4.17.20': resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} @@ -2753,6 +2211,9 @@ packages: '@types/react@19.2.2': resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/send@0.17.6': resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} @@ -3043,14 +2504,13 @@ packages: zod: optional: true + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -3168,6 +2628,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + axios-retry@4.5.0: resolution: {integrity: sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==} peerDependencies: @@ -3176,6 +2639,9 @@ packages: axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + axios@1.15.0: + resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -3185,6 +2651,10 @@ packages: base-x@5.0.1: resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + base32.js@0.1.0: + resolution: {integrity: sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==} + engines: {node: '>=0.12.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -3195,6 +2665,9 @@ packages: big.js@6.2.2: resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -3205,10 +2678,6 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} - engines: {node: '>=18'} - borsh@0.7.0: resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} @@ -3255,6 +2724,14 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -3307,6 +2784,9 @@ packages: cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + clsx@1.2.1: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} @@ -3326,6 +2806,10 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@14.0.0: resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} engines: {node: '>=20'} @@ -3359,10 +2843,6 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} - content-disposition@1.0.1: - resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} - engines: {node: '>=18'} - content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -3373,21 +2853,17 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -3488,13 +2964,21 @@ packages: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} - deep-eql@5.0.2: + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -3522,6 +3006,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + derive-valtio@0.1.0: resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==} peerDependencies: @@ -3790,32 +3278,18 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - eventsource-parser@3.0.6: - resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} - engines: {node: '>=18.0.0'} - - eventsource@3.0.7: - resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} - engines: {node: '>=18.0.0'} + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} - express-rate-limit@8.2.1: - resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - extension-port-stream@3.0.0: resolution: {integrity: sha512-an2S5quJMiy5bnZKEf6AkfH/7r8CzHvhchU40gxN+OM6HPhe7Z9T1FUychcf2M9PpPOO0Hf7BAEfJkw2TDIBDw==} engines: {node: '>=12.0.0'} @@ -3824,6 +3298,9 @@ packages: resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} engines: {node: '> 0.1.90'} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3837,9 +3314,15 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + fast-redact@3.5.0: resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} engines: {node: '>=6'} @@ -3856,6 +3339,9 @@ packages: fastestsmallesttextencoderdecoder@1.0.22: resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} + fastify@5.8.4: + resolution: {integrity: sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -3868,6 +3354,9 @@ packages: picomatch: optional: true + feaxios@0.0.23: + resolution: {integrity: sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -3887,9 +3376,9 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} + find-my-way@9.5.0: + resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + engines: {node: '>=20'} find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} @@ -3930,6 +3419,10 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3938,10 +3431,6 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3977,6 +3466,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -4012,6 +3505,10 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -4049,10 +3546,6 @@ packages: resolution: {integrity: sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg==} engines: {node: '>=16.9.0'} - hono@4.11.9: - resolution: {integrity: sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==} - engines: {node: '>=16.9.0'} - hot-shots@10.2.1: resolution: {integrity: sha512-tmjcyZkG/qADhcdC7UjAp8D7v7W2DOYFgaZ48fYMuayMQmVVUg8fntKmrjes/b40ef6yZ+qt1lB8kuEDfLC4zw==} engines: {node: '>=10.0.0'} @@ -4061,18 +3554,21 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -4088,10 +3584,6 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} - engines: {node: '>=0.10.0'} - idb-keyval@6.2.1: resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} @@ -4132,14 +3624,14 @@ packages: resolution: {integrity: sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==} engines: {node: '>=12.22.0'} - ip-address@10.0.1: - resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} - engines: {node: '>= 12'} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -4221,9 +3713,6 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -4232,6 +3721,10 @@ packages: resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} engines: {node: '>=10'} + is-retry-allowed@3.0.0: + resolution: {integrity: sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==} + engines: {node: '>=12'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -4300,8 +3793,8 @@ packages: engines: {node: '>=8'} hasBin: true - jose@6.1.0: - resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==} + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} @@ -4310,6 +3803,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} @@ -4340,15 +3836,15 @@ packages: json-rpc-random-id@1.0.1: resolution: {integrity: sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==} + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-schema-typed@8.0.2: - resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} - json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -4359,6 +3855,10 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + keccak@3.0.4: resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==} engines: {node: '>=10.0.0'} @@ -4373,6 +3873,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -4419,6 +3922,10 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -4440,17 +3947,9 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -4470,23 +3969,23 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} hasBin: true + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -4549,10 +4048,6 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - next@16.1.6: resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} engines: {node: '>=20.9.0'} @@ -4607,6 +4102,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + nwsapi@2.2.22: resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} @@ -4647,6 +4146,10 @@ packages: on-exit-leak-free@0.2.0: resolution: {integrity: sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -4700,6 +4203,10 @@ packages: typescript: optional: true + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -4758,9 +4265,6 @@ packages: path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} - path-to-regexp@8.3.0: - resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -4790,9 +4294,19 @@ packages: pino-abstract-transport@0.5.0: resolution: {integrity: sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + pino-std-serializers@4.0.0: resolution: {integrity: sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==} + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + pino@7.11.0: resolution: {integrity: sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==} hasBin: true @@ -4801,10 +4315,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pkce-challenge@5.0.1: - resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} - engines: {node: '>=16.20.0'} - pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -4868,6 +4378,9 @@ packages: wagmi: optional: true + poseidon-lite@0.2.1: + resolution: {integrity: sha512-xIr+G6HeYfOhCuswdqcFpSX47SPhm0EpisWJ6h7fHlWwaVIvH3dLnejpatrtw6Xc6HaLrpq05y7VRfvDmDGIog==} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -4923,6 +4436,12 @@ packages: process-warning@1.0.0: resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -4933,6 +4452,10 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -4949,10 +4472,6 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} - qs@6.14.1: - resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} - engines: {node: '>=0.6'} - query-string@7.1.3: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} @@ -4963,9 +4482,16 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -4974,10 +4500,6 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - react-dom@19.2.0: resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} peerDependencies: @@ -5002,6 +4524,10 @@ packages: resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==} engines: {node: '>= 12.13.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -5032,6 +4558,9 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -5048,19 +4577,25 @@ packages: engines: {node: '>= 0.4'} hasBin: true + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup@4.52.5: resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - rpc-websockets@9.3.1: resolution: {integrity: sha512-bY6a+i/lEtBJ/mUxwsCTgevoV1P0foXTVA7UoThzaIWbM+3NDqorf8NBWs5DmqKTFeA1IoNzgvkWjFCPgnzUiQ==} @@ -5088,6 +4623,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-regex2@5.1.0: + resolution: {integrity: sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==} + hasBin: true + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -5102,6 +4641,9 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -5120,21 +4662,16 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - serve-static@1.16.2: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -5206,6 +4743,9 @@ packages: sonic-boom@2.8.0: resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -5242,10 +4782,6 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -5365,6 +4901,10 @@ packages: thread-stream@0.15.2: resolution: {integrity: sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -5402,10 +4942,17 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -5490,10 +5037,6 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -5537,9 +5080,6 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici-types@7.21.0: - resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} - unix-dgram@2.0.7: resolution: {integrity: sha512-pWaQorcdxEUBFIKjCqqIlQaOoNVmchyoaNAJ/1LwyyfK2XSxcBhgJNiSE8ZRhR0xkNGyk4xInt1G03QPoKXY5A==} engines: {node: '>=0.10.48'} @@ -5613,6 +5153,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urijs@1.19.11: + resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} + use-sync-external-store@1.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: @@ -5908,18 +5451,6 @@ packages: utf-8-validate: optional: true - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -5953,11 +5484,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod-to-json-schema@3.25.1: - resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} - peerDependencies: - zod: ^3.25 || ^4 - zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} @@ -6027,6 +5553,29 @@ snapshots: '@adraffy/ens-normalize@1.11.1': {} + '@aptos-labs/aptos-cli@1.1.1': + dependencies: + commander: 12.1.0 + + '@aptos-labs/aptos-client@2.2.0(got@11.8.6)': + dependencies: + got: 11.8.6 + + '@aptos-labs/ts-sdk@5.2.1(got@11.8.6)': + dependencies: + '@aptos-labs/aptos-cli': 1.1.1 + '@aptos-labs/aptos-client': 2.2.0(got@11.8.6) + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + eventemitter3: 5.0.1 + js-base64: 3.7.8 + jwt-decode: 4.0.0 + poseidon-lite: 0.2.1 + transitivePeerDependencies: + - got + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -6057,9 +5606,9 @@ snapshots: - utf-8-validate - zod - '@base-org/account@2.4.0(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)': + '@base-org/account@2.4.0(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)': dependencies: - '@coinbase/cdp-sdk': 1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@coinbase/cdp-sdk': 1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 @@ -6091,53 +5640,7 @@ snapshots: abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) axios: 1.13.2 axios-retry: 4.5.0(axios@1.13.2) - jose: 6.1.0 - md5: 2.3.0 - uncrypto: 0.1.3 - viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zod: 3.25.76 - transitivePeerDependencies: - - bufferutil - - debug - - encoding - - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - - ws - - '@coinbase/cdp-sdk@1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana-program/system': 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) - abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) - axios: 1.13.2 - axios-retry: 4.5.0(axios@1.13.2) - jose: 6.1.0 - md5: 2.3.0 - uncrypto: 0.1.3 - viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zod: 3.25.76 - transitivePeerDependencies: - - bufferutil - - debug - - encoding - - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - - ws - - '@coinbase/cdp-sdk@1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana-program/system': 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) - abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) - axios: 1.13.2 - axios-retry: 4.5.0(axios@1.13.2) - jose: 6.1.0 + jose: 6.1.3 md5: 2.3.0 uncrypto: 0.1.3 viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) @@ -6377,6 +5880,29 @@ snapshots: ethereum-cryptography: 2.2.1 micro-ftch: 0.3.1 + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.3.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + '@gemini-wallet/core@0.2.0(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@metamask/rpc-errors': 7.0.2 @@ -6393,10 +5919,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@hono/node-server@1.19.9(hono@4.11.9)': - dependencies: - hono: 4.11.9 - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -6687,7 +6209,7 @@ snapshots: debug: 4.4.3 lodash: 4.17.21 pony-cause: 2.1.11 - semver: 7.7.3 + semver: 7.7.4 uuid: 9.0.1 transitivePeerDependencies: - supports-color @@ -6711,7 +6233,7 @@ snapshots: '@types/debug': 4.1.12 debug: 4.4.3 pony-cause: 2.1.11 - semver: 7.7.3 + semver: 7.7.4 uuid: 9.0.1 transitivePeerDependencies: - supports-color @@ -6730,28 +6252,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': - dependencies: - '@hono/node-server': 1.19.9(hono@4.11.9) - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - content-type: 1.0.5 - cors: 2.8.6 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.6 - express: 5.2.1 - express-rate-limit: 8.2.1(express@5.2.1) - hono: 4.11.9 - jose: 6.1.3 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) - transitivePeerDependencies: - - supports-color - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': optional: true @@ -6859,6 +6359,8 @@ snapshots: '@paulmillr/qr@0.2.1': {} + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -7206,1061 +6708,333 @@ snapshots: optional: true '@rollup/rollup-win32-arm64-msvc@4.52.5': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.52.5': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.52.5': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.52.5': - optional: true - - '@rtsao/scc@1.1.0': {} - - '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': - dependencies: - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - events: 3.3.0 - transitivePeerDependencies: - - bufferutil - - typescript - - utf-8-validate - - zod - - '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': - dependencies: - '@safe-global/safe-gateway-typescript-sdk': 3.23.1 - viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - transitivePeerDependencies: - - bufferutil - - typescript - - utf-8-validate - - zod - - '@safe-global/safe-gateway-typescript-sdk@3.23.1': {} - - '@scure/base@1.1.9': {} - - '@scure/base@1.2.6': {} - - '@scure/bip32@1.4.0': - dependencies: - '@noble/curves': 1.4.2 - '@noble/hashes': 1.4.0 - '@scure/base': 1.1.9 - - '@scure/bip32@1.6.2': - dependencies: - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 - '@scure/base': 1.2.6 - - '@scure/bip32@1.7.0': - dependencies: - '@noble/curves': 1.9.7 - '@noble/hashes': 1.8.0 - '@scure/base': 1.2.6 - - '@scure/bip39@1.3.0': - dependencies: - '@noble/hashes': 1.4.0 - '@scure/base': 1.1.9 - - '@scure/bip39@1.5.4': - dependencies: - '@noble/hashes': 1.7.1 - '@scure/base': 1.2.6 - - '@scure/bip39@1.6.0': - dependencies: - '@noble/hashes': 1.8.0 - '@scure/base': 1.2.6 - - '@socket.io/component-emitter@3.1.2': {} - - '@solana-program/compute-budget@0.11.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))': - dependencies: - '@solana/kit': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - - '@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - - '@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - - '@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.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.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))': - dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/sysvars': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - - '@solana-program/token-2022@0.6.1(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))': - dependencies: - '@solana/kit': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/sysvars': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - - '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - - '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - - '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - - '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - - '@solana-program/token@0.9.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))': - dependencies: - '@solana/kit': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - - '@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec': 2.3.0(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/accounts@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec': 3.0.3(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/accounts@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec': 5.5.1(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/addresses@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/assertions': 2.3.0(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/nominal-types': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/addresses@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/assertions': 3.0.3(typescript@5.9.3) - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/nominal-types': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/addresses@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/assertions': 5.5.1(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/assertions@2.3.0(typescript@5.9.3)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/assertions@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/assertions@5.5.1(typescript@5.9.3)': - dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - - '@solana/buffer-layout@4.0.1': - dependencies: - buffer: 6.0.3 - - '@solana/codecs-core@2.3.0(typescript@5.9.3)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/codecs-core@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/codecs-core@5.5.1(typescript@5.9.3)': - dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - - '@solana/codecs-data-structures@2.3.0(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/codecs-data-structures@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/codecs-data-structures@5.5.1(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - - '@solana/codecs-numbers@2.3.0(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/codecs-numbers@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/codecs-numbers@5.5.1(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - - '@solana/codecs-strings@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - fastestsmallesttextencoderdecoder: 1.0.22 - typescript: 5.9.3 - - '@solana/codecs-strings@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - fastestsmallesttextencoderdecoder: 1.0.22 - typescript: 5.9.3 - - '@solana/codecs-strings@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - fastestsmallesttextencoderdecoder: 1.0.22 - typescript: 5.9.3 - - '@solana/codecs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-data-structures': 2.3.0(typescript@5.9.3) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/options': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/codecs@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) - '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/options': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/codecs@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/options': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/errors@2.3.0(typescript@5.9.3)': - dependencies: - chalk: 5.6.2 - commander: 14.0.2 - typescript: 5.9.3 - - '@solana/errors@3.0.3(typescript@5.9.3)': - dependencies: - chalk: 5.6.2 - commander: 14.0.0 - typescript: 5.9.3 - - '@solana/errors@5.5.1(typescript@5.9.3)': - dependencies: - chalk: 5.6.2 - commander: 14.0.2 - optionalDependencies: - typescript: 5.9.3 - - '@solana/fast-stable-stringify@2.3.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@solana/fast-stable-stringify@3.0.3(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@solana/fast-stable-stringify@5.5.1(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 - - '@solana/functional@2.3.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@solana/functional@3.0.3(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@solana/functional@5.5.1(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 - - '@solana/instruction-plans@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/instructions': 3.0.3(typescript@5.9.3) - '@solana/promises': 3.0.3(typescript@5.9.3) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/instruction-plans@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/instructions': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/promises': 5.5.1(typescript@5.9.3) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/instructions@2.3.0(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/instructions@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/instructions@5.5.1(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - - '@solana/keys@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/assertions': 2.3.0(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/nominal-types': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/keys@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/assertions': 3.0.3(typescript@5.9.3) - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/nominal-types': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/keys@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/assertions': 5.5.1(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/instructions': 2.3.0(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/instructions': 2.3.0(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(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.9.3) - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/instructions': 2.3.0(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(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.9.3) - '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(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.9.3) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/instruction-plans': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/instructions': 3.0.3(typescript@5.9.3) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/programs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/instruction-plans': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/instructions': 3.0.3(typescript@5.9.3) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/programs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/instruction-plans': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/instructions': 3.0.3(typescript@5.9.3) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/programs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws + optional: true - '@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/accounts': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/instruction-plans': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/instructions': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/offchain-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/plugin-core': 5.5.1(typescript@5.9.3) - '@solana/programs': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-api': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-subscriptions': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/signers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/sysvars': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - bufferutil - - fastestsmallesttextencoderdecoder - - utf-8-validate + '@rollup/rollup-win32-ia32-msvc@4.52.5': + optional: true - '@solana/nominal-types@2.3.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 + '@rollup/rollup-win32-x64-gnu@4.52.5': + optional: true - '@solana/nominal-types@3.0.3(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 + '@rollup/rollup-win32-x64-msvc@4.52.5': + optional: true - '@solana/nominal-types@5.5.1(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 + '@rtsao/scc@1.1.0': {} - '@solana/offchain-messages@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 transitivePeerDependencies: - - fastestsmallesttextencoderdecoder + - bufferutil + - typescript + - utf-8-validate + - zod - '@solana/options@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-data-structures': 2.3.0(typescript@5.9.3) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 + '@safe-global/safe-gateway-typescript-sdk': 3.23.1 + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - - fastestsmallesttextencoderdecoder + - bufferutil + - typescript + - utf-8-validate + - zod - '@solana/options@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) - '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder + '@safe-global/safe-gateway-typescript-sdk@3.23.1': {} - '@solana/options@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder + '@scure/base@1.1.9': {} - '@solana/plugin-core@5.5.1(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 + '@scure/base@1.2.6': {} + + '@scure/bip32@1.4.0': + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 - '@solana/programs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@scure/bip32@1.6.2': dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/base': 1.2.6 - '@solana/programs@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@scure/bip32@1.7.0': dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 - '@solana/programs@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@scure/bip39@1.3.0': dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 - '@solana/promises@2.3.0(typescript@5.9.3)': + '@scure/bip39@1.5.4': dependencies: - typescript: 5.9.3 + '@noble/hashes': 1.7.1 + '@scure/base': 1.2.6 - '@solana/promises@3.0.3(typescript@5.9.3)': + '@scure/bip39@1.6.0': dependencies: - typescript: 5.9.3 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 - '@solana/promises@5.5.1(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 + '@sindresorhus/is@4.6.0': {} + + '@socket.io/component-emitter@3.1.2': {} - '@solana/rpc-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec': 2.3.0(typescript@5.9.3) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-api@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana/accounts@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/codecs-core': 3.0.3(typescript@5.9.3) '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) '@solana/rpc-spec': 3.0.3(typescript@5.9.3) - '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-api@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec': 5.5.1(typescript@5.9.3) - '@solana/rpc-transformers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: + '@solana/addresses@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/assertions': 3.0.3(typescript@5.9.3) + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/nominal-types': 3.0.3(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-parsed-types@2.3.0(typescript@5.9.3)': + '@solana/assertions@3.0.3(typescript@5.9.3)': dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) typescript: 5.9.3 - '@solana/rpc-parsed-types@3.0.3(typescript@5.9.3)': + '@solana/buffer-layout@4.0.1': dependencies: - typescript: 5.9.3 - - '@solana/rpc-parsed-types@5.5.1(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 + buffer: 6.0.3 - '@solana/rpc-spec-types@2.3.0(typescript@5.9.3)': + '@solana/codecs-core@2.3.0(typescript@5.9.3)': dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) typescript: 5.9.3 - '@solana/rpc-spec-types@3.0.3(typescript@5.9.3)': + '@solana/codecs-core@3.0.3(typescript@5.9.3)': dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) typescript: 5.9.3 - '@solana/rpc-spec-types@5.5.1(typescript@5.9.3)': - optionalDependencies: + '@solana/codecs-data-structures@3.0.3(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) typescript: 5.9.3 - '@solana/rpc-spec@2.3.0(typescript@5.9.3)': + '@solana/codecs-numbers@2.3.0(typescript@5.9.3)': dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.3) '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) typescript: 5.9.3 - '@solana/rpc-spec@3.0.3(typescript@5.9.3)': + '@solana/codecs-numbers@3.0.3(typescript@5.9.3)': dependencies: + '@solana/codecs-core': 3.0.3(typescript@5.9.3) '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) typescript: 5.9.3 - '@solana/rpc-spec@5.5.1(typescript@5.9.3)': + '@solana/codecs-strings@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - optionalDependencies: + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + fastestsmallesttextencoderdecoder: 1.0.22 typescript: 5.9.3 - '@solana/rpc-subscriptions-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/codecs@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) + '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/options': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-api@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/errors@2.3.0(typescript@5.9.3)': dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) - '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + chalk: 5.6.2 + commander: 14.0.2 typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-api@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/errors@3.0.3(typescript@5.9.3)': dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 5.5.1(typescript@5.9.3) - '@solana/rpc-transformers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: + chalk: 5.6.2 + commander: 14.0.0 typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/fast-stable-stringify@3.0.3(typescript@5.9.3)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) - '@solana/subscribable': 2.3.0(typescript@5.9.3) typescript: 5.9.3 - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/functional@3.0.3(typescript@5.9.3)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) - '@solana/subscribable': 2.3.0(typescript@5.9.3) typescript: 5.9.3 - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/instruction-plans@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) - '@solana/subscribable': 2.3.0(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/instructions': 3.0.3(typescript@5.9.3) + '@solana/promises': 3.0.3(typescript@5.9.3) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/instructions@3.0.3(typescript@5.9.3)': dependencies: + '@solana/codecs-core': 3.0.3(typescript@5.9.3) '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) - '@solana/subscribable': 3.0.3(typescript@5.9.3) typescript: 5.9.3 - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/keys@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/assertions': 3.0.3(typescript@5.9.3) + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/nominal-types': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: + '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/errors': 3.0.3(typescript@5.9.3) '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) - '@solana/subscribable': 3.0.3(typescript@5.9.3) + '@solana/instruction-plans': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/instructions': 3.0.3(typescript@5.9.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/programs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws - '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/nominal-types@3.0.3(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@solana/options@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) + '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) - '@solana/subscribable': 3.0.3(typescript@5.9.3) typescript: 5.9.3 - ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-channel-websocket@5.5.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@solana/programs@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 5.5.1(typescript@5.9.3) - '@solana/subscribable': 5.5.1(typescript@5.9.3) - ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - optionalDependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - - bufferutil - - utf-8-validate + - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-spec@2.3.0(typescript@5.9.3)': + '@solana/promises@3.0.3(typescript@5.9.3)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/promises': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/subscribable': 2.3.0(typescript@5.9.3) typescript: 5.9.3 - '@solana/rpc-subscriptions-spec@3.0.3(typescript@5.9.3)': + '@solana/rpc-api@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/promises': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - '@solana/subscribable': 3.0.3(typescript@5.9.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec': 3.0.3(typescript@5.9.3) + '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-spec@5.5.1(typescript@5.9.3)': + '@solana/rpc-parsed-types@3.0.3(typescript@5.9.3)': dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/promises': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - '@solana/subscribable': 5.5.1(typescript@5.9.3) - optionalDependencies: typescript: 5.9.3 - '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-spec-types@3.0.3(typescript@5.9.3)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/promises': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/subscribable': 2.3.0(typescript@5.9.3) typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-spec@3.0.3(typescript@5.9.3)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/promises': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/subscribable': 2.3.0(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions-api@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/promises': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/subscribable': 2.3.0(typescript@5.9.3) + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) + '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - - ws - '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/fast-stable-stringify': 3.0.3(typescript@5.9.3) '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/promises': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-subscriptions-api': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 3.0.3(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) - '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/subscribable': 3.0.3(typescript@5.9.3) typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions-spec@3.0.3(typescript@5.9.3)': dependencies: '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/fast-stable-stringify': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) '@solana/promises': 3.0.3(typescript@5.9.3) '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-subscriptions-api': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 3.0.3(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) - '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/subscribable': 3.0.3(typescript@5.9.3) typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 3.0.3(typescript@5.9.3) '@solana/fast-stable-stringify': 3.0.3(typescript@5.9.3) @@ -8268,7 +7042,7 @@ snapshots: '@solana/promises': 3.0.3(typescript@5.9.3) '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) '@solana/rpc-subscriptions-api': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 3.0.3(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-channel-websocket': 3.0.3(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -8278,37 +7052,6 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/rpc-subscriptions@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/fast-stable-stringify': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/promises': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-subscriptions-api': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 5.5.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-spec': 5.5.1(typescript@5.9.3) - '@solana/rpc-transformers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/subscribable': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - bufferutil - - fastestsmallesttextencoderdecoder - - utf-8-validate - - '@solana/rpc-transformers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/nominal-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/rpc-transformers@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/errors': 3.0.3(typescript@5.9.3) @@ -8320,26 +7063,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-transformers@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/rpc-transport-http@2.3.0(typescript@5.9.3)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 - undici-types: 7.16.0 - '@solana/rpc-transport-http@3.0.3(typescript@5.9.3)': dependencies: '@solana/errors': 3.0.3(typescript@5.9.3) @@ -8348,27 +7071,6 @@ snapshots: typescript: 5.9.3 undici-types: 7.16.0 - '@solana/rpc-transport-http@5.5.1(typescript@5.9.3)': - dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - undici-types: 7.21.0 - optionalDependencies: - typescript: 5.9.3 - - '@solana/rpc-types@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/nominal-types': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/rpc-types@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -8381,34 +7083,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-types@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/rpc@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/rpc-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-spec': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-transport-http': 2.3.0(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/rpc@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/errors': 3.0.3(typescript@5.9.3) @@ -8424,36 +7098,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/fast-stable-stringify': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/rpc-api': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-spec': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-transformers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-transport-http': 5.5.1(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/signers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/instructions': 2.3.0(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 2.3.0(typescript@5.9.3) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/signers@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -8468,48 +7112,11 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/signers@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/instructions': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - '@solana/offchain-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/subscribable@2.3.0(typescript@5.9.3)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 - '@solana/subscribable@3.0.3(typescript@5.9.3)': dependencies: '@solana/errors': 3.0.3(typescript@5.9.3) typescript: 5.9.3 - '@solana/subscribable@5.5.1(typescript@5.9.3)': - dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - - '@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/sysvars@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -8520,68 +7127,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/accounts': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/promises': 2.3.0(typescript@5.9.3) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/promises': 2.3.0(typescript@5.9.3) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(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.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/promises': 2.3.0(typescript@5.9.3) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(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.9.3) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -8599,119 +7144,17 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/promises': 3.0.3(typescript@5.9.3) - '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/promises': 3.0.3(typescript@5.9.3) - '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/transaction-confirmation@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/promises': 5.5.1(typescript@5.9.3) - '@solana/rpc': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - bufferutil - - fastestsmallesttextencoderdecoder - - utf-8-validate - - '@solana/transaction-messages@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-data-structures': 2.3.0(typescript@5.9.3) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/instructions': 2.3.0(typescript@5.9.3) - '@solana/nominal-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/transaction-messages@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) - '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/instructions': 3.0.3(typescript@5.9.3) - '@solana/nominal-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/transaction-messages@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/instructions': 5.5.1(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/transactions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-data-structures': 2.3.0(typescript@5.9.3) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/instructions': 2.3.0(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) + '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/instructions': 3.0.3(typescript@5.9.3) + '@solana/nominal-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - fastestsmallesttextencoderdecoder @@ -8734,30 +7177,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transactions@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/instructions': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/wallet-standard-features@1.3.0': - dependencies: - '@wallet-standard/base': 1.1.0 - '@wallet-standard/features': 1.1.0 - '@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.28.4 @@ -8801,6 +7220,31 @@ snapshots: '@stablelib/wipe@1.0.1': {} + '@stellar/js-xdr@3.1.2': {} + + '@stellar/stellar-base@14.1.0': + dependencies: + '@noble/curves': 1.9.7 + '@stellar/js-xdr': 3.1.2 + base32.js: 0.1.0 + bignumber.js: 9.3.1 + buffer: 6.0.3 + sha.js: 2.4.12 + + '@stellar/stellar-sdk@14.6.1': + dependencies: + '@stellar/stellar-base': 14.1.0 + axios: 1.15.0 + bignumber.js: 9.3.1 + commander: 14.0.2 + eventsource: 2.0.2 + feaxios: 0.0.23 + randombytes: 2.1.0 + toml: 3.0.0 + urijs: 1.19.11 + transitivePeerDependencies: + - debug + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -8809,6 +7253,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + '@tanstack/query-core@5.90.7': {} '@tanstack/react-query@5.90.7(react@19.2.0)': @@ -8821,6 +7269,13 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 22.19.0 + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 22.19.0 + '@types/responselike': 1.0.3 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -8865,12 +7320,18 @@ snapshots: '@types/express-serve-static-core': 5.1.0 '@types/serve-static': 1.15.10 + '@types/http-cache-semantics@4.2.0': {} + '@types/http-errors@2.0.5': {} '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} + '@types/keyv@3.1.4': + dependencies: + '@types/node': 22.19.0 + '@types/lodash@4.17.20': {} '@types/mime@1.3.5': {} @@ -8899,6 +7360,10 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/responselike@1.0.3': + dependencies: + '@types/node': 22.19.0 + '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 @@ -9061,7 +7526,7 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@wagmi/connectors@5.11.2(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(zod@3.25.76)': + '@wagmi/connectors@5.11.2(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(zod@3.25.76)': dependencies: '@base-org/account': 1.1.1(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(zod@3.25.76) '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(zod@3.25.76) @@ -9072,7 +7537,7 @@ snapshots: '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.19(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + porto: 0.2.19(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 @@ -9108,9 +7573,9 @@ snapshots: - wagmi - zod - '@wagmi/connectors@6.1.3(b7e123a1b2288f08e79400c54ddaa7b2)': + '@wagmi/connectors@6.1.3(3f9c8bf5c06df074762d0fde96a2c152)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + '@base-org/account': 2.4.0(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(zod@3.25.76) '@gemini-wallet/core': 0.3.1(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -9119,7 +7584,7 @@ snapshots: '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + porto: 0.2.35(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 @@ -9759,16 +8224,13 @@ snapshots: typescript: 5.9.3 zod: 4.1.12 + abstract-logging@2.0.1: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -9892,6 +8354,11 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + avvio@9.2.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.19.1 + axios-retry@4.5.0(axios@1.13.2): dependencies: axios: 1.13.2 @@ -9905,6 +8372,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.15.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + balanced-match@1.0.2: {} base-x@3.0.11: @@ -9913,12 +8388,16 @@ snapshots: base-x@5.0.1: {} + base32.js@0.1.0: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.9.19: {} big.js@6.2.2: {} + bignumber.js@9.3.1: {} + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -9943,20 +8422,6 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.2.2: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.3 - http-errors: 2.0.0 - iconv-lite: 0.7.2 - on-finished: 2.4.1 - qs: 6.14.1 - raw-body: 3.0.2 - type-is: 2.0.1 - transitivePeerDependencies: - - supports-color - borsh@0.7.0: dependencies: bn.js: 5.2.2 @@ -10016,6 +8481,18 @@ snapshots: cac@6.7.14: {} + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -10070,6 +8547,10 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 6.2.0 + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + clsx@1.2.1: {} cluster-key-slot@1.1.2: {} @@ -10084,6 +8565,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@12.1.0: {} + commander@14.0.0: {} commander@14.0.2: {} @@ -10104,24 +8587,17 @@ snapshots: dependencies: safe-buffer: 5.2.1 - content-disposition@1.0.1: {} - content-type@1.0.5: {} cookie-es@1.2.2: {} cookie-signature@1.0.6: {} - cookie-signature@1.2.2: {} - cookie@0.7.1: {} - core-util-is@1.0.3: {} + cookie@1.1.1: {} - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 + core-util-is@1.0.3: {} crc-32@1.2.2: {} @@ -10211,10 +8687,16 @@ snapshots: decode-uri-component@0.2.2: {} + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + deep-eql@5.0.2: {} deep-is@0.1.4: {} + defer-to-connect@2.0.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -10237,6 +8719,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + derive-valtio@0.1.0(valtio@1.13.2(@types/react@19.2.2)(react@19.2.0)): dependencies: valtio: 1.13.2(@types/react@19.2.2)(react@19.2.0) @@ -10640,19 +9124,10 @@ snapshots: events@3.3.0: {} - eventsource-parser@3.0.6: {} - - eventsource@3.0.7: - dependencies: - eventsource-parser: 3.0.6 + eventsource@2.0.2: {} expect-type@1.2.2: {} - express-rate-limit@8.2.1(express@5.2.1): - dependencies: - express: 5.2.1 - ip-address: 10.0.1 - express@4.21.2: dependencies: accepts: 1.3.8 @@ -10689,39 +9164,6 @@ snapshots: transitivePeerDependencies: - supports-color - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.2.2 - content-disposition: 1.0.1 - content-type: 1.0.5 - cookie: 0.7.1 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.0 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.1 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 - statuses: 2.0.1 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - extension-port-stream@3.0.0: dependencies: readable-stream: 3.6.2 @@ -10729,6 +9171,8 @@ snapshots: eyes@0.1.8: {} + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -10743,8 +9187,21 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.3.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + fast-redact@3.5.0: {} fast-safe-stringify@2.1.1: {} @@ -10755,6 +9212,24 @@ snapshots: fastestsmallesttextencoderdecoder@1.0.22: {} + fastify@5.8.4: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.2.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.5.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.4 + toad-cache: 3.7.0 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -10763,6 +9238,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + feaxios@0.0.23: + dependencies: + is-retry-allowed: 3.0.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -10788,16 +9267,11 @@ snapshots: transitivePeerDependencies: - supports-color - finalhandler@2.1.1: + find-my-way@9.5.0: dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.0 find-up@4.1.0: dependencies: @@ -10841,12 +9315,18 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + forwarded@0.2.0: {} fresh@0.5.2: {} - fresh@2.0.0: {} - fsevents@2.3.3: optional: true @@ -10887,6 +9367,10 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -10925,6 +9409,20 @@ snapshots: gopd@1.2.0: {} + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + graphemer@1.4.0: {} h3@1.15.4: @@ -10963,8 +9461,6 @@ snapshots: hono@4.10.4: {} - hono@4.11.9: {} - hot-shots@10.2.1: optionalDependencies: unix-dgram: 2.0.7 @@ -10973,6 +9469,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + http-cache-semantics@4.2.0: {} + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -10981,14 +9479,6 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -10996,6 +9486,11 @@ snapshots: transitivePeerDependencies: - supports-color + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -11015,10 +9510,6 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.7.2: - dependencies: - safer-buffer: 2.1.2 - idb-keyval@6.2.1: {} idb-keyval@6.2.2: {} @@ -11072,10 +9563,10 @@ snapshots: transitivePeerDependencies: - supports-color - ip-address@10.0.1: {} - ipaddr.js@1.9.1: {} + ipaddr.js@2.3.0: {} + iron-webcrypto@1.2.1: {} is-arguments@1.2.0: @@ -11158,8 +9649,6 @@ snapshots: is-potential-custom-element-name@1.0.1: {} - is-promise@4.0.0: {} - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -11169,6 +9658,8 @@ snapshots: is-retry-allowed@2.2.0: {} + is-retry-allowed@3.0.0: {} + is-set@2.0.3: {} is-shared-array-buffer@1.0.4: @@ -11245,12 +9736,14 @@ snapshots: - bufferutil - utf-8-validate - jose@6.1.0: {} + jose@5.10.0: {} jose@6.1.3: {} joycon@3.1.1: {} + js-base64@3.7.8: {} + js-tokens@9.0.1: {} js-yaml@4.1.0: @@ -11295,12 +9788,14 @@ snapshots: json-rpc-random-id@1.0.1: {} + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} - json-schema-typed@8.0.2: {} - json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-safe@5.0.1: {} @@ -11309,6 +9804,8 @@ snapshots: dependencies: minimist: 1.2.8 + jwt-decode@4.0.0: {} + keccak@3.0.4: dependencies: node-addon-api: 2.0.2 @@ -11326,6 +9823,12 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -11368,6 +9871,8 @@ snapshots: loupe@3.2.1: {} + lowercase-keys@2.0.0: {} + lru-cache@10.4.3: {} luxon@3.7.2: {} @@ -11386,12 +9891,8 @@ snapshots: media-typer@0.3.0: {} - media-typer@1.1.0: {} - merge-descriptors@1.0.3: {} - merge-descriptors@2.0.0: {} - merge2@1.4.1: {} methods@1.1.2: {} @@ -11405,18 +9906,16 @@ snapshots: mime-db@1.52.0: {} - mime-db@1.54.0: {} - mime-types@2.1.35: dependencies: mime-db: 1.52.0 - mime-types@3.0.2: - dependencies: - mime-db: 1.54.0 - mime@1.6.0: {} + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -11479,8 +9978,6 @@ snapshots: negotiator@0.6.3: {} - negotiator@1.0.0: {} - next@16.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@next/env': 16.1.6 @@ -11526,6 +10023,8 @@ snapshots: normalize-path@3.0.0: {} + normalize-url@6.1.0: {} + nwsapi@2.2.22: {} obj-multiplex@1.0.0: @@ -11577,6 +10076,8 @@ snapshots: on-exit-leak-free@0.2.0: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -11694,6 +10195,8 @@ snapshots: transitivePeerDependencies: - zod + p-cancelable@2.1.1: {} + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -11743,8 +10246,6 @@ snapshots: path-to-regexp@0.1.12: {} - path-to-regexp@8.3.0: {} - pathe@2.0.3: {} pathval@2.0.1: {} @@ -11764,8 +10265,28 @@ snapshots: duplexify: 4.1.3 split2: 4.2.0 + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + pino-std-serializers@4.0.0: {} + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + pino@7.11.0: dependencies: atomic-sleep: 1.0.0 @@ -11782,8 +10303,6 @@ snapshots: pirates@4.0.7: {} - pkce-challenge@5.0.1: {} - pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -11794,7 +10313,7 @@ snapshots: pony-cause@2.1.11: {} - porto@0.2.19(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): + porto@0.2.19(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): dependencies: '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) hono: 4.10.4 @@ -11808,13 +10327,13 @@ snapshots: '@tanstack/react-query': 5.90.7(react@19.2.0) react: 19.2.0 typescript: 5.9.3 - wagmi: 2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + wagmi: 2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) transitivePeerDependencies: - '@types/react' - immer - use-sync-external-store - porto@0.2.35(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): + porto@0.2.35(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): dependencies: '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) hono: 4.10.4 @@ -11828,12 +10347,14 @@ snapshots: '@tanstack/react-query': 5.90.7(react@19.2.0) react: 19.2.0 typescript: 5.9.3 - wagmi: 2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + wagmi: 2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) transitivePeerDependencies: - '@types/react' - immer - use-sync-external-store + poseidon-lite@0.2.1: {} + possible-typed-array-names@1.1.0: {} postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.20.6): @@ -11871,6 +10392,10 @@ snapshots: process-warning@1.0.0: {} + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -11880,6 +10405,8 @@ snapshots: proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} + pump@3.0.3: dependencies: end-of-stream: 1.4.5 @@ -11898,10 +10425,6 @@ snapshots: dependencies: side-channel: 1.1.0 - qs@6.14.1: - dependencies: - side-channel: 1.1.0 - query-string@7.1.3: dependencies: decode-uri-component: 0.2.2 @@ -11913,8 +10436,14 @@ snapshots: quick-format-unescaped@4.0.4: {} + quick-lru@5.1.1: {} + radix3@1.1.2: {} + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + range-parser@1.2.1: {} raw-body@2.5.2: @@ -11924,13 +10453,6 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - unpipe: 1.0.0 - react-dom@19.2.0(react@19.2.0): dependencies: react: 19.2.0 @@ -11958,6 +10480,8 @@ snapshots: real-require@0.1.0: {} + real-require@0.2.0: {} + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -11999,6 +10523,8 @@ snapshots: require-main-filename@2.0.0: {} + resolve-alpn@1.2.1: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -12011,8 +10537,16 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + ret@0.5.0: {} + reusify@1.1.0: {} + rfdc@1.4.1: {} + rollup@4.52.5: dependencies: '@types/estree': 1.0.8 @@ -12041,16 +10575,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 - router@2.2.0: - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.3.0 - transitivePeerDependencies: - - supports-color - rpc-websockets@9.3.1: dependencies: '@swc/helpers': 0.5.17 @@ -12093,6 +10617,10 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-regex2@5.1.0: + dependencies: + ret: 0.5.0 + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -12103,6 +10631,8 @@ snapshots: scheduler@0.27.0: {} + secure-json-parse@4.1.0: {} + semver@6.3.1: {} semver@7.7.3: {} @@ -12127,22 +10657,6 @@ snapshots: transitivePeerDependencies: - supports-color - send@1.2.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - serve-static@1.16.2: dependencies: encodeurl: 2.0.0 @@ -12152,17 +10666,10 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@2.2.1: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1 - transitivePeerDependencies: - - supports-color - set-blocking@2.0.0: {} + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -12293,6 +10800,10 @@ snapshots: dependencies: atomic-sleep: 1.0.0 + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} source-map@0.8.0-beta.0: @@ -12318,8 +10829,6 @@ snapshots: statuses@2.0.1: {} - statuses@2.0.2: {} - std-env@3.10.0: {} stop-iteration-iterator@1.1.0: @@ -12441,6 +10950,10 @@ snapshots: dependencies: real-require: 0.1.0 + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -12472,8 +10985,12 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toidentifier@1.0.1: {} + toml@3.0.0: {} + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -12559,12 +11076,6 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 - type-is@2.0.1: - dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.2 - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -12621,8 +11132,6 @@ snapshots: undici-types@7.16.0: {} - undici-types@7.21.0: {} - unix-dgram@2.0.7: dependencies: bindings: 1.5.0 @@ -12649,6 +11158,8 @@ snapshots: dependencies: punycode: 2.3.1 + urijs@1.19.11: {} + use-sync-external-store@1.2.0(react@19.2.0): dependencies: react: 19.2.0 @@ -12852,10 +11363,10 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): + wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.9.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): dependencies: '@tanstack/react-query': 5.90.7(react@19.2.0) - '@wagmi/connectors': 6.1.3(b7e123a1b2288f08e79400c54ddaa7b2) + '@wagmi/connectors': 6.1.3(3f9c8bf5c06df074762d0fde96a2c152) '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) react: 19.2.0 use-sync-external-store: 1.4.0(react@19.2.0) @@ -13022,11 +11533,6 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 5.0.10 - ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): - optionalDependencies: - bufferutil: 4.0.9 - utf-8-validate: 5.0.10 - xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} @@ -13060,10 +11566,6 @@ snapshots: yocto-queue@0.1.0: {} - zod-to-json-schema@3.25.1(zod@3.25.76): - dependencies: - zod: 3.25.76 - zod@3.22.4: {} zod@3.25.76: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a6ce226..a0d2d76 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,4 +5,5 @@ packages: - "typescript/packages/mcp" - "typescript/packages/http/*" - "typescript/packages/mechanisms/*" + - "!typescript/packages/mechanisms/svm" - "typescript/packages/legacy/*" diff --git a/typescript/package.json b/typescript/package.json index 307d86b..4bc5ef0 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -21,7 +21,7 @@ "lint:check": "turbo run lint:check", "format:check": "turbo run format:check", "test": "turbo run test", - "test:integration": "pnpm --filter @x402/core --filter @x402/evm --filter @x402/svm test:integration", + "test:integration": "pnpm --filter @x402/core --filter @x402/evm test:integration", "test:all": "pnpm test && pnpm test:integration" }, "keywords": [], diff --git a/typescript/packages/core/CHANGELOG.md b/typescript/packages/core/CHANGELOG.md index 9af23b1..d1a0383 100644 --- a/typescript/packages/core/CHANGELOG.md +++ b/typescript/packages/core/CHANGELOG.md @@ -1,5 +1,79 @@ # @x402/core Changelog +## 2.9.0 + +### Minor Changes + +- 2250cae: Migrated project from coinbase/x402 to x402-foundation/x402 organization +- d352574: Add SettlementOverrides support for partial settlement (upto scheme). Route handlers can call setSettlementOverrides() to settle less than the authorized maximum, enabling usage-based billing. + +### Patch Changes + +- 8cf3fca: Export all hook types and hook context interfaces from the server entry point +- c0e3969: Fixed HTTPFacilitatorClient not following 308 redirects from facilitator endpoints. Normalized base URL to strip trailing slashes and explicitly set `redirect: "follow"` on all fetch calls for cross-runtime compatibility. + +## 2.8.0 + +### Minor Changes + +- 067f297: Added `routePattern` to `HTTPRequestContext` and `pattern` to `CompiledRoute` to thread the matched route pattern through to server extensions, enabling dynamic route support in discovery extensions. +- 4c1e44f: Treat malformed facilitator success payloads as upstream facilitator errors and return 502 responses from framework middleware instead of flattening them into payment failures. +- 5135fab: Accept null in extra and extension fields + +## 2.7.0 + +### Minor Changes + +- 8931cb3: Added support for Express-style `:param` dynamic route parameters in route matching. Routes like `/api/users/:id` and `/api/chapters/:seriesId/:chapterId` now match correctly alongside the existing `[param]` (Next.js) and `*` (wildcard) patterns. + +## 2.6.0 + +### Minor Changes + +- f41baed: Added `x402Version` field to `VerifyRequest`, `SettleRequest`, `VerifyRequestV1`, and `SettleRequestV1` types to match what all SDK implementations already send in facilitator request bodies. +- aeef1bf: Added dynamic function for servers to generate custom response for settlement failures defaulting to empty +- 2564781: Include PAYMENT-RESPONSE header on settlement failure responses +- b341973: Remove duplicate server-local `ResourceInfo` interface; use the wire-format `ResourceInfo` from `types/payments.ts` directly throughout the server module. +- 29fe09a: Make ResourceInfo.description, ResourceInfo.mimeType, and PaymentPayload.resource optional to match v2 spec + +## 2.5.0 + +### Minor Changes + +- Bumped to align version with dependent packages (@x402/evm, @x402/extensions) + +### Patch Changes + +- 96a9db0: Fix extra field passthrough in buildPaymentRequirementsFromOptions for custom schemes +- d0a2b11: Added transport context to enrichSettleResponse and enrichPaymentRequiredResponse hooks + +## 2.4.0 + +### Minor Changes + +- 57a5488: Add Aptos blockchain support to x402 payment protocol + + - Introduces new `@x402/aptos` package with full client, server, and facilitator scheme implementations + - Supports exact payment mechanism for Aptos using native APT and fungible assets + - Includes sponsored transaction support where facilitator pays gas fees + - Provides `registerExactAptosScheme` helpers for easy client and server integration + - Adds Aptos network constants for mainnet and testnet + - Updates core types to support Aptos-specific payment flows + +- 018181b: Implement EIP-2612 gasless Permit2 approval extension + + - Added extension enrichment hooks to `x402Client`, enabling scheme clients to inject extension data (e.g. EIP-2612 permits) into payment payloads when the server advertises support + +### Patch Changes + +- 3fb55d7: Upgraded facilitator extension registration from string keys to FacilitatorExtension objects. Added FacilitatorContext threaded through SchemeNetworkFacilitator.verify/settle for mechanism access to extension capabilities + +## 2.3.1 + +### Patch Changes + +- 9ec9f15: Loosened zod optional any types to be nullable for Python interopability + ## 2.3.0 ### Minor Changes diff --git a/typescript/packages/core/README.md b/typescript/packages/core/README.md index c2c9218..debeb30 100644 --- a/typescript/packages/core/README.md +++ b/typescript/packages/core/README.md @@ -286,8 +286,8 @@ For blockchain-specific implementations: ## Examples -See the [examples directory](https://github.com/coinbase/x402/tree/main/examples/typescript) for complete examples. +See the [examples directory](https://github.com/x402-foundation/x402/tree/main/examples/typescript) for complete examples. ## Contributing -Contributions welcome! See [Contributing Guide](https://github.com/coinbase/x402/blob/main/CONTRIBUTING.md). +Contributions welcome! See [Contributing Guide](https://github.com/x402-foundation/x402/blob/main/CONTRIBUTING.md). diff --git a/typescript/packages/core/package.json b/typescript/packages/core/package.json index 80e044a..9fc6df5 100644 --- a/typescript/packages/core/package.json +++ b/typescript/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@x402/core", - "version": "2.3.0", + "version": "2.9.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/cjs/index.d.ts", @@ -18,8 +18,8 @@ }, "keywords": [], "license": "Apache-2.0", - "author": "Coinbase Inc.", - "repository": "https://github.com/coinbase/x402", + "author": "x402 Foundation", + "repository": "https://github.com/x402-foundation/x402", "description": "x402 Payment Protocol", "devDependencies": { "@eslint/js": "^9.24.0", diff --git a/typescript/packages/core/src/client/x402Client.ts b/typescript/packages/core/src/client/x402Client.ts index 7b39f5a..544aaa1 100644 --- a/typescript/packages/core/src/client/x402Client.ts +++ b/typescript/packages/core/src/client/x402Client.ts @@ -37,6 +37,35 @@ export type OnPaymentCreationFailureHook = ( export type SelectPaymentRequirements = (x402Version: number, paymentRequirements: PaymentRequirements[]) => PaymentRequirements; +/** + * Extension that can enrich payment payloads on the client side. + * + * Client extensions are invoked after the scheme creates the base payment payload + * but before it is returned. This allows mechanism-specific logic (e.g., EVM EIP-2612 + * permit signing) to enrich the payload's extensions data. + */ +export interface ClientExtension { + /** + * Unique key identifying this extension (e.g., "eip2612GasSponsoring"). + * Must match the extension key used in PaymentRequired.extensions. + */ + key: string; + + /** + * Called after payload creation when the extension key is present in + * paymentRequired.extensions. Allows the extension to enrich the payload + * with extension-specific data (e.g., signing an EIP-2612 permit). + * + * @param paymentPayload - The payment payload to enrich + * @param paymentRequired - The original PaymentRequired response + * @returns The enriched payment payload + */ + enrichPaymentPayload?: ( + paymentPayload: PaymentPayload, + paymentRequired: PaymentRequired, + ) => Promise; +} + /** * A policy function that filters or transforms payment requirements. * Policies are applied in order before the selector chooses the final option. @@ -101,6 +130,7 @@ export class x402Client { private readonly paymentRequirementsSelector: SelectPaymentRequirements; private readonly registeredClientSchemes: Map>> = new Map(); private readonly policies: PaymentPolicy[] = []; + private readonly registeredExtensions: Map = new Map(); private beforePaymentCreationHooks: BeforePaymentCreationHook[] = []; private afterPaymentCreationHooks: AfterPaymentCreationHook[] = []; @@ -185,6 +215,22 @@ export class x402Client { return this; } + /** + * Registers a client extension that can enrich payment payloads. + * + * Extensions are invoked after the scheme creates the base payload and the + * payload is wrapped with extensions/resource/accepted data. If the extension's + * key is present in `paymentRequired.extensions`, the extension's + * `enrichPaymentPayload` hook is called to modify the payload. + * + * @param extension - The client extension to register + * @returns The x402Client instance for chaining + */ + registerExtension(extension: ClientExtension): x402Client { + this.registeredExtensions.set(extension.key, extension); + return this; + } + /** * Register a hook to execute before payment payload creation. * Can abort creation by returning { abort: true, reason: string } @@ -258,20 +304,35 @@ export class x402Client { throw new Error(`No client registered for scheme: ${requirements.scheme} and network: ${requirements.network}`); } - const partialPayload = await schemeNetworkClient.createPaymentPayload(paymentRequired.x402Version, requirements); + const partialPayload = await schemeNetworkClient.createPaymentPayload( + paymentRequired.x402Version, + requirements, + { extensions: paymentRequired.extensions }, + ); let paymentPayload: PaymentPayload; if (partialPayload.x402Version == 1) { paymentPayload = partialPayload as PaymentPayload; } else { + // Merge server-declared extensions with any scheme-provided extensions. + // Scheme extensions overlay on top (e.g., EIP-2612 info enriches server declaration). + const mergedExtensions = this.mergeExtensions( + paymentRequired.extensions, + partialPayload.extensions, + ); + paymentPayload = { - ...partialPayload, - extensions: paymentRequired.extensions, + x402Version: partialPayload.x402Version, + payload: partialPayload.payload, + extensions: mergedExtensions, resource: paymentRequired.resource, accepted: requirements, }; } + // Enrich payload via registered client extensions (for non-scheme extensions) + paymentPayload = await this.enrichPaymentPayloadWithExtensions(paymentPayload, paymentRequired); + // Execute afterPaymentCreation hooks const createdContext: PaymentCreatedContext = { ...context, @@ -303,6 +364,67 @@ export class x402Client { + /** + * Merges server-declared extensions with scheme-provided extensions. + * Scheme extensions overlay on top of server extensions at each key, + * preserving server-provided schema while overlaying scheme-provided info. + * + * @param serverExtensions - Extensions declared by the server in the 402 response + * @param schemeExtensions - Extensions provided by the scheme client (e.g. EIP-2612) + * @returns The merged extensions object, or undefined if both inputs are undefined + */ + private mergeExtensions( + serverExtensions?: Record, + schemeExtensions?: Record, + ): Record | undefined { + if (!schemeExtensions) return serverExtensions; + if (!serverExtensions) return schemeExtensions; + + const merged = { ...serverExtensions }; + for (const [key, schemeValue] of Object.entries(schemeExtensions)) { + const serverValue = merged[key]; + if ( + serverValue && + typeof serverValue === "object" && + schemeValue && + typeof schemeValue === "object" + ) { + // Deep merge: scheme info overlays server info, schema preserved + merged[key] = { ...serverValue as Record, ...schemeValue as Record }; + } else { + merged[key] = schemeValue; + } + } + return merged; + } + + /** + * Enriches a payment payload by calling registered extension hooks. + * For each extension key present in the PaymentRequired response, + * invokes the corresponding extension's enrichPaymentPayload callback. + * + * @param paymentPayload - The payment payload to enrich with extension data + * @param paymentRequired - The PaymentRequired response containing extension declarations + * @returns The enriched payment payload with extension data applied + */ + private async enrichPaymentPayloadWithExtensions( + paymentPayload: PaymentPayload, + paymentRequired: PaymentRequired, + ): Promise { + if (!paymentRequired.extensions || this.registeredExtensions.size === 0) { + return paymentPayload; + } + + let enriched = paymentPayload; + for (const [key, extension] of this.registeredExtensions) { + if (key in paymentRequired.extensions && extension.enrichPaymentPayload) { + enriched = await extension.enrichPaymentPayload(enriched, paymentRequired); + } + } + + return enriched; + } + /** * Selects appropriate payment requirements based on registered clients and policies. * diff --git a/typescript/packages/core/src/facilitator/x402Facilitator.ts b/typescript/packages/core/src/facilitator/x402Facilitator.ts index db747d3..daa9836 100644 --- a/typescript/packages/core/src/facilitator/x402Facilitator.ts +++ b/typescript/packages/core/src/facilitator/x402Facilitator.ts @@ -1,6 +1,7 @@ import { x402Version } from ".."; import { SettleResponse, VerifyResponse } from "../types/facilitator"; -import { SchemeNetworkFacilitator } from "../types/mechanisms"; +import { FacilitatorExtension } from "../types/extensions"; +import { SchemeNetworkFacilitator, FacilitatorContext } from "../types/mechanisms"; import { PaymentPayload, PaymentRequirements } from "../types/payments"; import { Network } from "../types"; import { type SchemeData } from "../utils"; @@ -68,7 +69,7 @@ export class x402Facilitator { number, SchemeData[] // Array to support multiple facilitators per version > = new Map(); - private readonly extensions: string[] = []; + private readonly extensions: Map = new Map(); private beforeVerifyHooks: FacilitatorBeforeVerifyHook[] = []; private afterVerifyHooks: FacilitatorAfterVerifyHook[] = []; @@ -109,24 +110,31 @@ export class x402Facilitator { /** * Registers a protocol extension. * - * @param extension - The extension name to register (e.g., "bazaar", "sign_in_with_x") + * @param extension - The extension object to register * @returns The x402Facilitator instance for chaining */ - registerExtension(extension: string): x402Facilitator { - // Check if already registered - if (!this.extensions.includes(extension)) { - this.extensions.push(extension); - } + registerExtension(extension: FacilitatorExtension): x402Facilitator { + this.extensions.set(extension.key, extension); return this; } /** - * Gets the list of registered extensions. + * Gets the list of registered extension keys. * - * @returns Array of extension names + * @returns Array of extension key strings */ getExtensions(): string[] { - return [...this.extensions]; + return Array.from(this.extensions.keys()); + } + + /** + * Gets a registered extension by key. + * + * @param key - The extension key to look up + * @returns The extension object, or undefined if not registered + */ + getExtension(key: string): T | undefined { + return this.extensions.get(key) as T | undefined; } /** @@ -260,7 +268,7 @@ export class x402Facilitator { return { kinds, - extensions: this.extensions, + extensions: this.getExtensions(), signers, }; } @@ -324,9 +332,11 @@ export class x402Facilitator { ); } + const facilitatorContext = this.buildFacilitatorContext(); const verifyResult = await schemeNetworkFacilitator.verify( paymentPayload, paymentRequirements, + facilitatorContext, ); // Check if verification failed (isValid: false) @@ -440,9 +450,11 @@ export class x402Facilitator { ); } + const facilitatorContext = this.buildFacilitatorContext(); const settleResult = await schemeNetworkFacilitator.settle( paymentPayload, paymentRequirements, + facilitatorContext, ); // Execute afterSettle hooks @@ -474,6 +486,23 @@ export class x402Facilitator { } } + /** + * Builds a FacilitatorContext from the registered extensions map. + * Passed to mechanism verify/settle so they can access extension capabilities. + * + * @returns A FacilitatorContext backed by this facilitator's registered extensions + */ + private buildFacilitatorContext(): FacilitatorContext { + const extensionsMap = this.extensions; + return { + getExtension( + key: string, + ): T | undefined { + return extensionsMap.get(key) as T | undefined; + }, + }; + } + /** * Internal method to register a scheme facilitator. * diff --git a/typescript/packages/core/src/http/httpFacilitatorClient.ts b/typescript/packages/core/src/http/httpFacilitatorClient.ts index 29ce532..37bd2a0 100644 --- a/typescript/packages/core/src/http/httpFacilitatorClient.ts +++ b/typescript/packages/core/src/http/httpFacilitatorClient.ts @@ -5,7 +5,9 @@ import { SupportedResponse, VerifyError, SettleError, + FacilitatorResponseError, } from "../types/facilitator"; +import { z } from "../schemas"; const DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator"; @@ -60,6 +62,121 @@ const GET_SUPPORTED_RETRIES = 3; /** Base delay in ms for exponential backoff on retries */ const GET_SUPPORTED_RETRY_DELAY_MS = 1000; +const verifyResponseSchema: z.ZodType = z.object({ + isValid: z.boolean(), + invalidReason: z + .string() + .nullish() + .transform(v => v ?? undefined), + invalidMessage: z + .string() + .nullish() + .transform(v => v ?? undefined), + payer: z + .string() + .nullish() + .transform(v => v ?? undefined), + extensions: z + .record(z.string(), z.unknown()) + .nullish() + .transform(v => v ?? undefined), +}); + +const settleResponseSchema: z.ZodType = z.object({ + success: z.boolean(), + errorReason: z + .string() + .nullish() + .transform(v => v ?? undefined), + errorMessage: z + .string() + .nullish() + .transform(v => v ?? undefined), + payer: z + .string() + .nullish() + .transform(v => v ?? undefined), + transaction: z.string(), + network: z.custom(value => typeof value === "string"), + extensions: z + .record(z.string(), z.unknown()) + .nullish() + .transform(v => v ?? undefined), +}); + +const supportedKindSchema: z.ZodType = + z.object({ + x402Version: z.number(), + scheme: z.string(), + network: z.custom( + value => typeof value === "string", + ), + extra: z + .record(z.string(), z.unknown()) + .nullish() + .transform(v => v ?? undefined), + }); + +const supportedResponseSchema: z.ZodType = z.object({ + kinds: z.array(supportedKindSchema), + extensions: z.array(z.string()).default([]), + signers: z.record(z.string(), z.array(z.string())).default({}), +}); + +/** + * Produces a compact excerpt of a facilitator response body for error messages. + * + * @param text - The raw response body text + * @param limit - The maximum number of characters to include + * @returns A normalized excerpt suitable for logs and thrown errors + */ +function responseExcerpt(text: string, limit: number = 200): string { + const compact = text.trim().replace(/\s+/g, " "); + if (!compact) { + return ""; + } + + if (compact.length <= limit) { + return compact; + } + + return `${compact.slice(0, limit - 3)}...`; +} + +/** + * Parses and validates a successful facilitator response body. + * + * @param response - The HTTP response returned by the facilitator + * @param schema - The schema used to validate the response payload + * @param operation - The facilitator operation name for error reporting + * @returns The validated facilitator payload + */ +async function parseSuccessResponse( + response: Response, + schema: z.ZodType, + operation: string, +): Promise { + const text = await response.text(); + + let data: unknown; + try { + data = JSON.parse(text); + } catch { + throw new FacilitatorResponseError( + `Facilitator ${operation} returned invalid JSON: ${responseExcerpt(text)}`, + ); + } + + const parsed = schema.safeParse(data); + if (!parsed.success) { + throw new FacilitatorResponseError( + `Facilitator ${operation} returned invalid data: ${responseExcerpt(text)}`, + ); + } + + return parsed.data; +} + /** * HTTP-based client for interacting with x402 facilitator services * Handles HTTP communication with facilitator endpoints @@ -74,7 +191,9 @@ export class HTTPFacilitatorClient implements FacilitatorClient { * @param config - Configuration options for the facilitator client */ constructor(config?: FacilitatorConfig) { - this.url = config?.url || DEFAULT_FACILITATOR_URL; + // Normalize URL: strip trailing slashes to prevent redirect loops (e.g. 308) + // when constructing endpoint paths like `${url}/supported` + this.url = (config?.url || DEFAULT_FACILITATOR_URL).replace(/\/+$/, ""); this._createAuthHeaders = config?.createAuthHeaders; } @@ -101,6 +220,7 @@ export class HTTPFacilitatorClient implements FacilitatorClient { const response = await fetch(`${this.url}/verify`, { method: "POST", headers, + redirect: "follow", body: JSON.stringify({ x402Version: paymentPayload.x402Version, paymentPayload: this.toJsonSafe(paymentPayload), @@ -108,17 +228,25 @@ export class HTTPFacilitatorClient implements FacilitatorClient { }), }); - const data = await response.json(); + if (!response.ok) { + const text = await response.text(); + let data: unknown; + try { + data = JSON.parse(text); + } catch { + throw new Error(`Facilitator verify failed (${response.status}): ${responseExcerpt(text)}`); + } - if (typeof data === "object" && data !== null && "isValid" in data) { - const verifyResponse = data as VerifyResponse; - if (!response.ok) { - throw new VerifyError(response.status, verifyResponse); + if (typeof data === "object" && data !== null && "isValid" in data) { + throw new VerifyError(response.status, data as VerifyResponse); } - return verifyResponse; + + throw new Error( + `Facilitator verify failed (${response.status}): ${responseExcerpt(JSON.stringify(data))}`, + ); } - throw new Error(`Facilitator verify failed (${response.status}): ${JSON.stringify(data)}`); + return parseSuccessResponse(response, verifyResponseSchema, "verify"); } /** @@ -144,6 +272,7 @@ export class HTTPFacilitatorClient implements FacilitatorClient { const response = await fetch(`${this.url}/settle`, { method: "POST", headers, + redirect: "follow", body: JSON.stringify({ x402Version: paymentPayload.x402Version, paymentPayload: this.toJsonSafe(paymentPayload), @@ -151,17 +280,25 @@ export class HTTPFacilitatorClient implements FacilitatorClient { }), }); - const data = await response.json(); + if (!response.ok) { + const text = await response.text(); + let data: unknown; + try { + data = JSON.parse(text); + } catch { + throw new Error(`Facilitator settle failed (${response.status}): ${responseExcerpt(text)}`); + } - if (typeof data === "object" && data !== null && "success" in data) { - const settleResponse = data as SettleResponse; - if (!response.ok) { - throw new SettleError(response.status, settleResponse); + if (typeof data === "object" && data !== null && "success" in data) { + throw new SettleError(response.status, data as SettleResponse); } - return settleResponse; + + throw new Error( + `Facilitator settle failed (${response.status}): ${responseExcerpt(JSON.stringify(data))}`, + ); } - throw new Error(`Facilitator settle failed (${response.status}): ${JSON.stringify(data)}`); + return parseSuccessResponse(response, settleResponseSchema, "settle"); } /** @@ -185,14 +322,17 @@ export class HTTPFacilitatorClient implements FacilitatorClient { const response = await fetch(`${this.url}/supported`, { method: "GET", headers, + redirect: "follow", }); if (response.ok) { - return (await response.json()) as SupportedResponse; + return parseSuccessResponse(response, supportedResponseSchema, "supported"); } const errorText = await response.text().catch(() => response.statusText); - lastError = new Error(`Facilitator getSupported failed (${response.status}): ${errorText}`); + lastError = new Error( + `Facilitator getSupported failed (${response.status}): ${responseExcerpt(errorText)}`, + ); // Retry on 429 rate limit errors with exponential backoff if (response.status === 429 && attempt < GET_SUPPORTED_RETRIES - 1) { diff --git a/typescript/packages/core/src/http/index.ts b/typescript/packages/core/src/http/index.ts index ac20bc0..1b15671 100644 --- a/typescript/packages/core/src/http/index.ts +++ b/typescript/packages/core/src/http/index.ts @@ -82,6 +82,7 @@ export { x402HTTPResourceServer, HTTPAdapter, HTTPRequestContext, + HTTPTransportContext, HTTPResponseInstructions, HTTPProcessResult, PaywallConfig, @@ -93,7 +94,8 @@ export { DynamicPayTo, DynamicPrice, UnpaidResponseBody, - UnpaidResponseResult, + HTTPResponseBody, + SettlementFailedResponseBody, ProcessSettleResultResponse, ProcessSettleSuccessResponse, ProcessSettleFailureResponse, @@ -106,4 +108,5 @@ export { FacilitatorClient, FacilitatorConfig, } from "./httpFacilitatorClient"; +export { FacilitatorResponseError, getFacilitatorResponseError } from "../types"; export { x402HTTPClient, PaymentRequiredContext, PaymentRequiredHook } from "./x402HTTPClient"; diff --git a/typescript/packages/core/src/http/x402HTTPResourceServer.ts b/typescript/packages/core/src/http/x402HTTPResourceServer.ts index 7524162..0cd074e 100644 --- a/typescript/packages/core/src/http/x402HTTPResourceServer.ts +++ b/typescript/packages/core/src/http/x402HTTPResourceServer.ts @@ -1,4 +1,4 @@ -import { x402ResourceServer } from "../server"; +import { x402ResourceServer, SettlementOverrides } from "../server"; import { decodePaymentSignatureHeader, encodePaymentRequiredHeader, @@ -9,12 +9,15 @@ import { PaymentRequired, SettleResponse, SettleError, + FacilitatorResponseError, Price, Network, PaymentRequirements, } from "../types"; import { x402Version } from ".."; +export const SETTLEMENT_OVERRIDES_HEADER = "Settlement-Overrides"; + /** * Framework-agnostic HTTP adapter interface * Implementations provide framework-specific HTTP operations @@ -80,9 +83,9 @@ export type DynamicPayTo = (context: HTTPRequestContext) => string | Promise Price | Promise; /** - * Result of the unpaid response callback containing content type and body. + * Result of response body callbacks containing content type and body. */ -export interface UnpaidResponseResult { +export interface HTTPResponseBody { /** * The content type for the response (e.g., 'application/json', 'text/plain'). */ @@ -100,7 +103,16 @@ export interface UnpaidResponseResult { */ export type UnpaidResponseBody = ( context: HTTPRequestContext, -) => UnpaidResponseResult | Promise; +) => HTTPResponseBody | Promise; + +/** + * Dynamic function to generate a custom response for settlement failures. + * Receives the HTTP request context and settle failure result, returns the content type and body. + */ +export type SettlementFailedResponseBody = ( + context: HTTPRequestContext, + settleResult: Omit, +) => HTTPResponseBody | Promise; /** * A single payment option for a route @@ -146,6 +158,16 @@ export interface RouteConfig { */ unpaidResponseBody?: UnpaidResponseBody; + /** + * Optional callback to generate a custom response for settlement failures. + * If not provided, defaults to { contentType: 'application/json', body: {} }. + * + * @param context - The HTTP request context + * @param settleResult - The settlement failure result + * @returns An object containing both contentType and body for the 402 response + */ + settlementFailedResponseBody?: SettlementFailedResponseBody; + // Extensions extensions?: Record; } @@ -176,6 +198,7 @@ export interface CompiledRoute { verb: string; regex: RegExp; config: RouteConfig; + pattern: string; } /** @@ -186,6 +209,19 @@ export interface HTTPRequestContext { path: string; method: string; paymentHeader?: string; + routePattern?: string; +} + +/** + * HTTP transport context contains both request context and optional response data. + */ +export interface HTTPTransportContext { + /** The HTTP request context */ + request: HTTPRequestContext; + /** The response body buffer */ + responseBody?: Buffer; + /** Response headers set by the route handler (used for settlement overrides) */ + responseHeaders?: Record; } /** @@ -224,6 +260,8 @@ export type ProcessSettleFailureResponse = SettleResponse & { success: false; errorReason: string; errorMessage?: string; + headers: Record; + response: HTTPResponseInstructions; }; export type ProcessSettleResultResponse = @@ -299,6 +337,7 @@ export class x402HTTPResourceServer { verb: parsed.verb, regex: parsed.regex, config, + pattern: parsed.path, }); } } @@ -383,17 +422,21 @@ export class x402HTTPResourceServer { context: HTTPRequestContext, paywallConfig?: PaywallConfig, ): Promise { - const { adapter, path, method } = context; + const method = context.method || context.adapter.getMethod(); + context = { ...context, method }; + const { adapter, path } = context; // Find matching route - const routeConfig = this.getRouteConfig(path, method); - if (!routeConfig) { + const routeMatch = this.getRouteConfig(path, method); + if (!routeMatch) { return { type: "no-payment-required" }; // No payment required for this route } + const { config: routeConfig, pattern: routePattern } = routeMatch; + const enrichedContext: HTTPRequestContext = { ...context, routePattern }; // Execute request hooks before any payment processing for (const hook of this.protectedRequestHooks) { - const result = await hook(context, routeConfig); + const result = await hook(enrichedContext, routeConfig); if (result && "grantAccess" in result) { return { type: "no-payment-required" }; } @@ -417,7 +460,7 @@ export class x402HTTPResourceServer { // Create resource info, using config override if provided const resourceInfo = { - url: routeConfig.resource || context.adapter.getUrl(), + url: routeConfig.resource || enrichedContext.adapter.getUrl(), description: routeConfig.description || "", mimeType: routeConfig.mimeType || "", }; @@ -426,27 +469,29 @@ export class x402HTTPResourceServer { // (this method handles resolving dynamic functions internally) let requirements = await this.ResourceServer.buildPaymentRequirementsFromOptions( paymentOptions, - context, + enrichedContext, ); let extensions = routeConfig.extensions; if (extensions) { - extensions = this.ResourceServer.enrichExtensions(extensions, context); + extensions = this.ResourceServer.enrichExtensions(extensions, enrichedContext); } // createPaymentRequiredResponse already handles extension enrichment in the core layer + const transportContext: HTTPTransportContext = { request: enrichedContext }; const paymentRequired = await this.ResourceServer.createPaymentRequiredResponse( requirements, resourceInfo, !paymentPayload ? "Payment required" : undefined, extensions, + transportContext, ); // If no payment provided if (!paymentPayload) { // Resolve custom unpaid response body if provided const unpaidBody = routeConfig.unpaidResponseBody - ? await routeConfig.unpaidResponseBody(context) + ? await routeConfig.unpaidResponseBody(enrichedContext) : undefined; return { @@ -474,6 +519,7 @@ export class x402HTTPResourceServer { resourceInfo, "No matching payment requirements", routeConfig.extensions, + transportContext, ); return { type: "payment-error", @@ -492,6 +538,7 @@ export class x402HTTPResourceServer { resourceInfo, verifyResult.invalidReason, routeConfig.extensions, + transportContext, ); return { type: "payment-error", @@ -507,11 +554,15 @@ export class x402HTTPResourceServer { declaredExtensions: routeConfig.extensions, }; } catch (error) { + if (error instanceof FacilitatorResponseError) { + throw error; + } const errorResponse = await this.ResourceServer.createPaymentRequiredResponse( requirements, resourceInfo, error instanceof Error ? error.message : "Payment verification failed", routeConfig.extensions, + transportContext, ); return { type: "payment-error", @@ -526,28 +577,62 @@ export class x402HTTPResourceServer { * @param paymentPayload - The verified payment payload * @param requirements - The matching payment requirements * @param declaredExtensions - Optional declared extensions (for per-key enrichment) + * @param transportContext - Optional HTTP transport context + * @param settlementOverrides - Optional settlement overrides (e.g., partial settlement amount) * @returns ProcessSettleResultResponse - SettleResponse with headers if success or errorReason if failure */ async processSettlement( paymentPayload: PaymentPayload, requirements: PaymentRequirements, declaredExtensions?: Record, + transportContext?: HTTPTransportContext, + settlementOverrides?: SettlementOverrides, ): Promise { + if (transportContext?.request && !transportContext.request.method) { + transportContext = { + ...transportContext, + request: { + ...transportContext.request, + method: transportContext.request.adapter.getMethod(), + }, + }; + } try { + // Resolve overrides: explicit param takes precedence, fall back to response header + let resolvedOverrides = settlementOverrides; + if (!resolvedOverrides && transportContext?.responseHeaders) { + const overridesKey = SETTLEMENT_OVERRIDES_HEADER.toLowerCase(); + const rawValue = Object.entries(transportContext.responseHeaders).find( + ([key]) => key.toLowerCase() === overridesKey, + )?.[1]; + if (rawValue) { + try { + resolvedOverrides = JSON.parse(rawValue); + } catch { + // Ignore malformed header + } + } + } + const settleResponse = await this.ResourceServer.settlePayment( paymentPayload, requirements, declaredExtensions, + transportContext, + resolvedOverrides, ); if (!settleResponse.success) { - return { + const failure = { ...settleResponse, - success: false, + success: false as const, errorReason: settleResponse.errorReason || "Settlement failed", errorMessage: settleResponse.errorMessage || settleResponse.errorReason || "Settlement failed", + headers: this.createSettlementHeaders(settleResponse), }; + const response = await this.buildSettlementFailureResponse(failure, transportContext); + return { ...failure, response }; } return { @@ -557,23 +642,44 @@ export class x402HTTPResourceServer { requirements, }; } catch (error) { + if (error instanceof FacilitatorResponseError) { + throw error; + } if (error instanceof SettleError) { - return { + const errorReason = error.errorReason || error.message; + const settleResponse: SettleResponse = { success: false, - errorReason: error.errorReason || error.message, - errorMessage: error.errorMessage || error.errorReason || error.message, + errorReason, + errorMessage: error.errorMessage || errorReason, payer: error.payer, network: error.network, transaction: error.transaction, }; + const failure = { + ...settleResponse, + success: false as const, + errorReason, + headers: this.createSettlementHeaders(settleResponse), + }; + const response = await this.buildSettlementFailureResponse(failure, transportContext); + return { ...failure, response }; } - return { + const errorReason = error instanceof Error ? error.message : "Settlement failed"; + const settleResponse: SettleResponse = { success: false, - errorReason: error instanceof Error ? error.message : "Settlement failed", - errorMessage: error instanceof Error ? error.message : "Settlement failed", + errorReason, + errorMessage: errorReason, network: requirements.network as Network, transaction: "", }; + const failure = { + ...settleResponse, + success: false as const, + errorReason, + headers: this.createSettlementHeaders(settleResponse), + }; + const response = await this.buildSettlementFailureResponse(failure, transportContext); + return { ...failure, response }; } } @@ -584,8 +690,43 @@ export class x402HTTPResourceServer { * @returns True if the route requires payment, false otherwise */ requiresPayment(context: HTTPRequestContext): boolean { - const routeConfig = this.getRouteConfig(context.path, context.method); - return routeConfig !== undefined; + const method = context.method || context.adapter.getMethod(); + return this.getRouteConfig(context.path, method) !== undefined; + } + + /** + * Build HTTPResponseInstructions for settlement failure. + * Uses settlementFailedResponseBody hook if configured, otherwise defaults to empty body. + * + * @param failure - Settlement failure result with headers + * @param transportContext - Optional HTTP transport context for the request + * @returns HTTP response instructions for the 402 settlement failure response + */ + private async buildSettlementFailureResponse( + failure: Omit, + transportContext?: HTTPTransportContext, + ): Promise { + const settlementHeaders = failure.headers; + const routeConfig = transportContext + ? this.getRouteConfig(transportContext.request.path, transportContext.request.method) + : undefined; + + const customBody = routeConfig?.config.settlementFailedResponseBody + ? await routeConfig.config.settlementFailedResponseBody(transportContext!.request, failure) + : undefined; + + const contentType = customBody ? customBody.contentType : "application/json"; + const body = customBody ? customBody.body : {}; + + return { + status: 402, + headers: { + "Content-Type": contentType, + ...settlementHeaders, + }, + body, + isHtml: contentType.includes("text/html"), + }; } /** @@ -615,6 +756,21 @@ export class x402HTTPResourceServer { : [["*", this.routesConfig as RouteConfig] as [string, RouteConfig]]; for (const [pattern, config] of normalizedRoutes) { + // Warn if wildcard routes are used with discovery extensions + const pathPart = pattern.includes(" ") ? pattern.split(/\s+/)[1] : pattern; + if ( + pathPart && + pathPart.includes("*") && + config.extensions && + "bazaar" in config.extensions + ) { + console.warn( + `[x402] Route "${pattern}": Wildcard (*) patterns with bazaar discovery extensions ` + + `will auto-generate parameter names (var1, var2, ...). ` + + `Consider using named parameters instead (e.g. /weather/:city) for better discovery metadata.`, + ); + } + const paymentOptions = this.normalizePaymentOptions(config); for (const option of paymentOptions) { @@ -658,9 +814,12 @@ export class x402HTTPResourceServer { * * @param path - Request path * @param method - HTTP method - * @returns Route configuration or undefined if no match + * @returns Route configuration and pattern, or undefined if no match */ - private getRouteConfig(path: string, method: string): RouteConfig | undefined { + private getRouteConfig( + path: string, + method: string, + ): { config: RouteConfig; pattern: string } | undefined { const normalizedPath = this.normalizePath(path); const upperMethod = method.toUpperCase(); @@ -669,7 +828,8 @@ export class x402HTTPResourceServer { route.regex.test(normalizedPath) && (route.verb === "*" || route.verb === upperMethod), ); - return matchingRoute?.config; + if (!matchingRoute) return undefined; + return { config: matchingRoute.config, pattern: matchingRoute.pattern }; } /** @@ -720,7 +880,7 @@ export class x402HTTPResourceServer { isWebBrowser: boolean, paywallConfig?: PaywallConfig, customHtml?: string, - unpaidResponse?: UnpaidResponseResult, + unpaidResponse?: HTTPResponseBody, ): HTTPResponseInstructions { // Use 412 Precondition Failed for permit2_allowance_required error // This signals client needs to approve Permit2 before retrying @@ -782,24 +942,26 @@ export class x402HTTPResourceServer { /** * Parse route pattern into verb and regex * - * @param pattern - Route pattern like "GET /api/*" or "/api/[id]" + * @param pattern - Route pattern like "GET /api/*", "/api/[id]", or "/api/:id" * @returns Parsed pattern with verb and regex */ - private parseRoutePattern(pattern: string): { verb: string; regex: RegExp } { + private parseRoutePattern(pattern: string): { verb: string; regex: RegExp; path: string } { const [verb, path] = pattern.includes(" ") ? pattern.split(/\s+/) : ["*", pattern]; const regex = new RegExp( `^${ path + .replace(/\\/g, "\\\\") // Escape backslashes first .replace(/[$()+.?^{|}]/g, "\\$&") // Escape regex special chars .replace(/\*/g, ".*?") // Wildcards - .replace(/\[([^\]]+)\]/g, "[^/]+") // Parameters + .replace(/\[([^\]]+)\]/g, "[^/]+") // Parameters (Next.js style [param]) + .replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "[^/]+") // Parameters (Express style :param) .replace(/\//g, "\\/") // Escape slashes }$`, "i", ); - return { verb: verb.toUpperCase(), regex }; + return { verb: verb.toUpperCase(), regex, path }; } /** diff --git a/typescript/packages/core/src/schemas/index.ts b/typescript/packages/core/src/schemas/index.ts index fde7a4b..b66dbe4 100644 --- a/typescript/packages/core/src/schemas/index.ts +++ b/typescript/packages/core/src/schemas/index.ts @@ -22,7 +22,7 @@ export type Any = z.infer; * Optional any record schema - an optional object with unknown keys and values. * Used for optional extension fields like `extra` and `extensions`. */ -export const OptionalAny = z.record(z.unknown()).optional(); +export const OptionalAny = z.record(z.unknown()).optional().nullable(); export type OptionalAny = z.infer; // ============================================================================ diff --git a/typescript/packages/core/src/server/index.ts b/typescript/packages/core/src/server/index.ts index ada9a83..086e432 100644 --- a/typescript/packages/core/src/server/index.ts +++ b/typescript/packages/core/src/server/index.ts @@ -1,12 +1,34 @@ export { x402ResourceServer } from "./x402ResourceServer"; -export type { ResourceConfig, ResourceInfo, SettleResultContext } from "./x402ResourceServer"; +export type { + ResourceConfig, + PaymentRequiredContext, + VerifyContext, + VerifyResultContext, + VerifyFailureContext, + SettleContext, + SettleResultContext, + SettleFailureContext, + SettlementOverrides, + BeforeVerifyHook, + AfterVerifyHook, + OnVerifyFailureHook, + BeforeSettleHook, + AfterSettleHook, + OnSettleFailureHook, +} from "./x402ResourceServer"; export { HTTPFacilitatorClient } from "../http/httpFacilitatorClient"; export type { FacilitatorClient, FacilitatorConfig } from "../http/httpFacilitatorClient"; +export { FacilitatorResponseError, getFacilitatorResponseError } from "../types"; -export { x402HTTPResourceServer, RouteConfigurationError } from "../http/x402HTTPResourceServer"; +export { + x402HTTPResourceServer, + RouteConfigurationError, + SETTLEMENT_OVERRIDES_HEADER, +} from "../http/x402HTTPResourceServer"; export type { HTTPRequestContext, + HTTPTransportContext, HTTPResponseInstructions, HTTPProcessResult, PaywallConfig, @@ -16,9 +38,11 @@ export type { HTTPAdapter, RoutesConfig, UnpaidResponseBody, - UnpaidResponseResult, + HTTPResponseBody, + SettlementFailedResponseBody, ProcessSettleResultResponse, ProcessSettleSuccessResponse, ProcessSettleFailureResponse, RouteValidationError, + ProtectedRequestHook, } from "../http/x402HTTPResourceServer"; diff --git a/typescript/packages/core/src/server/x402ResourceServer.ts b/typescript/packages/core/src/server/x402ResourceServer.ts index 1358c9c..14f8aab 100644 --- a/typescript/packages/core/src/server/x402ResourceServer.ts +++ b/typescript/packages/core/src/server/x402ResourceServer.ts @@ -5,7 +5,12 @@ import { SupportedResponse, SupportedKind, } from "../types/facilitator"; -import { PaymentPayload, PaymentRequirements, PaymentRequired } from "../types/payments"; +import { + PaymentPayload, + PaymentRequirements, + PaymentRequired, + ResourceInfo, +} from "../types/payments"; import { SchemeNetworkServer } from "../types/mechanisms"; import { Price, Network, ResourceServerExtension, VerifyError } from "../types"; import { deepEqual, findByNetworkAndScheme } from "../utils"; @@ -25,15 +30,6 @@ export interface ResourceConfig { extra?: Record; // Scheme-specific additional data } -/** - * Resource information for PaymentRequired response - */ -export interface ResourceInfo { - url: string; - description: string; - mimeType: string; -} - /** * Lifecycle Hook Context Interfaces */ @@ -43,6 +39,7 @@ export interface PaymentRequiredContext { resourceInfo: ResourceInfo; error?: string; paymentRequiredResponse: PaymentRequired; + transportContext?: unknown; } export interface VerifyContext { @@ -65,6 +62,7 @@ export interface SettleContext { export interface SettleResultContext extends SettleContext { result: SettleResponse; + transportContext?: unknown; } export interface SettleFailureContext extends SettleContext { @@ -95,6 +93,74 @@ export type OnSettleFailureHook = ( context: SettleFailureContext, ) => Promise; +/** + * Optional overrides for settlement parameters. + * Used to support partial settlement (e.g., upto scheme billing by actual usage). + * + * Note: Overriding the amount to a value different from the agreed-upon + * `PaymentRequirements.amount` is only valid in schemes that explicitly support + * partial settlement, such as the `upto` scheme. Using this with standard + * x402 schemes (e.g., `exact`) will likely cause settlement verification to fail. + */ +export interface SettlementOverrides { + /** + * Amount to settle. Supports three formats: + * + * - **Raw atomic units** — e.g., `"1000"` settles exactly 1000 atomic units. + * - **Percent** — e.g., `"50%"` settles 50% of `PaymentRequirements.amount`. + * Supports up to two decimal places (e.g., `"33.33%"`). The result is floored + * to the nearest atomic unit. + * - **Dollar price** — e.g., `"$0.05"` converts a USD-denominated price to + * atomic units. Decimals are determined from the registered scheme's + * `getAssetDecimals` method, falling back to 6 (standard for USDC stablecoins). + * The result is rounded to the nearest atomic unit. + * + * The resolved amount must be <= the authorized maximum in `PaymentRequirements`. + * + * Note: Setting this to an amount other than `PaymentRequirements.amount` is + * only valid in schemes that support partial settlement, such as `upto`. + */ + amount?: string; +} + +/** + * Resolves a settlement override amount string to a final atomic-unit string. + * + * Supports three input formats (see {@link SettlementOverrides.amount}): + * - Raw atomic units: `"1000"` + * - Percent of `PaymentRequirements.amount`: `"50%"` + * - Dollar price: `"$0.05"` (converted using the provided decimals) + * + * @param rawAmount - The override amount string (e.g., `"1000"`, `"50%"`, `"$0.05"`) + * @param requirements - The payment requirements containing the base amount + * @param decimals - Decimal precision to use for dollar-format conversion (default 6) + * @returns The resolved amount as an atomic-unit string + */ +export function resolveSettlementOverrideAmount( + rawAmount: string, + requirements: PaymentRequirements, + decimals: number = 6, +): string { + // Percent format: "50%" or "33.33%" + const percentMatch = rawAmount.match(/^(\d+(?:\.\d{0,2})?)%$/); + if (percentMatch) { + const [intPart, decPart = ""] = percentMatch[1].split("."); + const scaledPercent = BigInt(intPart) * 100n + BigInt(decPart.padEnd(2, "0").slice(0, 2)); + const base = BigInt(requirements.amount); + return ((base * scaledPercent) / 10000n).toString(); + } + + // Dollar price format: "$0.05" + const dollarMatch = rawAmount.match(/^\$(\d+(?:\.\d+)?)$/); + if (dollarMatch) { + const dollars = parseFloat(dollarMatch[1]); + return Math.round(dollars * 10 ** decimals).toString(); + } + + // Raw atomic units (existing behavior) + return rawAmount; +} + /** * Core x402 protocol server for resource protection * Transport-agnostic implementation of the x402 payment protocol @@ -301,6 +367,7 @@ export class x402ResourceServer { // Clear existing mappings this.supportedResponsesMap.clear(); this.facilitatorClientsMap.clear(); + let lastError: Error | undefined; // Fetch supported kinds from all facilitator clients // Process in order to give precedence to earlier facilitators @@ -343,10 +410,24 @@ export class x402ResourceServer { } } } catch (error) { + lastError = error as Error; // Log error but continue with other facilitators console.warn(`Failed to fetch supported kinds from facilitator: ${error}`); } } + + if (this.supportedResponsesMap.size === 0) { + throw lastError + ? new Error( + "Failed to initialize: no supported payment kinds loaded from any facilitator.", + { + cause: lastError, + }, + ) + : new Error( + "Failed to initialize: no supported payment kinds loaded from any facilitator.", + ); + } } /** @@ -488,6 +569,7 @@ export class x402ResourceServer { price: Price | ((context: TContext) => Price | Promise); network: Network; maxTimeoutSeconds?: number; + extra?: Record; }>, context: TContext, ): Promise { @@ -506,6 +588,7 @@ export class x402ResourceServer { price: resolvedPrice, network: option.network, maxTimeoutSeconds: option.maxTimeoutSeconds, + extra: option.extra, }; // Use existing buildPaymentRequirements for each option @@ -523,6 +606,7 @@ export class x402ResourceServer { * @param resourceInfo - Resource information * @param error - Error message * @param extensions - Optional declared extensions (for per-key enrichment) + * @param transportContext - Optional transport-specific context (e.g., HTTP request, MCP tool context) * @returns Payment required response object */ async createPaymentRequiredResponse( @@ -530,6 +614,7 @@ export class x402ResourceServer { resourceInfo: ResourceInfo, error?: string, extensions?: Record, + transportContext?: unknown, ): Promise { // V2 response with resource at top level let response: PaymentRequired = { @@ -555,6 +640,7 @@ export class x402ResourceServer { resourceInfo, error, paymentRequiredResponse: response, + transportContext, }; const extensionData = await extension.enrichPaymentRequiredResponse( declaration, @@ -686,16 +772,36 @@ export class x402ResourceServer { * @param paymentPayload - The payment payload to settle * @param requirements - The payment requirements * @param declaredExtensions - Optional declared extensions (for per-key enrichment) + * @param transportContext - Optional transport-specific context (e.g., HTTP request/response, MCP tool context) + * @param settlementOverrides - Optional overrides for settlement parameters (e.g., partial settlement amount) * @returns Settlement response */ async settlePayment( paymentPayload: PaymentPayload, requirements: PaymentRequirements, declaredExtensions?: Record, + transportContext?: unknown, + settlementOverrides?: SettlementOverrides, ): Promise { + // Apply settlement overrides (e.g., partial settlement for upto scheme) + let effectiveRequirements = requirements; + if (settlementOverrides?.amount !== undefined) { + const scheme = findByNetworkAndScheme( + this.registeredServerSchemes, + requirements.scheme, + requirements.network as Network, + ); + const decimals = + scheme?.getAssetDecimals?.(requirements.asset ?? "", requirements.network as Network) ?? 6; + effectiveRequirements = { + ...requirements, + amount: resolveSettlementOverrideAmount(settlementOverrides.amount, requirements, decimals), + }; + } + const context: SettleContext = { paymentPayload, - requirements, + requirements: effectiveRequirements, }; // Execute beforeSettle hooks @@ -712,6 +818,9 @@ export class x402ResourceServer { }); } } catch (error) { + if (error instanceof SettleError) { + throw error; + } throw new SettleError(400, { success: false, errorReason: "before_settle_hook_error", @@ -726,8 +835,8 @@ export class x402ResourceServer { // Find the facilitator that supports this payment type const facilitatorClient = this.getFacilitatorClient( paymentPayload.x402Version, - requirements.network, - requirements.scheme, + effectiveRequirements.network, + effectiveRequirements.scheme, ); let settleResult: SettleResponse; @@ -738,7 +847,7 @@ export class x402ResourceServer { for (const client of this.facilitatorClients) { try { - settleResult = await client.settle(paymentPayload, requirements); + settleResult = await client.settle(paymentPayload, effectiveRequirements); break; } catch (error) { lastError = error as Error; @@ -749,19 +858,20 @@ export class x402ResourceServer { throw ( lastError || new Error( - `No facilitator supports ${requirements.scheme} on ${requirements.network} for v${paymentPayload.x402Version}`, + `No facilitator supports ${effectiveRequirements.scheme} on ${effectiveRequirements.network} for v${paymentPayload.x402Version}`, ) ); } } else { // Use the specific facilitator that supports this payment - settleResult = await facilitatorClient.settle(paymentPayload, requirements); + settleResult = await facilitatorClient.settle(paymentPayload, effectiveRequirements); } // Execute afterSettle hooks const resultContext: SettleResultContext = { ...context, result: settleResult, + transportContext, }; for (const hook of this.afterSettleHooks) { diff --git a/typescript/packages/core/src/types/extensions.ts b/typescript/packages/core/src/types/extensions.ts index 57da33a..c1e5b09 100644 --- a/typescript/packages/core/src/types/extensions.ts +++ b/typescript/packages/core/src/types/extensions.ts @@ -3,6 +3,22 @@ import type { PaymentRequiredContext, SettleResultContext } from "../server/x402 // Re-export context types from x402ResourceServer for convenience export type { PaymentRequiredContext, SettleResultContext }; +/** + * Base interface for facilitator extensions. + * Extensions registered with x402Facilitator are stored by key and made + * available to mechanism implementations via FacilitatorContext. + * + * Specific extensions extend this with additional capabilities: + * + * @example + * interface Erc20GasSponsoringExtension extends FacilitatorExtension { + * batchSigner: SmartWalletBatchSigner; + * } + */ +export interface FacilitatorExtension { + key: string; +} + export interface ResourceServerExtension { key: string; /** @@ -18,7 +34,7 @@ export interface ResourceServerExtension { * Return extension data to add to extensions[key], or undefined to skip. * * @param declaration - Extension declaration from route config - * @param context - PaymentRequired context containing response and requirements + * @param context - PaymentRequired context containing response, requirements, and optional transportContext * @returns Extension data to add to response.extensions[key] */ enrichPaymentRequiredResponse?: ( @@ -30,7 +46,7 @@ export interface ResourceServerExtension { * Return extension data to add to response.extensions[key], or undefined to skip. * * @param declaration - Extension declaration from route config - * @param context - Settlement result context containing payment payload, requirements, and result + * @param context - Settlement result context containing payment payload, requirements, result and optional transportContext * @returns Extension data to add to response.extensions[key] */ enrichSettlementResponse?: ( diff --git a/typescript/packages/core/src/types/facilitator.ts b/typescript/packages/core/src/types/facilitator.ts index b37c9e8..e330967 100644 --- a/typescript/packages/core/src/types/facilitator.ts +++ b/typescript/packages/core/src/types/facilitator.ts @@ -2,6 +2,7 @@ import { PaymentPayload, PaymentRequirements } from "./payments"; import { Network } from "./"; export type VerifyRequest = { + x402Version: number; paymentPayload: PaymentPayload; paymentRequirements: PaymentRequirements; }; @@ -15,6 +16,7 @@ export type VerifyResponse = { }; export type SettleRequest = { + x402Version: number; paymentPayload: PaymentPayload; paymentRequirements: PaymentRequirements; }; @@ -26,6 +28,8 @@ export type SettleResponse = { payer?: string; transaction: string; network: Network; + /** Actual amount settled in atomic token units. Present for schemes like `upto` where settlement amount may differ from the authorized maximum. */ + amount?: string; extensions?: Record; }; @@ -99,3 +103,37 @@ export class SettleError extends Error { this.network = response.network; } } + +/** + * Error thrown when a facilitator returns malformed success payload data. + */ +export class FacilitatorResponseError extends Error { + /** + * Creates a FacilitatorResponseError for malformed facilitator responses. + * + * @param message - The boundary error message + */ + constructor(message: string) { + super(message); + this.name = "FacilitatorResponseError"; + } +} + +/** + * Walks an error cause chain to find the first facilitator response error. + * + * @param error - The thrown value to inspect + * @returns The nested facilitator response error, if present + */ +export function getFacilitatorResponseError(error: unknown): FacilitatorResponseError | null { + let current = error; + + while (current instanceof Error) { + if (current instanceof FacilitatorResponseError) { + return current; + } + current = current.cause; + } + + return null; +} diff --git a/typescript/packages/core/src/types/index.ts b/typescript/packages/core/src/types/index.ts index d27c45b..6ef8e7f 100644 --- a/typescript/packages/core/src/types/index.ts +++ b/typescript/packages/core/src/types/index.ts @@ -5,7 +5,12 @@ export type { SettleResponse, SupportedResponse, } from "./facilitator"; -export { VerifyError, SettleError } from "./facilitator"; +export { + VerifyError, + SettleError, + FacilitatorResponseError, + getFacilitatorResponseError, +} from "./facilitator"; export type { PaymentRequirements, PaymentPayload, @@ -18,9 +23,12 @@ export type { SchemeNetworkServer, MoneyParser, PaymentPayloadResult, + PaymentPayloadContext, + FacilitatorContext, } from "./mechanisms"; export type { PaymentRequirementsV1, PaymentRequiredV1, PaymentPayloadV1 } from "./v1"; export type { + FacilitatorExtension, ResourceServerExtension, PaymentRequiredContext, SettleResultContext, diff --git a/typescript/packages/core/src/types/mechanisms.ts b/typescript/packages/core/src/types/mechanisms.ts index c1838d0..fb3fa46 100644 --- a/typescript/packages/core/src/types/mechanisms.ts +++ b/typescript/packages/core/src/types/mechanisms.ts @@ -2,6 +2,7 @@ import { SettleResponse, VerifyResponse } from "./facilitator"; import { PaymentRequirements } from "./payments"; import { PaymentPayload } from "./payments"; import { Price, Network, AssetAmount } from "."; +import { FacilitatorExtension } from "./extensions"; /** * Money parser function that converts a numeric amount to an AssetAmount @@ -17,9 +18,22 @@ export type MoneyParser = (amount: number, network: Network) => Promise; +export type PaymentPayloadResult = Pick & { + extensions?: Record; +}; + +/** + * Context passed to scheme's createPaymentPayload for extensions awareness. + * Contains the server-declared extensions from PaymentRequired so the scheme + * can check which extensions are advertised and respond accordingly. + */ +export interface PaymentPayloadContext { + extensions?: Record; +} export interface SchemeNetworkClient { readonly scheme: string; @@ -27,9 +41,19 @@ export interface SchemeNetworkClient { createPaymentPayload( x402Version: number, paymentRequirements: PaymentRequirements, + context?: PaymentPayloadContext, ): Promise; } +/** + * Context passed to SchemeNetworkFacilitator.verify/settle, providing + * access to registered facilitator extensions. Mechanism implementations + * use this to retrieve extension-provided capabilities (e.g., a batch signer). + */ +export interface FacilitatorContext { + getExtension(key: string): T | undefined; +} + export interface SchemeNetworkFacilitator { readonly scheme: string; @@ -92,8 +116,16 @@ export interface SchemeNetworkFacilitator { */ getSigners(network: string): string[]; - verify(payload: PaymentPayload, requirements: PaymentRequirements): Promise; - settle(payload: PaymentPayload, requirements: PaymentRequirements): Promise; + verify( + payload: PaymentPayload, + requirements: PaymentRequirements, + context?: FacilitatorContext, + ): Promise; + settle( + payload: PaymentPayload, + requirements: PaymentRequirements, + context?: FacilitatorContext, + ): Promise; } export interface SchemeNetworkServer { @@ -116,6 +148,17 @@ export interface SchemeNetworkServer { */ parsePrice(price: Price, network: Network): Promise; + /** + * Optional: Return the decimal precision of the asset for a given network. + * Used by `resolveSettlementOverrideAmount` to convert dollar-format overrides to atomic units. + * Defaults to 6 when not implemented. + * + * @param asset - The asset address or symbol + * @param network - The network identifier + * @returns Number of decimal places for the asset + */ + getAssetDecimals?(asset: string, network: Network): number; + /** * Build payment requirements for this scheme/network combination * diff --git a/typescript/packages/core/src/types/payments.ts b/typescript/packages/core/src/types/payments.ts index c4b9e17..129d136 100644 --- a/typescript/packages/core/src/types/payments.ts +++ b/typescript/packages/core/src/types/payments.ts @@ -2,8 +2,8 @@ import { Network } from "./"; export interface ResourceInfo { url: string; - description: string; - mimeType: string; + description?: string; + mimeType?: string; } export type PaymentRequirements = { @@ -26,7 +26,7 @@ export type PaymentRequired = { export type PaymentPayload = { x402Version: number; - resource: ResourceInfo; + resource?: ResourceInfo; accepted: PaymentRequirements; payload: Record; extensions?: Record; diff --git a/typescript/packages/core/src/types/v1/index.ts b/typescript/packages/core/src/types/v1/index.ts index da67499..8de7e42 100644 --- a/typescript/packages/core/src/types/v1/index.ts +++ b/typescript/packages/core/src/types/v1/index.ts @@ -30,11 +30,13 @@ export type PaymentPayloadV1 = { // Facilitator Requests/Responses export type VerifyRequestV1 = { + x402Version: number; paymentPayload: PaymentPayloadV1; paymentRequirements: PaymentRequirementsV1; }; export type SettleRequestV1 = { + x402Version: number; paymentPayload: PaymentPayloadV1; paymentRequirements: PaymentRequirementsV1; }; diff --git a/typescript/packages/core/test/integrations/extensions.test.ts b/typescript/packages/core/test/integrations/extensions.test.ts new file mode 100644 index 0000000..fbf3aa4 --- /dev/null +++ b/typescript/packages/core/test/integrations/extensions.test.ts @@ -0,0 +1,115 @@ +/** + * Integration tests for client extension hooks in the x402 payment flow. + * Tests the extension enrichment mechanism using the Cash mock scheme. + */ + +import { beforeEach, describe, expect, it } from "vitest"; +import { x402Client, ClientExtension } from "../../src/client"; +import { x402Facilitator } from "../../src/facilitator"; +import { x402ResourceServer } from "../../src/server"; +import { + buildCashPaymentRequirements, + CashFacilitatorClient, + CashSchemeNetworkClient, + CashSchemeNetworkFacilitator, + CashSchemeNetworkServer, +} from "../mocks"; +import { PaymentPayload, PaymentRequired } from "../../src/types"; + +describe("Extension Integration Tests", () => { + describe("Client Extension Enrichment in Full Payment Flow", () => { + let client: x402Client; + let server: x402ResourceServer; + + beforeEach(async () => { + client = new x402Client().register("x402:cash", new CashSchemeNetworkClient("John")); + + const facilitator = new x402Facilitator().register( + "x402:cash", + new CashSchemeNetworkFacilitator(), + ); + + const facilitatorClient = new CashFacilitatorClient(facilitator); + server = new x402ResourceServer(facilitatorClient); + server.register("x402:cash", new CashSchemeNetworkServer()); + await server.initialize(); + }); + + it("should enrich extensions in the payment payload when extension key matches", async () => { + // Register a test extension + let enrichWasCalled = false; + const testExtension: ClientExtension = { + key: "testGasSponsoring", + enrichPaymentPayload: async ( + payload: PaymentPayload, + _paymentRequired: PaymentRequired, + ) => { + enrichWasCalled = true; + const extensions = { ...(payload.extensions ?? {}) }; + extensions["testGasSponsoring"] = { + info: { + from: "0x1234", + signature: "0xabcd", + enriched: true, + }, + }; + return { ...payload, extensions }; + }, + }; + client.registerExtension(testExtension); + + // Server builds PaymentRequired with the extension declared + const accepts = [buildCashPaymentRequirements("Company Co.", "USD", "1")]; + const resource = { + url: "https://company.co", + description: "Test", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + // Add extension to the PaymentRequired (normally done by server route config) + paymentRequired.extensions = { + testGasSponsoring: { + info: { description: "Test gas sponsoring", version: "1" }, + schema: {}, + }, + }; + + // Client creates payload - extension should be enriched + const paymentPayload = await client.createPaymentPayload(paymentRequired); + + expect(enrichWasCalled).toBe(true); + expect(paymentPayload.extensions).toBeDefined(); + const extData = (paymentPayload.extensions as Record) + ?.testGasSponsoring as Record; + expect(extData).toBeDefined(); + expect((extData.info as Record)?.enriched).toBe(true); + expect((extData.info as Record)?.from).toBe("0x1234"); + }); + + it("should NOT enrich extensions when extension key is not in paymentRequired", async () => { + let enrichWasCalled = false; + const testExtension: ClientExtension = { + key: "missingExtension", + enrichPaymentPayload: async (payload: PaymentPayload) => { + enrichWasCalled = true; + return payload; + }, + }; + client.registerExtension(testExtension); + + const accepts = [buildCashPaymentRequirements("Company Co.", "USD", "1")]; + const resource = { + url: "https://company.co", + description: "Test", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + // No extensions on the paymentRequired + await client.createPaymentPayload(paymentRequired); + + expect(enrichWasCalled).toBe(false); + }); + }); +}); diff --git a/typescript/packages/core/test/integrations/upto.test.ts b/typescript/packages/core/test/integrations/upto.test.ts new file mode 100644 index 0000000..0800d50 --- /dev/null +++ b/typescript/packages/core/test/integrations/upto.test.ts @@ -0,0 +1,328 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { x402Client, x402HTTPClient } from "../../src/client"; +import { x402Facilitator } from "../../src/facilitator"; +import { + HTTPAdapter, + HTTPResponseInstructions, + x402HTTPResourceServer, + x402ResourceServer, +} from "../../src/server"; +import { + buildCashPaymentRequirements, + CashFacilitatorClient, + CashSchemeNetworkClient, + CashSchemeNetworkFacilitator, + CashSchemeNetworkServer, +} from "../mocks"; +import { Network, PaymentPayload, PaymentRequirements } from "../../src/types"; +import { SettlementOverrides } from "../../src/server/x402ResourceServer"; +import { SETTLEMENT_OVERRIDES_HEADER } from "../../src/http/x402HTTPResourceServer"; + +describe("Upto Integration Tests", () => { + describe("x402Client / x402ResourceServer — Upto-style partial settlement", () => { + let client: x402Client; + let server: x402ResourceServer; + + beforeEach(async () => { + client = new x402Client().register("x402:cash", new CashSchemeNetworkClient("Alice")); + + const facilitator = new x402Facilitator().register( + "x402:cash", + new CashSchemeNetworkFacilitator(), + ); + + const facilitatorClient = new CashFacilitatorClient(facilitator); + server = new x402ResourceServer(facilitatorClient); + server.register("x402:cash", new CashSchemeNetworkServer()); + await server.initialize(); + }); + + it("should settle with full amount when no overrides provided", async () => { + const accepts = [buildCashPaymentRequirements("Merchant", "USD", "1000")]; + const resource = { + url: "https://api.example.com/generate", + description: "AI generation", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const accepted = server.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + const verifyResponse = await server.verifyPayment(paymentPayload, accepted!); + expect(verifyResponse.isValid).toBe(true); + + // No overrides — settles for the full 1000 + const settleResponse = await server.settlePayment(paymentPayload, accepted!); + expect(settleResponse.success).toBe(true); + expect(settleResponse.transaction).toContain("1000"); + }); + + it("should settle with reduced amount when overrides specify partial amount", async () => { + const accepts = [buildCashPaymentRequirements("Merchant", "USD", "1000")]; + const resource = { + url: "https://api.example.com/generate", + description: "AI generation", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const accepted = server.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + const verifyResponse = await server.verifyPayment(paymentPayload, accepted!); + expect(verifyResponse.isValid).toBe(true); + + // Partial settlement — only charge 400 of authorized 1000 + const overrides: SettlementOverrides = { amount: "400" }; + const settleResponse = await server.settlePayment( + paymentPayload, + accepted!, + undefined, + undefined, + overrides, + ); + expect(settleResponse.success).toBe(true); + // The mock cash facilitator includes the amount in the transaction string + expect(settleResponse.transaction).toContain("400"); + expect(settleResponse.transaction).not.toContain("1000"); + }); + + it("should settle with zero amount when overrides specify zero", async () => { + const accepts = [buildCashPaymentRequirements("Merchant", "USD", "1000")]; + const resource = { + url: "https://api.example.com/generate", + description: "AI generation", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const accepted = server.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + await server.verifyPayment(paymentPayload, accepted!); + + // Zero settlement — free usage this time + const overrides: SettlementOverrides = { amount: "0" }; + const settleResponse = await server.settlePayment( + paymentPayload, + accepted!, + undefined, + undefined, + overrides, + ); + expect(settleResponse.success).toBe(true); + expect(settleResponse.transaction).toContain("0 USD"); + }); + + it("should not modify original requirements when overrides are applied", async () => { + const accepts = [buildCashPaymentRequirements("Merchant", "USD", "1000")]; + const resource = { + url: "https://api.example.com/generate", + description: "AI generation", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const accepted = server.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + const originalAmount = accepted!.amount; + + await server.settlePayment(paymentPayload, accepted!, undefined, undefined, { + amount: "250", + }); + + // Original requirements object should not be mutated + expect(accepted!.amount).toBe(originalAmount); + }); + }); + + describe("x402HTTPResourceServer — Upto processSettlement with overrides", () => { + let client: x402HTTPClient; + let httpServer: x402HTTPResourceServer; + + const routes = { + "/api/generate": { + accepts: { + scheme: "cash", + payTo: "merchant@example.com", + price: "$10.00", + network: "x402:cash" as Network, + }, + description: "AI generation with upto billing", + mimeType: "application/json", + }, + }; + + function createMockAdapter(): HTTPAdapter { + return { + getHeader: () => undefined, + getMethod: () => "GET", + getPath: () => "/api/generate", + getUrl: () => "https://example.com/api/generate", + getAcceptHeader: () => "application/json", + getUserAgent: () => "TestClient/1.0", + }; + } + + beforeEach(async () => { + const facilitator = new x402Facilitator().register( + "x402:cash", + new CashSchemeNetworkFacilitator(), + ); + + const facilitatorClient = new CashFacilitatorClient(facilitator); + + const paymentClient = new x402Client().register( + "x402:cash", + new CashSchemeNetworkClient("Alice"), + ); + client = new x402HTTPClient(paymentClient) as x402HTTPClient; + + const ResourceServer = new x402ResourceServer(facilitatorClient); + ResourceServer.register("x402:cash", new CashSchemeNetworkServer()); + await ResourceServer.initialize(); + + httpServer = new x402HTTPResourceServer(ResourceServer, routes); + }); + + it("should settle with overrides passed explicitly to processSettlement", async () => { + // Get PaymentRequired + const context = { adapter: createMockAdapter(), path: "/api/generate", method: "GET" }; + const httpResult = await httpServer.processHTTPRequest(context); + expect(httpResult.type).toBe("payment-error"); + + const initial402 = ( + httpResult as { type: "payment-error"; response: HTTPResponseInstructions } + ).response; + + // Client creates payment + const paymentRequired = client.getPaymentRequiredResponse( + name => initial402.headers[name], + initial402.body, + ); + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const requestHeaders = await client.encodePaymentSignatureHeader(paymentPayload); + + // Submit payment + context.adapter.getHeader = (name: string) => { + if (name === "PAYMENT-SIGNATURE") return requestHeaders["PAYMENT-SIGNATURE"]; + return undefined; + }; + const verified = await httpServer.processHTTPRequest(context); + expect(verified.type).toBe("payment-verified"); + + const { paymentPayload: verifiedPayload, paymentRequirements: verifiedRequirements } = + verified as { + type: "payment-verified"; + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + }; + + // Settle with partial override + const result = await httpServer.processSettlement( + verifiedPayload, + verifiedRequirements, + undefined, + undefined, + { amount: "3" }, + ); + expect(result.success).toBe(true); + if (result.success) { + expect(result.headers["PAYMENT-RESPONSE"]).toBeDefined(); + } + }); + + it("should extract overrides from transport context responseHeaders", async () => { + // Get PaymentRequired + const context = { adapter: createMockAdapter(), path: "/api/generate", method: "GET" }; + const httpResult = await httpServer.processHTTPRequest(context); + const initial402 = ( + httpResult as { type: "payment-error"; response: HTTPResponseInstructions } + ).response; + + // Client creates payment + const paymentRequired = client.getPaymentRequiredResponse( + name => initial402.headers[name], + initial402.body, + ); + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const requestHeaders = await client.encodePaymentSignatureHeader(paymentPayload); + + context.adapter.getHeader = (name: string) => { + if (name === "PAYMENT-SIGNATURE") return requestHeaders["PAYMENT-SIGNATURE"]; + return undefined; + }; + const verified = await httpServer.processHTTPRequest(context); + + const { paymentPayload: verifiedPayload, paymentRequirements: verifiedRequirements } = + verified as { + type: "payment-verified"; + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + }; + + // Pass overrides via transport context responseHeaders (simulating middleware extraction) + const result = await httpServer.processSettlement( + verifiedPayload, + verifiedRequirements, + undefined, + { + request: context, + responseHeaders: { + [SETTLEMENT_OVERRIDES_HEADER]: JSON.stringify({ amount: "5" }), + }, + }, + ); + expect(result.success).toBe(true); + }); + + it("explicit overrides should take precedence over header overrides", async () => { + const context = { adapter: createMockAdapter(), path: "/api/generate", method: "GET" }; + const httpResult = await httpServer.processHTTPRequest(context); + const initial402 = ( + httpResult as { type: "payment-error"; response: HTTPResponseInstructions } + ).response; + + const paymentRequired = client.getPaymentRequiredResponse( + name => initial402.headers[name], + initial402.body, + ); + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const requestHeaders = await client.encodePaymentSignatureHeader(paymentPayload); + + context.adapter.getHeader = (name: string) => { + if (name === "PAYMENT-SIGNATURE") return requestHeaders["PAYMENT-SIGNATURE"]; + return undefined; + }; + const verified = await httpServer.processHTTPRequest(context); + + const { paymentPayload: verifiedPayload, paymentRequirements: verifiedRequirements } = + verified as { + type: "payment-verified"; + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + }; + + // Both explicit and header overrides — explicit wins + const result = await httpServer.processSettlement( + verifiedPayload, + verifiedRequirements, + undefined, + { + request: context, + responseHeaders: { + [SETTLEMENT_OVERRIDES_HEADER]: JSON.stringify({ amount: "999" }), + }, + }, + { amount: "2" }, // explicit takes precedence + ); + expect(result.success).toBe(true); + }); + }); +}); diff --git a/typescript/packages/core/test/mocks/generic/MockFacilitatorClient.ts b/typescript/packages/core/test/mocks/generic/MockFacilitatorClient.ts index 1dda1e3..4cef009 100644 --- a/typescript/packages/core/test/mocks/generic/MockFacilitatorClient.ts +++ b/typescript/packages/core/test/mocks/generic/MockFacilitatorClient.ts @@ -63,10 +63,13 @@ export class MockFacilitatorClient implements FacilitatorClient { payloadOrRequest: PaymentPayload | VerifyRequest, requirements?: PaymentRequirements, ): Promise { - const payload = "payload" in payloadOrRequest ? payloadOrRequest.payload : payloadOrRequest; + const payload = + "paymentPayload" in payloadOrRequest ? payloadOrRequest.paymentPayload : payloadOrRequest; const reqs = - requirements || - ("requirements" in payloadOrRequest ? payloadOrRequest.requirements : undefined)!; + requirements ?? + ("paymentRequirements" in payloadOrRequest + ? payloadOrRequest.paymentRequirements + : undefined)!; this.verifyCalls.push({ payload, requirements: reqs }); @@ -93,10 +96,13 @@ export class MockFacilitatorClient implements FacilitatorClient { payloadOrRequest: PaymentPayload | SettleRequest, requirements?: PaymentRequirements, ): Promise { - const payload = "payload" in payloadOrRequest ? payloadOrRequest.payload : payloadOrRequest; + const payload = + "paymentPayload" in payloadOrRequest ? payloadOrRequest.paymentPayload : payloadOrRequest; const reqs = - requirements || - ("requirements" in payloadOrRequest ? payloadOrRequest.requirements : undefined)!; + requirements ?? + ("paymentRequirements" in payloadOrRequest + ? payloadOrRequest.paymentRequirements + : undefined)!; this.settleCalls.push({ payload, requirements: reqs }); diff --git a/typescript/packages/core/test/mocks/generic/MockSchemeServer.ts b/typescript/packages/core/test/mocks/generic/MockSchemeServer.ts index 8ec46cf..803124b 100644 --- a/typescript/packages/core/test/mocks/generic/MockSchemeServer.ts +++ b/typescript/packages/core/test/mocks/generic/MockSchemeServer.ts @@ -9,6 +9,7 @@ export class MockSchemeNetworkServer implements SchemeNetworkServer { public readonly scheme: string; private parsePriceResult: AssetAmount | Error; private enhanceResult: PaymentRequirements | Error | null = null; + private assetDecimalsResult: number | null = null; // Call tracking public parsePriceCalls: Array<{ price: Price; network: Network }> = []; @@ -71,7 +72,19 @@ export class MockSchemeNetworkServer implements SchemeNetworkServer { return this.enhanceResult || paymentRequirements; } + getAssetDecimals(_asset: string, _network: Network): number { + return this.assetDecimalsResult ?? 6; + } + // Helper methods for test configuration + /** + * + * @param result + */ + setAssetDecimalsResult(result: number): void { + this.assetDecimalsResult = result; + } + /** * * @param result diff --git a/typescript/packages/core/test/unit/client/x402Client.test.ts b/typescript/packages/core/test/unit/client/x402Client.test.ts index 40d9c37..b9401ca 100644 --- a/typescript/packages/core/test/unit/client/x402Client.test.ts +++ b/typescript/packages/core/test/unit/client/x402Client.test.ts @@ -592,4 +592,82 @@ describe("x402Client", () => { }); }); }); + + describe("Extension Hooks", () => { + it("should register and invoke extensions that match paymentRequired.extensions", async () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("exact"); + client.register("eip155:84532" as Network, mockClient); + + let enrichCalled = false; + client.registerExtension({ + key: "testExtension", + enrichPaymentPayload: async (payload, _paymentRequired) => { + enrichCalled = true; + return { + ...payload, + extensions: { + ...payload.extensions, + testExtension: { info: { enriched: true } }, + }, + }; + }, + }); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ + scheme: "exact", + network: "eip155:84532" as Network, + }), + ], + extensions: { + testExtension: { info: { description: "test" }, schema: {} }, + }, + }); + + const result = await client.createPaymentPayload(paymentRequired); + + expect(enrichCalled).toBe(true); + expect((result.extensions as Record)?.testExtension).toEqual({ + info: { enriched: true }, + }); + }); + + it("should NOT invoke extension when key is not in paymentRequired.extensions", async () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("exact"); + client.register("eip155:84532" as Network, mockClient); + + let enrichCalled = false; + client.registerExtension({ + key: "missingExtension", + enrichPaymentPayload: async payload => { + enrichCalled = true; + return payload; + }, + }); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ + scheme: "exact", + network: "eip155:84532" as Network, + }), + ], + extensions: {}, + }); + + await client.createPaymentPayload(paymentRequired); + + expect(enrichCalled).toBe(false); + }); + + it("should support chaining registerExtension", () => { + const client = new x402Client(); + const result = client.registerExtension({ key: "ext1" }).registerExtension({ key: "ext2" }); + + expect(result).toBe(client); + }); + }); }); diff --git a/typescript/packages/core/test/unit/facilitator/x402Facilitator.test.ts b/typescript/packages/core/test/unit/facilitator/x402Facilitator.test.ts index 3306927..1f50209 100644 --- a/typescript/packages/core/test/unit/facilitator/x402Facilitator.test.ts +++ b/typescript/packages/core/test/unit/facilitator/x402Facilitator.test.ts @@ -181,7 +181,7 @@ describe("x402Facilitator", () => { it("should register extension", () => { const facilitator = new x402Facilitator(); - const result = facilitator.registerExtension("bazaar"); + const result = facilitator.registerExtension({ key: "bazaar" }); expect(result).toBe(facilitator); expect(facilitator.getExtensions()).toEqual(["bazaar"]); @@ -190,7 +190,7 @@ describe("x402Facilitator", () => { it("should register multiple extensions", () => { const facilitator = new x402Facilitator(); - facilitator.registerExtension("bazaar").registerExtension("sign_in_with_x"); + facilitator.registerExtension({ key: "bazaar" }).registerExtension({ key: "sign_in_with_x" }); expect(facilitator.getExtensions()).toEqual(["bazaar", "sign_in_with_x"]); }); @@ -199,16 +199,16 @@ describe("x402Facilitator", () => { const facilitator = new x402Facilitator(); facilitator - .registerExtension("bazaar") - .registerExtension("bazaar") - .registerExtension("bazaar"); + .registerExtension({ key: "bazaar" }) + .registerExtension({ key: "bazaar" }) + .registerExtension({ key: "bazaar" }); expect(facilitator.getExtensions()).toEqual(["bazaar"]); }); it("should return copy of extensions array", () => { const facilitator = new x402Facilitator(); - facilitator.registerExtension("bazaar"); + facilitator.registerExtension({ key: "bazaar" }); const extensions = facilitator.getExtensions(); extensions.push("modified"); diff --git a/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts b/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts new file mode 100644 index 0000000..ef2214e --- /dev/null +++ b/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts @@ -0,0 +1,267 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { HTTPFacilitatorClient } from "../../../src/http/httpFacilitatorClient"; +import { FacilitatorResponseError, SettleError, VerifyError } from "../../../src/types"; +import { PaymentPayload, PaymentRequirements } from "../../../src/types/payments"; + +const paymentRequirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:8453", + asset: "0x0000000000000000000000000000000000000000", + amount: "1000000", + payTo: "0x1234567890123456789012345678901234567890", + maxTimeoutSeconds: 300, + extra: {}, +}; + +const paymentPayload: PaymentPayload = { + x402Version: 2, + accepted: paymentRequirements, + payload: { signature: "0xmock" }, +}; + +describe("HTTPFacilitatorClient", () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("throws FacilitatorResponseError for invalid verify JSON on success", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("not-json", { status: 200 }))); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + const error = await client + .verify(paymentPayload, paymentRequirements) + .catch(caught => caught as Error); + + expect(error).toBeInstanceOf(FacilitatorResponseError); + expect(error.message).toContain("Facilitator verify returned invalid JSON"); + }); + + it("throws FacilitatorResponseError for invalid settle data on success", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(new Response(JSON.stringify({ success: true }), { status: 200 })), + ); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + const error = await client + .settle(paymentPayload, paymentRequirements) + .catch(caught => caught as Error); + + expect(error).toBeInstanceOf(FacilitatorResponseError); + expect(error.message).toContain("Facilitator settle returned invalid data"); + }); + + it("throws FacilitatorResponseError for invalid supported data on success", async () => { + vi.stubGlobal( + "fetch", + vi + .fn() + .mockResolvedValue( + new Response(JSON.stringify({ kinds: [{ scheme: "exact" }] }), { status: 200 }), + ), + ); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + const error = await client.getSupported().catch(caught => caught as Error); + + expect(error).toBeInstanceOf(FacilitatorResponseError); + expect(error.message).toContain("Facilitator supported returned invalid data"); + }); + + it("preserves VerifyError semantics for valid non-200 verify responses", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + isValid: false, + invalidReason: "invalid_signature", + invalidMessage: "signature mismatch", + }), + { status: 400 }, + ), + ), + ); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + + await expect(client.verify(paymentPayload, paymentRequirements)).rejects.toThrow(VerifyError); + }); + + it("preserves SettleError semantics for valid non-200 settle responses", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + success: false, + errorReason: "insufficient_allowance", + transaction: "", + network: "eip155:8453", + }), + { status: 400 }, + ), + ), + ); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + + await expect(client.settle(paymentPayload, paymentRequirements)).rejects.toThrow(SettleError); + }); + + it("parses verify 200 when optional string fields are JSON null", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + isValid: true, + invalidReason: null, + invalidMessage: null, + payer: null, + }), + { status: 200 }, + ), + ), + ); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + const result = await client.verify(paymentPayload, paymentRequirements); + + expect(result.isValid).toBe(true); + expect(result.invalidReason).toBeUndefined(); + expect(result.invalidMessage).toBeUndefined(); + expect(result.payer).toBeUndefined(); + }); + + it("parses settle 200 when optional string fields are JSON null", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + success: true, + transaction: "0xabc", + network: "eip155:8453", + errorReason: null, + errorMessage: null, + payer: null, + }), + { status: 200 }, + ), + ), + ); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + const result = await client.settle(paymentPayload, paymentRequirements); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("0xabc"); + expect(result.network).toBe("eip155:8453"); + expect(result.errorReason).toBeUndefined(); + expect(result.errorMessage).toBeUndefined(); + expect(result.payer).toBeUndefined(); + }); + + describe("URL normalization", () => { + it("strips trailing slashes from the configured URL", () => { + const client = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator/" }); + expect(client.url).toBe("https://x402.org/facilitator"); + }); + + it("strips multiple trailing slashes", () => { + const client = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator///" }); + expect(client.url).toBe("https://x402.org/facilitator"); + }); + + it("leaves URLs without trailing slash unchanged", () => { + const client = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator" }); + expect(client.url).toBe("https://x402.org/facilitator"); + }); + + it("uses default URL when no config is provided", () => { + const client = new HTTPFacilitatorClient(); + expect(client.url).toBe("https://x402.org/facilitator"); + }); + }); + + describe("redirect handling", () => { + it("passes redirect: follow to fetch on getSupported", async () => { + const mockFetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" }], + }), + { status: 200 }, + ), + ); + vi.stubGlobal("fetch", mockFetch); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + await client.getSupported(); + + expect(mockFetch).toHaveBeenCalledWith( + "https://facilitator.test/supported", + expect.objectContaining({ redirect: "follow" }), + ); + }); + + it("passes redirect: follow to fetch on verify", async () => { + const mockFetch = vi + .fn() + .mockResolvedValue(new Response(JSON.stringify({ isValid: true }), { status: 200 })); + vi.stubGlobal("fetch", mockFetch); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + await client.verify(paymentPayload, paymentRequirements); + + expect(mockFetch).toHaveBeenCalledWith( + "https://facilitator.test/verify", + expect.objectContaining({ redirect: "follow" }), + ); + }); + + it("passes redirect: follow to fetch on settle", async () => { + const mockFetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + success: true, + transaction: "0xabc", + network: "eip155:8453", + }), + { status: 200 }, + ), + ); + vi.stubGlobal("fetch", mockFetch); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + await client.settle(paymentPayload, paymentRequirements); + + expect(mockFetch).toHaveBeenCalledWith( + "https://facilitator.test/settle", + expect.objectContaining({ redirect: "follow" }), + ); + }); + + it("constructs correct endpoint URLs after trailing slash normalization", async () => { + const mockFetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" }], + }), + { status: 200 }, + ), + ); + vi.stubGlobal("fetch", mockFetch); + + const client = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator/" }); + await client.getSupported(); + + expect(mockFetch).toHaveBeenCalledWith( + "https://x402.org/facilitator/supported", + expect.anything(), + ); + }); + }); +}); diff --git a/typescript/packages/core/test/unit/http/x402HTTPResourceServer.errors.test.ts b/typescript/packages/core/test/unit/http/x402HTTPResourceServer.errors.test.ts new file mode 100644 index 0000000..0d7e3c1 --- /dev/null +++ b/typescript/packages/core/test/unit/http/x402HTTPResourceServer.errors.test.ts @@ -0,0 +1,116 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { x402HTTPResourceServer, HTTPAdapter } from "../../../src/http/x402HTTPResourceServer"; +import { x402ResourceServer } from "../../../src/server/x402ResourceServer"; +import { FacilitatorResponseError, Network } from "../../../src/types"; +import { + MockFacilitatorClient, + MockSchemeNetworkServer, + buildPaymentPayload, + buildPaymentRequirements, + buildSupportedResponse, +} from "../../mocks"; +import { encodePaymentSignatureHeader } from "../../../src/http"; + +class MockHTTPAdapter implements HTTPAdapter { + constructor(private readonly headers: Record = {}) {} + + getHeader(name: string): string | undefined { + return this.headers[name.toLowerCase()]; + } + + getMethod(): string { + return "GET"; + } + + getPath(): string { + return "/api/test"; + } + + getUrl(): string { + return "https://example.com/api/test"; + } + + getAcceptHeader(): string { + return "application/json"; + } + + getUserAgent(): string { + return "Vitest"; + } +} + +describe("x402HTTPResourceServer facilitator response errors", () => { + let resourceServer: x402ResourceServer; + let facilitator: MockFacilitatorClient; + let httpServer: x402HTTPResourceServer; + const network = "eip155:8453" as Network; + + beforeEach(async () => { + facilitator = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network }], + }), + ); + + resourceServer = new x402ResourceServer(facilitator); + resourceServer.register(network, new MockSchemeNetworkServer("exact")); + await resourceServer.initialize(); + + httpServer = new x402HTTPResourceServer(resourceServer, { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00", + network, + }, + }, + }); + }); + + it("rethrows FacilitatorResponseError during verification", async () => { + facilitator.setVerifyResponse( + new FacilitatorResponseError("Facilitator verify returned invalid JSON: not-json"), + ); + + const accepted = buildPaymentRequirements({ + scheme: "exact", + network, + payTo: "0xabc", + asset: "USDC", + amount: "1000000", + }); + const payload = buildPaymentPayload({ + x402Version: 2, + accepted, + }); + + await expect( + httpServer.processHTTPRequest({ + adapter: new MockHTTPAdapter({ + "payment-signature": encodePaymentSignatureHeader(payload), + }), + path: "/api/test", + method: "GET", + paymentHeader: encodePaymentSignatureHeader(payload), + }), + ).rejects.toThrow(FacilitatorResponseError); + }); + + it("rethrows FacilitatorResponseError during settlement", async () => { + facilitator.setSettleResponse( + new FacilitatorResponseError('Facilitator settle returned invalid data: {"success":true}'), + ); + + const accepted = buildPaymentRequirements({ + scheme: "exact", + network, + payTo: "0xabc", + asset: "USDC", + amount: "1000000", + }); + await expect( + httpServer.processSettlement(buildPaymentPayload({ x402Version: 2, accepted }), accepted), + ).rejects.toThrow(FacilitatorResponseError); + }); +}); diff --git a/typescript/packages/core/test/unit/http/x402HTTPResourceServer.hooks.test.ts b/typescript/packages/core/test/unit/http/x402HTTPResourceServer.hooks.test.ts index d112a32..3de9e6a 100644 --- a/typescript/packages/core/test/unit/http/x402HTTPResourceServer.hooks.test.ts +++ b/typescript/packages/core/test/unit/http/x402HTTPResourceServer.hooks.test.ts @@ -273,6 +273,110 @@ describe("x402HTTPResourceServer Hooks", () => { expect(result.success).toBe(true); }); + + it("should pass transportContext to enrichSettlementResponse hook", async () => { + let receivedContext: SettleResultContext | undefined; + + const transportAwareExtension: ResourceServerExtension = { + key: "transport-aware", + enrichSettlementResponse: async (_declaration: unknown, context: SettleResultContext) => { + receivedContext = context; + return { transportAware: true }; + }, + }; + + extensionResourceServer.registerExtension(transportAwareExtension); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + extensions: { + "transport-aware": { config: "value" }, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(extensionResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + extensionMockFacilitator.setSettleResponse(buildSettleResponse({ success: true })); + + const transportContext = { + request: { path: "/api/test", method: "POST" }, + responseBody: Buffer.from("test response body"), + }; + + const result = await httpServer.processSettlement( + payload, + requirements, + routes["/api/test"].extensions, + transportContext, + ); + + expect(result.success).toBe(true); + expect(receivedContext).toBeDefined(); + expect(receivedContext?.transportContext).toBeDefined(); + expect(receivedContext?.transportContext).toEqual(transportContext); + }); + + it("should have undefined transportContext when not provided", async () => { + let receivedContext: SettleResultContext | undefined; + + const extension: ResourceServerExtension = { + key: "context-checker", + enrichSettlementResponse: async (_declaration: unknown, context: SettleResultContext) => { + receivedContext = context; + return { checked: true }; + }, + }; + + extensionResourceServer.registerExtension(extension); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + extensions: { + "context-checker": {}, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(extensionResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + extensionMockFacilitator.setSettleResponse(buildSettleResponse({ success: true })); + + // Call without transportContext + const result = await httpServer.processSettlement( + payload, + requirements, + routes["/api/test"].extensions, + ); + + expect(result.success).toBe(true); + expect(receivedContext).toBeDefined(); + expect(receivedContext?.transportContext).toBeUndefined(); + }); }); describe("enrichPaymentRequiredResponse", () => { @@ -331,6 +435,143 @@ describe("x402HTTPResourceServer Hooks", () => { } } }); + + it("should pass transportContext to enrichPaymentRequiredResponse hook", async () => { + let receivedContext: PaymentRequiredContext | undefined; + + const transportAwareExtension: ResourceServerExtension = { + key: "transport-aware-402", + enrichPaymentRequiredResponse: async ( + _declaration: unknown, + context: PaymentRequiredContext, + ) => { + receivedContext = context; + return { transportAware: true }; + }, + }; + + extensionResourceServer.registerExtension(transportAwareExtension); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + extensions: { + "transport-aware-402": { config: "value" }, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(extensionResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/test", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("payment-error"); + expect(receivedContext).toBeDefined(); + expect(receivedContext?.transportContext).toBeDefined(); + // The transport context should contain the request + const transportCtx = receivedContext?.transportContext as { request: HTTPRequestContext }; + expect(transportCtx.request).toBeDefined(); + expect(transportCtx.request.path).toBe("/api/test"); + expect(transportCtx.request.method).toBe("GET"); + }); + + it("should pass transportContext in all PaymentRequired error scenarios", async () => { + let receivedContexts: PaymentRequiredContext[] = []; + + const contextCapturingExtension: ResourceServerExtension = { + key: "context-capturer", + enrichPaymentRequiredResponse: async ( + _declaration: unknown, + context: PaymentRequiredContext, + ) => { + receivedContexts.push(context); + return { captured: true }; + }, + }; + + extensionResourceServer.registerExtension(contextCapturingExtension); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + extensions: { + "context-capturer": {}, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(extensionResourceServer, routes); + + // Test 1: No payment provided (main call) + const adapter1 = new MockHTTPAdapter(); + const context1: HTTPRequestContext = { + adapter: adapter1, + path: "/api/test", + method: "GET", + }; + + await httpServer.processHTTPRequest(context1); + expect(receivedContexts.length).toBe(1); + expect(receivedContexts[0].transportContext).toBeDefined(); + + // Test 2: Invalid payment (verification failure) + // The hook is called twice: once to build the base paymentRequired object, + // and once when verification fails to generate the error response + receivedContexts = []; + extensionMockFacilitator.setVerifyResponse( + buildVerifyResponse({ isValid: false, invalidReason: "invalid_signature" }), + ); + + const paymentRequired = await extensionResourceServer.createPaymentRequiredResponse( + await extensionResourceServer.buildPaymentRequirements({ + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }), + { url: "/api/test", description: "", mimeType: "" }, + ); + + const payload = buildPaymentPayload({ + accepted: paymentRequired.accepts[0], + resource: paymentRequired.resource, + }); + const paymentHeader = encodePaymentSignatureHeader(payload); + + const adapter2 = new MockHTTPAdapter({ + "payment-signature": paymentHeader, + }); + + const context2: HTTPRequestContext = { + adapter: adapter2, + path: "/api/test", + method: "GET", + }; + + await httpServer.processHTTPRequest(context2); + // Called twice: once for base paymentRequired, once for error response + expect(receivedContexts.length).toBe(2); + // Both should have transportContext + expect(receivedContexts[0].transportContext).toBeDefined(); + expect(receivedContexts[1].transportContext).toBeDefined(); + }); }); describe("Integration: All hooks together", () => { diff --git a/typescript/packages/core/test/unit/http/x402HTTPResourceService.test.ts b/typescript/packages/core/test/unit/http/x402HTTPResourceService.test.ts index b694130..2dda52f 100644 --- a/typescript/packages/core/test/unit/http/x402HTTPResourceService.test.ts +++ b/typescript/packages/core/test/unit/http/x402HTTPResourceService.test.ts @@ -372,6 +372,84 @@ describe("x402HTTPResourceServer", () => { expect(result.type).toBe("payment-error"); // Route matched }); + it("should match Express-style :param dynamic routes", async () => { + const routes = { + "/api/chapters/:seriesId/:chapterId": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/chapters/abc123/chapter-7", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("payment-error"); // Route matched + }); + + it("should match Express-style :param with HTTP method prefix", async () => { + const routes = { + "GET /api/users/:id": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/users/42", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("payment-error"); // Route matched + }); + + it("should not match :param against paths with extra segments", async () => { + const routes = { + "/api/users/:id": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/users/42/posts", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("no-payment-required"); + }); + it("should return no-payment-required for unmatched routes", async () => { const routes = { "/api/protected": { @@ -744,8 +822,171 @@ describe("x402HTTPResourceServer", () => { expect(result.success).toBe(false); if (!result.success) { expect(result.errorReason).toBe("Insufficient funds"); + expect(result.headers).toBeDefined(); + expect(result.headers["PAYMENT-RESPONSE"]).toBeDefined(); } }); + + it("should forward explicit settlementOverrides to settlePayment", async () => { + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + const result = await httpServer.processSettlement( + payload, + requirements, + undefined, + undefined, + { amount: "500000" }, + ); + + expect(result.success).toBe(true); + // Verify the facilitator received the overridden amount + expect(mockFacilitator.settleCalls[0].requirements.amount).toBe("500000"); + }); + + it("should extract overrides from responseHeaders in transport context", async () => { + const { SETTLEMENT_OVERRIDES_HEADER } = await import( + "../../../src/http/x402HTTPResourceServer" + ); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + const result = await httpServer.processSettlement(payload, requirements, undefined, { + request: { + adapter: new MockHTTPAdapter(), + path: "/api/test", + method: "GET", + }, + responseHeaders: { + [SETTLEMENT_OVERRIDES_HEADER]: JSON.stringify({ amount: "300000" }), + }, + }); + + expect(result.success).toBe(true); + expect(mockFacilitator.settleCalls[0].requirements.amount).toBe("300000"); + }); + + it("should ignore malformed overrides header gracefully", async () => { + const { SETTLEMENT_OVERRIDES_HEADER } = await import( + "../../../src/http/x402HTTPResourceServer" + ); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + const result = await httpServer.processSettlement(payload, requirements, undefined, { + request: { + adapter: new MockHTTPAdapter(), + path: "/api/test", + method: "GET", + }, + responseHeaders: { + [SETTLEMENT_OVERRIDES_HEADER]: "not-valid-json{{{", + }, + }); + + // Should succeed with original amount (malformed header is ignored) + expect(result.success).toBe(true); + expect(mockFacilitator.settleCalls[0].requirements.amount).toBe("1000000"); + }); + + it("should prefer explicit overrides over header overrides", async () => { + const { SETTLEMENT_OVERRIDES_HEADER } = await import( + "../../../src/http/x402HTTPResourceServer" + ); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + const result = await httpServer.processSettlement( + payload, + requirements, + undefined, + { + request: { + adapter: new MockHTTPAdapter(), + path: "/api/test", + method: "GET", + }, + responseHeaders: { + [SETTLEMENT_OVERRIDES_HEADER]: JSON.stringify({ amount: "999999" }), + }, + }, + { amount: "100000" }, // explicit takes precedence + ); + + expect(result.success).toBe(true); + expect(mockFacilitator.settleCalls[0].requirements.amount).toBe("100000"); + }); }); describe("Browser detection", () => { diff --git a/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts b/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts index 07380ef..1966efa 100644 --- a/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts +++ b/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts @@ -1,5 +1,8 @@ import { describe, it, expect, beforeEach } from "vitest"; -import { x402ResourceServer } from "../../../src/server/x402ResourceServer"; +import { + x402ResourceServer, + resolveSettlementOverrideAmount, +} from "../../../src/server/x402ResourceServer"; import { MockFacilitatorClient, MockSchemeNetworkServer, @@ -190,6 +193,24 @@ describe("x402ResourceServer", () => { expect(workingClient.getSupportedCalls).toBe(1); }); + it("should throw if all facilitators fail", async () => { + const failingClient1 = new MockFacilitatorClient(buildSupportedResponse()); + failingClient1.getSupported = async () => { + throw new Error("Network error"); + }; + + const failingClient2 = new MockFacilitatorClient(buildSupportedResponse()); + failingClient2.getSupported = async () => { + throw new Error("Rate limited"); + }; + + const server = new x402ResourceServer([failingClient1, failingClient2]); + + await expect(server.initialize()).rejects.toThrow( + "Failed to initialize: no supported payment kinds loaded from any facilitator", + ); + }); + it("should clear existing mappings on re-initialization", async () => { const mockClient1 = new MockFacilitatorClient( buildSupportedResponse({ @@ -622,11 +643,41 @@ describe("x402ResourceServer", () => { await expect( async () => await server.settlePayment(buildPaymentPayload(), buildPaymentRequirements()), - ).rejects.toThrow("before_settle_hook_error: Insufficient balance"); + ).rejects.toThrow("Insufficient balance"); expect(mockClient.settleCalls.length).toBe(0); // Facilitator not called }); + it("should preserve abort reason as errorReason in SettleError", async () => { + server.onBeforeSettle(async () => { + return { abort: true, reason: "Insufficient balance", message: "Not enough funds" }; + }); + + try { + await server.settlePayment(buildPaymentPayload(), buildPaymentRequirements()); + expect.unreachable("Should have thrown"); + } catch (error: any) { + expect(error.name).toBe("SettleError"); + expect(error.errorReason).toBe("Insufficient balance"); + expect(error.errorMessage).toBe("Not enough funds"); + } + }); + + it("should wrap unexpected hook errors as before_settle_hook_error", async () => { + server.onBeforeSettle(async () => { + throw new Error("Unexpected failure"); + }); + + try { + await server.settlePayment(buildPaymentPayload(), buildPaymentRequirements()); + expect.unreachable("Should have thrown"); + } catch (error: any) { + expect(error.name).toBe("SettleError"); + expect(error.errorReason).toBe("before_settle_hook_error"); + expect(error.errorMessage).toBe("Unexpected failure"); + } + }); + it("should execute multiple hooks in order", async () => { const executionOrder: number[] = []; @@ -772,6 +823,242 @@ describe("x402ResourceServer", () => { expect(result.success).toBe(true); expect(mockClient.settleCalls.length).toBe(1); }); + + it("should use original amount when no overrides provided", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements); + + expect(mockClient.settleCalls[0].requirements.amount).toBe("1000000"); + }); + + it("should override amount when settlementOverrides.amount is provided", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "500000" }); + + // Facilitator should receive the overridden amount + expect(mockClient.settleCalls[0].requirements.amount).toBe("500000"); + }); + + it("should not mutate original requirements when overrides applied", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "250000" }); + + // Original requirements must not be mutated + expect(requirements.amount).toBe("1000000"); + }); + + it("should use original amount when overrides has undefined amount", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, {}); + + expect(mockClient.settleCalls[0].requirements.amount).toBe("1000000"); + }); + + it("should allow settling for zero amount", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "0" }); + + expect(mockClient.settleCalls[0].requirements.amount).toBe("0"); + }); + + it("should resolve percent override through settlePayment", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "2000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "50%" }); + + expect(mockClient.settleCalls[0].requirements.amount).toBe("1000"); + }); + + it("should resolve dollar override through settlePayment with default decimals", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "$0.001" }); + + expect(mockClient.settleCalls[0].requirements.amount).toBe("1000"); + }); + + it("should resolve dollar override using scheme getAssetDecimals", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + const mockScheme = new MockSchemeNetworkServer("exact"); + mockScheme.setAssetDecimalsResult(8); + server.register("eip155:8453" as Network, mockScheme); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "$0.05" }); + + expect(mockClient.settleCalls[0].requirements.amount).toBe("5000000"); + }); + + it("should not mutate asset when dollar override is used", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + asset: "0xOriginalToken", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { + amount: "$0.10", + }); + + // Only amount changes, asset stays the same + expect(mockClient.settleCalls[0].requirements.amount).toBe("100000"); + expect(mockClient.settleCalls[0].requirements.asset).toBe("0xOriginalToken"); + }); + + it("should pass overridden requirements to beforeSettle hooks", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse(), + buildVerifyResponse({ isValid: true }), + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + + let hookAmount: string | undefined; + server.onBeforeSettle(async context => { + hookAmount = context.requirements.amount; + }); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ amount: "1000000" }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "300000" }); + + expect(hookAmount).toBe("300000"); + }); }); describe("findMatchingRequirements", () => { @@ -999,3 +1286,69 @@ describe("x402ResourceServer", () => { }); }); }); + +describe("resolveSettlementOverrideAmount", () => { + const baseRequirements = buildPaymentRequirements({ amount: "2000" }); + + describe("raw atomic units", () => { + it("passes through a plain numeric string unchanged", () => { + expect(resolveSettlementOverrideAmount("1000", baseRequirements)).toBe("1000"); + }); + + it("passes through '0'", () => { + expect(resolveSettlementOverrideAmount("0", baseRequirements)).toBe("0"); + }); + }); + + describe("percent format", () => { + it("resolves '50%' to half of requirements.amount", () => { + expect(resolveSettlementOverrideAmount("50%", baseRequirements)).toBe("1000"); + }); + + it("resolves '100%' to the full requirements.amount", () => { + expect(resolveSettlementOverrideAmount("100%", baseRequirements)).toBe("2000"); + }); + + it("resolves '0%' to 0", () => { + expect(resolveSettlementOverrideAmount("0%", baseRequirements)).toBe("0"); + }); + + it("resolves '25%' correctly", () => { + expect(resolveSettlementOverrideAmount("25%", baseRequirements)).toBe("500"); + }); + + it("resolves '33.33%' and floors to nearest atomic unit", () => { + const reqs = buildPaymentRequirements({ amount: "3000" }); + // 3000 * 3333 / 10000 = 999.9 → floored to 999 + expect(resolveSettlementOverrideAmount("33.33%", reqs)).toBe("999"); + }); + + it("resolves '10.5%' correctly", () => { + const reqs = buildPaymentRequirements({ amount: "1000" }); + // 1000 * 1050 / 10000 = 105 + expect(resolveSettlementOverrideAmount("10.5%", reqs)).toBe("105"); + }); + }); + + describe("dollar price format", () => { + it("converts '$1.00' using default 6 decimals", () => { + expect(resolveSettlementOverrideAmount("$1.00", baseRequirements)).toBe("1000000"); + }); + + it("converts '$0.05' using default 6 decimals", () => { + expect(resolveSettlementOverrideAmount("$0.05", baseRequirements)).toBe("50000"); + }); + + it("converts '$0.05' using 8 decimals when provided", () => { + expect(resolveSettlementOverrideAmount("$0.05", baseRequirements, 8)).toBe("5000000"); + }); + + it("converts '$0.001' using default 6 decimals", () => { + expect(resolveSettlementOverrideAmount("$0.001", baseRequirements)).toBe("1000"); + }); + + it("converts '$0' to '0'", () => { + expect(resolveSettlementOverrideAmount("$0", baseRequirements)).toBe("0"); + }); + }); +}); diff --git a/typescript/packages/extensions/CHANGELOG.md b/typescript/packages/extensions/CHANGELOG.md index 91c383c..e318ae2 100644 --- a/typescript/packages/extensions/CHANGELOG.md +++ b/typescript/packages/extensions/CHANGELOG.md @@ -1,5 +1,93 @@ # @x402/extensions Changelog +## 2.9.0 + +### Minor Changes + +- 2250cae: Migrated project from coinbase/x402 to x402-foundation/x402 organization + +### Patch Changes + +- Updated dependencies [8cf3fca] +- Updated dependencies [c0e3969] +- Updated dependencies [2250cae] +- Updated dependencies [d352574] + - @x402/core@2.9.0 + +## 2.8.0 + +### Minor Changes + +- 4f2f4f3: Added auth-only route support in createSIWxRequestHook via accepts: [] detection +- 067f297: Added dynamic route support to the Bazaar discovery extension — servers can now declare `[param]` route segments that consolidate to a single catalog entry per route template, with automatic `pathParams` enrichment and `:param`-style `routeTemplate` in discovery output. + +### Patch Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- 8b731cb: Replaced `sendRawApprovalAndSettle` with a generic `sendTransactions` signer method that accepts an array of pre-signed serialized transactions or unsigned call intents. The signer owns execution strategy (sequential, batched, or atomic bundling). Closed fail-open verification paths, aligned Permit2 amount check to exact match, and added `signerForNetwork` to the extensions package. +- f2bbb5c: Added offer-receipt extension to enable signed offers and receipts in x402 payment flows + +### Patch Changes + +- 34d2442: Removed dependencie on node’s crypto module +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- Updated dependencies + - @x402/core@2.6.0 + +## 2.5.0 + +### Minor Changes + +- 7fe268f: Implemented the erc20 approval gas sponsorship extension + +### Patch Changes + +- 1ab1c86: Guard against undefined `resource` in SIWX settle hook to prevent runtime crash when `PaymentPayload.resource` is absent +- Updated dependencies [96a9db0] +- Updated dependencies [d0a2b11] +- Updated dependencies + - @x402/core@2.5.0 + +## 2.4.0 + +### Minor Changes + +- 018181b: Implement EIP-2612 gasless Permit2 approval extension + + - Added `eip2612GasSponsoring` extension types, resource service declaration, and facilitator validation utilities + +- 664285e: Add MCP tool discovery support to the bazaar extension system + +### Patch Changes + +- 3fb55d7: Upgraded facilitator extension registration from string keys to FacilitatorExtension objects. Added FacilitatorContext threaded through SchemeNetworkFacilitator.verify/settle for mechanism access to extension capabilities +- Updated dependencies [57a5488] +- Updated dependencies [018181b] +- Updated dependencies [3fb55d7] + - @x402/core@2.4.0 + +## 2.3.1 + +### Patch Changes + +- f93fc09: Added solanakit support for siwx +- Updated dependencies [9ec9f15] + - @x402/core@2.3.1 + ## 2.3.0 ### Minor Changes diff --git a/typescript/packages/extensions/README.md b/typescript/packages/extensions/README.md index 47357dc..f937322 100644 --- a/typescript/packages/extensions/README.md +++ b/typescript/packages/extensions/README.md @@ -184,6 +184,52 @@ const resources = { }; ``` +#### Example: MCP Tool + +For MCP (Model Context Protocol) tools, use the `toolName` field instead of `bodyType`/`input`. The HTTP method is not relevant -- MCP tools are invoked by name. + +```typescript +import { declareDiscoveryExtension } from "@x402/extensions/bazaar"; + +const resources = { + "POST /mcp": { + accepts: { + scheme: "exact", + price: "$0.01", + network: "eip155:84532", + payTo: "0xYourAddress" + }, + extensions: { + ...declareDiscoveryExtension({ + toolName: "financial_analysis", + description: "Analyze financial data for a given ticker", + inputSchema: { + type: "object", + properties: { + ticker: { type: "string", description: "Stock ticker symbol" }, + analysis_type: { + type: "string", + enum: ["fundamental", "technical", "sentiment"], + }, + }, + required: ["ticker"], + }, + example: { ticker: "AAPL", analysis_type: "fundamental" }, + output: { + example: { + pe_ratio: 28.5, + recommendation: "hold", + confidence: 0.85 + } + }, + }), + }, + }, +}; +``` + +You can optionally specify `transport` to indicate the MCP transport type (`"streamable-http"` or `"sse"`). When omitted, `streamable-http` is assumed per the MCP spec. + #### Using with Next.js Middleware ```typescript @@ -305,9 +351,9 @@ const resourceServer = new x402ResourceServer(facilitatorClient) #### `declareDiscoveryExtension(config)` -Creates a discovery extension object for resource servers. +Creates a discovery extension object for resource servers. Accepts either an HTTP endpoint config or an MCP tool config. -**Parameters:** +**HTTP Parameters:** - `config.input` (optional): Example input values (query params for GET/HEAD/DELETE, body for POST/PUT/PATCH) - `config.inputSchema` (optional): JSON Schema for input validation - `config.bodyType` (required for body methods): For POST/PUT/PATCH, specify `"json"`, `"form-data"`, or `"text"`. This is how TypeScript discriminates between query methods (GET/HEAD/DELETE) and body methods. @@ -317,11 +363,22 @@ Creates a discovery extension object for resource servers. > **Note:** The HTTP method is NOT passed to this function. It is automatically inferred from the route key (e.g., `"GET /weather"`) or enriched by `bazaarResourceServerExtension` at runtime. +**MCP Parameters:** +- `config.toolName` (required): MCP tool name — the presence of this field identifies the config as MCP +- `config.description` (optional): Human-readable tool description +- `config.inputSchema` (required): JSON Schema for tool arguments +- `config.example` (optional): Example tool arguments +- `config.transport` (optional): MCP transport type (`"streamable-http"` or `"sse"`). Defaults to `streamable-http` per the MCP spec when omitted. +- `config.output` (optional): Output specification + - `output.example`: Example output data + - `output.schema`: JSON Schema for output validation + **Returns:** An object with a `bazaar` key containing the discovery extension. -**Example:** +**Examples:** ```typescript -const extension = declareDiscoveryExtension({ +// HTTP endpoint +const httpExtension = declareDiscoveryExtension({ input: { query: "search term" }, inputSchema: { properties: { query: { type: "string" } }, @@ -331,7 +388,21 @@ const extension = declareDiscoveryExtension({ example: { results: [] } } }); -// Returns: { bazaar: { info: {...}, schema: {...} } } + +// MCP tool +const mcpExtension = declareDiscoveryExtension({ + toolName: "search", + description: "Search for documents", + inputSchema: { + type: "object", + properties: { query: { type: "string" } }, + required: ["query"] + }, + output: { + example: { results: [] } + } +}); +// Both return: { bazaar: { info: {...}, schema: {...} } } ``` #### `extractDiscoveryInfo(paymentPayload, paymentRequirements, validate?)` @@ -346,12 +417,21 @@ Extracts discovery information from a payment request (for facilitators). **Returns:** `DiscoveredResource` object or `null` if not found. ```typescript -interface DiscoveredResource { +interface DiscoveredHTTPResource { resourceUrl: string; - method: string; + method: string; // e.g. "GET", "POST" x402Version: number; discoveryInfo: DiscoveryInfo; } + +interface DiscoveredMCPResource { + resourceUrl: string; + toolName: string; // MCP tool name + x402Version: number; + discoveryInfo: DiscoveryInfo; +} + +type DiscoveredResource = DiscoveredHTTPResource | DiscoveredMCPResource; ``` #### `validateDiscoveryExtension(extension)` @@ -368,7 +448,7 @@ Validates and extracts discovery info in one step. #### `bazaarResourceServerExtension` -A server extension that automatically enriches discovery extensions with HTTP method information from the request context. +A server extension that automatically enriches HTTP discovery extensions with method information from the request context. MCP extensions are passed through unchanged. **Usage:** ```typescript @@ -391,7 +471,7 @@ The Sign-In-With-X extension implements [CAIP-122](https://chainagnostic.org/CAI 1. Server returns 402 with `sign-in-with-x` extension containing challenge parameters 2. Client signs the CAIP-122 message with their wallet 3. Client sends signed proof in `SIGN-IN-WITH-X` header -4. Server verifies signature and grants access if wallet has previous payment +4. Server verifies signature and grants access either because the route is auth-only or because the wallet has previously paid ### Server Usage @@ -415,7 +495,7 @@ const resourceServer = new x402ResourceServer(facilitatorClient) .registerExtension(siwxResourceServerExtension) // Refreshes nonce/timestamps per request .onAfterSettle(createSIWxSettleHook({ storage })); // Records payments -// 2. Declare SIWX support in routes (network/domain/uri derived automatically) +// 2. Declare SIWX support in routes const routes = { "GET /data": { accepts: [{scheme: "exact", price: "$0.01", network: "eip155:8453", payTo}], @@ -423,11 +503,19 @@ const routes = { statement: 'Sign in to access your purchased content', }), }, + "GET /profile": { + accepts: [], + extensions: declareSIWxExtension({ + network: ["eip155:8453", "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"], + statement: 'Sign in to view your profile', + expirationSeconds: 300, + }), + }, }; // 3. Verify incoming SIWX proofs const httpServer = new x402HTTPResourceServer(resourceServer, routes) - .onProtectedRequest(createSIWxRequestHook({ storage })); // Grants access if paid + .onProtectedRequest(createSIWxRequestHook({ storage })); // Grants access when SIWX auth is sufficient // Optional: Enable smart wallet support (EIP-1271/EIP-6492) import { createPublicClient, http } from 'viem'; @@ -444,7 +532,7 @@ const httpServerWithSmartWallets = new x402HTTPResourceServer(resourceServer, ro The hooks automatically: - **siwxResourceServerExtension**: Derives `network` from `accepts`, `domain`/`uri` from request URL, refreshes `nonce`/`issuedAt`/`expirationTime` per request - **createSIWxSettleHook**: Records payment when settlement succeeds -- **createSIWxRequestHook**: Validates and verifies SIWX proofs, grants access if wallet has paid +- **createSIWxRequestHook**: Validates and verifies SIWX proofs, grants access for auth-only routes or when the wallet has paid #### Manual Usage (Advanced) @@ -454,18 +542,15 @@ import { parseSIWxHeader, validateSIWxMessage, verifySIWxSignature, - SIGN_IN_WITH_X, } from '@x402/extensions/sign-in-with-x'; // 1. Declare in PaymentRequired response -const extensions = { - [SIGN_IN_WITH_X]: declareSIWxExtension({ - domain: 'api.example.com', - resourceUri: 'https://api.example.com/data', - network: 'eip155:8453', - statement: 'Sign in to access your purchased content', - }), -}; +const extensions = declareSIWxExtension({ + domain: 'api.example.com', + resourceUri: 'https://api.example.com/data', + network: 'eip155:8453', + statement: 'Sign in to access your purchased content', +}); // 2. Verify incoming proof async function handleRequest(request: Request) { @@ -491,10 +576,8 @@ async function handleRequest(request: Request) { } // verification.address is the verified wallet - // Check if this wallet has paid before - const hasPaid = await checkPaymentHistory(verification.address); - if (hasPaid) { - // Grant access without payment + if (await isAuthOnlyRoute(request) || await checkPaymentHistory(verification.address)) { + // Grant access } } ``` @@ -532,15 +615,15 @@ import { // 1. Get extension and network from 402 response const paymentRequired = await response.json(); const extension = paymentRequired.extensions['sign-in-with-x']; -const paymentNetwork = paymentRequired.accepts[0].network; // e.g., "eip155:8453" +const paymentNetwork = paymentRequired.accepts[0]?.network; // undefined for auth-only routes // 2. Find matching chain in supportedChains -const matchingChain = extension.supportedChains.find( - chain => chain.chainId === paymentNetwork -); +const matchingChain = paymentNetwork + ? extension.supportedChains.find(chain => chain.chainId === paymentNetwork) + : extension.supportedChains[0]; if (!matchingChain) { - // Payment network not supported for SIWX + // No chain supported by this signer / route combination throw new Error('Chain not supported'); } @@ -584,6 +667,8 @@ declareSIWxExtension({ - `resourceUri` → from request URL - `domain` → parsed from resourceUri +For auth-only routes declared with `accepts: []`, `network` cannot be inferred from payment requirements and should be provided explicitly. + **Multi-chain support:** When `network` is an array (or multiple networks in `accepts`), `supportedChains` will contain one entry per network. #### `parseSIWxHeader(header)` diff --git a/typescript/packages/extensions/package.json b/typescript/packages/extensions/package.json index a92668f..696ae43 100644 --- a/typescript/packages/extensions/package.json +++ b/typescript/packages/extensions/package.json @@ -1,6 +1,6 @@ { "name": "@x402/extensions", - "version": "2.3.0", + "version": "2.9.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/cjs/index.d.ts", @@ -22,8 +22,8 @@ "extensions" ], "license": "Apache-2.0", - "author": "Coinbase Inc.", - "repository": "https://github.com/coinbase/x402", + "author": "x402 Foundation", + "repository": "https://github.com/x402-foundation/x402", "description": "x402 Payment Protocol Extensions", "devDependencies": { "@eslint/js": "^9.24.0", @@ -43,9 +43,11 @@ "vitest": "^3.0.5" }, "dependencies": { + "@noble/curves": "^1.9.0", "@scure/base": "^1.2.6", "@x402/core": "workspace:~", "ajv": "^8.17.1", + "jose": "^5.9.6", "siwe": "^2.3.2", "tweetnacl": "^1.0.3", "viem": "^2.43.5", @@ -82,6 +84,16 @@ "default": "./dist/cjs/sign-in-with-x/index.js" } }, + "./offer-receipt": { + "import": { + "types": "./dist/esm/offer-receipt/index.d.mts", + "default": "./dist/esm/offer-receipt/index.mjs" + }, + "require": { + "types": "./dist/cjs/offer-receipt/index.d.ts", + "default": "./dist/cjs/offer-receipt/index.js" + } + }, "./payment-identifier": { "import": { "types": "./dist/esm/payment-identifier/index.d.mts", diff --git a/typescript/packages/extensions/src/bazaar/facilitator.ts b/typescript/packages/extensions/src/bazaar/facilitator.ts index bf97674..201489a 100644 --- a/typescript/packages/extensions/src/bazaar/facilitator.ts +++ b/typescript/packages/extensions/src/bazaar/facilitator.ts @@ -10,9 +10,66 @@ import Ajv from "ajv/dist/2020.js"; import type { PaymentPayload, PaymentRequirements, PaymentRequirementsV1 } from "@x402/core/types"; import type { DiscoveryExtension, DiscoveryInfo } from "./types"; +import type { McpDiscoveryInfo } from "./mcp/types"; +import type { DiscoveredHTTPResource } from "./http/types"; +import type { DiscoveredMCPResource } from "./mcp/types"; import { BAZAAR } from "./types"; import { extractDiscoveryInfoV1 } from "./v1/facilitator"; +/** + * Valid routeTemplate pattern: must start with "/", contain only safe URL path characters + * and :param identifiers, and not include traversal sequences or scheme markers. + * + * Allowed: /users/:userId, /weather/:country/:city, /api/v1/items + */ +const ROUTE_TEMPLATE_REGEX = /^\/[a-zA-Z0-9_/:.\-~%]+$/; + +/** + * Checks whether a routeTemplate value is structurally valid. + * + * Expected format: "/:param" segments using colon-prefixed identifiers + * (e.g. "/users/:userId", "/weather/:country/:city"). + * + * The facilitator is a trust boundary: clients control the payment payload and + * can modify routeTemplate before submission. A malicious value could cause the + * facilitator to catalog the payment under an arbitrary URL (catalog poisoning). + * This function enforces minimal structural requirements: + * - Must be a non-empty string starting with "/" + * - Must match the safe URL path character set (alphanumeric, _, :, /, ., -, ~, %) + * - Must not contain ".." (path traversal) + * - Must not contain "://" (URL injection) + * + * @param value - The raw routeTemplate string from the client payload + * @returns true if the value is a valid routeTemplate, false otherwise + * + * @internal Exported for facilitator use. + */ +export function isValidRouteTemplate(value: string | undefined): value is string { + if (!value) return false; + if (!ROUTE_TEMPLATE_REGEX.test(value)) return false; + // Decode percent-encoding before traversal checks so that %2e%2e is caught. + let decoded: string; + try { + decoded = decodeURIComponent(value); + } catch { + return false; + } + if (decoded.includes("..")) return false; + if (decoded.includes("://")) return false; + return true; +} + +/** + * Validates a routeTemplate and returns it if valid, undefined otherwise. + * + * @param value - The raw routeTemplate string to validate + * @returns The validated value, or undefined if invalid + * @deprecated Use `isValidRouteTemplate` instead. + */ +export function validateRouteTemplate(value: string | undefined): string | undefined { + return isValidRouteTemplate(value) ? value : undefined; +} + /** * Validation result for discovery extensions */ @@ -99,14 +156,10 @@ export function validateDiscoveryExtension(extension: DiscoveryExtension): Valid * } * ``` */ -export interface DiscoveredResource { - resourceUrl: string; - description?: string; - mimeType?: string; - method: string; - x402Version: number; - discoveryInfo: DiscoveryInfo; -} +export type { DiscoveredHTTPResource } from "./http/types"; +export type { DiscoveredMCPResource } from "./mcp/types"; + +export type DiscoveredResource = DiscoveredHTTPResource | DiscoveredMCPResource; /** * Extracts discovery information from payment payload and requirements. @@ -125,14 +178,25 @@ export function extractDiscoveryInfo( let discoveryInfo: DiscoveryInfo | null = null; let resourceUrl: string; + let routeTemplate: string | undefined; + if (paymentPayload.x402Version === 2) { resourceUrl = paymentPayload.resource?.url ?? ""; if (paymentPayload.extensions) { - const bazaarExtension = paymentPayload.extensions[BAZAAR]; + const bazaarExtension = paymentPayload.extensions[BAZAAR.key]; if (bazaarExtension && typeof bazaarExtension === "object") { try { + // routeTemplate uses :param syntax (e.g. "/users/:userId", "/weather/:country/:city"). + // Must start with "/", must not contain ".." or "://". + // Validate before use: the client controls this field in the payment payload. + const rawExt = bazaarExtension as Record; + const rawTemplate = + typeof rawExt.routeTemplate === "string" ? rawExt.routeTemplate : undefined; + if (isValidRouteTemplate(rawTemplate)) { + routeTemplate = rawTemplate; + } const extension = bazaarExtension as DiscoveryExtension; if (validate) { @@ -166,7 +230,10 @@ export function extractDiscoveryInfo( // Strip query params (?) and hash sections (#) for discovery cataloging const url = new URL(resourceUrl); - const normalizedResourceUrl = `${url.origin}${url.pathname}`; + // If a routeTemplate is present (dynamic route), use it as the canonical path + const canonicalUrl = routeTemplate + ? `${url.origin}${routeTemplate}` + : `${url.origin}${url.pathname}`; // Extract description and mimeType from resource info (v2) or requirements (v1) let description: string | undefined; @@ -181,14 +248,20 @@ export function extractDiscoveryInfo( mimeType = requirementsV1.mimeType; } - return { - resourceUrl: normalizedResourceUrl, + const base = { + resourceUrl: canonicalUrl, description, mimeType, - method: discoveryInfo.input.method, x402Version: paymentPayload.x402Version, discoveryInfo, }; + + if (discoveryInfo.input.type === "mcp") { + // MCP routes are not parameterized; routeTemplate is not applicable. + return { ...base, toolName: (discoveryInfo as McpDiscoveryInfo).input.toolName }; + } + + return { ...base, routeTemplate, method: discoveryInfo.input.method }; } /** diff --git a/typescript/packages/extensions/src/bazaar/facilitatorClient.ts b/typescript/packages/extensions/src/bazaar/facilitatorClient.ts index 16125b3..8e62ba0 100644 --- a/typescript/packages/extensions/src/bazaar/facilitatorClient.ts +++ b/typescript/packages/extensions/src/bazaar/facilitatorClient.ts @@ -13,7 +13,6 @@ import { WithExtensions } from "../types"; export interface ListDiscoveryResourcesParams { /** * Filter by protocol type (e.g., "http", "mcp"). - * Currently, the only supported protocol type is "http". */ type?: string; diff --git a/typescript/packages/extensions/src/bazaar/http/index.ts b/typescript/packages/extensions/src/bazaar/http/index.ts new file mode 100644 index 0000000..7caed88 --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/http/index.ts @@ -0,0 +1,17 @@ +/** + * HTTP-specific Bazaar Discovery Extension types and builders + */ + +export type { + QueryDiscoveryInfo, + BodyDiscoveryInfo, + QueryDiscoveryExtension, + BodyDiscoveryExtension, + DeclareQueryDiscoveryExtensionConfig, + DeclareBodyDiscoveryExtensionConfig, + DiscoveredHTTPResource, +} from "./types"; + +export { isQueryExtensionConfig, isBodyExtensionConfig } from "./types"; + +export { createQueryDiscoveryExtension, createBodyDiscoveryExtension } from "./resourceService"; diff --git a/typescript/packages/extensions/src/bazaar/http/resourceService.ts b/typescript/packages/extensions/src/bazaar/http/resourceService.ts new file mode 100644 index 0000000..f23d947 --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/http/resourceService.ts @@ -0,0 +1,211 @@ +/** + * HTTP resource service functions for creating Bazaar discovery extensions + */ + +import type { + QueryDiscoveryExtension, + BodyDiscoveryExtension, + DeclareQueryDiscoveryExtensionConfig, + DeclareBodyDiscoveryExtensionConfig, +} from "./types"; + +/** + * Create a query discovery extension (GET, HEAD, DELETE) + * + * @param root0 - Configuration object for query discovery extension + * @param root0.method - HTTP method (GET, HEAD, DELETE) + * @param root0.input - Query parameters + * @param root0.inputSchema - JSON schema for query parameters + * @param root0.output - Output specification with example + * @param root0.pathParams - Concrete path parameter values extracted from the request + * @param root0.pathParamsSchema - JSON schema for path parameters + * @returns QueryDiscoveryExtension with info and schema + */ +export function createQueryDiscoveryExtension({ + method, + input = {}, + inputSchema = { properties: {} }, + pathParams, + pathParamsSchema, + output, +}: DeclareQueryDiscoveryExtensionConfig): QueryDiscoveryExtension { + return { + info: { + input: { + type: "http" as const, + ...(method ? { method } : {}), + ...(input ? { queryParams: input } : {}), + ...(pathParams ? { pathParams } : {}), + }, + ...(output?.example + ? { + output: { + type: "json", + example: output.example, + }, + } + : {}), + }, + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + input: { + type: "object", + properties: { + type: { + type: "string", + const: "http", + }, + method: { + type: "string", + enum: ["GET", "HEAD", "DELETE"], + }, + ...(inputSchema + ? { + queryParams: { + type: "object" as const, + ...(typeof inputSchema === "object" ? inputSchema : {}), + }, + } + : {}), + ...(pathParamsSchema + ? { + pathParams: { + type: "object" as const, + ...(typeof pathParamsSchema === "object" ? pathParamsSchema : {}), + }, + } + : {}), + }, + required: ["type", "method"] as ("type" | "method")[], + // pathParams are not declared here at schema build time -- + // the server extension's enrichDeclaration adds them to both info and schema + // atomically at request time, keeping data and schema consistent. + additionalProperties: false, + }, + ...(output?.example + ? { + output: { + type: "object" as const, + properties: { + type: { + type: "string" as const, + }, + example: { + type: "object" as const, + ...(output.schema && typeof output.schema === "object" ? output.schema : {}), + }, + }, + required: ["type"] as const, + }, + } + : {}), + }, + required: ["input"], + }, + }; +} + +/** + * Create a body discovery extension (POST, PUT, PATCH) + * + * @param root0 - Configuration object for body discovery extension + * @param root0.method - HTTP method (POST, PUT, PATCH) + * @param root0.input - Request body specification + * @param root0.inputSchema - JSON schema for request body + * @param root0.bodyType - Content type of body (json, form-data, text) + * @param root0.output - Output specification with example + * @param root0.pathParams - Concrete path parameter values extracted from the request + * @param root0.pathParamsSchema - JSON schema for path parameters + * @returns BodyDiscoveryExtension with info and schema + */ +export function createBodyDiscoveryExtension({ + method, + input = {}, + inputSchema = { properties: {} }, + pathParams, + pathParamsSchema, + bodyType, + output, +}: DeclareBodyDiscoveryExtensionConfig): BodyDiscoveryExtension { + return { + info: { + input: { + type: "http" as const, + ...(method ? { method } : {}), + bodyType, + body: input, + ...(pathParams ? { pathParams } : {}), + }, + ...(output?.example + ? { + output: { + type: "json", + example: output.example, + }, + } + : {}), + }, + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + input: { + type: "object", + properties: { + type: { + type: "string", + const: "http", + }, + method: { + type: "string", + enum: ["POST", "PUT", "PATCH"], + }, + bodyType: { + type: "string", + enum: ["json", "form-data", "text"], + }, + body: inputSchema, + ...(pathParamsSchema + ? { + pathParams: { + type: "object" as const, + ...(typeof pathParamsSchema === "object" ? pathParamsSchema : {}), + }, + } + : {}), + }, + required: ["type", "method", "bodyType", "body"] as ( + | "type" + | "method" + | "bodyType" + | "body" + )[], + // pathParams are not declared here at schema build time -- + // the server extension's enrichDeclaration adds them to both info and schema + // atomically at request time, keeping data and schema consistent. + additionalProperties: false, + }, + ...(output?.example + ? { + output: { + type: "object" as const, + properties: { + type: { + type: "string" as const, + }, + example: { + type: "object" as const, + ...(output.schema && typeof output.schema === "object" ? output.schema : {}), + }, + }, + required: ["type"] as const, + }, + } + : {}), + }, + required: ["input"], + }, + }; +} diff --git a/typescript/packages/extensions/src/bazaar/http/types.ts b/typescript/packages/extensions/src/bazaar/http/types.ts new file mode 100644 index 0000000..be47d0d --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/http/types.ts @@ -0,0 +1,196 @@ +/** + * HTTP-specific type definitions for the Bazaar Discovery Extension + */ + +import type { BodyMethods, QueryParamMethods } from "@x402/core/http"; +import type { DiscoveryInfo } from "../types"; + +/** Shared schema definition for an object-typed parameter map (queryParams, pathParams, etc.) */ +interface ParamMapSchemaProperty { + type: "object"; + properties?: Record; + additionalProperties?: boolean; +} + +/** + * Discovery info for query parameter methods (GET, HEAD, DELETE) + */ +export interface QueryDiscoveryInfo { + input: { + type: "http"; + /** Absent at declaration time; set by bazaarResourceServerExtension.enrichDeclaration */ + method?: QueryParamMethods; + queryParams?: Record; + pathParams?: Record; + headers?: Record; + }; + output?: { + type?: string; + format?: string; + example?: unknown; + }; +} + +/** + * Discovery info for body methods (POST, PUT, PATCH) + */ +export interface BodyDiscoveryInfo { + input: { + type: "http"; + /** Absent at declaration time; set by bazaarResourceServerExtension.enrichDeclaration */ + method?: BodyMethods; + bodyType: "json" | "form-data" | "text"; + body: Record; + queryParams?: Record; + pathParams?: Record; + headers?: Record; + }; + output?: { + type?: string; + format?: string; + example?: unknown; + }; +} + +/** + * Discovery extension for query parameter methods (GET, HEAD, DELETE) + */ +export interface QueryDiscoveryExtension { + info: QueryDiscoveryInfo; + routeTemplate?: string; + + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema"; + type: "object"; + properties: { + input: { + type: "object"; + properties: { + type: { + type: "string"; + const: "http"; + }; + method: { + type: "string"; + enum: QueryParamMethods[]; + }; + queryParams?: ParamMapSchemaProperty & { required?: string[] }; + pathParams?: ParamMapSchemaProperty; + headers?: { + type: "object"; + additionalProperties: { + type: "string"; + }; + }; + }; + required: ("type" | "method")[]; + additionalProperties?: boolean; + }; + output?: { + type: "object"; + properties?: Record; + required?: readonly string[]; + additionalProperties?: boolean; + }; + }; + required: ["input"]; + }; +} + +/** + * Discovery extension for body methods (POST, PUT, PATCH) + */ +export interface BodyDiscoveryExtension { + info: BodyDiscoveryInfo; + routeTemplate?: string; + + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema"; + type: "object"; + properties: { + input: { + type: "object"; + properties: { + type: { + type: "string"; + const: "http"; + }; + method: { + type: "string"; + enum: BodyMethods[]; + }; + bodyType: { + type: "string"; + enum: ["json", "form-data", "text"]; + }; + body: Record; + queryParams?: ParamMapSchemaProperty & { required?: string[] }; + pathParams?: ParamMapSchemaProperty; + headers?: { + type: "object"; + additionalProperties: { + type: "string"; + }; + }; + }; + required: ("type" | "method" | "bodyType" | "body")[]; + additionalProperties?: boolean; + }; + output?: { + type: "object"; + properties?: Record; + required?: readonly string[]; + additionalProperties?: boolean; + }; + }; + required: ["input"]; + }; +} + +export interface DeclareQueryDiscoveryExtensionConfig { + method?: QueryParamMethods; + input?: Record; + inputSchema?: Record; + pathParams?: Record; + pathParamsSchema?: Record; + output?: { + example?: unknown; + schema?: Record; + }; +} + +export interface DeclareBodyDiscoveryExtensionConfig { + method?: BodyMethods; + input?: Record; + inputSchema?: Record; + pathParams?: Record; + pathParamsSchema?: Record; + bodyType: "json" | "form-data" | "text"; + output?: { + example?: unknown; + schema?: Record; + }; +} + +export interface DiscoveredHTTPResource { + resourceUrl: string; + description?: string; + mimeType?: string; + /** Present after server extension enrichment; may be absent for pre-enrichment data */ + method?: string; + routeTemplate?: string; + x402Version: number; + discoveryInfo: DiscoveryInfo; +} + +export const isQueryExtensionConfig = ( + config: DeclareQueryDiscoveryExtensionConfig | DeclareBodyDiscoveryExtensionConfig, +): config is DeclareQueryDiscoveryExtensionConfig => { + return !("bodyType" in config) && !("toolName" in config); +}; + +export const isBodyExtensionConfig = ( + config: DeclareQueryDiscoveryExtensionConfig | DeclareBodyDiscoveryExtensionConfig, +): config is DeclareBodyDiscoveryExtensionConfig => { + return "bodyType" in config; +}; diff --git a/typescript/packages/extensions/src/bazaar/index.ts b/typescript/packages/extensions/src/bazaar/index.ts index f2d94d8..235f713 100644 --- a/typescript/packages/extensions/src/bazaar/index.ts +++ b/typescript/packages/extensions/src/bazaar/index.ts @@ -2,7 +2,8 @@ * Bazaar Discovery Extension for x402 v2 and v1 * * Enables facilitators to automatically catalog and index x402-enabled resources - * by following the server's provided discovery instructions. + * by following the server's provided discovery instructions. Supports both + * HTTP endpoints and MCP (Model Context Protocol) tools. * * ## V2 Usage * @@ -15,17 +16,25 @@ * ```typescript * import { declareDiscoveryExtension, BAZAAR } from '@x402/extensions/bazaar'; * - * // Declare a GET endpoint - * const extension = declareDiscoveryExtension( - * "GET", - * { query: "example" }, - * { - * properties: { - * query: { type: "string" } - * }, + * // Declare an HTTP GET endpoint + * const httpExtension = declareDiscoveryExtension({ + * input: { query: "example" }, + * inputSchema: { + * properties: { query: { type: "string" } }, * required: ["query"] * } - * ); + * }); + * + * // Declare an MCP tool + * const mcpExtension = declareDiscoveryExtension({ + * toolName: "financial_analysis", + * description: "Analyze financial data for a given ticker", + * inputSchema: { + * type: "object", + * properties: { ticker: { type: "string" } }, + * required: ["ticker"] + * } + * }); * * // Include in PaymentRequired response * const paymentRequired = { @@ -33,7 +42,7 @@ * resource: { ... }, * accepts: [ ... ], * extensions: { - * [BAZAAR]: extension + * [BAZAAR.key]: extension * } * }; * ``` @@ -76,12 +85,24 @@ export type { DiscoveryInfo, QueryDiscoveryInfo, BodyDiscoveryInfo, + McpDiscoveryInfo, QueryDiscoveryExtension, BodyDiscoveryExtension, + McpDiscoveryExtension, DiscoveryExtension, + DeclareQueryDiscoveryExtensionConfig, + DeclareBodyDiscoveryExtensionConfig, + DeclareMcpDiscoveryExtensionConfig, + DeclareDiscoveryExtensionConfig, + DeclareDiscoveryExtensionInput, } from "./types"; -export { BAZAAR } from "./types"; +export { + BAZAAR, + isMcpExtensionConfig, + isQueryExtensionConfig, + isBodyExtensionConfig, +} from "./types"; // Export resource service functions (for servers) export { declareDiscoveryExtension } from "./resourceService"; @@ -91,10 +112,14 @@ export { bazaarResourceServerExtension } from "./server"; // Export facilitator functions (for facilitators) export { validateDiscoveryExtension, + isValidRouteTemplate, + validateRouteTemplate, extractDiscoveryInfo, extractDiscoveryInfoFromExtension, validateAndExtract, type ValidationResult, + type DiscoveredHTTPResource, + type DiscoveredMCPResource, type DiscoveredResource, } from "./facilitator"; diff --git a/typescript/packages/extensions/src/bazaar/mcp/index.ts b/typescript/packages/extensions/src/bazaar/mcp/index.ts new file mode 100644 index 0000000..9ff0a7d --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/mcp/index.ts @@ -0,0 +1,14 @@ +/** + * MCP-specific Bazaar Discovery Extension types and builders + */ + +export type { + McpDiscoveryInfo, + McpDiscoveryExtension, + DeclareMcpDiscoveryExtensionConfig, + DiscoveredMCPResource, +} from "./types"; + +export { isMcpExtensionConfig } from "./types"; + +export { createMcpDiscoveryExtension } from "./resourceService"; diff --git a/typescript/packages/extensions/src/bazaar/mcp/resourceService.ts b/typescript/packages/extensions/src/bazaar/mcp/resourceService.ts new file mode 100644 index 0000000..ca8b122 --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/mcp/resourceService.ts @@ -0,0 +1,110 @@ +/** + * MCP resource service functions for creating Bazaar discovery extensions + */ + +import type { McpDiscoveryExtension, DeclareMcpDiscoveryExtensionConfig } from "./types"; + +/** + * Create an MCP tool discovery extension + * + * @param root0 - Configuration object for MCP discovery extension + * @param root0.toolName - MCP tool name + * @param root0.description - Tool description + * @param root0.inputSchema - JSON Schema for tool arguments + * @param root0.example - Example tool arguments + * @param root0.output - Output specification with example + * @param root0.transport - MCP transport type (streamable-http or sse) + * @returns McpDiscoveryExtension with info and schema + */ +export function createMcpDiscoveryExtension({ + toolName, + description, + transport, + inputSchema, + example, + output, +}: DeclareMcpDiscoveryExtensionConfig): McpDiscoveryExtension { + return { + info: { + input: { + type: "mcp", + toolName, + ...(description !== undefined ? { description } : {}), + ...(transport !== undefined ? { transport } : {}), + inputSchema, + ...(example !== undefined ? { example } : {}), + }, + ...(output?.example + ? { + output: { + type: "json", + example: output.example, + }, + } + : {}), + }, + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + input: { + type: "object", + properties: { + type: { + type: "string", + const: "mcp", + }, + toolName: { + type: "string", + }, + ...(description !== undefined + ? { + description: { + type: "string" as const, + }, + } + : {}), + ...(transport !== undefined + ? { + transport: { + type: "string" as const, + enum: ["streamable-http", "sse"], + }, + } + : {}), + inputSchema: { + type: "object" as const, + }, + ...(example !== undefined + ? { + example: { + type: "object" as const, + }, + } + : {}), + }, + required: ["type", "toolName", "inputSchema"] as ("type" | "toolName" | "inputSchema")[], + additionalProperties: false, + }, + ...(output?.example + ? { + output: { + type: "object" as const, + properties: { + type: { + type: "string" as const, + }, + example: { + type: "object" as const, + ...(output.schema && typeof output.schema === "object" ? output.schema : {}), + }, + }, + required: ["type"] as const, + }, + } + : {}), + }, + required: ["input"], + }, + }; +} diff --git a/typescript/packages/extensions/src/bazaar/mcp/types.ts b/typescript/packages/extensions/src/bazaar/mcp/types.ts new file mode 100644 index 0000000..a2aa97b --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/mcp/types.ts @@ -0,0 +1,95 @@ +/** + * MCP-specific type definitions for the Bazaar Discovery Extension + */ + +import type { DiscoveryInfo } from "../types"; + +/** + * Discovery info for MCP tools + */ +export interface McpDiscoveryInfo { + input: { + type: "mcp"; + toolName: string; + description?: string; + transport?: "streamable-http" | "sse"; + inputSchema: Record; + example?: Record; + }; + output?: { + type?: string; + format?: string; + example?: unknown; + }; +} + +/** + * Discovery extension for MCP tools + */ +export interface McpDiscoveryExtension { + info: McpDiscoveryInfo; + + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema"; + type: "object"; + properties: { + input: { + type: "object"; + properties: { + type: { + type: "string"; + const: "mcp"; + }; + toolName: { + type: "string"; + }; + description?: { + type: "string"; + }; + transport?: { + type: "string"; + enum: ["streamable-http", "sse"]; + }; + inputSchema: Record; + example?: Record; + }; + required: ("type" | "toolName" | "inputSchema")[]; + additionalProperties?: boolean; + }; + output?: { + type: "object"; + properties?: Record; + required?: readonly string[]; + additionalProperties?: boolean; + }; + }; + required: ["input"]; + }; +} + +export interface DeclareMcpDiscoveryExtensionConfig { + toolName: string; + description?: string; + transport?: "streamable-http" | "sse"; + inputSchema: Record; + example?: Record; + output?: { + example?: unknown; + schema?: Record; + }; +} + +export interface DiscoveredMCPResource { + resourceUrl: string; + description?: string; + mimeType?: string; + toolName: string; + x402Version: number; + discoveryInfo: DiscoveryInfo; +} + +export const isMcpExtensionConfig = ( + config: DeclareMcpDiscoveryExtensionConfig | Record, +): config is DeclareMcpDiscoveryExtensionConfig => { + return "toolName" in config; +}; diff --git a/typescript/packages/extensions/src/bazaar/resourceService.ts b/typescript/packages/extensions/src/bazaar/resourceService.ts index fb121cf..bd1f14c 100644 --- a/typescript/packages/extensions/src/bazaar/resourceService.ts +++ b/typescript/packages/extensions/src/bazaar/resourceService.ts @@ -1,185 +1,24 @@ /** - * Resource Service functions for creating Bazaar discovery extensions + * Resource Service entry point for creating Bazaar discovery extensions * - * These functions help servers declare the shape of their endpoints - * for facilitator discovery and cataloging in the Bazaar. + * This module provides the unified `declareDiscoveryExtension` function that + * routes to protocol-specific builders in http/ and mcp/. */ +import type { DiscoveryExtension, DeclareDiscoveryExtensionInput } from "./types"; +import type { + DeclareQueryDiscoveryExtensionConfig, + DeclareBodyDiscoveryExtensionConfig, +} from "./http/types"; +import type { DeclareMcpDiscoveryExtensionConfig } from "./mcp/types"; import { - type DiscoveryExtension, - type QueryDiscoveryExtension, - type BodyDiscoveryExtension, - type DeclareDiscoveryExtensionInput, - type DeclareQueryDiscoveryExtensionConfig, - type DeclareBodyDiscoveryExtensionConfig, -} from "./types"; + createQueryDiscoveryExtension, + createBodyDiscoveryExtension, +} from "./http/resourceService"; +import { createMcpDiscoveryExtension } from "./mcp/resourceService"; /** - * Internal helper to create a query discovery extension - * - * @param root0 - Configuration object for query discovery extension - * @param root0.method - HTTP method (GET, HEAD, DELETE) - * @param root0.input - Query parameters - * @param root0.inputSchema - JSON schema for query parameters - * @param root0.output - Output specification with example - * @returns QueryDiscoveryExtension with info and schema - */ -function createQueryDiscoveryExtension({ - method, - input = {}, - inputSchema = { properties: {} }, - output, -}: DeclareQueryDiscoveryExtensionConfig): QueryDiscoveryExtension { - return { - info: { - input: { - type: "http", - ...(method ? { method } : {}), - ...(input ? { queryParams: input } : {}), - } as QueryDiscoveryExtension["info"]["input"], - ...(output?.example - ? { - output: { - type: "json", - example: output.example, - }, - } - : {}), - }, - schema: { - $schema: "https://json-schema.org/draft/2020-12/schema", - type: "object", - properties: { - input: { - type: "object", - properties: { - type: { - type: "string", - const: "http", - }, - method: { - type: "string", - enum: ["GET", "HEAD", "DELETE"], - }, - ...(inputSchema - ? { - queryParams: { - type: "object" as const, - ...(typeof inputSchema === "object" ? inputSchema : {}), - }, - } - : {}), - }, - required: ["type"] as ("type" | "method")[], - additionalProperties: false, - }, - ...(output?.example - ? { - output: { - type: "object" as const, - properties: { - type: { - type: "string" as const, - }, - example: { - type: "object" as const, - ...(output.schema && typeof output.schema === "object" ? output.schema : {}), - }, - }, - required: ["type"] as const, - }, - } - : {}), - }, - required: ["input"], - }, - }; -} - -/** - * Internal helper to create a body discovery extension - * - * @param root0 - Configuration object for body discovery extension - * @param root0.method - HTTP method (POST, PUT, PATCH) - * @param root0.input - Request body specification - * @param root0.inputSchema - JSON schema for request body - * @param root0.bodyType - Content type of body (json, form-data, text) - required for body methods - * @param root0.output - Output specification with example - * @returns BodyDiscoveryExtension with info and schema - */ -function createBodyDiscoveryExtension({ - method, - input = {}, - inputSchema = { properties: {} }, - bodyType, - output, -}: DeclareBodyDiscoveryExtensionConfig): BodyDiscoveryExtension { - return { - info: { - input: { - type: "http", - ...(method ? { method } : {}), - bodyType, - body: input, - } as BodyDiscoveryExtension["info"]["input"], - ...(output?.example - ? { - output: { - type: "json", - example: output.example, - }, - } - : {}), - }, - schema: { - $schema: "https://json-schema.org/draft/2020-12/schema", - type: "object", - properties: { - input: { - type: "object", - properties: { - type: { - type: "string", - const: "http", - }, - method: { - type: "string", - enum: ["POST", "PUT", "PATCH"], - }, - bodyType: { - type: "string", - enum: ["json", "form-data", "text"], - }, - body: inputSchema, - }, - required: ["type", "bodyType", "body"] as ("type" | "method" | "bodyType" | "body")[], - additionalProperties: false, - }, - ...(output?.example - ? { - output: { - type: "object" as const, - properties: { - type: { - type: "string" as const, - }, - example: { - type: "object" as const, - ...(output.schema && typeof output.schema === "object" ? output.schema : {}), - }, - }, - required: ["type"] as const, - }, - } - : {}), - }, - required: ["input"], - }, - }; -} - -/** - * Create a discovery extension for any HTTP method + * Create a discovery extension for any HTTP method or MCP tool * * This function helps servers declare how their endpoint should be called, * including the expected input parameters/body and output format. @@ -225,11 +64,32 @@ function createBodyDiscoveryExtension({ * example: { success: true, id: "123" } * } * }); + * + * // For an MCP tool + * const mcpExtension = declareDiscoveryExtension({ + * toolName: "financial_analysis", + * description: "Analyze financial data for a given ticker", + * inputSchema: { + * type: "object", + * properties: { + * ticker: { type: "string" }, + * }, + * required: ["ticker"], + * }, + * output: { + * example: { pe_ratio: 28.5, recommendation: "hold" } + * } + * }); * ``` */ export function declareDiscoveryExtension( config: DeclareDiscoveryExtensionInput, ): Record { + if ("toolName" in config) { + const extension = createMcpDiscoveryExtension(config as DeclareMcpDiscoveryExtensionConfig); + return { bazaar: extension as DiscoveryExtension }; + } + const bodyType = (config as DeclareBodyDiscoveryExtensionConfig).bodyType; const isBodyMethod = bodyType !== undefined; diff --git a/typescript/packages/extensions/src/bazaar/server.ts b/typescript/packages/extensions/src/bazaar/server.ts index 66ff348..b7a42a7 100644 --- a/typescript/packages/extensions/src/bazaar/server.ts +++ b/typescript/packages/extensions/src/bazaar/server.ts @@ -2,6 +2,16 @@ import type { ResourceServerExtension } from "@x402/core/types"; import type { HTTPRequestContext } from "@x402/core/http"; import { BAZAAR } from "./types"; +// Non-global: safe for test/split (no stateful lastIndex side-effects). +const BRACKET_PARAM_REGEX = /\[([^\]]+)\]/; +// Global variant required for String.replace to substitute ALL occurrences. +// JS String.replace with a non-global regex replaces only the first match. +// (String.replaceAll with a non-global regex would work in ES2021+, but the +// target lib is ES2020 — keep this separate constant to avoid that constraint.) +const BRACKET_PARAM_REGEX_ALL = /\[([^\]]+)\]/g; + +const COLON_PARAM_REGEX = /:([a-zA-Z_][a-zA-Z0-9_]*)/; + /** * Type guard to check if context is an HTTP request context. * @@ -12,6 +22,102 @@ function isHTTPRequestContext(ctx: unknown): ctx is HTTPRequestContext { return ctx !== null && typeof ctx === "object" && "method" in ctx && "adapter" in ctx; } +/** + * Converts wildcard segments in a route pattern to named :varN parameters + * so they can be treated as dynamic routes for discovery catalog normalization. + * + * @param pattern - Route pattern that may contain wildcard segments + * @returns The pattern with wildcard segments replaced by :var1, :var2, etc. + */ +function normalizeWildcardPattern(pattern: string): string { + if (!pattern.includes("*")) { + return pattern; + } + let counter = 0; + return pattern + .split("/") + .map(seg => { + if (seg === "*") { + counter++; + return `:var${counter}`; + } + return seg; + }) + .join("/"); +} + +/** + * Converts a parameterized route pattern into a :param template and extracts concrete + * param values from the URL path in a single call. + * + * Supports both [param] (Next.js) and :param (Express) syntax. The output routeTemplate + * always uses :param syntax regardless of input format. + * + * @param routePattern - Route pattern (e.g. "/users/[userId]" or "/users/:userId") + * @param urlPath - Concrete URL path (e.g. "/users/123") + * @returns Object with routeTemplate and pathParams, or null if no params detected + */ +function extractDynamicRouteInfo( + routePattern: string, + urlPath: string, +): { routeTemplate: string; pathParams: Record } | null { + const hasBracket = BRACKET_PARAM_REGEX.test(routePattern); + const hasColon = COLON_PARAM_REGEX.test(routePattern); + if (!hasBracket && !hasColon) { + return null; + } + // When both [param] and :param are present, normalize brackets to colons first + // so all params are extracted uniformly. + const normalizedPattern = hasBracket + ? routePattern.replace(BRACKET_PARAM_REGEX_ALL, ":$1") + : routePattern; + const pathParams = extractPathParams(normalizedPattern, urlPath, false); + return { routeTemplate: normalizedPattern, pathParams }; +} + +/** + * Extracts concrete path parameter values by matching a URL path against a route pattern. + * + * @param routePattern - Route pattern with [paramName] or :paramName segments + * @param urlPath - Concrete URL path (e.g. "/users/123") + * @param isBracket - True if pattern uses [param] syntax, false for :param + * @returns Record mapping param names to their values + */ +function extractPathParams( + routePattern: string, + urlPath: string, + isBracket: boolean, +): Record { + const paramNames: string[] = []; + const splitRegex = isBracket ? BRACKET_PARAM_REGEX : COLON_PARAM_REGEX; + // Split on param markers so literal segments can be regex-escaped independently. + // Without escaping, a route like /api/v1.0/[id] would produce a regex where '.' matches + // any character (e.g. /api/v1X0/123 would incorrectly match). + const parts = routePattern.split(splitRegex); + const regexParts: string[] = []; + parts.forEach((part, i) => { + if (i % 2 === 0) { + // Literal segment – escape all regex metacharacters + regexParts.push(part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); + } else { + // Param name + paramNames.push(part); + regexParts.push("([^/]+)"); + } + }); + + const regex = new RegExp(`^${regexParts.join("")}$`); + const match = urlPath.match(regex); + + if (!match) return {}; + + const result: Record = {}; + paramNames.forEach((name, idx) => { + result[name] = match[idx + 1]; + }); + return result; +} + interface ExtensionDeclaration { [key: string]: unknown; info?: { @@ -35,7 +141,7 @@ interface ExtensionDeclaration { } export const bazaarResourceServerExtension: ResourceServerExtension = { - key: BAZAAR, + key: BAZAAR.key, enrichDeclaration: (declaration, transportContext) => { if (!isHTTPRequestContext(transportContext)) { @@ -43,6 +149,12 @@ export const bazaarResourceServerExtension: ResourceServerExtension = { } const extension = declaration as ExtensionDeclaration; + + // MCP extensions don't need HTTP method enrichment + if (extension.info?.input?.type === "mcp") { + return declaration; + } + const method = transportContext.method; // At declaration time, the schema uses a broad enum (["GET", "HEAD", "DELETE"] or ["POST", "PUT", "PATCH"]) @@ -57,7 +169,7 @@ export const bazaarResourceServerExtension: ResourceServerExtension = { }, }; - return { + const enrichedResult = { ...extension, info: { ...(extension.info || {}), @@ -83,5 +195,45 @@ export const bazaarResourceServerExtension: ResourceServerExtension = { }, }, }; + + // Dynamic routes: translate [param]/:param → :param for the routeTemplate catalog key; + // pathParams carries runtime values (distinct from pathParamsSchema in the declaration). + // Wildcard * segments are auto-converted to :var1, :var2, etc. for catalog normalization. + const rawRoutePattern = (transportContext as HTTPRequestContext).routePattern; + const routePattern = rawRoutePattern ? normalizeWildcardPattern(rawRoutePattern) : undefined; + const dynamicRoute = routePattern + ? extractDynamicRouteInfo(routePattern, transportContext.adapter.getPath()) + : null; + if (dynamicRoute) { + const inputSchemaProps = enrichedResult.schema?.properties?.input?.properties || {}; + const hasPathParamsInSchema = "pathParams" in inputSchemaProps; + return { + ...enrichedResult, + routeTemplate: dynamicRoute.routeTemplate, + info: { + ...enrichedResult.info, + input: { ...enrichedResult.info.input, pathParams: dynamicRoute.pathParams }, + }, + ...(!hasPathParamsInSchema + ? { + schema: { + ...enrichedResult.schema, + properties: { + ...enrichedResult.schema?.properties, + input: { + ...enrichedResult.schema?.properties?.input, + properties: { + ...inputSchemaProps, + pathParams: { type: "object" }, + }, + }, + }, + }, + } + : {}), + }; + } + + return enrichedResult; }, }; diff --git a/typescript/packages/extensions/src/bazaar/types.ts b/typescript/packages/extensions/src/bazaar/types.ts index 3bec6c2..9d2c1b9 100644 --- a/typescript/packages/extensions/src/bazaar/types.ts +++ b/typescript/packages/extensions/src/bazaar/types.ts @@ -1,185 +1,71 @@ /** - * Type definitions for the Bazaar Discovery Extension + * Shared type definitions for the Bazaar Discovery Extension + * + * Protocol-specific types live in their own directories (http/, mcp/). + * This file defines the shared unions, constants, and utility types, + * and re-exports all protocol-specific types for backwards compatibility. */ -import type { BodyMethods, QueryParamMethods } from "@x402/core/http"; +import type { FacilitatorExtension } from "@x402/core/types"; -/** - * Extension identifier constant for the Bazaar discovery extension - */ -export const BAZAAR = "bazaar"; +// --- Shared union types --- -/** - * Discovery info for query parameter methods (GET, HEAD, DELETE) - */ -export interface QueryDiscoveryInfo { - input: { - type: "http"; - method: QueryParamMethods; - queryParams?: Record; - headers?: Record; - }; - output?: { - type?: string; - format?: string; - example?: unknown; - }; -} +import type { QueryDiscoveryInfo, BodyDiscoveryInfo } from "./http/types"; +import type { McpDiscoveryInfo } from "./mcp/types"; +import type { QueryDiscoveryExtension, BodyDiscoveryExtension } from "./http/types"; +import type { McpDiscoveryExtension } from "./mcp/types"; +import type { + DeclareQueryDiscoveryExtensionConfig, + DeclareBodyDiscoveryExtensionConfig, +} from "./http/types"; +import type { DeclareMcpDiscoveryExtensionConfig } from "./mcp/types"; -/** - * Discovery info for body methods (POST, PUT, PATCH) - */ -export interface BodyDiscoveryInfo { - input: { - type: "http"; - method: BodyMethods; - bodyType: "json" | "form-data" | "text"; - body: Record; - queryParams?: Record; - headers?: Record; - }; - output?: { - type?: string; - format?: string; - example?: unknown; - }; -} +// Re-export protocol-specific types +export type { + QueryDiscoveryInfo, + BodyDiscoveryInfo, + QueryDiscoveryExtension, + BodyDiscoveryExtension, + DeclareQueryDiscoveryExtensionConfig, + DeclareBodyDiscoveryExtensionConfig, + DiscoveredHTTPResource, +} from "./http/types"; -/** - * Combined discovery info type - */ -export type DiscoveryInfo = QueryDiscoveryInfo | BodyDiscoveryInfo; +export { isQueryExtensionConfig, isBodyExtensionConfig } from "./http/types"; -/** - * Discovery extension for query parameter methods (GET, HEAD, DELETE) - */ -export interface QueryDiscoveryExtension { - info: QueryDiscoveryInfo; +export type { + McpDiscoveryInfo, + McpDiscoveryExtension, + DeclareMcpDiscoveryExtensionConfig, + DiscoveredMCPResource, +} from "./mcp/types"; + +export { isMcpExtensionConfig } from "./mcp/types"; - schema: { - $schema: "https://json-schema.org/draft/2020-12/schema"; - type: "object"; - properties: { - input: { - type: "object"; - properties: { - type: { - type: "string"; - const: "http"; - }; - method: { - type: "string"; - enum: QueryParamMethods[]; - }; - queryParams?: { - type: "object"; - properties?: Record; - required?: string[]; - additionalProperties?: boolean; - }; - headers?: { - type: "object"; - additionalProperties: { - type: "string"; - }; - }; - }; - required: ("type" | "method")[]; - additionalProperties?: boolean; - }; - output?: { - type: "object"; - properties?: Record; - required?: readonly string[]; - additionalProperties?: boolean; - }; - }; - required: ["input"]; - }; -} +// --- Shared constants --- /** - * Discovery extension for body methods (POST, PUT, PATCH) + * Extension identifier for the Bazaar discovery extension. */ -export interface BodyDiscoveryExtension { - info: BodyDiscoveryInfo; +export const BAZAAR: FacilitatorExtension = { key: "bazaar" }; - schema: { - $schema: "https://json-schema.org/draft/2020-12/schema"; - type: "object"; - properties: { - input: { - type: "object"; - properties: { - type: { - type: "string"; - const: "http"; - }; - method: { - type: "string"; - enum: BodyMethods[]; - }; - bodyType: { - type: "string"; - enum: ["json", "form-data", "text"]; - }; - body: Record; - queryParams?: { - type: "object"; - properties?: Record; - required?: string[]; - additionalProperties?: boolean; - }; - headers?: { - type: "object"; - additionalProperties: { - type: "string"; - }; - }; - }; - required: ("type" | "method" | "bodyType" | "body")[]; - additionalProperties?: boolean; - }; - output?: { - type: "object"; - properties?: Record; - required?: readonly string[]; - additionalProperties?: boolean; - }; - }; - required: ["input"]; - }; -} +/** + * Combined discovery info type + */ +export type DiscoveryInfo = QueryDiscoveryInfo | BodyDiscoveryInfo | McpDiscoveryInfo; /** * Combined discovery extension type */ -export type DiscoveryExtension = QueryDiscoveryExtension | BodyDiscoveryExtension; - -export interface DeclareQueryDiscoveryExtensionConfig { - method?: QueryParamMethods; - input?: Record; - inputSchema?: Record; - output?: { - example?: unknown; - schema?: Record; - }; -} - -export interface DeclareBodyDiscoveryExtensionConfig { - method?: BodyMethods; - input?: Record; - inputSchema?: Record; - bodyType: "json" | "form-data" | "text"; - output?: { - example?: unknown; - schema?: Record; - }; -} +export type DiscoveryExtension = + | QueryDiscoveryExtension + | BodyDiscoveryExtension + | McpDiscoveryExtension; export type DeclareDiscoveryExtensionConfig = | DeclareQueryDiscoveryExtensionConfig - | DeclareBodyDiscoveryExtensionConfig; + | DeclareBodyDiscoveryExtensionConfig + | DeclareMcpDiscoveryExtensionConfig; /** * Distributive Omit - properly distributes Omit over union types. @@ -194,21 +80,10 @@ export type DistributiveOmit = T extends T ? Omit : /** * Config type for declareDiscoveryExtension function. - * Uses DistributiveOmit to preserve bodyType discriminant in the union. + * Uses DistributiveOmit to preserve bodyType discriminant in the union for HTTP configs. + * MCP config has no `method` field so it's included directly. */ -export type DeclareDiscoveryExtensionInput = DistributiveOmit< - DeclareDiscoveryExtensionConfig, - "method" ->; - -export const isQueryExtensionConfig = ( - config: DeclareDiscoveryExtensionConfig, -): config is DeclareQueryDiscoveryExtensionConfig => { - return !("bodyType" in config); -}; - -export const isBodyExtensionConfig = ( - config: DeclareDiscoveryExtensionConfig, -): config is DeclareBodyDiscoveryExtensionConfig => { - return "bodyType" in config; -}; +export type DeclareDiscoveryExtensionInput = + | DistributiveOmit + | DistributiveOmit + | DeclareMcpDiscoveryExtensionConfig; diff --git a/typescript/packages/extensions/src/eip2612-gas-sponsoring/facilitator.ts b/typescript/packages/extensions/src/eip2612-gas-sponsoring/facilitator.ts new file mode 100644 index 0000000..332bc01 --- /dev/null +++ b/typescript/packages/extensions/src/eip2612-gas-sponsoring/facilitator.ts @@ -0,0 +1,86 @@ +/** + * Facilitator functions for extracting and validating EIP-2612 Gas Sponsoring extension data. + * + * These functions help facilitators extract the EIP-2612 permit data from payment + * payloads and validate it before calling settleWithPermit. + */ + +import type { PaymentPayload } from "@x402/core/types"; +import { + EIP2612_GAS_SPONSORING, + type Eip2612GasSponsoringInfo, + type Eip2612GasSponsoringExtension, +} from "./types"; + +/** + * Extracts the EIP-2612 gas sponsoring info from a payment payload's extensions. + * + * Returns the info if the extension is present and contains the required client-populated + * fields (from, asset, spender, amount, nonce, deadline, signature, version). + * + * @param paymentPayload - The payment payload to extract from + * @returns The EIP-2612 gas sponsoring info, or null if not present + */ +export function extractEip2612GasSponsoringInfo( + paymentPayload: PaymentPayload, +): Eip2612GasSponsoringInfo | null { + if (!paymentPayload.extensions) { + return null; + } + + const extension = paymentPayload.extensions[EIP2612_GAS_SPONSORING.key] as + | Eip2612GasSponsoringExtension + | undefined; + + if (!extension?.info) { + return null; + } + + const info = extension.info as Record; + + // Check that the client has populated the required fields + if ( + !info.from || + !info.asset || + !info.spender || + !info.amount || + !info.nonce || + !info.deadline || + !info.signature || + !info.version + ) { + return null; + } + + return info as unknown as Eip2612GasSponsoringInfo; +} + +/** + * Validates that the EIP-2612 gas sponsoring info has valid format. + * + * Performs basic validation on the info fields: + * - Addresses are valid hex (0x + 40 hex chars) + * - Amount, nonce, deadline are numeric strings + * - Signature is a hex string + * - Version is a numeric version string + * + * @param info - The EIP-2612 gas sponsoring info to validate + * @returns True if the info is valid, false otherwise + */ +export function validateEip2612GasSponsoringInfo(info: Eip2612GasSponsoringInfo): boolean { + const addressPattern = /^0x[a-fA-F0-9]{40}$/; + const numericPattern = /^[0-9]+$/; + const hexPattern = /^0x[a-fA-F0-9]+$/; + const versionPattern = /^[0-9]+(\.[0-9]+)*$/; + + return ( + addressPattern.test(info.from) && + addressPattern.test(info.asset) && + addressPattern.test(info.spender) && + numericPattern.test(info.amount) && + numericPattern.test(info.nonce) && + numericPattern.test(info.deadline) && + hexPattern.test(info.signature) && + versionPattern.test(info.version) + ); +} diff --git a/typescript/packages/extensions/src/eip2612-gas-sponsoring/index.ts b/typescript/packages/extensions/src/eip2612-gas-sponsoring/index.ts new file mode 100644 index 0000000..1697444 --- /dev/null +++ b/typescript/packages/extensions/src/eip2612-gas-sponsoring/index.ts @@ -0,0 +1,52 @@ +/** + * EIP-2612 Gas Sponsoring Extension for x402 + * + * Enables gasless Permit2 approval for tokens that implement EIP-2612. + * The client signs an off-chain permit, and the facilitator submits it + * on-chain via `x402Permit2Proxy.settleWithPermit`. + * + * ## For Resource Servers + * + * ```typescript + * import { declareEip2612GasSponsoringExtension } from '@x402/extensions'; + * + * const routes = [ + * { + * path: "/api/data", + * price: "$0.01", + * extensions: { + * ...declareEip2612GasSponsoringExtension(), + * }, + * }, + * ]; + * ``` + * + * ## For Facilitators + * + * ```typescript + * import { + * extractEip2612GasSponsoringInfo, + * validateEip2612GasSponsoringInfo, + * } from '@x402/extensions'; + * + * const info = extractEip2612GasSponsoringInfo(paymentPayload); + * if (info && validateEip2612GasSponsoringInfo(info)) { + * // Use settleWithPermit instead of settle + * } + * ``` + */ + +// Export types +export type { + Eip2612GasSponsoringInfo, + Eip2612GasSponsoringServerInfo, + Eip2612GasSponsoringExtension, +} from "./types"; + +export { EIP2612_GAS_SPONSORING } from "./types"; + +// Export resource service functions (for servers) +export { declareEip2612GasSponsoringExtension } from "./resourceService"; + +// Export facilitator functions +export { extractEip2612GasSponsoringInfo, validateEip2612GasSponsoringInfo } from "./facilitator"; diff --git a/typescript/packages/extensions/src/eip2612-gas-sponsoring/resourceService.ts b/typescript/packages/extensions/src/eip2612-gas-sponsoring/resourceService.ts new file mode 100644 index 0000000..314f56d --- /dev/null +++ b/typescript/packages/extensions/src/eip2612-gas-sponsoring/resourceService.ts @@ -0,0 +1,102 @@ +/** + * Resource Service functions for declaring the EIP-2612 Gas Sponsoring extension. + * + * These functions help servers declare support for EIP-2612 gasless Permit2 approvals + * in the PaymentRequired response extensions. + */ + +import { EIP2612_GAS_SPONSORING, type Eip2612GasSponsoringExtension } from "./types"; + +/** + * The JSON Schema for the EIP-2612 gas sponsoring extension info. + * Matches the schema defined in the spec. + */ +const eip2612GasSponsoringSchema: Record = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + from: { + type: "string", + pattern: "^0x[a-fA-F0-9]{40}$", + description: "The address of the sender.", + }, + asset: { + type: "string", + pattern: "^0x[a-fA-F0-9]{40}$", + description: "The address of the ERC-20 token contract.", + }, + spender: { + type: "string", + pattern: "^0x[a-fA-F0-9]{40}$", + description: "The address of the spender (Canonical Permit2).", + }, + amount: { + type: "string", + pattern: "^[0-9]+$", + description: "The amount to approve (uint256). Typically MaxUint.", + }, + nonce: { + type: "string", + pattern: "^[0-9]+$", + description: "The current nonce of the sender.", + }, + deadline: { + type: "string", + pattern: "^[0-9]+$", + description: "The timestamp at which the signature expires.", + }, + signature: { + type: "string", + pattern: "^0x[a-fA-F0-9]+$", + description: "The 65-byte concatenated signature (r, s, v) as a hex string.", + }, + version: { + type: "string", + pattern: "^[0-9]+(\\.[0-9]+)*$", + description: "Schema version identifier.", + }, + }, + required: ["from", "asset", "spender", "amount", "nonce", "deadline", "signature", "version"], +}; + +/** + * Declares the EIP-2612 gas sponsoring extension for inclusion in + * PaymentRequired.extensions. + * + * The server advertises that it (or its facilitator) supports EIP-2612 + * gasless Permit2 approval. The client will populate the info with the + * actual permit signature data. + * + * @returns An object keyed by the extension identifier containing the extension declaration + * + * @example + * ```typescript + * import { declareEip2612GasSponsoringExtension } from '@x402/extensions'; + * + * const routes = [ + * { + * path: "/api/data", + * price: "$0.01", + * extensions: { + * ...declareEip2612GasSponsoringExtension(), + * }, + * }, + * ]; + * ``` + */ +export function declareEip2612GasSponsoringExtension(): Record< + string, + Eip2612GasSponsoringExtension +> { + const key = EIP2612_GAS_SPONSORING.key; + return { + [key]: { + info: { + description: + "The facilitator accepts EIP-2612 gasless Permit to `Permit2` canonical contract.", + version: "1", + }, + schema: eip2612GasSponsoringSchema, + }, + }; +} diff --git a/typescript/packages/extensions/src/eip2612-gas-sponsoring/types.ts b/typescript/packages/extensions/src/eip2612-gas-sponsoring/types.ts new file mode 100644 index 0000000..81c414e --- /dev/null +++ b/typescript/packages/extensions/src/eip2612-gas-sponsoring/types.ts @@ -0,0 +1,65 @@ +/** + * Type definitions for the EIP-2612 Gas Sponsoring Extension + * + * This extension enables gasless approval of the Permit2 contract for tokens + * that implement EIP-2612. The client signs an off-chain permit, and the + * facilitator submits it on-chain via `x402Permit2Proxy.settleWithPermit`. + */ + +import type { FacilitatorExtension } from "@x402/core/types"; + +/** + * Extension identifier for the EIP-2612 gas sponsoring extension. + */ +export const EIP2612_GAS_SPONSORING: FacilitatorExtension = { key: "eip2612GasSponsoring" }; + +/** + * EIP-2612 gas sponsoring info populated by the client. + * + * Contains the EIP-2612 permit signature and parameters that the facilitator + * needs to call `x402Permit2Proxy.settleWithPermit`. + */ +export interface Eip2612GasSponsoringInfo { + /** Index signature for compatibility with Record */ + [key: string]: unknown; + /** The address of the sender (token owner). */ + from: string; + /** The address of the ERC-20 token contract. */ + asset: string; + /** The address of the spender (Canonical Permit2). */ + spender: string; + /** The amount to approve (uint256 as decimal string). Typically MaxUint256. */ + amount: string; + /** The current EIP-2612 nonce of the sender (decimal string). */ + nonce: string; + /** The timestamp at which the permit signature expires (decimal string). */ + deadline: string; + /** The 65-byte concatenated EIP-2612 permit signature (r, s, v) as a hex string. */ + signature: string; + /** Schema version identifier. */ + version: string; +} + +/** + * Server-side EIP-2612 gas sponsoring info included in PaymentRequired. + * Contains a description and version; the client populates the rest. + */ +export interface Eip2612GasSponsoringServerInfo { + /** Index signature for compatibility with Record */ + [key: string]: unknown; + /** Human-readable description of the extension. */ + description: string; + /** Schema version identifier. */ + version: string; +} + +/** + * The full extension object as it appears in PaymentRequired.extensions + * and PaymentPayload.extensions. + */ +export interface Eip2612GasSponsoringExtension { + /** Extension info - server-provided or client-enriched. */ + info: Eip2612GasSponsoringServerInfo | Eip2612GasSponsoringInfo; + /** JSON Schema describing the expected structure of info. */ + schema: Record; +} diff --git a/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/facilitator.ts b/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/facilitator.ts new file mode 100644 index 0000000..9f9208b --- /dev/null +++ b/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/facilitator.ts @@ -0,0 +1,78 @@ +/** + * Facilitator functions for extracting and validating ERC-20 Approval Gas Sponsoring + * extension data. + * + * These functions help facilitators extract the pre-signed approve() transaction + * from payment payloads and validate it before broadcasting and settling. + */ + +import Ajv from "ajv/dist/2020.js"; +import type { PaymentPayload } from "@x402/core/types"; +import { + ERC20_APPROVAL_GAS_SPONSORING, + type Erc20ApprovalGasSponsoringInfo, + type Erc20ApprovalGasSponsoringExtension, +} from "./types"; +import { erc20ApprovalGasSponsoringSchema } from "./resourceService"; + +/** + * Extracts the ERC-20 approval gas sponsoring info from a payment payload's extensions. + * + * Performs structural extraction only — checks that the extension is present and + * contains all required fields. Does NOT validate field formats (use + * validateErc20ApprovalGasSponsoringInfo for that). + * + * @param paymentPayload - The payment payload to extract from + * @returns The ERC-20 approval gas sponsoring info, or null if not present + */ +export function extractErc20ApprovalGasSponsoringInfo( + paymentPayload: PaymentPayload, +): Erc20ApprovalGasSponsoringInfo | null { + if (!paymentPayload.extensions) { + return null; + } + + const extension = paymentPayload.extensions[ERC20_APPROVAL_GAS_SPONSORING.key] as + | Erc20ApprovalGasSponsoringExtension + | undefined; + + if (!extension?.info) { + return null; + } + + const info = extension.info as Record; + + if ( + !info.from || + !info.asset || + !info.spender || + !info.amount || + !info.signedTransaction || + !info.version + ) { + return null; + } + + return info as unknown as Erc20ApprovalGasSponsoringInfo; +} + +/** + * Validates that the ERC-20 approval gas sponsoring info has valid format. + * + * Validates the info against the canonical JSON Schema, checking: + * - All required fields are present + * - Addresses are valid hex (0x + 40 hex chars) + * - Amount is a numeric string + * - signedTransaction is a hex string + * - Version is a numeric version string + * + * @param info - The ERC-20 approval gas sponsoring info to validate + * @returns True if the info is valid, false otherwise + */ +export function validateErc20ApprovalGasSponsoringInfo( + info: Erc20ApprovalGasSponsoringInfo, +): boolean { + const ajv = new Ajv({ strict: false, allErrors: true }); + const validate = ajv.compile(erc20ApprovalGasSponsoringSchema); + return validate(info) as boolean; +} diff --git a/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/index.ts b/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/index.ts new file mode 100644 index 0000000..d12339b --- /dev/null +++ b/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/index.ts @@ -0,0 +1,67 @@ +/** + * ERC-20 Approval Gas Sponsoring Extension for x402 + * + * Enables gasless Permit2 approval for generic ERC-20 tokens that do NOT + * implement EIP-2612. The client signs (but does not broadcast) a raw + * `approve(Permit2, MaxUint256)` transaction, and the facilitator broadcasts + * it atomically before settling the Permit2 payment. + * + * ## For Resource Servers + * + * ```typescript + * import { declareErc20ApprovalGasSponsoringExtension } from '@x402/extensions'; + * + * const routes = [ + * { + * path: "/api/data", + * price: { amount: "1000", asset: "0x...", extra: { assetTransferMethod: "permit2" } }, + * extensions: { + * ...declareErc20ApprovalGasSponsoringExtension(), + * }, + * }, + * ]; + * ``` + * + * ## For Facilitators + * + * ```typescript + * import { + * extractErc20ApprovalGasSponsoringInfo, + * validateErc20ApprovalGasSponsoringInfo, + * } from '@x402/extensions'; + * + * const info = extractErc20ApprovalGasSponsoringInfo(paymentPayload); + * if (info && validateErc20ApprovalGasSponsoringInfo(info)) { + * // Broadcast the pre-signed approve tx, then call standard settle() + * } + * ``` + */ + +// Export types +export type { + TransactionRequest, + Erc20ApprovalGasSponsoringInfo, + Erc20ApprovalGasSponsoringServerInfo, + Erc20ApprovalGasSponsoringExtension, + Erc20ApprovalGasSponsoringSigner, + Erc20ApprovalGasSponsoringBaseSigner, + Erc20ApprovalGasSponsoringFacilitatorExtension, +} from "./types"; + +export { + ERC20_APPROVAL_GAS_SPONSORING, + ERC20_APPROVAL_GAS_SPONSORING_VERSION, + createErc20ApprovalGasSponsoringExtension, +} from "./types"; + +// Export resource service functions (for servers) +export { + declareErc20ApprovalGasSponsoringExtension, + erc20ApprovalGasSponsoringSchema, +} from "./resourceService"; + +// Export facilitator functions +export { + extractErc20ApprovalGasSponsoringInfo, + validateErc20ApprovalGasSponsoringInfo, +} from "./facilitator"; diff --git a/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/resourceService.ts b/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/resourceService.ts new file mode 100644 index 0000000..d93db64 --- /dev/null +++ b/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/resourceService.ts @@ -0,0 +1,97 @@ +/** + * Resource Service functions for declaring the ERC-20 Approval Gas Sponsoring extension. + * + * These functions help servers declare support for ERC-20 approval gas sponsoring + * in the PaymentRequired response extensions. Use this for tokens that do NOT + * implement EIP-2612 (generic ERC-20 tokens). + */ + +import { + ERC20_APPROVAL_GAS_SPONSORING, + ERC20_APPROVAL_GAS_SPONSORING_VERSION, + type Erc20ApprovalGasSponsoringExtension, +} from "./types"; + +/** + * The JSON Schema for the ERC-20 approval gas sponsoring extension info. + * Matches the schema defined in the spec. + */ +export const erc20ApprovalGasSponsoringSchema: Record = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + from: { + type: "string", + pattern: "^0x[a-fA-F0-9]{40}$", + description: "The address of the sender (token owner).", + }, + asset: { + type: "string", + pattern: "^0x[a-fA-F0-9]{40}$", + description: "The address of the ERC-20 token contract.", + }, + spender: { + type: "string", + pattern: "^0x[a-fA-F0-9]{40}$", + description: "The address of the spender (Canonical Permit2).", + }, + amount: { + type: "string", + pattern: "^[0-9]+$", + description: "The amount approved (uint256). Always MaxUint256.", + }, + signedTransaction: { + type: "string", + pattern: "^0x[a-fA-F0-9]+$", + description: "The RLP-encoded signed EIP-1559 transaction as a hex string.", + }, + version: { + type: "string", + pattern: "^[0-9]+(\\.[0-9]+)*$", + description: "Schema version identifier.", + }, + }, + required: ["from", "asset", "spender", "amount", "signedTransaction", "version"], +}; + +/** + * Declares the ERC-20 approval gas sponsoring extension for inclusion in + * PaymentRequired.extensions. + * + * The server advertises that it (or its facilitator) supports broadcasting + * a pre-signed `approve(Permit2, MaxUint256)` transaction on the client's behalf. + * Use this for tokens that do NOT implement EIP-2612. + * + * @returns An object keyed by the extension identifier containing the extension declaration + * + * @example + * ```typescript + * import { declareErc20ApprovalGasSponsoringExtension } from '@x402/extensions'; + * + * const routes = [ + * { + * path: "/api/data", + * price: { amount: "1000", asset: "0x...", extra: { assetTransferMethod: "permit2" } }, + * extensions: { + * ...declareErc20ApprovalGasSponsoringExtension(), + * }, + * }, + * ]; + * ``` + */ +export function declareErc20ApprovalGasSponsoringExtension(): Record< + string, + Erc20ApprovalGasSponsoringExtension +> { + const key = ERC20_APPROVAL_GAS_SPONSORING.key; + return { + [key]: { + info: { + description: + "The facilitator broadcasts a pre-signed ERC-20 approve() transaction to grant Permit2 allowance.", + version: ERC20_APPROVAL_GAS_SPONSORING_VERSION, + }, + schema: erc20ApprovalGasSponsoringSchema, + }, + }; +} diff --git a/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/types.ts b/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/types.ts new file mode 100644 index 0000000..5876aa7 --- /dev/null +++ b/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/types.ts @@ -0,0 +1,166 @@ +/** + * Type definitions for the ERC-20 Approval Gas Sponsoring Extension + * + * This extension enables gasless Permit2 approval for generic ERC-20 tokens + * that do NOT implement EIP-2612. The client signs (but does not broadcast) a + * raw `approve(Permit2, MaxUint256)` transaction, and the facilitator broadcasts + * it atomically before settling the Permit2 payment. + */ + +import type { FacilitatorExtension } from "@x402/core/types"; + +/** + * A single transaction to be executed by the signer. + * - `0x${string}`: a pre-signed serialized transaction (broadcast as-is via sendRawTransaction) + * - `{ to, data, gas? }`: an unsigned call intent (signer signs and broadcasts) + */ +export type TransactionRequest = + | `0x${string}` + | { to: `0x${string}`; data: `0x${string}`; gas?: bigint }; + +/** + * Signer capability carried by the ERC-20 approval extension when registered in a facilitator. + * + * Mirrors FacilitatorEvmSigner (from @x402/evm) plus `sendTransactions`. + * The signer owns execution of multiple transactions, enabling production implementations + * to bundle them atomically (e.g., Flashbots, multicall, smart account batching) + * while simpler implementations can execute them sequentially. + * + * The method signatures are duplicated here (rather than extending FacilitatorEvmSigner) + * to avoid a circular dependency between @x402/extensions and @x402/evm. + */ +export interface Erc20ApprovalGasSponsoringSigner { + getAddresses(): readonly `0x${string}`[]; + readContract(args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; + }): Promise; + verifyTypedData(args: { + address: `0x${string}`; + domain: Record; + types: Record; + primaryType: string; + message: Record; + signature: `0x${string}`; + }): Promise; + writeContract(args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args: readonly unknown[]; + gas?: bigint; + }): Promise<`0x${string}`>; + sendTransaction(args: { to: `0x${string}`; data: `0x${string}` }): Promise<`0x${string}`>; + waitForTransactionReceipt(args: { hash: `0x${string}` }): Promise<{ status: string }>; + getCode(args: { address: `0x${string}` }): Promise<`0x${string}` | undefined>; + sendTransactions(transactions: TransactionRequest[]): Promise<`0x${string}`[]>; +} + +/** + * Extension identifier for the ERC-20 approval gas sponsoring extension. + */ +export const ERC20_APPROVAL_GAS_SPONSORING = { + key: "erc20ApprovalGasSponsoring", +} as const satisfies FacilitatorExtension; + +/** Current schema version for the ERC-20 approval gas sponsoring extension info. */ +export const ERC20_APPROVAL_GAS_SPONSORING_VERSION = "1"; + +/** + * Extended extension object registered in a facilitator via registerExtension(). + * Carries the signer that owns the full approve+settle flow for ERC-20 tokens + * that lack EIP-2612. + * + * @example + * ```typescript + * import { createErc20ApprovalGasSponsoringExtension } from '@x402/extensions'; + * + * facilitator.registerExtension( + * createErc20ApprovalGasSponsoringExtension(signer), + * ); + * ``` + */ +export interface Erc20ApprovalGasSponsoringFacilitatorExtension extends FacilitatorExtension { + key: "erc20ApprovalGasSponsoring"; + /** Default signer with approve+settle capability. Optional — settlement fails gracefully if absent. */ + signer?: Erc20ApprovalGasSponsoringSigner; + /** Network-specific signer resolver. Takes precedence over `signer` when provided. */ + signerForNetwork?: (network: string) => Erc20ApprovalGasSponsoringSigner | undefined; +} + +/** + * Base signer shape without `sendTransactions`. + * Matches the FacilitatorEvmSigner shape from @x402/evm (duplicated to avoid circular dep). + */ +export type Erc20ApprovalGasSponsoringBaseSigner = Omit< + Erc20ApprovalGasSponsoringSigner, + "sendTransactions" +>; + +/** + * Create an ERC-20 approval gas sponsoring extension ready to register in a facilitator. + * + * @param signer - A complete signer with `sendTransactions` already implemented. + * The signer decides how to execute the transactions (sequentially, batched, or atomically). + * @param signerForNetwork - Optional network-specific signer resolver. When provided, + * takes precedence over `signer` and allows different settlement signers per network. + * @returns A fully configured extension to pass to `facilitator.registerExtension()` + */ +export function createErc20ApprovalGasSponsoringExtension( + signer: Erc20ApprovalGasSponsoringSigner, + signerForNetwork?: (network: string) => Erc20ApprovalGasSponsoringSigner | undefined, +): Erc20ApprovalGasSponsoringFacilitatorExtension { + return { ...ERC20_APPROVAL_GAS_SPONSORING, signer, signerForNetwork }; +} + +/** + * ERC-20 approval gas sponsoring info populated by the client. + * + * Contains the RLP-encoded signed `approve(Permit2, MaxUint256)` transaction + * that the facilitator broadcasts before settling the Permit2 payment. + * + * Note: Unlike EIP-2612, there is no nonce/deadline/signature — instead the + * entire signed transaction is included as `signedTransaction`. + */ +export interface Erc20ApprovalGasSponsoringInfo { + /** Index signature for compatibility with Record */ + [key: string]: unknown; + /** The address of the sender (token owner who signed the tx). */ + from: `0x${string}`; + /** The address of the ERC-20 token contract. */ + asset: `0x${string}`; + /** The address of the spender (Canonical Permit2). */ + spender: `0x${string}`; + /** The amount approved (uint256 as decimal string). Always MaxUint256. */ + amount: string; + /** The RLP-encoded signed EIP-1559 transaction as a hex string. */ + signedTransaction: `0x${string}`; + /** Schema version identifier. */ + version: string; +} + +/** + * Server-side ERC-20 approval gas sponsoring info included in PaymentRequired. + * Contains a description and version; the client populates the rest. + */ +export interface Erc20ApprovalGasSponsoringServerInfo { + /** Index signature for compatibility with Record */ + [key: string]: unknown; + /** Human-readable description of the extension. */ + description: string; + /** Schema version identifier. */ + version: string; +} + +/** + * The full extension object as it appears in PaymentRequired.extensions + * and PaymentPayload.extensions. + */ +export interface Erc20ApprovalGasSponsoringExtension { + /** Extension info - server-provided or client-enriched. */ + info: Erc20ApprovalGasSponsoringServerInfo | Erc20ApprovalGasSponsoringInfo; + /** JSON Schema describing the expected structure of info. */ + schema: Record; +} diff --git a/typescript/packages/extensions/src/index.ts b/typescript/packages/extensions/src/index.ts index f51b8a8..a8e18bf 100644 --- a/typescript/packages/extensions/src/index.ts +++ b/typescript/packages/extensions/src/index.ts @@ -8,6 +8,15 @@ export { bazaarResourceServerExtension } from "./bazaar/server"; // Sign-in-with-x extension export * from "./sign-in-with-x"; +// Offer/Receipt extension +export * from "./offer-receipt"; + // Payment-identifier extension export * from "./payment-identifier"; export { paymentIdentifierResourceServerExtension } from "./payment-identifier/resourceServer"; + +// EIP-2612 Gas Sponsoring extension +export * from "./eip2612-gas-sponsoring"; + +// ERC-20 Approval Gas Sponsoring extension +export * from "./erc20-approval-gas-sponsoring"; diff --git a/typescript/packages/extensions/src/offer-receipt/README.md b/typescript/packages/extensions/src/offer-receipt/README.md new file mode 100644 index 0000000..d1e785a --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/README.md @@ -0,0 +1,218 @@ +# x402 Offer/Receipt Extension + +Enables signed offers and receipts for the x402 payment protocol (v1.0). + +## Overview + +``` +┌─────────┐ ┌─────────────────┐ ┌─────────────┐ +│ Client │ │ Resource Server │ │ Facilitator │ +└────┬────┘ └────────┬────────┘ └──────┬──────┘ + │ │ │ + │ GET /resource │ │ + │ ──────────────────────────────────►│ │ + │ │ │ + │ 402 + PaymentRequirements │ │ + │ + SignedOffer(s) │ │ + │ ◄──────────────────────────────────│ │ + │ │ │ + │ GET /resource + Payment Header │ │ + │ ──────────────────────────────────►│ │ + │ │ │ + │ │ Verify + Settle │ + │ │ ────────────────────────────────────►│ + │ │ │ + │ │ Settlement Response │ + │ │ ◄────────────────────────────────────│ + │ │ │ + │ 200 + Resource + SignedReceipt │ │ + │ ◄──────────────────────────────────│ │ + │ │ │ +``` + +The **Offer** is signed by the resource server and included in the 402 response. Each `accepts[]` entry has a corresponding signed offer, proving those specific payment requirements are authentic. + +The **Receipt** is signed by the resource server after successful payment and included in the success response. It proves service was delivered. + +## Why Receipts? + +Receipts are **portable proofs of paid service**. They enable: + +- **Verified user reviews**: Like a "Verified Purchase" badge +- **Audit trails**: Cryptographic proof of service delivery +- **Dispute resolution**: Evidence that service was delivered after payment +- **Agent memory**: AI agents can prove past interactions with services + +## Why Offers? + +Signed offers: +- Give clients a fallback for proof of interaction if a signed receipt is not sent +- Prove the offer came from the resource server +- Prevent clients from creating their own offer and claiming it came from a server + +## Installation + +```bash +npm install @x402/extensions +``` + +## Server Usage + +To enable offer/receipt signing on your resource server: + +```typescript +import { x402ResourceServer } from "@x402/core/server"; +import { + createOfferReceiptExtension, + createJWSOfferReceiptIssuer, + declareOfferReceipt, +} from "@x402/extensions/offer-receipt"; + +// Create an issuer (JWS or EIP-712) +const issuer = createJWSOfferReceiptIssuer("did:web:api.example.com#key-1", jwsSigner); + +// Register the extension +const server = new x402ResourceServer(facilitator) + .registerExtension(createOfferReceiptExtension(issuer)); + +// Declare in route config +const routes = { + "GET /api/data": { + accepts: { payTo, scheme: "exact", price: "$0.01", network: "eip155:8453" }, + extensions: { + ...declareOfferReceipt({ includeTxHash: false }) + } + } +}; +``` + +### Signature Formats + +Two formats are supported: + +- **JWS** - Best for server-side signing with managed keys (HSM, KMS, etc.) +- **EIP-712** - Best for wallet-based signing (MetaMask, WalletConnect, etc.) + +## Client Usage + +### Using wrapFetchWithPayment + +The `wrapFetchWithPayment` wrapper can be used with offers and receipts by capturing offers in the `onPaymentRequired` hook and extracting the receipt from the response. Note that this approach does not control which `accepts[]` entry is selected - the client's selector/policies determine that independently. + +```typescript +import { wrapFetchWithPayment, x402Client, x402HTTPClient } from "@x402/fetch"; +import { registerExactEvmScheme } from "@x402/evm/exact/client"; +import { registerExactSvmScheme } from "@x402/svm/exact/client"; +import { privateKeyToAccount } from "viem/accounts"; +import { createKeyPairSignerFromBytes } from "@solana/kit"; +import { base58 } from "@scure/base"; +import { + extractOffersFromPaymentRequired, + decodeSignedOffers, + extractReceiptFromResponse, + type DecodedOffer, +} from "@x402/extensions/offer-receipt"; + +// Set up signers +const evmSigner = privateKeyToAccount(evmPrivateKey); +const svmSigner = await createKeyPairSignerFromBytes(base58.decode(svmPrivateKey)); + +// Configure x402 client +const client = new x402Client(); +registerExactEvmScheme(client, { signer: evmSigner }); +registerExactSvmScheme(client, { signer: svmSigner }); + +const httpClient = new x402HTTPClient(client); + +// Store offers for later matching with receipt +let capturedOffers: DecodedOffer[] = []; + +// Capture offers in onPaymentRequired hook +httpClient.onPaymentRequired(async ({ paymentRequired }) => { + const offers = extractOffersFromPaymentRequired(paymentRequired); + capturedOffers = decodeSignedOffers(offers); +}); + +// Create payment-enabled fetch +const fetchWithPay = wrapFetchWithPayment(fetch, httpClient); + +// Make request (payment handled automatically) +const response = await fetchWithPay(url); + +// Extract receipt from response headers +const receipt = extractReceiptFromResponse(response); + +// Match receipt to captured offer using receipt payload fields +// (receipt contains network, amount, etc. to identify which offer was accepted) +``` + +### Raw Flow + +For full control over offer selection, use the raw flow. See the [Offer/Receipt Example](../../../../../examples/typescript/clients/offer-receipt/) for a complete working implementation. + +The example demonstrates: +1. Making a request and receiving a 402 with signed offers +2. Extracting and decoding offers to inspect payment options +3. Selecting an offer and finding the matching `accepts[]` entry +4. Making payment and receiving a signed receipt +5. Verifying the receipt payload + +### Future: wrapFetchWithPaymentExtended + +We may add a `wrapFetchWithPaymentExtended` wrapper that selects payment options based on signed offers rather than the `accepts[]` array directly. This would guarantee that the selected payment option has a corresponding signed offer, which is the correct approach when attestation proofs are important. + +## Using Receipts as Proofs + +Signed receipts serve as cryptographic proofs of commercial transactions. These proofs can be submitted to downstream trust and reputation platforms: + +- **[OMATrust](https://github.com/oma3dao/omatrust-docs)** - Decentralized reputation system for verified user reviews and service attestations +- **[PEAC Protocol](https://github.com/peacprotocol/peac)** - Payment Evidence and Attestation Chain for commercial transaction proofs + +Integration libraries for these platforms will be added in future releases. + +## Payload Structure + +For detailed payload field definitions, see the [Extension Specification](../../../../../specs/extensions/extension-offer-and-receipt.md): +- §4.2 Offer Payload Fields +- §5.2 Receipt Payload Fields + +## Security Considerations + +The `extractPayload()` functions extract payloads without verifying the signature or checking signer authorization. This is by design — signer authorization requires resolving key bindings (did:web documents, attestations, etc.) which varies by deployment and is outside the scope of x402 client utilities. + +For production use, downstream trust systems verify: +1. The signature is valid (EIP-712 or JWS) +2. The signing key is authorized for the resource domain + +### Key-to-Domain Binding + +To establish trust, bind the signing key's DID to the resource domain using: + +1. **`did:web` DID Document** - Serve at `https://example.com/.well-known/did.json` +2. **DNS TXT Record** - Add a TXT record binding a DID to the domain +3. **Key Binding Attestation** - Create an attestation specifying the key's purpose and authorized domain + +### Key Management + +For production deployments: + +- **JWS signing**: Use HSM or KMS-backed keys. The `kid` in the JWS header should be a DID URL that resolves to the public key. +- **EIP-712 signing**: The signing wallet should be the `payTo` address, or have an on-chain/off-chain authorization linking it to the service. +- **Key rotation**: Update DID documents or attestations when rotating keys. Old receipts remain valid if the key was authorized at issuance time. + +## Files + +| File | Description | +|------|-------------| +| [types.ts](./types.ts) | Type definitions for offers, receipts, and signers | +| [signing.ts](./signing.ts) | Signing utilities and offer/receipt creation | +| [server.ts](./server.ts) | Server extension and signer factories | +| [client.ts](./client.ts) | Client-side extraction utilities | + +## Examples + +- [Offer/Receipt Client Example](../../../../../examples/typescript/clients/offer-receipt/) - Complete example showing offer/receipt extraction + +## Related + +- [Extension Specification](../../../../../specs/extensions/extension-offer-and-receipt.md) diff --git a/typescript/packages/extensions/src/offer-receipt/client.ts b/typescript/packages/extensions/src/offer-receipt/client.ts new file mode 100644 index 0000000..0ad22ad --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/client.ts @@ -0,0 +1,193 @@ +/** + * Client-side utilities for extracting offers and receipts from x402 responses + * + * Provides utilities for clients who want to access signed offers and receipts + * from x402 payment flows. Useful for verified reviews, audit trails, and dispute resolution. + * + * @see README.md for usage examples (raw and wrapper flows) + * @see examples/typescript/clients/offer-receipt/ for complete example + */ + +import { decodePaymentResponseHeader } from "@x402/core/http"; +import type { PaymentRequired, PaymentRequirements, SettleResponse } from "@x402/core/types"; +import { OFFER_RECEIPT, type OfferPayload, type SignedOffer, type SignedReceipt } from "./types"; +import { extractOfferPayload, extractReceiptPayload } from "./signing"; + +/** + * A signed offer with its decoded payload fields at the top level. + * Combines the signed offer metadata with the decoded payload for easy access. + */ +export interface DecodedOffer extends OfferPayload { + /** The original signed offer (for passing to other functions or downstream systems) */ + signedOffer: SignedOffer; + /** The signature format used */ + format: "jws" | "eip712"; + /** Index into accepts[] array (hint for matching), may be undefined */ + acceptIndex?: number; +} + +/** + * Structure of offer-receipt extension data in PaymentRequired.extensions + */ +interface OfferReceiptExtensionInfo { + info?: { + offers?: SignedOffer[]; + receipt?: SignedReceipt; + }; +} + +// ============================================================================ +// Exported Functions +// ============================================================================ + +/** + * Verify that a receipt's payload matches the offer and payer. + * + * This performs basic payload field verification: + * - resourceUrl matches the offer + * - network matches the offer + * - payer matches one of the client's wallet addresses + * - issuedAt is recent (within maxAgeSeconds) + * + * NOTE: This does NOT verify the signature or key binding. See the comment + * in the offer-receipt example for guidance on full verification. + * + * @param receipt - The signed receipt from the server + * @param offer - The decoded offer that was accepted + * @param payerAddresses - Array of the client's wallet addresses (EVM, SVM, etc.) + * @param maxAgeSeconds - Maximum age of receipt in seconds (default: 3600 = 1 hour) + * @returns true if all checks pass, false otherwise + */ +export function verifyReceiptMatchesOffer( + receipt: SignedReceipt, + offer: DecodedOffer, + payerAddresses: string[], + maxAgeSeconds: number = 3600, +): boolean { + const payload = extractReceiptPayload(receipt); + + const resourceUrlMatch = payload.resourceUrl === offer.resourceUrl; + const networkMatch = payload.network === offer.network; + const payerMatch = payerAddresses.some( + addr => payload.payer.toLowerCase() === addr.toLowerCase(), + ); + const issuedRecently = Math.floor(Date.now() / 1000) - payload.issuedAt < maxAgeSeconds; + + return resourceUrlMatch && networkMatch && payerMatch && issuedRecently; +} + +/** + * Extract signed offers from a PaymentRequired response. + * + * Call this immediately after receiving a 402 response to save the offers. + * If the settlement response doesn't include a receipt, you'll still have + * the offers for attestation purposes. + * + * @param paymentRequired - The PaymentRequired object from the 402 response + * @returns Array of signed offers, or empty array if none present + */ +export function extractOffersFromPaymentRequired(paymentRequired: PaymentRequired): SignedOffer[] { + const extData = paymentRequired.extensions?.[OFFER_RECEIPT] as + | OfferReceiptExtensionInfo + | undefined; + return extData?.info?.offers ?? []; +} + +/** + * Decode all signed offers and return them with payload fields at the top level. + * + * Use this to inspect offer details (network, amount, etc.) for selection. + * JWS decoding is cheap (base64 decode, no crypto), so decoding all offers + * upfront is fine even with multiple offers. + * + * @param offers - Array of signed offers from extractOffersFromPaymentRequired + * @returns Array of decoded offers with payload fields at top level + */ +export function decodeSignedOffers(offers: SignedOffer[]): DecodedOffer[] { + return offers.map(offer => { + const payload = extractOfferPayload(offer); + return { + // Spread payload fields at top level + ...payload, + // Include metadata + signedOffer: offer, + format: offer.format, + acceptIndex: offer.acceptIndex, + }; + }); +} + +/** + * Find the accepts[] entry that matches a signed or decoded offer. + * + * Use this after selecting an offer to get the PaymentRequirements + * object needed for createPaymentPayload. + * + * Uses the offer's acceptIndex as a hint for faster lookup, but verifies + * the payload matches in case indices got out of sync. + * + * @param offer - A DecodedOffer (from decodeSignedOffers) or SignedOffer + * @param accepts - Array of payment requirements from paymentRequired.accepts + * @returns The matching PaymentRequirements, or undefined if not found + */ +export function findAcceptsObjectFromSignedOffer( + offer: DecodedOffer | SignedOffer, + accepts: PaymentRequirements[], +): PaymentRequirements | undefined { + // Check if it's a DecodedOffer (has signedOffer property) or SignedOffer + const isDecoded = "signedOffer" in offer; + const payload = isDecoded ? offer : extractOfferPayload(offer); + const acceptIndex = isDecoded ? offer.acceptIndex : offer.acceptIndex; + + // Use acceptIndex as a hint - check that index first + if (acceptIndex !== undefined && acceptIndex < accepts.length) { + const hinted = accepts[acceptIndex]; + if ( + hinted.network === payload.network && + hinted.scheme === payload.scheme && + hinted.asset === payload.asset && + hinted.payTo === payload.payTo && + hinted.amount === payload.amount + ) { + return hinted; + } + } + + // Fall back to searching all accepts + return accepts.find( + req => + req.network === payload.network && + req.scheme === payload.scheme && + req.asset === payload.asset && + req.payTo === payload.payTo && + req.amount === payload.amount, + ); +} + +/** + * Extract signed receipt from a successful payment response. + * + * Call this after a successful payment to get the server's signed receipt. + * The receipt proves the service was delivered after payment. + * + * @param response - The Response object from the successful request + * @returns The signed receipt, or undefined if not present + */ +export function extractReceiptFromResponse(response: Response): SignedReceipt | undefined { + const paymentResponseHeader = + response.headers.get("PAYMENT-RESPONSE") || response.headers.get("X-PAYMENT-RESPONSE"); + + if (!paymentResponseHeader) { + return undefined; + } + + try { + const settlementResponse = decodePaymentResponseHeader(paymentResponseHeader) as SettleResponse; + const receiptExtData = settlementResponse.extensions?.[OFFER_RECEIPT] as + | OfferReceiptExtensionInfo + | undefined; + return receiptExtData?.info?.receipt; + } catch { + return undefined; + } +} diff --git a/typescript/packages/extensions/src/offer-receipt/did.ts b/typescript/packages/extensions/src/offer-receipt/did.ts new file mode 100644 index 0000000..3984417 --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/did.ts @@ -0,0 +1,252 @@ +/** + * DID Resolution Utilities + * + * Extracts public keys from DID key identifiers. Supports did:key, did:jwk, did:web. + * Uses @noble/curves and @scure/base for cryptographic operations. + */ + +import * as jose from "jose"; +import { base58 } from "@scure/base"; +import { secp256k1 } from "@noble/curves/secp256k1"; +import { p256 } from "@noble/curves/nist"; + +// Multicodec prefixes for supported key types +const MULTICODEC_ED25519_PUB = 0xed; +const MULTICODEC_SECP256K1_PUB = 0xe7; +const MULTICODEC_P256_PUB = 0x1200; + +/** + * Extract a public key from a DID key identifier (kid). + * Supports did:key, did:jwk, did:web. + * + * @param kid - The key identifier (DID URL, e.g., did:key:z6Mk..., did:web:example.com#key-1) + * @returns The extracted public key + */ +export async function extractPublicKeyFromKid(kid: string): Promise { + const [didPart, fragment] = kid.split("#"); + const parts = didPart.split(":"); + + if (parts.length < 3 || parts[0] !== "did") { + throw new Error(`Invalid DID format: ${kid}`); + } + + const method = parts[1]; + const identifier = parts.slice(2).join(":"); + + switch (method) { + case "key": + return extractKeyFromDidKey(identifier); + case "jwk": + return extractKeyFromDidJwk(identifier); + case "web": + return resolveDidWeb(identifier, fragment); + default: + throw new Error( + `Unsupported DID method "${method}". Supported: did:key, did:jwk, did:web. ` + + `Provide the public key directly for other methods.`, + ); + } +} + +/** + * Extract public key from did:key identifier (multibase-encoded) + * + * @param identifier - The did:key identifier (without the "did:key:" prefix) + * @returns The extracted public key + */ +async function extractKeyFromDidKey(identifier: string): Promise { + if (!identifier.startsWith("z")) { + throw new Error(`Unsupported multibase encoding. Expected 'z' (base58-btc).`); + } + + const decoded = base58.decode(identifier.slice(1)); + const { codec, keyBytes } = readMulticodec(decoded); + + switch (codec) { + case MULTICODEC_ED25519_PUB: + return importAsymmetricJWK({ + kty: "OKP", + crv: "Ed25519", + x: jose.base64url.encode(keyBytes), + }); + + case MULTICODEC_SECP256K1_PUB: { + const point = secp256k1.Point.fromHex(keyBytes); + const uncompressed = point.toBytes(false); + return importAsymmetricJWK({ + kty: "EC", + crv: "secp256k1", + x: jose.base64url.encode(uncompressed.slice(1, 33)), + y: jose.base64url.encode(uncompressed.slice(33, 65)), + }); + } + + case MULTICODEC_P256_PUB: { + const point = p256.Point.fromHex(keyBytes); + const uncompressed = point.toBytes(false); + return importAsymmetricJWK({ + kty: "EC", + crv: "P-256", + x: jose.base64url.encode(uncompressed.slice(1, 33)), + y: jose.base64url.encode(uncompressed.slice(33, 65)), + }); + } + + default: + throw new Error( + `Unsupported key type in did:key (multicodec: 0x${codec.toString(16)}). ` + + `Supported: Ed25519, secp256k1, P-256.`, + ); + } +} + +/** + * Extract public key from did:jwk identifier (base64url-encoded JWK) + * + * @param identifier - The did:jwk identifier (without the "did:jwk:" prefix) + * @returns The extracted public key + */ +async function extractKeyFromDidJwk(identifier: string): Promise { + const jwkJson = new TextDecoder().decode(jose.base64url.decode(identifier)); + const jwk = JSON.parse(jwkJson) as jose.JWK; + return importAsymmetricJWK(jwk); +} + +/** + * Resolve did:web by fetching DID document from .well-known/did.json + * + * @param identifier - The did:web identifier (without the "did:web:" prefix) + * @param fragment - Optional fragment to identify specific key + * @returns The extracted public key + */ +async function resolveDidWeb(identifier: string, fragment?: string): Promise { + const parts = identifier.split(":"); + const domain = decodeURIComponent(parts[0]); + const path = parts.slice(1).map(decodeURIComponent).join("/"); + + // did:web spec allows HTTP for localhost (https://w3c-ccg.github.io/did-method-web/#read-resolve) + const host = domain.split(":")[0]; + const scheme = host === "localhost" || host === "127.0.0.1" ? "http" : "https"; + + const url = path + ? `${scheme}://${domain}/${path}/did.json` + : `${scheme}://${domain}/.well-known/did.json`; + + let didDocument: DIDDocument; + try { + const response = await fetch(url, { + headers: { Accept: "application/did+json, application/json" }, + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + didDocument = (await response.json()) as DIDDocument; + } catch (error) { + throw new Error( + `Failed to resolve did:web:${identifier}: ${error instanceof Error ? error.message : error}`, + ); + } + + const fullDid = `did:web:${identifier}`; + const keyId = fragment ? `${fullDid}#${fragment}` : undefined; + const method = findVerificationMethod(didDocument, keyId); + + if (!method) { + throw new Error(`No verification method found for ${keyId || fullDid}`); + } + + if (method.publicKeyJwk) { + return importAsymmetricJWK(method.publicKeyJwk); + } + if (method.publicKeyMultibase) { + return extractKeyFromDidKey(method.publicKeyMultibase); + } + + throw new Error(`Verification method ${method.id} has no supported key format`); +} + +/** + * Read multicodec varint prefix from bytes + * + * @param bytes - The encoded bytes + * @returns The codec identifier and remaining key bytes + */ +function readMulticodec(bytes: Uint8Array): { codec: number; keyBytes: Uint8Array } { + let codec = 0; + let shift = 0; + let offset = 0; + + for (const byte of bytes) { + codec |= (byte & 0x7f) << shift; + offset++; + if ((byte & 0x80) === 0) break; + shift += 7; + } + + return { codec, keyBytes: bytes.slice(offset) }; +} + +/** + * Import an asymmetric JWK as a KeyLike + * + * @param jwk - The JWK to import + * @returns The imported key + */ +async function importAsymmetricJWK(jwk: jose.JWK): Promise { + const key = await jose.importJWK(jwk); + if (key instanceof Uint8Array) { + throw new Error("Symmetric keys are not supported"); + } + return key; +} + +interface DIDDocument { + id: string; + verificationMethod?: VerificationMethod[]; + assertionMethod?: (string | VerificationMethod)[]; + authentication?: (string | VerificationMethod)[]; +} + +interface VerificationMethod { + id: string; + type: string; + controller: string; + publicKeyJwk?: jose.JWK; + publicKeyMultibase?: string; +} + +/** + * Find a verification method in a DID document + * + * @param doc - The DID document + * @param keyId - Optional specific key ID to find + * @returns The verification method or undefined + */ +function findVerificationMethod(doc: DIDDocument, keyId?: string): VerificationMethod | undefined { + const methods = doc.verificationMethod || []; + + if (keyId) { + return methods.find(m => m.id === keyId); + } + + // Prefer assertionMethod, then authentication, then any + for (const ref of doc.assertionMethod || []) { + if (typeof ref === "string") { + const m = methods.find(m => m.id === ref); + if (m) return m; + } else { + return ref; + } + } + + for (const ref of doc.authentication || []) { + if (typeof ref === "string") { + const m = methods.find(m => m.id === ref); + if (m) return m; + } else { + return ref; + } + } + + return methods[0]; +} diff --git a/typescript/packages/extensions/src/offer-receipt/index.ts b/typescript/packages/extensions/src/offer-receipt/index.ts new file mode 100644 index 0000000..47af659 --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/index.ts @@ -0,0 +1,96 @@ +/** + * x402 Offer/Receipt Extension + */ + +// Types +export { + OFFER_RECEIPT, + type SignatureFormat, + type Signer, + type JWSSigner, + type EIP712Signer, + type OfferPayload, + type SignedOffer, + type JWSSignedOffer, + type EIP712SignedOffer, + type ReceiptPayload, + type SignedReceipt, + type JWSSignedReceipt, + type EIP712SignedReceipt, + type OfferReceiptDeclaration, + type OfferReceiptIssuer, + type OfferInput, + type ReceiptInput, + isJWSSignedOffer, + isEIP712SignedOffer, + isJWSSignedReceipt, + isEIP712SignedReceipt, + isJWSSigner, + isEIP712Signer, +} from "./types"; + +// Signing utilities and offer/receipt creation +export { + // Canonicalization + canonicalize, + hashCanonical, + getCanonicalBytes, + // JWS + createJWS, + extractJWSHeader, + extractJWSPayload, + // EIP-712 + createOfferDomain, + createReceiptDomain, + OFFER_TYPES, + RECEIPT_TYPES, + prepareOfferForEIP712, + prepareReceiptForEIP712, + hashOfferTypedData, + hashReceiptTypedData, + signOfferEIP712, + signReceiptEIP712, + type SignTypedDataFn, + // Network utilities + extractEIP155ChainId, + convertNetworkStringToCAIP2, + extractChainIdFromCAIP2, + // Offer creation + createOfferJWS, + createOfferEIP712, + extractOfferPayload, + // Receipt creation + createReceiptJWS, + createReceiptEIP712, + extractReceiptPayload, +} from "./signing"; + +// Server extension and factory functions +export { + createOfferReceiptExtension, + declareOfferReceiptExtension, + createJWSOfferReceiptIssuer, + createEIP712OfferReceiptIssuer, +} from "./server"; + +// Client utilities for extracting offers/receipts +export { + decodeSignedOffers, + extractOffersFromPaymentRequired, + extractReceiptFromResponse, + findAcceptsObjectFromSignedOffer, + verifyReceiptMatchesOffer, + type DecodedOffer, +} from "./client"; + +// Verification utilities (exported from signing.ts) +export { + verifyOfferSignatureEIP712, + verifyReceiptSignatureEIP712, + verifyOfferSignatureJWS, + verifyReceiptSignatureJWS, + type EIP712VerificationResult, +} from "./signing"; + +// DID resolution utilities +export { extractPublicKeyFromKid } from "./did"; diff --git a/typescript/packages/extensions/src/offer-receipt/server.ts b/typescript/packages/extensions/src/offer-receipt/server.ts new file mode 100644 index 0000000..7f954a0 --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/server.ts @@ -0,0 +1,323 @@ +/** + * Offer-Receipt Extension for x402ResourceServer + * + * This module provides the ResourceServerExtension implementation that uses + * the extension hooks (enrichPaymentRequiredResponse, enrichSettlementResponse) + * to add signed offers and receipts to x402 payment flows. + * + * Based on: x402/specs/extensions/extension-offer-and-receipt.md (v1.0) + */ + +import type { + ResourceServerExtension, + PaymentRequiredContext, + SettleResultContext, +} from "@x402/core/types"; +import type { PaymentRequirements } from "@x402/core/types"; +import type { HTTPTransportContext } from "@x402/core/http"; +import { + OFFER_RECEIPT, + type OfferReceiptIssuer, + type OfferReceiptDeclaration, + type OfferInput, + type SignedOffer, + type SignedReceipt, + type JWSSigner, +} from "./types"; +import { + createOfferJWS, + createOfferEIP712, + createReceiptJWS, + createReceiptEIP712, + type SignTypedDataFn, +} from "./signing"; + +// ============================================================================ +// JSON Schemas for Extension Responses +// ============================================================================ + +/** + * JSON Schema for offer extension data (§6.1) + */ +const OFFER_SCHEMA = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + offers: { + type: "array", + items: { + type: "object", + properties: { + format: { type: "string" }, + acceptIndex: { type: "integer" }, + payload: { + type: "object", + properties: { + version: { type: "integer" }, + resourceUrl: { type: "string" }, + scheme: { type: "string" }, + network: { type: "string" }, + asset: { type: "string" }, + payTo: { type: "string" }, + amount: { type: "string" }, + validUntil: { type: "integer" }, + }, + required: ["version", "resourceUrl", "scheme", "network", "asset", "payTo", "amount"], + }, + signature: { type: "string" }, + }, + required: ["format", "signature"], + }, + }, + }, + required: ["offers"], +}; + +/** + * JSON Schema for receipt extension data (§6.5) + */ +const RECEIPT_SCHEMA = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + receipt: { + type: "object", + properties: { + format: { type: "string" }, + payload: { + type: "object", + properties: { + version: { type: "integer" }, + network: { type: "string" }, + resourceUrl: { type: "string" }, + payer: { type: "string" }, + issuedAt: { type: "integer" }, + transaction: { type: "string" }, + }, + required: ["version", "network", "resourceUrl", "payer", "issuedAt"], + }, + signature: { type: "string" }, + }, + required: ["format", "signature"], + }, + }, + required: ["receipt"], +}; + +// ============================================================================ +// Extension Factory +// ============================================================================ + +/** + * Convert PaymentRequirements to OfferInput + * + * @param requirements - The payment requirements + * @param acceptIndex - Index into accepts[] array + * @param offerValiditySeconds - Optional validity duration override + * @returns The offer input object + */ +function requirementsToOfferInput( + requirements: PaymentRequirements, + acceptIndex: number, + offerValiditySeconds?: number, +): OfferInput { + return { + acceptIndex, + scheme: requirements.scheme, + network: requirements.network, + asset: requirements.asset, + payTo: requirements.payTo, + amount: requirements.amount, + offerValiditySeconds: offerValiditySeconds ?? requirements.maxTimeoutSeconds, + }; +} + +/** + * Creates an offer-receipt extension for use with x402ResourceServer. + * + * The extension uses the hook system to: + * 1. Add signed offers to each PaymentRequirements in 402 responses + * 2. Add signed receipts to settlement responses after successful payment + * + * @param issuer - The issuer to use for creating and signing offers and receipts + * @returns ResourceServerExtension that can be registered with x402ResourceServer + */ +export function createOfferReceiptExtension(issuer: OfferReceiptIssuer): ResourceServerExtension { + return { + key: OFFER_RECEIPT, + + // Add signed offers to 402 PaymentRequired response + enrichPaymentRequiredResponse: async ( + declaration: unknown, + context: PaymentRequiredContext, + ): Promise => { + const config = declaration as OfferReceiptDeclaration | undefined; + + // Get resource URL from transport context or payment required response + const resourceUrl = + context.paymentRequiredResponse.resource?.url || + (context.transportContext as HTTPTransportContext)?.request?.adapter?.getUrl?.(); + + if (!resourceUrl) { + console.warn("[offer-receipt] No resource URL available for signing offers"); + return undefined; + } + + // Sign offers for each payment requirement + const offers: SignedOffer[] = []; + + for (let i = 0; i < context.requirements.length; i++) { + const requirement = context.requirements[i]; + try { + const offerInput = requirementsToOfferInput(requirement, i, config?.offerValiditySeconds); + const signedOffer = await issuer.issueOffer(resourceUrl, offerInput); + offers.push(signedOffer); + } catch (error) { + console.error(`[offer-receipt] Failed to sign offer for requirement ${i}:`, error); + } + } + + if (offers.length === 0) { + return undefined; + } + + // Return extension data per spec structure + return { + info: { + offers, + }, + schema: OFFER_SCHEMA, + }; + }, + + // Add signed receipt to settlement response + enrichSettlementResponse: async ( + declaration: unknown, + context: SettleResultContext, + ): Promise => { + const config = declaration as OfferReceiptDeclaration | undefined; + + // Skip if settlement failed + if (!context.result.success) { + return undefined; + } + + // Get payer from settlement result + const payer = context.result.payer; + if (!payer) { + console.warn("[offer-receipt] No payer available for signing receipt"); + return undefined; + } + + // Get network and transaction from settlement result + const network = context.result.network; + if (!network) { + console.warn("[offer-receipt] No network available for signing receipt"); + return undefined; + } + const transaction = context.result.transaction; + + // Get resource URL from transport context + const resourceUrl = ( + context.transportContext as HTTPTransportContext + )?.request?.adapter?.getUrl?.(); + + if (!resourceUrl) { + console.warn("[offer-receipt] No resource URL available for signing receipt"); + return undefined; + } + + // Determine whether to include transaction hash (default: false for privacy) + const includeTxHash = config?.includeTxHash === true; + + try { + const signedReceipt: SignedReceipt = await issuer.issueReceipt( + resourceUrl, + payer, + network, + includeTxHash ? transaction || undefined : undefined, + ); + // Return extension data per spec structure + return { + info: { + receipt: signedReceipt, + }, + schema: RECEIPT_SCHEMA, + }; + } catch (error) { + console.error("[offer-receipt] Failed to sign receipt:", error); + return undefined; + } + }, + }; +} + +/** + * Declare offer-receipt extension for a route + * + * Use this in route configuration to enable offer-receipt for a specific endpoint. + * + * @param config - Optional configuration for the extension + * @returns Extension declaration object to spread into route config + */ +export function declareOfferReceiptExtension( + config?: OfferReceiptDeclaration, +): Record { + return { + [OFFER_RECEIPT]: { + includeTxHash: config?.includeTxHash, + offerValiditySeconds: config?.offerValiditySeconds, + }, + }; +} + +// ============================================================================ +// Issuer Factory Functions +// ============================================================================ + +/** + * Create an OfferReceiptIssuer that uses JWS format + * + * @param kid - Key identifier DID (e.g., did:web:api.example.com#key-1) + * @param jwsSigner - JWS signer with sign() function and algorithm + * @returns OfferReceiptIssuer for use with createOfferReceiptExtension + */ +export function createJWSOfferReceiptIssuer(kid: string, jwsSigner: JWSSigner): OfferReceiptIssuer { + return { + kid, + format: "jws", + + async issueOffer(resourceUrl: string, input: OfferInput) { + return createOfferJWS(resourceUrl, input, jwsSigner); + }, + + async issueReceipt(resourceUrl: string, payer: string, network: string, transaction?: string) { + return createReceiptJWS({ resourceUrl, payer, network, transaction }, jwsSigner); + }, + }; +} + +/** + * Create an OfferReceiptIssuer that uses EIP-712 format + * + * @param kid - Key identifier DID (e.g., did:pkh:eip155:1:0x...) + * @param signTypedData - Function to sign EIP-712 typed data + * @returns OfferReceiptIssuer for use with createOfferReceiptExtension + */ +export function createEIP712OfferReceiptIssuer( + kid: string, + signTypedData: SignTypedDataFn, +): OfferReceiptIssuer { + return { + kid, + format: "eip712", + + async issueOffer(resourceUrl: string, input: OfferInput) { + return createOfferEIP712(resourceUrl, input, signTypedData); + }, + + async issueReceipt(resourceUrl: string, payer: string, network: string, transaction?: string) { + return createReceiptEIP712({ resourceUrl, payer, network, transaction }, signTypedData); + }, + }; +} diff --git a/typescript/packages/extensions/src/offer-receipt/signing.ts b/typescript/packages/extensions/src/offer-receipt/signing.ts new file mode 100644 index 0000000..54d3864 --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/signing.ts @@ -0,0 +1,852 @@ +/** + * Signing utilities for x402 Offer/Receipt Extension + * + * This module provides: + * - JCS (JSON Canonicalization Scheme) per RFC 8785 + * - JWS (JSON Web Signature) signing and extraction + * - EIP-712 typed data signing + * - Offer/Receipt creation utilities + * - Signature verification utilities + * + * Based on: x402/specs/extensions/extension-offer-and-receipt.md (v1.0) §3 + */ + +import * as jose from "jose"; +import { hashTypedData, recoverTypedDataAddress, type Hex, type TypedDataDomain } from "viem"; +import type { + JWSSigner, + OfferPayload, + ReceiptPayload, + SignedOffer, + SignedReceipt, + OfferInput, + ReceiptInput, +} from "./types"; +import { + isJWSSignedOffer, + isEIP712SignedOffer, + isJWSSignedReceipt, + isEIP712SignedReceipt, + type JWSSignedOffer, + type EIP712SignedOffer, + type JWSSignedReceipt, + type EIP712SignedReceipt, +} from "./types"; +import { extractPublicKeyFromKid } from "./did"; + +// ============================================================================ +// JCS Canonicalization (RFC 8785) +// ============================================================================ + +/** + * Canonicalize a JSON object using JCS (RFC 8785) + * + * Rules: + * 1. Object keys are sorted lexicographically by UTF-16 code units + * 2. No whitespace between tokens + * 3. Numbers use shortest representation (no trailing zeros) + * 4. Strings use minimal escaping + * 5. null, true, false are lowercase literals + * + * @param value - The object to canonicalize + * @returns The canonicalized JSON string + */ +export function canonicalize(value: unknown): string { + return serializeValue(value); +} + +/** + * Serialize a value to canonical JSON + * + * @param value - The value to serialize + * @returns The serialized string + */ +function serializeValue(value: unknown): string { + if (value === null) return "null"; + if (value === undefined) return "null"; + + const type = typeof value; + if (type === "boolean") return value ? "true" : "false"; + if (type === "number") return serializeNumber(value as number); + if (type === "string") return serializeString(value as string); + if (Array.isArray(value)) return serializeArray(value); + if (type === "object") return serializeObject(value as Record); + + throw new Error(`Cannot canonicalize value of type ${type}`); +} + +/** + * Serialize a number to canonical JSON + * + * @param num - The number to serialize + * @returns The serialized string + */ +function serializeNumber(num: number): string { + if (!Number.isFinite(num)) throw new Error("Cannot canonicalize Infinity or NaN"); + if (Object.is(num, -0)) return "0"; + return String(num); +} + +/** + * Serialize a string to canonical JSON + * + * @param str - The string to serialize + * @returns The serialized string with proper escaping + */ +function serializeString(str: string): string { + let result = '"'; + for (let i = 0; i < str.length; i++) { + const char = str[i]; + const code = str.charCodeAt(i); + if (code < 0x20) { + result += "\\u" + code.toString(16).padStart(4, "0"); + } else if (char === '"') { + result += '\\"'; + } else if (char === "\\") { + result += "\\\\"; + } else { + result += char; + } + } + return result + '"'; +} + +/** + * Serialize an array to canonical JSON + * + * @param arr - The array to serialize + * @returns The serialized string + */ +function serializeArray(arr: unknown[]): string { + return "[" + arr.map(serializeValue).join(",") + "]"; +} + +/** + * Serialize an object to canonical JSON with sorted keys + * + * @param obj - The object to serialize + * @returns The serialized string with sorted keys + */ +function serializeObject(obj: Record): string { + const keys = Object.keys(obj).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + const pairs: string[] = []; + for (const key of keys) { + const value = obj[key]; + if (value !== undefined) { + pairs.push(serializeString(key) + ":" + serializeValue(value)); + } + } + return "{" + pairs.join(",") + "}"; +} + +/** + * Hash a canonicalized object using SHA-256 + * + * @param obj - The object to hash + * @returns The SHA-256 hash as Uint8Array + */ +export async function hashCanonical(obj: unknown): Promise { + const canonical = canonicalize(obj); + const data = new TextEncoder().encode(canonical); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + return new Uint8Array(hashBuffer); +} + +/** + * Get canonical bytes of an object (UTF-8 encoded) + * + * @param obj - The object to encode + * @returns The UTF-8 encoded canonical JSON + */ +export function getCanonicalBytes(obj: unknown): Uint8Array { + return new TextEncoder().encode(canonicalize(obj)); +} + +// ============================================================================ +// JWS Signing (§3.3) +// ============================================================================ + +/** + * Create a JWS Compact Serialization from a payload + * + * Assembles the full JWS structure (header.payload.signature) using the + * signer's algorithm and kid. The signer only needs to sign bytes and + * return the base64url-encoded signature. + * + * @param payload - The payload object to sign + * @param signer - The JWS signer + * @returns The JWS compact serialization string + */ +export async function createJWS(payload: T, signer: JWSSigner): Promise { + const headerObj = { alg: signer.algorithm, kid: signer.kid }; + const headerB64 = jose.base64url.encode(new TextEncoder().encode(JSON.stringify(headerObj))); + const canonical = canonicalize(payload); + const payloadB64 = jose.base64url.encode(new TextEncoder().encode(canonical)); + const signingInput = new TextEncoder().encode(`${headerB64}.${payloadB64}`); + const signatureB64 = await signer.sign(signingInput); + return `${headerB64}.${payloadB64}.${signatureB64}`; +} + +/** + * Extract JWS header without verification + * + * @param jws - The JWS compact serialization string + * @returns The decoded header object + */ +export function extractJWSHeader(jws: string): { alg: string; kid?: string } { + const parts = jws.split("."); + if (parts.length !== 3) throw new Error("Invalid JWS format"); + const headerJson = jose.base64url.decode(parts[0]); + return JSON.parse(new TextDecoder().decode(headerJson)); +} + +/** + * Extract JWS payload + * + * Note: This extracts the payload without verifying the signature or + * checking signer authorization. Signature verification requires resolving + * key bindings (did:web documents, attestations, etc.) which is outside + * the scope of x402 client utilities. + * + * @param jws - The JWS compact serialization string + * @returns The decoded payload + */ +export function extractJWSPayload(jws: string): T { + const parts = jws.split("."); + if (parts.length !== 3) throw new Error("Invalid JWS format"); + const payloadJson = jose.base64url.decode(parts[1]); + return JSON.parse(new TextDecoder().decode(payloadJson)); +} + +// ============================================================================ +// EIP-712 Domain Configuration (§3.2) +// ============================================================================ + +/** + * Create EIP-712 domain for offer signing + * + * @returns The EIP-712 domain object + */ +export function createOfferDomain(): TypedDataDomain { + return { name: "x402 offer", version: "1", chainId: 1 }; +} + +/** + * Create EIP-712 domain for receipt signing + * + * @returns The EIP-712 domain object + */ +export function createReceiptDomain(): TypedDataDomain { + return { name: "x402 receipt", version: "1", chainId: 1 }; +} + +/** + * EIP-712 types for Offer (§4.3) + */ +export const OFFER_TYPES = { + Offer: [ + { name: "version", type: "uint256" }, + { name: "resourceUrl", type: "string" }, + { name: "scheme", type: "string" }, + { name: "network", type: "string" }, + { name: "asset", type: "string" }, + { name: "payTo", type: "string" }, + { name: "amount", type: "string" }, + { name: "validUntil", type: "uint256" }, + ], +}; + +/** + * EIP-712 types for Receipt (§5.3) + */ +export const RECEIPT_TYPES = { + Receipt: [ + { name: "version", type: "uint256" }, + { name: "network", type: "string" }, + { name: "resourceUrl", type: "string" }, + { name: "payer", type: "string" }, + { name: "issuedAt", type: "uint256" }, + { name: "transaction", type: "string" }, + ], +}; + +// ============================================================================ +// EIP-712 Payload Preparation +// ============================================================================ + +/** + * Prepare offer payload for EIP-712 signing + * + * @param payload - The offer payload + * @returns The prepared message object for EIP-712 + */ +export function prepareOfferForEIP712(payload: OfferPayload): { + version: bigint; + resourceUrl: string; + scheme: string; + network: string; + asset: string; + payTo: string; + amount: string; + validUntil: bigint; +} { + return { + version: BigInt(payload.version), + resourceUrl: payload.resourceUrl, + scheme: payload.scheme, + network: payload.network, + asset: payload.asset, + payTo: payload.payTo, + amount: payload.amount, + validUntil: BigInt(payload.validUntil), + }; +} + +/** + * Prepare receipt payload for EIP-712 signing + * + * @param payload - The receipt payload + * @returns The prepared message object for EIP-712 + */ +export function prepareReceiptForEIP712(payload: ReceiptPayload): { + version: bigint; + network: string; + resourceUrl: string; + payer: string; + issuedAt: bigint; + transaction: string; +} { + return { + version: BigInt(payload.version), + network: payload.network, + resourceUrl: payload.resourceUrl, + payer: payload.payer, + issuedAt: BigInt(payload.issuedAt), + transaction: payload.transaction, + }; +} + +// ============================================================================ +// EIP-712 Hashing +// ============================================================================ + +/** + * Hash offer typed data for EIP-712 + * + * @param payload - The offer payload + * @returns The EIP-712 hash + */ +export function hashOfferTypedData(payload: OfferPayload): Hex { + return hashTypedData({ + domain: createOfferDomain(), + types: OFFER_TYPES, + primaryType: "Offer", + message: prepareOfferForEIP712(payload), + }); +} + +/** + * Hash receipt typed data for EIP-712 + * + * @param payload - The receipt payload + * @returns The EIP-712 hash + */ +export function hashReceiptTypedData(payload: ReceiptPayload): Hex { + return hashTypedData({ + domain: createReceiptDomain(), + types: RECEIPT_TYPES, + primaryType: "Receipt", + message: prepareReceiptForEIP712(payload), + }); +} + +// ============================================================================ +// EIP-712 Signing +// ============================================================================ + +/** + * Function type for signing EIP-712 typed data + */ +export type SignTypedDataFn = (params: { + domain: TypedDataDomain; + types: Record>; + primaryType: string; + message: Record; +}) => Promise; + +/** + * Sign an offer using EIP-712 + * + * @param payload - The offer payload + * @param signTypedData - The signing function + * @returns The signature hex string + */ +export async function signOfferEIP712( + payload: OfferPayload, + signTypedData: SignTypedDataFn, +): Promise { + return signTypedData({ + domain: createOfferDomain(), + types: OFFER_TYPES, + primaryType: "Offer", + message: prepareOfferForEIP712(payload) as unknown as Record, + }); +} + +/** + * Sign a receipt using EIP-712 + * + * @param payload - The receipt payload + * @param signTypedData - The signing function + * @returns The signature hex string + */ +export async function signReceiptEIP712( + payload: ReceiptPayload, + signTypedData: SignTypedDataFn, +): Promise { + return signTypedData({ + domain: createReceiptDomain(), + types: RECEIPT_TYPES, + primaryType: "Receipt", + message: prepareReceiptForEIP712(payload) as unknown as Record, + }); +} + +// ============================================================================ +// Network Utilities +// ============================================================================ + +/** + * Extract chain ID from an EIP-155 network string (strict format) + * + * @param network - The network string in "eip155:" format + * @returns The chain ID number + * @throws Error if network is not in "eip155:" format + */ +export function extractEIP155ChainId(network: string): number { + const match = network.match(/^eip155:(\d+)$/); + if (!match) { + throw new Error(`Invalid network format: ${network}. Expected "eip155:"`); + } + return parseInt(match[1], 10); +} + +/** + * V1 EVM network name to chain ID mapping + * Based on x402 v1 protocol network identifiers + */ +const V1_EVM_NETWORK_CHAIN_IDS: Record = { + ethereum: 1, + sepolia: 11155111, + abstract: 2741, + "abstract-testnet": 11124, + "base-sepolia": 84532, + base: 8453, + "avalanche-fuji": 43113, + avalanche: 43114, + iotex: 4689, + sei: 1329, + "sei-testnet": 1328, + polygon: 137, + "polygon-amoy": 80002, + peaq: 3338, + story: 1514, + educhain: 41923, + "skale-base-sepolia": 324705682, +}; + +/** + * V1 Solana network name to CAIP-2 mapping + */ +const V1_SOLANA_NETWORKS: Record = { + solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "solana-devnet": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + "solana-testnet": "solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z", +}; + +/** + * Convert a network string to CAIP-2 format + * + * Handles both CAIP-2 format and legacy x402 v1 network strings: + * - CAIP-2: "eip155:8453" → "eip155:8453" (passed through) + * - V1 EVM: "base" → "eip155:8453", "base-sepolia" → "eip155:84532" + * - V1 Solana: "solana" → "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + * + * @param network - The network string to convert + * @returns The CAIP-2 formatted network string + * @throws Error if network is not a recognized v1 identifier or CAIP-2 format + */ +export function convertNetworkStringToCAIP2(network: string): string { + // Already CAIP-2 format + if (network.includes(":")) return network; + + // Check V1 EVM networks + const chainId = V1_EVM_NETWORK_CHAIN_IDS[network.toLowerCase()]; + if (chainId !== undefined) { + return `eip155:${chainId}`; + } + + // Check V1 Solana networks + const solanaNetwork = V1_SOLANA_NETWORKS[network.toLowerCase()]; + if (solanaNetwork) { + return solanaNetwork; + } + + throw new Error( + `Unknown network identifier: "${network}". Expected CAIP-2 format (e.g., "eip155:8453") or v1 name (e.g., "base", "solana").`, + ); +} + +/** + * Extract chain ID from a CAIP-2 network string (EVM only) + * + * @param network - The CAIP-2 network string + * @returns Chain ID number, or undefined for non-EVM networks + */ +export function extractChainIdFromCAIP2(network: string): number | undefined { + const [namespace, reference] = network.split(":"); + if (namespace === "eip155" && reference) { + const chainId = parseInt(reference, 10); + return isNaN(chainId) ? undefined : chainId; + } + return undefined; +} + +// ============================================================================ +// Offer Creation (§4) +// ============================================================================ + +/** Default offer validity in seconds (matches x402ResourceServer.ts) */ +const DEFAULT_MAX_TIMEOUT_SECONDS = 300; + +/** Current extension version */ +const EXTENSION_VERSION = 1; + +/** + * Create an offer payload from input + * + * @param resourceUrl - The resource URL being paid for + * @param input - The offer input parameters + * @returns The offer payload + */ +function createOfferPayload(resourceUrl: string, input: OfferInput): OfferPayload { + const now = Math.floor(Date.now() / 1000); + const offerValiditySeconds = input.offerValiditySeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS; + + return { + version: EXTENSION_VERSION, + resourceUrl, + scheme: input.scheme, + network: input.network, + asset: input.asset, + payTo: input.payTo, + amount: input.amount, + validUntil: now + offerValiditySeconds, + }; +} + +/** + * Create a signed offer using JWS + * + * @param resourceUrl - The resource URL being paid for + * @param input - The offer input parameters + * @param signer - The JWS signer + * @returns The signed offer with JWS format + */ +export async function createOfferJWS( + resourceUrl: string, + input: OfferInput, + signer: JWSSigner, +): Promise { + const payload = createOfferPayload(resourceUrl, input); + const jws = await createJWS(payload, signer); + return { + format: "jws", + acceptIndex: input.acceptIndex, + signature: jws, + }; +} + +/** + * Create a signed offer using EIP-712 + * + * @param resourceUrl - The resource URL being paid for + * @param input - The offer input parameters + * @param signTypedData - The signing function + * @returns The signed offer with EIP-712 format + */ +export async function createOfferEIP712( + resourceUrl: string, + input: OfferInput, + signTypedData: SignTypedDataFn, +): Promise { + const payload = createOfferPayload(resourceUrl, input); + const signature = await signOfferEIP712(payload, signTypedData); + return { + format: "eip712", + acceptIndex: input.acceptIndex, + payload, + signature, + }; +} + +/** + * Extract offer payload + * + * Note: This extracts the payload without verifying the signature or + * checking signer authorization. Signer authorization requires resolving + * key bindings (did:web documents, attestations, etc.) which is outside + * the scope of x402 client utilities. See spec §4.5.1. + * + * @param offer - The signed offer + * @returns The offer payload + */ +export function extractOfferPayload(offer: SignedOffer): OfferPayload { + if (isJWSSignedOffer(offer)) { + return extractJWSPayload(offer.signature); + } else if (isEIP712SignedOffer(offer)) { + return offer.payload; + } + throw new Error(`Unknown offer format: ${(offer as SignedOffer).format}`); +} + +// ============================================================================ +// Receipt Creation (§5) +// ============================================================================ + +/** + * Create a receipt payload for EIP-712 (requires all fields per spec §5.3) + * + * Per spec: "implementations MUST set unused fields to empty string" + * for EIP-712 signing where fixed schemas require all fields. + * + * @param input - The receipt input parameters + * @returns The receipt payload with all fields + */ +function createReceiptPayloadForEIP712(input: ReceiptInput): ReceiptPayload { + return { + version: EXTENSION_VERSION, + network: input.network, + resourceUrl: input.resourceUrl, + payer: input.payer, + issuedAt: Math.floor(Date.now() / 1000), + transaction: input.transaction ?? "", + }; +} + +/** + * Create a receipt payload for JWS (omits optional fields when not provided) + * + * Per spec §5.2: transaction is optional and should be omitted in JWS + * when not provided (privacy-minimal by default). + * + * @param input - The receipt input parameters + * @returns The receipt payload with optional fields omitted if not provided + */ +function createReceiptPayloadForJWS( + input: ReceiptInput, +): Omit & { transaction?: string } { + const payload: Omit & { transaction?: string } = { + version: EXTENSION_VERSION, + network: input.network, + resourceUrl: input.resourceUrl, + payer: input.payer, + issuedAt: Math.floor(Date.now() / 1000), + }; + if (input.transaction) { + payload.transaction = input.transaction; + } + return payload; +} + +/** + * Create a signed receipt using JWS + * + * @param input - The receipt input parameters + * @param signer - The JWS signer + * @returns The signed receipt with JWS format + */ +export async function createReceiptJWS( + input: ReceiptInput, + signer: JWSSigner, +): Promise { + const payload = createReceiptPayloadForJWS(input); + const jws = await createJWS(payload, signer); + return { format: "jws", signature: jws }; +} + +/** + * Create a signed receipt using EIP-712 + * + * @param input - The receipt input parameters + * @param signTypedData - The signing function + * @returns The signed receipt with EIP-712 format + */ +export async function createReceiptEIP712( + input: ReceiptInput, + signTypedData: SignTypedDataFn, +): Promise { + const payload = createReceiptPayloadForEIP712(input); + const signature = await signReceiptEIP712(payload, signTypedData); + return { format: "eip712", payload, signature }; +} + +/** + * Extract receipt payload + * + * Note: This extracts the payload without verifying the signature or + * checking signer authorization. Signer authorization requires resolving + * key bindings (did:web documents, attestations, etc.) which is outside + * the scope of x402 client utilities. See spec §5.5. + * + * @param receipt - The signed receipt + * @returns The receipt payload + */ +export function extractReceiptPayload(receipt: SignedReceipt): ReceiptPayload { + if (isJWSSignedReceipt(receipt)) { + return extractJWSPayload(receipt.signature); + } else if (isEIP712SignedReceipt(receipt)) { + return receipt.payload; + } + throw new Error(`Unknown receipt format: ${(receipt as SignedReceipt).format}`); +} + +// ============================================================================ +// Signature Verification +// ============================================================================ + +/** + * Result of EIP-712 signature verification + */ +export interface EIP712VerificationResult { + signer: Hex; + payload: T; +} + +/** + * Verify an EIP-712 signed offer and recover the signer address. + * Does NOT verify signer authorization for the resourceUrl - see spec §4.5.1. + * + * @param offer - The EIP-712 signed offer + * @returns The recovered signer address and payload + */ +export async function verifyOfferSignatureEIP712( + offer: EIP712SignedOffer, +): Promise> { + if (offer.format !== "eip712") { + throw new Error(`Expected eip712 format, got ${offer.format}`); + } + if (!offer.payload || !("scheme" in offer.payload)) { + throw new Error("Invalid offer: missing or malformed payload"); + } + + const signer = await recoverTypedDataAddress({ + domain: createOfferDomain(), + types: OFFER_TYPES, + primaryType: "Offer", + message: prepareOfferForEIP712(offer.payload), + signature: offer.signature as Hex, + }); + + return { signer, payload: offer.payload }; +} + +/** + * Verify an EIP-712 signed receipt and recover the signer address. + * Does NOT verify signer authorization for the resourceUrl - see spec §4.5.1. + * + * @param receipt - The EIP-712 signed receipt + * @returns The recovered signer address and payload + */ +export async function verifyReceiptSignatureEIP712( + receipt: EIP712SignedReceipt, +): Promise> { + if (receipt.format !== "eip712") { + throw new Error(`Expected eip712 format, got ${receipt.format}`); + } + if (!receipt.payload || !("payer" in receipt.payload)) { + throw new Error("Invalid receipt: missing or malformed payload"); + } + + const signer = await recoverTypedDataAddress({ + domain: createReceiptDomain(), + types: RECEIPT_TYPES, + primaryType: "Receipt", + message: prepareReceiptForEIP712(receipt.payload), + signature: receipt.signature as Hex, + }); + + return { signer, payload: receipt.payload }; +} + +/** + * Verify a JWS signed offer. + * Does NOT verify signer authorization for the resourceUrl - see spec §4.5.1. + * If no publicKey provided, extracts from kid (supports did:key, did:jwk, did:web). + * + * @param offer - The JWS signed offer + * @param publicKey - Optional public key (JWK or KeyLike). If not provided, extracted from kid. + * @returns The verified payload + */ +export async function verifyOfferSignatureJWS( + offer: JWSSignedOffer, + publicKey?: jose.KeyLike | jose.JWK, +): Promise { + if (offer.format !== "jws") { + throw new Error(`Expected jws format, got ${offer.format}`); + } + const key = await resolveVerificationKey(offer.signature, publicKey); + const { payload } = await jose.compactVerify(offer.signature, key); + return JSON.parse(new TextDecoder().decode(payload)) as OfferPayload; +} + +/** + * Verify a JWS signed receipt. + * Does NOT verify signer authorization for the resourceUrl - see spec §4.5.1. + * If no publicKey provided, extracts from kid (supports did:key, did:jwk, did:web). + * + * @param receipt - The JWS signed receipt + * @param publicKey - Optional public key (JWK or KeyLike). If not provided, extracted from kid. + * @returns The verified payload + */ +export async function verifyReceiptSignatureJWS( + receipt: JWSSignedReceipt, + publicKey?: jose.KeyLike | jose.JWK, +): Promise { + if (receipt.format !== "jws") { + throw new Error(`Expected jws format, got ${receipt.format}`); + } + const key = await resolveVerificationKey(receipt.signature, publicKey); + const { payload } = await jose.compactVerify(receipt.signature, key); + return JSON.parse(new TextDecoder().decode(payload)) as ReceiptPayload; +} + +/** + * Resolve the verification key for JWS verification + * + * @param jws - The JWS compact serialization string + * @param providedKey - Optional explicit public key + * @returns The resolved public key + */ +async function resolveVerificationKey( + jws: string, + providedKey?: jose.KeyLike | jose.JWK, +): Promise { + if (providedKey) { + if ("kty" in providedKey) { + const key = await jose.importJWK(providedKey); + if (key instanceof Uint8Array) { + throw new Error("Symmetric keys are not supported for JWS verification"); + } + return key; + } + return providedKey; + } + + const header = extractJWSHeader(jws); + if (!header.kid) { + throw new Error("No public key provided and JWS header missing kid"); + } + + return extractPublicKeyFromKid(header.kid); +} diff --git a/typescript/packages/extensions/src/offer-receipt/types.ts b/typescript/packages/extensions/src/offer-receipt/types.ts new file mode 100644 index 0000000..546e8c0 --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/types.ts @@ -0,0 +1,302 @@ +/** + * Type definitions for the x402 Offer/Receipt Extension + * + * Based on: x402/specs/extensions/extension-offer-and-receipt.md (v1.0) + * + * Offers prove payment requirements originated from a resource server. + * Receipts prove service was delivered after payment. + */ + +/** + * Extension identifier constant + */ +export const OFFER_RECEIPT = "offer-receipt"; + +/** + * Supported signature formats (§3.1) + */ +export type SignatureFormat = "jws" | "eip712"; + +// ============================================================================ +// Low-Level Signer Interfaces +// ============================================================================ + +/** + * Base signer interface for pluggable signing backends + */ +export interface Signer { + /** Key identifier DID (e.g., did:web:api.example.com#key-1) */ + kid: string; + /** Sign payload and return signature string */ + sign: (payload: Uint8Array) => Promise; + /** Signature format */ + format: SignatureFormat; +} + +/** + * JWS-specific signer with algorithm info + */ +export interface JWSSigner extends Signer { + format: "jws"; + /** JWS algorithm (e.g., ES256K, EdDSA) */ + algorithm: string; +} + +/** + * EIP-712 specific signer + */ +export interface EIP712Signer extends Signer { + format: "eip712"; + /** Chain ID for EIP-712 domain */ + chainId: number; +} + +// ============================================================================ +// Offer Types (§4) +// ============================================================================ + +/** + * Offer payload fields (§4.2) + * + * Required: version, resourceUrl, scheme, network, asset, payTo, amount + * Optional: validUntil + */ +export interface OfferPayload { + /** Offer payload schema version (currently 1) */ + version: number; + /** The paid resource URL */ + resourceUrl: string; + /** Payment scheme identifier (e.g., "exact") */ + scheme: string; + /** Blockchain network identifier (CAIP-2 format, e.g., "eip155:8453") */ + network: string; + /** Token contract address or "native" */ + asset: string; + /** Recipient wallet address */ + payTo: string; + /** Required payment amount */ + amount: string; + /** Unix timestamp (seconds) when the offer expires (optional) */ + validUntil: number; +} + +/** + * Signed offer in JWS format (§3.1.1) + * + * "When format = 'jws': payload MUST be omitted" + */ +export interface JWSSignedOffer { + format: "jws"; + /** Index into accepts[] array (unsigned envelope field, §4.1.1) */ + acceptIndex?: number; + /** JWS Compact Serialization string (header.payload.signature) */ + signature: string; +} + +/** + * Signed offer in EIP-712 format (§3.1.1) + * + * "When format = 'eip712': payload is REQUIRED" + */ +export interface EIP712SignedOffer { + format: "eip712"; + /** Index into accepts[] array (unsigned envelope field, §4.1.1) */ + acceptIndex?: number; + /** The canonical payload fields */ + payload: OfferPayload; + /** Hex-encoded ECDSA signature (0x-prefixed, 65 bytes: r+s+v) */ + signature: string; +} + +/** + * Union type for signed offers + */ +export type SignedOffer = JWSSignedOffer | EIP712SignedOffer; + +// ============================================================================ +// Receipt Types (§5) +// ============================================================================ + +/** + * Receipt payload fields (§5.2) + * + * Required: version, network, resourceUrl, payer, issuedAt + * Optional: transaction (for verifiability over privacy) + */ +export interface ReceiptPayload { + /** Receipt payload schema version (currently 1) */ + version: number; + /** Blockchain network identifier (CAIP-2 format, e.g., "eip155:8453") */ + network: string; + /** The paid resource URL */ + resourceUrl: string; + /** Payer identifier (commonly a wallet address) */ + payer: string; + /** Unix timestamp (seconds) when receipt was issued */ + issuedAt: number; + /** Blockchain transaction hash (optional - for verifiability over privacy) */ + transaction: string; +} + +/** + * Signed receipt in JWS format (§3.1.1) + */ +export interface JWSSignedReceipt { + format: "jws"; + /** JWS Compact Serialization string */ + signature: string; +} + +/** + * Signed receipt in EIP-712 format (§3.1.1) + */ +export interface EIP712SignedReceipt { + format: "eip712"; + /** The receipt payload */ + payload: ReceiptPayload; + /** Hex-encoded ECDSA signature */ + signature: string; +} + +/** + * Union type for signed receipts + */ +export type SignedReceipt = JWSSignedReceipt | EIP712SignedReceipt; + +// ============================================================================ +// Extension Configuration Types +// ============================================================================ + +/** + * Declaration for the offer-receipt extension in route config + * Used by servers to declare that a route uses offer-receipt + */ +export interface OfferReceiptDeclaration { + /** Include transaction hash in receipt (default: false for privacy). Set to true for verifiability. */ + includeTxHash?: boolean; + /** Offer validity duration in seconds. Default: 300 (see x402ResourceServer.ts) */ + offerValiditySeconds?: number; +} + +/** + * Input for creating an offer (derived from PaymentRequirements) + */ +export interface OfferInput { + /** Index into accepts[] array this offer corresponds to (0-based) */ + acceptIndex: number; + /** Payment scheme identifier */ + scheme: string; + /** Blockchain network identifier (CAIP-2 format) */ + network: string; + /** Token contract address or "native" */ + asset: string; + /** Recipient wallet address */ + payTo: string; + /** Required payment amount */ + amount: string; + /** Offer validity duration in seconds. Default: 300 (see x402ResourceServer.ts) */ + offerValiditySeconds?: number; +} + +/** + * High-level issuer interface for the offer-receipt extension. + * Creates and signs offers and receipts. + * Used by createOfferReceiptExtension() + */ +export interface OfferReceiptIssuer { + /** Key identifier DID */ + kid: string; + /** Signature format */ + format: SignatureFormat; + /** Create and sign an offer for a resource */ + issueOffer(resourceUrl: string, input: OfferInput): Promise; + /** Create and sign a receipt for a completed payment */ + issueReceipt( + resourceUrl: string, + payer: string, + network: string, + transaction?: string, + ): Promise; +} + +// ============================================================================ +// Type Guards +// ============================================================================ + +/** + * Check if an offer is JWS format + * + * @param offer - The signed offer to check + * @returns True if the offer uses JWS format + */ +export function isJWSSignedOffer(offer: SignedOffer): offer is JWSSignedOffer { + return offer.format === "jws"; +} + +/** + * Check if an offer is EIP-712 format + * + * @param offer - The signed offer to check + * @returns True if the offer uses EIP-712 format + */ +export function isEIP712SignedOffer(offer: SignedOffer): offer is EIP712SignedOffer { + return offer.format === "eip712"; +} + +/** + * Check if a receipt is JWS format + * + * @param receipt - The signed receipt to check + * @returns True if the receipt uses JWS format + */ +export function isJWSSignedReceipt(receipt: SignedReceipt): receipt is JWSSignedReceipt { + return receipt.format === "jws"; +} + +/** + * Check if a receipt is EIP-712 format + * + * @param receipt - The signed receipt to check + * @returns True if the receipt uses EIP-712 format + */ +export function isEIP712SignedReceipt(receipt: SignedReceipt): receipt is EIP712SignedReceipt { + return receipt.format === "eip712"; +} + +/** + * Check if a signer is JWS format + * + * @param signer - The signer to check + * @returns True if the signer uses JWS format + */ +export function isJWSSigner(signer: Signer): signer is JWSSigner { + return signer.format === "jws"; +} + +/** + * Check if a signer is EIP-712 format + * + * @param signer - The signer to check + * @returns True if the signer uses EIP-712 format + */ +export function isEIP712Signer(signer: Signer): signer is EIP712Signer { + return signer.format === "eip712"; +} + +// ============================================================================ +// Receipt Input Type +// ============================================================================ + +/** + * Input for creating a receipt + */ +export interface ReceiptInput { + /** The resource URL that was paid for */ + resourceUrl: string; + /** The payer identifier (wallet address) */ + payer: string; + /** The blockchain network (CAIP-2 format) */ + network: string; + /** The transaction hash (optional, for verifiability) */ + transaction?: string; +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/hooks.ts b/typescript/packages/extensions/src/sign-in-with-x/hooks.ts index 8713e8b..c416d2b 100644 --- a/typescript/packages/extensions/src/sign-in-with-x/hooks.ts +++ b/typescript/packages/extensions/src/sign-in-with-x/hooks.ts @@ -54,7 +54,7 @@ export function createSIWxSettleHook(options: CreateSIWxHookOptions) { const { storage, onEvent } = options; return async (ctx: { - paymentPayload: { payload: unknown; resource: { url: string } }; + paymentPayload: { payload: unknown; resource?: { url: string } }; result: { success: boolean; payer?: string }; }): Promise => { // Only record payment if settlement succeeded @@ -64,14 +64,22 @@ export function createSIWxSettleHook(options: CreateSIWxHookOptions) { const address = ctx.result.payer; if (!address) return; - const resource = new URL(ctx.paymentPayload.resource.url).pathname; + // resource is optional per the v2 spec (section 5.2.2) + const resourceUrl = ctx.paymentPayload.resource?.url; + if (!resourceUrl) return; + + const resource = new URL(resourceUrl).pathname; await storage.recordPayment(resource, address); onEvent?.({ type: "payment_recorded", resource, address }); }; } /** - * Creates an onProtectedRequest hook that validates SIWX auth before payment. + * Creates an onProtectedRequest hook that validates SIWX auth. + * + * For paid routes: grants access when the SIWX signature is valid and the address has paid. + * For auth-only routes (accepts: []): grants access on valid SIWX signature alone. + * Auth-only detection uses the routeConfig passed by x402HTTPResourceServer. * * @param options - Hook configuration * @returns Hook function for x402HTTPResourceServer.onProtectedRequest() @@ -95,10 +103,13 @@ export function createSIWxRequestHook(options: CreateSIWxHookOptions) { ); } - return async (context: { - adapter: { getHeader(name: string): string | undefined; getUrl(): string }; - path: string; - }): Promise => { + return async ( + context: { + adapter: { getHeader(name: string): string | undefined; getUrl(): string }; + path: string; + }, + routeConfig?: { accepts?: unknown }, + ): Promise => { // Try both cases for header (HTTP headers are case-insensitive) const header = context.adapter.getHeader(SIGN_IN_WITH_X) || @@ -130,8 +141,11 @@ export function createSIWxRequestHook(options: CreateSIWxHookOptions) { } } - const hasPaid = await storage.hasPaid(context.path, verification.address); - if (hasPaid) { + // Auth-only routes (accepts: []) grant access on valid SIWX alone + const isAuthOnly = Array.isArray(routeConfig?.accepts) && routeConfig.accepts.length === 0; + + const shouldGrant = isAuthOnly || (await storage.hasPaid(context.path, verification.address)); + if (shouldGrant) { // Record nonce as used before granting access if (storage.recordNonce) { await storage.recordNonce(payload.nonce); diff --git a/typescript/packages/extensions/src/sign-in-with-x/index.ts b/typescript/packages/extensions/src/sign-in-with-x/index.ts index 75dde2f..1fe8b83 100644 --- a/typescript/packages/extensions/src/sign-in-with-x/index.ts +++ b/typescript/packages/extensions/src/sign-in-with-x/index.ts @@ -5,65 +5,8 @@ * Allows clients to prove control of a wallet that may have previously paid * for a resource, enabling servers to grant access without requiring repurchase. * - * ## Server Usage - * - * ```typescript - * import { - * declareSIWxExtension, - * parseSIWxHeader, - * validateSIWxMessage, - * verifySIWxSignature, - * SIGN_IN_WITH_X, - * } from '@x402/extensions/sign-in-with-x'; - * - * // 1. Declare auth requirement in PaymentRequired response - * const extensions = declareSIWxExtension({ - * domain: 'api.example.com', - * resourceUri: 'https://api.example.com/data', - * network: 'eip155:8453', - * statement: 'Sign in to access your purchased content', - * }); - * - * // 2. Verify incoming proof - * const header = request.headers.get('SIGN-IN-WITH-X'); - * if (header) { - * const payload = parseSIWxHeader(header); - * - * const validation = await validateSIWxMessage( - * payload, - * 'https://api.example.com/data' - * ); - * - * if (validation.valid) { - * const verification = await verifySIWxSignature(payload); - * if (verification.valid) { - * // Authentication successful! - * // verification.address is the verified wallet - * } - * } - * } - * ``` - * - * ## Client Usage - * - * ```typescript - * import { - * createSIWxPayload, - * encodeSIWxHeader, - * } from '@x402/extensions/sign-in-with-x'; - * - * // 1. Get extension info from 402 response - * const serverInfo = paymentRequired.extensions['sign-in-with-x'].info; - * - * // 2. Create signed payload - * const payload = await createSIWxPayload(serverInfo, wallet); - * - * // 3. Encode for header - * const header = encodeSIWxHeader(payload); - * - * // 4. Send authenticated request - * fetch(url, { headers: { 'SIGN-IN-WITH-X': header } }); - * ``` + * Auth-only routes (accepts: []) are supported — the SIWX request hook + * grants access on a valid signature alone, no payment required. * * @module sign-in-with-x */ diff --git a/typescript/packages/extensions/src/sign-in-with-x/server.ts b/typescript/packages/extensions/src/sign-in-with-x/server.ts index 429f12d..5135000 100644 --- a/typescript/packages/extensions/src/sign-in-with-x/server.ts +++ b/typescript/packages/extensions/src/sign-in-with-x/server.ts @@ -6,7 +6,6 @@ * - Refresh time-based fields per request (nonce, issuedAt, expirationTime) */ -import { randomBytes } from "crypto"; import type { ResourceServerExtension, PaymentRequiredContext } from "@x402/core/types"; import type { SIWxExtension, SIWxExtensionInfo, SupportedChain, DeclareSIWxOptions } from "./types"; import { SIGN_IN_WITH_X } from "./types"; @@ -62,7 +61,9 @@ export const siwxResourceServerExtension: ResourceServerExtension = { } // Generate fresh time-based fields - const nonce = randomBytes(16).toString("hex"); + const nonce = Array.from(globalThis.crypto.getRandomValues(new Uint8Array(16))) + .map(b => b.toString(16).padStart(2, "0")) + .join(""); const issuedAt = new Date().toISOString(); // Calculate expirationTime based on configured duration diff --git a/typescript/packages/extensions/test/bazaar.test.ts b/typescript/packages/extensions/test/bazaar.test.ts index 5dbd9dd..69ca52c 100644 --- a/typescript/packages/extensions/test/bazaar.test.ts +++ b/typescript/packages/extensions/test/bazaar.test.ts @@ -7,19 +7,21 @@ import { BAZAAR, declareDiscoveryExtension, validateDiscoveryExtension, + isValidRouteTemplate, extractDiscoveryInfo, extractDiscoveryInfoFromExtension, extractDiscoveryInfoV1, validateAndExtract, bazaarResourceServerExtension, } from "../src/bazaar/index"; -import type { BodyDiscoveryInfo, DiscoveryExtension } from "../src/bazaar/types"; +import type { BodyDiscoveryInfo, McpDiscoveryInfo, DiscoveryExtension } from "../src/bazaar/types"; +import type { DiscoveredMCPResource } from "../src/bazaar/facilitator"; import type { HTTPAdapter, HTTPRequestContext } from "@x402/core/http"; describe("Bazaar Discovery Extension", () => { describe("BAZAAR constant", () => { it("should export the correct extension identifier", () => { - expect(BAZAAR).toBe("bazaar"); + expect(BAZAAR.key).toBe("bazaar"); }); }); @@ -176,6 +178,7 @@ describe("Bazaar Discovery Extension", () => { describe("validateDiscoveryExtension", () => { it("should validate a correct GET extension", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: { query: "test" }, inputSchema: { properties: { @@ -192,6 +195,7 @@ describe("Bazaar Discovery Extension", () => { it("should validate a correct POST extension", () => { const declared = declareDiscoveryExtension({ + method: "POST", input: { name: "John" }, inputSchema: { properties: { @@ -206,6 +210,19 @@ describe("Bazaar Discovery Extension", () => { expect(result.valid).toBe(true); }); + it("should fail validation when method is absent", () => { + // Per spec, method is required. An extension without method (e.g. pre-enrichment) + // must be rejected. + const declared = declareDiscoveryExtension({ + input: { query: "test" }, + inputSchema: { properties: { query: { type: "string" } } }, + }); + + const result = validateDiscoveryExtension(declared.bazaar); + expect(result.valid).toBe(false); + expect(result.errors?.some(e => e.includes("method"))).toBe(true); + }); + it("should detect invalid extension structure", () => { const invalidExtension = { info: { @@ -241,6 +258,7 @@ describe("Bazaar Discovery Extension", () => { describe("extractDiscoveryInfoFromExtension", () => { it("should extract info from a valid extension", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: { query: "test" }, inputSchema: { properties: { @@ -305,6 +323,7 @@ describe("Bazaar Discovery Extension", () => { describe("extractDiscoveryInfo (full flow)", () => { it("should extract info from v2 PaymentPayload with extensions", () => { const declared = declareDiscoveryExtension({ + method: "POST", input: { userId: "123" }, inputSchema: { properties: { @@ -324,7 +343,7 @@ describe("Bazaar Discovery Extension", () => { accepted: {} as unknown, resource: { url: "http://example.com/test" }, extensions: { - [BAZAAR]: extension, + [BAZAAR.key]: extension, }, }; @@ -337,6 +356,7 @@ describe("Bazaar Discovery Extension", () => { it("should strip query params from v2 resourceUrl", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: { city: "NYC" }, inputSchema: { properties: { @@ -359,7 +379,7 @@ describe("Bazaar Discovery Extension", () => { mimeType: "application/json", }, extensions: { - [BAZAAR]: extension, + [BAZAAR.key]: extension, }, }; @@ -373,6 +393,7 @@ describe("Bazaar Discovery Extension", () => { it("should strip hash sections from v2 resourceUrl", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: {}, inputSchema: { properties: {} }, }); @@ -391,7 +412,7 @@ describe("Bazaar Discovery Extension", () => { mimeType: "text/html", }, extensions: { - [BAZAAR]: extension, + [BAZAAR.key]: extension, }, }; @@ -403,6 +424,7 @@ describe("Bazaar Discovery Extension", () => { it("should strip both query params and hash sections from v2 resourceUrl", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: {}, inputSchema: { properties: {} }, }); @@ -421,7 +443,7 @@ describe("Bazaar Discovery Extension", () => { mimeType: "text/html", }, extensions: { - [BAZAAR]: extension, + [BAZAAR.key]: extension, }, }; @@ -557,6 +579,7 @@ describe("Bazaar Discovery Extension", () => { describe("validateAndExtract", () => { it("should return valid result with info for correct extension", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: { query: "test" }, inputSchema: { properties: { @@ -920,6 +943,7 @@ describe("Bazaar Discovery Extension", () => { describe("Integration - Full workflow", () => { it("should handle GET endpoint with output schema (e2e scenario)", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: {}, inputSchema: { properties: {}, @@ -961,6 +985,7 @@ describe("Bazaar Discovery Extension", () => { it("should handle complete v2 server-to-facilitator workflow", () => { const declared = declareDiscoveryExtension({ + method: "POST", input: { userId: "123", action: "create" }, inputSchema: { properties: { @@ -986,11 +1011,11 @@ describe("Bazaar Discovery Extension", () => { }, accepts: [], extensions: { - [BAZAAR]: extension, + [BAZAAR.key]: extension, }, }; - const bazaarExt = paymentRequired.extensions?.[BAZAAR] as DiscoveryExtension; + const bazaarExt = paymentRequired.extensions?.[BAZAAR.key] as DiscoveryExtension; expect(bazaarExt).toBeDefined(); const validation = validateDiscoveryExtension(bazaarExt); @@ -1066,6 +1091,7 @@ describe("Bazaar Discovery Extension", () => { it("should handle unified extraction for both v1 and v2", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: { limit: 10 }, inputSchema: { properties: { @@ -1084,7 +1110,7 @@ describe("Bazaar Discovery Extension", () => { accepted: {} as unknown, resource: { url: "http://example.com/v2" }, extensions: { - [BAZAAR]: v2Extension, + [BAZAAR.key]: v2Extension, }, }; @@ -1266,6 +1292,57 @@ describe("Bazaar Discovery Extension", () => { expect(required).toContain("method"); }); + it("should produce a valid extension after enrichment (GET)", () => { + const declared = declareDiscoveryExtension({ + input: { query: "test" }, + inputSchema: { properties: { query: { type: "string" } } }, + }); + + // Pre-enrichment: method not set, validation should fail + const preResult = validateDiscoveryExtension(declared.bazaar); + expect(preResult.valid).toBe(false); + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/test", + adapter: createMockAdapter(), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + declared.bazaar, + httpContext, + ) as DiscoveryExtension; + + // Post-enrichment: validation should pass + const postResult = validateDiscoveryExtension(enriched); + expect(postResult.valid).toBe(true); + }); + + it("should produce a valid extension after enrichment (POST)", () => { + const declared = declareDiscoveryExtension({ + input: { data: "test" }, + inputSchema: { properties: { data: { type: "string" } } }, + bodyType: "json", + }); + + const preResult = validateDiscoveryExtension(declared.bazaar); + expect(preResult.valid).toBe(false); + + const httpContext: HTTPRequestContext = { + method: "POST", + path: "/test", + adapter: createMockAdapter(), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + declared.bazaar, + httpContext, + ) as DiscoveryExtension; + + const postResult = validateDiscoveryExtension(enriched); + expect(postResult.valid).toBe(true); + }); + it("should return unchanged declaration for non-HTTP context", () => { const declared = declareDiscoveryExtension({ input: { data: "test" }, @@ -1288,4 +1365,692 @@ describe("Bazaar Discovery Extension", () => { expect(methodEnum).toEqual(["POST", "PUT", "PATCH"]); }); }); + + describe("declareDiscoveryExtension - MCP tool", () => { + it("should create a valid MCP extension with tool info", () => { + const result = declareDiscoveryExtension({ + toolName: "financial_analysis", + description: "Analyze financial data for a given ticker", + inputSchema: { + type: "object", + properties: { + ticker: { type: "string", description: "Stock ticker symbol" }, + analysis_type: { + type: "string", + enum: ["fundamental", "technical", "sentiment"], + }, + }, + required: ["ticker"], + }, + example: { ticker: "AAPL", analysis_type: "fundamental" }, + }); + + expect(result).toHaveProperty("bazaar"); + const extension = result.bazaar; + expect(extension).toHaveProperty("info"); + expect(extension).toHaveProperty("schema"); + expect(extension.info.input.type).toBe("mcp"); + expect((extension.info as McpDiscoveryInfo).input.toolName).toBe("financial_analysis"); + expect((extension.info as McpDiscoveryInfo).input.description).toBe( + "Analyze financial data for a given ticker", + ); + expect((extension.info as McpDiscoveryInfo).input.inputSchema).toBeDefined(); + expect((extension.info as McpDiscoveryInfo).input.example).toEqual({ + ticker: "AAPL", + analysis_type: "fundamental", + }); + }); + + it("should create an MCP extension without optional fields", () => { + const result = declareDiscoveryExtension({ + toolName: "simple_tool", + inputSchema: { + type: "object", + properties: { + query: { type: "string" }, + }, + }, + }); + + const extension = result.bazaar; + expect(extension.info.input.type).toBe("mcp"); + expect((extension.info as McpDiscoveryInfo).input.toolName).toBe("simple_tool"); + expect((extension.info as McpDiscoveryInfo).input.description).toBeUndefined(); + expect((extension.info as McpDiscoveryInfo).input.example).toBeUndefined(); + }); + + it("should create an MCP extension with transport field", () => { + const result = declareDiscoveryExtension({ + toolName: "streaming_tool", + transport: "sse", + inputSchema: { + type: "object", + properties: { + query: { type: "string" }, + }, + }, + }); + + const extension = result.bazaar; + expect(extension.info.input.type).toBe("mcp"); + expect((extension.info as McpDiscoveryInfo).input.transport).toBe("sse"); + }); + + it("should omit transport when not provided (defaults to streamable-http per spec)", () => { + const result = declareDiscoveryExtension({ + toolName: "default_transport_tool", + inputSchema: { + type: "object", + properties: { + query: { type: "string" }, + }, + }, + }); + + const extension = result.bazaar; + expect((extension.info as McpDiscoveryInfo).input.transport).toBeUndefined(); + }); + + it("should create an MCP extension with output example", () => { + const result = declareDiscoveryExtension({ + toolName: "weather_tool", + inputSchema: { + type: "object", + properties: { + city: { type: "string" }, + }, + }, + output: { + example: { temperature: 72, condition: "sunny" }, + }, + }); + + const extension = result.bazaar; + expect(extension.info.output?.example).toEqual({ temperature: 72, condition: "sunny" }); + }); + }); + + describe("validateDiscoveryExtension - MCP", () => { + it("should validate a correct MCP extension", () => { + const declared = declareDiscoveryExtension({ + toolName: "my_tool", + inputSchema: { + type: "object", + properties: { + query: { type: "string" }, + }, + }, + }); + + const extension = declared.bazaar; + const result = validateDiscoveryExtension(extension); + expect(result.valid).toBe(true); + expect(result.errors).toBeUndefined(); + }); + + it("should validate an MCP extension with all optional fields", () => { + const declared = declareDiscoveryExtension({ + toolName: "full_tool", + description: "A fully specified tool", + transport: "streamable-http", + inputSchema: { + type: "object", + properties: { + input: { type: "string" }, + }, + required: ["input"], + }, + example: { input: "test" }, + output: { + example: { result: "success" }, + }, + }); + + const extension = declared.bazaar; + const result = validateDiscoveryExtension(extension); + expect(result.valid).toBe(true); + }); + }); + + describe("extractDiscoveryInfo - MCP", () => { + it("should extract MCP discovery info with tool name as method", () => { + const declared = declareDiscoveryExtension({ + toolName: "financial_analysis", + description: "Analyze financial data", + inputSchema: { + type: "object", + properties: { + ticker: { type: "string" }, + }, + }, + }); + + const extension = declared.bazaar; + + const paymentPayload = { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + accepted: {} as unknown, + resource: { + url: "https://mcp.example.com/tools", + description: "MCP Tool Server", + mimeType: "application/json", + }, + extensions: { + [BAZAAR.key]: extension, + }, + }; + + const discovered = extractDiscoveryInfo(paymentPayload, {} as unknown); + + expect(discovered).not.toBeNull(); + expect(discovered!.discoveryInfo.input.type).toBe("mcp"); + expect((discovered as DiscoveredMCPResource).toolName).toBe("financial_analysis"); + expect(discovered!.resourceUrl).toBe("https://mcp.example.com/tools"); + expect(discovered!.description).toBe("MCP Tool Server"); + }); + + it("should strip query params from MCP resource URL", () => { + const declared = declareDiscoveryExtension({ + toolName: "search", + inputSchema: { type: "object", properties: {} }, + }); + + const extension = declared.bazaar; + + const paymentPayload = { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + accepted: {} as unknown, + resource: { + url: "https://mcp.example.com/tools?session=abc", + }, + extensions: { + [BAZAAR.key]: extension, + }, + }; + + const discovered = extractDiscoveryInfo(paymentPayload, {} as unknown); + + expect(discovered).not.toBeNull(); + expect(discovered!.resourceUrl).toBe("https://mcp.example.com/tools"); + }); + }); + + describe("validateAndExtract - MCP", () => { + it("should validate and extract MCP discovery info", () => { + const declared = declareDiscoveryExtension({ + toolName: "code_review", + description: "Review code changes", + inputSchema: { + type: "object", + properties: { + diff: { type: "string" }, + language: { type: "string" }, + }, + required: ["diff"], + }, + example: { diff: "--- a/file.ts\n+++ b/file.ts", language: "typescript" }, + }); + + const extension = declared.bazaar; + const result = validateAndExtract(extension); + expect(result.valid).toBe(true); + expect(result.info).toBeDefined(); + expect(result.info!.input.type).toBe("mcp"); + }); + }); + + describe("extractDiscoveryInfoFromExtension - MCP", () => { + it("should extract info from a valid MCP extension", () => { + const declared = declareDiscoveryExtension({ + toolName: "translate", + inputSchema: { + type: "object", + properties: { + text: { type: "string" }, + target_language: { type: "string" }, + }, + }, + }); + + const extension = declared.bazaar; + const info = extractDiscoveryInfoFromExtension(extension); + expect(info).toEqual(extension.info); + expect(info.input.type).toBe("mcp"); + }); + }); + + describe("bazaarResourceServerExtension - MCP", () => { + it("should not modify MCP extensions even with HTTP context", () => { + const declared = declareDiscoveryExtension({ + toolName: "my_tool", + description: "A tool", + inputSchema: { + type: "object", + properties: { + query: { type: "string" }, + }, + }, + }); + + const extension = declared.bazaar; + + const mockAdapter: HTTPAdapter = { + getMethod: () => "POST", + getUrl: () => new URL("http://localhost/test"), + getHeader: () => undefined, + setHeader: () => {}, + setStatusCode: () => {}, + setBody: () => {}, + getBody: () => ({}), + }; + + const httpContext: HTTPRequestContext = { + method: "POST", + path: "/test", + adapter: mockAdapter, + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as DiscoveryExtension; + + // MCP extension should remain unchanged + expect(enriched.info.input.type).toBe("mcp"); + expect((enriched.info as McpDiscoveryInfo).input.toolName).toBe("my_tool"); + }); + }); + + describe("dynamic routes", () => { + const createMockAdapterWithPath = (path: string): HTTPAdapter => ({ + getHeader: () => undefined, + getMethod: () => "GET", + getPath: () => path, + getUrl: () => `http://example.com${path}`, + getAcceptHeader: () => "application/json", + getUserAgent: () => "test-agent", + }); + + it("should leave static routes unchanged", () => { + const declared = declareDiscoveryExtension({ + input: { query: "test" }, + inputSchema: { properties: { query: { type: "string" } } }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users", + routePattern: "/users", + adapter: createMockAdapterWithPath("/users"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBeUndefined(); + }); + + it("should produce routeTemplate for dynamic routes", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/123", + routePattern: "/users/[userId]", + adapter: createMockAdapterWithPath("/users/123"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/users/:userId"); + }); + + it("should extract path params from concrete URL", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/123", + routePattern: "/users/[userId]", + adapter: createMockAdapterWithPath("/users/123"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + const info = enriched.info as Record; + const input = info.input as Record; + expect(input.pathParams).toEqual({ userId: "123" }); + }); + + it("should extract multiple path params", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/42/posts/7", + routePattern: "/users/[userId]/posts/[postId]", + adapter: createMockAdapterWithPath("/users/42/posts/7"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/users/:userId/posts/:postId"); + const info = enriched.info as Record; + const input = info.input as Record; + expect(input.pathParams).toEqual({ userId: "42", postId: "7" }); + }); + + it("should use routeTemplate for canonical URL in facilitator", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + // Simulate enriched extension with routeTemplate + const enrichedExtension = { + ...extension, + routeTemplate: "/users/:userId", + info: { + ...extension.info, + input: { ...extension.info.input, pathParams: { userId: "123" } }, + }, + }; + + const paymentPayload = { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + accepted: {} as unknown, + resource: { url: "http://example.com/users/123" }, + extensions: { + [BAZAAR.key]: enrichedExtension, + }, + }; + + const discovered = extractDiscoveryInfo(paymentPayload, {} as unknown, false); + + expect(discovered).not.toBeNull(); + expect(discovered!.resourceUrl).toBe("http://example.com/users/:userId"); + // Narrow to DiscoveredHTTPResource to access routeTemplate (HTTP-only field) + expect((discovered as import("./..").DiscoveredHTTPResource).routeTemplate).toBe( + "/users/:userId", + ); + }); + + it("should return empty pathParams when URL path does not match pattern structure", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + // Pattern expects /users/[userId] but path is /api/other — structurally mismatched. + // This can occur in production if middleware and extension patterns diverge. + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/api/other", + routePattern: "/users/[userId]", + adapter: createMockAdapterWithPath("/api/other"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + const info = enriched.info as Record; + const input = info.input as Record; + // extractPathParams returns {} gracefully when pattern and URL structure don't match + expect(input.pathParams).toEqual({}); + }); + + it("should produce routeTemplate for :param style routes", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/123", + routePattern: "/users/:userId", + adapter: createMockAdapterWithPath("/users/123"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/users/:userId"); + }); + + it("should extract path params from :param style routes", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/42/posts/7", + routePattern: "/users/:userId/posts/:postId", + adapter: createMockAdapterWithPath("/users/42/posts/7"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/users/:userId/posts/:postId"); + const info = enriched.info as Record; + const input = info.input as Record; + expect(input.pathParams).toEqual({ userId: "42", postId: "7" }); + }); + + it("should auto-convert wildcard * to :varN for discovery", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/weather/san-francisco", + routePattern: "/weather/*", + adapter: createMockAdapterWithPath("/weather/san-francisco"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/weather/:var1"); + const info = enriched.info as Record; + const input = info.input as Record; + expect(input.pathParams).toEqual({ var1: "san-francisco" }); + }); + + it("should auto-convert multiple wildcards to :var1, :var2, etc.", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/api/users/42/posts/7", + routePattern: "/api/*/*/posts/*", + adapter: createMockAdapterWithPath("/api/users/42/posts/7"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/api/:var1/:var2/posts/:var3"); + }); + + it("should handle mixed [param] and :param patterns", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/42/posts/7", + routePattern: "/users/[userId]/posts/:postId", + adapter: createMockAdapterWithPath("/users/42/posts/7"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/users/:userId/posts/:postId"); + const info = enriched.info as Record; + const input = info.input as Record; + expect(input.pathParams).toEqual({ userId: "42", postId: "7" }); + }); + + it("should pass schema validation after enrichment with auto-injected pathParams", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/123", + routePattern: "/users/:userId", + adapter: createMockAdapterWithPath("/users/123"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as import("../src/bazaar/http/types").QueryDiscoveryExtension; + + const result = validateDiscoveryExtension(enriched); + expect(result.valid).toBe(true); + }); + + it("should use concrete URL for static routes in facilitator", () => { + const declared = declareDiscoveryExtension({ + input: { query: "test" }, + inputSchema: { properties: { query: { type: "string" } } }, + }); + const extension = declared.bazaar; + + const paymentPayload = { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + accepted: {} as unknown, + resource: { url: "http://example.com/search?q=test" }, + extensions: { + [BAZAAR.key]: extension, + }, + }; + + const discovered = extractDiscoveryInfo(paymentPayload, {} as unknown, false); + + expect(discovered).not.toBeNull(); + expect(discovered!.resourceUrl).toBe("http://example.com/search"); + // Narrow to DiscoveredHTTPResource to access routeTemplate (HTTP-only field) + expect((discovered as import("./..").DiscoveredHTTPResource).routeTemplate).toBeUndefined(); + }); + }); + + describe("isValidRouteTemplate", () => { + it("returns false for empty string", () => { + expect(isValidRouteTemplate("")).toBe(false); + }); + + it("returns false for undefined input", () => { + expect(isValidRouteTemplate(undefined)).toBe(false); + }); + + it("returns false for paths not starting with /", () => { + expect(isValidRouteTemplate("users/123")).toBe(false); + expect(isValidRouteTemplate("relative/path")).toBe(false); + expect(isValidRouteTemplate("no-slash")).toBe(false); + }); + + it("returns false for paths containing ..", () => { + expect(isValidRouteTemplate("/users/../admin")).toBe(false); + expect(isValidRouteTemplate("/../etc/passwd")).toBe(false); + expect(isValidRouteTemplate("/users/..")).toBe(false); + }); + + it("returns false for paths containing ://", () => { + expect(isValidRouteTemplate("http://evil.com/path")).toBe(false); + expect(isValidRouteTemplate("/users/http://evil")).toBe(false); + expect(isValidRouteTemplate("javascript://foo")).toBe(false); + }); + + it("returns true for valid paths", () => { + expect(isValidRouteTemplate("/users/:userId")).toBe(true); + expect(isValidRouteTemplate("/api/v1/items")).toBe(true); + expect(isValidRouteTemplate("/products/:productId/reviews/:reviewId")).toBe(true); + expect(isValidRouteTemplate("/weather/:country/:city")).toBe(true); + }); + + it("rejects paths with spaces or invalid characters", () => { + expect(isValidRouteTemplate("/users/ bad")).toBe(false); + expect(isValidRouteTemplate("/path with spaces")).toBe(false); + }); + + it("rejects /users/..hidden because it contains '..' as a substring", () => { + expect(isValidRouteTemplate("/users/..hidden")).toBe(false); + }); + + it("rejects percent-encoded traversal sequences", () => { + expect(isValidRouteTemplate("/users/%2e%2e/admin")).toBe(false); + expect(isValidRouteTemplate("/users/%2E%2E/admin")).toBe(false); + }); + }); }); diff --git a/typescript/packages/extensions/test/eip2612-gas-sponsoring.test.ts b/typescript/packages/extensions/test/eip2612-gas-sponsoring.test.ts new file mode 100644 index 0000000..6e99c63 --- /dev/null +++ b/typescript/packages/extensions/test/eip2612-gas-sponsoring.test.ts @@ -0,0 +1,170 @@ +/** + * Tests for EIP-2612 Gas Sponsoring Extension + */ + +import { describe, it, expect } from "vitest"; +import { + EIP2612_GAS_SPONSORING, + declareEip2612GasSponsoringExtension, + extractEip2612GasSponsoringInfo, + validateEip2612GasSponsoringInfo, +} from "../src/eip2612-gas-sponsoring/index"; +import type { + Eip2612GasSponsoringInfo, + Eip2612GasSponsoringExtension, +} from "../src/eip2612-gas-sponsoring/types"; +import type { PaymentPayload } from "@x402/core/types"; + +describe("EIP-2612 Gas Sponsoring Extension", () => { + describe("EIP2612_GAS_SPONSORING constant", () => { + it("should export the correct extension identifier", () => { + expect(EIP2612_GAS_SPONSORING.key).toBe("eip2612GasSponsoring"); + }); + }); + + describe("declareEip2612GasSponsoringExtension", () => { + it("should create a valid extension declaration", () => { + const result = declareEip2612GasSponsoringExtension(); + + expect(result).toHaveProperty("eip2612GasSponsoring"); + const extension = result.eip2612GasSponsoring; + + // Check info contains server-side defaults + expect(extension.info).toHaveProperty("description"); + expect(extension.info).toHaveProperty("version", "1"); + + // Check schema has required fields + expect(extension.schema).toHaveProperty("$schema"); + expect(extension.schema).toHaveProperty("type", "object"); + expect(extension.schema).toHaveProperty("properties"); + expect(extension.schema).toHaveProperty("required"); + + const required = extension.schema.required as string[]; + expect(required).toContain("from"); + expect(required).toContain("asset"); + expect(required).toContain("spender"); + expect(required).toContain("amount"); + expect(required).toContain("nonce"); + expect(required).toContain("deadline"); + expect(required).toContain("signature"); + expect(required).toContain("version"); + }); + }); + + describe("extractEip2612GasSponsoringInfo", () => { + const validInfo: Eip2612GasSponsoringInfo = { + from: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + nonce: "0", + deadline: "1740672154", + signature: + "0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c", + version: "1", + }; + + it("should extract info from a valid payload", () => { + const payload = { + x402Version: 2, + extensions: { + eip2612GasSponsoring: { + info: validInfo, + schema: {}, + } as Eip2612GasSponsoringExtension, + }, + } as unknown as PaymentPayload; + + const result = extractEip2612GasSponsoringInfo(payload); + expect(result).not.toBeNull(); + expect(result!.from).toBe(validInfo.from); + expect(result!.asset).toBe(validInfo.asset); + expect(result!.spender).toBe(validInfo.spender); + expect(result!.signature).toBe(validInfo.signature); + }); + + it("should return null when no extensions", () => { + const payload = { + x402Version: 2, + } as unknown as PaymentPayload; + + const result = extractEip2612GasSponsoringInfo(payload); + expect(result).toBeNull(); + }); + + it("should return null when extension is missing", () => { + const payload = { + x402Version: 2, + extensions: {}, + } as unknown as PaymentPayload; + + const result = extractEip2612GasSponsoringInfo(payload); + expect(result).toBeNull(); + }); + + it("should return null when info is incomplete", () => { + const payload = { + x402Version: 2, + extensions: { + eip2612GasSponsoring: { + info: { + description: "test", + version: "1", + }, + schema: {}, + }, + }, + } as unknown as PaymentPayload; + + const result = extractEip2612GasSponsoringInfo(payload); + expect(result).toBeNull(); + }); + }); + + describe("validateEip2612GasSponsoringInfo", () => { + it("should validate correct info", () => { + const info: Eip2612GasSponsoringInfo = { + from: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + nonce: "0", + deadline: "1740672154", + signature: "0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a12832597641736", + version: "1", + }; + + expect(validateEip2612GasSponsoringInfo(info)).toBe(true); + }); + + it("should reject invalid address format", () => { + const info: Eip2612GasSponsoringInfo = { + from: "invalid-address", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "100", + nonce: "0", + deadline: "1740672154", + signature: "0xabc", + version: "1", + }; + + expect(validateEip2612GasSponsoringInfo(info)).toBe(false); + }); + + it("should reject non-numeric amount", () => { + const info: Eip2612GasSponsoringInfo = { + from: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "not-a-number", + nonce: "0", + deadline: "1740672154", + signature: "0xabc", + version: "1", + }; + + expect(validateEip2612GasSponsoringInfo(info)).toBe(false); + }); + }); +}); diff --git a/typescript/packages/extensions/test/erc20-approval-gas-sponsoring.test.ts b/typescript/packages/extensions/test/erc20-approval-gas-sponsoring.test.ts new file mode 100644 index 0000000..1194458 --- /dev/null +++ b/typescript/packages/extensions/test/erc20-approval-gas-sponsoring.test.ts @@ -0,0 +1,244 @@ +/** + * Tests for ERC-20 Approval Gas Sponsoring Extension + */ + +import { describe, it, expect } from "vitest"; +import { + ERC20_APPROVAL_GAS_SPONSORING, + declareErc20ApprovalGasSponsoringExtension, + extractErc20ApprovalGasSponsoringInfo, + validateErc20ApprovalGasSponsoringInfo, +} from "../src/erc20-approval-gas-sponsoring/index"; +import type { + Erc20ApprovalGasSponsoringInfo, + Erc20ApprovalGasSponsoringExtension, +} from "../src/erc20-approval-gas-sponsoring/types"; +import type { PaymentPayload } from "@x402/core/types"; + +describe("ERC-20 Approval Gas Sponsoring Extension", () => { + describe("ERC20_APPROVAL_GAS_SPONSORING constant", () => { + it("should export the correct extension identifier", () => { + expect(ERC20_APPROVAL_GAS_SPONSORING.key).toBe("erc20ApprovalGasSponsoring"); + }); + }); + + describe("declareErc20ApprovalGasSponsoringExtension", () => { + it("should create a valid extension declaration", () => { + const result = declareErc20ApprovalGasSponsoringExtension(); + + expect(result).toHaveProperty("erc20ApprovalGasSponsoring"); + const extension = result.erc20ApprovalGasSponsoring; + + // Check info contains server-side defaults + expect(extension.info).toHaveProperty("description"); + expect(extension.info).toHaveProperty("version", "1"); + + // Check schema has required fields + expect(extension.schema).toHaveProperty("$schema"); + expect(extension.schema).toHaveProperty("type", "object"); + expect(extension.schema).toHaveProperty("properties"); + expect(extension.schema).toHaveProperty("required"); + + const required = extension.schema.required as string[]; + expect(required).toContain("from"); + expect(required).toContain("asset"); + expect(required).toContain("spender"); + expect(required).toContain("amount"); + expect(required).toContain("signedTransaction"); + expect(required).toContain("version"); + }); + + it("should NOT include nonce, deadline, or signature in required fields", () => { + const result = declareErc20ApprovalGasSponsoringExtension(); + const required = result.erc20ApprovalGasSponsoring.schema.required as string[]; + + expect(required).not.toContain("nonce"); + expect(required).not.toContain("deadline"); + expect(required).not.toContain("signature"); + }); + + it("should have correct schema properties for signedTransaction", () => { + const result = declareErc20ApprovalGasSponsoringExtension(); + const properties = result.erc20ApprovalGasSponsoring.schema.properties as Record< + string, + unknown + >; + + expect(properties).toHaveProperty("signedTransaction"); + const signedTxSchema = properties.signedTransaction as Record; + expect(signedTxSchema.type).toBe("string"); + // signedTransaction uses hex pattern + expect(signedTxSchema.pattern).toContain("0x"); + }); + }); + + describe("extractErc20ApprovalGasSponsoringInfo", () => { + const validInfo: Erc20ApprovalGasSponsoringInfo = { + from: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + asset: "0xeED520980fC7C7B4eB379B96d61CEdea2423005a", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + signedTransaction: + "0x02f8708501234567890185012345678901850174876e80082011094eed520980fc7c7b4eb379b96d61cedea2423005a80b844095ea7b3000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + version: "1", + }; + + it("should extract info from a valid payload", () => { + const payload = { + x402Version: 2, + extensions: { + erc20ApprovalGasSponsoring: { + info: validInfo, + schema: {}, + } as Erc20ApprovalGasSponsoringExtension, + }, + } as unknown as PaymentPayload; + + const result = extractErc20ApprovalGasSponsoringInfo(payload); + expect(result).not.toBeNull(); + expect(result!.from).toBe(validInfo.from); + expect(result!.asset).toBe(validInfo.asset); + expect(result!.spender).toBe(validInfo.spender); + expect(result!.signedTransaction).toBe(validInfo.signedTransaction); + }); + + it("should return null when no extensions", () => { + const payload = { + x402Version: 2, + } as unknown as PaymentPayload; + + const result = extractErc20ApprovalGasSponsoringInfo(payload); + expect(result).toBeNull(); + }); + + it("should return null when extension is missing", () => { + const payload = { + x402Version: 2, + extensions: {}, + } as unknown as PaymentPayload; + + const result = extractErc20ApprovalGasSponsoringInfo(payload); + expect(result).toBeNull(); + }); + + it("should return null when info is incomplete (only server-side fields)", () => { + const payload = { + x402Version: 2, + extensions: { + erc20ApprovalGasSponsoring: { + info: { + description: "test", + version: "1", + }, + schema: {}, + }, + }, + } as unknown as PaymentPayload; + + const result = extractErc20ApprovalGasSponsoringInfo(payload); + expect(result).toBeNull(); + }); + + it("should return null when signedTransaction is missing", () => { + const payload = { + x402Version: 2, + extensions: { + erc20ApprovalGasSponsoring: { + info: { + from: validInfo.from, + asset: validInfo.asset, + spender: validInfo.spender, + amount: validInfo.amount, + // signedTransaction is missing + version: validInfo.version, + }, + schema: {}, + }, + }, + } as unknown as PaymentPayload; + + const result = extractErc20ApprovalGasSponsoringInfo(payload); + expect(result).toBeNull(); + }); + }); + + describe("validateErc20ApprovalGasSponsoringInfo", () => { + it("should validate correct info", () => { + const info: Erc20ApprovalGasSponsoringInfo = { + from: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + asset: "0xeED520980fC7C7B4eB379B96d61CEdea2423005a", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + signedTransaction: "0x02f8ab", + version: "1", + }; + + expect(validateErc20ApprovalGasSponsoringInfo(info)).toBe(true); + }); + + it("should reject invalid from address format", () => { + const info: Erc20ApprovalGasSponsoringInfo = { + from: "invalid-address", + asset: "0xeED520980fC7C7B4eB379B96d61CEdea2423005a", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "100", + signedTransaction: "0x02ab", + version: "1", + }; + + expect(validateErc20ApprovalGasSponsoringInfo(info)).toBe(false); + }); + + it("should reject non-numeric amount", () => { + const info: Erc20ApprovalGasSponsoringInfo = { + from: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + asset: "0xeED520980fC7C7B4eB379B96d61CEdea2423005a", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "not-a-number", + signedTransaction: "0x02ab", + version: "1", + }; + + expect(validateErc20ApprovalGasSponsoringInfo(info)).toBe(false); + }); + + it("should reject invalid signedTransaction (not hex)", () => { + const info: Erc20ApprovalGasSponsoringInfo = { + from: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + asset: "0xeED520980fC7C7B4eB379B96d61CEdea2423005a", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "100", + signedTransaction: "not-a-hex-string", + version: "1", + }; + + expect(validateErc20ApprovalGasSponsoringInfo(info)).toBe(false); + }); + + it("should reject invalid spender address", () => { + const info: Erc20ApprovalGasSponsoringInfo = { + from: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + asset: "0xeED520980fC7C7B4eB379B96d61CEdea2423005a", + spender: "not-an-address", + amount: "100", + signedTransaction: "0x02ab", + version: "1", + }; + + expect(validateErc20ApprovalGasSponsoringInfo(info)).toBe(false); + }); + + it("should reject invalid version format", () => { + const info: Erc20ApprovalGasSponsoringInfo = { + from: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + asset: "0xeED520980fC7C7B4eB379B96d61CEdea2423005a", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "100", + signedTransaction: "0x02ab", + version: "not.a.version", + }; + + expect(validateErc20ApprovalGasSponsoringInfo(info)).toBe(false); + }); + }); +}); diff --git a/typescript/packages/extensions/test/offer-receipt-test-utils.ts b/typescript/packages/extensions/test/offer-receipt-test-utils.ts new file mode 100644 index 0000000..b526c09 --- /dev/null +++ b/typescript/packages/extensions/test/offer-receipt-test-utils.ts @@ -0,0 +1,135 @@ +/** + * Test utilities for x402 Offer/Receipt Extension + * + * These are convenience functions for testing only. + * Production implementations should use HSM, TPM, or secure key management. + */ + +import * as jose from "jose"; +import { secp256k1 } from "@noble/curves/secp256k1"; +import type { JWSSigner } from "../src/offer-receipt/types"; + +// ============================================================================ +// P-256 (ES256) Utilities - Clean Web Crypto implementation +// ============================================================================ + +/** + * Create an ES256 (P-256) JWS signer from a CryptoKey (FOR TESTING ONLY) + * + * The signer's sign() function returns ONLY the raw base64url-encoded signature. + * The library's createJWS function is responsible for assembling the + * full JWS compact serialization (header.payload.signature). + * + * @param privateKey - The CryptoKey private key (P-256) + * @param kid - The key identifier + * @returns A JWS signer + */ +export function createES256Signer(privateKey: CryptoKey, kid: string): JWSSigner { + return { + kid, + algorithm: "ES256", + format: "jws", + sign: async (signingInput: Uint8Array): Promise => { + const signature = await crypto.subtle.sign( + { name: "ECDSA", hash: "SHA-256" }, + privateKey, + signingInput, + ); + return jose.base64url.encode(new Uint8Array(signature)); + }, + }; +} + +/** + * Generate a P-256 (ES256) key pair for testing + * + * Returns both the CryptoKey (for signing) and JWK (for verification). + * + * @returns Promise resolving to privateKey CryptoKey and publicKey JWK + */ +export async function generateES256KeyPair(): Promise<{ + privateKey: CryptoKey; + publicKeyJWK: jose.JWK; +}> { + const keyPair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [ + "sign", + "verify", + ]); + + const publicKeyJWK = await crypto.subtle.exportKey("jwk", keyPair.publicKey); + + return { + privateKey: keyPair.privateKey, + publicKeyJWK, + }; +} + +// ============================================================================ +// secp256k1 (ES256K) Utilities - For EVM-compatible testing +// ============================================================================ + +/** + * SHA-256 hash using Web Crypto API + * + * @param data - The data to hash + * @returns The SHA-256 hash as Uint8Array + */ +async function sha256(data: Uint8Array): Promise { + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + return new Uint8Array(hashBuffer); +} + +/** + * Create an ES256K (secp256k1) JWS signer from a JWK (FOR TESTING ONLY) + * + * @param jwk - The JWK private key + * @param kid - The key identifier + * @returns A JWS signer + */ +export async function createES256KSigner(jwk: jose.JWK, kid: string): Promise { + if (jwk.crv !== "secp256k1") { + throw new Error(`Unsupported curve: ${jwk.crv}. Use createJWSSigner for P-256.`); + } + if (!jwk.d) { + throw new Error("JWK must contain private key (d parameter)"); + } + + const privateKeyBytes = jose.base64url.decode(jwk.d); + + return { + kid, + algorithm: "ES256K", + format: "jws", + sign: async (signingInput: Uint8Array): Promise => { + const hash = await sha256(signingInput); + const signature = secp256k1.sign(hash, privateKeyBytes); + + // JWS uses concatenated r || s format (not DER) + const r = signature.r.toString(16).padStart(64, "0"); + const s = signature.s.toString(16).padStart(64, "0"); + const sigBytes = new Uint8Array(64); + for (let i = 0; i < 32; i++) { + sigBytes[i] = parseInt(r.slice(i * 2, i * 2 + 2), 16); + sigBytes[i + 32] = parseInt(s.slice(i * 2, i * 2 + 2), 16); + } + + return jose.base64url.encode(sigBytes); + }, + }; +} + +/** + * Generate an ES256K (secp256k1) key pair (FOR TESTING ONLY) + * + * @returns Promise resolving to an object with privateKey and publicKey JWKs + */ +export async function generateES256KKeyPair(): Promise<{ + privateKey: jose.JWK; + publicKey: jose.JWK; +}> { + const { privateKey, publicKey } = await jose.generateKeyPair("ES256K"); + return { + privateKey: await jose.exportJWK(privateKey), + publicKey: await jose.exportJWK(publicKey), + }; +} diff --git a/typescript/packages/extensions/test/offer-receipt.test.ts b/typescript/packages/extensions/test/offer-receipt.test.ts new file mode 100644 index 0000000..504d98c --- /dev/null +++ b/typescript/packages/extensions/test/offer-receipt.test.ts @@ -0,0 +1,2144 @@ +/** + * Specification-driven tests for x402 Offer/Receipt Extension + */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from "vitest"; +import * as jose from "jose"; +import { privateKeyToAccount } from "viem/accounts"; +import { recoverTypedDataAddress } from "viem"; +import type { Hex } from "viem"; + +import { + canonicalize, + hashCanonical, + getCanonicalBytes, + createJWS, + extractJWSHeader, + extractJWSPayload, + createOfferJWS, + createOfferEIP712, + extractOfferPayload, + createReceiptJWS, + createReceiptEIP712, + extractReceiptPayload, + createOfferDomain, + createReceiptDomain, + OFFER_TYPES, + RECEIPT_TYPES, + prepareOfferForEIP712, + prepareReceiptForEIP712, + hashOfferTypedData, + hashReceiptTypedData, + convertNetworkStringToCAIP2, + extractChainIdFromCAIP2, + extractEIP155ChainId, + extractOffersFromPaymentRequired, + decodeSignedOffers, + findAcceptsObjectFromSignedOffer, + extractReceiptFromResponse, + declareOfferReceiptExtension, + createJWSOfferReceiptIssuer, + createEIP712OfferReceiptIssuer, + verifyReceiptMatchesOffer, + verifyOfferSignatureEIP712, + verifyReceiptSignatureEIP712, + verifyOfferSignatureJWS, + verifyReceiptSignatureJWS, + extractPublicKeyFromKid, + OFFER_RECEIPT, + type JWSSigner, + type OfferPayload, + type ReceiptPayload, + type EIP712SignedOffer, + type EIP712SignedReceipt, + type JWSSignedOffer, +} from "../src/offer-receipt"; + +import { + createES256Signer, + generateES256KeyPair, + createES256KSigner, + generateES256KKeyPair, +} from "./offer-receipt-test-utils"; + +const TEST_PRIVATE_KEY = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex; + +// ============================================================================ +// Core JWS Assembly Tests (Layer 1) +// These tests verify createJWS produces valid JWS compact serialization. +// Higher-level tests (createOfferJWS, createReceiptJWS) depend on this. +// ============================================================================ + +describe("createJWS (Core JWS Assembly)", () => { + let signer: JWSSigner; + let publicKeyJWK: jose.JWK; + + beforeAll(async () => { + const keyPair = await generateES256KeyPair(); + publicKeyJWK = keyPair.publicKeyJWK; + signer = createES256Signer(keyPair.privateKey, "did:web:example.com#key-1"); + }); + + it("produces valid JWS compact serialization (header.payload.signature)", async () => { + const payload = { test: "data", number: 42 }; + const jws = await createJWS(payload, signer); + + // Must be three dot-separated parts + const parts = jws.split("."); + expect(parts).toHaveLength(3); + expect(parts[0].length).toBeGreaterThan(0); // header + expect(parts[1].length).toBeGreaterThan(0); // payload + expect(parts[2].length).toBeGreaterThan(0); // signature + }); + + it("includes alg and kid in JWS header", async () => { + const payload = { test: "data" }; + const jws = await createJWS(payload, signer); + + const header = extractJWSHeader(jws); + expect(header.alg).toBe("ES256"); + expect(header.kid).toBe("did:web:example.com#key-1"); + }); + + it("encodes payload as canonicalized JSON", async () => { + // Keys in different order should produce same canonical form + const payload = { z: 1, a: 2 }; + const jws = await createJWS(payload, signer); + + const decoded = extractJWSPayload(jws); + expect(decoded).toEqual({ a: 2, z: 1 }); // Canonicalized order + }); + + it("produces signature verifiable with jose.compactVerify", async () => { + const payload = { resourceUrl: "https://example.com", amount: "1000" }; + const jws = await createJWS(payload, signer); + + const key = await jose.importJWK(publicKeyJWK); + const { payload: verifiedPayload } = await jose.compactVerify(jws, key); + const decoded = JSON.parse(new TextDecoder().decode(verifiedPayload)); + + expect(decoded.resourceUrl).toBe("https://example.com"); + expect(decoded.amount).toBe("1000"); + }); + + it("round-trips through extractJWSHeader and extractJWSPayload", async () => { + const payload = { version: 1, data: "test" }; + const jws = await createJWS(payload, signer); + + // Should be able to extract header and payload + const header = extractJWSHeader(jws); + const extractedPayload = extractJWSPayload(jws); + + expect(header.alg).toBe("ES256"); + expect(header.kid).toBe("did:web:example.com#key-1"); + expect(extractedPayload.version).toBe(1); + expect(extractedPayload.data).toBe("test"); + }); +}); + +describe("x402 Offer/Receipt Extension", () => { + describe("§3.1 Common Object Shape", () => { + describe("JWS format", () => { + let signer: JWSSigner; + beforeAll(async () => { + const keyPair = await generateES256KKeyPair(); + signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + }); + + it("JWS offer has format='jws', signature field, no payload field", async () => { + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + expect(offer.format).toBe("jws"); + expect(offer.signature).toMatch(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/); + expect(offer).not.toHaveProperty("payload"); + }); + }); + + describe("EIP-712 format", () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + it("EIP-712 offer has format='eip712', payload field, hex signature", async () => { + const offer = await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + p => account.signTypedData(p), + ); + expect(offer.format).toBe("eip712"); + expect(offer).toHaveProperty("payload"); + expect(offer.signature).toMatch(/^0x[a-fA-F0-9]{130}$/); + }); + }); + }); + + describe("§3.2 EIP-712 Domain", () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + it("Offer domain: name='x402 offer', version='1', chainId=1 (canonical)", async () => { + await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + p => { + expect(p.domain.name).toBe("x402 offer"); + expect(p.domain.version).toBe("1"); + expect(Number(p.domain.chainId)).toBe(1); + return account.signTypedData(p); + }, + ); + }); + + it("Receipt domain: name='x402 receipt', version='1', chainId=1 (canonical)", async () => { + await createReceiptEIP712( + { + resourceUrl: "https://api.example.com/resource", + payer: "0xabc123", + network: "eip155:8453", + }, + p => { + expect(p.domain.name).toBe("x402 receipt"); + expect(p.domain.version).toBe("1"); + expect(Number(p.domain.chainId)).toBe(1); + return account.signTypedData(p); + }, + ); + }); + + it("EIP-712 chainId is constant regardless of payment network", async () => { + // Even with different payment networks, chainId should always be 1 + await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:137", // Polygon + asset: "native", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + p => { + expect(Number(p.domain.chainId)).toBe(1); // Still 1, not 137 + return account.signTypedData(p); + }, + ); + }); + }); + + describe("§3.3 JWS Header Requirements", () => { + it("JWS header MUST include alg and kid", async () => { + const keyPair = await generateES256KKeyPair(); + const expectedKid = "did:web:api.example.com#key-1"; + const signer = await createES256KSigner(keyPair.privateKey, expectedKid); + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + const header = JSON.parse( + new TextDecoder().decode(jose.base64url.decode(offer.signature.split(".")[0])), + ); + expect(header.alg).toBe("ES256K"); + expect(header.kid).toBe(expectedKid); + }); + }); + + describe("§4.2 Offer Payload Fields", () => { + it("Offer payload includes all required fields per spec v1.0", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const beforeCreate = Math.floor(Date.now() / 1000); + const offer = await createOfferJWS( + "https://api.example.com/premium", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + amount: "10000", + offerValiditySeconds: 60, + }, + signer, + ); + const payload = extractOfferPayload(offer); + // Required fields per spec §4.2 + expect(payload.version).toBe(1); + expect(payload.resourceUrl).toBe("https://api.example.com/premium"); + expect(payload.scheme).toBe("exact"); + expect(payload.network).toBe("eip155:8453"); + expect(payload.asset).toBe("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"); + expect(payload.payTo).toBe("0x209693Bc6afc0C5328bA36FaF03C514EF312287C"); + expect(payload.amount).toBe("10000"); + // validUntil should be approximately now + offerValiditySeconds + expect(payload.validUntil).toBeGreaterThanOrEqual(beforeCreate + 60); + expect(payload.validUntil).toBeLessThanOrEqual(beforeCreate + 62); // Allow 2s tolerance + }); + }); + + describe("§5.2 Receipt Payload Fields (Privacy-Minimal)", () => { + it("JWS receipt omits transaction when not provided (privacy-minimal)", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + const payload = extractReceiptPayload(receipt); + // Required fields per spec §5.2 + expect(payload.version).toBe(1); + expect(payload.network).toBe("eip155:8453"); + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.payer).toBe("0x857b06519E91e3A54538791bDbb0E22373e36b66"); + expect(typeof payload.issuedAt).toBe("number"); + // Per spec: transaction is optional, should be omitted in JWS when not provided + expect(payload).not.toHaveProperty("transaction"); + }); + + it("JWS receipt includes transaction when provided", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + transaction: "0xabc123", + }, + signer, + ); + const payload = extractReceiptPayload(receipt); + expect(payload.transaction).toBe("0xabc123"); + }); + + it("EIP-712 receipt uses empty string for transaction when not provided", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + const receipt = await createReceiptEIP712( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + p => account.signTypedData(p), + ); + const payload = extractReceiptPayload(receipt); + // Per spec §5.3: EIP-712 MUST set unused optional fields to empty string + expect(payload.transaction).toBe(""); + }); + }); + + describe("JCS Canonicalization (RFC 8785)", () => { + it("sorts object keys lexicographically", () => { + expect(canonicalize({ z: 1, a: 2 })).toBe('{"a":2,"z":1}'); + }); + it("handles nested objects", () => { + expect(canonicalize({ b: { d: 1, c: 2 }, a: 3 })).toBe('{"a":3,"b":{"c":2,"d":1}}'); + }); + it("handles arrays (preserves order)", () => { + expect(canonicalize({ arr: [3, 1, 2] })).toBe('{"arr":[3,1,2]}'); + }); + it("handles -0 as 0", () => { + expect(canonicalize({ n: -0 })).toBe('{"n":0}'); + }); + }); + + describe("Cryptographic Verification", () => { + it("JWS signature verifies with jose.compactVerify", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const publicKey = await jose.importJWK(keyPair.publicKey); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const { payload } = await jose.compactVerify(offer.signature, publicKey); + const decoded = JSON.parse(new TextDecoder().decode(payload)); + expect(decoded.resourceUrl).toBe("https://api.example.com/resource"); + }); + + it("EIP-712 signature recovers correct signer", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + + const offer = await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + p => account.signTypedData(p), + ); + + const recovered = await recoverTypedDataAddress({ + domain: createOfferDomain(), + types: OFFER_TYPES, + primaryType: "Offer", + message: prepareOfferForEIP712(offer.payload), + signature: offer.signature as Hex, + }); + + expect(recovered.toLowerCase()).toBe(account.address.toLowerCase()); + }); + }); +}); + +describe("Attestation Helper", () => { + describe("convertNetworkStringToCAIP2", () => { + it("passes through CAIP-2 format unchanged", () => { + expect(convertNetworkStringToCAIP2("eip155:8453")).toBe("eip155:8453"); + expect(convertNetworkStringToCAIP2("eip155:1")).toBe("eip155:1"); + expect(convertNetworkStringToCAIP2("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp")).toBe( + "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + ); + }); + + it("converts v1 Solana network names", () => { + expect(convertNetworkStringToCAIP2("solana")).toBe("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"); + expect(convertNetworkStringToCAIP2("Solana")).toBe("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"); + expect(convertNetworkStringToCAIP2("solana-devnet")).toBe( + "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + ); + }); + + it("converts v1 EVM network names to CAIP-2", () => { + expect(convertNetworkStringToCAIP2("base")).toBe("eip155:8453"); + expect(convertNetworkStringToCAIP2("base-sepolia")).toBe("eip155:84532"); + expect(convertNetworkStringToCAIP2("ethereum")).toBe("eip155:1"); + expect(convertNetworkStringToCAIP2("polygon")).toBe("eip155:137"); + expect(convertNetworkStringToCAIP2("avalanche")).toBe("eip155:43114"); + }); + + it("throws for unknown network identifiers", () => { + expect(() => convertNetworkStringToCAIP2("unknown-network")).toThrow( + 'Unknown network identifier: "unknown-network"', + ); + expect(() => convertNetworkStringToCAIP2("foo")).toThrow('Unknown network identifier: "foo"'); + }); + }); + + describe("extractChainIdFromCAIP2", () => { + it("extracts chain ID from EVM networks", () => { + expect(extractChainIdFromCAIP2("eip155:8453")).toBe(8453); + expect(extractChainIdFromCAIP2("eip155:1")).toBe(1); + expect(extractChainIdFromCAIP2("eip155:137")).toBe(137); + }); + + it("returns undefined for non-EVM networks", () => { + expect(extractChainIdFromCAIP2("solana:mainnet")).toBeUndefined(); + expect(extractChainIdFromCAIP2("cosmos:cosmoshub-4")).toBeUndefined(); + }); + }); + + describe("extractReceiptPayload", () => { + it("extracts payload from JWS receipt", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + + const payload = extractReceiptPayload(receipt); + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.payer).toBe("0x857b06519E91e3A54538791bDbb0E22373e36b66"); + expect(typeof payload.issuedAt).toBe("number"); + }); + + it("extracts payload from EIP-712 receipt", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + const receipt = await createReceiptEIP712( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + p => account.signTypedData(p), + ); + + const payload = extractReceiptPayload(receipt); + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.payer).toBe("0x857b06519E91e3A54538791bDbb0E22373e36b66"); + }); + }); +}); + +describe("Client Utilities", () => { + describe("extractOffersFromPaymentRequired", () => { + it("extracts offers from PaymentRequired extensions", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer1 = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const paymentRequired = { + accepts: [ + { + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + ], + extensions: { + [OFFER_RECEIPT]: { + info: { + offers: [offer1], + }, + }, + }, + }; + + const offers = extractOffersFromPaymentRequired(paymentRequired as any); + expect(offers).toHaveLength(1); + expect(offers[0].format).toBe("jws"); + }); + + it("returns empty array when no offers present", () => { + const paymentRequired = { + accepts: [], + extensions: {}, + }; + + const offers = extractOffersFromPaymentRequired(paymentRequired as any); + expect(offers).toEqual([]); + }); + + it("returns empty array when extensions is undefined", () => { + const paymentRequired = { + accepts: [], + }; + + const offers = extractOffersFromPaymentRequired(paymentRequired as any); + expect(offers).toEqual([]); + }); + }); + + describe("decodeSignedOffers", () => { + it("decodes JWS offers with payload fields at top level", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer]); + expect(decoded).toHaveLength(1); + expect(decoded[0].network).toBe("eip155:8453"); + expect(decoded[0].amount).toBe("10000"); + expect(decoded[0].format).toBe("jws"); + expect(decoded[0].acceptIndex).toBe(0); + expect(decoded[0].signedOffer).toBe(offer); + }); + + it("decodes EIP-712 offers", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + const offer = await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 1, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "5000", + }, + p => account.signTypedData(p), + ); + + const decoded = decodeSignedOffers([offer]); + expect(decoded).toHaveLength(1); + expect(decoded[0].network).toBe("eip155:8453"); + expect(decoded[0].amount).toBe("5000"); + expect(decoded[0].format).toBe("eip712"); + expect(decoded[0].acceptIndex).toBe(1); + }); + + it("returns empty array for empty input", () => { + const decoded = decodeSignedOffers([]); + expect(decoded).toEqual([]); + }); + }); + + describe("findAcceptsObjectFromSignedOffer", () => { + it("finds matching accepts entry using acceptIndex hint", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const accepts = [ + { + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + maxAmountRequired: "10000", + }, + ]; + + const found = findAcceptsObjectFromSignedOffer(offer, accepts as any); + expect(found).toBeDefined(); + expect(found?.network).toBe("eip155:8453"); + }); + + it("finds matching accepts entry with DecodedOffer", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + const accepts = [ + { + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + maxAmountRequired: "10000", + }, + ]; + + const found = findAcceptsObjectFromSignedOffer(decoded, accepts as any); + expect(found).toBeDefined(); + expect(found?.network).toBe("eip155:8453"); + }); + + it("falls back to searching all accepts when hint misses", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 5, // Wrong index + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const accepts = [ + { + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + maxAmountRequired: "10000", + }, + ]; + + const found = findAcceptsObjectFromSignedOffer(offer, accepts as any); + expect(found).toBeDefined(); + expect(found?.network).toBe("eip155:8453"); + }); + + it("returns undefined when no match found", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const accepts = [ + { + scheme: "exact", + network: "eip155:1", // Different network + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + maxAmountRequired: "10000", + }, + ]; + + const found = findAcceptsObjectFromSignedOffer(offer, accepts as any); + expect(found).toBeUndefined(); + }); + }); + + describe("extractReceiptFromResponse", () => { + it("extracts receipt from PAYMENT-RESPONSE header", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + + const settlementResponse = { + success: true, + extensions: { + [OFFER_RECEIPT]: { + info: { receipt }, + }, + }, + }; + + const headers = new Headers(); + headers.set("PAYMENT-RESPONSE", btoa(JSON.stringify(settlementResponse))); + + const response = new Response("OK", { headers }); + const extracted = extractReceiptFromResponse(response); + + expect(extracted).toBeDefined(); + expect(extracted?.format).toBe("jws"); + }); + + it("extracts receipt from X-PAYMENT-RESPONSE header", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + + const settlementResponse = { + success: true, + extensions: { + [OFFER_RECEIPT]: { + info: { receipt }, + }, + }, + }; + + const headers = new Headers(); + headers.set("X-PAYMENT-RESPONSE", btoa(JSON.stringify(settlementResponse))); + + const response = new Response("OK", { headers }); + const extracted = extractReceiptFromResponse(response); + + expect(extracted).toBeDefined(); + expect(extracted?.format).toBe("jws"); + }); + + it("returns undefined when no header present", () => { + const response = new Response("OK"); + const extracted = extractReceiptFromResponse(response); + expect(extracted).toBeUndefined(); + }); + + it("returns undefined when header has no receipt", () => { + const settlementResponse = { + success: true, + extensions: {}, + }; + + const headers = new Headers(); + headers.set("PAYMENT-RESPONSE", btoa(JSON.stringify(settlementResponse))); + + const response = new Response("OK", { headers }); + const extracted = extractReceiptFromResponse(response); + expect(extracted).toBeUndefined(); + }); + }); + + describe("verifyReceiptMatchesOffer", () => { + it("returns true when receipt matches offer and payer", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const payerAddress = "0x857b06519E91e3A54538791bDbb0E22373e36b66"; + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: payerAddress, + network: "eip155:8453", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + const result = verifyReceiptMatchesOffer(receipt, decoded, [payerAddress]); + expect(result).toBe(true); + }); + + it("returns true with case-insensitive payer address match", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + // Uppercase payer address should still match + const result = verifyReceiptMatchesOffer(receipt, decoded, [ + "0x857B06519E91E3A54538791BDBB0E22373E36B66", + ]); + expect(result).toBe(true); + }); + + it("returns false when resourceUrl does not match", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const payerAddress = "0x857b06519E91e3A54538791bDbb0E22373e36b66"; + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/different-resource", + payer: payerAddress, + network: "eip155:8453", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + const result = verifyReceiptMatchesOffer(receipt, decoded, [payerAddress]); + expect(result).toBe(false); + }); + + it("returns false when network does not match", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const payerAddress = "0x857b06519E91e3A54538791bDbb0E22373e36b66"; + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: payerAddress, + network: "eip155:1", // Different network + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + const result = verifyReceiptMatchesOffer(receipt, decoded, [payerAddress]); + expect(result).toBe(false); + }); + + it("returns false when payer does not match any address", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + // Different payer address + const result = verifyReceiptMatchesOffer(receipt, decoded, [ + "0xDifferentAddress1234567890123456789012345", + ]); + expect(result).toBe(false); + }); + + it("returns true when payer matches one of multiple addresses", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const payerAddress = "0x857b06519E91e3A54538791bDbb0E22373e36b66"; + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: payerAddress, + network: "eip155:8453", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + // Multiple addresses, one matches + const result = verifyReceiptMatchesOffer(receipt, decoded, [ + "0xOtherAddress12345678901234567890123456789", + payerAddress, + "SolanaAddressHere", + ]); + expect(result).toBe(true); + }); + }); +}); + +describe("Utility Functions", () => { + describe("hashCanonical", () => { + it("returns SHA-256 hash of canonicalized object", async () => { + const hash = await hashCanonical({ b: 2, a: 1 }); + expect(hash).toBeInstanceOf(Uint8Array); + expect(hash.length).toBe(32); // SHA-256 produces 32 bytes + }); + + it("produces same hash for equivalent objects with different key order", async () => { + const hash1 = await hashCanonical({ z: 1, a: 2 }); + const hash2 = await hashCanonical({ a: 2, z: 1 }); + expect(hash1).toEqual(hash2); + }); + + it("produces different hashes for different objects", async () => { + const hash1 = await hashCanonical({ a: 1 }); + const hash2 = await hashCanonical({ a: 2 }); + expect(hash1).not.toEqual(hash2); + }); + }); + + describe("getCanonicalBytes", () => { + it("returns UTF-8 encoded canonical JSON", () => { + const bytes = getCanonicalBytes({ b: 2, a: 1 }); + expect(bytes).toBeInstanceOf(Uint8Array); + const decoded = new TextDecoder().decode(bytes); + expect(decoded).toBe('{"a":1,"b":2}'); + }); + + it("handles nested objects", () => { + const bytes = getCanonicalBytes({ outer: { z: 1, a: 2 } }); + const decoded = new TextDecoder().decode(bytes); + expect(decoded).toBe('{"outer":{"a":2,"z":1}}'); + }); + }); + + describe("hashOfferTypedData", () => { + it("returns EIP-712 hash for offer payload", () => { + const payload: OfferPayload = { + version: 1, + resourceUrl: "https://api.example.com/resource", + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + validUntil: 1700000000, + }; + const hash = hashOfferTypedData(payload); + expect(hash).toMatch(/^0x[a-fA-F0-9]{64}$/); + }); + }); + + describe("hashReceiptTypedData", () => { + it("returns EIP-712 hash for receipt payload", () => { + const payload: ReceiptPayload = { + version: 1, + network: "eip155:8453", + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + issuedAt: 1700000000, + transaction: "", + }; + const hash = hashReceiptTypedData(payload); + expect(hash).toMatch(/^0x[a-fA-F0-9]{64}$/); + }); + + it("produces different hashes for different payloads", () => { + const payload1: ReceiptPayload = { + version: 1, + network: "eip155:8453", + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + issuedAt: 1700000000, + transaction: "", + }; + const payload2: ReceiptPayload = { + ...payload1, + payer: "0x1234567890123456789012345678901234567890", + }; + const hash1 = hashReceiptTypedData(payload1); + const hash2 = hashReceiptTypedData(payload2); + expect(hash1).not.toBe(hash2); + }); + }); + + describe("extractEIP155ChainId", () => { + it("extracts chain ID from valid eip155 network string", () => { + expect(extractEIP155ChainId("eip155:8453")).toBe(8453); + expect(extractEIP155ChainId("eip155:1")).toBe(1); + expect(extractEIP155ChainId("eip155:137")).toBe(137); + }); + + it("throws for non-eip155 networks", () => { + expect(() => extractEIP155ChainId("solana:mainnet")).toThrow( + 'Invalid network format: solana:mainnet. Expected "eip155:"', + ); + }); + + it("throws for malformed eip155 strings", () => { + expect(() => extractEIP155ChainId("eip155:")).toThrow( + 'Invalid network format: eip155:. Expected "eip155:"', + ); + expect(() => extractEIP155ChainId("eip155:abc")).toThrow( + 'Invalid network format: eip155:abc. Expected "eip155:"', + ); + }); + + it("throws for strings without colon", () => { + expect(() => extractEIP155ChainId("base")).toThrow( + 'Invalid network format: base. Expected "eip155:"', + ); + }); + }); + + describe("createReceiptDomain", () => { + it("creates receipt domain with correct name, version, and canonical chainId", () => { + const domain = createReceiptDomain(); + expect(domain.name).toBe("x402 receipt"); + expect(domain.version).toBe("1"); + expect(domain.chainId).toBe(1); + }); + }); + + describe("prepareReceiptForEIP712", () => { + it("converts receipt payload to EIP-712 message format", () => { + const payload: ReceiptPayload = { + version: 1, + network: "eip155:8453", + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + issuedAt: 1700000000, + transaction: "0xabc123", + }; + const prepared = prepareReceiptForEIP712(payload); + expect(prepared.version).toBe(BigInt(1)); + expect(prepared.network).toBe("eip155:8453"); + expect(prepared.resourceUrl).toBe("https://api.example.com/resource"); + expect(prepared.payer).toBe("0x857b06519E91e3A54538791bDbb0E22373e36b66"); + expect(prepared.issuedAt).toBe(BigInt(1700000000)); + expect(prepared.transaction).toBe("0xabc123"); + }); + }); + + describe("RECEIPT_TYPES", () => { + it("has correct EIP-712 type definition", () => { + expect(RECEIPT_TYPES.Receipt).toBeDefined(); + expect(RECEIPT_TYPES.Receipt).toHaveLength(6); + const fieldNames = RECEIPT_TYPES.Receipt.map(f => f.name); + expect(fieldNames).toContain("version"); + expect(fieldNames).toContain("network"); + expect(fieldNames).toContain("resourceUrl"); + expect(fieldNames).toContain("payer"); + expect(fieldNames).toContain("issuedAt"); + expect(fieldNames).toContain("transaction"); + }); + }); +}); + +describe("Server Extension Utilities", () => { + describe("declareOfferReceiptExtension", () => { + it("returns extension declaration with default values", () => { + const declaration = declareOfferReceiptExtension(); + expect(declaration).toHaveProperty(OFFER_RECEIPT); + expect(declaration[OFFER_RECEIPT].includeTxHash).toBeUndefined(); + expect(declaration[OFFER_RECEIPT].offerValiditySeconds).toBeUndefined(); + }); + + it("returns extension declaration with custom config", () => { + const declaration = declareOfferReceiptExtension({ + includeTxHash: true, + offerValiditySeconds: 120, + }); + expect(declaration[OFFER_RECEIPT].includeTxHash).toBe(true); + expect(declaration[OFFER_RECEIPT].offerValiditySeconds).toBe(120); + }); + }); + + describe("createJWSOfferReceiptIssuer", () => { + it("creates issuer with correct properties", async () => { + const keyPair = await generateES256KKeyPair(); + const jwsSigner = await createES256KSigner( + keyPair.privateKey, + "did:web:api.example.com#key-1", + ); + + const issuer = createJWSOfferReceiptIssuer("did:web:api.example.com#key-1", jwsSigner); + + expect(issuer.kid).toBe("did:web:api.example.com#key-1"); + expect(issuer.format).toBe("jws"); + expect(typeof issuer.issueOffer).toBe("function"); + expect(typeof issuer.issueReceipt).toBe("function"); + }); + + it("issueOffer creates valid JWS offer", async () => { + const keyPair = await generateES256KKeyPair(); + const jwsSigner = await createES256KSigner( + keyPair.privateKey, + "did:web:api.example.com#key-1", + ); + const issuer = createJWSOfferReceiptIssuer("did:web:api.example.com#key-1", jwsSigner); + + const offer = await issuer.issueOffer("https://api.example.com/resource", { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }); + + expect(offer.format).toBe("jws"); + expect(offer.signature).toMatch(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/); + }); + + it("issueReceipt creates valid JWS receipt", async () => { + const keyPair = await generateES256KKeyPair(); + const jwsSigner = await createES256KSigner( + keyPair.privateKey, + "did:web:api.example.com#key-1", + ); + const issuer = createJWSOfferReceiptIssuer("did:web:api.example.com#key-1", jwsSigner); + + const receipt = await issuer.issueReceipt( + "https://api.example.com/resource", + "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "eip155:8453", + "0xabc123", + ); + + expect(receipt.format).toBe("jws"); + expect(receipt.signature).toMatch(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/); + }); + }); + + describe("createEIP712OfferReceiptIssuer", () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + + it("creates issuer with correct properties", () => { + const issuer = createEIP712OfferReceiptIssuer(`did:pkh:eip155:8453:${account.address}`, p => + account.signTypedData(p), + ); + + expect(issuer.kid).toBe(`did:pkh:eip155:8453:${account.address}`); + expect(issuer.format).toBe("eip712"); + expect(typeof issuer.issueOffer).toBe("function"); + expect(typeof issuer.issueReceipt).toBe("function"); + }); + + it("issueOffer creates valid EIP-712 offer", async () => { + const issuer = createEIP712OfferReceiptIssuer(`did:pkh:eip155:8453:${account.address}`, p => + account.signTypedData(p), + ); + + const offer = await issuer.issueOffer("https://api.example.com/resource", { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }); + + expect(offer.format).toBe("eip712"); + expect(offer).toHaveProperty("payload"); + expect(offer.signature).toMatch(/^0x[a-fA-F0-9]{130}$/); + }); + + it("issueReceipt creates valid EIP-712 receipt", async () => { + const issuer = createEIP712OfferReceiptIssuer(`did:pkh:eip155:8453:${account.address}`, p => + account.signTypedData(p), + ); + + const receipt = await issuer.issueReceipt( + "https://api.example.com/resource", + "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "eip155:8453", + "0xabc123", + ); + + expect(receipt.format).toBe("eip712"); + expect(receipt).toHaveProperty("payload"); + expect(receipt.signature).toMatch(/^0x[a-fA-F0-9]{130}$/); + }); + }); + + /** + * NOTE: createOfferReceiptExtension is not tested here because it requires + * a mock ResourceServer with PaymentRequiredContext and SettleResultContext. + * The extension hooks (enrichPaymentRequiredResponse, enrichSettlementResponse) + * depend on the full server context which would require significant mocking. + * The signer factories above test the core signing functionality. + */ +}); + +// ============================================================================ +// Signature Verification Tests +// ============================================================================ + +describe("Signature Verification", () => { + describe("EIP-712 Verification", () => { + describe("verifyOfferSignatureEIP712", () => { + it("should verify a valid EIP-712 signed offer and recover signer", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + + const offer = await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + amount: "10000", + }, + p => account.signTypedData(p), + ); + + const result = await verifyOfferSignatureEIP712(offer); + + expect(result.signer.toLowerCase()).toBe(account.address.toLowerCase()); + expect(result.payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(result.payload.scheme).toBe("exact"); + expect(result.payload.network).toBe("eip155:8453"); + expect(result.payload.amount).toBe("10000"); + }); + + it("should throw for wrong format", async () => { + const invalidOffer = { + format: "jws", + signature: "test.jws.signature", + } as unknown as EIP712SignedOffer; + + await expect(verifyOfferSignatureEIP712(invalidOffer)).rejects.toThrow( + "Expected eip712 format", + ); + }); + + it("should throw for invalid offer payload", async () => { + const invalidOffer = { + format: "eip712", + payload: null, + signature: "0x1234", + } as unknown as EIP712SignedOffer; + + await expect(verifyOfferSignatureEIP712(invalidOffer)).rejects.toThrow( + "Invalid offer: missing or malformed payload", + ); + }); + + it("should recover different address for tampered signature", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + + const offer = await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "native", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + amount: "10000", + }, + p => account.signTypedData(p), + ); + + // Tamper with the signature + const tamperedOffer = { + ...offer, + signature: offer.signature.slice(0, -4) + "0000", + }; + + // Should recover a different address (not throw) + const result = await verifyOfferSignatureEIP712(tamperedOffer); + expect(result.signer).toBeDefined(); + // The recovered address will likely be different + }); + }); + + describe("verifyReceiptSignatureEIP712", () => { + it("should verify a valid EIP-712 signed receipt and recover signer", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + + const receipt = await createReceiptEIP712( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + transaction: "0x1234567890abcdef", + }, + p => account.signTypedData(p), + ); + + const result = await verifyReceiptSignatureEIP712(receipt); + + expect(result.signer.toLowerCase()).toBe(account.address.toLowerCase()); + expect(result.payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(result.payload.payer).toBe("0x857b06519E91e3A54538791bDbb0E22373e36b66"); + expect(result.payload.network).toBe("eip155:8453"); + }); + + it("should throw for wrong format", async () => { + const invalidReceipt = { + format: "jws", + signature: "test.jws.signature", + } as unknown as EIP712SignedReceipt; + + await expect(verifyReceiptSignatureEIP712(invalidReceipt)).rejects.toThrow( + "Expected eip712 format", + ); + }); + + it("should throw for invalid receipt payload", async () => { + const invalidReceipt = { + format: "eip712", + payload: { version: 1 }, // missing payer + signature: "0x1234", + } as unknown as EIP712SignedReceipt; + + await expect(verifyReceiptSignatureEIP712(invalidReceipt)).rejects.toThrow( + "Invalid receipt: missing or malformed payload", + ); + }); + }); + }); + + describe("JWS Verification", () => { + describe("verifyOfferSignatureJWS", () => { + it("should verify a JWS signed offer with explicit public key", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + amount: "10000", + }, + signer, + ); + + // Pass JWK directly - function accepts both KeyLike and JWK + const payload = await verifyOfferSignatureJWS(offer, keyPair.publicKey); + + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.scheme).toBe("exact"); + expect(payload.amount).toBe("10000"); + }); + + it("should verify a JWS signed offer with JWK public key", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "native", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + amount: "5000", + }, + signer, + ); + + const payload = await verifyOfferSignatureJWS(offer, keyPair.publicKey); + + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.amount).toBe("5000"); + }); + + it("should verify a JWS signed offer by extracting key from did:jwk kid", async () => { + const keyPair = await generateES256KKeyPair(); + // Create signer with did:jwk kid (self-contained key) + const kid = `did:jwk:${jose.base64url.encode(JSON.stringify(keyPair.publicKey))}#0`; + const signer = await createES256KSigner(keyPair.privateKey, kid); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "native", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + amount: "7500", + }, + signer, + ); + + // No public key provided - should extract from kid + const payload = await verifyOfferSignatureJWS(offer); + + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.amount).toBe("7500"); + }); + + it("should throw for wrong format", async () => { + const invalidOffer = { + format: "eip712", + payload: {}, + signature: "0x1234", + } as unknown as JWSSignedOffer; + + await expect(verifyOfferSignatureJWS(invalidOffer)).rejects.toThrow("Expected jws format"); + }); + + it("should throw for invalid JWS signature", async () => { + const keyPair = await generateES256KKeyPair(); + + const invalidOffer: JWSSignedOffer = { + format: "jws", + signature: "invalid.jws.signature", + }; + + // Pass JWK directly + await expect(verifyOfferSignatureJWS(invalidOffer, keyPair.publicKey)).rejects.toThrow(); + }); + + it("should throw when no key provided and kid missing", async () => { + const { privateKey } = await jose.generateKeyPair("ES256K"); + const payload = JSON.stringify({ version: 1, resourceUrl: "test" }); + const jws = await new jose.CompactSign(new TextEncoder().encode(payload)) + .setProtectedHeader({ alg: "ES256K" }) // No kid + .sign(privateKey); + + const offer: JWSSignedOffer = { format: "jws", signature: jws }; + + await expect(verifyOfferSignatureJWS(offer)).rejects.toThrow( + "No public key provided and JWS header missing kid", + ); + }); + }); + + describe("verifyReceiptSignatureJWS", () => { + it("should verify a JWS signed receipt", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + + // Pass JWK directly + const payload = await verifyReceiptSignatureJWS(receipt, keyPair.publicKey); + + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.payer).toBe("0x857b06519E91e3A54538791bDbb0E22373e36b66"); + expect(payload.network).toBe("eip155:8453"); + }); + + it("should verify a JWS signed receipt by extracting key from did:jwk kid", async () => { + const keyPair = await generateES256KKeyPair(); + const kid = `did:jwk:${jose.base64url.encode(JSON.stringify(keyPair.publicKey))}#0`; + const signer = await createES256KSigner(keyPair.privateKey, kid); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + transaction: "0xabcdef", + }, + signer, + ); + + // No public key provided - should extract from kid + const payload = await verifyReceiptSignatureJWS(receipt); + + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.transaction).toBe("0xabcdef"); + }); + }); + }); +}); + +// ============================================================================ +// DID Key Resolution Tests +// ============================================================================ + +describe("extractPublicKeyFromKid", () => { + describe("did:jwk", () => { + it("should extract key from did:jwk", async () => { + const { publicKey } = await jose.generateKeyPair("ES256K"); + const jwk = await jose.exportJWK(publicKey); + const kid = `did:jwk:${jose.base64url.encode(JSON.stringify(jwk))}`; + + const extractedKey = await extractPublicKeyFromKid(kid); + expect(extractedKey).toBeDefined(); + }); + + it("should handle did:jwk with fragment", async () => { + const { publicKey } = await jose.generateKeyPair("ES256"); + const jwk = await jose.exportJWK(publicKey); + const kid = `did:jwk:${jose.base64url.encode(JSON.stringify(jwk))}#key-1`; + + const extractedKey = await extractPublicKeyFromKid(kid); + expect(extractedKey).toBeDefined(); + }); + }); + + describe("did:key", () => { + it("should extract Ed25519 key from did:key", async () => { + // Known Ed25519 did:key (from did-key spec examples) + const kid = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"; + + const extractedKey = await extractPublicKeyFromKid(kid); + expect(extractedKey).toBeDefined(); + }); + }); + + describe("error cases", () => { + it("should throw for invalid DID format", async () => { + await expect(extractPublicKeyFromKid("not-a-did")).rejects.toThrow("Invalid DID format"); + }); + + it("should throw for unsupported DID method", async () => { + await expect(extractPublicKeyFromKid("did:unsupported:123")).rejects.toThrow( + 'Unsupported DID method "unsupported"', + ); + }); + + it("should throw for did:key with unsupported multibase", async () => { + await expect(extractPublicKeyFromKid("did:key:f1234")).rejects.toThrow( + "Unsupported multibase encoding", + ); + }); + }); + + describe("did:web", () => { + let originalFetch: typeof global.fetch; + + beforeEach(() => { + originalFetch = global.fetch; + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it("should resolve did:web by fetching DID document", async () => { + const { publicKey } = await jose.generateKeyPair("ES256K"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [ + { + id: "did:web:api.example.com#key-1", + type: "JsonWebKey2020", + controller: "did:web:api.example.com", + publicKeyJwk: jwk, + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:api.example.com#key-1"); + expect(extractedKey).toBeDefined(); + expect(global.fetch).toHaveBeenCalledWith( + "https://api.example.com/.well-known/did.json", + expect.any(Object), + ); + }); + + it("should resolve did:web with path", async () => { + const { publicKey } = await jose.generateKeyPair("ES256"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:example.com:users:alice", + verificationMethod: [ + { + id: "did:web:example.com:users:alice#key-1", + type: "JsonWebKey2020", + controller: "did:web:example.com:users:alice", + publicKeyJwk: jwk, + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:example.com:users:alice#key-1"); + expect(extractedKey).toBeDefined(); + expect(global.fetch).toHaveBeenCalledWith( + "https://example.com/users/alice/did.json", + expect.any(Object), + ); + }); + + it("should use http:// for did:web:localhost", async () => { + const { publicKey } = await jose.generateKeyPair("ES256"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:localhost%3A3000", + verificationMethod: [ + { + id: "did:web:localhost%3A3000#key-1", + type: "JsonWebKey2020", + controller: "did:web:localhost%3A3000", + publicKeyJwk: jwk, + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:localhost%3A3000#key-1"); + expect(extractedKey).toBeDefined(); + expect(global.fetch).toHaveBeenCalledWith( + "http://localhost:3000/.well-known/did.json", + expect.any(Object), + ); + }); + + it("should use http:// for did:web:127.0.0.1", async () => { + const { publicKey } = await jose.generateKeyPair("ES256"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:127.0.0.1%3A8080", + verificationMethod: [ + { + id: "did:web:127.0.0.1%3A8080#key-1", + type: "JsonWebKey2020", + controller: "did:web:127.0.0.1%3A8080", + publicKeyJwk: jwk, + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:127.0.0.1%3A8080#key-1"); + expect(extractedKey).toBeDefined(); + expect(global.fetch).toHaveBeenCalledWith( + "http://127.0.0.1:8080/.well-known/did.json", + expect.any(Object), + ); + }); + + it("should still use https:// for non-localhost domains", async () => { + const { publicKey } = await jose.generateKeyPair("ES256"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:example.com", + verificationMethod: [ + { + id: "did:web:example.com#key-1", + type: "JsonWebKey2020", + controller: "did:web:example.com", + publicKeyJwk: jwk, + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:example.com#key-1"); + expect(extractedKey).toBeDefined(); + expect(global.fetch).toHaveBeenCalledWith( + "https://example.com/.well-known/did.json", + expect.any(Object), + ); + }); + + it("should throw when did:web fetch fails", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 404, + statusText: "Not Found", + }); + + await expect(extractPublicKeyFromKid("did:web:nonexistent.example.com")).rejects.toThrow( + "Failed to resolve did:web", + ); + }); + + it("should throw when verification method not found", async () => { + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + await expect(extractPublicKeyFromKid("did:web:api.example.com#nonexistent")).rejects.toThrow( + "No verification method found", + ); + }); + + // Malformed DID Document Tests + + it("should throw when DID document has no verificationMethod array", async () => { + const didDocument = { id: "did:web:api.example.com" }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + await expect(extractPublicKeyFromKid("did:web:api.example.com#key-1")).rejects.toThrow( + "No verification method found", + ); + }); + + it("should throw when verification method has no key material", async () => { + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [ + { + id: "did:web:api.example.com#key-1", + type: "JsonWebKey2020", + controller: "did:web:api.example.com", + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + await expect(extractPublicKeyFromKid("did:web:api.example.com#key-1")).rejects.toThrow( + "has no supported key format", + ); + }); + + it("should throw when fetch returns invalid JSON", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.reject(new Error("Invalid JSON")), + }); + + await expect(extractPublicKeyFromKid("did:web:api.example.com")).rejects.toThrow( + "Failed to resolve did:web", + ); + }); + + it("should throw when network error occurs", async () => { + global.fetch = vi.fn().mockRejectedValue(new Error("Network error")); + + await expect(extractPublicKeyFromKid("did:web:api.example.com")).rejects.toThrow( + "Failed to resolve did:web", + ); + }); + + // DID Document structure variations + + it("should resolve key from assertionMethod reference", async () => { + const { publicKey } = await jose.generateKeyPair("ES256K"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [ + { + id: "did:web:api.example.com#key-1", + type: "JsonWebKey2020", + controller: "did:web:api.example.com", + publicKeyJwk: jwk, + }, + ], + assertionMethod: ["did:web:api.example.com#key-1"], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:api.example.com"); + expect(extractedKey).toBeDefined(); + }); + + it("should resolve key from authentication reference", async () => { + const { publicKey } = await jose.generateKeyPair("ES256K"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [ + { + id: "did:web:api.example.com#auth-key", + type: "JsonWebKey2020", + controller: "did:web:api.example.com", + publicKeyJwk: jwk, + }, + ], + authentication: ["did:web:api.example.com#auth-key"], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:api.example.com"); + expect(extractedKey).toBeDefined(); + }); + + it("should resolve embedded verification method in assertionMethod", async () => { + const { publicKey } = await jose.generateKeyPair("ES256K"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [], + assertionMethod: [ + { + id: "did:web:api.example.com#embedded-key", + type: "JsonWebKey2020", + controller: "did:web:api.example.com", + publicKeyJwk: jwk, + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:api.example.com"); + expect(extractedKey).toBeDefined(); + }); + + it("should handle publicKeyMultibase format in did:web", async () => { + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [ + { + id: "did:web:api.example.com#key-1", + type: "Ed25519VerificationKey2020", + controller: "did:web:api.example.com", + publicKeyMultibase: "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:api.example.com#key-1"); + expect(extractedKey).toBeDefined(); + }); + }); +}); + +// ============================================================================ +// Real DID Document Fixtures (captured from live endpoints) +// ============================================================================ + +describe("Real DID Document Fixtures", () => { + let originalFetch: typeof global.fetch; + + beforeEach(() => { + originalFetch = global.fetch; + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + // Captured from https://identity.foundation/.well-known/did.json (P-256 key) + const IDENTITY_FOUNDATION_DID_DOC = { + "@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"], + id: "did:web:identity.foundation", + verificationMethod: [ + { + id: "did:web:identity.foundation#XXS7zTsbIIAxgNlYEXJ4y810GFeLkYdqfK3ChhoQn7c", + type: "JsonWebKey2020", + controller: "did:web:identity.foundation", + publicKeyJwk: { + kty: "EC", + kid: "XXS7zTsbIIAxgNlYEXJ4y810GFeLkYdqfK3ChhoQn7c", + crv: "P-256", + alg: "ES256", + x: "TIIYSHfbBoXZi-B8Q5KBEmYpg6gXk0Getwt2nDPhxvI", + y: "zNbtUvyDHTdmtz3tyiw84UYgzma1X8r4ToP7PbCVHgI", + }, + }, + ], + authentication: ["did:web:identity.foundation#XXS7zTsbIIAxgNlYEXJ4y810GFeLkYdqfK3ChhoQn7c"], + assertionMethod: ["did:web:identity.foundation#XXS7zTsbIIAxgNlYEXJ4y810GFeLkYdqfK3ChhoQn7c"], + }; + + // Captured from https://demo.spruceid.com/.well-known/did.json (Ed25519 key) + const SPRUCE_DID_DOC = { + "@context": [ + "https://www.w3.org/ns/did/v1", + { "@id": "https://w3id.org/security#publicKeyJwk", "@type": "@json" }, + ], + id: "did:web:demo.spruceid.com", + verificationMethod: [ + { + id: "did:web:demo.spruceid.com#_t-v-Ep7AtkELhhvAzCCDzy1O5Bn_z1CVFv9yiRXdHY", + type: "Ed25519VerificationKey2018", + controller: "did:web:demo.spruceid.com", + publicKeyJwk: { + kty: "OKP", + crv: "Ed25519", + x: "2yv3J-Sf263OmwDLS9uFPTRD0PzbvfBGKLiSnPHtXIU", + }, + }, + ], + authentication: ["did:web:demo.spruceid.com#_t-v-Ep7AtkELhhvAzCCDzy1O5Bn_z1CVFv9yiRXdHY"], + assertionMethod: ["did:web:demo.spruceid.com#_t-v-Ep7AtkELhhvAzCCDzy1O5Bn_z1CVFv9yiRXdHY"], + }; + + it("should parse identity.foundation DID document (P-256)", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(IDENTITY_FOUNDATION_DID_DOC), + }); + + const key = await extractPublicKeyFromKid( + "did:web:identity.foundation#XXS7zTsbIIAxgNlYEXJ4y810GFeLkYdqfK3ChhoQn7c", + ); + expect(key).toBeDefined(); + }); + + it("should parse identity.foundation via assertionMethod (no fragment)", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(IDENTITY_FOUNDATION_DID_DOC), + }); + + const key = await extractPublicKeyFromKid("did:web:identity.foundation"); + expect(key).toBeDefined(); + }); + + it("should parse demo.spruceid.com DID document (Ed25519)", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(SPRUCE_DID_DOC), + }); + + const key = await extractPublicKeyFromKid( + "did:web:demo.spruceid.com#_t-v-Ep7AtkELhhvAzCCDzy1O5Bn_z1CVFv9yiRXdHY", + ); + expect(key).toBeDefined(); + }); + + it("should parse demo.spruceid.com via assertionMethod (no fragment)", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(SPRUCE_DID_DOC), + }); + + const key = await extractPublicKeyFromKid("did:web:demo.spruceid.com"); + expect(key).toBeDefined(); + }); +}); diff --git a/typescript/packages/extensions/test/sign-in-with-x.test.ts b/typescript/packages/extensions/test/sign-in-with-x.test.ts index 94284fe..004b9c3 100644 --- a/typescript/packages/extensions/test/sign-in-with-x.test.ts +++ b/typescript/packages/extensions/test/sign-in-with-x.test.ts @@ -31,27 +31,53 @@ import { type SolanaSigner, type EVMSigner, type EVMMessageVerifier, - type SIWxExtension, } from "../src/sign-in-with-x/index"; import { safeBase64Encode } from "@x402/core/utils"; import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; import nacl from "tweetnacl"; import { randomBytes } from "crypto"; +import type { SIWxExtension } from "../src/sign-in-with-x/index"; /** - * Helper to generate fresh time-based fields for tests. - * In production, these are generated by enrichPaymentRequiredResponse. + * Test-only helper: builds a complete SIWX extension with nonce/issuedAt. * - * @param expirationSeconds - Optional expiration duration in seconds - * @returns Time-based fields for SIWX extension + * @param opts - Challenge configuration + * @param opts.domain - Server domain + * @param opts.resourceUri - Full resource URI + * @param opts.network - CAIP-2 network identifier(s) + * @param opts.statement - Human-readable signing statement + * @param opts.expirationSeconds - Challenge TTL in seconds + * @returns Extension object keyed by "sign-in-with-x" */ -function generateTimeBasedFields(expirationSeconds?: number) { - const nonce = randomBytes(16).toString("hex"); - const issuedAt = new Date().toISOString(); - const expirationTime = expirationSeconds - ? new Date(Date.now() + expirationSeconds * 1000).toISOString() - : undefined; - return { nonce, issuedAt, expirationTime }; +function createTestChallenge(opts: { + domain: string; + resourceUri: string; + network: string | string[]; + statement?: string; + expirationSeconds?: number; +}): Record { + const networks = Array.isArray(opts.network) ? opts.network : [opts.network]; + return { + "sign-in-with-x": { + info: { + domain: opts.domain, + uri: opts.resourceUri, + version: "1", + nonce: randomBytes(16).toString("hex"), + issuedAt: new Date().toISOString(), + ...(opts.expirationSeconds !== undefined && { + expirationTime: new Date(Date.now() + opts.expirationSeconds * 1000).toISOString(), + }), + ...(opts.statement && { statement: opts.statement }), + resources: [opts.resourceUri], + }, + supportedChains: networks.map(n => ({ + chainId: n, + type: n.startsWith("solana:") ? ("ed25519" as const) : ("eip191" as const), + })), + schema: { header: "sign-in-with-x", type: "object" }, + }, + }; } const validPayload = { @@ -134,7 +160,7 @@ describe("Sign-In-With-X Extension", () => { }); describe("declareSIWxExtension", () => { - it("should create extension with supportedChains array (without time-based fields)", () => { + it("should create static declaration without time-based fields", () => { const result = declareSIWxExtension({ domain: "api.example.com", resourceUri: "https://api.example.com/data", @@ -149,11 +175,10 @@ describe("Sign-In-With-X Extension", () => { expect(extension.info.uri).toBe("https://api.example.com/data"); expect(extension.schema).toBeDefined(); - // Time-based fields are NOT generated by declareSIWxExtension - // They are generated per-request by enrichPaymentRequiredResponse + // Time-based fields are NOT generated by declareSIWxExtension; + // they are generated per-request by enrichPaymentRequiredResponse expect(extension.info.nonce).toBeUndefined(); expect(extension.info.issuedAt).toBeUndefined(); - expect(extension.info.expirationTime).toBeUndefined(); // Check supportedChains array expect(extension.supportedChains).toHaveLength(1); @@ -179,8 +204,9 @@ describe("Sign-In-With-X Extension", () => { expect(extension.supportedChains[1].chainId).toBe(SOLANA_DEVNET); expect(extension.supportedChains[1].type).toBe("ed25519"); - // Time-based fields are NOT generated - only _options are stored + // Static declaration — no time-based fields expect(extension.info.nonce).toBeUndefined(); + expect(extension.info.issuedAt).toBeUndefined(); expect(extension._options.expirationSeconds).toBe(300); }); @@ -255,7 +281,7 @@ describe("Sign-In-With-X Extension", () => { it("should sign and verify a message with a real wallet", async () => { const account = privateKeyToAccount(generatePrivateKey()); - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: "eip155:8453", @@ -263,10 +289,8 @@ describe("Sign-In-With-X Extension", () => { }); const ext = extension["sign-in-with-x"]; - // Add time-based fields (in production, enrichPaymentRequiredResponse does this) const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -285,17 +309,15 @@ describe("Sign-In-With-X Extension", () => { it("should reject tampered signature", async () => { const account = privateKeyToAccount(generatePrivateKey()); - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: "eip155:8453", }); const ext = extension["sign-in-with-x"]; - // Add time-based fields (in production, enrichPaymentRequiredResponse does this) const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -305,6 +327,36 @@ describe("Sign-In-With-X Extension", () => { const verification = await verifySIWxSignature(payload); expect(verification.valid).toBe(false); }); + + it("should work for auth-only endpoints (no enrichment)", async () => { + const account = privateKeyToAccount(generatePrivateKey()); + const resourceUri = "https://api.example.com/resource"; + + const extensions = createTestChallenge({ + domain: "api.example.com", + resourceUri, + network: "eip155:8453", + statement: "Sign in to access", + expirationSeconds: 300, + }); + + const ext = extensions["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + + const parsed = parseSIWxHeader(header); + const validation = await validateSIWxMessage(parsed, resourceUri); + expect(validation.valid).toBe(true); + + const result = await verifySIWxSignature(parsed); + expect(result.valid).toBe(true); + expect(result.address?.toLowerCase()).toBe(account.address.toLowerCase()); + }); }); describe("Smart wallet verification (evmVerifier option)", () => { @@ -312,7 +364,7 @@ describe("Sign-In-With-X Extension", () => { const mockVerifier: EVMMessageVerifier = vi.fn().mockResolvedValue(true); const account = privateKeyToAccount(generatePrivateKey()); - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: "eip155:8453", @@ -321,7 +373,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -343,7 +394,7 @@ describe("Sign-In-With-X Extension", () => { it("should fallback to EOA verification when no verifier provided", async () => { const account = privateKeyToAccount(generatePrivateKey()); - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: "eip155:8453", @@ -352,7 +403,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -368,7 +418,7 @@ describe("Sign-In-With-X Extension", () => { const mockVerifier: EVMMessageVerifier = vi.fn().mockResolvedValue(false); const account = privateKeyToAccount(generatePrivateKey()); - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: "eip155:8453", @@ -377,7 +427,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -395,7 +444,7 @@ describe("Sign-In-With-X Extension", () => { const mockVerifier: EVMMessageVerifier = vi.fn().mockRejectedValue(new Error("RPC error")); const account = privateKeyToAccount(generatePrivateKey()); - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: "eip155:8453", @@ -404,7 +453,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -428,7 +476,7 @@ describe("Sign-In-With-X Extension", () => { publicKey: address, }; - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: SOLANA_MAINNET, @@ -437,7 +485,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -798,7 +845,7 @@ describe("Sign-In-With-X Extension", () => { publicKey: address, }; - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: SOLANA_MAINNET, @@ -808,7 +855,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -834,7 +880,7 @@ describe("Sign-In-With-X Extension", () => { publicKey: { toBase58: () => address }, }; - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: SOLANA_DEVNET, @@ -843,7 +889,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -1046,7 +1091,7 @@ describe("SIWX Hooks", () => { storage.recordPayment("/resource", account.address); // Create valid SIWX header - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "example.com", resourceUri: "http://example.com/resource", network: "eip155:8453", @@ -1054,7 +1099,6 @@ describe("SIWX Hooks", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -1080,7 +1124,7 @@ describe("SIWX Hooks", () => { // Don't pre-record payment - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "example.com", resourceUri: "http://example.com/resource", network: "eip155:8453", @@ -1088,7 +1132,6 @@ describe("SIWX Hooks", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -1201,7 +1244,7 @@ describe("SIWX Hooks", () => { storage.recordPayment("/resource", account.address); // Create valid SIWX header - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "example.com", resourceUri: "http://example.com/resource", network: "eip155:8453", @@ -1209,7 +1252,6 @@ describe("SIWX Hooks", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -1242,7 +1284,7 @@ describe("SIWX Hooks", () => { storage.recordPayment("/resource", account.address); // Create valid SIWX header - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "example.com", resourceUri: "http://example.com/resource", network: "eip155:8453", @@ -1250,7 +1292,6 @@ describe("SIWX Hooks", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -1282,7 +1323,7 @@ describe("SIWX Hooks", () => { storage.recordPayment("/resource", account.address); // Create valid SIWX header - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "example.com", resourceUri: "http://example.com/resource", network: "eip155:8453", @@ -1290,7 +1331,6 @@ describe("SIWX Hooks", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -1322,6 +1362,127 @@ describe("SIWX Hooks", () => { expect(result2).toEqual({ grantAccess: true }); }); }); + + describe("auth-only routes (accepts: [])", () => { + it("should grant access with valid SIWX when accepts is empty array", async () => { + const storage = new InMemorySIWxStorage(); + const account = privateKeyToAccount(generatePrivateKey()); + + // Do NOT record any payment — auth-only should not require it + + const extension = createTestChallenge({ + domain: "example.com", + resourceUri: "http://example.com/profile", + network: "eip155:8453", + }); + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + + const hook = createSIWxRequestHook({ storage }); + const result = await hook( + { + adapter: { + getHeader: (name: string) => + name === "sign-in-with-x" || name === "SIGN-IN-WITH-X" ? header : undefined, + getUrl: () => "http://example.com/profile", + }, + path: "/profile", + }, + { accepts: [] }, + ); + + expect(result).toEqual({ grantAccess: true }); + }); + + it("should reject nonce replay on auth-only routes", async () => { + const base = new InMemorySIWxStorage(); + const usedNonces = new Set(); + const storage = { + ...base, + hasPaid: base.hasPaid.bind(base), + recordPayment: base.recordPayment.bind(base), + hasUsedNonce: (nonce: string) => usedNonces.has(nonce), + recordNonce: (nonce: string) => { + usedNonces.add(nonce); + }, + }; + + const account = privateKeyToAccount(generatePrivateKey()); + const events: SIWxHookEvent[] = []; + + const extension = createTestChallenge({ + domain: "example.com", + resourceUri: "http://example.com/profile", + network: "eip155:8453", + }); + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + + const hook = createSIWxRequestHook({ storage, onEvent: e => events.push(e) }); + const authOnlyRoute = { accepts: [] }; + const context = { + adapter: { + getHeader: (name: string) => + name === "sign-in-with-x" || name === "SIGN-IN-WITH-X" ? header : undefined, + getUrl: () => "http://example.com/profile", + }, + path: "/profile", + }; + + // First request should succeed + const result1 = await hook(context, authOnlyRoute); + expect(result1).toEqual({ grantAccess: true }); + + // Second request with same nonce should be rejected + const result2 = await hook(context, authOnlyRoute); + expect(result2).toBeUndefined(); + expect(events.some(e => e.type === "nonce_reused")).toBe(true); + }); + + it("should NOT grant access without routeConfig when address has not paid", async () => { + const storage = new InMemorySIWxStorage(); + const account = privateKeyToAccount(generatePrivateKey()); + + // No payment recorded, no routeConfig passed — should behave as before + const extension = createTestChallenge({ + domain: "example.com", + resourceUri: "http://example.com/resource", + network: "eip155:8453", + }); + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + + const hook = createSIWxRequestHook({ storage }); + const result = await hook({ + adapter: { + getHeader: (name: string) => + name === "sign-in-with-x" || name === "SIGN-IN-WITH-X" ? header : undefined, + getUrl: () => "http://example.com/resource", + }, + path: "/resource", + }); + + expect(result).toBeUndefined(); + }); + }); }); describe("createSIWxClientHook", () => { @@ -1340,29 +1501,16 @@ describe("SIWX Hooks", () => { const account = privateKeyToAccount(generatePrivateKey()); const hook = createSIWxClientHook(account); - const declaration = declareSIWxExtension({ + const challenge = createTestChallenge({ domain: "example.com", resourceUri: "http://example.com/resource", network: "eip155:1", }); - // Simulate what enrichPaymentRequiredResponse does: add time-based fields - const ext = declaration["sign-in-with-x"]; - const enrichedExtension = { - "sign-in-with-x": { - info: { - ...ext.info, - ...generateTimeBasedFields(300), - }, - supportedChains: ext.supportedChains, - schema: ext.schema, - }, - }; - const result = await hook({ paymentRequired: { accepts: [{ network: "eip155:1" }], - extensions: enrichedExtension, + extensions: challenge, }, }); @@ -1435,4 +1583,23 @@ describe("siwxResourceServerExtension", () => { expect(result.info.domain).toBe("api.example.com"); expect(result.info.uri).toBe("https://api.example.com/data"); }); + + it("should generate time-based fields from static declaration", async () => { + const declaration = declareSIWxExtension({ expirationSeconds: 300 }); + const ext = declaration["sign-in-with-x"]; + + // Static declaration has no nonce/issuedAt + expect(ext.info.nonce).toBeUndefined(); + expect(ext.info.issuedAt).toBeUndefined(); + + const result = (await siwxResourceServerExtension.enrichPaymentRequiredResponse!( + ext, + mockContext(["eip155:8453"]), + )) as SIWxExtension; + + // Enrichment generates fresh time-based fields + expect(result.info.nonce).toHaveLength(32); + expect(result.info.issuedAt).toBeDefined(); + expect(result.info.expirationTime).toBeDefined(); + }); }); diff --git a/typescript/packages/extensions/tsup.config.ts b/typescript/packages/extensions/tsup.config.ts index f082e07..bd85a73 100644 --- a/typescript/packages/extensions/tsup.config.ts +++ b/typescript/packages/extensions/tsup.config.ts @@ -5,6 +5,7 @@ const baseConfig = { index: "src/index.ts", "bazaar/index": "src/bazaar/index.ts", "sign-in-with-x/index": "src/sign-in-with-x/index.ts", + "offer-receipt/index": "src/offer-receipt/index.ts", "payment-identifier/index": "src/payment-identifier/index.ts", }, dts: { diff --git a/typescript/packages/http/axios/CHANGELOG.md b/typescript/packages/http/axios/CHANGELOG.md index ee014bc..31371ce 100644 --- a/typescript/packages/http/axios/CHANGELOG.md +++ b/typescript/packages/http/axios/CHANGELOG.md @@ -1,5 +1,60 @@ # @x402/axios Changelog +## 2.9.0 + +### Minor Changes + +- 2250cae: Migrated project from coinbase/x402 to x402-foundation/x402 organization + +### Patch Changes + +- Updated dependencies [8cf3fca] +- Updated dependencies [c0e3969] +- Updated dependencies [2250cae] +- Updated dependencies [d352574] + - @x402/core@2.9.0 + +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- Updated dependencies + - @x402/core@2.6.0 + +## 2.5.0 + +### Minor Changes + +- Updated dependencies [96a9db0] +- Updated dependencies [d0a2b11] +- Updated dependencies + - @x402/core@2.5.0 + +## 2.4.0 + +### Minor Changes + +- Updated dependencies [57a5488] +- Updated dependencies [018181b] +- Updated dependencies [3fb55d7] + - @x402/core@2.4.0 + ## 2.3.0 ### Minor Changes diff --git a/typescript/packages/http/axios/README.md b/typescript/packages/http/axios/README.md index 95ff9c3..6cd3b42 100644 --- a/typescript/packages/http/axios/README.md +++ b/typescript/packages/http/axios/README.md @@ -23,7 +23,7 @@ const account = privateKeyToAccount("0xYourPrivateKey"); const api = wrapAxiosWithPaymentFromConfig(axios.create(), { schemes: [ { - network: "eip155:8453", // Base Sepolia + network: "eip155:8453", // Base Mainnet client: new ExactEvmScheme(account), }, ], @@ -151,7 +151,7 @@ const api = wrapAxiosWithPaymentFromConfig(axios.create(), { schemes: [ // EVM chains { - network: "eip155:8453", // Base Sepolia + network: "eip155:8453", // Base Mainnet client: new ExactEvmScheme(evmAccount), }, // SVM chains diff --git a/typescript/packages/http/axios/package.json b/typescript/packages/http/axios/package.json index 7faa9fe..4dc73c0 100644 --- a/typescript/packages/http/axios/package.json +++ b/typescript/packages/http/axios/package.json @@ -1,6 +1,6 @@ { "name": "@x402/axios", - "version": "2.3.0", + "version": "2.9.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", @@ -17,8 +17,8 @@ }, "keywords": [], "license": "Apache-2.0", - "author": "Coinbase Inc.", - "repository": "https://github.com/coinbase/x402", + "author": "x402 Foundation", + "repository": "https://github.com/x402-foundation/x402", "description": "x402 Payment Protocol", "devDependencies": { "@eslint/js": "^9.24.0", diff --git a/typescript/packages/http/express/CHANGELOG.md b/typescript/packages/http/express/CHANGELOG.md index 3e5517f..52722a4 100644 --- a/typescript/packages/http/express/CHANGELOG.md +++ b/typescript/packages/http/express/CHANGELOG.md @@ -1,5 +1,88 @@ # @x402/express Changelog +## 2.9.0 + +### Minor Changes + +- 2250cae: Migrated project from coinbase/x402 to x402-foundation/x402 organization +- d352574: Add SettlementOverrides support for partial settlement (upto scheme). Route handlers can call setSettlementOverrides() to settle less than the authorized maximum, enabling usage-based billing. + +### Patch Changes + +- Updated dependencies [8cf3fca] +- Updated dependencies [c0e3969] +- Updated dependencies [2250cae] +- Updated dependencies [d352574] + - @x402/core@2.9.0 + - @x402/paywall@2.9.0 + - @x402/extensions@2.9.0 + +## 2.8.0 + +### Minor Changes + +- 4c1e44f: Treat malformed facilitator success payloads as upstream facilitator errors and return 502 responses from framework middleware instead of flattening them into payment failures. +- Updated dependencies [4f2f4f3] +- Updated dependencies [067f297] +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/extensions@2.8.0 + - @x402/core@2.8.0 + - @x402/paywall@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [34d2442] +- Updated dependencies [8b731cb] +- Updated dependencies [f2bbb5c] +- Updated dependencies [8931cb3] +- Updated dependencies [34d2442] + - @x402/extensions@2.7.0 + - @x402/core@2.7.0 + - @x402/paywall@2.7.0 + +## 2.6.0 + +### Minor Changes + +- aeef1bf: Added dynamic function for servers to generate custom response for settlement failures defaulting to empty +- 205257b: Cleaned up dependencies +- 2564781: Include PAYMENT-RESPONSE header on settlement failure responses +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + - @x402/paywall@2.6.0 + +## 2.5.0 + +### Minor Changes + +- Updated dependencies [96a9db0] +- Updated dependencies [7fe268f] +- Updated dependencies [1ab1c86] +- Updated dependencies [d0a2b11] +- Updated dependencies + - @x402/core@2.5.0 + - @x402/extensions@2.5.0 + - @x402/paywall@2.4.1 + +## 2.4.0 + +### Minor Changes + +- Updated dependencies [57a5488] +- Updated dependencies [018181b] +- Updated dependencies [3fb55d7] + - @x402/core@2.4.0 + - @x402/extensions@2.4.0 + - @x402/paywall@2.4.0 + ## 2.3.0 ### Minor Changes diff --git a/typescript/packages/http/express/package.json b/typescript/packages/http/express/package.json index add96cb..a7378e6 100644 --- a/typescript/packages/http/express/package.json +++ b/typescript/packages/http/express/package.json @@ -1,6 +1,6 @@ { "name": "@x402/express", - "version": "2.3.0", + "version": "2.9.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", @@ -17,8 +17,8 @@ }, "keywords": [], "license": "Apache-2.0", - "author": "Coinbase Inc.", - "repository": "https://github.com/coinbase/x402", + "author": "x402 Foundation", + "repository": "https://github.com/x402-foundation/x402", "description": "x402 Payment Protocol", "devDependencies": { "@eslint/js": "^9.24.0", @@ -40,15 +40,13 @@ "vitest": "^3.0.5" }, "dependencies": { - "@coinbase/cdp-sdk": "^1.22.0", - "@solana/kit": "^2.1.1", "@x402/core": "workspace:~", "@x402/extensions": "workspace:~", "viem": "^2.39.3", "zod": "^3.24.2" }, "peerDependencies": { - "@x402/paywall": "workspace:*", + "@x402/paywall": "workspace:^", "express": "^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { diff --git a/typescript/packages/http/express/src/index.test.ts b/typescript/packages/http/express/src/index.test.ts index 6d46dd5..92947c9 100644 --- a/typescript/packages/http/express/src/index.test.ts +++ b/typescript/packages/http/express/src/index.test.ts @@ -7,6 +7,7 @@ import type { FacilitatorClient, } from "@x402/core/server"; import { + FacilitatorResponseError, x402ResourceServer, x402HTTPResourceServer as HTTPResourceServer, } from "@x402/core/server"; @@ -40,6 +41,28 @@ let mockRegisterPaywallProvider: ReturnType; let mockRequiresPayment: ReturnType; vi.mock("@x402/core/server", () => ({ + SETTLEMENT_OVERRIDES_HEADER: "Settlement-Overrides", + FacilitatorResponseError: class FacilitatorResponseError extends Error { + /** + * Creates a mock facilitator response error. + * + * @param message - Error message. + */ + constructor(message: string) { + super(message); + this.name = "FacilitatorResponseError"; + } + }, + getFacilitatorResponseError: (error: unknown) => { + let current = error; + while (current instanceof Error) { + if (current.name === "FacilitatorResponseError") { + return current; + } + current = (current as Error & { cause?: unknown }).cause; + } + return null; + }, x402ResourceServer: vi.fn().mockImplementation(() => ({ initialize: vi.fn().mockResolvedValue(undefined), registerExtension: vi.fn(), @@ -71,7 +94,15 @@ function setupMockHttpServer( processResult: HTTPProcessResult, settlementResult: | { success: true; headers: Record } - | { success: false; errorReason: string } = { success: true, headers: {} }, + | { + success: false; + errorReason: string; + headers: Record; + response: { status: number; headers: Record; body?: unknown }; + } = { + success: true, + headers: {}, + }, ): void { mockProcessHTTPRequest.mockResolvedValue(processResult); mockProcessSettlement.mockResolvedValue(settlementResult); @@ -128,6 +159,16 @@ function createMockResponse(): Response & { this._headers[key] = value; return this; }), + getHeaders: vi.fn(function (this: typeof res) { + return this._headers; + }), + getHeader: vi.fn(function (this: typeof res, key: string) { + return this._headers[key] ?? undefined; + }), + removeHeader: vi.fn(function (this: typeof res, key: string) { + delete this._headers[key]; + return this; + }), json: vi.fn(function (this: typeof res, body: unknown) { this._body = body; this._ended = true; @@ -316,6 +357,13 @@ describe("paymentMiddleware", () => { mockPaymentPayload, mockPaymentRequirements, undefined, + expect.objectContaining({ + request: expect.objectContaining({ + path: "/api/test", + method: "GET", + }), + responseBody: expect.any(Buffer), + }), ); expect(res.setHeader).toHaveBeenCalledWith("PAYMENT-RESPONSE", "settled"); }); @@ -376,9 +424,145 @@ describe("paymentMiddleware", () => { await middleware(req, res, next); expect(res.status).toHaveBeenCalledWith(402); + expect(res.json).toHaveBeenCalledWith({}); + }); + + it("returns 502 when facilitator init fails during protected request", async () => { + const initialize = vi.fn().mockRejectedValue( + new Error("Failed to initialize: no supported payment kinds loaded from any facilitator.", { + cause: new FacilitatorResponseError( + "Facilitator supported returned invalid JSON: not-json", + ), + }), + ); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize, + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + + const middleware = paymentMiddleware(mockRoutes, {} as unknown as x402ResourceServer); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(mockProcessHTTPRequest).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(502); + expect(res.json).toHaveBeenCalledWith({ + error: "Facilitator supported returned invalid JSON: not-json", + }); + }); + + it("retries initialization after a facilitator init failure", async () => { + const initialize = vi + .fn() + .mockRejectedValueOnce( + new Error("Failed to initialize: no supported payment kinds loaded from any facilitator.", { + cause: new FacilitatorResponseError( + "Facilitator supported returned invalid JSON: not-json", + ), + }), + ) + .mockResolvedValueOnce(undefined); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize, + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + mockProcessHTTPRequest.mockResolvedValue({ type: "no-payment-required" }); + + const middleware = paymentMiddleware(mockRoutes, {} as unknown as x402ResourceServer); + const firstRes = createMockResponse(); + const secondRes = createMockResponse(); + const next = vi.fn(); + + await middleware(createMockRequest(), firstRes, next); + await middleware(createMockRequest(), secondRes, next); + + expect(firstRes.status).toHaveBeenCalledWith(502); + expect(initialize).toHaveBeenCalledTimes(2); + expect(mockProcessHTTPRequest).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + }); + + it("returns 502 when processHTTPRequest surfaces FacilitatorResponseError", async () => { + mockProcessHTTPRequest.mockRejectedValue( + new FacilitatorResponseError("Facilitator verify returned invalid JSON: not-json"), + ); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(502); expect(res.json).toHaveBeenCalledWith({ - error: "Settlement failed", - details: "Settlement rejected", + error: "Facilitator verify returned invalid JSON: not-json", + }); + expect(next).not.toHaveBeenCalled(); + }); + + it("returns 502 when settlement surfaces FacilitatorResponseError", async () => { + setupMockHttpServer({ + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }); + mockProcessSettlement.mockRejectedValue( + new FacilitatorResponseError('Facilitator settle returned invalid data: {"success":true}'), + ); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(() => { + res.statusCode = 200; + res.end(); + }); + + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(502); + expect(res.json).toHaveBeenCalledWith({ + error: 'Facilitator settle returned invalid data: {"success":true}', }); }); @@ -389,7 +573,19 @@ describe("paymentMiddleware", () => { paymentPayload: mockPaymentPayload, paymentRequirements: mockPaymentRequirements, }, - { success: false, errorReason: "Insufficient funds" }, + { + success: false, + errorReason: "Insufficient funds", + headers: { "PAYMENT-RESPONSE": "settlement-failed-encoded" }, + response: { + status: 402, + headers: { + "Content-Type": "application/json", + "PAYMENT-RESPONSE": "settlement-failed-encoded", + }, + body: {}, + }, + }, ); const middleware = paymentMiddleware( @@ -408,11 +604,9 @@ describe("paymentMiddleware", () => { await middleware(req, res, next); + expect(res.setHeader).toHaveBeenCalledWith("PAYMENT-RESPONSE", "settlement-failed-encoded"); expect(res.status).toHaveBeenCalledWith(402); - expect(res.json).toHaveBeenCalledWith({ - error: "Settlement failed", - details: "Insufficient funds", - }); + expect(res.json).toHaveBeenCalledWith({}); }); it("passes paywallConfig to processHTTPRequest", async () => { diff --git a/typescript/packages/http/express/src/index.ts b/typescript/packages/http/express/src/index.ts index 6a12ce9..efd262f 100644 --- a/typescript/packages/http/express/src/index.ts +++ b/typescript/packages/http/express/src/index.ts @@ -6,11 +6,26 @@ import { x402ResourceServer, RoutesConfig, FacilitatorClient, + FacilitatorResponseError, + getFacilitatorResponseError, + SETTLEMENT_OVERRIDES_HEADER, + SettlementOverrides, } from "@x402/core/server"; import { SchemeNetworkServer, Network } from "@x402/core/types"; import { NextFunction, Request, Response } from "express"; import { ExpressAdapter } from "./adapter"; +/** + * Set settlement overrides on the response for partial settlement. + * The middleware will extract these before settlement and strip the header from the client response. + * + * @param res - Express response object + * @param overrides - Settlement overrides (e.g., { amount: "500" } for partial settlement) + */ +export function setSettlementOverrides(res: Response, overrides: SettlementOverrides): void { + res.setHeader(SETTLEMENT_OVERRIDES_HEADER, JSON.stringify(overrides)); +} + /** * Check if any routes in the configuration declare bazaar extensions * @@ -44,6 +59,16 @@ export interface SchemeRegistration { server: SchemeNetworkServer; } +/** + * Sends a normalized 502 response for facilitator boundary failures. + * + * @param res - The Express response to write to + * @param error - The facilitator response error to surface + */ +function sendFacilitatorError(res: Response, error: FacilitatorResponseError): void { + res.status(502).json({ error: error.message }); +} + /** * Express payment middleware for x402 protocol (direct HTTP server instance). * @@ -82,6 +107,28 @@ export function paymentMiddlewareFromHTTPServer( // Store initialization promise (not the result) // httpServer.initialize() fetches facilitator support and validates routes let initPromise: Promise | null = syncFacilitatorOnStart ? httpServer.initialize() : null; + let isInitialized = false; + + /** + * Ensures facilitator initialization succeeds once, while allowing retries after failures. + */ + async function initializeHttpServer(): Promise { + if (!syncFacilitatorOnStart || isInitialized) { + return; + } + + if (!initPromise) { + initPromise = httpServer.initialize(); + } + + try { + await initPromise; + isInitialized = true; + } catch (error) { + initPromise = null; + throw error; + } + } // Dynamically register bazaar extension if routes declare it and not already registered // Skip if pre-registered (e.g., in serverless environments where static imports are used) @@ -112,9 +159,17 @@ export function paymentMiddlewareFromHTTPServer( } // Only initialize when processing a protected route - if (initPromise) { - await initPromise; - initPromise = null; // Clear after first await + if (syncFacilitatorOnStart && !isInitialized) { + try { + await initializeHttpServer(); + } catch (error) { + const facilitatorError = getFacilitatorResponseError(error); + if (facilitatorError) { + sendFacilitatorError(res, facilitatorError); + return; + } + return next(error); + } } // Await bazaar extension loading if needed @@ -124,7 +179,16 @@ export function paymentMiddlewareFromHTTPServer( } // Process payment requirement check - const result = await httpServer.processHTTPRequest(context, paywallConfig); + let result: Awaited>; + try { + result = await httpServer.processHTTPRequest(context, paywallConfig); + } catch (error) { + if (error instanceof FacilitatorResponseError) { + sendFacilitatorError(res, error); + return; + } + return next(error); + } // Handle the different result types switch (result.type) { @@ -231,19 +295,39 @@ export function paymentMiddlewareFromHTTPServer( } try { + // Build response body buffer from buffered write/end calls + const responseBody = Buffer.concat( + bufferedCalls.flatMap(([m, args]) => + (m === "write" || m === "end") && args[0] ? [Buffer.from(args[0])] : [], + ), + ); + + const responseHeaders: Record = {}; + for (const [key, value] of Object.entries(res.getHeaders())) { + if (value != null) { + responseHeaders[key] = String(value); + } + } + const settleResult = await httpServer.processSettlement( paymentPayload, paymentRequirements, declaredExtensions, + { request: context, responseBody, responseHeaders }, ); // If settlement fails, return an error and do not send the buffered response if (!settleResult.success) { bufferedCalls = []; - res.status(402).json({ - error: "Settlement failed", - details: settleResult.errorReason, + const { response } = settleResult; + Object.entries(response.headers).forEach(([key, value]) => { + res.setHeader(key, value); }); + if (response.isHtml) { + res.status(response.status).send(response.body); + } else { + res.status(response.status).json(response.body ?? {}); + } return; } @@ -252,13 +336,15 @@ export function paymentMiddlewareFromHTTPServer( res.setHeader(key, value); }); } catch (error) { + if (error instanceof FacilitatorResponseError) { + bufferedCalls = []; + sendFacilitatorError(res, error); + return; + } console.error(error); // If settlement fails, don't send the buffered response bufferedCalls = []; - res.status(402).json({ - error: "Settlement failed", - details: error instanceof Error ? error.message : "Unknown error", - }); + res.status(402).json({}); return; } finally { settled = true; @@ -382,9 +468,9 @@ export type { SchemeNetworkServer, } from "@x402/core/types"; -export type { PaywallProvider, PaywallConfig } from "@x402/core/server"; +export type { PaywallProvider, PaywallConfig, SettlementOverrides } from "@x402/core/server"; -export { RouteConfigurationError } from "@x402/core/server"; +export { RouteConfigurationError, SETTLEMENT_OVERRIDES_HEADER } from "@x402/core/server"; export type { RouteValidationError } from "@x402/core/server"; diff --git a/typescript/packages/mcp/.prettierignore b/typescript/packages/http/fastify/.prettierignore similarity index 100% rename from typescript/packages/mcp/.prettierignore rename to typescript/packages/http/fastify/.prettierignore diff --git a/typescript/packages/mcp/.prettierrc b/typescript/packages/http/fastify/.prettierrc similarity index 100% rename from typescript/packages/mcp/.prettierrc rename to typescript/packages/http/fastify/.prettierrc diff --git a/typescript/packages/http/fastify/CHANGELOG.md b/typescript/packages/http/fastify/CHANGELOG.md new file mode 100644 index 0000000..aa91070 --- /dev/null +++ b/typescript/packages/http/fastify/CHANGELOG.md @@ -0,0 +1,19 @@ +# @x402/fastify + +## 2.9.0 + +### Minor Changes + +- bd42498: Added Fastify framework adapter for x402 payment middleware +- 2250cae: Migrated project from coinbase/x402 to x402-foundation/x402 organization + +### Patch Changes + +- a0ec8e6: Applied monkey-patch on reply.raw write operations and buffered response to prevent content leak from direct raw writes bypassing Fastify's onSend lifecycle +- Updated dependencies [8cf3fca] +- Updated dependencies [c0e3969] +- Updated dependencies [2250cae] +- Updated dependencies [d352574] + - @x402/core@2.9.0 + - @x402/paywall@2.9.0 + - @x402/extensions@2.9.0 diff --git a/typescript/packages/http/fastify/README.md b/typescript/packages/http/fastify/README.md new file mode 100644 index 0000000..c8b17bf --- /dev/null +++ b/typescript/packages/http/fastify/README.md @@ -0,0 +1,254 @@ +# @x402/fastify + +Fastify middleware integration for the x402 Payment Protocol. This package provides payment middleware for adding x402 payment requirements to your Fastify applications. + +## Installation + +```bash +pnpm install @x402/fastify +``` + +## Quick Start + +```typescript +import Fastify from "fastify"; +import { paymentMiddleware, x402ResourceServer } from "@x402/fastify"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { HTTPFacilitatorClient } from "@x402/core/server"; + +const app = Fastify(); + +const facilitatorClient = new HTTPFacilitatorClient({ url: "https://facilitator.x402.org" }); +const resourceServer = new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()); + +// Apply the payment middleware with your configuration +paymentMiddleware( + app, + { + "GET /protected-route": { + accepts: { + scheme: "exact", + price: "$0.10", + network: "eip155:84532", + payTo: "0xYourAddress", + }, + description: "Access to premium content", + }, + }, + resourceServer, +); + +// Implement your protected route +app.get("/protected-route", async () => { + return { message: "This content is behind a paywall" }; +}); + +app.listen({ port: 3000 }); +``` + +## Configuration + +The `paymentMiddleware` function accepts the following parameters: + +```typescript +paymentMiddleware( + app: FastifyInstance, + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart?: boolean +) +``` + +### Parameters + +1. **`app`** (required): The Fastify instance to register hooks on +2. **`routes`** (required): Route configurations for protected endpoints +3. **`server`** (required): Pre-configured x402ResourceServer instance +4. **`paywallConfig`** (optional): Configuration for the built-in paywall UI +5. **`paywall`** (optional): Custom paywall provider +6. **`syncFacilitatorOnStart`** (optional): Whether to sync with facilitator on startup (defaults to true) + +## API Reference + +### FastifyAdapter + +The `FastifyAdapter` class implements the `HTTPAdapter` interface from `@x402/core`, providing Fastify-specific request handling: + +```typescript +class FastifyAdapter implements HTTPAdapter { + getHeader(name: string): string | undefined; + getMethod(): string; + getPath(): string; + getUrl(): string; + getAcceptHeader(): string; + getUserAgent(): string; +} +``` + +### Middleware Function + +```typescript +function paymentMiddleware( + app: FastifyInstance, + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart?: boolean, +): void; +``` + +Registers Fastify hooks (`onRequest` and `onSend`) that: + +1. Use the provided x402ResourceServer for payment processing +2. Check if the incoming request matches a protected route +3. Validate payment headers if required +4. Return payment instructions (402 status) if payment is missing or invalid +5. Process the request if payment is valid +6. Handle settlement after successful response + +### Route Configuration + +Routes are passed as the second parameter to `paymentMiddleware`: + +```typescript +const routes: RoutesConfig = { + "GET /api/protected": { + accepts: { + scheme: "exact", + price: "$0.10", + network: "eip155:84532", + payTo: "0xYourAddress", + maxTimeoutSeconds: 60, + }, + description: "Premium API access", + }, +}; + +paymentMiddleware(app, routes, resourceServer); +``` + +### Paywall Configuration + +The middleware automatically displays a paywall UI when browsers request protected endpoints. + +**Option 1: Full Paywall UI (Recommended)** + +Install the optional `@x402/paywall` package for a complete wallet connection and payment UI: + +```bash +pnpm add @x402/paywall +``` + +Then configure it: + +```typescript +const paywallConfig: PaywallConfig = { + appName: "Your App Name", + appLogo: "/path/to/logo.svg", + testnet: true, +}; + +paymentMiddleware(app, routes, resourceServer, paywallConfig); +``` + +**Option 2: Basic Paywall (No Installation)** + +Without `@x402/paywall` installed, the middleware returns a basic HTML page with payment instructions. + +**Option 3: Custom Paywall Provider** + +Provide your own paywall provider: + +```typescript +paymentMiddleware(app, routes, resourceServer, paywallConfig, customPaywallProvider); +``` + +## Advanced Usage + +### Multiple Protected Routes + +```typescript +paymentMiddleware( + app, + { + "GET /api/premium/*": { + accepts: { + scheme: "exact", + price: "$1.00", + network: "eip155:8453", + payTo: "0xYourAddress", + }, + description: "Premium API access", + }, + "GET /api/data": { + accepts: { + scheme: "exact", + price: "$0.50", + network: "eip155:84532", + payTo: "0xYourAddress", + maxTimeoutSeconds: 120, + }, + description: "Data endpoint access", + }, + }, + resourceServer, +); +``` + +### Multiple Payment Networks + +```typescript +paymentMiddleware( + app, + { + "GET /weather": { + accepts: [ + { + scheme: "exact", + price: "$0.001", + network: "eip155:84532", + payTo: evmAddress, + }, + { + scheme: "exact", + price: "$0.001", + network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + payTo: svmAddress, + }, + ], + description: "Weather data", + mimeType: "application/json", + }, + }, + new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()) + .register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme()), +); +``` + +### Custom Facilitator Client + +If you need to use a custom facilitator server, configure it when creating the x402ResourceServer: + +```typescript +import { HTTPFacilitatorClient } from "@x402/core/server"; +import { x402ResourceServer } from "@x402/fastify"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; + +const customFacilitator = new HTTPFacilitatorClient({ + url: "https://your-facilitator.com", + createAuthHeaders: async () => ({ + verify: { Authorization: "Bearer your-token" }, + settle: { Authorization: "Bearer your-token" }, + }), +}); + +const resourceServer = new x402ResourceServer(customFacilitator) + .register("eip155:84532", new ExactEvmScheme()); + +paymentMiddleware(app, routes, resourceServer, paywallConfig); +``` diff --git a/typescript/packages/mcp/eslint.config.js b/typescript/packages/http/fastify/eslint.config.js similarity index 98% rename from typescript/packages/mcp/eslint.config.js rename to typescript/packages/http/fastify/eslint.config.js index ca28b5c..28e5647 100644 --- a/typescript/packages/mcp/eslint.config.js +++ b/typescript/packages/http/fastify/eslint.config.js @@ -21,6 +21,7 @@ export default [ module: "readonly", require: "readonly", Buffer: "readonly", + Headers: "readonly", exports: "readonly", setTimeout: "readonly", clearTimeout: "readonly", diff --git a/typescript/packages/mcp/package.json b/typescript/packages/http/fastify/package.json similarity index 72% rename from typescript/packages/mcp/package.json rename to typescript/packages/http/fastify/package.json index 15476e4..2c1195b 100644 --- a/typescript/packages/mcp/package.json +++ b/typescript/packages/http/fastify/package.json @@ -1,13 +1,12 @@ { - "name": "@x402/mcp", - "version": "2.3.0-alpha", + "name": "@x402/fastify", + "version": "2.9.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", - "types": "./dist/cjs/index.d.ts", + "types": "./dist/index.d.ts", "scripts": { "start": "tsx --env-file=.env index.ts", "test": "vitest run", - "test:integration": "vitest run --dir test/integration", "test:watch": "vitest", "build": "tsup", "watch": "tsc --watch", @@ -18,33 +17,39 @@ }, "keywords": [], "license": "Apache-2.0", - "author": "Coinbase Inc.", - "repository": "https://github.com/coinbase/x402", - "description": "x402 Payment Protocol", + "author": "x402 Foundation", + "repository": "https://github.com/x402-foundation/x402", + "description": "x402 Payment Protocol - Fastify middleware", "devDependencies": { "@eslint/js": "^9.24.0", "@types/node": "^22.13.4", "@typescript-eslint/eslint-plugin": "^8.29.1", "@typescript-eslint/parser": "^8.29.1", - "@x402/evm": "workspace:~", "eslint": "^9.24.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsdoc": "^50.6.9", "eslint-plugin-prettier": "^5.2.6", - "express": "^4.21.2", + "fastify": "^5.0.0", "prettier": "3.5.2", "tsup": "^8.4.0", "tsx": "^4.19.2", "typescript": "^5.7.3", - "viem": "^2.27.2", + "vite": "^6.2.6", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.0.5", - "vite": "^6.2.6" + "vitest": "^3.0.5" }, "dependencies": { "@x402/core": "workspace:~", - "@modelcontextprotocol/sdk": "^1.12.1", - "zod": "^3.24.2" + "@x402/extensions": "workspace:~" + }, + "peerDependencies": { + "fastify": "^5.0.0", + "@x402/paywall": "workspace:*" + }, + "peerDependenciesMeta": { + "@x402/paywall": { + "optional": true + } }, "exports": { ".": { diff --git a/typescript/packages/http/fastify/src/adapter.test.ts b/typescript/packages/http/fastify/src/adapter.test.ts new file mode 100644 index 0000000..9b0675f --- /dev/null +++ b/typescript/packages/http/fastify/src/adapter.test.ts @@ -0,0 +1,169 @@ +import { describe, it, expect } from "vitest"; +import { FastifyRequest } from "fastify"; +import { FastifyAdapter } from "./adapter"; + +/** + * Factory for creating mock Fastify requests. + * + * @param options - Configuration options for the mock request. + * @param options.url - The request URL path with optional query string. + * @param options.method - The HTTP method. + * @param options.headers - Request headers. + * @param options.query - Query parameters. + * @param options.body - Request body. + * @param options.protocol - The request protocol. + * @param options.hostname - The request hostname. + * @param options.host - The request host header, including port if present. + * @returns A mock Fastify request. + */ +function createMockRequest( + options: { + url?: string; + method?: string; + headers?: Record; + query?: Record; + body?: unknown; + protocol?: string; + hostname?: string; + host?: string; + } = {}, +): FastifyRequest { + return { + url: options.url || "/api/test", + method: options.method || "GET", + headers: options.headers || {}, + query: options.query || {}, + body: options.body, + protocol: options.protocol || "https", + hostname: options.hostname || "example.com", + host: options.host || options.hostname || "example.com", + } as unknown as FastifyRequest; +} + +describe("FastifyAdapter", () => { + describe("getHeader", () => { + it("returns header value when present", () => { + const req = createMockRequest({ headers: { "x-payment": "test-payment" } }); + const adapter = new FastifyAdapter(req); + expect(adapter.getHeader("X-Payment")).toBe("test-payment"); + }); + + it("returns first value for array headers", () => { + const req = createMockRequest({ headers: { "x-payment": ["first", "second"] } }); + const adapter = new FastifyAdapter(req); + expect(adapter.getHeader("X-Payment")).toBe("first"); + }); + + it("returns undefined for missing headers", () => { + const req = createMockRequest(); + const adapter = new FastifyAdapter(req); + expect(adapter.getHeader("X-Missing")).toBeUndefined(); + }); + }); + + describe("getMethod", () => { + it("returns the HTTP method", () => { + const req = createMockRequest({ method: "POST" }); + const adapter = new FastifyAdapter(req); + expect(adapter.getMethod()).toBe("POST"); + }); + }); + + describe("getPath", () => { + it("returns the pathname without query string", () => { + const req = createMockRequest({ url: "/api/weather?city=NYC" }); + const adapter = new FastifyAdapter(req); + expect(adapter.getPath()).toBe("/api/weather"); + }); + + it("returns the pathname when no query string", () => { + const req = createMockRequest({ url: "/api/test" }); + const adapter = new FastifyAdapter(req); + expect(adapter.getPath()).toBe("/api/test"); + }); + }); + + describe("getUrl", () => { + it("returns the full URL", () => { + const req = createMockRequest({ + url: "/api/test?foo=bar", + protocol: "https", + hostname: "example.com", + host: "example.com:3000", + }); + const adapter = new FastifyAdapter(req); + expect(adapter.getUrl()).toBe("https://example.com:3000/api/test?foo=bar"); + }); + }); + + describe("getAcceptHeader", () => { + it("returns Accept header when present", () => { + const req = createMockRequest({ headers: { accept: "text/html" } }); + const adapter = new FastifyAdapter(req); + expect(adapter.getAcceptHeader()).toBe("text/html"); + }); + + it("returns empty string when missing", () => { + const req = createMockRequest(); + const adapter = new FastifyAdapter(req); + expect(adapter.getAcceptHeader()).toBe(""); + }); + }); + + describe("getUserAgent", () => { + it("returns User-Agent header when present", () => { + const req = createMockRequest({ headers: { "user-agent": "Mozilla/5.0" } }); + const adapter = new FastifyAdapter(req); + expect(adapter.getUserAgent()).toBe("Mozilla/5.0"); + }); + + it("returns empty string when missing", () => { + const req = createMockRequest(); + const adapter = new FastifyAdapter(req); + expect(adapter.getUserAgent()).toBe(""); + }); + }); + + describe("getQueryParams", () => { + it("returns all query parameters", () => { + const req = createMockRequest({ query: { foo: "bar", baz: "qux" } }); + const adapter = new FastifyAdapter(req); + expect(adapter.getQueryParams()).toEqual({ foo: "bar", baz: "qux" }); + }); + + it("returns empty object when no query params", () => { + const req = createMockRequest({ query: {} }); + const adapter = new FastifyAdapter(req); + expect(adapter.getQueryParams()).toEqual({}); + }); + }); + + describe("getQueryParam", () => { + it("returns single value for single param", () => { + const req = createMockRequest({ query: { city: "NYC" } }); + const adapter = new FastifyAdapter(req); + expect(adapter.getQueryParam("city")).toBe("NYC"); + }); + + it("returns undefined for missing param", () => { + const req = createMockRequest({ query: {} }); + const adapter = new FastifyAdapter(req); + expect(adapter.getQueryParam("missing")).toBeUndefined(); + }); + }); + + describe("getBody", () => { + it("returns parsed body", () => { + const body = { data: "test" }; + const req = createMockRequest({ body }); + const adapter = new FastifyAdapter(req); + expect(adapter.getBody()).toEqual(body); + }); + + it("returns undefined when no body", () => { + const req = createMockRequest(); + const adapter = new FastifyAdapter(req); + expect(adapter.getBody()).toBeUndefined(); + }); + }); +}); diff --git a/typescript/packages/http/fastify/src/adapter.ts b/typescript/packages/http/fastify/src/adapter.ts new file mode 100644 index 0000000..706bc56 --- /dev/null +++ b/typescript/packages/http/fastify/src/adapter.ts @@ -0,0 +1,99 @@ +import { HTTPAdapter } from "@x402/core/server"; +import { FastifyRequest } from "fastify"; + +/** + * Fastify adapter implementation for the x402 HTTP protocol. + */ +export class FastifyAdapter implements HTTPAdapter { + /** + * Creates a new FastifyAdapter instance. + * + * @param request - The Fastify request object + */ + constructor(private request: FastifyRequest) {} + + /** + * Gets a header value from the request. + * + * @param name - The header name + * @returns The header value or undefined + */ + getHeader(name: string): string | undefined { + const value = this.request.headers[name.toLowerCase()]; + return Array.isArray(value) ? value[0] : value; + } + + /** + * Gets the HTTP method of the request. + * + * @returns The HTTP method + */ + getMethod(): string { + return this.request.method; + } + + /** + * Gets the path of the request. + * + * @returns The request path without query string + */ + getPath(): string { + return this.request.url.split("?")[0]; + } + + /** + * Gets the full URL of the request. + * + * @returns The full request URL + */ + getUrl(): string { + return `${this.request.protocol}://${this.request.host || this.request.hostname}${this.request.url}`; + } + + /** + * Gets the Accept header from the request. + * + * @returns The Accept header value or empty string + */ + getAcceptHeader(): string { + return this.getHeader("accept") || ""; + } + + /** + * Gets the User-Agent header from the request. + * + * @returns The User-Agent header value or empty string + */ + getUserAgent(): string { + return this.getHeader("user-agent") || ""; + } + + /** + * Gets all query parameters from the request URL. + * + * @returns Record of query parameter key-value pairs + */ + getQueryParams(): Record { + return (this.request.query as Record) || {}; + } + + /** + * Gets a specific query parameter by name. + * + * @param name - The query parameter name + * @returns The query parameter value(s) or undefined + */ + getQueryParam(name: string): string | string[] | undefined { + return this.getQueryParams()[name]; + } + + /** + * Gets the parsed request body. + * Fastify automatically parses JSON bodies. + * + * @returns The parsed request body + */ + getBody(): unknown { + return this.request.body; + } +} diff --git a/typescript/packages/http/fastify/src/index.test.ts b/typescript/packages/http/fastify/src/index.test.ts new file mode 100644 index 0000000..ce0e2af --- /dev/null +++ b/typescript/packages/http/fastify/src/index.test.ts @@ -0,0 +1,890 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; +import type { + HTTPProcessResult, + x402HTTPResourceServer, + PaywallProvider, + FacilitatorClient, +} from "@x402/core/server"; +import { + x402ResourceServer, + x402HTTPResourceServer as HTTPResourceServer, +} from "@x402/core/server"; +import type { PaymentPayload, PaymentRequirements, SchemeNetworkServer } from "@x402/core/types"; +import { paymentMiddleware, paymentMiddlewareFromConfig, type SchemeRegistration } from "./index"; + +// --- Test Fixtures --- +const mockRoutes = { + "/api/*": { + accepts: { scheme: "exact", payTo: "0x123", price: "$0.01", network: "eip155:84532" }, + }, +} as const; + +const mockPaymentPayload = { + scheme: "exact", + network: "eip155:84532", + payload: { signature: "0xabc" }, +} as unknown as PaymentPayload; + +const mockPaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + maxAmountRequired: "1000", + payTo: "0x123", +} as unknown as PaymentRequirements; + +// --- Mock setup --- +let mockProcessHTTPRequest: ReturnType; +let mockProcessSettlement: ReturnType; +let mockRegisterPaywallProvider: ReturnType; +let mockRequiresPayment: ReturnType; + +vi.mock("@x402/core/server", () => ({ + SETTLEMENT_OVERRIDES_HEADER: "Settlement-Overrides", + FacilitatorResponseError: class FacilitatorResponseError extends Error { + /** + * Mock error class matching @x402/core/server FacilitatorResponseError. + * + * @param message - Error message passed to the superclass. + */ + constructor(message: string) { + super(message); + this.name = "FacilitatorResponseError"; + } + }, + getFacilitatorResponseError: (error: unknown) => { + let current = error; + while (current instanceof Error) { + if (current.name === "FacilitatorResponseError") { + return current; + } + current = (current as Error & { cause?: unknown }).cause; + } + return null; + }, + x402ResourceServer: vi.fn().mockImplementation(() => ({ + initialize: vi.fn().mockResolvedValue(undefined), + registerExtension: vi.fn(), + register: vi.fn(), + hasExtension: vi.fn().mockReturnValue(false), + })), + x402HTTPResourceServer: vi.fn().mockImplementation((server, routes) => ({ + initialize: vi.fn().mockResolvedValue(undefined), + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + })), +})); + +// --- Hook Capture --- +type HookHandler = (...args: unknown[]) => Promise; + +/** + * Captured hooks from a mock Fastify instance. + */ +interface CapturedHooks { + onRequest: HookHandler[]; + onSend: HookHandler[]; +} + +/** + * Creates a mock Fastify instance that captures registered hooks. + * + * @returns Object containing the mock app and captured hooks. + */ +function createMockApp(): { app: FastifyInstance; hooks: CapturedHooks } { + const hooks: CapturedHooks = { onRequest: [], onSend: [] }; + + const app = { + addHook: vi.fn((name: string, handler: HookHandler) => { + if (name === "onRequest") hooks.onRequest.push(handler); + if (name === "onSend") hooks.onSend.push(handler); + }), + decorateRequest: vi.fn(), + } as unknown as FastifyInstance; + + return { app, hooks }; +} + +/** + * Sets up the mock HTTP server to return specified results. + * + * @param processResult - The result to return from processHTTPRequest. + * @param settlementResult - Result to return from processSettlement. + */ +function setupMockHttpServer( + processResult: HTTPProcessResult, + settlementResult: + | { success: true; headers: Record } + | { + success: false; + errorReason: string; + headers: Record; + response: { + status: number; + headers: Record; + body?: unknown; + isHtml?: boolean; + }; + } = { + success: true, + headers: {}, + }, +): void { + mockProcessHTTPRequest.mockResolvedValue(processResult); + mockProcessSettlement.mockResolvedValue(settlementResult); +} + +/** + * Creates a mock Fastify request for testing. + * + * @param options - Configuration options for the mock request. + * @param options.url - The request URL path. + * @param options.method - The HTTP method. + * @param options.headers - Request headers. + * @returns A mock Fastify request. + */ +function createMockRequest( + options: { + url?: string; + method?: string; + headers?: Record; + } = {}, +): FastifyRequest { + return { + url: options.url || "/api/test", + method: options.method || "GET", + headers: options.headers || {}, + query: {}, + body: undefined, + protocol: "https", + hostname: "example.com", + } as unknown as FastifyRequest; +} + +/** + * Creates a mock Fastify reply for testing. + * + * @returns A mock Fastify reply with tracking properties. + */ +function createMockReply(): FastifyReply & { + _status: number; + _headers: Record; + _body: unknown; + _type: string | undefined; +} { + const reply = { + _status: 200, + _headers: {} as Record, + _body: undefined as unknown, + _type: undefined as string | undefined, + statusCode: 200, + raw: { + write: vi.fn(), + end: vi.fn(), + writeHead: vi.fn(), + flushHeaders: vi.fn(), + }, + getHeaders: vi.fn(function (this: typeof reply) { + return this._headers; + }), + getHeader: vi.fn(function (this: typeof reply, key: string) { + return this._headers[key]; + }), + removeHeader: vi.fn(function (this: typeof reply, key: string) { + delete this._headers[key]; + return this; + }), + header: vi.fn(function (this: typeof reply, key: string, value: string) { + this._headers[key] = value; + return this; + }), + status: vi.fn(function (this: typeof reply, code: number) { + this._status = code; + this.statusCode = code; + return this; + }), + type: vi.fn(function (this: typeof reply, contentType: string) { + this._type = contentType; + return this; + }), + send: vi.fn(function (this: typeof reply, body: unknown) { + this._body = body; + return this; + }), + }; + + return reply as unknown as typeof reply; +} + +// --- Tests --- +describe("paymentMiddleware", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockProcessHTTPRequest = vi.fn(); + mockProcessSettlement = vi.fn(); + mockRegisterPaywallProvider = vi.fn(); + mockRequiresPayment = vi.fn().mockReturnValue(true); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize: vi.fn().mockResolvedValue(undefined), + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + }); + + it("registers onRequest and onSend hooks", () => { + const { app } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + expect(app.addHook).toHaveBeenCalledWith("onRequest", expect.any(Function)); + expect(app.addHook).toHaveBeenCalledWith("onSend", expect.any(Function)); + }); + + it("proceeds when no-payment-required", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalled(); + expect(reply.send).not.toHaveBeenCalled(); + }); + + it("skips payment check for non-protected routes", async () => { + mockRequiresPayment.mockReturnValue(false); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest({ url: "/health" }); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).not.toHaveBeenCalled(); + expect(reply.send).not.toHaveBeenCalled(); + }); + + it("returns 402 HTML for payment-error with isHtml", async () => { + setupMockHttpServer({ + type: "payment-error", + response: { + status: 402, + body: "Paywall", + headers: { "PAYMENT-REQUIRED": "encoded-data" }, + isHtml: true, + }, + }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(reply.status).toHaveBeenCalledWith(402); + expect(reply.type).toHaveBeenCalledWith("text/html"); + expect(reply.send).toHaveBeenCalledWith("Paywall"); + expect(reply.header).toHaveBeenCalledWith("PAYMENT-REQUIRED", "encoded-data"); + }); + + it("returns 402 JSON for payment-error", async () => { + setupMockHttpServer({ + type: "payment-error", + response: { + status: 402, + body: { error: "Payment required" }, + headers: {}, + isHtml: false, + }, + }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(reply.status).toHaveBeenCalledWith(402); + expect(reply.send).toHaveBeenCalledWith({ error: "Payment required" }); + }); + + it("stashes payment context on request for payment-verified", async () => { + setupMockHttpServer({ + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(reply.send).not.toHaveBeenCalled(); + expect(request.x402Context).toBeDefined(); + expect(request.x402RawGuard).toBeDefined(); + }); + + it("settles payment and adds headers in onSend for verified payments", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: true, headers: { "PAYMENT-RESPONSE": "settled" } }, + ); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + + // Step 1: onRequest stashes payment context + await hooks.onRequest[0](request, reply); + + // Step 2: onSend settles payment + const payload = JSON.stringify({ data: "premium content" }); + const result = await hooks.onSend[0](request, reply, payload); + + expect(mockProcessSettlement).toHaveBeenCalledWith( + mockPaymentPayload, + mockPaymentRequirements, + undefined, + expect.objectContaining({ + request: expect.objectContaining({ + path: "/api/test", + method: "GET", + }), + responseBody: expect.any(Buffer), + }), + ); + expect(reply.header).toHaveBeenCalledWith("PAYMENT-RESPONSE", "settled"); + expect(result).toBe(payload); + }); + + it("passes Buffer payload bytes to settlement without JSON stringifying them", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: true, headers: { "PAYMENT-RESPONSE": "settled" } }, + ); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + const payload = Buffer.from([0, 1, 2, 255]); + + await hooks.onRequest[0](request, reply); + const result = await hooks.onSend[0](request, reply, payload); + + expect(result).toBe(payload); + expect(mockProcessSettlement).toHaveBeenCalledTimes(1); + expect( + (mockProcessSettlement.mock.calls[0]?.[3] as { responseBody?: Buffer }).responseBody, + ).toEqual(payload); + }); + + it("skips settlement for non-payment requests in onSend", async () => { + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + const payload = JSON.stringify({ data: "free content" }); + + const result = await hooks.onSend[0](request, reply, payload); + + expect(mockProcessSettlement).not.toHaveBeenCalled(); + expect(result).toBe(payload); + }); + + it("skips settlement when handler returns >= 400", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: true, headers: {} }, + ); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + + await hooks.onRequest[0](request, reply); + + reply.statusCode = 500; + const payload = JSON.stringify({ error: "Server error" }); + const result = await hooks.onSend[0](request, reply, payload); + + expect(mockProcessSettlement).not.toHaveBeenCalled(); + expect(result).toBe(payload); + }); + + it("returns 402 when settlement fails", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { + success: false, + errorReason: "Insufficient funds", + headers: {}, + response: { + status: 402, + headers: { + "PAYMENT-RESPONSE": "failed", + "Content-Type": "application/json", + }, + body: { error: "Settlement failed" }, + }, + }, + ); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + + await hooks.onRequest[0](request, reply); + + reply.type("application/octet-stream"); + const payload = JSON.stringify({ data: "premium content" }); + const result = await hooks.onSend[0](request, reply, payload); + + expect(reply.status).toHaveBeenCalledWith(402); + expect(reply.header).toHaveBeenCalledWith("PAYMENT-RESPONSE", "failed"); + expect(reply.type).toHaveBeenCalledWith("application/json"); + expect(result).toBe(JSON.stringify({ error: "Settlement failed" })); + }); + + it("returns 402 when settlement throws error", async () => { + setupMockHttpServer({ + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }); + mockProcessSettlement.mockRejectedValue(new Error("Settlement rejected")); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + + await hooks.onRequest[0](request, reply); + + const payload = JSON.stringify({ data: "premium content" }); + const result = await hooks.onSend[0](request, reply, payload); + + expect(reply.status).toHaveBeenCalledWith(402); + expect(reply.type).toHaveBeenCalledWith("application/json"); + expect(result).toBe(JSON.stringify({})); + }); + + it("passes paywallConfig to processHTTPRequest", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + const paywallConfig = { appName: "test-app" }; + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + paywallConfig, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith(expect.anything(), paywallConfig); + }); + + it("registers custom paywall provider", () => { + const { app } = createMockApp(); + const paywall: PaywallProvider = { generateHtml: vi.fn() }; + + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + paywall, + false, + ); + + expect(mockRegisterPaywallProvider).toHaveBeenCalledWith(paywall); + }); +}); + +describe("paymentMiddlewareFromConfig", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockProcessHTTPRequest = vi.fn(); + mockProcessSettlement = vi.fn(); + mockRegisterPaywallProvider = vi.fn(); + mockRequiresPayment = vi.fn().mockReturnValue(true); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize: vi.fn().mockResolvedValue(undefined), + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + + vi.mocked(x402ResourceServer).mockImplementation( + () => + ({ + initialize: vi.fn().mockResolvedValue(undefined), + registerExtension: vi.fn(), + register: vi.fn(), + }) as unknown as x402ResourceServer, + ); + }); + + it("creates x402ResourceServer with facilitator clients", () => { + const { app } = createMockApp(); + const facilitator = { verify: vi.fn(), settle: vi.fn() } as unknown as FacilitatorClient; + + paymentMiddlewareFromConfig(app, mockRoutes, facilitator); + + expect(x402ResourceServer).toHaveBeenCalledWith(facilitator); + }); + + it("registers scheme servers for each network", () => { + const { app } = createMockApp(); + const schemeServer = { verify: vi.fn(), settle: vi.fn() } as unknown as SchemeNetworkServer; + const schemes: SchemeRegistration[] = [ + { network: "eip155:84532", server: schemeServer }, + { network: "eip155:8453", server: schemeServer }, + ]; + + paymentMiddlewareFromConfig(app, mockRoutes, undefined, schemes); + + const serverInstance = vi.mocked(x402ResourceServer).mock.results[0].value; + expect(serverInstance.register).toHaveBeenCalledTimes(2); + expect(serverInstance.register).toHaveBeenCalledWith("eip155:84532", schemeServer); + expect(serverInstance.register).toHaveBeenCalledWith("eip155:8453", schemeServer); + }); + + it("registers hooks on the Fastify instance", () => { + const { app } = createMockApp(); + paymentMiddlewareFromConfig(app, mockRoutes); + + expect(app.addHook).toHaveBeenCalledWith("onRequest", expect.any(Function)); + expect(app.addHook).toHaveBeenCalledWith("onSend", expect.any(Function)); + }); +}); + +describe("FastifyAdapter integration", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockProcessHTTPRequest = vi.fn(); + mockProcessSettlement = vi.fn(); + mockRegisterPaywallProvider = vi.fn(); + mockRequiresPayment = vi.fn().mockReturnValue(true); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize: vi.fn().mockResolvedValue(undefined), + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + }); + + it("extracts path and method from request", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest({ url: "/api/weather", method: "POST" }); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + path: "/api/weather", + method: "POST", + }), + undefined, + ); + }); + + it("strips query string from path", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest({ url: "/api/weather?city=NYC" }); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + path: "/api/weather", + }), + undefined, + ); + }); + + it("extracts payment-signature header", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest({ headers: { "payment-signature": "sig-data" } }); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: "sig-data", + }), + undefined, + ); + }); + + it("extracts x-payment header", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest({ headers: { "x-payment": "payment-data" } }); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: "payment-data", + }), + undefined, + ); + }); + + it("prefers payment-signature over x-payment", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest({ + headers: { "payment-signature": "sig-data", "x-payment": "x-payment-data" }, + }); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: "sig-data", + }), + undefined, + ); + }); + + it("returns undefined paymentHeader when no payment headers present", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: undefined, + }), + undefined, + ); + }); +}); diff --git a/typescript/packages/http/fastify/src/index.ts b/typescript/packages/http/fastify/src/index.ts new file mode 100644 index 0000000..1e32952 --- /dev/null +++ b/typescript/packages/http/fastify/src/index.ts @@ -0,0 +1,580 @@ +import type { ServerResponse } from "http"; +import { + HTTPRequestContext, + PaywallConfig, + PaywallProvider, + x402HTTPResourceServer, + x402ResourceServer, + RoutesConfig, + FacilitatorClient, + FacilitatorResponseError, + getFacilitatorResponseError, + SETTLEMENT_OVERRIDES_HEADER, + SettlementOverrides, +} from "@x402/core/server"; +import { + SchemeNetworkServer, + Network, + PaymentPayload, + PaymentRequirements, +} from "@x402/core/types"; +import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; +import { FastifyAdapter } from "./adapter"; + +/** + * Sets settlement overrides on a Fastify reply for partial settlement (upto scheme). + * The middleware extracts these before settlement and strips the header from the client response. + * + * @param reply - The Fastify reply object + * @param overrides - Settlement overrides (e.g., { amount: "500" } for partial settlement) + */ +export function setSettlementOverrides(reply: FastifyReply, overrides: SettlementOverrides): void { + reply.header(SETTLEMENT_OVERRIDES_HEADER, JSON.stringify(overrides)); +} + +interface X402PaymentContext { + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + declaredExtensions?: Record; + requestContext: HTTPRequestContext; +} + +interface BufferedWriteHead { + method: "writeHead"; + statusCode: number; + headers?: Record; +} + +interface BufferedWrite { + method: "write"; + data: string | Buffer; +} + +interface BufferedEnd { + method: "end"; + data?: string | Buffer; +} + +interface BufferedFlushHeaders { + method: "flushHeaders"; +} + +type BufferedRawCall = BufferedWriteHead | BufferedWrite | BufferedEnd | BufferedFlushHeaders; + +interface RawGuard { + triggered: boolean; + buffer: BufferedRawCall[]; + deactivate: () => void; +} + +declare module "fastify" { + interface FastifyRequest { + x402Context?: X402PaymentContext; + x402RawGuard?: RawGuard; + } +} + +/** + * Gets a header value from a plain header record using a case-insensitive lookup. + * + * @param headers - Headers to search + * @param headerName - Header name to find + * @returns Matching header value or undefined + */ +function getHeaderValue(headers: Record, headerName: string): string | undefined { + const target = headerName.toLowerCase(); + return Object.entries(headers).find(([key]) => key.toLowerCase() === target)?.[1]; +} + +/** + * Converts a Fastify onSend payload into the byte representation used for settlement. + * + * @param payload - Fastify payload + * @returns Buffer when the payload can be represented eagerly, otherwise undefined + */ +function getResponseBodyBuffer(payload: unknown): Buffer | undefined { + if (typeof payload === "string") { + return Buffer.from(payload); + } + + if (Buffer.isBuffer(payload)) { + return payload; + } + + if (payload instanceof Uint8Array) { + return Buffer.from(payload); + } + + if (payload instanceof ArrayBuffer) { + return Buffer.from(new Uint8Array(payload)); + } + + if (payload && typeof payload === "object" && "pipe" in payload) { + return undefined; + } + + return Buffer.from(JSON.stringify(payload ?? {})); +} + +/** + * Check if any routes in the configuration declare bazaar extensions. + * + * @param routes - Route configuration + * @returns True if any route has extensions.bazaar defined + */ +function checkIfBazaarNeeded(routes: RoutesConfig): boolean { + if ("accepts" in routes) { + return !!(routes.extensions && "bazaar" in routes.extensions); + } + + return Object.values(routes).some(routeConfig => { + return !!(routeConfig.extensions && "bazaar" in routeConfig.extensions); + }); +} + +/** + * Buffers reply.raw method calls on a protected route so that settlement + * can inspect the response body before anything reaches the client. + * + * Fastify's normal reply flow (return value / reply.send) goes through the + * onSend hook where settlement runs before data reaches the client. However, + * reply.raw gives direct access to the underlying Node.js ServerResponse, + * allowing data to be flushed without triggering onSend. + * + * This guard intercepts writeHead/write/end/flushHeaders, stores them in a + * buffer, and ensures Fastify's lifecycle still fires (via reply.send on end) + * so that onSend can reconstruct the response, settle, then replay the calls. + * + * The guard is deactivated at the start of onSend so that Fastify's own + * internal reply.raw usage (which happens after onSend) is unaffected. + * + * @param reply - Fastify reply whose raw ServerResponse is wrapped for buffering. + * @returns Guard state and buffer used to replay raw writes after settlement. + */ +function guardReplyRaw(reply: FastifyReply): RawGuard { + const raw = reply.raw; + const origWrite = raw.write; + const origEnd = raw.end; + const origWriteHead = raw.writeHead; + const origFlushHeaders = raw.flushHeaders; + + let active = true; + const guard: RawGuard = { + triggered: false, + buffer: [], + deactivate() { + if (!active) return; + active = false; + raw.write = origWrite; + raw.end = origEnd; + raw.writeHead = origWriteHead; + raw.flushHeaders = origFlushHeaders; + }, + }; + + raw.writeHead = function (this: ServerResponse, ...args: unknown[]) { + if (active) { + guard.triggered = true; + const statusCode = args[0] as number; + const headers = (typeof args[1] === "string" ? args[2] : args[1]) as + | Record + | undefined; + guard.buffer.push({ method: "writeHead", statusCode, headers }); + return this; + } + return Reflect.apply(origWriteHead, this, args) as ServerResponse; + } as ServerResponse["writeHead"]; + + raw.write = function (this: ServerResponse, ...args: unknown[]) { + if (active) { + guard.triggered = true; + guard.buffer.push({ method: "write", data: args[0] as string | Buffer }); + return true; + } + return Reflect.apply(origWrite, this, args) as boolean; + } as ServerResponse["write"]; + + raw.end = function (this: ServerResponse, ...args: unknown[]) { + if (active) { + guard.triggered = true; + const data = + typeof args[0] === "function" ? undefined : (args[0] as string | Buffer | undefined); + guard.buffer.push({ method: "end", data }); + return this; + } + return Reflect.apply(origEnd, this, args) as ServerResponse; + } as ServerResponse["end"]; + + raw.flushHeaders = function (this: ServerResponse) { + if (active) { + guard.triggered = true; + guard.buffer.push({ method: "flushHeaders" }); + } else { + origFlushHeaders.call(this); + } + }; + + return guard; +} + +/** + * Sends a normalized 502 response for facilitator boundary failures. + * + * @param reply - The Fastify reply to write to + * @param error - The facilitator response error to surface + */ +function sendFacilitatorError(reply: FastifyReply, error: FacilitatorResponseError): void { + reply.status(502).send({ error: error.message }); +} + +/** + * Configuration for registering a payment scheme with a specific network. + */ +export interface SchemeRegistration { + /** + * The network identifier (e.g., 'eip155:84532', 'solana:mainnet') + */ + network: Network; + + /** + * The scheme server implementation for this network + */ + server: SchemeNetworkServer; +} + +/** + * Registers x402 payment middleware on a Fastify instance using a pre-configured HTTP server. + * + * Use this when you need to configure HTTP-level hooks. + * + * @param app - The Fastify instance + * @param httpServer - Pre-configured x402HTTPResourceServer instance + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * + * @example + * ```typescript + * import { paymentMiddlewareFromHTTPServer, x402ResourceServer, x402HTTPResourceServer } from "@x402/fastify"; + * + * const resourceServer = new x402ResourceServer(facilitatorClient) + * .register(NETWORK, new ExactEvmScheme()); + * + * const httpServer = new x402HTTPResourceServer(resourceServer, routes) + * .onProtectedRequest(requestHook); + * + * paymentMiddlewareFromHTTPServer(app, httpServer); + * ``` + */ +export function paymentMiddlewareFromHTTPServer( + app: FastifyInstance, + httpServer: x402HTTPResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +): void { + if (paywall) { + httpServer.registerPaywallProvider(paywall); + } + + app.decorateRequest("x402Context", undefined); + app.decorateRequest("x402RawGuard", undefined); + + let initPromise: Promise | null = syncFacilitatorOnStart ? httpServer.initialize() : null; + let isInitialized = false; + + /** + * Ensures facilitator initialization succeeds once, while allowing retries after failures. + */ + async function initializeHttpServer(): Promise { + if (!syncFacilitatorOnStart || isInitialized) { + return; + } + + if (!initPromise) { + initPromise = httpServer.initialize(); + } + + try { + await initPromise; + isInitialized = true; + } catch (error) { + initPromise = null; + throw error; + } + } + + let bazaarPromise: Promise | null = null; + if (checkIfBazaarNeeded(httpServer.routes) && !httpServer.server.hasExtension("bazaar")) { + bazaarPromise = import("@x402/extensions/bazaar") + .then(({ bazaarResourceServerExtension }) => { + httpServer.server.registerExtension(bazaarResourceServerExtension); + }) + .catch(err => { + console.error("Failed to load bazaar extension:", err); + }); + } + + app.addHook("onRequest", async (request: FastifyRequest, reply: FastifyReply) => { + const path = request.url.split("?")[0]; + const adapter = new FastifyAdapter(request); + const context: HTTPRequestContext = { + adapter, + path, + method: request.method, + paymentHeader: + (request.headers["payment-signature"] as string | undefined) || + (request.headers["x-payment"] as string | undefined), + }; + + if (!httpServer.requiresPayment(context)) { + return; + } + + if (syncFacilitatorOnStart && !isInitialized) { + try { + await initializeHttpServer(); + } catch (error) { + const facilitatorError = getFacilitatorResponseError(error); + if (facilitatorError) { + return sendFacilitatorError(reply, facilitatorError); + } + throw error; + } + } + + if (bazaarPromise) { + await bazaarPromise; + bazaarPromise = null; + } + + let result: Awaited>; + try { + result = await httpServer.processHTTPRequest(context, paywallConfig); + } catch (error) { + if (error instanceof FacilitatorResponseError) { + return sendFacilitatorError(reply, error); + } + throw error; + } + + switch (result.type) { + case "no-payment-required": + return; + + case "payment-error": { + const { response } = result; + for (const [key, value] of Object.entries(response.headers)) { + reply.header(key, value); + } + if (response.isHtml) { + return reply.status(response.status).type("text/html").send(response.body); + } else { + return reply.status(response.status).send(response.body || {}); + } + } + + case "payment-verified": { + request.x402Context = { + paymentPayload: result.paymentPayload, + paymentRequirements: result.paymentRequirements, + declaredExtensions: result.declaredExtensions, + requestContext: context, + }; + request.x402RawGuard = guardReplyRaw(reply); + return; + } + } + }); + + app.addHook("onSend", async (request: FastifyRequest, reply: FastifyReply, payload: unknown) => { + const rawGuard = request.x402RawGuard; + if (rawGuard) { + rawGuard.deactivate(); + } + + const x402Context = request.x402Context; + if (!x402Context) { + return payload; + } + + let effectivePayload: unknown = payload; + if (rawGuard?.triggered && rawGuard.buffer.length > 0) { + const writeHeadCall = rawGuard.buffer.find( + (c): c is BufferedWriteHead => c.method === "writeHead", + ); + if (writeHeadCall) { + reply.status(writeHeadCall.statusCode); + if (writeHeadCall.headers) { + for (const [key, value] of Object.entries(writeHeadCall.headers)) { + if (value != null) reply.header(key, String(value)); + } + } + } + + const bodyChunks: Buffer[] = []; + for (const call of rawGuard.buffer) { + if (call.method === "write") { + bodyChunks.push(Buffer.from(call.data)); + } else if (call.method === "end" && call.data != null) { + bodyChunks.push(Buffer.from(call.data)); + } + } + if (bodyChunks.length > 0) { + effectivePayload = Buffer.concat(bodyChunks); + } + } + + if (reply.statusCode >= 400) { + return effectivePayload; + } + + try { + const responseBody = getResponseBodyBuffer(effectivePayload); + + const responseHeaders: Record = {}; + for (const [key, value] of Object.entries(reply.getHeaders())) { + if (value != null) { + responseHeaders[key] = String(value); + } + } + + const settleResult = await httpServer.processSettlement( + x402Context.paymentPayload, + x402Context.paymentRequirements, + x402Context.declaredExtensions, + { request: x402Context.requestContext, responseBody, responseHeaders }, + ); + + if (!settleResult.success) { + const { response } = settleResult; + for (const [key, value] of Object.entries(response.headers)) { + reply.header(key, value); + } + reply.status(response.status); + reply.type( + getHeaderValue(response.headers, "content-type") || + (response.isHtml ? "text/html" : "application/json"), + ); + return response.isHtml ? String(response.body ?? "") : JSON.stringify(response.body ?? {}); + } + + for (const [key, value] of Object.entries(settleResult.headers)) { + reply.header(key, value); + } + return effectivePayload; + } catch (error) { + if (error instanceof FacilitatorResponseError) { + reply.status(502); + reply.type("application/json"); + return JSON.stringify({ error: error.message }); + } + console.error(error); + reply.status(402); + reply.type("application/json"); + return JSON.stringify({}); + } + }); +} + +/** + * Registers x402 payment middleware on a Fastify instance using a pre-configured resource server. + * + * Use this when you want to pass a pre-configured x402ResourceServer instance. + * This provides more flexibility for testing, custom configuration, and reusing + * server instances across multiple middlewares. + * + * @param app - The Fastify instance + * @param routes - Route configurations for protected endpoints + * @param server - Pre-configured x402ResourceServer instance + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * + * @example + * ```typescript + * import { paymentMiddleware } from "@x402/fastify"; + * + * const server = new x402ResourceServer(myFacilitatorClient) + * .register(NETWORK, new ExactEvmScheme()); + * + * paymentMiddleware(app, routes, server, paywallConfig); + * ``` + */ +export function paymentMiddleware( + app: FastifyInstance, + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +): void { + const httpServer = new x402HTTPResourceServer(server, routes); + + paymentMiddlewareFromHTTPServer(app, httpServer, paywallConfig, paywall, syncFacilitatorOnStart); +} + +/** + * Registers x402 payment middleware on a Fastify instance using configuration. + * + * Use this when you want to quickly set up middleware with simple configuration. + * This function creates and configures the x402ResourceServer internally. + * + * @param app - The Fastify instance + * @param routes - Route configurations for protected endpoints + * @param facilitatorClients - Optional facilitator client(s) for payment processing + * @param schemes - Optional array of scheme registrations for server-side payment processing + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * + * @example + * ```typescript + * import { paymentMiddlewareFromConfig } from "@x402/fastify"; + * + * paymentMiddlewareFromConfig( + * app, + * routes, + * myFacilitatorClient, + * [{ network: "eip155:8453", server: evmSchemeServer }], + * paywallConfig + * ); + * ``` + */ +export function paymentMiddlewareFromConfig( + app: FastifyInstance, + routes: RoutesConfig, + facilitatorClients?: FacilitatorClient | FacilitatorClient[], + schemes?: SchemeRegistration[], + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +): void { + const ResourceServer = new x402ResourceServer(facilitatorClients); + + if (schemes) { + for (const { network, server: schemeServer } of schemes) { + ResourceServer.register(network, schemeServer); + } + } + + paymentMiddleware(app, routes, ResourceServer, paywallConfig, paywall, syncFacilitatorOnStart); +} + +export { x402ResourceServer, x402HTTPResourceServer } from "@x402/core/server"; + +export type { + PaymentRequired, + PaymentRequirements, + PaymentPayload, + Network, + SchemeNetworkServer, +} from "@x402/core/types"; + +export type { PaywallProvider, PaywallConfig } from "@x402/core/server"; + +export { RouteConfigurationError, SETTLEMENT_OVERRIDES_HEADER } from "@x402/core/server"; + +export type { RouteValidationError } from "@x402/core/server"; + +export { FastifyAdapter } from "./adapter"; diff --git a/typescript/packages/http/fastify/src/malformedPathBypass.test.ts b/typescript/packages/http/fastify/src/malformedPathBypass.test.ts new file mode 100644 index 0000000..f56750b --- /dev/null +++ b/typescript/packages/http/fastify/src/malformedPathBypass.test.ts @@ -0,0 +1,157 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; +import { paymentMiddleware } from "./index"; +import { + x402HTTPResourceServer, + x402ResourceServer, + type HTTPRequestContext, +} from "@x402/core/server"; + +type HookHandler = (...args: unknown[]) => Promise; + +/** + * Captured hooks from a mock Fastify instance. + */ +interface CapturedHooks { + onRequest: HookHandler[]; + onSend: HookHandler[]; +} + +/** + * Creates a mock Fastify instance that captures registered hooks. + * + * @returns Object containing the mock app and captured hooks. + */ +function createMockApp(): { app: FastifyInstance; hooks: CapturedHooks } { + const hooks: CapturedHooks = { onRequest: [], onSend: [] }; + + const app = { + addHook: vi.fn((name: string, handler: HookHandler) => { + if (name === "onRequest") hooks.onRequest.push(handler); + if (name === "onSend") hooks.onSend.push(handler); + }), + decorateRequest: vi.fn(), + } as unknown as FastifyInstance; + + return { app, hooks }; +} + +/** + * Creates a mock Fastify request for testing. + * + * @param options - Configuration options for the mock request. + * @param options.url - The request URL path. + * @param options.method - The HTTP method. + * @param options.headers - Request headers. + * @returns A mock Fastify request. + */ +function createMockRequest( + options: { + url?: string; + method?: string; + headers?: Record; + } = {}, +): FastifyRequest { + return { + url: options.url || "/api/test", + method: options.method || "GET", + headers: options.headers || {}, + query: {}, + body: undefined, + protocol: "https", + hostname: "example.com", + } as unknown as FastifyRequest; +} + +/** + * Creates a mock Fastify reply for testing. + * + * @returns A mock Fastify reply with tracking properties. + */ +function createMockReply(): FastifyReply & { + _status: number; + _headers: Record; + _body: unknown; + _type: string | undefined; +} { + const reply = { + _status: 200, + _headers: {} as Record, + _body: undefined as unknown, + _type: undefined as string | undefined, + statusCode: 200, + header: vi.fn(function (this: typeof reply, key: string, value: string) { + this._headers[key] = value; + return this; + }), + status: vi.fn(function (this: typeof reply, code: number) { + this._status = code; + this.statusCode = code; + return this; + }), + type: vi.fn(function (this: typeof reply, contentType: string) { + this._type = contentType; + return this; + }), + send: vi.fn(function (this: typeof reply, body: unknown) { + this._body = body; + return this; + }), + }; + + return reply as unknown as typeof reply; +} + +describe("paymentMiddleware malformed path bypass", () => { + let processSpy: ReturnType; + + beforeEach(() => { + processSpy = vi + .spyOn(x402HTTPResourceServer.prototype, "processHTTPRequest") + .mockImplementation(async (context: HTTPRequestContext) => { + return { + type: "payment-error", + response: { + status: 402, + body: { error: "Payment required", path: context.path }, + headers: {}, + isHtml: false, + }, + }; + }); + }); + + afterEach(() => { + processSpy.mockRestore(); + }); + + it.each(["/paywall/some-param%", "/paywall/some-param%c0"])( + "does not skip payment check and returns 402 for %s", + async path => { + const routes = { + "/paywall/*": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00", + network: "eip155:8453", + }, + }, + }; + + const server = new x402ResourceServer(); + + const { app, hooks } = createMockApp(); + paymentMiddleware(app, routes, server, undefined, undefined, false); + + const request = createMockRequest({ url: path }); + const reply = createMockReply(); + + await hooks.onRequest[0](request, reply); + + expect(processSpy).toHaveBeenCalled(); + expect(processSpy.mock.calls[0]?.[0]).toEqual(expect.objectContaining({ path })); + expect(reply._status).toBe(402); + }, + ); +}); diff --git a/typescript/packages/mcp/tsconfig.json b/typescript/packages/http/fastify/tsconfig.json similarity index 68% rename from typescript/packages/mcp/tsconfig.json rename to typescript/packages/http/fastify/tsconfig.json index d14a78d..1b119d3 100644 --- a/typescript/packages/mcp/tsconfig.json +++ b/typescript/packages/http/fastify/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "allowJs": false, "checkJs": false diff --git a/typescript/packages/mcp/tsup.config.ts b/typescript/packages/http/fastify/tsup.config.ts similarity index 100% rename from typescript/packages/mcp/tsup.config.ts rename to typescript/packages/http/fastify/tsup.config.ts diff --git a/typescript/packages/mcp/vitest.config.ts b/typescript/packages/http/fastify/vitest.config.ts similarity index 63% rename from typescript/packages/mcp/vitest.config.ts rename to typescript/packages/http/fastify/vitest.config.ts index c1e74c9..156f8c9 100644 --- a/typescript/packages/mcp/vitest.config.ts +++ b/typescript/packages/http/fastify/vitest.config.ts @@ -5,8 +5,6 @@ import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig(({ mode }) => ({ test: { env: loadEnv(mode, process.cwd(), ""), - // Exclude integration tests from default test run (they require real blockchain) - exclude: ["**/node_modules/**", "**/dist/**", "**/test/integration/**"], }, plugins: [tsconfigPaths({ projects: ["."] })], })); diff --git a/typescript/packages/http/fetch/CHANGELOG.md b/typescript/packages/http/fetch/CHANGELOG.md index c3c08f1..22113e9 100644 --- a/typescript/packages/http/fetch/CHANGELOG.md +++ b/typescript/packages/http/fetch/CHANGELOG.md @@ -1,5 +1,60 @@ # @x402/fetch Changelog +## 2.9.0 + +### Minor Changes + +- 2250cae: Migrated project from coinbase/x402 to x402-foundation/x402 organization + +### Patch Changes + +- Updated dependencies [8cf3fca] +- Updated dependencies [c0e3969] +- Updated dependencies [2250cae] +- Updated dependencies [d352574] + - @x402/core@2.9.0 + +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- Updated dependencies + - @x402/core@2.6.0 + +## 2.5.0 + +### Minor Changes + +- Updated dependencies [96a9db0] +- Updated dependencies [d0a2b11] +- Updated dependencies + - @x402/core@2.5.0 + +## 2.4.0 + +### Minor Changes + +- Updated dependencies [57a5488] +- Updated dependencies [018181b] +- Updated dependencies [3fb55d7] + - @x402/core@2.4.0 + ## 2.3.0 ### Minor Changes diff --git a/typescript/packages/http/fetch/README.md b/typescript/packages/http/fetch/README.md index d5eebbe..d78819a 100644 --- a/typescript/packages/http/fetch/README.md +++ b/typescript/packages/http/fetch/README.md @@ -22,7 +22,7 @@ const account = privateKeyToAccount("0xYourPrivateKey"); const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, { schemes: [ { - network: "eip155:8453", // Base Sepolia + network: "eip155:8453", // Base Mainnet client: new ExactEvmScheme(account), }, ], diff --git a/typescript/packages/http/fetch/package.json b/typescript/packages/http/fetch/package.json index 1b18c88..17dc7fe 100644 --- a/typescript/packages/http/fetch/package.json +++ b/typescript/packages/http/fetch/package.json @@ -1,6 +1,6 @@ { "name": "@x402/fetch", - "version": "2.3.0", + "version": "2.9.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", @@ -17,8 +17,8 @@ }, "keywords": [], "license": "Apache-2.0", - "author": "Coinbase Inc.", - "repository": "https://github.com/coinbase/x402", + "author": "x402 Foundation", + "repository": "https://github.com/x402-foundation/x402", "description": "x402 Payment Protocol Fetch Extension", "devDependencies": { "@eslint/js": "^9.24.0", diff --git a/typescript/packages/http/hono/CHANGELOG.md b/typescript/packages/http/hono/CHANGELOG.md index f2e84f0..17d5aaf 100644 --- a/typescript/packages/http/hono/CHANGELOG.md +++ b/typescript/packages/http/hono/CHANGELOG.md @@ -1,5 +1,87 @@ # @x402/hono Changelog +## 2.9.0 + +### Minor Changes + +- 2250cae: Migrated project from coinbase/x402 to x402-foundation/x402 organization +- d352574: Add SettlementOverrides support for partial settlement (upto scheme). Route handlers can call setSettlementOverrides() to settle less than the authorized maximum, enabling usage-based billing. + +### Patch Changes + +- Updated dependencies [8cf3fca] +- Updated dependencies [c0e3969] +- Updated dependencies [2250cae] +- Updated dependencies [d352574] + - @x402/core@2.9.0 + - @x402/paywall@2.9.0 + - @x402/extensions@2.9.0 + +## 2.8.0 + +### Minor Changes + +- 4c1e44f: Treat malformed facilitator success payloads as upstream facilitator errors and return 502 responses from framework middleware instead of flattening them into payment failures. +- Updated dependencies [4f2f4f3] +- Updated dependencies [067f297] +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/extensions@2.8.0 + - @x402/core@2.8.0 + - @x402/paywall@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [34d2442] +- Updated dependencies [8b731cb] +- Updated dependencies [f2bbb5c] +- Updated dependencies [8931cb3] +- Updated dependencies [34d2442] + - @x402/extensions@2.7.0 + - @x402/core@2.7.0 + - @x402/paywall@2.7.0 + +## 2.6.0 + +### Minor Changes + +- aeef1bf: Added dynamic function for servers to generate custom response for settlement failures defaulting to empty +- 2564781: Include PAYMENT-RESPONSE header on settlement failure responses +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + - @x402/paywall@2.6.0 + +## 2.5.0 + +### Minor Changes + +- Updated dependencies [96a9db0] +- Updated dependencies [7fe268f] +- Updated dependencies [1ab1c86] +- Updated dependencies [d0a2b11] +- Updated dependencies + - @x402/core@2.5.0 + - @x402/extensions@2.5.0 + - @x402/paywall@2.4.1 + +## 2.4.0 + +### Minor Changes + +- Updated dependencies [57a5488] +- Updated dependencies [018181b] +- Updated dependencies [3fb55d7] + - @x402/core@2.4.0 + - @x402/extensions@2.4.0 + - @x402/paywall@2.4.0 + ## 2.3.0 ### Minor Changes diff --git a/typescript/packages/http/hono/package.json b/typescript/packages/http/hono/package.json index 50b4b99..f240bb0 100644 --- a/typescript/packages/http/hono/package.json +++ b/typescript/packages/http/hono/package.json @@ -1,6 +1,6 @@ { "name": "@x402/hono", - "version": "2.3.0", + "version": "2.9.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", @@ -17,8 +17,8 @@ }, "keywords": [], "license": "Apache-2.0", - "author": "Coinbase Inc.", - "repository": "https://github.com/coinbase/x402", + "author": "x402 Foundation", + "repository": "https://github.com/x402-foundation/x402", "description": "x402 Payment Protocol", "devDependencies": { "@eslint/js": "^9.24.0", @@ -45,7 +45,7 @@ }, "peerDependencies": { "hono": "^4.0.0", - "@x402/paywall": "workspace:*" + "@x402/paywall": "workspace:^" }, "peerDependenciesMeta": { "@x402/paywall": { diff --git a/typescript/packages/http/hono/src/index.test.ts b/typescript/packages/http/hono/src/index.test.ts index 25b1edb..0f6a292 100644 --- a/typescript/packages/http/hono/src/index.test.ts +++ b/typescript/packages/http/hono/src/index.test.ts @@ -7,6 +7,7 @@ import type { FacilitatorClient, } from "@x402/core/server"; import { + FacilitatorResponseError, x402ResourceServer, x402HTTPResourceServer as HTTPResourceServer, } from "@x402/core/server"; @@ -40,6 +41,28 @@ let mockRegisterPaywallProvider: ReturnType; let mockRequiresPayment: ReturnType; vi.mock("@x402/core/server", () => ({ + SETTLEMENT_OVERRIDES_HEADER: "Settlement-Overrides", + FacilitatorResponseError: class FacilitatorResponseError extends Error { + /** + * Creates a mock facilitator response error. + * + * @param message - Error message. + */ + constructor(message: string) { + super(message); + this.name = "FacilitatorResponseError"; + } + }, + getFacilitatorResponseError: (error: unknown) => { + let current = error; + while (current instanceof Error) { + if (current.name === "FacilitatorResponseError") { + return current; + } + current = (current as Error & { cause?: unknown }).cause; + } + return null; + }, x402ResourceServer: vi.fn().mockImplementation(() => ({ initialize: vi.fn().mockResolvedValue(undefined), registerExtension: vi.fn(), @@ -71,7 +94,15 @@ function setupMockHttpServer( processResult: HTTPProcessResult, settlementResult: | { success: true; headers: Record } - | { success: false; errorReason: string } = { success: true, headers: {} }, + | { + success: false; + errorReason: string; + headers: Record; + response: { status: number; headers: Record; body?: unknown }; + } = { + success: true, + headers: {}, + }, ): void { mockProcessHTTPRequest.mockResolvedValue(processResult); mockProcessSettlement.mockResolvedValue(settlementResult); @@ -294,11 +325,14 @@ describe("paymentMiddleware", () => { ); const context = createMockContext(); - // Create a proper Response mock with headers + // Create a proper Response mock with headers and clone method const responseHeaders = new Headers(); const mockResponse = { status: 200, headers: responseHeaders, + clone: () => ({ + arrayBuffer: async () => new ArrayBuffer(0), + }), } as unknown as Response; const next = vi.fn().mockImplementation(async () => { @@ -312,6 +346,13 @@ describe("paymentMiddleware", () => { mockPaymentPayload, mockPaymentRequirements, undefined, + expect.objectContaining({ + request: expect.objectContaining({ + path: "/api/test", + method: "GET", + }), + responseBody: expect.any(Buffer), + }), ); expect(responseHeaders.get("PAYMENT-RESPONSE")).toBe("settled"); }); @@ -367,18 +408,55 @@ describe("paymentMiddleware", () => { context.res = { status: 200, headers: responseHeaders, + clone: () => ({ + arrayBuffer: async () => new ArrayBuffer(0), + }), } as unknown as Response; }); await middleware(context, next); - expect(context.json).toHaveBeenCalledWith( - { - error: "Settlement failed", - details: "Settlement rejected", - }, - 402, + expect(context.json).toHaveBeenCalledWith({}, 402); + }); + + it("retries initialization after a facilitator init failure", async () => { + const initialize = vi + .fn() + .mockRejectedValueOnce( + new Error("Failed to initialize: no supported payment kinds loaded from any facilitator.", { + cause: new FacilitatorResponseError( + "Facilitator supported returned invalid JSON: not-json", + ), + }), + ) + .mockResolvedValueOnce(undefined); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize, + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, ); + mockProcessHTTPRequest.mockResolvedValue({ type: "no-payment-required" }); + + const middleware = paymentMiddleware(mockRoutes, {} as unknown as x402ResourceServer); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(createMockContext(), next); + await middleware(createMockContext(), next); + + expect(initialize).toHaveBeenCalledTimes(2); + expect(mockProcessHTTPRequest).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); }); it("returns 402 when settlement returns success: false", async () => { @@ -388,7 +466,19 @@ describe("paymentMiddleware", () => { paymentPayload: mockPaymentPayload, paymentRequirements: mockPaymentRequirements, }, - { success: false, errorReason: "Insufficient funds" }, + { + success: false, + errorReason: "Insufficient funds", + headers: { "PAYMENT-RESPONSE": "settlement-failed-encoded" }, + response: { + status: 402, + headers: { + "Content-Type": "application/json", + "PAYMENT-RESPONSE": "settlement-failed-encoded", + }, + body: {}, + }, + }, ); const middleware = paymentMiddleware( @@ -405,18 +495,18 @@ describe("paymentMiddleware", () => { context.res = { status: 200, headers: responseHeaders, + clone: () => ({ + arrayBuffer: async () => new ArrayBuffer(0), + }), } as unknown as Response; }); await middleware(context, next); - expect(context.json).toHaveBeenCalledWith( - { - error: "Settlement failed", - details: "Insufficient funds", - }, - 402, - ); + expect(context.res?.status).toBe(402); + expect(context.res?.headers.get("PAYMENT-RESPONSE")).toBe("settlement-failed-encoded"); + const body = await context.res?.json(); + expect(body).toEqual({}); }); it("passes paywallConfig to processHTTPRequest", async () => { diff --git a/typescript/packages/http/hono/src/index.ts b/typescript/packages/http/hono/src/index.ts index 5a02b06..049dd08 100644 --- a/typescript/packages/http/hono/src/index.ts +++ b/typescript/packages/http/hono/src/index.ts @@ -6,11 +6,26 @@ import { x402ResourceServer, RoutesConfig, FacilitatorClient, + FacilitatorResponseError, + getFacilitatorResponseError, + SETTLEMENT_OVERRIDES_HEADER, + SettlementOverrides, } from "@x402/core/server"; import { SchemeNetworkServer, Network } from "@x402/core/types"; import { Context, MiddlewareHandler } from "hono"; import { HonoAdapter } from "./adapter"; +/** + * Set settlement overrides on the response for partial settlement. + * The middleware will extract these before settlement and strip the header from the client response. + * + * @param c - Hono context + * @param overrides - Settlement overrides (e.g., { amount: "500" } for partial settlement) + */ +export function setSettlementOverrides(c: Context, overrides: SettlementOverrides): void { + c.header(SETTLEMENT_OVERRIDES_HEADER, JSON.stringify(overrides)); +} + /** * Check if any routes in the configuration declare bazaar extensions * @@ -44,6 +59,17 @@ export interface SchemeRegistration { server: SchemeNetworkServer; } +/** + * Builds a normalized 502 response for facilitator boundary failures. + * + * @param c - The current Hono context + * @param error - The facilitator response error to surface + * @returns A JSON 502 response + */ +function facilitatorErrorResponse(c: Context, error: FacilitatorResponseError): Response { + return c.json({ error: error.message }, 502); +} + /** * Hono payment middleware for x402 protocol (direct HTTP server instance). * @@ -82,6 +108,28 @@ export function paymentMiddlewareFromHTTPServer( // Store initialization promise (not the result) // httpServer.initialize() fetches facilitator support and validates routes let initPromise: Promise | null = syncFacilitatorOnStart ? httpServer.initialize() : null; + let isInitialized = false; + + /** + * Ensures facilitator initialization succeeds once, while allowing retries after failures. + */ + async function initializeHttpServer(): Promise { + if (!syncFacilitatorOnStart || isInitialized) { + return; + } + + if (!initPromise) { + initPromise = httpServer.initialize(); + } + + try { + await initPromise; + isInitialized = true; + } catch (error) { + initPromise = null; + throw error; + } + } // Dynamically register bazaar extension if routes declare it and not already registered // Skip if pre-registered (e.g., in serverless environments where static imports are used) @@ -112,9 +160,16 @@ export function paymentMiddlewareFromHTTPServer( } // Only initialize when processing a protected route - if (initPromise) { - await initPromise; - initPromise = null; // Clear after first await + if (syncFacilitatorOnStart && !isInitialized) { + try { + await initializeHttpServer(); + } catch (error) { + const facilitatorError = getFacilitatorResponseError(error); + if (facilitatorError) { + return facilitatorErrorResponse(c, facilitatorError); + } + throw error; + } } // Await bazaar extension loading if needed @@ -124,7 +179,15 @@ export function paymentMiddlewareFromHTTPServer( } // Process payment requirement check - const result = await httpServer.processHTTPRequest(context, paywallConfig); + let result: Awaited>; + try { + result = await httpServer.processHTTPRequest(context, paywallConfig); + } catch (error) { + if (error instanceof FacilitatorResponseError) { + return facilitatorErrorResponse(c, error); + } + throw error; + } // Handle the different result types switch (result.type) { @@ -159,6 +222,14 @@ export function paymentMiddlewareFromHTTPServer( return; } + // Get response body for extensions + const responseBody = Buffer.from(await res.clone().arrayBuffer()); + + const responseHeaders: Record = {}; + res.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + // Clear the response so we can modify headers c.res = undefined; @@ -167,17 +238,19 @@ export function paymentMiddlewareFromHTTPServer( paymentPayload, paymentRequirements, declaredExtensions, + { request: context, responseBody, responseHeaders }, ); if (!settleResult.success) { // Settlement failed - do not return the protected resource - res = c.json( - { - error: "Settlement failed", - details: settleResult.errorReason, - }, - 402, - ); + const { response } = settleResult; + const body = response.isHtml + ? String(response.body ?? "") + : JSON.stringify(response.body ?? {}); + res = new Response(body, { + status: response.status, + headers: response.headers, + }); } else { // Settlement succeeded - add headers to response Object.entries(settleResult.headers).forEach(([key, value]) => { @@ -185,15 +258,14 @@ export function paymentMiddlewareFromHTTPServer( }); } } catch (error) { + if (error instanceof FacilitatorResponseError) { + res = facilitatorErrorResponse(c, error); + c.res = res; + return; + } console.error(error); // If settlement fails, return an error response - res = c.json( - { - error: "Settlement failed", - details: error instanceof Error ? error.message : "Unknown error", - }, - 402, - ); + res = c.json({}, 402); } // Restore the response (potentially modified with settlement headers) @@ -302,9 +374,9 @@ export type { SchemeNetworkServer, } from "@x402/core/types"; -export type { PaywallProvider, PaywallConfig } from "@x402/core/server"; +export type { PaywallProvider, PaywallConfig, SettlementOverrides } from "@x402/core/server"; -export { RouteConfigurationError } from "@x402/core/server"; +export { RouteConfigurationError, SETTLEMENT_OVERRIDES_HEADER } from "@x402/core/server"; export type { RouteValidationError } from "@x402/core/server"; diff --git a/typescript/packages/http/next/CHANGELOG.md b/typescript/packages/http/next/CHANGELOG.md index 4dd58c9..3542e43 100644 --- a/typescript/packages/http/next/CHANGELOG.md +++ b/typescript/packages/http/next/CHANGELOG.md @@ -1,5 +1,87 @@ # @x402/next Changelog +## 2.9.0 + +### Minor Changes + +- 2250cae: Migrated project from coinbase/x402 to x402-foundation/x402 organization + +### Patch Changes + +- Updated dependencies [8cf3fca] +- Updated dependencies [c0e3969] +- Updated dependencies [2250cae] +- Updated dependencies [d352574] + - @x402/core@2.9.0 + - @x402/paywall@2.9.0 + - @x402/extensions@2.9.0 + +## 2.8.0 + +### Minor Changes + +- 4c1e44f: Treat malformed facilitator success payloads as upstream facilitator errors and return 502 responses from framework middleware instead of flattening them into payment failures. +- Updated dependencies [4f2f4f3] +- Updated dependencies [067f297] +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/extensions@2.8.0 + - @x402/core@2.8.0 + - @x402/paywall@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [34d2442] +- Updated dependencies [8b731cb] +- Updated dependencies [f2bbb5c] +- Updated dependencies [8931cb3] +- Updated dependencies [34d2442] + - @x402/extensions@2.7.0 + - @x402/core@2.7.0 + - @x402/paywall@2.7.0 + +## 2.6.0 + +### Minor Changes + +- aeef1bf: Added dynamic function for servers to generate custom response for settlement failures defaulting to empty +- 205257b: Cleaned up dependencies +- 2564781: Include PAYMENT-RESPONSE header on settlement failure responses +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + - @x402/paywall@2.6.0 + +## 2.5.0 + +### Minor Changes + +- Updated dependencies [96a9db0] +- Updated dependencies [7fe268f] +- Updated dependencies [1ab1c86] +- Updated dependencies [d0a2b11] +- Updated dependencies + - @x402/core@2.5.0 + - @x402/extensions@2.5.0 + - @x402/paywall@2.4.1 + +## 2.4.0 + +### Minor Changes + +- Updated dependencies [57a5488] +- Updated dependencies [018181b] +- Updated dependencies [3fb55d7] + - @x402/core@2.4.0 + - @x402/extensions@2.4.0 + - @x402/paywall@2.4.0 + ## 2.3.0 ### Minor Changes diff --git a/typescript/packages/http/next/package.json b/typescript/packages/http/next/package.json index 0e74cb0..2cf70f2 100644 --- a/typescript/packages/http/next/package.json +++ b/typescript/packages/http/next/package.json @@ -1,6 +1,6 @@ { "name": "@x402/next", - "version": "2.3.0", + "version": "2.9.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", @@ -17,8 +17,8 @@ }, "keywords": [], "license": "Apache-2.0", - "author": "Coinbase Inc.", - "repository": "https://github.com/coinbase/x402", + "author": "x402 Foundation", + "repository": "https://github.com/x402-foundation/x402", "description": "x402 Payment Protocol", "devDependencies": { "@eslint/js": "^9.24.0", @@ -38,14 +38,13 @@ "vitest": "^3.0.5" }, "dependencies": { - "@coinbase/cdp-sdk": "^1.22.0", "@x402/core": "workspace:~", "@x402/extensions": "workspace:~", "zod": "^3.24.2" }, "peerDependencies": { "next": "^16.0.10", - "@x402/paywall": "workspace:*" + "@x402/paywall": "workspace:^" }, "peerDependenciesMeta": { "@x402/paywall": { diff --git a/typescript/packages/http/next/src/index.test.ts b/typescript/packages/http/next/src/index.test.ts index b1bd6d1..3d1ded9 100644 --- a/typescript/packages/http/next/src/index.test.ts +++ b/typescript/packages/http/next/src/index.test.ts @@ -30,6 +30,27 @@ const mockFunctions = { // Mock @x402/core/server vi.mock("@x402/core/server", () => ({ + FacilitatorResponseError: class FacilitatorResponseError extends Error { + /** + * Creates a mock facilitator response error. + * + * @param message - Error message. + */ + constructor(message: string) { + super(message); + this.name = "FacilitatorResponseError"; + } + }, + getFacilitatorResponseError: (error: unknown) => { + let current = error; + while (current instanceof Error) { + if (current.name === "FacilitatorResponseError") { + return current; + } + current = (current as Error & { cause?: unknown }).cause; + } + return null; + }, x402ResourceServer: vi.fn().mockImplementation(() => ({ initialize: vi.fn().mockResolvedValue(undefined), registerExtension: vi.fn(), @@ -87,7 +108,15 @@ function createMockHttpServer( processResult: HTTPProcessResult, settlementResult: | { success: true; headers: Record } - | { success: false; errorReason: string } = { success: true, headers: {} }, + | { + success: false; + errorReason: string; + headers: Record; + response: { status: number; headers: Record; body?: unknown }; + } = { + success: true, + headers: {}, + }, ): x402HTTPResourceServer { return { processHTTPRequest: vi.fn().mockResolvedValue(processResult), @@ -226,6 +255,13 @@ describe("paymentProxy", () => { mockPaymentPayload, mockPaymentRequirements, undefined, + expect.objectContaining({ + request: expect.objectContaining({ + path: "/api/test", + method: "GET", + }), + responseBody: expect.any(Buffer), + }), ); }); @@ -264,7 +300,7 @@ describe("paymentProxy", () => { expect(response.status).toBe(402); const body = await response.json(); - expect(body.error).toBe("Settlement failed"); + expect(body).toEqual({}); }); it("returns 402 when settlement returns success: false, not the resource", async () => { @@ -274,7 +310,19 @@ describe("paymentProxy", () => { paymentPayload: mockPaymentPayload, paymentRequirements: mockPaymentRequirements, }, - { success: false, errorReason: "Insufficient funds" }, + { + success: false, + errorReason: "Insufficient funds", + headers: { "PAYMENT-RESPONSE": "settlement-failed-encoded" }, + response: { + status: 402, + headers: { + "Content-Type": "application/json", + "PAYMENT-RESPONSE": "settlement-failed-encoded", + }, + body: {}, + }, + }, ); setupMockCreateHttpServer(mockServer); @@ -283,8 +331,8 @@ describe("paymentProxy", () => { expect(response.status).toBe(402); const body = await response.json(); - expect(body.error).toBe("Settlement failed"); - expect(body.details).toBe("Insufficient funds"); + expect(body).toEqual({}); + expect(response.headers.get("PAYMENT-RESPONSE")).toBe("settlement-failed-encoded"); }); }); @@ -382,7 +430,7 @@ describe("withX402", () => { expect(handler).toHaveBeenCalled(); expect(response.status).toBe(402); const body = await response.json(); - expect(body.error).toBe("Settlement failed"); + expect(body).toEqual({}); }); it("returns 402 when settlement returns success: false, not the handler response", async () => { @@ -392,7 +440,19 @@ describe("withX402", () => { paymentPayload: mockPaymentPayload, paymentRequirements: mockPaymentRequirements, }, - { success: false, errorReason: "Insufficient funds" }, + { + success: false, + errorReason: "Insufficient funds", + headers: { "PAYMENT-RESPONSE": "settlement-failed-encoded" }, + response: { + status: 402, + headers: { + "Content-Type": "application/json", + "PAYMENT-RESPONSE": "settlement-failed-encoded", + }, + body: {}, + }, + }, ); setupMockCreateHttpServer(mockServer); const handler = vi.fn().mockResolvedValue(NextResponse.json({ data: "protected" })); @@ -403,8 +463,8 @@ describe("withX402", () => { expect(handler).toHaveBeenCalled(); expect(response.status).toBe(402); const body = await response.json(); - expect(body.error).toBe("Settlement failed"); - expect(body.details).toBe("Insufficient funds"); + expect(body).toEqual({}); + expect(response.headers.get("PAYMENT-RESPONSE")).toBe("settlement-failed-encoded"); }); }); diff --git a/typescript/packages/http/next/src/index.ts b/typescript/packages/http/next/src/index.ts index 0b76466..aab58e9 100644 --- a/typescript/packages/http/next/src/index.ts +++ b/typescript/packages/http/next/src/index.ts @@ -5,6 +5,7 @@ import { RoutesConfig, RouteConfig, FacilitatorClient, + FacilitatorResponseError, } from "@x402/core/server"; import { SchemeNetworkServer, Network } from "@x402/core/types"; import { NextRequest, NextResponse } from "next/server"; @@ -13,6 +14,8 @@ import { createRequestContext, handlePaymentError, handleSettlement, + createFacilitatorErrorResponse, + getFacilitatorResponseError, } from "./utils"; import { x402HTTPResourceServer } from "@x402/core/server"; @@ -85,7 +88,15 @@ export function paymentProxyFromHTTPServer( } // Only initialize when processing a protected route - await init(); + try { + await init(); + } catch (error) { + const facilitatorError = getFacilitatorResponseError(error); + if (facilitatorError) { + return createFacilitatorErrorResponse(facilitatorError); + } + throw error; + } // Await bazaar extension loading if needed if (bazaarPromise) { @@ -94,7 +105,15 @@ export function paymentProxyFromHTTPServer( } // Process payment requirement check - const result = await httpServer.processHTTPRequest(context, paywallConfig); + let result: Awaited>; + try { + result = await httpServer.processHTTPRequest(context, paywallConfig); + } catch (error) { + if (error instanceof FacilitatorResponseError) { + return createFacilitatorErrorResponse(error); + } + throw error; + } // Handle the different result types switch (result.type) { @@ -117,6 +136,7 @@ export function paymentProxyFromHTTPServer( paymentPayload, paymentRequirements, declaredExtensions, + context, ); } } @@ -293,6 +313,7 @@ export function withX402FromHTTPServer( paymentPayload, paymentRequirements, declaredExtensions, + context, ) as Promise>; } } diff --git a/typescript/packages/http/next/src/utils.test.ts b/typescript/packages/http/next/src/utils.test.ts index 78dd4af..2b0167b 100644 --- a/typescript/packages/http/next/src/utils.test.ts +++ b/typescript/packages/http/next/src/utils.test.ts @@ -13,15 +13,38 @@ import { handleSettlement, } from "./utils"; +let mockInitialize: ReturnType; + // Mock @x402/core/server vi.mock("@x402/core/server", () => { const MockHTTPResourceServer = vi.fn().mockImplementation(() => ({ - initialize: vi.fn().mockResolvedValue(undefined), + initialize: mockInitialize, registerPaywallProvider: vi.fn(), processSettlement: vi.fn(), requiresPayment: vi.fn().mockReturnValue(true), })); return { + FacilitatorResponseError: class FacilitatorResponseError extends Error { + /** + * Creates a mock facilitator response error. + * + * @param message - Error message. + */ + constructor(message: string) { + super(message); + this.name = "FacilitatorResponseError"; + } + }, + getFacilitatorResponseError: (error: unknown) => { + let current = error; + while (current instanceof Error) { + if (current.name === "FacilitatorResponseError") { + return current; + } + current = (current as Error & { cause?: unknown }).cause; + } + return null; + }, x402HTTPResourceServer: MockHTTPResourceServer, x402ResourceServer: vi.fn(), }; @@ -62,6 +85,10 @@ function createMockResourceServer(): x402ResourceServer { } describe("createHttpServer", () => { + beforeEach(() => { + mockInitialize = vi.fn().mockResolvedValue(undefined); + }); + it("creates server and initializes on start by default", async () => { const routes = { "/api/*": { @@ -107,6 +134,25 @@ describe("createHttpServer", () => { await init(); expect(httpServer.registerPaywallProvider).toHaveBeenCalledWith(paywall); }); + + it("retries initialization after a facilitator init failure", async () => { + mockInitialize = vi + .fn() + .mockRejectedValueOnce(new Error("not-json")) + .mockResolvedValueOnce(undefined); + const routes = { + "/api/*": { + accepts: { scheme: "exact", payTo: "0x123", price: "$0.01", network: "eip155:84532" }, + }, + } as const; + const server = createMockResourceServer(); + + const { init } = createHttpServer(routes, server); + + await expect(init()).rejects.toThrow("not-json"); + await expect(init()).resolves.toBeUndefined(); + expect(mockInitialize).toHaveBeenCalledTimes(2); + }); }); describe("createRequestContext", () => { @@ -256,6 +302,10 @@ describe("handleSettlement", () => { mockPaymentPayload, mockRequirements, undefined, + expect.objectContaining({ + request: undefined, + responseBody: expect.any(Buffer), + }), ); }); @@ -265,6 +315,15 @@ describe("handleSettlement", () => { errorReason: "Insufficient funds", transaction: "", network: "eip155:84532", + headers: { "PAYMENT-RESPONSE": "settlement-failed-encoded" }, + response: { + status: 402, + headers: { + "Content-Type": "application/json", + "PAYMENT-RESPONSE": "settlement-failed-encoded", + }, + body: {}, + }, }); const response = new NextResponse("OK", { status: 200 }); @@ -276,9 +335,9 @@ describe("handleSettlement", () => { ); expect(result.status).toBe(402); - const body = (await result.json()) as { error: string; details: string }; - expect(body.error).toBe("Settlement failed"); - expect(body.details).toBe("Insufficient funds"); + const body = await result.json(); + expect(body).toEqual({}); + expect(result.headers.get("PAYMENT-RESPONSE")).toBe("settlement-failed-encoded"); }); it("returns 402 error response when settlement throws", async () => { @@ -293,8 +352,7 @@ describe("handleSettlement", () => { ); expect(result.status).toBe(402); - const body = (await result.json()) as { error: string; details: string }; - expect(body.error).toBe("Settlement failed"); - expect(body.details).toBe("Settlement rejected"); + const body = await result.json(); + expect(body).toEqual({}); }); }); diff --git a/typescript/packages/http/next/src/utils.ts b/typescript/packages/http/next/src/utils.ts index 1e5ec9c..11b634a 100644 --- a/typescript/packages/http/next/src/utils.ts +++ b/typescript/packages/http/next/src/utils.ts @@ -6,6 +6,8 @@ import { x402HTTPResourceServer, x402ResourceServer, RoutesConfig, + FacilitatorResponseError, + getFacilitatorResponseError as getCoreFacilitatorResponseError, } from "@x402/core/server"; import { PaymentPayload, PaymentRequirements } from "@x402/core/types"; import { NextAdapter } from "./adapter"; @@ -18,6 +20,21 @@ export interface HttpServerInstance { init: () => Promise; } +export const getFacilitatorResponseError = getCoreFacilitatorResponseError; + +/** + * Builds a normalized 502 response for facilitator boundary failures. + * + * @param error - The facilitator response error to surface + * @returns A JSON 502 response + */ +export function createFacilitatorErrorResponse(error: FacilitatorResponseError): NextResponse { + return new NextResponse(JSON.stringify({ error: error.message }), { + status: 502, + headers: { "Content-Type": "application/json" }, + }); +} + /** * Prepares an existing x402HTTPResourceServer with initialization logic * @@ -39,14 +56,28 @@ export function prepareHttpServer( // Store initialization promise (not the result) // httpServer.initialize() fetches facilitator support and validates routes let initPromise: Promise | null = syncFacilitatorOnStart ? httpServer.initialize() : null; + let isInitialized = false; return { httpServer, + /** + * Ensures facilitator initialization succeeds once, while allowing retries after failures. + */ async init() { - // Ensure initialization completes before processing - if (initPromise) { + if (!syncFacilitatorOnStart || isInitialized) { + return; + } + + if (!initPromise) { + initPromise = httpServer.initialize(); + } + + try { await initPromise; - initPromise = null; // Clear after first await + isInitialized = true; + } catch (error) { + initPromise = null; + throw error; } }, }; @@ -121,6 +152,7 @@ export function handlePaymentError(response: HTTPResponseInstructions): NextResp * @param paymentPayload - The payment payload from the client * @param paymentRequirements - The payment requirements for the route * @param declaredExtensions - Optional declared extensions (for per-key enrichment) + * @param httpContext - Optional HTTP request context for extensions * @returns The response with settlement headers or an error response if settlement fails */ export async function handleSettlement( @@ -129,6 +161,7 @@ export async function handleSettlement( paymentPayload: PaymentPayload, paymentRequirements: PaymentRequirements, declaredExtensions?: Record, + httpContext?: HTTPRequestContext, ): Promise { // If the response from the protected route is >= 400, do not settle payment if (response.status >= 400) { @@ -136,24 +169,24 @@ export async function handleSettlement( } try { + // Get response body for extensions + const responseBody = Buffer.from(await response.clone().arrayBuffer()); + const result = await httpServer.processSettlement( paymentPayload, paymentRequirements, declaredExtensions, + { request: httpContext, responseBody }, ); if (!result.success) { // Settlement failed - do not return the protected resource - return new NextResponse( - JSON.stringify({ - error: "Settlement failed", - details: result.errorReason, - }), - { - status: 402, - headers: { "Content-Type": "application/json" }, - }, - ); + const { response } = result; + const body = response.isHtml ? response.body : JSON.stringify(response.body ?? {}); + return new NextResponse(body, { + status: response.status, + headers: response.headers, + }); } // Settlement succeeded - add headers and return original response @@ -163,17 +196,14 @@ export async function handleSettlement( return response; } catch (error) { + if (error instanceof FacilitatorResponseError) { + return createFacilitatorErrorResponse(error); + } console.error("Settlement failed:", error); // If settlement fails, return an error response - return new NextResponse( - JSON.stringify({ - error: "Settlement failed", - details: error instanceof Error ? error.message : "Unknown error", - }), - { - status: 402, - headers: { "Content-Type": "application/json" }, - }, - ); + return new NextResponse(JSON.stringify({}), { + status: 402, + headers: { "Content-Type": "application/json" }, + }); } } diff --git a/typescript/packages/http/paywall/CHANGELOG.md b/typescript/packages/http/paywall/CHANGELOG.md index 88bb904..d22370a 100644 --- a/typescript/packages/http/paywall/CHANGELOG.md +++ b/typescript/packages/http/paywall/CHANGELOG.md @@ -1,5 +1,66 @@ # @x402/paywall Changelog +## 2.9.0 + +### Minor Changes + +- 2250cae: Migrated project from coinbase/x402 to x402-foundation/x402 organization + +### Patch Changes + +- Updated dependencies [8cf3fca] +- Updated dependencies [c0e3969] +- Updated dependencies [2250cae] +- Updated dependencies [d352574] + - @x402/core@2.9.0 + +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- 34d2442: Fixed encoding of characters outside of the Latin1 range +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- 29fe09a: Make ResourceInfo.description, ResourceInfo.mimeType, and PaymentPayload.resource optional to match v2 spec +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + +## 2.5.0 + +### Minor Changes + +- Updated dependencies [96a9db0] +- Updated dependencies [d0a2b11] +- Updated dependencies + - @x402/core@2.5.0 + +## 2.4.0 + +### Minor Changes + +- Updated dependencies [57a5488] +- Updated dependencies [018181b] +- Updated dependencies [3fb55d7] + - @x402/core@2.4.0 + ## 2.3.0 ### Minor Changes diff --git a/typescript/packages/http/paywall/package.json b/typescript/packages/http/paywall/package.json index 31bda28..79e1ac0 100644 --- a/typescript/packages/http/paywall/package.json +++ b/typescript/packages/http/paywall/package.json @@ -1,6 +1,6 @@ { "name": "@x402/paywall", - "version": "2.3.0", + "version": "2.9.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", @@ -10,7 +10,7 @@ "test": "vitest run", "test:watch": "vitest", "build": "tsup", - "build:paywall": "tsx src/evm/build.ts && tsx src/svm/build.ts", + "build:paywall": "tsx src/evm/build.ts", "watch": "tsc --watch", "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", @@ -24,8 +24,8 @@ "http-402" ], "license": "Apache-2.0", - "author": "Coinbase Inc.", - "repository": "https://github.com/coinbase/x402", + "author": "x402 Foundation", + "repository": "https://github.com/x402-foundation/x402", "description": "x402 Payment Protocol Paywall UI", "devDependencies": { "@craftamap/esbuild-plugin-html": "^0.9.0", @@ -36,7 +36,6 @@ "@typescript-eslint/eslint-plugin": "^8.29.1", "@typescript-eslint/parser": "^8.29.1", "@x402/evm": "workspace:~", - "@x402/svm": "workspace:~", "buffer": "^6.0.3", "esbuild": "^0.25.4", "eslint": "^9.24.0", @@ -54,13 +53,6 @@ "vitest": "^3.0.5" }, "dependencies": { - "@scure/base": "^1.2.6", - "@solana-program/compute-budget": "^0.8.0", - "@solana-program/token": "^0.5.1", - "@solana-program/token-2022": "^0.4.2", - "@solana/kit": "^2.1.1", - "@solana/transaction-confirmation": "^2.1.1", - "@solana/wallet-standard-features": "^1.3.0", "@tanstack/react-query": "^5.90.7", "@wagmi/connectors": "^5.8.1", "@wagmi/core": "^2.17.1", @@ -96,16 +88,6 @@ "types": "./dist/cjs/evm/index.d.cts", "default": "./dist/cjs/evm/index.cjs" } - }, - "./svm": { - "import": { - "types": "./dist/esm/svm/index.d.ts", - "default": "./dist/esm/svm/index.js" - }, - "require": { - "types": "./dist/cjs/svm/index.d.cts", - "default": "./dist/cjs/svm/index.cjs" - } } }, "files": [ diff --git a/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx b/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx index d3be221..b28c130 100644 --- a/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx +++ b/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx @@ -3,8 +3,9 @@ import { createPublicClient, formatUnits, http, publicActions, type Chain } from import * as allChains from "viem/chains"; import { useAccount, useSwitchChain, useWalletClient, useConnect, useDisconnect } from "wagmi"; -import { registerExactEvmScheme } from "@x402/evm/exact/client"; +import { ExactEvmScheme } from "@x402/evm/exact/client"; import { x402Client } from "@x402/core/client"; +import { encodePaymentSignatureHeader } from "@x402/core/http"; import type { PaymentRequired } from "@x402/core/types"; import { getUSDCBalance } from "./utils"; @@ -146,16 +147,15 @@ export function EvmPaywall({ paymentRequired, onSuccessfulResponse }: EvmPaywall setStatus("Creating payment signature..."); - // Create client and register EVM schemes (handles v1 and v2) const signer = wagmiToClientSigner(walletClient); const client = new x402Client(); - registerExactEvmScheme(client, { signer }); + client.register("eip155:*", new ExactEvmScheme(signer)); // Create payment payload - client automatically handles version const paymentPayload = await client.createPaymentPayload(paymentRequired); // Encode as base64 JSON for v2 header - const paymentHeader = btoa(JSON.stringify(paymentPayload)); + const paymentHeader = encodePaymentSignatureHeader(paymentPayload); setStatus("Requesting content with payment..."); const response = await fetch(x402.currentUrl, { diff --git a/typescript/packages/http/paywall/src/evm/gen/template.ts b/typescript/packages/http/paywall/src/evm/gen/template.ts index 8d9a00e..d31f495 100644 --- a/typescript/packages/http/paywall/src/evm/gen/template.ts +++ b/typescript/packages/http/paywall/src/evm/gen/template.ts @@ -3,4 +3,4 @@ * The pre-built EVM paywall template with inlined CSS and JS */ export const EVM_PAYWALL_TEMPLATE = - '\n \n \n Payment Required\n \n
\n \n \n '; + '\n \n \n Payment Required\n \n
\n \n \n '; diff --git a/typescript/packages/http/paywall/src/index.ts b/typescript/packages/http/paywall/src/index.ts index 29443fe..447cd30 100644 --- a/typescript/packages/http/paywall/src/index.ts +++ b/typescript/packages/http/paywall/src/index.ts @@ -14,4 +14,3 @@ export type { // Re-export network handlers for convenience export { evmPaywall } from "./evm"; -export { svmPaywall } from "./svm"; diff --git a/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx b/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx index 309e65c..74fa2f4 100644 --- a/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx +++ b/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx @@ -2,8 +2,9 @@ import { useCallback, useEffect, useRef, useState } from "react"; import type { WalletAccount } from "@wallet-standard/base"; import type { WalletWithSolanaFeatures } from "@solana/wallet-standard-features"; -import { registerExactSvmScheme } from "@x402/svm/exact/client"; +import { ExactSvmScheme } from "@x402/svm/exact/client"; import { x402Client } from "@x402/core/client"; +import { encodePaymentSignatureHeader } from "@x402/core/http"; import type { PaymentRequired } from "@x402/core/types"; import { Spinner } from "./Spinner"; @@ -181,11 +182,11 @@ export function SolanaPaywall({ paymentRequired, onSuccessfulResponse }: SolanaP setStatus("Creating payment signature..."); const client = new x402Client(); - registerExactSvmScheme(client, { signer: walletSigner }); + client.register("solana:*", new ExactSvmScheme(walletSigner)); const paymentPayload = await client.createPaymentPayload(paymentRequired); - const paymentHeader = btoa(JSON.stringify(paymentPayload)); + const paymentHeader = encodePaymentSignatureHeader(paymentPayload); setStatus("Requesting content with payment..."); const response = await fetch(x402.currentUrl, { diff --git a/typescript/packages/http/paywall/src/svm/gen/template.ts b/typescript/packages/http/paywall/src/svm/gen/template.ts index 0ba985a..9544316 100644 --- a/typescript/packages/http/paywall/src/svm/gen/template.ts +++ b/typescript/packages/http/paywall/src/svm/gen/template.ts @@ -3,4 +3,4 @@ * The pre-built SVM paywall template with inlined CSS and JS */ export const SVM_PAYWALL_TEMPLATE = - '\n \n \n Payment Required\n \n
\n \n \n '; + '\n \n \n Payment Required\n \n
\n \n \n '; diff --git a/typescript/packages/http/paywall/src/types.ts b/typescript/packages/http/paywall/src/types.ts index e1d5fe2..1ac791a 100644 --- a/typescript/packages/http/paywall/src/types.ts +++ b/typescript/packages/http/paywall/src/types.ts @@ -35,8 +35,8 @@ export interface PaymentRequired { error?: string; resource?: { url: string; - description: string; - mimeType: string; + description?: string; + mimeType?: string; }; accepts: PaymentRequirements[]; extensions?: Record; diff --git a/typescript/packages/http/paywall/tsup.config.ts b/typescript/packages/http/paywall/tsup.config.ts index cc8ba6a..f0f4b40 100644 --- a/typescript/packages/http/paywall/tsup.config.ts +++ b/typescript/packages/http/paywall/tsup.config.ts @@ -4,7 +4,6 @@ const baseConfig = { entry: { index: "src/index.ts", "evm/index": "src/evm/index.ts", - "svm/index": "src/svm/index.ts", }, dts: { resolve: true, diff --git a/typescript/packages/mcp/CHANGELOG.md b/typescript/packages/mcp/CHANGELOG.md deleted file mode 100644 index 2abf632..0000000 --- a/typescript/packages/mcp/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -# @x402/mcp Changelog - -## 2.3.0-alpha - -- Initial alpha prerelease of @x402/mcp package for Model Context Protocol integration with x402 payment protocol. diff --git a/typescript/packages/mcp/README.md b/typescript/packages/mcp/README.md deleted file mode 100644 index 0f7eb4e..0000000 --- a/typescript/packages/mcp/README.md +++ /dev/null @@ -1,351 +0,0 @@ -# @x402/mcp - -MCP (Model Context Protocol) integration for the x402 payment protocol. This package enables paid tool calls in MCP servers and automatic payment handling in MCP clients. - -## Installation - -```bash -npm install @x402/mcp @x402/core @modelcontextprotocol/sdk -``` - -## Quick Start (Recommended) - -### Server - Using Payment Wrapper - -```typescript -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { createPaymentWrapper, x402ResourceServer } from "@x402/mcp"; -import { HTTPFacilitatorClient } from "@x402/core/server"; -import { ExactEvmScheme } from "@x402/evm/exact/server"; -import { z } from "zod"; - -// Create standard MCP server -const mcpServer = new McpServer({ name: "premium-api", version: "1.0.0" }); - -// Set up x402 for payment handling -const facilitatorClient = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator" }); -const resourceServer = new x402ResourceServer(facilitatorClient); -resourceServer.register("eip155:84532", new ExactEvmScheme()); -await resourceServer.initialize(); - -// Build payment requirements -const accepts = await resourceServer.buildPaymentRequirements({ - scheme: "exact", - network: "eip155:84532", - payTo: "0x...", // Your wallet address - price: "$0.10", -}); - -// Create payment wrapper with accepts array -const paid = createPaymentWrapper(resourceServer, { - accepts, -}); - -// Register paid tools - wrap handler -mcpServer.tool( - "financial_analysis", - "Advanced AI-powered financial analysis. Costs $0.10.", - { ticker: z.string() }, - paid(async (args) => { - const analysis = await performAnalysis(args.ticker); - return { content: [{ type: "text", text: analysis }] }; - }) -); - -// Register free tools - no wrapper needed -mcpServer.tool("ping", "Health check", {}, async () => ({ - content: [{ type: "text", text: "pong" }], -})); - -// Connect to transport -await mcpServer.connect(transport); -``` - -### Client - Using Factory Function - -```typescript -import { createX402MCPClient } from "@x402/mcp"; -import { ExactEvmScheme } from "@x402/evm/exact/client"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; - -// Create client with factory (simplest approach) -const client = createX402MCPClient({ - name: "my-agent", - version: "1.0.0", - schemes: [{ network: "eip155:84532", client: new ExactEvmScheme(walletAccount) }], - autoPayment: true, - onPaymentRequested: async ({ paymentRequired }) => { - console.log(`Tool requires payment: ${paymentRequired.accepts[0].amount}`); - return true; // Return false to deny payment - }, -}); - -// Connect and use -const transport = new SSEClientTransport(new URL("http://localhost:4022/sse")); -await client.connect(transport); - -const result = await client.callTool("financial_analysis", { ticker: "AAPL" }); -console.log(result.content); - -if (result.paymentMade) { - console.log("Payment settled:", result.paymentResponse?.transaction); -} -``` - -## Advanced Features - -### Production Hooks - -Add hooks for logging, rate limiting, receipts, and more: - -```typescript -// Build payment requirements -const accepts = await resourceServer.buildPaymentRequirements({ - scheme: "exact", - network: "eip155:84532", - payTo: "0x...", - price: "$0.10", -}); - -const paid = createPaymentWrapper(resourceServer, { - accepts, - hooks: { - // Called after payment verification, before tool execution - // Return false to abort execution - onBeforeExecution: async ({ toolName, paymentPayload, paymentRequirements }) => { - console.log(`Executing ${toolName} for ${paymentPayload.payer}`); - - // Rate limiting example - if (await isRateLimited(paymentPayload.payer)) { - console.log("Rate limit exceeded"); - return false; // Abort execution, don't charge - } - - return true; // Continue - }, - - // Called after tool execution, before settlement - onAfterExecution: async ({ toolName, result, paymentPayload }) => { - // Log metrics - await metrics.record(toolName, result.isError); - }, - - // Called after successful settlement - onAfterSettlement: async ({ toolName, settlement, paymentPayload }) => { - // Send receipt to user - await sendReceipt(paymentPayload.payer, { - tool: toolName, - transaction: settlement.transaction, - network: settlement.network, - }); - }, - }, -}); - -// All tools using this wrapper inherit the hooks -mcpServer.tool("search", "Premium search", { query: z.string() }, - paid(async (args) => ({ content: [...] })) -); -``` - -### Multiple Wrappers with Different Prices - -Create separate wrappers for different payment tiers: - -```typescript -// Build requirements for different price points -const basicAccepts = await resourceServer.buildPaymentRequirements({ - scheme: "exact", - network: "eip155:84532", - payTo: "0x...", - price: "$0.05", -}); - -const premiumAccepts = await resourceServer.buildPaymentRequirements({ - scheme: "exact", - network: "eip155:84532", - payTo: "0x...", - price: "$0.50", -}); - -// Create wrappers with different prices -const paidBasic = createPaymentWrapper(resourceServer, { accepts: basicAccepts }); -const paidPremium = createPaymentWrapper(resourceServer, { accepts: premiumAccepts }); - -// Register tools with appropriate pricing -mcpServer.tool("basic_search", "...", {}, paidBasic(async (args) => ({ content: [...] }))); -mcpServer.tool("premium_search", "...", {}, paidPremium(async (args) => ({ content: [...] }))); -``` - -### Multiple Payment Options - -Support multiple payment methods by including multiple requirements: - -```typescript -// Build requirements for different payment schemes -const exactPayment = await resourceServer.buildPaymentRequirements({ - scheme: "exact", - network: "eip155:84532", - payTo: "0x...", - price: "$0.10", -}); - -const subscriptionPayment = await resourceServer.buildPaymentRequirements({ - scheme: "subscription", - network: "eip155:1", - payTo: "0x...", - price: "$50", // Monthly subscription -}); - -// Client can choose either payment method -const paid = createPaymentWrapper(resourceServer, { - accepts: [exactPayment[0], subscriptionPayment[0]], -}); -``` - -### Client - Wrapper Functions - -```typescript -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { wrapMCPClientWithPayment, wrapMCPClientWithPaymentFromConfig } from "@x402/mcp"; -import { x402Client } from "@x402/core/client"; -import { ExactEvmScheme } from "@x402/evm/exact/client"; - -// Option 1: Wrap existing client with existing payment client -const mcpClient = new Client({ name: "my-agent", version: "1.0.0" }); -const paymentClient = new x402Client() - .register("eip155:84532", new ExactEvmScheme(walletAccount)); - -const x402Mcp = wrapMCPClientWithPayment(mcpClient, paymentClient, { - autoPayment: true, -}); - -// Option 2: Wrap existing client with config -const x402Mcp2 = wrapMCPClientWithPaymentFromConfig(mcpClient, { - schemes: [{ network: "eip155:84532", client: new ExactEvmScheme(walletAccount) }], -}); -``` - -## Payment Flow - -1. **Client calls tool** → No payment attached -2. **Server returns 402** → PaymentRequired in structured result (see SDK Limitation below) -3. **Client creates payment** → Using x402Client -4. **Client retries with payment** → PaymentPayload in `_meta["x402/payment"]` -5. **Server verifies & executes** → Tool runs if payment valid -6. **Server settles payment** → Transaction submitted -7. **Server returns result** → SettleResponse in `_meta["x402/payment-response"]` - -## MCP SDK Limitation - -The x402 MCP transport spec defines payment errors using JSON-RPC's native error format: -```json -{ "error": { "code": 402, "data": { /* PaymentRequired */ } } } -``` - -However, the MCP SDK converts `McpError` exceptions to tool results with `isError: true`, losing the `error.data` field. To work around this, we embed the error structure in the result content: - -```json -{ - "content": [{ "type": "text", "text": "{\"x402/error\": {\"code\": 402, \"data\": {...}}}" }], - "isError": true -} -``` - -The client parses this structure to extract PaymentRequired data. This is a pragmatic workaround that maintains compatibility while we track upstream SDK improvements. - -## Configuration Options - -### x402MCPClientOptions - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `autoPayment` | `boolean` | `true` | Automatically retry with payment on 402 | -| `onPaymentRequested` | `function` | `() => true` | Hook for human-in-the-loop approval when payment is requested | - -### X402MCPServerConfig (Factory) - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `name` | `string` | Required | MCP server name | -| `version` | `string` | Required | MCP server version | -| `facilitator` | `string \| FacilitatorClient` | Default facilitator | Facilitator for payment processing | -| `schemes` | `SchemeRegistration[]` | `[]` | Payment scheme registrations | -| `syncFacilitatorOnStart` | `boolean` | `true` | Initialize facilitator immediately | - -### MCPToolPaymentConfig - -| Option | Type | Required | Description | -|--------|------|----------|-------------| -| `scheme` | `string` | Yes | Payment scheme (e.g., "exact") | -| `network` | `Network` | Yes | CAIP-2 network ID (e.g., "eip155:84532") | -| `price` | `Price` | Yes | Price (e.g., "$0.10" or "1000000") | -| `payTo` | `string` | Yes | Recipient wallet address | -| `maxTimeoutSeconds` | `number` | No | Payment timeout (default: 60) | -| `extra` | `object` | No | Scheme-specific parameters (e.g., EIP-712 domain) | -| `resource` | `object` | No | Resource metadata | - -### PaymentWrapperConfig (for createPaymentWrapper) - -| Option | Type | Required | Description | -|--------|------|----------|-------------| -| `scheme` | `string` | Yes | Payment scheme (e.g., "exact") | -| `network` | `Network` | Yes | CAIP-2 network ID (e.g., "eip155:84532") | -| `payTo` | `string` | Yes | Recipient wallet address | -| `price` | `Price` | No | Price - omit to specify per-tool | -| `maxTimeoutSeconds` | `number` | No | Payment timeout (default: 60) | -| `extra` | `object` | No | Scheme-specific parameters | -| `resource` | `object` | No | Resource metadata | - -## Hooks - -### Client Hooks - -```typescript -const client = createX402MCPClient({...}); - -// Called when a 402 is received (before payment) -// Return { payment } to use custom payment, { abort: true } to stop -client.onPaymentRequired(async ({ toolName, paymentRequired }) => { - const cached = await cache.get(toolName); - if (cached) return { payment: cached }; -}); - -// Called before payment is created -client.onBeforePayment(async ({ paymentRequired }) => { - await logPaymentAttempt(paymentRequired); -}); - -// Called after payment is submitted -client.onAfterPayment(async ({ paymentPayload, settleResponse }) => { - await saveReceipt(settleResponse.transaction); -}); -``` - -### Server Hooks - -```typescript -const server = createX402MCPServer({...}); - -// Called after verification, before tool execution -// Return false to abort and return 402 -server.onBeforeExecution(async ({ toolName, paymentPayload }) => { - if (isBlocked(paymentPayload.signer)) { - return false; // Aborts execution - } -}); - -// Called after tool execution, before settlement -server.onAfterExecution(async ({ toolName, result }) => { - metrics.recordExecution(toolName, result.isError); -}); - -// Called after successful settlement -server.onAfterSettlement(async ({ toolName, settlement }) => { - await logTransaction(toolName, settlement.transaction); -}); -``` - -## License - -Apache-2.0 diff --git a/typescript/packages/mcp/src/client/index.ts b/typescript/packages/mcp/src/client/index.ts deleted file mode 100644 index 88dd1f9..0000000 --- a/typescript/packages/mcp/src/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./x402MCPClient"; diff --git a/typescript/packages/mcp/src/client/x402MCPClient.ts b/typescript/packages/mcp/src/client/x402MCPClient.ts deleted file mode 100644 index 2b0a8de..0000000 --- a/typescript/packages/mcp/src/client/x402MCPClient.ts +++ /dev/null @@ -1,908 +0,0 @@ -import type { - PaymentPayload, - PaymentRequired, - SettleResponse, - Network, - SchemeNetworkClient, -} from "@x402/core/types"; -import { isPaymentRequired } from "@x402/core/schemas"; -import { x402Client } from "@x402/core/client"; -import type { x402ClientConfig } from "@x402/core/client"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; - -import type { - MCPResultWithMeta, - PaymentRequestedContext, - x402MCPClientOptions, - PaymentRequiredHook, - PaymentRequiredContext, -} from "../types"; -import { MCP_PAYMENT_REQUIRED_CODE, MCP_PAYMENT_META_KEY } from "../types"; -import { extractPaymentResponseFromMeta } from "../utils"; - -// ============================================================================ -// MCP SDK Result Types -// ============================================================================ - -/** - * MCP content item - using a flexible type that matches the MCP SDK's content format. - * The MCP SDK returns content items with a `type` discriminator and type-specific fields. - * We use this type to preserve the original response structure from the SDK. - */ -export type MCPContentItem = { - [key: string]: unknown; - type: string; -}; - -/** - * Result returned by MCP SDK callTool method. - * This mirrors the SDK's CallToolResult type to ensure compatibility. - */ -interface MCPCallToolResult { - content: MCPContentItem[]; - isError?: boolean; - _meta?: Record; - structuredContent?: Record; -} - -// ============================================================================ -// Type Guards -// ============================================================================ - -/** - * Type guard for MCP text content - * - * @param content - The content item to check - * @returns True if the content is a text content item with a string text field - */ -function isMCPTextContent(content: MCPContentItem): content is MCPContentItem & { text: string } { - return content.type === "text" && typeof content.text === "string"; -} - -/** - * Type guard for MCPCallToolResult - * - * @param result - The result to check - * @returns True if the result is a valid MCP call tool result - */ -function isMCPCallToolResult(result: unknown): result is MCPCallToolResult { - if (typeof result !== "object" || result === null) { - return false; - } - - const obj = result as Record; - return Array.isArray(obj.content); -} - -// ============================================================================ -// Hook Types -// ============================================================================ - -/** - * Hook called before payment is created - */ -export type BeforePaymentHook = (context: PaymentRequestedContext) => Promise | void; - -/** - * Hook called after payment is submitted - */ -export type AfterPaymentHook = (context: { - toolName: string; - paymentPayload: PaymentPayload; - result: MCPResultWithMeta; - settleResponse: SettleResponse | null; -}) => Promise | void; - -// ============================================================================ -// Public Types -// ============================================================================ - -/** - * Result of a tool call with payment metadata. - * Content is forwarded directly from the MCP SDK to preserve the original response structure. - */ -export interface x402MCPToolCallResult { - /** The tool result content, forwarded directly from MCP SDK */ - content: MCPContentItem[]; - /** Whether the tool returned an error */ - isError?: boolean; - /** Payment settlement response if payment was made */ - paymentResponse?: SettleResponse; - /** Whether payment was required and submitted */ - paymentMade: boolean; -} - -/** - * x402-enabled MCP client that handles payment for tool calls. - * - * Wraps an MCP client to automatically detect 402 (payment required) errors - * from tool calls, create payment payloads, and retry with payment attached. - * - * PROTOCOL COMPLIANCE: - * This wrapper is a COMPLETE, TRANSPARENT passthrough exposing all 19 public methods - * from the MCP SDK Client class. It's suitable for any MCP use case including: - * - Chatbots and conversational agents - * - IDE integrations (like Cursor, VSCode) - * - Autonomous agents - * - Custom MCP applications - * - * Only callTool() is enhanced with payment handling. All other methods are direct - * passthroughs ensuring full MCP protocol compatibility. - * - * STABILITY: - * Depends on formal MCP specification (JSON-RPC 2.0 based) with semantic versioning. - * Proven stable across SDK versions: 1.9.0 → 1.12.1 → 1.15.1 - * - * @example - * ```typescript - * import { Client } from "@modelcontextprotocol/sdk/client/index.js"; - * import { x402MCPClient } from "@x402/mcp"; - * import { x402Client } from "@x402/core/client"; - * import { ExactEvmScheme } from "@x402/evm/exact/client"; - * - * const paymentClient = new x402Client() - * .register("eip155:84532", new ExactEvmScheme(account)); - * - * const mcpClient = new Client({ name: "my-agent", version: "1.0.0" }, {...}); - * const x402Mcp = new x402MCPClient(mcpClient, paymentClient, { - * autoPayment: true, - * onPaymentRequested: async ({ paymentRequired }) => { - * return confirm(`Pay ${paymentRequired.accepts[0].amount}?`); - * }, - * }); - * - * await x402Mcp.connect(transport); - * - * // Full MCP protocol access - all 19 methods available - * const tools = await x402Mcp.listTools(); - * const resource = await x402Mcp.readResource({ uri: "file://..." }); - * const prompt = await x402Mcp.getPrompt({ name: "code-review" }); - * const result = await x402Mcp.callTool("financial_analysis", { ticker: "AAPL" }); - * ``` - */ -export class x402MCPClient { - private readonly mcpClient: Client; - private readonly _paymentClient: x402Client; - private readonly options: Required; - private readonly paymentRequiredHooks: PaymentRequiredHook[] = []; - private readonly beforePaymentHooks: BeforePaymentHook[] = []; - private readonly afterPaymentHooks: AfterPaymentHook[] = []; - - /** - * Creates a new x402MCPClient instance. - * - * @param mcpClient - The underlying MCP client instance - * @param paymentClient - The x402 client for creating payment payloads - * @param options - Configuration options - */ - constructor( - mcpClient: Client, - paymentClient: x402Client, - options: x402MCPClientOptions = {}, - ) { - this.mcpClient = mcpClient; - this._paymentClient = paymentClient; - this.options = { - autoPayment: options.autoPayment ?? true, - onPaymentRequested: options.onPaymentRequested ?? (() => true), - }; - } - - /** - * Get the underlying MCP client instance. - * - * @returns The MCP client instance - */ - get client(): Client { - return this.mcpClient; - } - - /** - * Get the underlying x402 payment client instance. - * - * @returns The x402 client instance - */ - get paymentClient(): x402Client { - return this._paymentClient; - } - - /** - * Connect to an MCP server transport. - * Passthrough to the underlying MCP client. - * - * @param transport - The transport to connect to - * @returns Promise that resolves when connected - */ - async connect(transport: Parameters[0]): Promise { - await this.mcpClient.connect(transport); - } - - /** - * Close the MCP connection. - * Passthrough to the underlying MCP client. - * - * @returns Promise that resolves when closed - */ - async close(): Promise { - await this.mcpClient.close(); - } - - /** - * List available tools from the server. - * Passthrough to the underlying MCP client. - * - * @returns Promise resolving to the list of tools - */ - async listTools(): ReturnType { - return this.mcpClient.listTools(); - } - - /** - * List available resources from the server. - * Passthrough to the underlying MCP client. - * - * @returns Promise resolving to the list of resources - */ - async listResources(): ReturnType { - return this.mcpClient.listResources(); - } - - /** - * List available prompts from the server. - * Passthrough to the underlying MCP client. - * - * @returns Promise resolving to the list of prompts - */ - async listPrompts(): ReturnType { - return this.mcpClient.listPrompts(); - } - - /** - * Get a specific prompt from the server. - * Passthrough to the underlying MCP client. - * - * @param args - Arguments for getPrompt method - * @returns Promise resolving to the prompt - */ - async getPrompt(...args: Parameters): ReturnType { - return this.mcpClient.getPrompt(...args); - } - - /** - * Read a resource from the server. - * Passthrough to the underlying MCP client. - * - * @param args - Arguments for readResource method - * @returns Promise resolving to the resource content - */ - async readResource(...args: Parameters): ReturnType { - return this.mcpClient.readResource(...args); - } - - /** - * List resource templates from the server. - * Passthrough to the underlying MCP client. - * - * @param args - Arguments for listResourceTemplates method - * @returns Promise resolving to the list of resource templates - */ - async listResourceTemplates(...args: Parameters): ReturnType { - return this.mcpClient.listResourceTemplates(...args); - } - - /** - * Subscribe to resource updates. - * Passthrough to the underlying MCP client. - * - * @param args - Arguments for subscribeResource method - * @returns Promise resolving when subscribed - */ - async subscribeResource(...args: Parameters): ReturnType { - return this.mcpClient.subscribeResource(...args); - } - - /** - * Unsubscribe from resource updates. - * Passthrough to the underlying MCP client. - * - * @param args - Arguments for unsubscribeResource method - * @returns Promise resolving when unsubscribed - */ - async unsubscribeResource(...args: Parameters): ReturnType { - return this.mcpClient.unsubscribeResource(...args); - } - - /** - * Ping the server. - * Passthrough to the underlying MCP client. - * - * @param args - Arguments for ping method - * @returns Promise resolving to ping response - */ - async ping(...args: Parameters): ReturnType { - return this.mcpClient.ping(...args); - } - - /** - * Request completion suggestions. - * Passthrough to the underlying MCP client. - * - * @param args - Arguments for complete method - * @returns Promise resolving to completion suggestions - */ - async complete(...args: Parameters): ReturnType { - return this.mcpClient.complete(...args); - } - - /** - * Set the logging level on the server. - * Passthrough to the underlying MCP client. - * - * @param args - Arguments for setLoggingLevel method - * @returns Promise resolving when level is set - */ - async setLoggingLevel(...args: Parameters): ReturnType { - return this.mcpClient.setLoggingLevel(...args); - } - - /** - * Get server capabilities after initialization. - * Passthrough to the underlying MCP client. - * - * @returns Server capabilities or undefined if not initialized - */ - getServerCapabilities(): ReturnType { - return this.mcpClient.getServerCapabilities(); - } - - /** - * Get server version information after initialization. - * Passthrough to the underlying MCP client. - * - * @returns Server version info or undefined if not initialized - */ - getServerVersion(): ReturnType { - return this.mcpClient.getServerVersion(); - } - - /** - * Get server instructions after initialization. - * Passthrough to the underlying MCP client. - * - * @returns Server instructions or undefined if not initialized - */ - getInstructions(): ReturnType { - return this.mcpClient.getInstructions(); - } - - /** - * Send notification that roots list has changed. - * Passthrough to the underlying MCP client. - * - * @returns Promise resolving when notification is sent - */ - async sendRootsListChanged(): ReturnType { - return this.mcpClient.sendRootsListChanged(); - } - - /** - * Register a hook to run when a 402 payment required is received. - * Hooks run in order; first to return a result wins. - * - * This can be used to: - * - Provide pre-existing payment payloads (implementation-specific, not part of x402 spec) - * - Abort the payment flow for certain tools - * - Log or track payment required events - * - * Note: Payment caching is an implementation pattern and not defined in the x402 MCP - * transport specification. Implementations that cache payments should ensure cached - * payloads are still valid (not expired, correct nonce, etc.). - * - * @param hook - Hook function - * @returns This instance for chaining - * - * @example - * ```typescript - * // Example: Custom payment handling (implementation-specific) - * client.onPaymentRequired(async ({ toolName, paymentRequired }) => { - * // Custom logic to provide a payment or abort - * if (shouldAbort(toolName)) { - * return { abort: true }; - * } - * // Return undefined to proceed with normal payment flow - * }); - * ``` - */ - onPaymentRequired(hook: PaymentRequiredHook): this { - this.paymentRequiredHooks.push(hook); - return this; - } - - /** - * Register a hook to run before payment is created. - * - * @param hook - Hook function - * @returns This instance for chaining - */ - onBeforePayment(hook: BeforePaymentHook): this { - this.beforePaymentHooks.push(hook); - return this; - } - - /** - * Register a hook to run after payment is submitted. - * - * @param hook - Hook function - * @returns This instance for chaining - */ - onAfterPayment(hook: AfterPaymentHook): this { - this.afterPaymentHooks.push(hook); - return this; - } - - /** - * Calls a tool, automatically handling 402 payment required errors. - * - * If the tool returns a 402 error and autoPayment is enabled, this method - * will automatically create a payment payload and retry the tool call. - * - * @param name - The name of the tool to call - * @param args - Arguments to pass to the tool - * @param options - Optional MCP request options (timeout, signal, etc.) - * @param options.timeout - Request timeout in milliseconds (default: 60000) - * @param options.signal - AbortSignal for cancellation - * @param options.resetTimeoutOnProgress - If true, progress notifications reset the timeout - * @returns The tool result with payment metadata - * @throws Error if payment is required but autoPayment is disabled and no payment provided - * @throws Error if payment approval is denied - * @throws Error if payment creation fails - */ - async callTool( - name: string, - args: Record = {}, - options?: { timeout?: number; signal?: AbortSignal; resetTimeoutOnProgress?: boolean }, - ): Promise { - // First attempt without payment - const result = await this.mcpClient.callTool({ name, arguments: args }, undefined, options); - - // Validate result structure - if (!isMCPCallToolResult(result)) { - throw new Error("Invalid MCP tool result: missing content array"); - } - - // Check if this is a payment required response (isError with payment_required in content) - const paymentRequired = this.extractPaymentRequiredFromResult(result); - - if (!paymentRequired) { - // Not a payment required response, forward original MCP response as-is - return { - content: result.content, - isError: result.isError, - paymentMade: false, - }; - } - - // Payment required - run onPaymentRequired hooks first - const paymentRequiredContext: PaymentRequiredContext = { - toolName: name, - arguments: args, - paymentRequired, - }; - - // Run payment required hooks - first to return a result wins - for (const hook of this.paymentRequiredHooks) { - const hookResult = await hook(paymentRequiredContext); - if (hookResult) { - if (hookResult.abort) { - throw new Error("Payment aborted by hook"); - } - if (hookResult.payment) { - // Use the hook-provided payment - return this.callToolWithPayment(name, args, hookResult.payment, options); - } - } - } - - // No hook handled it, proceed with normal flow - if (!this.options.autoPayment) { - // Auto-payment disabled, throw with payment info - const err = new Error("Payment required") as Error & { - code: number; - paymentRequired: PaymentRequired; - }; - err.code = MCP_PAYMENT_REQUIRED_CODE; - err.paymentRequired = paymentRequired; - throw err; - } - - // Create payment requested context - const paymentRequestedContext: PaymentRequestedContext = { - toolName: name, - arguments: args, - paymentRequired, - }; - - // Check if payment is approved via onPaymentRequested hook - const approved = await this.options.onPaymentRequested(paymentRequestedContext); - if (!approved) { - throw new Error("Payment request denied"); - } - - // Run before payment hooks - for (const hook of this.beforePaymentHooks) { - await hook(paymentRequestedContext); - } - - // Create payment payload - const paymentPayload = await this._paymentClient.createPaymentPayload(paymentRequired); - - // Retry with payment - return this.callToolWithPayment(name, args, paymentPayload, options); - } - - /** - * Calls a tool with an explicit payment payload. - * - * Use this method when you want to provide payment upfront or when - * implementing custom payment handling. - * - * @param name - The name of the tool to call - * @param args - Arguments to pass to the tool - * @param paymentPayload - The payment payload to include - * @param options - Optional MCP request options (timeout, signal, etc.) - * @param options.timeout - Request timeout in milliseconds (default: 60000) - * @param options.signal - AbortSignal for cancellation - * @param options.resetTimeoutOnProgress - If true, progress notifications reset the timeout - * @returns The tool result with payment metadata - */ - async callToolWithPayment( - name: string, - args: Record, - paymentPayload: PaymentPayload, - options?: { timeout?: number; signal?: AbortSignal; resetTimeoutOnProgress?: boolean }, - ): Promise { - // Build the call parameters with payment metadata - // Note: The MCP SDK's callTool accepts _meta but the types don't always expose it - const callParams = { - name, - arguments: args, - _meta: { - [MCP_PAYMENT_META_KEY]: paymentPayload, - }, - }; - - // Call with payment in _meta - const result = await this.mcpClient.callTool(callParams, undefined, options); - - // Validate result structure - if (!isMCPCallToolResult(result)) { - throw new Error("Invalid MCP tool result: missing content array"); - } - - // Build result with meta for extraction (preserve _meta if present) - const resultWithMeta: MCPResultWithMeta = { - content: result.content, - isError: result.isError, - _meta: result._meta, - }; - - // Extract payment response from _meta - const paymentResponse = extractPaymentResponseFromMeta(resultWithMeta); - - // Run after payment hooks - for (const hook of this.afterPaymentHooks) { - await hook({ - toolName: name, - paymentPayload, - result: resultWithMeta, - settleResponse: paymentResponse, - }); - } - - // Forward original MCP response content as-is - return { - content: result.content, - isError: result.isError, - paymentResponse: paymentResponse ?? undefined, - paymentMade: true, - }; - } - - /** - * Probes a tool to discover its payment requirements. - * - * **WARNING: Side Effects** - This method actually calls the tool to trigger a 402 response. - * If the tool is free (no payment required), it will execute and return null. - * Use with caution on tools that have side effects or are expensive to run. - * - * Useful for displaying pricing information to users before calling paid tools. - * - * @param name - The name of the tool to probe - * @param args - Arguments that may affect pricing (for dynamic pricing scenarios) - * @returns The payment requirements if the tool requires payment, null if the tool is free - * - * @example - * ```typescript - * // Check if a tool requires payment before calling - * const requirements = await client.getToolPaymentRequirements("expensive_analysis"); - * - * if (requirements) { - * const price = requirements.accepts[0]; - * console.log(`This tool costs ${price.amount} on ${price.network}`); - * // Optionally show user and get confirmation before calling - * } else { - * console.log("This tool is free"); - * // Note: the tool has already executed! - * } - * ``` - */ - async getToolPaymentRequirements( - name: string, - args: Record = {}, - ): Promise { - // Note: This actually calls the tool to trigger 402 if paid. - // If the tool is free, it will execute as a side effect. - const result = await this.mcpClient.callTool({ name, arguments: args }); - - // Validate result structure - if (!isMCPCallToolResult(result)) { - return null; - } - - // Check if this is a payment required response - return this.extractPaymentRequiredFromResult(result); - } - - // ============================================================================ - // Private Methods - // ============================================================================ - - /** - * Extracts PaymentRequired from a tool result (structured 402 response). - * - * Per MCP transport spec, supports: - * 1. structuredContent with direct PaymentRequired object (optional, preferred) - * 2. content[0].text with JSON-encoded PaymentRequired object (required) - * - * @param result - The tool call result - * @returns PaymentRequired if this is a 402 response, null otherwise - */ - private extractPaymentRequiredFromResult(result: MCPCallToolResult): PaymentRequired | null { - // Only check if isError is true - if (!result.isError) { - return null; - } - - if (result.structuredContent) { - const extracted = this.extractPaymentRequiredFromObject(result.structuredContent); - if (extracted) { - return extracted; - } - } - - const content = result.content; - if (content.length === 0) { - return null; - } - - const firstItem = content[0]; - if (!isMCPTextContent(firstItem)) { - return null; - } - - try { - const parsed: unknown = JSON.parse(firstItem.text); - if (typeof parsed === "object" && parsed !== null) { - const extracted = this.extractPaymentRequiredFromObject( - parsed as Record, - ); - if (extracted) { - return extracted; - } - } - } catch { - // Not JSON, not our structured response - } - - return null; - } - - /** - * Extracts PaymentRequired from an object. - * Expects direct PaymentRequired format (per MCP transport spec). - * - * @param obj - The object to extract from - * @returns PaymentRequired if found, null otherwise - */ - private extractPaymentRequiredFromObject(obj: Record): PaymentRequired | null { - if (isPaymentRequired(obj)) { - return obj as PaymentRequired; - } - - return null; - } - -} - -/** - * Configuration for createx402MCPClient factory - */ -export interface x402MCPClientConfig { - /** MCP client name */ - name: string; - - /** MCP client version */ - version: string; - - /** - * Payment scheme registrations. - * Each registration maps a network to its scheme client implementation. - */ - schemes: Array<{ - network: Network; - client: SchemeNetworkClient; - x402Version?: number; - }>; - - /** - * Whether to automatically retry tool calls with payment on 402 errors. - * - * @default true - */ - autoPayment?: boolean; - - /** - * Hook called when a payment is requested. - * Return true to proceed with payment, false to abort. - */ - onPaymentRequested?: (context: PaymentRequestedContext) => Promise | boolean; - - /** - * Additional MCP client options - */ - mcpClientOptions?: Record; -} - -/** - * Wraps an existing MCP client with x402 payment handling. - * - * Use this when you already have an MCP client instance and want to add - * payment capabilities. For a simpler setup, use createx402MCPClient instead. - * - * @param mcpClient - The MCP client to wrap - * @param paymentClient - The x402 client for payment handling - * @param options - Configuration options - * @returns An x402MCPClient instance - * - * @example - * ```typescript - * import { Client } from "@modelcontextprotocol/sdk/client/index.js"; - * import { wrapMCPClientWithPayment } from "@x402/mcp"; - * import { x402Client } from "@x402/core/client"; - * import { ExactEvmScheme } from "@x402/evm/exact/client"; - * - * const mcpClient = new Client({ name: "my-agent", version: "1.0.0" }); - * const paymentClient = new x402Client() - * .register("eip155:84532", new ExactEvmScheme(account)); - * - * const x402Mcp = wrapMCPClientWithPayment(mcpClient, paymentClient, { - * autoPayment: true, - * }); - * - * await x402Mcp.connect(transport); - * const result = await x402Mcp.callTool("paid_tool", { arg: "value" }); - * ``` - */ -export function wrapMCPClientWithPayment( - mcpClient: Client, - paymentClient: x402Client, - options?: x402MCPClientOptions, -): x402MCPClient { - return new x402MCPClient(mcpClient, paymentClient, options); -} - -/** - * Wraps an existing MCP client with x402 payment handling using a config object. - * - * Similar to wrapMCPClientWithPayment but uses a configuration object for - * setting up the payment client, similar to the axios pattern. - * - * @param mcpClient - The MCP client to wrap - * @param config - Payment client configuration - * @param options - x402 MCP client options - * @returns An x402MCPClient instance - * - * @example - * ```typescript - * import { Client } from "@modelcontextprotocol/sdk/client/index.js"; - * import { wrapMCPClientWithPaymentFromConfig } from "@x402/mcp"; - * import { ExactEvmScheme } from "@x402/evm/exact/client"; - * - * const mcpClient = new Client({ name: "my-agent", version: "1.0.0" }); - * - * const x402Mcp = wrapMCPClientWithPaymentFromConfig(mcpClient, { - * schemes: [ - * { network: "eip155:84532", client: new ExactEvmScheme(account) }, - * ], - * }); - * - * await x402Mcp.connect(transport); - * ``` - */ -export function wrapMCPClientWithPaymentFromConfig( - mcpClient: Client, - config: x402ClientConfig, - options?: x402MCPClientOptions, -): x402MCPClient { - const paymentClient = x402Client.fromConfig(config); - return new x402MCPClient(mcpClient, paymentClient, options); -} - -/** - * Creates a fully configured x402 MCP client with sensible defaults. - * - * This factory function provides the simplest way to create an x402-enabled MCP client. - * It handles creation of both the underlying MCP Client and x402Client, making it - * easy to get started with paid tool calls. - * - * @param config - Client configuration options - * @returns A configured x402MCPClient instance - * - * @example - * ```typescript - * import { createx402MCPClient } from "@x402/mcp"; - * import { ExactEvmScheme } from "@x402/evm/exact/client"; - * import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; - * - * const client = createx402MCPClient({ - * name: "my-agent", - * version: "1.0.0", - * schemes: [ - * { network: "eip155:84532", client: new ExactEvmScheme(account) }, - * ], - * autoPayment: true, - * onPaymentRequested: async ({ paymentRequired }) => { - * console.log(`Payment required: ${paymentRequired.accepts[0].amount}`); - * return true; // Auto-approve - * }, - * }); - * - * // Connect to server - * const transport = new SSEClientTransport(new URL("http://localhost:4022/sse")); - * await client.connect(transport); - * - * // List available tools - * const { tools } = await client.listTools(); - * - * // Call a paid tool (payment handled automatically) - * const result = await client.callTool("get_weather", { city: "NYC" }); - * ``` - */ -export function createx402MCPClient(config: x402MCPClientConfig): x402MCPClient { - // Create MCP client - const mcpClient = new Client( - { - name: config.name, - version: config.version, - }, - config.mcpClientOptions, - ); - - // Create x402 payment client - const paymentClient = new x402Client(); - - // Register schemes - for (const scheme of config.schemes) { - if (scheme.x402Version === 1) { - paymentClient.registerV1(scheme.network, scheme.client); - } else { - paymentClient.register(scheme.network, scheme.client); - } - } - - // Create x402MCPClient with options - return new x402MCPClient(mcpClient, paymentClient, { - autoPayment: config.autoPayment, - onPaymentRequested: config.onPaymentRequested, - }); -} diff --git a/typescript/packages/mcp/src/index.ts b/typescript/packages/mcp/src/index.ts deleted file mode 100644 index ac9e19f..0000000 --- a/typescript/packages/mcp/src/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -// @x402/mcp - MCP (Model Context Protocol) integration for x402 payment protocol -// -// This package provides MCP-native payment handling for AI agents and MCP servers. -// It enables paid tool calls following the x402 protocol over MCP transport. - -// Client exports -export { - x402MCPClient, - createx402MCPClient, - wrapMCPClientWithPayment, - wrapMCPClientWithPaymentFromConfig, -} from "./client"; -export type { - BeforePaymentHook, - AfterPaymentHook, - x402MCPToolCallResult, - x402MCPClientConfig, - MCPContentItem, -} from "./client"; - -// Server exports -export { createPaymentWrapper } from "./server"; -export type { - PaymentWrapperConfig, - PaymentWrappedHandler, - WrappedToolResult, - ToolResult, - MCPToolCallback, -} from "./server"; - -// Type exports -export { - MCP_PAYMENT_REQUIRED_CODE, - MCP_PAYMENT_META_KEY, - MCP_PAYMENT_RESPONSE_META_KEY, - isPaymentRequiredError, -} from "./types"; -export type { - // Core MCP types - MCPToolContext, - MCPToolPaymentConfig, - MCPPaymentProcessResult, - MCPPaymentError, - x402MCPClientOptions, - PaymentRequestedContext, - MCPToolResultWithPayment, - MCPRequestParamsWithMeta, - MCPResultWithMeta, - MCPPaymentRequiredError, - DynamicPayTo, - DynamicPrice, - ToolContentItem, - // Server hook types - ServerHookContext, - BeforeExecutionHook, - AfterExecutionContext, - AfterExecutionHook, - SettlementContext, - AfterSettlementHook, - // Client hook types - PaymentRequiredContext, - PaymentRequiredHookResult, - PaymentRequiredHook, -} from "./types"; - -// Utility exports -export { - extractPaymentFromMeta, - attachPaymentToMeta, - extractPaymentResponseFromMeta, - attachPaymentResponseToMeta, - createPaymentRequiredError, - extractPaymentRequiredFromError, - createToolResourceUrl, -} from "./utils"; - -// ============================================================================ -// Convenience Re-exports from @x402/core -// ============================================================================ -// These re-exports provide common types and classes that MCP users frequently need, -// reducing the number of separate package imports required. - -export { x402Client } from "@x402/core/client"; -export type { x402ClientConfig, SelectPaymentRequirements, PaymentPolicy } from "@x402/core/client"; - -export { x402ResourceServer } from "@x402/core/server"; - -export type { - PaymentPayload, - PaymentRequired, - PaymentRequirements, - SettleResponse, - Network, - SchemeNetworkClient, - SchemeNetworkServer, -} from "@x402/core/types"; diff --git a/typescript/packages/mcp/src/server/index.ts b/typescript/packages/mcp/src/server/index.ts deleted file mode 100644 index feba1bd..0000000 --- a/typescript/packages/mcp/src/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./paymentWrapper"; diff --git a/typescript/packages/mcp/src/server/paymentWrapper.ts b/typescript/packages/mcp/src/server/paymentWrapper.ts deleted file mode 100644 index 57f0087..0000000 --- a/typescript/packages/mcp/src/server/paymentWrapper.ts +++ /dev/null @@ -1,367 +0,0 @@ -/** - * Payment wrapper for MCP tool handlers - * - * This module provides a functional API for adding x402 payment to MCP tool handlers. - * Use createPaymentWrapper to wrap tool handlers with payment verification and settlement. - */ - -import type { PaymentRequirements } from "@x402/core/types"; -import { x402ResourceServer } from "@x402/core/server"; - -import type { - MCPToolContext, - BeforeExecutionHook, - AfterExecutionHook, - AfterSettlementHook, - ServerHookContext, - AfterExecutionContext, - SettlementContext, -} from "../types"; -import { MCP_PAYMENT_RESPONSE_META_KEY } from "../types"; -import { createToolResourceUrl, extractPaymentFromMeta } from "../utils"; - -/** - * Configuration for payment wrapper. - */ -export interface PaymentWrapperConfig { - /** - * Payment requirements that must be satisfied to call the tool. - * Typically a single entry, but can support multiple payment options. - * - * Each requirement specifies: - * - scheme: Payment scheme identifier (e.g., "exact") - * - network: Blockchain network in CAIP-2 format (e.g., "eip155:84532") - * - amount: Payment amount in token's smallest unit - * - asset: Token contract address - * - payTo: Recipient wallet address - * - maxTimeoutSeconds: Payment timeout (optional) - * - extra: Scheme-specific data (optional) - */ - accepts: PaymentRequirements[]; - - /** Resource metadata for the tool */ - resource?: { - /** Custom URL for the resource (defaults to mcp://tool/{toolName}) */ - url?: string; - /** Human-readable description of the tool */ - description?: string; - /** MIME type of the tool response */ - mimeType?: string; - }; - - /** Hooks for payment lifecycle events */ - hooks?: { - /** Called after payment verification, before tool execution. Return false to abort. */ - onBeforeExecution?: BeforeExecutionHook; - /** Called after tool execution, before settlement */ - onAfterExecution?: AfterExecutionHook; - /** Called after successful settlement */ - onAfterSettlement?: AfterSettlementHook; - }; -} - -/** - * Result type for wrapped tool handlers. - * Matches the MCP SDK's expected tool result format with optional _meta. - */ -export interface WrappedToolResult { - [key: string]: unknown; - content: Array<{ type: "text"; text: string }>; - isError?: boolean; - _meta?: Record; - structuredContent?: Record; -} - -/** - * Tool result type without payment metadata - */ -export interface ToolResult { - [key: string]: unknown; - content: Array<{ type: "text"; text: string }>; - isError?: boolean; -} - -/** - * Handler function type for tools to be wrapped with payment. - */ -export type PaymentWrappedHandler> = ( - args: TArgs, - context: MCPToolContext, -) => Promise | ToolResult; - -/** - * MCP SDK compatible tool callback type. - * This type matches the signature expected by McpServer.tool() for tools with arguments. - * The extra parameter contains _meta and other request context from the MCP SDK. - */ -export type MCPToolCallback> = ( - args: TArgs, - extra: unknown, -) => WrappedToolResult | Promise; - -/** - * Creates a reusable payment wrapper for adding x402 payment to MCP tool handlers. - * - * This is the primary API for integrating x402 payments with MCP servers. - * Use this when you have an existing McpServer and want to add payment to specific tools. - * - * @param resourceServer - The x402 resource server for payment verification/settlement - * @param config - Payment configuration with accepts array - * @returns A function that wraps tool handlers with payment logic - * - * @example - * ```typescript - * // Build payment requirements using resource server - * const accepts = await resourceServer.buildPaymentRequirements({ - * scheme: "exact", - * network: "eip155:84532", - * payTo: "0xRecipient", - * price: "$0.10", - * }); - * - * // Create wrapper with payment requirements - * const paid = createPaymentWrapper(resourceServer, { - * accepts, - * hooks: { - * onBeforeExecution: async ({ paymentPayload }) => { - * if (await isRateLimited(paymentPayload.payer)) return false; - * }, - * onAfterSettlement: async ({ settlement }) => { - * await sendReceipt(settlement.transaction); - * }, - * }, - * }); - * - * // Use with McpServer.tool() - * mcpServer.tool("search", "Premium search ($0.10)", { query: z.string() }, - * paid(async (args) => ({ - * content: [{ type: "text", text: "Search results..." }] - * })) - * ); - * ``` - */ -export function createPaymentWrapper( - resourceServer: x402ResourceServer, - config: PaymentWrapperConfig, -): >( - handler: PaymentWrappedHandler, -) => MCPToolCallback { - // Validate accepts array - if (!config.accepts || config.accepts.length === 0) { - throw new Error("PaymentWrapperConfig.accepts must have at least one payment requirement"); - } - - // Return wrapper function that takes only the handler - return >( - handler: PaymentWrappedHandler, - ): MCPToolCallback => { - return async (args: TArgs, extra: unknown): Promise => { - // Extract _meta from extra if it's an object - const _meta = (extra as { _meta?: Record })?._meta; - // Derive toolName from resource URL if available, otherwise use placeholder - const toolName = config.resource?.url?.replace("mcp://tool/", "") || "paid_tool"; - - const context: MCPToolContext = { - toolName, - arguments: args, - meta: _meta, - }; - - // Extract payment from _meta if present - const paymentPayload = extractPaymentFromMeta({ - name: toolName, - arguments: args, - _meta, - }); - - // Use first payment requirement (typically only one) - const paymentRequirements = config.accepts[0]; - - // If no payment provided, return 402 error - if (!paymentPayload) { - return createPaymentRequiredResult( - resourceServer, - toolName, - config, - "Payment required to access this tool", - ); - } - - // Verify payment - const verifyResult = await resourceServer.verifyPayment(paymentPayload, paymentRequirements); - - if (!verifyResult.isValid) { - return createPaymentRequiredResult( - resourceServer, - toolName, - config, - verifyResult.invalidReason || "Payment verification failed", - ); - } - - // Build hook context - const hookContext: ServerHookContext = { - toolName, - arguments: args, - paymentRequirements, - paymentPayload, - }; - - // Run onBeforeExecution hook if present - if (config.hooks?.onBeforeExecution) { - const hookResult = await config.hooks.onBeforeExecution(hookContext); - if (hookResult === false) { - return createPaymentRequiredResult( - resourceServer, - toolName, - config, - "Execution blocked by hook", - ); - } - } - - // Execute the tool handler - const result = await handler(args, context); - - // Build after execution context - const afterExecContext: AfterExecutionContext = { - ...hookContext, - result, - }; - - // Run onAfterExecution hook if present - if (config.hooks?.onAfterExecution) { - await config.hooks.onAfterExecution(afterExecContext); - } - - // If the tool handler returned an error, don't proceed to settlement - if (result.isError) { - return result; - } - - // Settle the payment - try { - const settleResult = await resourceServer.settlePayment( - paymentPayload, - paymentRequirements, - ); - - // Run onAfterSettlement hook if present - if (config.hooks?.onAfterSettlement) { - const settlementContext: SettlementContext = { - ...hookContext, - settlement: settleResult, - }; - await config.hooks.onAfterSettlement(settlementContext); - } - - // Return result with payment response in _meta - return { - content: result.content, - isError: result.isError, - _meta: { - [MCP_PAYMENT_RESPONSE_META_KEY]: settleResult, - }, - }; - } catch (settleError) { - // Settlement failed after execution - return 402 error - return createSettlementFailedResult( - resourceServer, - toolName, - config, - settleError instanceof Error ? settleError.message : "Settlement failed", - ); - } - }; - }; -} - -/** - * Helper to create 402 payment required result from wrapper config. - * - * @param resourceServer - The x402 resource server for creating payment required response - * @param toolName - Name of the tool for resource URL - * @param config - Payment wrapper configuration - * @param errorMessage - Error message describing why payment is required - * @returns Promise resolving to structured 402 error result with payment requirements - */ -async function createPaymentRequiredResult( - resourceServer: x402ResourceServer, - toolName: string, - config: PaymentWrapperConfig, - errorMessage: string, -): Promise { - const resourceInfo = { - url: createToolResourceUrl(toolName, config.resource?.url), - description: config.resource?.description || `Tool: ${toolName}`, - mimeType: config.resource?.mimeType || "application/json", - }; - - const paymentRequired = await resourceServer.createPaymentRequiredResponse( - config.accepts, - resourceInfo, - errorMessage, - ); - - return { - structuredContent: paymentRequired as unknown as Record, - content: [ - { - type: "text" as const, - text: JSON.stringify(paymentRequired), - }, - ], - isError: true, - }; -} - -/** - * Helper to create 402 settlement failed result from wrapper config. - * - * @param resourceServer - The x402 resource server for creating error response - * @param toolName - Name of the tool for resource URL - * @param config - Payment wrapper configuration - * @param errorMessage - Error message describing the settlement failure - * @returns Promise resolving to structured 402 error result with settlement failure info - */ -async function createSettlementFailedResult( - resourceServer: x402ResourceServer, - toolName: string, - config: PaymentWrapperConfig, - errorMessage: string, -): Promise { - const resourceInfo = { - url: createToolResourceUrl(toolName, config.resource?.url), - description: config.resource?.description || `Tool: ${toolName}`, - mimeType: config.resource?.mimeType || "application/json", - }; - - const paymentRequired = await resourceServer.createPaymentRequiredResponse( - config.accepts, - resourceInfo, - `Payment settlement failed: ${errorMessage}`, - ); - - const settlementFailure = { - success: false, - errorReason: errorMessage, - transaction: "", - network: config.accepts[0].network, - }; - - const errorData = { - ...paymentRequired, - [MCP_PAYMENT_RESPONSE_META_KEY]: settlementFailure, - }; - - return { - structuredContent: errorData as unknown as Record, - content: [ - { - type: "text" as const, - text: JSON.stringify(errorData), - }, - ], - isError: true, - }; -} diff --git a/typescript/packages/mcp/src/types/index.ts b/typescript/packages/mcp/src/types/index.ts deleted file mode 100644 index 2a34c12..0000000 --- a/typescript/packages/mcp/src/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./mcp"; diff --git a/typescript/packages/mcp/src/types/mcp.ts b/typescript/packages/mcp/src/types/mcp.ts deleted file mode 100644 index 9b12dc2..0000000 --- a/typescript/packages/mcp/src/types/mcp.ts +++ /dev/null @@ -1,323 +0,0 @@ -import type { - Network, - PaymentPayload, - PaymentRequired, - PaymentRequirements, - Price, - SettleResponse, -} from "@x402/core/types"; -import { isObject } from "../utils/encoding"; - -/** - * MCP JSON-RPC error code for payment required (x402) - */ -export const MCP_PAYMENT_REQUIRED_CODE = 402; - -/** - * MCP _meta key for payment payload (client → server) - */ -export const MCP_PAYMENT_META_KEY = "x402/payment"; - -/** - * MCP _meta key for payment response (server → client) - */ -export const MCP_PAYMENT_RESPONSE_META_KEY = "x402/payment-response"; - -/** - * Dynamic function to resolve payTo address based on tool call context - */ -export type DynamicPayTo = (context: MCPToolContext) => string | Promise; - -/** - * Dynamic function to resolve price based on tool call context - */ -export type DynamicPrice = (context: MCPToolContext) => Price | Promise; - -/** - * Context provided to dynamic functions and hooks during tool execution - */ -export interface MCPToolContext { - /** The name of the tool being called */ - toolName: string; - /** The arguments passed to the tool */ - arguments: Record; - /** Optional metadata from the request */ - meta?: Record; -} - -/** - * Payment configuration for a paid MCP tool - */ -export interface MCPToolPaymentConfig { - /** Payment scheme identifier (e.g., "exact") */ - scheme: string; - - /** Blockchain network identifier in CAIP-2 format (e.g., "eip155:84532") */ - network: Network; - - /** Price for the tool call (e.g., "$0.10", "1000000") */ - price: Price | DynamicPrice; - - /** Recipient wallet address or dynamic resolver */ - payTo: string | DynamicPayTo; - - /** Maximum time allowed for payment completion in seconds */ - maxTimeoutSeconds?: number; - - /** Scheme-specific additional information */ - extra?: Record; - - /** Resource metadata for the tool */ - resource?: { - /** Custom URL for the resource (defaults to mcp://tool/{toolName}) */ - url?: string; - /** Human-readable description of the tool */ - description?: string; - /** MIME type of the tool response */ - mimeType?: string; - }; -} - -/** - * Result of processing an MCP tool request for payment - */ -export type MCPPaymentProcessResult = - | { type: "no-payment-required" } - | { - type: "payment-verified"; - paymentPayload: PaymentPayload; - paymentRequirements: PaymentRequirements; - } - | { - type: "payment-error"; - error: MCPPaymentError; - }; - -/** - * MCP payment error structure for JSON-RPC error responses - */ -export interface MCPPaymentError { - /** JSON-RPC error code (402 for payment required) */ - code: number; - /** Human-readable error message */ - message: string; - /** PaymentRequired data for 402 errors */ - data?: PaymentRequired; -} - -/** - * Context provided to onPaymentRequired hooks - */ -export interface PaymentRequiredContext { - /** The tool name that returned 402 */ - toolName: string; - /** The arguments that were passed to the tool */ - arguments: Record; - /** The payment requirements from the server */ - paymentRequired: PaymentRequired; -} - -/** - * Result from onPaymentRequired hook - */ -export interface PaymentRequiredHookResult { - /** Custom payment payload to use instead of auto-generated */ - payment?: PaymentPayload; - /** Skip payment and abort the call */ - abort?: boolean; -} - -/** - * Hook called when a 402 response is received, before payment processing. - * Return payment to use that instead of auto-generating, abort: true to stop. - * Return void/undefined to proceed with normal payment flow. - */ -export type PaymentRequiredHook = ( - context: PaymentRequiredContext, -) => Promise | PaymentRequiredHookResult | void; - -/** - * Options for x402MCPClient - */ -export interface x402MCPClientOptions { - /** - * Whether to automatically retry tool calls with payment on 402 errors. - * When true (default), the client will automatically create and submit - * payment when a 402 error is received. - * When false, the client will throw the 402 error for manual handling. - * - * @default true - */ - autoPayment?: boolean; - - /** - * Hook called when a payment is requested by the server (402 response). - * Return true to proceed with payment, false to abort. - * Only called when autoPayment is true. - * - * This can be used to implement human-in-the-loop approval. - */ - onPaymentRequested?: (context: PaymentRequestedContext) => Promise | boolean; -} - -/** - * Context provided to payment requested hook - */ -export interface PaymentRequestedContext { - /** The tool being called */ - toolName: string; - /** The arguments passed to the tool */ - arguments: Record; - /** The payment requirements from the server */ - paymentRequired: PaymentRequired; -} - -// ============================================================================ -// Server Hooks -// ============================================================================ - -/** - * Context provided to server-side hooks during tool execution - */ -export interface ServerHookContext { - /** The name of the tool being called */ - toolName: string; - /** The arguments passed to the tool */ - arguments: Record; - /** The resolved payment requirements */ - paymentRequirements: PaymentRequirements; - /** The payment payload from the client */ - paymentPayload: PaymentPayload; -} - -/** - * Hook called before tool execution (after payment verification) - * Return false to abort execution and return a 402 error - */ -export type BeforeExecutionHook = ( - context: ServerHookContext, -) => Promise | boolean | void; - -/** - * Context for after execution hook including the result - */ -export interface AfterExecutionContext extends ServerHookContext { - /** The tool execution result */ - result: { - content: Array<{ type: "text"; text: string }>; - isError?: boolean; - }; -} - -/** - * Hook called after tool execution (before settlement) - */ -export type AfterExecutionHook = (context: AfterExecutionContext) => Promise | void; - -/** - * Context for settlement hooks - */ -export interface SettlementContext extends ServerHookContext { - /** The settlement result */ - settlement: SettleResponse; -} - -/** - * Hook called after successful settlement - */ -export type AfterSettlementHook = (context: SettlementContext) => Promise | void; - -/** - * Tool content item type - */ -export interface ToolContentItem { - [key: string]: unknown; - type: string; - text?: string; -} - -/** - * Result of a tool call that includes payment response metadata - */ -export interface MCPToolResultWithPayment { - /** Standard MCP tool result content */ - content: ToolContentItem[]; - /** Whether the tool execution resulted in an error */ - isError?: boolean; - /** Payment response metadata (settlement info) */ - paymentResponse?: SettleResponse; -} - -/** - * MCP metadata with payment - */ -export interface MCPMetaWithPayment { - [key: string]: unknown; - [MCP_PAYMENT_META_KEY]?: PaymentPayload; -} - -/** - * MCP request params with optional _meta field for payment - */ -export interface MCPRequestParamsWithMeta { - /** Tool name */ - name: string; - /** Tool arguments */ - arguments?: Record; - /** Metadata including potential payment payload */ - _meta?: MCPMetaWithPayment; -} - -/** - * MCP metadata with payment response - */ -export interface MCPMetaWithPaymentResponse { - [key: string]: unknown; - [MCP_PAYMENT_RESPONSE_META_KEY]?: SettleResponse; -} - -/** - * MCP result with optional _meta field for payment response - */ -export interface MCPResultWithMeta { - /** Tool result content */ - content?: ToolContentItem[]; - /** Whether the result is an error */ - isError?: boolean; - /** Metadata including potential payment response */ - _meta?: MCPMetaWithPaymentResponse; -} - -/** - * MCP JSON-RPC error with payment required data - */ -export interface MCPPaymentRequiredError { - /** JSON-RPC error code (402) */ - code: typeof MCP_PAYMENT_REQUIRED_CODE; - /** Error message */ - message: string; - /** PaymentRequired data */ - data: PaymentRequired; -} - -/** - * Type guard to check if an error is a payment required error - * - * @param error - The error to check - * @returns True if the error is a payment required error - */ -export function isPaymentRequiredError(error: unknown): error is MCPPaymentRequiredError { - if (!isObject(error)) { - return false; - } - - if (error.code !== MCP_PAYMENT_REQUIRED_CODE || typeof error.message !== "string") { - return false; - } - - if (!isObject(error.data)) { - return false; - } - - return "x402Version" in error.data && "accepts" in error.data; -} diff --git a/typescript/packages/mcp/src/utils/encoding.ts b/typescript/packages/mcp/src/utils/encoding.ts deleted file mode 100644 index b8d694f..0000000 --- a/typescript/packages/mcp/src/utils/encoding.ts +++ /dev/null @@ -1,228 +0,0 @@ -import type { PaymentPayload, PaymentRequired, SettleResponse } from "@x402/core/types"; -import { - MCP_PAYMENT_META_KEY, - MCP_PAYMENT_REQUIRED_CODE, - MCP_PAYMENT_RESPONSE_META_KEY, - type MCPPaymentRequiredError, - type MCPRequestParamsWithMeta, - type MCPResultWithMeta, -} from "../types"; - -// ============================================================================ -// Type Guards -// ============================================================================ - -/** - * Type guard for checking if a value is a non-null object. - * Exported for use in other modules. - * - * @param value - The value to check - * @returns True if value is a non-null object - */ -export function isObject(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -/** - * Type guard for PaymentPayload structure. - * Only performs minimal structural validation - full validation happens in verifyPayment. - * - * @param value - The value to check - * @returns True if value is a PaymentPayload structure - */ -function isPaymentPayloadStructure(value: unknown): value is PaymentPayload { - if (!isObject(value)) { - return false; - } - // PaymentPayload must have x402Version and payload fields - return "x402Version" in value && "payload" in value; -} - -/** - * Type guard for SettleResponse structure. - * - * @param value - The value to check - * @returns True if value is a SettleResponse structure - */ -function isSettleResponseStructure(value: unknown): value is SettleResponse { - if (!isObject(value)) { - return false; - } - return "success" in value; -} - -/** - * Type guard for PaymentRequired structure. - * - * @param value - The value to check - * @returns True if value is a PaymentRequired structure - */ -function isPaymentRequiredStructure(value: unknown): value is PaymentRequired { - if (!isObject(value)) { - return false; - } - return ( - "x402Version" in value && - "accepts" in value && - Array.isArray((value as { accepts: unknown }).accepts) - ); -} - -// ============================================================================ -// Extraction Functions -// ============================================================================ - -/** - * Extracts payment payload from MCP request _meta field. - * Matches HTTP transport's simple validation approach. - * - * @param params - MCP request parameters that may contain _meta - * @returns The payment payload if present and valid, null otherwise - */ -export function extractPaymentFromMeta( - params: MCPRequestParamsWithMeta | undefined, -): PaymentPayload | null { - if (!params?._meta) { - return null; - } - - const payment = params._meta[MCP_PAYMENT_META_KEY]; - - // Simple validation - just check it has expected structure - // Full validation happens in verifyPayment - if (!isPaymentPayloadStructure(payment)) { - return null; - } - - return payment; -} - -/** - * Attaches payment payload to MCP request params _meta field - * - * @param params - Original request params containing name and optional arguments - * @param params.name - The tool name - * @param params.arguments - Optional tool arguments - * @param paymentPayload - Payment payload to attach - * @returns New params object with payment in _meta - */ -export function attachPaymentToMeta( - params: { name: string; arguments?: Record }, - paymentPayload: PaymentPayload, -): MCPRequestParamsWithMeta { - return { - ...params, - _meta: { - [MCP_PAYMENT_META_KEY]: paymentPayload, - }, - }; -} - -/** - * Extracts payment response from MCP result _meta field - * - * @param result - MCP result that may contain _meta - * @returns The settlement response if present, null otherwise - */ -export function extractPaymentResponseFromMeta( - result: MCPResultWithMeta | undefined, -): SettleResponse | null { - if (!result?._meta) { - return null; - } - - const response = result._meta[MCP_PAYMENT_RESPONSE_META_KEY]; - - // Validate it has the required structure - if (!isSettleResponseStructure(response)) { - return null; - } - - return response; -} - -/** - * Result content item for MCP responses - */ -interface ResultContentItem { - [key: string]: unknown; - type: string; -} - -/** - * Attaches settlement response to MCP result _meta field - * - * @param result - Original result object containing content and optional isError flag - * @param result.content - The tool result content array - * @param result.isError - Optional flag indicating if the result is an error - * @param settleResponse - Settlement response to attach - * @returns New result object with payment response in _meta - */ -export function attachPaymentResponseToMeta( - result: { content: ResultContentItem[]; isError?: boolean }, - settleResponse: SettleResponse, -): MCPResultWithMeta { - return { - ...result, - _meta: { - [MCP_PAYMENT_RESPONSE_META_KEY]: settleResponse, - }, - }; -} - -/** - * Creates an MCP JSON-RPC error for payment required (402) - * - * @param paymentRequired - The payment requirements - * @param message - Optional custom error message - * @returns JSON-RPC error object - */ -export function createPaymentRequiredError( - paymentRequired: PaymentRequired, - message?: string, -): MCPPaymentRequiredError { - return { - code: MCP_PAYMENT_REQUIRED_CODE, - message: message || "Payment required", - data: paymentRequired, - }; -} - -/** - * Extracts PaymentRequired from an MCP JSON-RPC error - * - * @param error - The error object from a JSON-RPC response - * @returns The PaymentRequired if this is a 402 error, null otherwise - */ -export function extractPaymentRequiredFromError(error: unknown): PaymentRequired | null { - if (!isObject(error)) { - return null; - } - - // Check if this is a 402 payment required error - if (error.code !== MCP_PAYMENT_REQUIRED_CODE) { - return null; - } - - // Extract and validate the data field - const data = error.data; - if (!isPaymentRequiredStructure(data)) { - return null; - } - - return data; -} - -/** - * Creates a resource URL for an MCP tool - * - * @param toolName - The name of the tool - * @param customUrl - Optional custom URL override - * @returns The resource URL - */ -export function createToolResourceUrl(toolName: string, customUrl?: string): string { - if (customUrl) { - return customUrl; - } - return `mcp://tool/${toolName}`; -} diff --git a/typescript/packages/mcp/src/utils/index.ts b/typescript/packages/mcp/src/utils/index.ts deleted file mode 100644 index 3ecdd50..0000000 --- a/typescript/packages/mcp/src/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./encoding"; diff --git a/typescript/packages/mcp/test/integration/mcp-payment-flow.test.ts b/typescript/packages/mcp/test/integration/mcp-payment-flow.test.ts deleted file mode 100644 index cd12d67..0000000 --- a/typescript/packages/mcp/test/integration/mcp-payment-flow.test.ts +++ /dev/null @@ -1,405 +0,0 @@ -/** - * Integration tests for MCP payment flow - * - * These tests verify the complete payment flow from client to server, - * using mocked MCP transport but real x402 payment processing logic. - */ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { x402MCPClient, x402MCPServer } from "../../src"; -import type { - PaymentPayload, - PaymentRequirements, - SettleResponse, - VerifyResponse, - SupportedResponse, - FacilitatorClient, -} from "@x402/core/types"; -import { z } from "zod"; - -// ============================================================================ -// Test Fixtures -// ============================================================================ - -const TEST_NETWORK = "eip155:84532" as const; -const TEST_RECIPIENT = "0x1234567890123456789012345678901234567890"; -const TEST_ASSET = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; // USDC on Base Sepolia -const TEST_PRICE = "1000"; // 0.001 USDC - -const mockPaymentRequirements: PaymentRequirements = { - scheme: "exact", - network: TEST_NETWORK, - amount: TEST_PRICE, - asset: TEST_ASSET, - payTo: TEST_RECIPIENT, - maxTimeoutSeconds: 60, - extra: { name: "USDC", version: "2" }, -}; - -const mockPaymentPayload: PaymentPayload = { - x402Version: 2, - payload: { - signature: "0xmocksignature", - authorization: { - from: "0xclient", - to: TEST_RECIPIENT, - value: TEST_PRICE, - validAfter: 0, - validBefore: Math.floor(Date.now() / 1000) + 3600, - nonce: "0x1", - }, - }, -}; - -const mockSettleResponse: SettleResponse = { - success: true, - transaction: "0xtxhash123456", - network: TEST_NETWORK, -}; - -// ============================================================================ -// Mock Implementations -// ============================================================================ - -/** - * Mock facilitator client for testing - */ -class MockFacilitatorClient implements FacilitatorClient { - readonly scheme = "exact"; - readonly network = TEST_NETWORK; - readonly x402Version = 2; - - verify = vi.fn().mockResolvedValue({ isValid: true } as VerifyResponse); - settle = vi.fn().mockResolvedValue(mockSettleResponse); - getSupported = vi.fn().mockResolvedValue({ - x402Version: 2, - kinds: [ - { - scheme: "exact", - network: TEST_NETWORK, - asset: TEST_ASSET, - extra: { name: "USDC", version: "2" }, - }, - ], - } as SupportedResponse); -} - -/** - * Mock MCP client that simulates the transport layer - * - * @param serverHandler - The handler function for tool calls - * @returns Mock MCP client instance - */ -function createMockMcpClient( - serverHandler: ( - name: string, - args: Record, - meta?: Record, - ) => Promise<{ - content: Array<{ type: string; text: string }>; - isError?: boolean; - _meta?: Record; - }>, -) { - return { - connect: vi.fn().mockResolvedValue(undefined), - close: vi.fn().mockResolvedValue(undefined), - listTools: vi.fn().mockResolvedValue({ tools: [] }), - listResources: vi.fn().mockResolvedValue({ resources: [] }), - listPrompts: vi.fn().mockResolvedValue({ prompts: [] }), - callTool: vi - .fn() - .mockImplementation( - async (params: { - name: string; - arguments?: Record; - _meta?: Record; - }) => { - return serverHandler(params.name, params.arguments ?? {}, params._meta); - }, - ), - }; -} - -/** - * Mock payment client that creates mock payment payloads - * - * @returns Mock payment client instance - */ -function createMockPaymentClient() { - return { - createPaymentPayload: vi.fn().mockResolvedValue(mockPaymentPayload), - register: vi.fn().mockReturnThis(), - registerV1: vi.fn().mockReturnThis(), - }; -} - -// ============================================================================ -// Integration Tests -// ============================================================================ - -describe("MCP Payment Flow Integration", () => { - let facilitator: MockFacilitatorClient; - let server: x402MCPServer; - let serverToolHandlers: Map< - string, - ( - args: Record, - extra: { _meta?: Record }, - ) => Promise<{ - content: Array<{ type: "text"; text: string }>; - isError?: boolean; - _meta?: Record; - }> - >; - - beforeEach(async () => { - facilitator = new MockFacilitatorClient(); - serverToolHandlers = new Map(); - - // Create server with mock facilitator - const mockMcpServer = { - tool: vi - .fn() - .mockImplementation( - ( - name: string, - _desc: string, - _schema: unknown, - handler: typeof serverToolHandlers extends Map ? H : never, - ) => { - serverToolHandlers.set(name, handler); - }, - ), - resource: vi.fn(), - prompt: vi.fn(), - }; - - const mockResourceServer = { - initialize: vi.fn().mockResolvedValue(undefined), - register: vi.fn(), - verifyPayment: facilitator.verify, - settlePayment: facilitator.settle, - buildPaymentRequirements: vi.fn().mockResolvedValue([mockPaymentRequirements]), - createPaymentRequiredResponse: vi - .fn() - .mockImplementation((_requirements, resourceInfo, errorMessage) => ({ - x402Version: 2, - accepts: [mockPaymentRequirements], - error: errorMessage, - resource: resourceInfo, - })), - }; - - server = new x402MCPServer( - mockMcpServer as unknown as ConstructorParameters[0], - mockResourceServer as unknown as ConstructorParameters[1], - ); - - // Register test tools - server.paidTool( - "get_weather", - { - description: "Get weather for a city", - inputSchema: { city: z.string() }, - }, - { - scheme: "exact", - network: TEST_NETWORK, - price: "$0.001", - payTo: TEST_RECIPIENT, - extra: { name: "USDC", version: "2" }, - }, - async ({ city }) => ({ - content: [ - { type: "text" as const, text: JSON.stringify({ city, weather: "sunny", temp: 72 }) }, - ], - }), - ); - - server.tool("ping", "Health check", {}, async () => ({ - content: [{ type: "text" as const, text: "pong" }], - })); - - await server.initialize(); - }); - - describe("complete payment flow", () => { - it("should handle free tool calls without payment", async () => { - const mockPaymentClient = createMockPaymentClient(); - - // Create mock MCP client that routes to server handlers - const mockMcpClient = createMockMcpClient(async (name, args, meta) => { - const handler = serverToolHandlers.get(name); - if (!handler) throw new Error(`Tool ${name} not found`); - return handler(args, { _meta: meta }); - }); - - const client = new x402MCPClient( - mockMcpClient as unknown as ConstructorParameters[0], - mockPaymentClient as unknown as ConstructorParameters[1], - ); - - const result = await client.callTool("ping"); - - expect(result.paymentMade).toBe(false); - expect(result.content[0]?.text).toBe("pong"); - expect(mockPaymentClient.createPaymentPayload).not.toHaveBeenCalled(); - }); - - it("should complete paid tool flow with auto-payment", async () => { - const mockPaymentClient = createMockPaymentClient(); - - // Track call sequence - let callCount = 0; - - const mockMcpClient = createMockMcpClient(async (name, args, meta) => { - callCount++; - const handler = serverToolHandlers.get(name); - if (!handler) throw new Error(`Tool ${name} not found`); - return handler(args, { _meta: meta }); - }); - - const client = new x402MCPClient( - mockMcpClient as unknown as ConstructorParameters[0], - mockPaymentClient as unknown as ConstructorParameters[1], - { autoPayment: true }, - ); - - const result = await client.callTool("get_weather", { city: "San Francisco" }); - - // Should have made 2 calls: initial (402) + retry (with payment) - expect(callCount).toBe(2); - - // Payment should have been made - expect(result.paymentMade).toBe(true); - expect(mockPaymentClient.createPaymentPayload).toHaveBeenCalled(); - - // Should have valid result - const content = JSON.parse(result.content[0]?.text ?? "{}"); - expect(content.city).toBe("San Francisco"); - expect(content.weather).toBe("sunny"); - - // Payment response should be present - expect(result.paymentResponse).toEqual(mockSettleResponse); - }); - - it("should call approval hook before payment", async () => { - const approvalHook = vi.fn().mockResolvedValue(true); - const mockPaymentClient = createMockPaymentClient(); - - const mockMcpClient = createMockMcpClient(async (name, args, meta) => { - const handler = serverToolHandlers.get(name); - if (!handler) throw new Error(`Tool ${name} not found`); - return handler(args, { _meta: meta }); - }); - - const client = new x402MCPClient( - mockMcpClient as unknown as ConstructorParameters[0], - mockPaymentClient as unknown as ConstructorParameters[1], - { autoPayment: true, onPaymentRequested: approvalHook }, - ); - - await client.callTool("get_weather", { city: "NYC" }); - - expect(approvalHook).toHaveBeenCalledWith( - expect.objectContaining({ - toolName: "get_weather", - arguments: { city: "NYC" }, - paymentRequired: expect.objectContaining({ - x402Version: 2, - accepts: expect.any(Array), - }), - }), - ); - }); - - it("should abort if payment request denied", async () => { - const approvalHook = vi.fn().mockResolvedValue(false); - const mockPaymentClient = createMockPaymentClient(); - - const mockMcpClient = createMockMcpClient(async (name, args, meta) => { - const handler = serverToolHandlers.get(name); - if (!handler) throw new Error(`Tool ${name} not found`); - return handler(args, { _meta: meta }); - }); - - const client = new x402MCPClient( - mockMcpClient as unknown as ConstructorParameters[0], - mockPaymentClient as unknown as ConstructorParameters[1], - { autoPayment: true, onPaymentRequested: approvalHook }, - ); - - await expect(client.callTool("get_weather", { city: "NYC" })).rejects.toThrow( - "Payment request denied", - ); - - // Payment should not have been created - expect(mockPaymentClient.createPaymentPayload).not.toHaveBeenCalled(); - }); - }); - - describe("error handling", () => { - it("should handle payment verification failure", async () => { - // Make verification fail - facilitator.verify.mockResolvedValueOnce({ - isValid: false, - invalidReason: "Invalid signature", - }); - - const mockPaymentClient = createMockPaymentClient(); - - const mockMcpClient = createMockMcpClient(async (name, args, meta) => { - const handler = serverToolHandlers.get(name); - if (!handler) throw new Error(`Tool ${name} not found`); - return handler(args, { _meta: meta }); - }); - - const client = new x402MCPClient( - mockMcpClient as unknown as ConstructorParameters[0], - mockPaymentClient as unknown as ConstructorParameters[1], - { autoPayment: true }, - ); - - // The second attempt with payment will also get 402 due to verification failure - // This will cause infinite retry unless we limit it - // For this test, we expect the result to have isError: true - const result = await client.callTool("get_weather", { city: "NYC" }); - - // After first 402, client retries with payment - // Server verifies, fails, returns 402 again - // Client sees 402 again and (since payment was already made) returns the error result - expect(result.isError).toBe(true); - }); - - it("should handle settlement failure gracefully", async () => { - // Make settlement fail - facilitator.settle.mockRejectedValueOnce(new Error("Network error during settlement")); - - const mockPaymentClient = createMockPaymentClient(); - - const mockMcpClient = createMockMcpClient(async (name, args, meta) => { - const handler = serverToolHandlers.get(name); - if (!handler) throw new Error(`Tool ${name} not found`); - return handler(args, { _meta: meta }); - }); - - const client = new x402MCPClient( - mockMcpClient as unknown as ConstructorParameters[0], - mockPaymentClient as unknown as ConstructorParameters[1], - { autoPayment: true }, - ); - - const result = await client.callTool("get_weather", { city: "NYC" }); - - // Per MCP spec, settlement failure returns a 402 error (not content with error in _meta) - // The client should see this as an error response - expect(result.paymentMade).toBe(true); - expect(result.isError).toBe(true); - expect(result.content.length).toBeGreaterThan(0); - // The error content contains the settlement failure info - const errorText = result.content[0].type === "text" ? result.content[0].text : ""; - expect(errorText).toContain("Payment settlement failed"); - expect(errorText).toContain("Network error"); - }); - }); -}); diff --git a/typescript/packages/mcp/test/integration/mcp-sse-evm.test.ts b/typescript/packages/mcp/test/integration/mcp-sse-evm.test.ts deleted file mode 100644 index 4b2cd5a..0000000 --- a/typescript/packages/mcp/test/integration/mcp-sse-evm.test.ts +++ /dev/null @@ -1,406 +0,0 @@ -/** - * Real SSE MCP Integration Tests - * - * These tests verify the complete MCP payment flow using: - * - Real SSE transport (not mocked) - * - Real EVM blockchain transactions on Base Sepolia - * - Real x402 payment processing - * - * Required environment variables: - * - CLIENT_PRIVATE_KEY: Private key for the client wallet (payer) - * - FACILITATOR_PRIVATE_KEY: Private key for the facilitator wallet (settles payments) - * - * These tests make REAL blockchain transactions on Base Sepolia testnet. - */ -import { describe, it, expect, beforeAll, afterAll } from "vitest"; -import { Server } from "http"; -import express from "express"; -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { z } from "zod"; -import { privateKeyToAccount } from "viem/accounts"; -import { createWalletClient, createPublicClient, http } from "viem"; -import { baseSepolia } from "viem/chains"; - -import { x402MCPClient, x402MCPServer } from "../../src"; -import { x402Client } from "@x402/core/client"; -import { x402ResourceServer, FacilitatorClient } from "@x402/core/server"; -import { x402Facilitator } from "@x402/core/facilitator"; -import type { - PaymentPayload, - PaymentRequirements, - VerifyResponse, - SettleResponse, - SupportedResponse, -} from "@x402/core/types"; -import { toFacilitatorEvmSigner } from "@x402/evm"; -import { ExactEvmScheme as ExactEvmClientScheme } from "@x402/evm/exact/client"; -import { ExactEvmScheme as ExactEvmServerScheme } from "@x402/evm/exact/server"; -import { ExactEvmScheme as ExactEvmFacilitatorScheme } from "@x402/evm/exact/facilitator"; - -// ============================================================================ -// Environment Setup -// ============================================================================ - -const CLIENT_PRIVATE_KEY = process.env.CLIENT_PRIVATE_KEY as `0x${string}`; -const FACILITATOR_PRIVATE_KEY = process.env.FACILITATOR_PRIVATE_KEY as `0x${string}`; - -// Skip tests if environment variables are not set -const SKIP_TESTS = !CLIENT_PRIVATE_KEY || !FACILITATOR_PRIVATE_KEY; - -if (SKIP_TESTS) { - console.warn( - "⚠️ Skipping real SSE integration tests: CLIENT_PRIVATE_KEY and FACILITATOR_PRIVATE_KEY must be set", - ); -} - -// ============================================================================ -// Test Configuration -// ============================================================================ - -const TEST_PORT = 4099; -const TEST_NETWORK = "eip155:84532" as const; // Base Sepolia -// Base Sepolia USDC: 0x036CbD53842c5426634e7929541eC2318f3dCF7e - -/** - * EVM Facilitator Client wrapper for x402ResourceServer - */ -class EvmFacilitatorClient implements FacilitatorClient { - readonly scheme = "exact"; - readonly network = TEST_NETWORK; - readonly x402Version = 2; - - /** - * Creates a new EvmFacilitatorClient instance - * - * @param facilitator - The x402 facilitator to wrap - */ - constructor(private readonly facilitator: x402Facilitator) {} - - /** - * Verifies a payment payload - * - * @param paymentPayload - The payment payload to verify - * @param paymentRequirements - The payment requirements - * @returns Promise resolving to verification response - */ - verify( - paymentPayload: PaymentPayload, - paymentRequirements: PaymentRequirements, - ): Promise { - return this.facilitator.verify(paymentPayload, paymentRequirements); - } - - /** - * Settles a payment - * - * @param paymentPayload - The payment payload to settle - * @param paymentRequirements - The payment requirements - * @returns Promise resolving to settlement response - */ - settle( - paymentPayload: PaymentPayload, - paymentRequirements: PaymentRequirements, - ): Promise { - return this.facilitator.settle(paymentPayload, paymentRequirements); - } - - /** - * Gets supported payment kinds - * - * @returns Promise resolving to supported response - */ - getSupported(): Promise { - return Promise.resolve(this.facilitator.getSupported()); - } -} - -// ============================================================================ -// Test Suite -// ============================================================================ - -describe.skipIf(SKIP_TESTS)("Real SSE MCP Integration Tests", () => { - let httpServer: Server; - let x402Server: x402MCPServer; - let x402ClientInstance: x402MCPClient; - let clientAddress: `0x${string}`; - let recipientAddress: `0x${string}`; - let transports: Map; - - beforeAll(async () => { - // ======================================================================== - // Setup Client (Payer) - // ======================================================================== - const clientAccount = privateKeyToAccount(CLIENT_PRIVATE_KEY); - clientAddress = clientAccount.address; - console.log(`\n🔑 Client address: ${clientAddress}`); - - const evmClientScheme = new ExactEvmClientScheme(clientAccount); - const paymentClient = new x402Client().register(TEST_NETWORK, evmClientScheme); - - // ======================================================================== - // Setup Facilitator (Settles Payments) - // ======================================================================== - const facilitatorAccount = privateKeyToAccount(FACILITATOR_PRIVATE_KEY); - recipientAddress = facilitatorAccount.address; // Use facilitator as recipient for simplicity - console.log(`🔑 Facilitator/Recipient address: ${recipientAddress}`); - - const publicClient = createPublicClient({ - chain: baseSepolia, - transport: http(), - }); - - const walletClient = createWalletClient({ - account: facilitatorAccount, - chain: baseSepolia, - transport: http(), - }); - - const facilitatorSigner = toFacilitatorEvmSigner({ - address: facilitatorAccount.address, - readContract: args => - publicClient.readContract({ - ...args, - args: args.args || [], - } as never), - verifyTypedData: args => publicClient.verifyTypedData(args as never), - writeContract: args => - walletClient.writeContract({ - ...args, - args: args.args || [], - } as never), - sendTransaction: args => walletClient.sendTransaction(args), - waitForTransactionReceipt: args => publicClient.waitForTransactionReceipt(args), - getCode: args => publicClient.getCode(args), - }); - - const evmFacilitator = new ExactEvmFacilitatorScheme(facilitatorSigner); - const facilitator = new x402Facilitator().register(TEST_NETWORK, evmFacilitator); - const facilitatorClient = new EvmFacilitatorClient(facilitator); - - // ======================================================================== - // Setup MCP Server with x402 - // ======================================================================== - const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js"); - const mcpServer = new McpServer({ - name: "x402 Test Server", - version: "1.0.0", - }); - - const resourceServer = new x402ResourceServer(facilitatorClient); - resourceServer.register(TEST_NETWORK, new ExactEvmServerScheme()); - await resourceServer.initialize(); - - x402Server = new x402MCPServer(mcpServer, resourceServer); - - // Register a FREE tool - x402Server.tool("ping", "A free health check tool", {}, async () => ({ - content: [{ type: "text" as const, text: "pong" }], - })); - - // Register a PAID tool - x402Server.paidTool( - "get_weather", - { - description: "Get weather for a city. Requires payment.", - inputSchema: { - city: z.string().describe("The city name"), - }, - }, - { - scheme: "exact", - network: TEST_NETWORK, - price: "$0.001", // 0.001 USDC = 1000 atomic units - payTo: recipientAddress, - extra: { name: "USDC", version: "2" }, - }, - async ({ city }) => ({ - content: [ - { - type: "text" as const, - text: JSON.stringify({ city, weather: "sunny", temperature: 72 }), - }, - ], - }), - ); - - await x402Server.initialize(); - - // ======================================================================== - // Start Express Server for SSE - // ======================================================================== - const app = express(); - transports = new Map(); - - app.get("/sse", async (req, res) => { - const transport = new SSEServerTransport("/messages", res); - const sessionId = crypto.randomUUID(); - transports.set(sessionId, transport); - res.on("close", () => { - transports.delete(sessionId); - }); - await mcpServer.connect(transport); - }); - - app.post("/messages", express.json(), async (req, res) => { - const transport = Array.from(transports.values())[0]; - if (!transport) { - res.status(400).json({ error: "No active SSE connection" }); - return; - } - await transport.handlePostMessage(req, res, req.body); - }); - - httpServer = app.listen(TEST_PORT); - console.log(`\n🚀 Test MCP Server running on http://localhost:${TEST_PORT}\n`); - - // ======================================================================== - // Setup x402 MCP Client with SSE Transport - // ======================================================================== - // Small delay to ensure server is ready - await new Promise(resolve => setTimeout(resolve, 100)); - - const sseTransport = new SSEClientTransport(new URL(`http://localhost:${TEST_PORT}/sse`)); - const mcpClient = new Client({ name: "x402-test-client", version: "1.0.0" }); - - x402ClientInstance = new x402MCPClient(mcpClient, paymentClient, { - autoPayment: true, - onPaymentRequested: async ({ paymentRequired }) => { - console.log(`\n💰 Payment requested: ${paymentRequired.accepts[0].amount} atomic units`); - return true; // Auto-approve for tests - }, - }); - - await x402ClientInstance.connect(sseTransport); - console.log("✅ x402 MCP Client connected via SSE\n"); - }, 30000); // 30s timeout for setup - - afterAll(async () => { - if (x402ClientInstance) { - await x402ClientInstance.close(); - } - if (httpServer) { - httpServer.close(); - } - }); - - // ========================================================================== - // Test 1: SSE Connection works - // ========================================================================== - it("should establish SSE connection successfully", async () => { - // If we got here, connection was established in beforeAll - expect(x402ClientInstance).toBeDefined(); - expect(x402ClientInstance.client).toBeDefined(); - }); - - // ========================================================================== - // Test 2: list/tools works without payment - // ========================================================================== - it("should list tools without requiring payment", async () => { - const result = await x402ClientInstance.listTools(); - - expect(result.tools).toBeDefined(); - expect(result.tools.length).toBeGreaterThan(0); - - // Verify our tools are listed - const toolNames = result.tools.map(t => t.name); - expect(toolNames).toContain("ping"); - expect(toolNames).toContain("get_weather"); - - console.log("📋 Available tools:", toolNames.join(", ")); - }); - - // ========================================================================== - // Test 3: Free tool works without payment - // ========================================================================== - it("should call free tool without payment", async () => { - const result = await x402ClientInstance.callTool("ping"); - - expect(result.paymentMade).toBe(false); - expect(result.isError).toBeFalsy(); - expect(result.content.length).toBeGreaterThan(0); - - const textContent = result.content[0] as { type: string; text: string }; - expect(textContent.text).toBe("pong"); - - console.log("🏓 Free tool result:", textContent.text); - }); - - // ========================================================================== - // Test 4: Paid tool returns 402 without payment - // ========================================================================== - it("should receive 402 for paid tool without payment (manual test)", async () => { - // Test with autoPayment disabled to see the 402 - const manualClient = new x402MCPClient( - x402ClientInstance.client, - x402ClientInstance.paymentClient, - { autoPayment: false }, - ); - - try { - await manualClient.callTool("get_weather", { city: "San Francisco" }); - // Should not reach here - expect.fail("Should have thrown 402 error"); - } catch (error) { - const err = error as { code?: number; paymentRequired?: unknown }; - expect(err.code).toBe(402); - expect(err.paymentRequired).toBeDefined(); - console.log("💳 402 Payment Required received as expected"); - } - }); - - // ========================================================================== - // Test 5: Paid tool with payment succeeds (REAL BLOCKCHAIN TRANSACTION) - // ========================================================================== - it("should complete paid tool with auto-payment and settle on blockchain", async () => { - console.log("\n🔄 Starting paid tool call with real blockchain settlement...\n"); - - const result = await x402ClientInstance.callTool("get_weather", { city: "New York" }); - - // Verify payment was made - expect(result.paymentMade).toBe(true); - expect(result.isError).toBeFalsy(); - - // Verify we got the tool result - expect(result.content.length).toBeGreaterThan(0); - const textContent = result.content[0] as { type: string; text: string }; - const weatherData = JSON.parse(textContent.text); - expect(weatherData.city).toBe("New York"); - - console.log("🌤️ Weather data:", JSON.stringify(weatherData, null, 2)); - - // Verify payment response (settlement result) - expect(result.paymentResponse).toBeDefined(); - expect(result.paymentResponse?.success).toBe(true); - expect(result.paymentResponse?.transaction).toBeDefined(); - expect(result.paymentResponse?.network).toBe(TEST_NETWORK); - - console.log("\n✅ Settlement successful!"); - console.log(` Transaction: ${result.paymentResponse?.transaction}`); - console.log(` Network: ${result.paymentResponse?.network}`); - console.log( - ` View on BaseScan: https://sepolia.basescan.org/tx/${result.paymentResponse?.transaction}\n`, - ); - }, 60000); // 60s timeout for blockchain transaction - - // ========================================================================== - // Test 6: Multiple paid tool calls work - // ========================================================================== - it("should handle multiple paid tool calls", async () => { - console.log("\n🔄 Starting second paid tool call...\n"); - - const result = await x402ClientInstance.callTool("get_weather", { city: "Los Angeles" }); - - expect(result.paymentMade).toBe(true); - expect(result.isError).toBeFalsy(); - expect(result.paymentResponse?.success).toBe(true); - - const textContent = result.content[0] as { type: string; text: string }; - const weatherData = JSON.parse(textContent.text); - expect(weatherData.city).toBe("Los Angeles"); - - console.log("✅ Second settlement successful!"); - console.log(` Transaction: ${result.paymentResponse?.transaction}\n`); - }, 60000); -}); diff --git a/typescript/packages/mcp/test/unit/client.test.ts b/typescript/packages/mcp/test/unit/client.test.ts deleted file mode 100644 index 337d757..0000000 --- a/typescript/packages/mcp/test/unit/client.test.ts +++ /dev/null @@ -1,717 +0,0 @@ -/** - * Unit tests for x402MCPClient - */ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { x402MCPClient, createx402MCPClient, wrapMCPClientWithPayment } from "../../src/client"; -import { MCP_PAYMENT_REQUIRED_CODE, MCP_PAYMENT_META_KEY } from "../../src/types"; -import type { PaymentPayload, PaymentRequired, SettleResponse } from "@x402/core/types"; - -// ============================================================================ -// Mock Types -// ============================================================================ - -interface MockMCPClient { - connect: ReturnType; - close: ReturnType; - listTools: ReturnType; - listResources: ReturnType; - listPrompts: ReturnType; - callTool: ReturnType; -} - -interface MockPaymentClient { - createPaymentPayload: ReturnType; - register: ReturnType; - registerV1: ReturnType; -} - -// ============================================================================ -// Test Fixtures -// ============================================================================ - -const mockPaymentRequired: PaymentRequired = { - x402Version: 2, - accepts: [ - { - scheme: "exact", - network: "eip155:84532", - amount: "1000", - asset: "0xtoken", - payTo: "0xrecipient", - maxTimeoutSeconds: 60, - extra: {}, - }, - ], - error: "Payment required", - resource: { - url: "mcp://tool/test", - description: "Test tool", - mimeType: "application/json", - }, -}; - -const mockPaymentPayload: PaymentPayload = { - x402Version: 2, - payload: { - signature: "0x123", - authorization: { - from: "0xabc", - to: "0xdef", - value: "1000", - validAfter: 0, - validBefore: Math.floor(Date.now() / 1000) + 3600, - nonce: "0x1", - }, - }, -}; - -const mockSettleResponse: SettleResponse = { - success: true, - transaction: "0xtxhash123", - network: "eip155:84532", -}; - -/** - * V1 PaymentRequired for interoperability testing (ethanniser/x402-mcp style) - */ -const mockPaymentRequiredV1 = { - x402Version: 1, - error: "Payment required", - accepts: [ - { - scheme: "exact", - network: "base-sepolia", - maxAmountRequired: "1000", - asset: "0xtoken", - payTo: "0xrecipient", - maxTimeoutSeconds: 60, - resource: "mcp://tool/test", - mimeType: "application/json", - description: "Test tool", - extra: {}, - }, - ], -}; - -/** - * Creates a PaymentRequired response in content format (per MCP transport spec) - * - * @param paymentRequired - The payment required object - * @returns MCP tool result with direct PaymentRequired in content - */ -function createEmbeddedPaymentError(paymentRequired: PaymentRequired): { - content: Array<{ type: "text"; text: string }>; - isError: true; -} { - return { - content: [ - { - type: "text", - text: JSON.stringify(paymentRequired), - }, - ], - isError: true, - }; -} - -/** - * Creates a structuredContent response with direct PaymentRequired (ethanniser/x402-mcp style) - * - * @param paymentRequired - The payment required object - * @returns MCP tool result with structuredContent - */ -function createStructuredContentDirectPaymentError( - paymentRequired: PaymentRequired | typeof mockPaymentRequiredV1, -): { - content: Array<{ type: "text"; text: string }>; - structuredContent: Record; - isError: true; -} { - return { - structuredContent: paymentRequired as Record, - content: [{ type: "text", text: JSON.stringify(paymentRequired) }], - isError: true, - }; -} - -/** - * Creates a content-only response with direct PaymentRequired (V1 compatibility fallback) - * - * @param paymentRequired - The payment required object - * @returns MCP tool result with content fallback - */ -function createContentDirectPaymentError( - paymentRequired: PaymentRequired | typeof mockPaymentRequiredV1, -): { - content: Array<{ type: "text"; text: string }>; - isError: true; -} { - return { - content: [{ type: "text", text: JSON.stringify(paymentRequired) }], - isError: true, - }; -} - -// ============================================================================ -// Mock Factories -// ============================================================================ - -/** - * Creates a mock MCP client for testing - * - * @returns Mock MCP client instance - */ -function createMockMCPClient(): MockMCPClient { - return { - connect: vi.fn().mockResolvedValue(undefined), - close: vi.fn().mockResolvedValue(undefined), - listTools: vi.fn().mockResolvedValue({ tools: [] }), - listResources: vi.fn().mockResolvedValue({ resources: [] }), - listPrompts: vi.fn().mockResolvedValue({ prompts: [] }), - callTool: vi.fn(), - }; -} - -/** - * Creates a mock x402 payment client for testing - * - * @returns Mock payment client instance - */ -function createMockPaymentClient(): MockPaymentClient { - return { - createPaymentPayload: vi.fn().mockResolvedValue(mockPaymentPayload), - register: vi.fn().mockReturnThis(), - registerV1: vi.fn().mockReturnThis(), - }; -} - -// ============================================================================ -// x402MCPClient Tests -// ============================================================================ - -describe("x402MCPClient", () => { - let mockMcpClient: MockMCPClient; - let mockPaymentClient: MockPaymentClient; - let client: x402MCPClient; - - beforeEach(() => { - mockMcpClient = createMockMCPClient(); - mockPaymentClient = createMockPaymentClient(); - client = new x402MCPClient( - mockMcpClient as unknown as Parameters[0], - mockPaymentClient as unknown as Parameters[1], - ); - }); - - describe("constructor and accessors", () => { - it("should expose underlying MCP client", () => { - expect(client.client).toBe(mockMcpClient); - }); - - it("should expose underlying payment client", () => { - expect(client.paymentClient).toBe(mockPaymentClient); - }); - - it("should default autoPayment to true", async () => { - // Test by calling a paid tool - should auto-pay - mockMcpClient.callTool - .mockResolvedValueOnce(createEmbeddedPaymentError(mockPaymentRequired)) - .mockResolvedValueOnce({ - content: [{ type: "text", text: "success" }], - _meta: { "x402/payment-response": mockSettleResponse }, - }); - - // Should not throw because autoPayment is enabled - await expect(client.callTool("test")).resolves.toBeDefined(); - }); - }); - - describe("passthrough methods", () => { - it("should passthrough connect()", async () => { - const transport = {} as Parameters[0]; - await client.connect(transport); - expect(mockMcpClient.connect).toHaveBeenCalledWith(transport); - }); - - it("should passthrough close()", async () => { - await client.close(); - expect(mockMcpClient.close).toHaveBeenCalled(); - }); - - it("should passthrough listTools()", async () => { - const tools = { tools: [{ name: "test", description: "Test tool" }] }; - mockMcpClient.listTools.mockResolvedValue(tools); - - const result = await client.listTools(); - expect(result).toEqual(tools); - expect(mockMcpClient.listTools).toHaveBeenCalled(); - }); - - it("should passthrough listResources()", async () => { - await client.listResources(); - expect(mockMcpClient.listResources).toHaveBeenCalled(); - }); - - it("should passthrough listPrompts()", async () => { - await client.listPrompts(); - expect(mockMcpClient.listPrompts).toHaveBeenCalled(); - }); - }); - - describe("callTool - free tools", () => { - it("should call free tool without payment", async () => { - mockMcpClient.callTool.mockResolvedValue({ - content: [{ type: "text", text: "pong" }], - isError: false, - }); - - const result = await client.callTool("ping"); - - expect(result.paymentMade).toBe(false); - expect(result.content[0]?.text).toBe("pong"); - expect(mockMcpClient.callTool).toHaveBeenCalledTimes(1); - }); - }); - - describe("callTool - paid tools with autoPayment", () => { - it("should auto-pay and retry on 402", async () => { - // First call returns 402, second call with payment succeeds - mockMcpClient.callTool - .mockResolvedValueOnce(createEmbeddedPaymentError(mockPaymentRequired)) - .mockResolvedValueOnce({ - content: [{ type: "text", text: "paid result" }], - _meta: { "x402/payment-response": mockSettleResponse }, - }); - - const result = await client.callTool("paid_tool", { arg: "value" }); - - expect(result.paymentMade).toBe(true); - expect(result.content[0]?.text).toBe("paid result"); - expect(result.paymentResponse).toEqual(mockSettleResponse); - expect(mockMcpClient.callTool).toHaveBeenCalledTimes(2); - expect(mockPaymentClient.createPaymentPayload).toHaveBeenCalledWith(mockPaymentRequired); - }); - - it("should include payment in _meta on retry", async () => { - mockMcpClient.callTool - .mockResolvedValueOnce(createEmbeddedPaymentError(mockPaymentRequired)) - .mockResolvedValueOnce({ - content: [{ type: "text", text: "result" }], - }); - - await client.callTool("paid_tool"); - - // Second call should include payment in _meta - const secondCall = mockMcpClient.callTool.mock.calls[1][0]; - expect(secondCall._meta?.[MCP_PAYMENT_META_KEY]).toEqual(mockPaymentPayload); - }); - }); - - describe("callTool - paid tools without autoPayment", () => { - beforeEach(() => { - client = new x402MCPClient( - mockMcpClient as unknown as Parameters[0], - mockPaymentClient as unknown as Parameters[1], - { autoPayment: false }, - ); - }); - - it("should throw with payment info when autoPayment is disabled", async () => { - mockMcpClient.callTool.mockResolvedValue(createEmbeddedPaymentError(mockPaymentRequired)); - - await expect(client.callTool("paid_tool")).rejects.toMatchObject({ - message: "Payment required", - code: MCP_PAYMENT_REQUIRED_CODE, - paymentRequired: mockPaymentRequired, - }); - }); - }); - - describe("callTool - approval flow", () => { - it("should call onPaymentRequested hook", async () => { - const approvalHook = vi.fn().mockResolvedValue(true); - client = new x402MCPClient( - mockMcpClient as unknown as Parameters[0], - mockPaymentClient as unknown as Parameters[1], - { autoPayment: true, onPaymentRequested: approvalHook }, - ); - - mockMcpClient.callTool - .mockResolvedValueOnce(createEmbeddedPaymentError(mockPaymentRequired)) - .mockResolvedValueOnce({ content: [{ type: "text", text: "result" }] }); - - await client.callTool("paid_tool", { arg: "value" }); - - expect(approvalHook).toHaveBeenCalledWith({ - toolName: "paid_tool", - arguments: { arg: "value" }, - paymentRequired: mockPaymentRequired, - }); - }); - - it("should throw if payment request is denied", async () => { - const approvalHook = vi.fn().mockResolvedValue(false); - client = new x402MCPClient( - mockMcpClient as unknown as Parameters[0], - mockPaymentClient as unknown as Parameters[1], - { autoPayment: true, onPaymentRequested: approvalHook }, - ); - - mockMcpClient.callTool.mockResolvedValue(createEmbeddedPaymentError(mockPaymentRequired)); - - await expect(client.callTool("paid_tool")).rejects.toThrow("Payment request denied"); - }); - }); - - describe("hooks", () => { - it("should call beforePayment hooks", async () => { - const beforeHook = vi.fn(); - client.onBeforePayment(beforeHook); - - mockMcpClient.callTool - .mockResolvedValueOnce(createEmbeddedPaymentError(mockPaymentRequired)) - .mockResolvedValueOnce({ content: [{ type: "text", text: "result" }] }); - - await client.callTool("paid_tool"); - - expect(beforeHook).toHaveBeenCalledWith({ - toolName: "paid_tool", - arguments: {}, - paymentRequired: mockPaymentRequired, - }); - }); - - it("should call afterPayment hooks", async () => { - const afterHook = vi.fn(); - client.onAfterPayment(afterHook); - - mockMcpClient.callTool - .mockResolvedValueOnce(createEmbeddedPaymentError(mockPaymentRequired)) - .mockResolvedValueOnce({ - content: [{ type: "text", text: "result" }], - _meta: { "x402/payment-response": mockSettleResponse }, - }); - - await client.callTool("paid_tool"); - - expect(afterHook).toHaveBeenCalledWith( - expect.objectContaining({ - toolName: "paid_tool", - paymentPayload: mockPaymentPayload, - settleResponse: mockSettleResponse, - }), - ); - }); - - it("should support chaining hooks", () => { - const result = client.onBeforePayment(() => {}).onAfterPayment(() => {}); - expect(result).toBe(client); - }); - }); - - describe("callToolWithPayment", () => { - it("should call tool with explicit payment", async () => { - mockMcpClient.callTool.mockResolvedValue({ - content: [{ type: "text", text: "result" }], - _meta: { "x402/payment-response": mockSettleResponse }, - }); - - const result = await client.callToolWithPayment("tool", { arg: "value" }, mockPaymentPayload); - - expect(result.paymentMade).toBe(true); - expect(result.paymentResponse).toEqual(mockSettleResponse); - - const callArgs = mockMcpClient.callTool.mock.calls[0][0]; - expect(callArgs._meta?.[MCP_PAYMENT_META_KEY]).toEqual(mockPaymentPayload); - }); - }); - - describe("getToolPaymentRequirements", () => { - it("should return payment requirements for paid tools", async () => { - mockMcpClient.callTool.mockResolvedValue(createEmbeddedPaymentError(mockPaymentRequired)); - - const result = await client.getToolPaymentRequirements("paid_tool"); - - expect(result).toEqual(mockPaymentRequired); - }); - - it("should return null for free tools", async () => { - mockMcpClient.callTool.mockResolvedValue({ - content: [{ type: "text", text: "result" }], - isError: false, - }); - - const result = await client.getToolPaymentRequirements("free_tool"); - - expect(result).toBeNull(); - }); - }); -}); - -// ============================================================================ -// Factory Function Tests -// ============================================================================ - -describe("wrapMCPClientWithPayment", () => { - it("should create x402MCPClient instance", () => { - const mockMcpClient = createMockMCPClient(); - const mockPaymentClient = createMockPaymentClient(); - - const client = wrapMCPClientWithPayment( - mockMcpClient as unknown as Parameters[0], - mockPaymentClient as unknown as Parameters[1], - ); - - expect(client).toBeInstanceOf(x402MCPClient); - }); -}); - -describe("createx402MCPClient", () => { - it("should create client with config", () => { - const mockSchemeClient = { - createPaymentPayload: vi.fn(), - }; - - const client = createx402MCPClient({ - name: "test-client", - version: "1.0.0", - schemes: [ - { - network: "eip155:84532", - client: mockSchemeClient as unknown as Parameters< - typeof createx402MCPClient - >[0]["schemes"][0]["client"], - }, - ], - }); - - expect(client).toBeInstanceOf(x402MCPClient); - }); -}); - -// ============================================================================ -// Response Format Interoperability Tests -// ============================================================================ - -describe("x402MCPClient response format interoperability", () => { - let mockMcpClient: MockMCPClient; - let mockPaymentClient: MockPaymentClient; - let client: x402MCPClient; - - beforeEach(() => { - mockMcpClient = createMockMCPClient(); - mockPaymentClient = createMockPaymentClient(); - client = new x402MCPClient( - mockMcpClient as unknown as Parameters[0], - mockPaymentClient as unknown as Parameters[1], - ); - }); - - describe("structuredContent formats", () => { - it("should parse structuredContent with direct PaymentRequired V2", async () => { - mockMcpClient.callTool - .mockResolvedValueOnce(createStructuredContentDirectPaymentError(mockPaymentRequired)) - .mockResolvedValueOnce({ - content: [{ type: "text", text: "success" }], - _meta: { "x402/payment-response": mockSettleResponse }, - }); - - const result = await client.callTool("paid_tool"); - - expect(result.paymentMade).toBe(true); - expect(mockPaymentClient.createPaymentPayload).toHaveBeenCalledWith(mockPaymentRequired); - }); - - it("should parse structuredContent with direct PaymentRequired V1 (ethanniser/x402-mcp style)", async () => { - mockMcpClient.callTool - .mockResolvedValueOnce(createStructuredContentDirectPaymentError(mockPaymentRequiredV1)) - .mockResolvedValueOnce({ - content: [{ type: "text", text: "success" }], - _meta: { "x402/payment-response": mockSettleResponse }, - }); - - const result = await client.callTool("paid_tool"); - - expect(result.paymentMade).toBe(true); - expect(mockPaymentClient.createPaymentPayload).toHaveBeenCalledWith(mockPaymentRequiredV1); - }); - }); - - describe("content fallback formats", () => { - it("should parse content with direct PaymentRequired V2", async () => { - mockMcpClient.callTool - .mockResolvedValueOnce(createContentDirectPaymentError(mockPaymentRequired)) - .mockResolvedValueOnce({ - content: [{ type: "text", text: "success" }], - _meta: { "x402/payment-response": mockSettleResponse }, - }); - - const result = await client.callTool("paid_tool"); - - expect(result.paymentMade).toBe(true); - expect(mockPaymentClient.createPaymentPayload).toHaveBeenCalledWith(mockPaymentRequired); - }); - - it("should parse content with direct PaymentRequired V1 (no wrapper)", async () => { - mockMcpClient.callTool - .mockResolvedValueOnce(createContentDirectPaymentError(mockPaymentRequiredV1)) - .mockResolvedValueOnce({ - content: [{ type: "text", text: "success" }], - _meta: { "x402/payment-response": mockSettleResponse }, - }); - - const result = await client.callTool("paid_tool"); - - expect(result.paymentMade).toBe(true); - expect(mockPaymentClient.createPaymentPayload).toHaveBeenCalledWith(mockPaymentRequiredV1); - }); - }); - - describe("priority order", () => { - it("should prefer structuredContent over content fallback", async () => { - const contentFallbackPaymentRequired = { - ...mockPaymentRequired, - error: "From content fallback", - }; - const mixedResponse = { - structuredContent: mockPaymentRequired as Record, - content: [ - { - type: "text" as const, - text: JSON.stringify(contentFallbackPaymentRequired), - }, - ], - isError: true as const, - }; - - mockMcpClient.callTool.mockResolvedValueOnce(mixedResponse).mockResolvedValueOnce({ - content: [{ type: "text", text: "success" }], - _meta: { "x402/payment-response": mockSettleResponse }, - }); - - const result = await client.callTool("paid_tool"); - - expect(result.paymentMade).toBe(true); - // Should use the structuredContent version (original mockPaymentRequired) - expect(mockPaymentClient.createPaymentPayload).toHaveBeenCalledWith(mockPaymentRequired); - }); - }); - - describe("getToolPaymentRequirements with different formats", () => { - it("should extract requirements from structuredContent format", async () => { - mockMcpClient.callTool.mockResolvedValue( - createStructuredContentDirectPaymentError(mockPaymentRequired), - ); - - const result = await client.getToolPaymentRequirements("paid_tool"); - - expect(result).toEqual(mockPaymentRequired); - }); - - it("should extract V1 requirements from structuredContent format", async () => { - mockMcpClient.callTool.mockResolvedValue( - createStructuredContentDirectPaymentError(mockPaymentRequiredV1), - ); - - const result = await client.getToolPaymentRequirements("paid_tool"); - - expect(result).toEqual(mockPaymentRequiredV1); - }); - }); -}); - -// ============================================================================ -// onPaymentRequired Hook Tests -// ============================================================================ - -describe("x402MCPClient onPaymentRequired hook", () => { - let mockMcpClient: ReturnType; - let mockPaymentClient: ReturnType; - let client: x402MCPClient; - - beforeEach(() => { - mockMcpClient = createMockMCPClient(); - mockPaymentClient = createMockPaymentClient(); - - client = new x402MCPClient( - mockMcpClient as unknown as ConstructorParameters[0], - mockPaymentClient as unknown as ConstructorParameters[1], - { autoPayment: true }, - ); - }); - - it("should call hook when payment required is received", async () => { - const hook = vi.fn(); - client.onPaymentRequired(hook); - - // First call returns 402 - mockMcpClient.callTool.mockResolvedValueOnce(createEmbeddedPaymentError(mockPaymentRequired)); - // Second call (with payment) returns success - mockMcpClient.callTool.mockResolvedValueOnce({ - content: [{ type: "text", text: "result" }], - _meta: { "x402/payment-response": mockSettleResponse }, - }); - - await client.callTool("tool", { arg: "value" }); - - expect(hook).toHaveBeenCalledWith( - expect.objectContaining({ - toolName: "tool", - arguments: { arg: "value" }, - paymentRequired: mockPaymentRequired, - }), - ); - }); - - it("should use hook-provided payment instead of auto-generating", async () => { - const customPayment = { - ...mockPaymentPayload, - payload: { ...mockPaymentPayload.payload, signature: "0xcustom" }, - }; - client.onPaymentRequired(() => ({ payment: customPayment })); - - // First call returns 402 - mockMcpClient.callTool.mockResolvedValueOnce(createEmbeddedPaymentError(mockPaymentRequired)); - // Second call (with payment) returns success - mockMcpClient.callTool.mockResolvedValueOnce({ - content: [{ type: "text", text: "result" }], - _meta: { "x402/payment-response": mockSettleResponse }, - }); - - await client.callTool("tool", {}); - - // Should not auto-generate payment - expect(mockPaymentClient.createPaymentPayload).not.toHaveBeenCalled(); - - // Should use custom payment - const callArgs = mockMcpClient.callTool.mock.calls[1][0]; - expect(callArgs._meta?.[MCP_PAYMENT_META_KEY]).toEqual(customPayment); - }); - - it("should abort payment when hook returns abort: true", async () => { - client.onPaymentRequired(() => ({ abort: true })); - - mockMcpClient.callTool.mockResolvedValueOnce(createEmbeddedPaymentError(mockPaymentRequired)); - - await expect(client.callTool("tool", {})).rejects.toThrow("Payment aborted by hook"); - }); - - it("should continue to next hook if first returns void", async () => { - const hook1 = vi.fn(); - const hook2 = vi.fn().mockReturnValue({ abort: true }); - - client.onPaymentRequired(hook1).onPaymentRequired(hook2); - - mockMcpClient.callTool.mockResolvedValueOnce(createEmbeddedPaymentError(mockPaymentRequired)); - - await expect(client.callTool("tool", {})).rejects.toThrow("Payment aborted by hook"); - - expect(hook1).toHaveBeenCalled(); - expect(hook2).toHaveBeenCalled(); - }); - - it("should return this for method chaining", () => { - const result = client.onPaymentRequired(() => {}); - expect(result).toBe(client); - }); -}); diff --git a/typescript/packages/mcp/test/unit/server.test.ts b/typescript/packages/mcp/test/unit/server.test.ts deleted file mode 100644 index 69fb01b..0000000 --- a/typescript/packages/mcp/test/unit/server.test.ts +++ /dev/null @@ -1,460 +0,0 @@ -/** - * Unit tests for createPaymentWrapper - */ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { createPaymentWrapper } from "../../src/server"; -import { MCP_PAYMENT_RESPONSE_META_KEY } from "../../src/types"; -import type { - PaymentPayload, - PaymentRequirements, - SettleResponse, - VerifyResponse, -} from "@x402/core/types"; - -// ============================================================================ -// Mock Types -// ============================================================================ - -interface MockResourceServer { - verifyPayment: ReturnType; - settlePayment: ReturnType; - createPaymentRequiredResponse: ReturnType; -} - -// ============================================================================ -// Test Fixtures -// ============================================================================ - -const mockPaymentRequirements: PaymentRequirements = { - scheme: "exact", - network: "eip155:84532", - amount: "1000", - asset: "0xtoken", - payTo: "0xrecipient", - maxTimeoutSeconds: 60, - extra: {}, -}; - -const mockPaymentPayload: PaymentPayload = { - x402Version: 2, - payload: { - signature: "0x123", - authorization: { - from: "0xabc", - to: "0xdef", - value: "1000", - validAfter: 0, - validBefore: Math.floor(Date.now() / 1000) + 3600, - nonce: "0x1", - }, - }, -}; - -const mockVerifyResponse: VerifyResponse = { - isValid: true, -}; - -const mockSettleResponse: SettleResponse = { - success: true, - transaction: "0xtxhash123", - network: "eip155:84532", -}; - -const mockPaymentRequired = { - x402Version: 2, - accepts: [mockPaymentRequirements], - error: "Payment required", - resource: { - url: "mcp://tool/test", - description: "Test tool", - mimeType: "application/json", - }, -}; - -// ============================================================================ -// Mock Factory -// ============================================================================ - -/** - * Creates a mock resource server for testing - * - * @returns Mock resource server instance - */ -function createMockResourceServer(): MockResourceServer { - return { - verifyPayment: vi.fn().mockResolvedValue(mockVerifyResponse), - settlePayment: vi.fn().mockResolvedValue(mockSettleResponse), - createPaymentRequiredResponse: vi.fn().mockResolvedValue(mockPaymentRequired), - }; -} - -// ============================================================================ -// createPaymentWrapper Tests -// ============================================================================ - -describe("createPaymentWrapper", () => { - let mockResourceServer: MockResourceServer; - - beforeEach(() => { - mockResourceServer = createMockResourceServer(); - }); - - describe("basic payment flow", () => { - it("should require payment when no payment provided", async () => { - const paid = createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - { - accepts: [mockPaymentRequirements], - }, - ); - - const handler = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "success" }], - }); - - const wrappedHandler = paid(handler); - const result = await wrappedHandler({ test: "arg" }, {}); - - expect(result.isError).toBe(true); - expect(result.structuredContent).toEqual(mockPaymentRequired); - expect(handler).not.toHaveBeenCalled(); - }); - - it("should verify payment and execute tool when payment provided", async () => { - const paid = createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - { - accepts: [mockPaymentRequirements], - }, - ); - - const handler = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "success" }], - }); - - const wrappedHandler = paid(handler); - const result = await wrappedHandler( - { test: "arg" }, - { _meta: { "x402/payment": mockPaymentPayload } }, - ); - - expect(mockResourceServer.verifyPayment).toHaveBeenCalledWith( - mockPaymentPayload, - mockPaymentRequirements, - ); - expect(handler).toHaveBeenCalled(); - expect(result.content).toEqual([{ type: "text", text: "success" }]); - expect(result._meta?.[MCP_PAYMENT_RESPONSE_META_KEY]).toEqual(mockSettleResponse); - }); - - it("should settle payment after successful execution", async () => { - const paid = createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - { - accepts: [mockPaymentRequirements], - }, - ); - - const handler = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "success" }], - }); - - const wrappedHandler = paid(handler); - await wrappedHandler({ test: "arg" }, { _meta: { "x402/payment": mockPaymentPayload } }); - - expect(mockResourceServer.settlePayment).toHaveBeenCalledWith( - mockPaymentPayload, - mockPaymentRequirements, - ); - }); - - it("should not settle payment if tool returns error", async () => { - const paid = createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - { - accepts: [mockPaymentRequirements], - }, - ); - - const handler = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "error" }], - isError: true, - }); - - const wrappedHandler = paid(handler); - const result = await wrappedHandler( - { test: "arg" }, - { _meta: { "x402/payment": mockPaymentPayload } }, - ); - - expect(result.isError).toBe(true); - expect(mockResourceServer.settlePayment).not.toHaveBeenCalled(); - }); - - it("should return 402 if payment verification fails", async () => { - mockResourceServer.verifyPayment.mockResolvedValueOnce({ - isValid: false, - invalidReason: "Insufficient funds", - }); - - const paid = createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - { - accepts: [mockPaymentRequirements], - }, - ); - - const handler = vi.fn(); - const wrappedHandler = paid(handler); - const result = await wrappedHandler( - { test: "arg" }, - { _meta: { "x402/payment": mockPaymentPayload } }, - ); - - expect(result.isError).toBe(true); - expect(result.structuredContent).toEqual(mockPaymentRequired); - expect(handler).not.toHaveBeenCalled(); - }); - }); - - describe("accepts array validation", () => { - it("should throw error if accepts array is empty", () => { - expect(() => - createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - { - accepts: [], - }, - ), - ).toThrow("PaymentWrapperConfig.accepts must have at least one payment requirement"); - }); - - it("should throw error if accepts is not provided", () => { - expect(() => - createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - {} as Parameters[1], - ), - ).toThrow("PaymentWrapperConfig.accepts must have at least one payment requirement"); - }); - }); - - describe("hooks", () => { - it("should call onBeforeExecution hook before tool execution", async () => { - const beforeHook = vi.fn().mockResolvedValue(true); - const paid = createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - { - accepts: [mockPaymentRequirements], - hooks: { - onBeforeExecution: beforeHook, - }, - }, - ); - - const handler = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "success" }], - }); - - const wrappedHandler = paid(handler); - await wrappedHandler({ test: "arg" }, { _meta: { "x402/payment": mockPaymentPayload } }); - - expect(beforeHook).toHaveBeenCalledWith( - expect.objectContaining({ - toolName: expect.any(String), - arguments: { test: "arg" }, - paymentPayload: mockPaymentPayload, - paymentRequirements: mockPaymentRequirements, - }), - ); - expect(handler).toHaveBeenCalled(); - }); - - it("should abort execution when onBeforeExecution returns false", async () => { - const beforeHook = vi.fn().mockResolvedValue(false); - const paid = createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - { - accepts: [mockPaymentRequirements], - hooks: { - onBeforeExecution: beforeHook, - }, - }, - ); - - const handler = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "success" }], - }); - - const wrappedHandler = paid(handler); - const result = await wrappedHandler( - { test: "arg" }, - { _meta: { "x402/payment": mockPaymentPayload } }, - ); - - expect(beforeHook).toHaveBeenCalled(); - expect(handler).not.toHaveBeenCalled(); - expect(result.isError).toBe(true); - expect(result.structuredContent).toBeDefined(); - }); - - it("should call onAfterExecution hook after tool execution", async () => { - const afterHook = vi.fn(); - const paid = createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - { - accepts: [mockPaymentRequirements], - hooks: { - onAfterExecution: afterHook, - }, - }, - ); - - const handler = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "success" }], - }); - - const wrappedHandler = paid(handler); - await wrappedHandler({ test: "arg" }, { _meta: { "x402/payment": mockPaymentPayload } }); - - expect(afterHook).toHaveBeenCalledWith( - expect.objectContaining({ - toolName: expect.any(String), - arguments: { test: "arg" }, - paymentPayload: mockPaymentPayload, - paymentRequirements: mockPaymentRequirements, - result: expect.objectContaining({ - content: [{ type: "text", text: "success" }], - }), - }), - ); - }); - - it("should call onAfterSettlement hook after successful settlement", async () => { - const settlementHook = vi.fn(); - const paid = createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - { - accepts: [mockPaymentRequirements], - hooks: { - onAfterSettlement: settlementHook, - }, - }, - ); - - const handler = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "success" }], - }); - - const wrappedHandler = paid(handler); - await wrappedHandler({ test: "arg" }, { _meta: { "x402/payment": mockPaymentPayload } }); - - expect(settlementHook).toHaveBeenCalledWith( - expect.objectContaining({ - toolName: expect.any(String), - arguments: { test: "arg" }, - paymentPayload: mockPaymentPayload, - paymentRequirements: mockPaymentRequirements, - settlement: mockSettleResponse, - }), - ); - }); - - it("should call all hooks in correct order", async () => { - const callOrder: string[] = []; - const beforeHook = vi.fn(async () => { - callOrder.push("before"); - return true; - }); - const afterHook = vi.fn(async () => { - callOrder.push("after"); - }); - const settlementHook = vi.fn(async () => { - callOrder.push("settlement"); - }); - - const paid = createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - { - accepts: [mockPaymentRequirements], - hooks: { - onBeforeExecution: beforeHook, - onAfterExecution: afterHook, - onAfterSettlement: settlementHook, - }, - }, - ); - - const handler = vi.fn(async () => { - callOrder.push("handler"); - return { content: [{ type: "text", text: "success" }] }; - }); - - const wrappedHandler = paid(handler); - await wrappedHandler({ test: "arg" }, { _meta: { "x402/payment": mockPaymentPayload } }); - - expect(callOrder).toEqual(["before", "handler", "after", "settlement"]); - }); - }); - - describe("multiple payment requirements", () => { - it("should use first payment requirement from accepts array", async () => { - const alternateRequirements: PaymentRequirements = { - scheme: "subscription", - network: "eip155:1", - amount: "5000", - asset: "0xalternate", - payTo: "0xalt", - maxTimeoutSeconds: 120, - extra: {}, - }; - - const paid = createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - { - accepts: [mockPaymentRequirements, alternateRequirements], - }, - ); - - const handler = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "success" }], - }); - - const wrappedHandler = paid(handler); - await wrappedHandler({ test: "arg" }, { _meta: { "x402/payment": mockPaymentPayload } }); - - // Should verify with first requirement - expect(mockResourceServer.verifyPayment).toHaveBeenCalledWith( - mockPaymentPayload, - mockPaymentRequirements, - ); - }); - }); - - describe("settlement failures", () => { - it("should return 402 error when settlement fails", async () => { - mockResourceServer.settlePayment.mockRejectedValueOnce(new Error("Settlement failed")); - - const paid = createPaymentWrapper( - mockResourceServer as unknown as Parameters[0], - { - accepts: [mockPaymentRequirements], - }, - ); - - const handler = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "success" }], - }); - - const wrappedHandler = paid(handler); - const result = await wrappedHandler( - { test: "arg" }, - { _meta: { "x402/payment": mockPaymentPayload } }, - ); - - expect(handler).toHaveBeenCalled(); // Handler executed - expect(result.isError).toBe(true); // But error returned due to settlement failure - expect(result.structuredContent).toBeDefined(); - }); - }); -}); diff --git a/typescript/packages/mcp/test/unit/utils.test.ts b/typescript/packages/mcp/test/unit/utils.test.ts deleted file mode 100644 index 80ba8dd..0000000 --- a/typescript/packages/mcp/test/unit/utils.test.ts +++ /dev/null @@ -1,318 +0,0 @@ -/** - * Unit tests for MCP utils module - */ -import { describe, it, expect } from "vitest"; -import { - isObject, - extractPaymentFromMeta, - attachPaymentToMeta, - extractPaymentResponseFromMeta, - attachPaymentResponseToMeta, - createPaymentRequiredError, - extractPaymentRequiredFromError, - createToolResourceUrl, -} from "../../src/utils/encoding"; -import { MCP_PAYMENT_META_KEY, MCP_PAYMENT_RESPONSE_META_KEY } from "../../src/types"; -import type { PaymentPayload, PaymentRequired, SettleResponse } from "@x402/core/types"; - -// ============================================================================ -// Test Fixtures -// ============================================================================ - -const mockPaymentPayload: PaymentPayload = { - x402Version: 2, - payload: { - signature: "0x123", - authorization: { - from: "0xabc", - to: "0xdef", - value: "1000", - validAfter: 0, - validBefore: Math.floor(Date.now() / 1000) + 3600, - nonce: "0x1", - }, - }, -}; - -const mockSettleResponse: SettleResponse = { - success: true, - transaction: "0xtxhash123", - network: "eip155:84532", -}; - -const mockPaymentRequired: PaymentRequired = { - x402Version: 2, - accepts: [ - { - scheme: "exact", - network: "eip155:84532", - amount: "1000", - asset: "0xtoken", - payTo: "0xrecipient", - maxAmountRequired: "1000", - extra: {}, - }, - ], - error: "Payment required", - resource: { - url: "mcp://tool/test", - description: "Test tool", - mimeType: "application/json", - }, -}; - -// ============================================================================ -// isObject Tests -// ============================================================================ - -describe("isObject", () => { - it("should return true for plain objects", () => { - expect(isObject({})).toBe(true); - expect(isObject({ key: "value" })).toBe(true); - expect(isObject({ nested: { object: true } })).toBe(true); - }); - - it("should return true for arrays", () => { - // Arrays are objects in JavaScript - expect(isObject([])).toBe(true); - expect(isObject([1, 2, 3])).toBe(true); - }); - - it("should return false for null", () => { - expect(isObject(null)).toBe(false); - }); - - it("should return false for primitives", () => { - expect(isObject(undefined)).toBe(false); - expect(isObject(42)).toBe(false); - expect(isObject("string")).toBe(false); - expect(isObject(true)).toBe(false); - expect(isObject(Symbol("test"))).toBe(false); - }); -}); - -// ============================================================================ -// extractPaymentFromMeta Tests -// ============================================================================ - -describe("extractPaymentFromMeta", () => { - it("should extract valid payment payload from _meta", () => { - const params = { - name: "test_tool", - arguments: {}, - _meta: { - [MCP_PAYMENT_META_KEY]: mockPaymentPayload, - }, - }; - - const result = extractPaymentFromMeta(params); - expect(result).toEqual(mockPaymentPayload); - }); - - it("should return null if _meta is missing", () => { - const params = { - name: "test_tool", - arguments: {}, - }; - - expect(extractPaymentFromMeta(params)).toBeNull(); - }); - - it("should return null if payment key is missing", () => { - const params = { - name: "test_tool", - arguments: {}, - _meta: {}, - }; - - expect(extractPaymentFromMeta(params)).toBeNull(); - }); - - it("should return null if params is undefined", () => { - expect(extractPaymentFromMeta(undefined)).toBeNull(); - }); - - it("should return null if payment structure is invalid", () => { - const params = { - name: "test_tool", - _meta: { - [MCP_PAYMENT_META_KEY]: { invalid: "structure" }, - }, - }; - - expect(extractPaymentFromMeta(params)).toBeNull(); - }); -}); - -// ============================================================================ -// attachPaymentToMeta Tests -// ============================================================================ - -describe("attachPaymentToMeta", () => { - it("should attach payment payload to params", () => { - const params = { name: "test_tool", arguments: { arg: "value" } }; - - const result = attachPaymentToMeta(params, mockPaymentPayload); - - expect(result.name).toBe("test_tool"); - expect(result.arguments).toEqual({ arg: "value" }); - expect(result._meta?.[MCP_PAYMENT_META_KEY]).toEqual(mockPaymentPayload); - }); - - it("should work with empty arguments", () => { - const params = { name: "test_tool" }; - - const result = attachPaymentToMeta(params, mockPaymentPayload); - - expect(result._meta?.[MCP_PAYMENT_META_KEY]).toEqual(mockPaymentPayload); - }); -}); - -// ============================================================================ -// extractPaymentResponseFromMeta Tests -// ============================================================================ - -describe("extractPaymentResponseFromMeta", () => { - it("should extract valid settle response from _meta", () => { - const result = { - content: [{ type: "text", text: "result" }], - _meta: { - [MCP_PAYMENT_RESPONSE_META_KEY]: mockSettleResponse, - }, - }; - - const response = extractPaymentResponseFromMeta(result); - expect(response).toEqual(mockSettleResponse); - }); - - it("should return null if _meta is missing", () => { - const result = { - content: [{ type: "text", text: "result" }], - }; - - expect(extractPaymentResponseFromMeta(result)).toBeNull(); - }); - - it("should return null if result is undefined", () => { - expect(extractPaymentResponseFromMeta(undefined)).toBeNull(); - }); - - it("should return null if response structure is invalid", () => { - const result = { - content: [], - _meta: { - [MCP_PAYMENT_RESPONSE_META_KEY]: { invalid: "structure" }, - }, - }; - - expect(extractPaymentResponseFromMeta(result)).toBeNull(); - }); -}); - -// ============================================================================ -// attachPaymentResponseToMeta Tests -// ============================================================================ - -describe("attachPaymentResponseToMeta", () => { - it("should attach settle response to result", () => { - const result = { - content: [{ type: "text" as const, text: "result" }], - isError: false, - }; - - const withMeta = attachPaymentResponseToMeta(result, mockSettleResponse); - - expect(withMeta.content).toEqual(result.content); - expect(withMeta.isError).toBe(false); - expect(withMeta._meta?.[MCP_PAYMENT_RESPONSE_META_KEY]).toEqual(mockSettleResponse); - }); -}); - -// ============================================================================ -// createPaymentRequiredError Tests -// ============================================================================ - -describe("createPaymentRequiredError", () => { - it("should create error with default message", () => { - const error = createPaymentRequiredError(mockPaymentRequired); - - expect(error.code).toBe(402); - expect(error.message).toBe("Payment required"); - expect(error.data).toEqual(mockPaymentRequired); - }); - - it("should create error with custom message", () => { - const error = createPaymentRequiredError(mockPaymentRequired, "Custom error message"); - - expect(error.code).toBe(402); - expect(error.message).toBe("Custom error message"); - expect(error.data).toEqual(mockPaymentRequired); - }); -}); - -// ============================================================================ -// extractPaymentRequiredFromError Tests -// ============================================================================ - -describe("extractPaymentRequiredFromError", () => { - it("should extract PaymentRequired from valid error", () => { - const error = { - code: 402, - message: "Payment required", - data: mockPaymentRequired, - }; - - const result = extractPaymentRequiredFromError(error); - expect(result).toEqual(mockPaymentRequired); - }); - - it("should return null for non-402 error code", () => { - const error = { - code: 500, - message: "Server error", - data: mockPaymentRequired, - }; - - expect(extractPaymentRequiredFromError(error)).toBeNull(); - }); - - it("should return null for null error", () => { - expect(extractPaymentRequiredFromError(null)).toBeNull(); - }); - - it("should return null for non-object error", () => { - expect(extractPaymentRequiredFromError("error")).toBeNull(); - expect(extractPaymentRequiredFromError(42)).toBeNull(); - }); - - it("should return null if data is missing x402 fields", () => { - const error = { - code: 402, - message: "Payment required", - data: { invalid: "structure" }, - }; - - expect(extractPaymentRequiredFromError(error)).toBeNull(); - }); -}); - -// ============================================================================ -// createToolResourceUrl Tests -// ============================================================================ - -describe("createToolResourceUrl", () => { - it("should return custom URL if provided", () => { - const url = createToolResourceUrl("test_tool", "https://custom.url/tool"); - expect(url).toBe("https://custom.url/tool"); - }); - - it("should generate default mcp:// URL", () => { - const url = createToolResourceUrl("test_tool"); - expect(url).toBe("mcp://tool/test_tool"); - }); - - it("should handle empty custom URL", () => { - const url = createToolResourceUrl("test_tool", ""); - expect(url).toBe("mcp://tool/test_tool"); - }); -}); diff --git a/typescript/packages/mechanisms/aptos/.prettierignore b/typescript/packages/mechanisms/aptos/.prettierignore new file mode 100644 index 0000000..0510cef --- /dev/null +++ b/typescript/packages/mechanisms/aptos/.prettierignore @@ -0,0 +1,3 @@ +# build output +dist/ +node_modules/ diff --git a/typescript/packages/mechanisms/aptos/.prettierrc b/typescript/packages/mechanisms/aptos/.prettierrc new file mode 100644 index 0000000..ffb416b --- /dev/null +++ b/typescript/packages/mechanisms/aptos/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/packages/mechanisms/aptos/CHANGELOG.md b/typescript/packages/mechanisms/aptos/CHANGELOG.md new file mode 100644 index 0000000..daf1521 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/CHANGELOG.md @@ -0,0 +1,71 @@ +# @x402/aptos + +## 2.9.0 + +### Minor Changes + +- 2250cae: Migrated project from coinbase/x402 to x402-foundation/x402 organization + +### Patch Changes + +- Updated dependencies [8cf3fca] +- Updated dependencies [c0e3969] +- Updated dependencies [2250cae] +- Updated dependencies [d352574] + - @x402/core@2.9.0 + +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + +## 2.5.0 + +### Patch Changes + +- Updated dependencies [96a9db0] +- Updated dependencies [d0a2b11] +- Updated dependencies + - @x402/core@2.5.0 + +## 2.4.0 + +### Minor Changes + +- 57a5488: Add Aptos blockchain support to x402 payment protocol + + - Introduces new `@x402/aptos` package with full client, server, and facilitator scheme implementations + - Supports exact payment mechanism for Aptos using native APT and fungible assets + - Includes sponsored transaction support where facilitator pays gas fees + - Provides `registerExactAptosScheme` helpers for easy client and server integration + - Adds Aptos network constants for mainnet and testnet + - Updates core types to support Aptos-specific payment flows + +### Patch Changes + +- Updated dependencies [57a5488] +- Updated dependencies [018181b] +- Updated dependencies [3fb55d7] + - @x402/core@2.4.0 diff --git a/typescript/packages/mechanisms/aptos/README.md b/typescript/packages/mechanisms/aptos/README.md new file mode 100644 index 0000000..c8ed626 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/README.md @@ -0,0 +1,74 @@ +# @x402/aptos + +Aptos implementation of the x402 payment protocol. + +## Installation + +```bash +npm install @x402/aptos +# or +pnpm add @x402/aptos +``` + +## Usage + +### Client + +```typescript +import { Account, Ed25519PrivateKey } from "@aptos-labs/ts-sdk"; +import { ExactAptosScheme } from "@x402/aptos/exact/client"; + +// Create signer from private key +const privateKey = new Ed25519PrivateKey("0x..."); +const account = Account.fromPrivateKey({ privateKey }); + +// Register scheme with client +client.register("aptos:*", new ExactAptosScheme(account)); +``` + +### Facilitator + +```typescript +import { Account, Ed25519PrivateKey } from "@aptos-labs/ts-sdk"; +import { ExactAptosScheme } from "@x402/aptos/exact/facilitator"; +import { toFacilitatorAptosSigner } from "@x402/aptos"; + +// Create facilitator signer +const privateKey = new Ed25519PrivateKey("0x..."); +const account = Account.fromPrivateKey({ privateKey }); +const signer = toFacilitatorAptosSigner(account); + +// Register scheme with facilitator +facilitator.register("aptos:2", new ExactAptosScheme(signer)); +``` + +### Server + +```typescript +import { x402ResourceServer } from "@x402/core/server"; +import { ExactAptosScheme } from "@x402/aptos/exact/server"; + +// Create and configure server +const server = new x402ResourceServer({ facilitatorUrl: "https://..." }); +server.register("aptos:*", new ExactAptosScheme()); + +// Use parsePrice to convert amounts (e.g., "$1.00" or { amount: "1000000", asset: "0x..." }) +// The scheme handles USDC conversion automatically +``` + +## Features + +- **Sponsored Transactions**: Facilitators can pay gas fees on behalf of clients +- **Fungible Asset Transfers**: Uses Aptos's native `primary_fungible_store::transfer` +- **Network Support**: Mainnet (`aptos:1`) and Testnet (`aptos:2`) + +## Testnet Resources + +For testing on Aptos testnet, you can obtain test tokens from these faucets: + +- **Test APT**: https://aptos.dev/network/faucet or through an account on [geomi.dev](https://geomi.dev/manage/faucet) +- **Test USDC**: https://faucet.circle.com/ + +## License + +Apache-2.0 diff --git a/typescript/packages/mechanisms/aptos/eslint.config.js b/typescript/packages/mechanisms/aptos/eslint.config.js new file mode 100644 index 0000000..d3ffb66 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/eslint.config.js @@ -0,0 +1,92 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts", "**/*.tsx"], + ignores: ["**/*.test.ts", "test/**/*"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + console: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, + { + files: ["**/*.test.ts", "test/**/*"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + }, + rules: { + "prettier/prettier": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/member-ordering": "off", + }, + }, +]; diff --git a/typescript/packages/mechanisms/aptos/package.json b/typescript/packages/mechanisms/aptos/package.json new file mode 100644 index 0000000..c966552 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/package.json @@ -0,0 +1,95 @@ +{ + "name": "@x402/aptos", + "version": "2.9.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/cjs/index.d.ts", + "scripts": { + "start": "tsx --env-file=.env index.ts", + "build": "tsup", + "test": "vitest run", + "test:integration": "vitest run --config vitest.integration.config.ts", + "test:watch": "vitest", + "watch": "tsc --watch", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "keywords": [ + "x402", + "payment", + "protocol", + "aptos" + ], + "license": "Apache-2.0", + "author": "Aptos Labs", + "repository": "https://github.com/x402-foundation/x402", + "description": "x402 Payment Protocol Aptos Implementation", + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/node": "^22.13.4", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsup": "^8.4.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vite": "^6.2.6", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.5" + }, + "dependencies": { + "@x402/core": "workspace:*", + "@aptos-labs/ts-sdk": "^5.2.1" + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./exact/client": { + "import": { + "types": "./dist/esm/exact/client/index.d.mts", + "default": "./dist/esm/exact/client/index.mjs" + }, + "require": { + "types": "./dist/cjs/exact/client/index.d.ts", + "default": "./dist/cjs/exact/client/index.js" + } + }, + "./exact/server": { + "import": { + "types": "./dist/esm/exact/server/index.d.mts", + "default": "./dist/esm/exact/server/index.mjs" + }, + "require": { + "types": "./dist/cjs/exact/server/index.d.ts", + "default": "./dist/cjs/exact/server/index.js" + } + }, + "./exact/facilitator": { + "import": { + "types": "./dist/esm/exact/facilitator/index.d.mts", + "default": "./dist/esm/exact/facilitator/index.mjs" + }, + "require": { + "types": "./dist/cjs/exact/facilitator/index.d.ts", + "default": "./dist/cjs/exact/facilitator/index.js" + } + } + }, + "files": [ + "dist" + ] +} diff --git a/typescript/packages/mechanisms/aptos/src/constants.ts b/typescript/packages/mechanisms/aptos/src/constants.ts new file mode 100644 index 0000000..bf5804e --- /dev/null +++ b/typescript/packages/mechanisms/aptos/src/constants.ts @@ -0,0 +1,82 @@ +import { Network, NetworkToNodeAPI } from "@aptos-labs/ts-sdk"; + +/** + * CAIP-2 network identifier for Aptos Mainnet + */ +export const APTOS_MAINNET_CAIP2 = "aptos:1"; + +/** + * CAIP-2 network identifier for Aptos Testnet + */ +export const APTOS_TESTNET_CAIP2 = "aptos:2"; + +/** + * Regex pattern for validating Aptos addresses + * Matches 64 hex characters with 0x prefix + */ +export const APTOS_ADDRESS_REGEX = /^0x[a-fA-F0-9]{64}$/; + +/** + * The primary fungible store transfer function + */ +export const TRANSFER_FUNCTION = "0x1::primary_fungible_store::transfer"; + +/** + * Maximum gas amount allowed for sponsored transactions to prevent gas draining attacks. + * The Aptos SDK defaults to 200000 for simple transactions, so we allow some headroom. + */ +export const MAX_GAS_AMOUNT = 500000n; + +/** + * Maps CAIP-2 network identifiers to Aptos chain IDs. + * + * @param network - The CAIP-2 network identifier (e.g., "aptos:1") + * @returns The corresponding chain ID + */ +export function getAptosChainId(network: string): number { + switch (network) { + case APTOS_MAINNET_CAIP2: + return 1; + case APTOS_TESTNET_CAIP2: + return 2; + default: + throw new Error(`Unsupported Aptos network: ${network}`); + } +} + +/** + * Default USDC fungible asset metadata address on mainnet. + */ +export const USDC_MAINNET_FA = "0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b"; + +/** + * Default USDC fungible asset metadata address on testnet. + */ +export const USDC_TESTNET_FA = "0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832"; + +/** + * Maps CAIP-2 network identifiers to Aptos SDK Network enum. + * + * @param network - The CAIP-2 network identifier (e.g., "aptos:1") + * @returns The corresponding Aptos SDK Network enum value + */ +export function getAptosNetwork(network: string): Network { + switch (network) { + case APTOS_MAINNET_CAIP2: + return Network.MAINNET; + case APTOS_TESTNET_CAIP2: + return Network.TESTNET; + default: + throw new Error(`Unsupported Aptos network: ${network}`); + } +} + +/** + * Gets the default RPC URL for the given Aptos network. + * + * @param network - The Aptos SDK Network enum value + * @returns The default RPC URL for the network + */ +export function getAptosRpcUrl(network: Network): string { + return NetworkToNodeAPI[network]; +} diff --git a/typescript/packages/mechanisms/aptos/src/exact/client/index.ts b/typescript/packages/mechanisms/aptos/src/exact/client/index.ts new file mode 100644 index 0000000..5bf3bc5 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/src/exact/client/index.ts @@ -0,0 +1 @@ +export { ExactAptosScheme } from "./scheme"; diff --git a/typescript/packages/mechanisms/aptos/src/exact/client/scheme.ts b/typescript/packages/mechanisms/aptos/src/exact/client/scheme.ts new file mode 100644 index 0000000..719b08b --- /dev/null +++ b/typescript/packages/mechanisms/aptos/src/exact/client/scheme.ts @@ -0,0 +1,102 @@ +import { AccountAddress, Aptos, AptosConfig, SimpleTransaction } from "@aptos-labs/ts-sdk"; +import type { PaymentPayload, PaymentRequirements, SchemeNetworkClient } from "@x402/core/types"; +import { APTOS_ADDRESS_REGEX, getAptosNetwork, getAptosRpcUrl } from "../../constants"; +import type { ClientAptosSigner, ClientAptosConfig } from "../../signer"; +import type { ExactAptosPayload } from "../../types"; +import { encodeAptosPayload } from "../../utils"; + +/** + * Aptos client implementation for the Exact payment scheme. + */ +export class ExactAptosScheme implements SchemeNetworkClient { + readonly scheme = "exact"; + + /** + * Creates a new ExactAptosScheme instance. + * + * @param signer - The Aptos account for signing transactions + * @param config - Optional configuration with custom RPC URL + */ + constructor( + private readonly signer: ClientAptosSigner, + private readonly config?: ClientAptosConfig, + ) {} + + /** + * Creates a payment payload for the Exact scheme. + * + * @param x402Version - The x402 protocol version + * @param paymentRequirements - The payment requirements + * @returns Promise resolving to a payment payload + */ + async createPaymentPayload( + x402Version: number, + paymentRequirements: PaymentRequirements, + ): Promise> { + if (!this.signer.accountAddress) { + throw new Error("Aptos account address is required"); + } + if (!paymentRequirements.asset) { + throw new Error("Asset is required"); + } + if (!paymentRequirements.asset.match(APTOS_ADDRESS_REGEX)) { + throw new Error("Invalid asset address"); + } + if (!paymentRequirements.payTo) { + throw new Error("Pay-to address is required"); + } + if (!paymentRequirements.payTo.match(APTOS_ADDRESS_REGEX)) { + throw new Error("Invalid pay-to address"); + } + if (!paymentRequirements.amount) { + throw new Error("Amount is required"); + } + if (!paymentRequirements.amount.match(/^[0-9]+$/)) { + throw new Error("Amount must be a number"); + } + + const aptosNetwork = getAptosNetwork(paymentRequirements.network); + const rpcUrl = this.config?.rpcUrl || getAptosRpcUrl(aptosNetwork); + const aptosConfig = new AptosConfig({ + network: aptosNetwork, + fullnode: rpcUrl, + }); + const aptos = new Aptos(aptosConfig); + + const feePayer = paymentRequirements.extra?.feePayer; + const isSponsored = typeof feePayer === "string"; + + const builtTransaction = await aptos.transaction.build.simple({ + sender: this.signer.accountAddress, + data: { + function: "0x1::primary_fungible_store::transfer", + typeArguments: ["0x1::fungible_asset::Metadata"], + functionArguments: [ + paymentRequirements.asset, + paymentRequirements.payTo, + paymentRequirements.amount, + ], + }, + withFeePayer: isSponsored, + }); + + // For sponsored transactions, set the actual fee payer address (SDK uses 0x0 placeholder) + const transaction = isSponsored + ? new SimpleTransaction(builtTransaction.rawTransaction, AccountAddress.from(feePayer)) + : builtTransaction; + + const senderAuthenticator = this.signer.signTransactionWithAuthenticator(transaction); + const transactionBytes = transaction.bcsToBytes(); + const authenticatorBytes = senderAuthenticator.bcsToBytes(); + const base64Transaction = encodeAptosPayload(transactionBytes, authenticatorBytes); + + const payload: ExactAptosPayload = { + transaction: base64Transaction, + }; + + return { + x402Version, + payload, + }; + } +} diff --git a/typescript/packages/mechanisms/aptos/src/exact/facilitator/index.ts b/typescript/packages/mechanisms/aptos/src/exact/facilitator/index.ts new file mode 100644 index 0000000..5bf3bc5 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/src/exact/facilitator/index.ts @@ -0,0 +1 @@ +export { ExactAptosScheme } from "./scheme"; diff --git a/typescript/packages/mechanisms/aptos/src/exact/facilitator/scheme.ts b/typescript/packages/mechanisms/aptos/src/exact/facilitator/scheme.ts new file mode 100644 index 0000000..89f4cd0 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/src/exact/facilitator/scheme.ts @@ -0,0 +1,370 @@ +import { AccountAddress, Deserializer, Ed25519PublicKey, PublicKey } from "@aptos-labs/ts-sdk"; +import type { + PaymentPayload, + PaymentRequirements, + SchemeNetworkFacilitator, + SettleResponse, + VerifyResponse, +} from "@x402/core/types"; +import type { FacilitatorAptosSigner } from "../../signer"; +import type { ExactAptosPayload } from "../../types"; +import { createAptosClient, deserializeAptosPayment } from "../../utils"; +import { getAptosChainId, MAX_GAS_AMOUNT } from "../../constants"; + +/** + * Aptos facilitator implementation for the Exact payment scheme. + */ +export class ExactAptosScheme implements SchemeNetworkFacilitator { + readonly scheme = "exact"; + readonly caipFamily = "aptos:*"; + + /** + * Creates a new ExactAptosFacilitator instance. + * + * @param signer - The Aptos facilitator signer for transaction submission + * @param sponsorTransactions - Whether to sponsor transactions (pay gas fees). Defaults to true. + */ + constructor( + private readonly signer: FacilitatorAptosSigner, + private readonly sponsorTransactions: boolean = true, + ) {} + + /** + * Get mechanism-specific extra data for the supported kinds endpoint. + * + * @param _ - The network identifier (unused) + * @returns Extra data with fee payer address, or undefined if sponsorship is disabled + */ + getExtra(_: string): Record | undefined { + if (!this.sponsorTransactions) { + return undefined; + } + const addresses = this.signer.getAddresses(); + const randomIndex = Math.floor(Math.random() * addresses.length); + return { feePayer: addresses[randomIndex] }; + } + + /** + * Get signer addresses used by this facilitator. + * + * @param _ - The network identifier (unused) + * @returns Array of fee payer addresses + */ + getSigners(_: string): string[] { + return [...this.signer.getAddresses()]; + } + + /** + * Verifies a payment payload. + * + * @param payload - The payment payload to verify + * @param requirements - The payment requirements + * @returns Promise resolving to verification response + */ + async verify( + payload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + try { + const aptosPayload = payload.payload as ExactAptosPayload; + const signerAddresses = this.signer.getAddresses(); + const isSponsored = typeof requirements.extra?.feePayer === "string"; + + // Step 2: Verify x402Version is 2 + if (payload.x402Version !== 2) { + return { + isValid: false, + invalidReason: "invalid_exact_aptos_payload_unsupported_version", + payer: "", + }; + } + + // Step 3: Verify the network matches + if (payload.accepted.scheme !== "exact" || requirements.scheme !== "exact") { + return { isValid: false, invalidReason: "unsupported_scheme", payer: "" }; + } + + if (payload.accepted.network !== requirements.network) { + return { isValid: false, invalidReason: "network_mismatch", payer: "" }; + } + + // If sponsored, verify the fee payer is managed by this facilitator + if (isSponsored && !signerAddresses.includes(requirements.extra.feePayer as string)) { + return { isValid: false, invalidReason: "fee_payer_not_managed_by_facilitator", payer: "" }; + } + + // Step 4: Deserialize the BCS-encoded transaction and verify the signature + const { transaction, senderAuthenticator, entryFunction } = deserializeAptosPayment( + aptosPayload.transaction, + ); + const senderAddress = transaction.rawTransaction.sender.toString(); + + // Verify chain ID matches expected network + const expectedChainId = getAptosChainId(requirements.network); + const txChainId = Number(transaction.rawTransaction.chain_id.chainId); + if (txChainId !== expectedChainId) { + return { + isValid: false, + invalidReason: `invalid_exact_aptos_payload_chain_id_mismatch: expected ${expectedChainId}, got ${txChainId}`, + payer: senderAddress, + }; + } + + // Verify sender matches authenticator public key (for Ed25519 accounts) + // Note: SingleKey and MultiKey authenticators are validated during simulation (step 11) + if (senderAuthenticator.isEd25519()) { + const pubKey = senderAuthenticator.public_key as Ed25519PublicKey; + const derivedAddress = AccountAddress.from(pubKey.authKey().derivedAddress()); + if (!derivedAddress.equals(transaction.rawTransaction.sender)) { + return { + isValid: false, + invalidReason: "invalid_exact_aptos_payload_sender_authenticator_mismatch", + payer: senderAddress, + }; + } + } + + // For sponsored transactions, verify max gas to prevent gas draining + if (isSponsored) { + const maxGasAmount = BigInt(transaction.rawTransaction.max_gas_amount); + if (maxGasAmount > MAX_GAS_AMOUNT) { + return { + isValid: false, + invalidReason: `invalid_exact_aptos_payload_gas_too_high: ${maxGasAmount} > ${MAX_GAS_AMOUNT}`, + payer: senderAddress, + }; + } + } + + // For sponsored transactions, verify fee payer address matches + if (isSponsored) { + const expectedFeePayer = AccountAddress.from(requirements.extra.feePayer as string); + if (!transaction.feePayerAddress || !expectedFeePayer.equals(transaction.feePayerAddress)) { + return { + isValid: false, + invalidReason: "invalid_exact_aptos_payload_fee_payer_mismatch", + payer: senderAddress, + }; + } + } + + // SECURITY (reference implementation): Prevent facilitator from signing away their own tokens + if (isSponsored && signerAddresses.includes(senderAddress)) { + return { + isValid: false, + invalidReason: "invalid_exact_aptos_payload_fee_payer_transferring_funds", + payer: senderAddress, + }; + } + + // Step 5: Verify the transaction has not expired + const EXPIRATION_BUFFER_SECONDS = 5; + const expirationTimestamp = Number(transaction.rawTransaction.expiration_timestamp_secs); + if (expirationTimestamp < Math.floor(Date.now() / 1000) + EXPIRATION_BUFFER_SECONDS) { + return { + isValid: false, + invalidReason: "invalid_exact_aptos_payload_transaction_expired", + payer: senderAddress, + }; + } + + // Step 6: Verify the transaction contains a fungible asset transfer operation + // We accept both primary_fungible_store::transfer and fungible_asset::transfer: + // - primary_fungible_store::transfer operates on primary stores (the default store for each asset) + // and automatically creates the recipient's store if it doesn't exist + // - fungible_asset::transfer is a lower-level function for arbitrary store-to-store transfers + // and is more gas efficient when stores already exist + if (!entryFunction) { + return { + isValid: false, + invalidReason: "invalid_exact_aptos_payload_missing_entry_function", + payer: senderAddress, + }; + } + + const moduleAddress = entryFunction.module_name.address; + const moduleName = entryFunction.module_name.name.identifier; + const functionName = entryFunction.function_name.identifier; + + const isPrimaryFungibleStore = + AccountAddress.ONE.equals(moduleAddress) && + moduleName === "primary_fungible_store" && + functionName === "transfer"; + + const isFungibleAsset = + AccountAddress.ONE.equals(moduleAddress) && + moduleName === "fungible_asset" && + functionName === "transfer"; + + if (!isPrimaryFungibleStore && !isFungibleAsset) { + return { + isValid: false, + invalidReason: "invalid_exact_aptos_payload_wrong_function", + payer: senderAddress, + }; + } + + if (entryFunction.type_args.length !== 1) { + return { + isValid: false, + invalidReason: "invalid_exact_aptos_payload_wrong_type_args", + payer: senderAddress, + }; + } + + const args = entryFunction.args; + if (args.length !== 3) { + return { + isValid: false, + invalidReason: "invalid_exact_aptos_payload_wrong_args", + payer: senderAddress, + }; + } + + const [faAddressArg, recipientAddressArg, amountArg] = args; + + // Step 7: Verify the transfer is for the correct asset + const faAddress = AccountAddress.from(faAddressArg.bcsToBytes()); + if (!faAddress.equals(AccountAddress.from(requirements.asset))) { + return { + isValid: false, + invalidReason: "invalid_exact_aptos_payload_asset_mismatch", + payer: senderAddress, + }; + } + + // Step 8: Verify the transfer amount matches + const amount = new Deserializer(amountArg.bcsToBytes()).deserializeU64().toString(10); + if (amount !== requirements.amount) { + return { + isValid: false, + invalidReason: "invalid_exact_aptos_payload_amount_mismatch", + payer: senderAddress, + }; + } + + // Step 9: Verify the transfer recipient matches + const recipientAddress = AccountAddress.from(recipientAddressArg.bcsToBytes()); + if (!recipientAddress.equals(AccountAddress.from(requirements.payTo))) { + return { + isValid: false, + invalidReason: "invalid_exact_aptos_payload_recipient_mismatch", + payer: senderAddress, + }; + } + + // Step 10: Verify the sender has sufficient balance + const aptos = createAptosClient(requirements.network); + const balance = await aptos.getCurrentFungibleAssetBalances({ + options: { + where: { + owner_address: { _eq: senderAddress }, + asset_type: { _eq: requirements.asset }, + }, + }, + }); + const currentBalance = BigInt(balance[0]?.amount ?? 0); + if (currentBalance < BigInt(requirements.amount)) { + return { + isValid: false, + invalidReason: "invalid_exact_aptos_payload_insufficient_balance", + payer: senderAddress, + }; + } + + // Step 11: Simulate the transaction + let publicKey: PublicKey | undefined; + if (senderAuthenticator.isEd25519()) { + publicKey = senderAuthenticator.public_key; + } else if (senderAuthenticator.isSingleKey()) { + publicKey = senderAuthenticator.public_key; + } else if (senderAuthenticator.isMultiKey()) { + publicKey = senderAuthenticator.public_keys; + } + + const simulationResult = ( + await aptos.transaction.simulate.simple({ signerPublicKey: publicKey, transaction }) + )[0]; + + if (!simulationResult.success) { + return { + isValid: false, + invalidReason: `invalid_exact_aptos_payload_simulation_failed: ${simulationResult.vm_status}`, + payer: senderAddress, + }; + } + + return { isValid: true, invalidReason: undefined, payer: senderAddress }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + isValid: false, + invalidReason: `invalid_exact_aptos_payload_verification_error: ${errorMessage}`, + payer: "", + }; + } + } + + /** + * Settles a payment by submitting the transaction. + * + * @param payload - The payment payload to settle + * @param requirements - The payment requirements + * @returns Promise resolving to settlement response + */ + async settle( + payload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + const aptosPayload = payload.payload as ExactAptosPayload; + + const valid = await this.verify(payload, requirements); + if (!valid.isValid) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: valid.invalidReason ?? "verification_failed", + payer: valid.payer || "", + }; + } + + try { + const { transaction, senderAuthenticator } = deserializeAptosPayment( + aptosPayload.transaction, + ); + const senderAddress = transaction.rawTransaction.sender.toStringLong(); + const isSponsored = typeof requirements.extra?.feePayer === "string"; + + const pendingTxn = isSponsored + ? await this.signer.signAndSubmitAsFeePayer( + transaction, + senderAuthenticator, + requirements.network, + ) + : await this.signer.submitTransaction( + transaction, + senderAuthenticator, + requirements.network, + ); + + await this.signer.waitForTransaction(pendingTxn.hash, requirements.network); + + return { + success: true, + transaction: pendingTxn.hash, + network: payload.accepted.network, + payer: senderAddress, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + success: false, + errorReason: `transaction_failed: ${errorMessage}`, + transaction: "", + network: payload.accepted.network, + payer: valid.payer || "", + }; + } + } +} diff --git a/typescript/packages/mechanisms/aptos/src/exact/index.ts b/typescript/packages/mechanisms/aptos/src/exact/index.ts new file mode 100644 index 0000000..97a7221 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/src/exact/index.ts @@ -0,0 +1 @@ +export { ExactAptosScheme } from "./client/scheme"; diff --git a/typescript/packages/mechanisms/aptos/src/exact/server/index.ts b/typescript/packages/mechanisms/aptos/src/exact/server/index.ts new file mode 100644 index 0000000..5bf3bc5 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/src/exact/server/index.ts @@ -0,0 +1 @@ +export { ExactAptosScheme } from "./scheme"; diff --git a/typescript/packages/mechanisms/aptos/src/exact/server/scheme.ts b/typescript/packages/mechanisms/aptos/src/exact/server/scheme.ts new file mode 100644 index 0000000..9290746 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/src/exact/server/scheme.ts @@ -0,0 +1,137 @@ +import type { + AssetAmount, + Money, + MoneyParser, + Network, + PaymentRequirements, + Price, + SchemeNetworkServer, +} from "@x402/core/types"; +import { APTOS_ADDRESS_REGEX, USDC_MAINNET_FA, USDC_TESTNET_FA } from "../../constants"; + +/** + * Aptos server implementation for the Exact payment scheme. + */ +export class ExactAptosScheme implements SchemeNetworkServer { + readonly scheme = "exact"; + private moneyParsers: MoneyParser[] = []; + + /** + * Register a custom money parser in the parser chain. + * + * @param parser - Custom function to convert amount to AssetAmount (or null to skip) + * @returns The service instance for chaining + */ + registerMoneyParser(parser: MoneyParser): ExactAptosScheme { + this.moneyParsers.push(parser); + return this; + } + + /** + * Parses a price into an asset amount. + * + * @param price - The price to parse + * @param network - The network to use + * @returns Promise that resolves to the parsed asset amount + */ + async parsePrice(price: Price, network: Network): Promise { + if (typeof price === "object" && price !== null && "amount" in price) { + if (!price.asset) { + throw new Error(`Asset address must be specified for AssetAmount on network ${network}`); + } + if (!APTOS_ADDRESS_REGEX.test(price.asset)) { + throw new Error(`Invalid asset address format: ${price.asset}`); + } + return { amount: price.amount, asset: price.asset, extra: price.extra || {} }; + } + + const amount = this.parseMoneyToDecimal(price as Money); + + for (const parser of this.moneyParsers) { + const result = await parser(amount, network); + if (result !== null) { + return result; + } + } + + return this.defaultMoneyConversion(amount, network); + } + + /** + * Build payment requirements for this scheme/network combination + * + * @param paymentRequirements - The base payment requirements + * @param supportedKind - The supported kind configuration + * @param supportedKind.x402Version - The x402 protocol version + * @param supportedKind.scheme - The payment scheme + * @param supportedKind.network - The network identifier + * @param supportedKind.extra - Extra metadata including feePayer address + * @param extensionKeys - Extension keys supported by the facilitator + * @returns Enhanced payment requirements with feePayer in extra + */ + enhancePaymentRequirements( + paymentRequirements: PaymentRequirements, + supportedKind: { + x402Version: number; + scheme: string; + network: Network; + extra?: Record; + }, + extensionKeys: string[], + ): Promise { + void extensionKeys; + + const extra: Record = { ...paymentRequirements.extra }; + if (typeof supportedKind.extra?.feePayer === "string") { + extra.feePayer = supportedKind.extra.feePayer; + } + + return Promise.resolve({ ...paymentRequirements, extra }); + } + + /** + * Parse Money to a decimal number. + * + * @param money - The money value to parse + * @returns Decimal number + */ + private parseMoneyToDecimal(money: string | number): number { + if (typeof money === "number") { + return money; + } + const cleanMoney = money.replace(/^\$/, "").trim(); + const amount = parseFloat(cleanMoney); + if (isNaN(amount)) { + throw new Error(`Invalid money format: ${money}`); + } + return amount; + } + + /** + * Default money conversion to USDC. + * + * @param amount - The decimal amount + * @param network - The network to use + * @returns The parsed asset amount in USDC + */ + private defaultMoneyConversion(amount: number, network: Network): AssetAmount { + const decimals = 6; + const tokenAmount = this.convertToTokenAmount(amount.toString(), decimals); + const asset = network === "aptos:2" ? USDC_TESTNET_FA : USDC_MAINNET_FA; + return { amount: tokenAmount, asset, extra: {} }; + } + + /** + * Convert a decimal amount string to a token amount string. + * + * @param amount - The decimal amount + * @param decimals - Number of decimals for the token + * @returns The amount in atomic units as a string + */ + private convertToTokenAmount(amount: string, decimals: number): string { + const parts = amount.split("."); + const wholePart = parts[0] || "0"; + const fractionalPart = (parts[1] || "").padEnd(decimals, "0").slice(0, decimals); + return BigInt(wholePart + fractionalPart).toString(); + } +} diff --git a/typescript/packages/mechanisms/aptos/src/index.ts b/typescript/packages/mechanisms/aptos/src/index.ts new file mode 100644 index 0000000..e62e1f1 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/src/index.ts @@ -0,0 +1,14 @@ +// Exact scheme exports +export * from "./exact"; + +// Types +export * from "./types"; + +// Constants +export * from "./constants"; + +// Signer utilities +export * from "./signer"; + +// Utils +export * from "./utils"; diff --git a/typescript/packages/mechanisms/aptos/src/signer.ts b/typescript/packages/mechanisms/aptos/src/signer.ts new file mode 100644 index 0000000..18649ff --- /dev/null +++ b/typescript/packages/mechanisms/aptos/src/signer.ts @@ -0,0 +1,151 @@ +import { + Account, + Ed25519PrivateKey, + Aptos, + AptosConfig, + SimpleTransaction, + AccountAuthenticator, + PrivateKey, + PrivateKeyVariants, + type PendingTransactionResponse, +} from "@aptos-labs/ts-sdk"; +import { getAptosNetwork, getAptosRpcUrl } from "./constants"; + +/** + * Client-side signer for creating and signing Aptos transactions + */ +export type ClientAptosSigner = Account; + +/** + * Configuration for client operations + */ +export type ClientAptosConfig = { + /** + * Optional custom RPC URL for the client to use + */ + rpcUrl?: string; +}; + +/** + * Minimal facilitator signer interface for Aptos operations + */ +export type FacilitatorAptosSigner = { + /** + * Get all addresses this facilitator can use for signing + */ + getAddresses(): readonly string[]; + + /** + * Sign a transaction as the fee payer and submit it + */ + signAndSubmitAsFeePayer( + transaction: SimpleTransaction, + senderAuthenticator: AccountAuthenticator, + network: string, + ): Promise; + + /** + * Submit a fully-signed transaction (non-sponsored) + */ + submitTransaction( + transaction: SimpleTransaction, + senderAuthenticator: AccountAuthenticator, + network: string, + ): Promise; + + /** + * Simulate a transaction to verify it would succeed + */ + simulateTransaction(transaction: SimpleTransaction, network: string): Promise; + + /** + * Wait for transaction confirmation + */ + waitForTransaction(txHash: string, network: string): Promise; +}; + +/** + * Creates a client signer from a private key + * + * @param privateKey - The private key as a hex string or AIP-80 format + * @returns An Aptos Account instance + */ +export async function createClientSigner(privateKey: string): Promise { + const formattedKey = PrivateKey.formatPrivateKey(privateKey, PrivateKeyVariants.Ed25519); + const privateKeyBytes = new Ed25519PrivateKey(formattedKey); + return Account.fromPrivateKey({ privateKey: privateKeyBytes }); +} + +/** + * Create a facilitator signer from an Aptos Account + * + * @param account - The Aptos Account that will act as fee payer + * @param rpcConfig - Optional RPC configuration + * @returns FacilitatorAptosSigner instance + */ +export function toFacilitatorAptosSigner( + account: Account, + rpcConfig?: { defaultRpcUrl?: string } | Record, +): FacilitatorAptosSigner { + const getRpcUrl = (network: string): string => { + if (rpcConfig) { + if ("defaultRpcUrl" in rpcConfig && rpcConfig.defaultRpcUrl) { + return rpcConfig.defaultRpcUrl; + } + if (network in rpcConfig) { + return (rpcConfig as Record)[network]; + } + } + return getAptosRpcUrl(getAptosNetwork(network)); + }; + + const getAptos = (network: string): Aptos => { + const aptosNetwork = getAptosNetwork(network); + const rpcUrl = getRpcUrl(network); + return new Aptos(new AptosConfig({ network: aptosNetwork, fullnode: rpcUrl })); + }; + + return { + getAddresses: () => [account.accountAddress.toStringLong()], + + signAndSubmitAsFeePayer: async ( + transaction: SimpleTransaction, + senderAuthenticator: AccountAuthenticator, + network: string, + ) => { + const aptos = getAptos(network); + transaction.feePayerAddress = account.accountAddress; + const feePayerAuthenticator = aptos.transaction.signAsFeePayer({ + signer: account, + transaction, + }); + return aptos.transaction.submit.simple({ + transaction, + senderAuthenticator, + feePayerAuthenticator, + }); + }, + + submitTransaction: async ( + transaction: SimpleTransaction, + senderAuthenticator: AccountAuthenticator, + network: string, + ) => { + const aptos = getAptos(network); + return aptos.transaction.submit.simple({ transaction, senderAuthenticator }); + }, + + simulateTransaction: async (transaction: SimpleTransaction, network: string) => { + const aptos = getAptos(network); + const results = await aptos.transaction.simulate.simple({ transaction }); + if (results.length === 0 || !results[0].success) { + throw new Error(`Simulation failed: ${results[0]?.vm_status || "unknown error"}`); + } + }, + + waitForTransaction: async (txHash: string, network: string) => { + const aptos = getAptos(network); + await aptos.waitForTransaction({ transactionHash: txHash }); + }, + }; +} diff --git a/typescript/packages/mechanisms/aptos/src/types.ts b/typescript/packages/mechanisms/aptos/src/types.ts new file mode 100644 index 0000000..2ceff33 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/src/types.ts @@ -0,0 +1,23 @@ +/** + * Exact Aptos payload structure containing a base64 encoded transaction + */ +export type ExactAptosPayload = { + /** + * Base64 encoded JSON containing transaction and senderAuthenticator byte arrays + */ + transaction: string; +}; + +/** + * Decoded Aptos payment payload structure + */ +export type DecodedAptosPayload = { + /** + * Transaction bytes as number array + */ + transaction: number[]; + /** + * Sender authenticator bytes as number array + */ + senderAuthenticator: number[]; +}; diff --git a/typescript/packages/mechanisms/aptos/src/utils.ts b/typescript/packages/mechanisms/aptos/src/utils.ts new file mode 100644 index 0000000..f71e030 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/src/utils.ts @@ -0,0 +1,94 @@ +import { + Deserializer, + SimpleTransaction, + AccountAuthenticator, + TransactionPayloadEntryFunction, + TransactionPayload, + EntryFunction, + Aptos, + AptosConfig, +} from "@aptos-labs/ts-sdk"; +import type { DecodedAptosPayload } from "./types"; +import { getAptosNetwork, getAptosRpcUrl } from "./constants"; + +/** + * Deserialize an Aptos transaction and authenticator from the payment payload. + * + * @param transactionBase64 - The base64 encoded transaction payload + * @returns The deserialized transaction and authenticator + */ +export function deserializeAptosPayment(transactionBase64: string): { + transaction: SimpleTransaction; + senderAuthenticator: AccountAuthenticator; + entryFunction?: EntryFunction; +} { + // Decode the base64 payload + const decoded = Buffer.from(transactionBase64, "base64").toString("utf8"); + const parsed: DecodedAptosPayload = JSON.parse(decoded); + + // Deserialize the transaction bytes + const transactionBytes = Uint8Array.from(parsed.transaction); + const transaction = SimpleTransaction.deserialize(new Deserializer(transactionBytes)); + + // Deserialize the authenticator bytes + const authBytes = Uint8Array.from(parsed.senderAuthenticator); + const senderAuthenticator = AccountAuthenticator.deserialize(new Deserializer(authBytes)); + + // Only Entry Function transactions are supported + if (!isEntryFunctionPayload(transaction.rawTransaction.payload)) { + return { transaction, senderAuthenticator }; + } + + const entryFunction = transaction.rawTransaction.payload.entryFunction; + + return { transaction, senderAuthenticator, entryFunction }; +} + +/** + * Checks if it's an entry function payload. + * + * @param payload - The payload to check + * @returns True if it's an entry function payload + */ +export function isEntryFunctionPayload( + payload: TransactionPayload, +): payload is TransactionPayloadEntryFunction { + return "entryFunction" in payload; +} + +/** + * Create an Aptos SDK client for the given network + * + * @param network - CAIP-2 network identifier (e.g., "aptos:1") + * @param rpcUrl - Optional custom RPC URL + * @returns Aptos SDK client + */ +export function createAptosClient(network: string, rpcUrl?: string): Aptos { + const aptosNetwork = getAptosNetwork(network); + const fullnodeUrl = rpcUrl || getAptosRpcUrl(aptosNetwork); + + const config = new AptosConfig({ + network: aptosNetwork, + fullnode: fullnodeUrl, + }); + + return new Aptos(config); +} + +/** + * Encode an Aptos payment payload to base64 + * + * @param transactionBytes - The serialized transaction bytes + * @param authenticatorBytes - The serialized authenticator bytes + * @returns Base64 encoded payload + */ +export function encodeAptosPayload( + transactionBytes: Uint8Array, + authenticatorBytes: Uint8Array, +): string { + const payload: DecodedAptosPayload = { + transaction: Array.from(transactionBytes), + senderAuthenticator: Array.from(authenticatorBytes), + }; + return Buffer.from(JSON.stringify(payload)).toString("base64"); +} diff --git a/typescript/packages/mechanisms/aptos/test/integrations/exact-aptos.test.ts b/typescript/packages/mechanisms/aptos/test/integrations/exact-aptos.test.ts new file mode 100644 index 0000000..287eb93 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/test/integrations/exact-aptos.test.ts @@ -0,0 +1,570 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { x402Client, x402HTTPClient } from "@x402/core/client"; +import { x402Facilitator } from "@x402/core/facilitator"; +import { + HTTPAdapter, + HTTPResponseInstructions, + x402HTTPResourceServer, + x402ResourceServer, + FacilitatorClient, +} from "@x402/core/server"; +import { + Network, + PaymentPayload, + PaymentRequirements, + VerifyResponse, + SettleResponse, + SupportedResponse, +} from "@x402/core/types"; +import { ExactAptosScheme as ExactAptosClient } from "../../src/exact/client/scheme"; +import { ExactAptosScheme as ExactAptosServer } from "../../src/exact/server/scheme"; +import { ExactAptosScheme as ExactAptosFacilitator } from "../../src/exact/facilitator/scheme"; +import { createClientSigner, toFacilitatorAptosSigner } from "../../src"; +import type { ExactAptosPayload } from "../../src/types"; + +// Load private keys from environment +const CLIENT_PRIVATE_KEY = process.env.APTOS_CLIENT_PRIVATE_KEY; +const FACILITATOR_PRIVATE_KEY = process.env.APTOS_FACILITATOR_PRIVATE_KEY; + +if (!CLIENT_PRIVATE_KEY || !FACILITATOR_PRIVATE_KEY) { + throw new Error( + "APTOS_CLIENT_PRIVATE_KEY and APTOS_FACILITATOR_PRIVATE_KEY environment variables must be set for integration tests", + ); +} + +// Aptos testnet USDC address +const USDC_TESTNET = "0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832"; + +/** + * Aptos Facilitator Client wrapper + * Wraps the x402Facilitator for use with x402ResourceServer + */ +class AptosFacilitatorClient implements FacilitatorClient { + readonly scheme = "exact"; + readonly network = "aptos:2"; // Testnet + readonly x402Version = 2; + + /** + * Creates a new AptosFacilitatorClient instance + * + * @param facilitator - The x402 facilitator to wrap + */ + constructor(private readonly facilitator: x402Facilitator) {} + + /** + * Verifies a payment payload + * + * @param paymentPayload - The payment payload to verify + * @param paymentRequirements - The payment requirements + * @returns Promise resolving to verification response + */ + verify( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise { + return this.facilitator.verify(paymentPayload, paymentRequirements); + } + + /** + * Settles a payment + * + * @param paymentPayload - The payment payload to settle + * @param paymentRequirements - The payment requirements + * @returns Promise resolving to settlement response + */ + settle( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise { + return this.facilitator.settle(paymentPayload, paymentRequirements); + } + + /** + * Gets supported payment kinds + * + * @returns Promise resolving to supported response + */ + getSupported(): Promise { + // Delegate to actual facilitator to get real supported kinds + return Promise.resolve(this.facilitator.getSupported()); + } +} + +/** + * Build Aptos payment requirements for testing + * + * @param payTo - The recipient address + * @param amount - The payment amount in smallest units + * @param feePayer - Optional fee payer address (undefined for non-sponsored) + * @param network - The network identifier (defaults to Aptos Testnet) + * @returns Payment requirements object + */ +function buildAptosPaymentRequirements( + payTo: string, + amount: string, + feePayer?: string, + network: Network = "aptos:2", +): PaymentRequirements { + return { + scheme: "exact", + network, + asset: USDC_TESTNET, + amount, + payTo, + maxTimeoutSeconds: 3600, + extra: feePayer ? { feePayer } : {}, + }; +} + +describe("Aptos Integration Tests", () => { + describe("x402Client / x402ResourceServer / x402Facilitator - Aptos Flow", () => { + let client: x402Client; + let server: x402ResourceServer; + let clientAddress: string; + let facilitatorAddress: string; + + beforeEach(async () => { + // Create client account and signer from environment variable + const clientAccount = await createClientSigner(CLIENT_PRIVATE_KEY); + clientAddress = clientAccount.accountAddress.toStringLong(); + + const aptosClient = new ExactAptosClient(clientAccount); + client = new x402Client().register("aptos:2", aptosClient); + + // Create facilitator account and signer from environment variable + const facilitatorAccount = await createClientSigner(FACILITATOR_PRIVATE_KEY); + facilitatorAddress = facilitatorAccount.accountAddress.toStringLong(); + const facilitatorSigner = toFacilitatorAptosSigner(facilitatorAccount); + + const aptosFacilitator = new ExactAptosFacilitator(facilitatorSigner); + const facilitator = new x402Facilitator().register("aptos:2", aptosFacilitator); + + const facilitatorClient = new AptosFacilitatorClient(facilitator); + server = new x402ResourceServer(facilitatorClient); + server.register("aptos:2", new ExactAptosServer()); + await server.initialize(); // Initialize to fetch supported kinds + }); + + it("server should successfully verify an Aptos payment from a client", async () => { + // Server - builds PaymentRequired response + const accepts = [ + buildAptosPaymentRequirements( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "1000", // 0.001 USDC + facilitatorAddress, + ), + ]; + const resource = { + url: "https://company.co", + description: "Company Co. resource", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + // Client - responds with PaymentPayload response + const paymentPayload = await client.createPaymentPayload(paymentRequired); + + expect(paymentPayload).toBeDefined(); + expect(paymentPayload.x402Version).toBe(2); + expect(paymentPayload.accepted.scheme).toBe("exact"); + expect(paymentPayload.accepted.network).toBe("aptos:2"); + + // Verify the payload structure + const aptosPayload = paymentPayload.payload as ExactAptosPayload; + expect(aptosPayload.transaction).toBeDefined(); + expect(typeof aptosPayload.transaction).toBe("string"); + + // Server - maps payment payload to payment requirements + const accepted = server.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + const verifyResponse = await server.verifyPayment(paymentPayload, accepted!); + + if (!verifyResponse.isValid) { + console.log("Verification failed!"); + console.log("Invalid reason:", verifyResponse.invalidReason); + console.log("Payer:", verifyResponse.payer); + console.log("Client address:", clientAddress); + } + + expect(verifyResponse.isValid).toBe(true); + expect(verifyResponse.payer).toBe(clientAddress); + }); + + it("client should create a non-sponsored payment payload when feePayer is not provided", async () => { + // Payment requirements without feePayer (non-sponsored mode) + const accepts = [ + buildAptosPaymentRequirements( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "1000", + undefined, // No fee payer - client pays gas + ), + ]; + const resource = { + url: "https://company.co", + description: "Company Co. resource", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + // Client should create payload without fee payer + const paymentPayload = await client.createPaymentPayload(paymentRequired); + + expect(paymentPayload).toBeDefined(); + expect(paymentPayload.x402Version).toBe(2); + expect(paymentPayload.accepted.scheme).toBe("exact"); + expect(paymentPayload.accepted.network).toBe("aptos:2"); + + const aptosPayload = paymentPayload.payload as ExactAptosPayload; + expect(aptosPayload.transaction).toBeDefined(); + expect(typeof aptosPayload.transaction).toBe("string"); + }); + + it("server should successfully verify a non-sponsored Aptos payment", async () => { + // Create a non-sponsoring facilitator + const facilitatorAccount = await createClientSigner(FACILITATOR_PRIVATE_KEY); + const facilitatorSigner = toFacilitatorAptosSigner(facilitatorAccount); + const aptosFacilitator = new ExactAptosFacilitator(facilitatorSigner, false); // sponsorTransactions = false + const facilitator = new x402Facilitator().register("aptos:2", aptosFacilitator); + + const facilitatorClient = new AptosFacilitatorClient(facilitator); + const nonSponsoringServer = new x402ResourceServer(facilitatorClient); + nonSponsoringServer.register("aptos:2", new ExactAptosServer()); + await nonSponsoringServer.initialize(); + + // Payment requirements without feePayer (non-sponsored mode) + const accepts = [ + buildAptosPaymentRequirements( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "1000", + undefined, // No fee payer - client pays gas + ), + ]; + const resource = { + url: "https://company.co", + description: "Company Co. resource", + mimeType: "application/json", + }; + const paymentRequired = await nonSponsoringServer.createPaymentRequiredResponse( + accepts, + resource, + ); + + // Client creates non-sponsored payload + const paymentPayload = await client.createPaymentPayload(paymentRequired); + + expect(paymentPayload).toBeDefined(); + expect(paymentPayload.accepted.extra?.feePayer).toBeUndefined(); + + // Server verifies the non-sponsored payment + const accepted = nonSponsoringServer.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + const verifyResponse = await nonSponsoringServer.verifyPayment(paymentPayload, accepted!); + + if (!verifyResponse.isValid) { + console.log("Non-sponsored verification failed!"); + console.log("Invalid reason:", verifyResponse.invalidReason); + } + + expect(verifyResponse.isValid).toBe(true); + expect(verifyResponse.payer).toBe(clientAddress); + }); + }); + + describe("x402HTTPClient / x402HTTPResourceServer / x402Facilitator - Aptos Flow", () => { + let client: x402HTTPClient; + let httpServer: x402HTTPResourceServer; + + const routes = { + "/api/protected": { + accepts: { + scheme: "exact", + payTo: "0x0000000000000000000000000000000000000000000000000000000000000001", + price: "$0.001", + network: "aptos:2" as Network, + }, + description: "Access to protected API", + mimeType: "application/json", + }, + }; + + const mockAdapter: HTTPAdapter = { + getHeader: () => { + return undefined; + }, + getMethod: () => "GET", + getPath: () => "/api/protected", + getUrl: () => "https://example.com/api/protected", + getAcceptHeader: () => "application/json", + getUserAgent: () => "TestClient/1.0", + }; + + beforeEach(async () => { + // Create facilitator account and signer from environment variable + const facilitatorAccount = await createClientSigner(FACILITATOR_PRIVATE_KEY); + const facilitatorSigner = toFacilitatorAptosSigner(facilitatorAccount); + + const aptosFacilitator = new ExactAptosFacilitator(facilitatorSigner); + const facilitator = new x402Facilitator().register("aptos:2", aptosFacilitator); + + const facilitatorClient = new AptosFacilitatorClient(facilitator); + + // Create client account and signer from environment variable + const clientAccount = await createClientSigner(CLIENT_PRIVATE_KEY); + + const aptosClient = new ExactAptosClient(clientAccount); + const paymentClient = new x402Client().register("aptos:2", aptosClient); + client = new x402HTTPClient(paymentClient) as x402HTTPClient; + + // Create resource server and register schemes (composition pattern) + const ResourceServer = new x402ResourceServer(facilitatorClient); + ResourceServer.register("aptos:2", new ExactAptosServer()); + await ResourceServer.initialize(); // Initialize to fetch supported kinds + + httpServer = new x402HTTPResourceServer(ResourceServer, routes); + }); + + it("middleware should successfully verify an Aptos payment from an http client", async () => { + // Middleware creates a PaymentRequired response + const context = { + adapter: mockAdapter, + path: "/api/protected", + method: "GET", + }; + + // No payment made, get PaymentRequired response & header + const httpProcessResult = (await httpServer.processHTTPRequest(context))!; + + expect(httpProcessResult.type).toBe("payment-error"); + + const initial402Response = ( + httpProcessResult as { type: "payment-error"; response: HTTPResponseInstructions } + ).response; + + expect(initial402Response).toBeDefined(); + expect(initial402Response.status).toBe(402); + expect(initial402Response.headers).toBeDefined(); + expect(initial402Response.headers["PAYMENT-REQUIRED"]).toBeDefined(); + + // Client responds to PaymentRequired and submits a request with a PaymentPayload + const paymentRequired = client.getPaymentRequiredResponse( + name => initial402Response.headers[name], + initial402Response.body, + ); + const paymentPayload = await client.createPaymentPayload(paymentRequired); + + expect(paymentPayload).toBeDefined(); + expect(paymentPayload.accepted.scheme).toBe("exact"); + expect(paymentPayload.accepted.network).toBe("aptos:2"); + + const requestHeaders = await client.encodePaymentSignatureHeader(paymentPayload); + + // Middleware handles PAYMENT-SIGNATURE request + mockAdapter.getHeader = (name: string) => { + if (name === "PAYMENT-SIGNATURE") { + return requestHeaders["PAYMENT-SIGNATURE"]; + } + return undefined; + }; + + const httpProcessResult2 = await httpServer.processHTTPRequest(context); + + // No need to respond, can continue with request + expect(httpProcessResult2.type).toBe("payment-verified"); + const { + paymentPayload: verifiedPaymentPayload, + paymentRequirements: verifiedPaymentRequirements, + } = httpProcessResult2 as { + type: "payment-verified"; + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + }; + + expect(verifiedPaymentPayload).toBeDefined(); + expect(verifiedPaymentRequirements).toBeDefined(); + }); + }); + + describe("Price Parsing Integration", () => { + let server: x402ResourceServer; + let aptosServer: ExactAptosServer; + + beforeEach(async () => { + const facilitatorAccount = await createClientSigner(FACILITATOR_PRIVATE_KEY); + const facilitatorSigner = toFacilitatorAptosSigner(facilitatorAccount); + const facilitator = new x402Facilitator().register( + "aptos:2", + new ExactAptosFacilitator(facilitatorSigner), + ); + + const facilitatorClient = new AptosFacilitatorClient(facilitator); + server = new x402ResourceServer(facilitatorClient); + + aptosServer = new ExactAptosServer(); + server.register("aptos:2", aptosServer); + await server.initialize(); + }); + + it("should parse Money formats and build payment requirements", async () => { + // Test different Money formats + // USDC has 6 decimals + const testCases = [ + { input: "$1.00", expectedAmount: "1000000" }, + { input: "1.50", expectedAmount: "1500000" }, + { input: 2.5, expectedAmount: "2500000" }, + ]; + + for (const testCase of testCases) { + const requirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: "0x0000000000000000000000000000000000000000000000000000000000000001", + price: testCase.input, + network: "aptos:2" as Network, + }); + + expect(requirements).toHaveLength(1); + expect(requirements[0].amount).toBe(testCase.expectedAmount); + expect(requirements[0].asset).toBe(USDC_TESTNET); + } + }); + + it("should handle AssetAmount pass-through", async () => { + const customAsset = { + amount: "5000000", + asset: "0x0000000000000000000000000000000000000000000000000000000000000abc", + extra: { foo: "bar" }, + }; + + const requirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: "0x0000000000000000000000000000000000000000000000000000000000000001", + price: customAsset, + network: "aptos:2" as Network, + }); + + expect(requirements).toHaveLength(1); + expect(requirements[0].amount).toBe("5000000"); + expect(requirements[0].asset).toBe( + "0x0000000000000000000000000000000000000000000000000000000000000abc", + ); + expect(requirements[0].extra?.foo).toBe("bar"); + }); + + it("should use registerMoneyParser for custom conversion", async () => { + // register custom parser: large amounts use a different token + aptosServer.registerMoneyParser(async (amount, _network) => { + if (amount > 100) { + return { + amount: (amount * 1e8).toString(), // APT has 8 decimals + asset: "0x000000000000000000000000000000000000000000000000000000000000000a", // APT + extra: { token: "APT", tier: "large" }, + }; + } + return null; // Use default for small amounts + }); + + // Test large amount - should use custom parser + const largeRequirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: "0x0000000000000000000000000000000000000000000000000000000000000001", + price: 150, // Large amount + network: "aptos:2" as Network, + }); + + expect(largeRequirements[0].amount).toBe((150 * 1e8).toString()); + expect(largeRequirements[0].asset).toBe( + "0x000000000000000000000000000000000000000000000000000000000000000a", + ); + expect(largeRequirements[0].extra?.token).toBe("APT"); + expect(largeRequirements[0].extra?.tier).toBe("large"); + + // Test small amount - should use default USDC + const smallRequirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: "0x0000000000000000000000000000000000000000000000000000000000000001", + price: 50, // Small amount + network: "aptos:2" as Network, + }); + + expect(smallRequirements[0].amount).toBe("50000000"); // 50 * 1e6 (USDC) + expect(smallRequirements[0].asset).toBe(USDC_TESTNET); + }); + + it("should support multiple MoneyParser in chain", async () => { + aptosServer + .registerMoneyParser(async amount => { + if (amount > 1000) { + return { + amount: (amount * 1e8).toString(), + asset: "0xAPT", + extra: { tier: "vip" }, + }; + } + return null; + }) + .registerMoneyParser(async amount => { + if (amount > 100) { + return { + amount: (amount * 1e6).toString(), + asset: "0xUSDT", + extra: { tier: "premium" }, + }; + } + return null; + }); + // < 100 uses default USDC + + // VIP tier + const vipReq = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: "0x0000000000000000000000000000000000000000000000000000000000000001", + price: 2000, + network: "aptos:2" as Network, + }); + expect(vipReq[0].extra?.tier).toBe("vip"); + expect(vipReq[0].asset).toBe("0xAPT"); + + // Premium tier + const premiumReq = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: "0x0000000000000000000000000000000000000000000000000000000000000001", + price: 500, + network: "aptos:2" as Network, + }); + expect(premiumReq[0].extra?.tier).toBe("premium"); + expect(premiumReq[0].asset).toBe("0xUSDT"); + + // Standard tier (default) + const standardReq = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: "0x0000000000000000000000000000000000000000000000000000000000000001", + price: 50, + network: "aptos:2" as Network, + }); + expect(standardReq[0].asset).toBe(USDC_TESTNET); + }); + + it("should avoid floating-point rounding error", async () => { + // Test different Money formats + const testCases = [ + { input: "$4.02", expectedAmount: "4020000" }, + { input: "4.02", expectedAmount: "4020000" }, + { input: 4.02, expectedAmount: "4020000" }, + ]; + + for (const testCase of testCases) { + const requirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: "0x0000000000000000000000000000000000000000000000000000000000000001", + price: testCase.input, + network: "aptos:2" as Network, + }); + + expect(requirements).toHaveLength(1); + expect(requirements[0].amount).toBe(testCase.expectedAmount); + expect(requirements[0].asset).toBe(USDC_TESTNET); + } + }); + }); +}); diff --git a/typescript/packages/mechanisms/aptos/test/unit/constants.test.ts b/typescript/packages/mechanisms/aptos/test/unit/constants.test.ts new file mode 100644 index 0000000..8394ee1 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/test/unit/constants.test.ts @@ -0,0 +1,115 @@ +import { describe, it, expect } from "vitest"; +import { Network } from "@aptos-labs/ts-sdk"; +import { + APTOS_MAINNET_CAIP2, + APTOS_TESTNET_CAIP2, + APTOS_ADDRESS_REGEX, + TRANSFER_FUNCTION, + MAX_GAS_AMOUNT, + getAptosNetwork, + getAptosRpcUrl, + getAptosChainId, +} from "../../src/constants"; + +describe("Aptos Constants", () => { + describe("Network identifiers", () => { + it("should have correct CAIP-2 format for mainnet", () => { + expect(APTOS_MAINNET_CAIP2).toBe("aptos:1"); + }); + + it("should have correct CAIP-2 format for testnet", () => { + expect(APTOS_TESTNET_CAIP2).toBe("aptos:2"); + }); + }); + + describe("APTOS_ADDRESS_REGEX", () => { + it("should match valid Aptos addresses", () => { + const validAddress = "0x0000000000000000000000000000000000000000000000000000000000000001"; + expect(APTOS_ADDRESS_REGEX.test(validAddress)).toBe(true); + }); + + it("should match addresses with mixed case hex", () => { + const validAddress = "0xABCDef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; + expect(APTOS_ADDRESS_REGEX.test(validAddress)).toBe(true); + }); + + it("should reject addresses without 0x prefix", () => { + const invalidAddress = "0000000000000000000000000000000000000000000000000000000000000001"; + expect(APTOS_ADDRESS_REGEX.test(invalidAddress)).toBe(false); + }); + + it("should reject addresses with wrong length", () => { + expect(APTOS_ADDRESS_REGEX.test("0x1234")).toBe(false); + expect(APTOS_ADDRESS_REGEX.test("0x" + "a".repeat(65))).toBe(false); + }); + + it("should reject addresses with invalid characters", () => { + const invalidAddress = "0xGGGG000000000000000000000000000000000000000000000000000000000001"; + expect(APTOS_ADDRESS_REGEX.test(invalidAddress)).toBe(false); + }); + }); + + describe("TRANSFER_FUNCTION", () => { + it("should be the correct primary fungible store transfer function", () => { + expect(TRANSFER_FUNCTION).toBe("0x1::primary_fungible_store::transfer"); + }); + }); + + describe("getAptosNetwork", () => { + it("should return MAINNET for aptos:1", () => { + expect(getAptosNetwork("aptos:1")).toBe(Network.MAINNET); + }); + + it("should return TESTNET for aptos:2", () => { + expect(getAptosNetwork("aptos:2")).toBe(Network.TESTNET); + }); + + it("should throw for unsupported networks", () => { + expect(() => getAptosNetwork("aptos:99")).toThrow("Unsupported Aptos network"); + expect(() => getAptosNetwork("ethereum:1")).toThrow("Unsupported Aptos network"); + expect(() => getAptosNetwork("invalid")).toThrow("Unsupported Aptos network"); + }); + }); + + describe("getAptosRpcUrl", () => { + it("should return a valid URL for mainnet", () => { + const url = getAptosRpcUrl(Network.MAINNET); + expect(url).toContain("aptos"); + expect(url.startsWith("https://")).toBe(true); + }); + + it("should return a valid URL for testnet", () => { + const url = getAptosRpcUrl(Network.TESTNET); + expect(url).toContain("aptos"); + expect(url.startsWith("https://")).toBe(true); + }); + + it("should return different URLs for different networks", () => { + const mainnetUrl = getAptosRpcUrl(Network.MAINNET); + const testnetUrl = getAptosRpcUrl(Network.TESTNET); + expect(mainnetUrl).not.toBe(testnetUrl); + }); + }); + + describe("MAX_GAS_AMOUNT", () => { + it("should be a reasonable limit for simple transfers", () => { + expect(MAX_GAS_AMOUNT).toBe(500000n); + }); + }); + + describe("getAptosChainId", () => { + it("should return 1 for mainnet", () => { + expect(getAptosChainId("aptos:1")).toBe(1); + }); + + it("should return 2 for testnet", () => { + expect(getAptosChainId("aptos:2")).toBe(2); + }); + + it("should throw for unsupported networks", () => { + expect(() => getAptosChainId("aptos:99")).toThrow("Unsupported Aptos network"); + expect(() => getAptosChainId("ethereum:1")).toThrow("Unsupported Aptos network"); + expect(() => getAptosChainId("invalid")).toThrow("Unsupported Aptos network"); + }); + }); +}); diff --git a/typescript/packages/mechanisms/aptos/test/unit/index.test.ts b/typescript/packages/mechanisms/aptos/test/unit/index.test.ts new file mode 100644 index 0000000..515120d --- /dev/null +++ b/typescript/packages/mechanisms/aptos/test/unit/index.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect, vi } from "vitest"; +import { ExactAptosScheme as ExactAptosClient } from "../../src/exact/client/scheme"; +import { ExactAptosScheme as ExactAptosFacilitator } from "../../src/exact/facilitator/scheme"; +import { ExactAptosScheme as ExactAptosServer } from "../../src/exact/server/scheme"; +import { + APTOS_MAINNET_CAIP2, + APTOS_TESTNET_CAIP2, + APTOS_ADDRESS_REGEX, + TRANSFER_FUNCTION, + MAX_GAS_AMOUNT, + getAptosNetwork, + getAptosRpcUrl, + getAptosChainId, +} from "../../src/index"; +import type { PaymentRequirements } from "@x402/core/types"; + +describe("@x402/aptos", () => { + describe("exports", () => { + it("should export main scheme classes", () => { + expect(ExactAptosClient).toBeDefined(); + expect(ExactAptosFacilitator).toBeDefined(); + expect(ExactAptosServer).toBeDefined(); + }); + + it("should export constants", () => { + expect(APTOS_MAINNET_CAIP2).toBe("aptos:1"); + expect(APTOS_TESTNET_CAIP2).toBe("aptos:2"); + expect(APTOS_ADDRESS_REGEX).toBeDefined(); + expect(TRANSFER_FUNCTION).toBe("0x1::primary_fungible_store::transfer"); + expect(MAX_GAS_AMOUNT).toBe(500000n); + }); + + it("should export utility functions", () => { + expect(getAptosNetwork).toBeDefined(); + expect(getAptosRpcUrl).toBeDefined(); + expect(getAptosChainId).toBeDefined(); + }); + }); + + describe("ExactAptosServer", () => { + it("should have scheme property set to exact", () => { + const server = new ExactAptosServer(); + expect(server.scheme).toBe("exact"); + }); + }); + + describe("ExactAptosFacilitator", () => { + it("should return feePayer in getExtra for sponsored transactions", () => { + const mockSigner = { + getAddresses: () => ["0x123"], + signAndSubmitAsFeePayer: vi.fn(), + submitTransaction: vi.fn(), + simulateTransaction: vi.fn(), + waitForTransaction: vi.fn(), + }; + const facilitator = new ExactAptosFacilitator(mockSigner); + const extra = facilitator.getExtra("aptos:2"); + expect(extra).toBeDefined(); + expect(extra?.feePayer).toBe("0x123"); + }); + + it("should return all signer addresses in getSigners", () => { + const mockSigner = { + getAddresses: () => ["0x123", "0x456"], + signAndSubmitAsFeePayer: vi.fn(), + submitTransaction: vi.fn(), + simulateTransaction: vi.fn(), + waitForTransaction: vi.fn(), + }; + const facilitator = new ExactAptosFacilitator(mockSigner); + const signers = facilitator.getSigners("aptos:2"); + expect(signers).toEqual(["0x123", "0x456"]); + }); + }); + + describe("ExactAptosServer enhancePaymentRequirements", () => { + it("should add feePayer from supportedKind.extra when sponsored", async () => { + const server = new ExactAptosServer(); + const requirements: PaymentRequirements = { + scheme: "exact", + network: "aptos:2", + asset: "0x123", + amount: "1000", + payTo: "0x456", + maxTimeoutSeconds: 3600, + }; + const supportedKind = { + x402Version: 2, + scheme: "exact", + network: "aptos:2" as const, + extra: { feePayer: "0x789" }, + }; + + const enhanced = await server.enhancePaymentRequirements(requirements, supportedKind, []); + expect(enhanced.extra?.feePayer).toBe("0x789"); + }); + + it("should not add feePayer when supportedKind.extra has no feePayer (non-sponsored)", async () => { + const server = new ExactAptosServer(); + const requirements: PaymentRequirements = { + scheme: "exact", + network: "aptos:2", + asset: "0x123", + amount: "1000", + payTo: "0x456", + maxTimeoutSeconds: 3600, + }; + const supportedKind = { + x402Version: 2, + scheme: "exact", + network: "aptos:2" as const, + extra: {}, + }; + + const enhanced = await server.enhancePaymentRequirements(requirements, supportedKind, []); + expect(enhanced.extra?.feePayer).toBeUndefined(); + }); + }); +}); diff --git a/typescript/packages/mechanisms/aptos/test/unit/signer.test.ts b/typescript/packages/mechanisms/aptos/test/unit/signer.test.ts new file mode 100644 index 0000000..84ffdec --- /dev/null +++ b/typescript/packages/mechanisms/aptos/test/unit/signer.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect } from "vitest"; +import { Account } from "@aptos-labs/ts-sdk"; +import { createClientSigner, toFacilitatorAptosSigner } from "../../src/signer"; + +describe("Aptos Signer", () => { + describe("createClientSigner", () => { + it("should create a client signer from a valid private key", async () => { + // Generate a test account to get a valid private key + const testAccount = Account.generate(); + const privateKey = testAccount.privateKey.toString(); + + const signer = await createClientSigner(privateKey); + + expect(signer).toBeDefined(); + expect(signer.accountAddress).toBeDefined(); + expect(signer.accountAddress.toString()).toBe(testAccount.accountAddress.toString()); + }); + + it("should handle AIP-80 formatted private keys", async () => { + // Generate a test account + const testAccount = Account.generate(); + // AIP-80 format includes the key type prefix + const privateKey = testAccount.privateKey.toString(); + + const signer = await createClientSigner(privateKey); + + expect(signer).toBeDefined(); + expect(signer.signTransactionWithAuthenticator).toBeDefined(); + }); + + it("should throw for invalid private keys", async () => { + await expect(createClientSigner("invalid-key")).rejects.toThrow(); + }); + }); + + describe("toFacilitatorAptosSigner", () => { + it("should return signer addresses", () => { + const testAccount = Account.generate(); + const facilitatorSigner = toFacilitatorAptosSigner(testAccount); + + const addresses = facilitatorSigner.getAddresses(); + + expect(addresses).toHaveLength(1); + expect(addresses[0]).toBe(testAccount.accountAddress.toStringLong()); + }); + + it("should implement all required methods", () => { + const testAccount = Account.generate(); + const facilitatorSigner = toFacilitatorAptosSigner(testAccount); + + expect(facilitatorSigner.getAddresses).toBeDefined(); + expect(facilitatorSigner.signAndSubmitAsFeePayer).toBeDefined(); + expect(facilitatorSigner.submitTransaction).toBeDefined(); + expect(facilitatorSigner.simulateTransaction).toBeDefined(); + expect(facilitatorSigner.waitForTransaction).toBeDefined(); + }); + + it("should use custom RPC URL when provided", () => { + const testAccount = Account.generate(); + const customRpcUrl = "https://custom-rpc.example.com"; + const facilitatorSigner = toFacilitatorAptosSigner(testAccount, { + defaultRpcUrl: customRpcUrl, + }); + + // The signer should be created successfully with custom config + expect(facilitatorSigner).toBeDefined(); + expect(facilitatorSigner.getAddresses()).toHaveLength(1); + }); + + it("should support network-specific RPC URLs", () => { + const testAccount = Account.generate(); + const rpcConfig = { + "aptos:1": "https://mainnet-rpc.example.com", + "aptos:2": "https://testnet-rpc.example.com", + }; + const facilitatorSigner = toFacilitatorAptosSigner(testAccount, rpcConfig); + + expect(facilitatorSigner).toBeDefined(); + expect(facilitatorSigner.getAddresses()).toHaveLength(1); + }); + }); +}); diff --git a/typescript/packages/mechanisms/aptos/test/unit/types.test.ts b/typescript/packages/mechanisms/aptos/test/unit/types.test.ts new file mode 100644 index 0000000..0343887 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/test/unit/types.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from "vitest"; +import type { ExactAptosPayload, DecodedAptosPayload } from "../../src/types"; + +describe("Aptos Types", () => { + describe("ExactAptosPayload", () => { + it("should accept valid payload structure", () => { + const payload: ExactAptosPayload = { + transaction: "base64encodedtransaction==", + }; + + expect(payload.transaction).toBeDefined(); + expect(typeof payload.transaction).toBe("string"); + }); + + it("should accept empty transaction string", () => { + const payload: ExactAptosPayload = { + transaction: "", + }; + + expect(payload.transaction).toBe(""); + }); + + it("should accept long base64 transaction strings", () => { + const longTransaction = "A".repeat(1000) + "=="; + const payload: ExactAptosPayload = { + transaction: longTransaction, + }; + + expect(payload.transaction).toBe(longTransaction); + expect(payload.transaction.length).toBe(1002); + }); + }); + + describe("DecodedAptosPayload", () => { + it("should accept valid decoded structure", () => { + const decoded: DecodedAptosPayload = { + transaction: [1, 2, 3, 4, 5], + senderAuthenticator: [10, 20, 30], + }; + + expect(decoded.transaction).toBeDefined(); + expect(decoded.senderAuthenticator).toBeDefined(); + expect(Array.isArray(decoded.transaction)).toBe(true); + expect(Array.isArray(decoded.senderAuthenticator)).toBe(true); + }); + + it("should accept empty arrays", () => { + const decoded: DecodedAptosPayload = { + transaction: [], + senderAuthenticator: [], + }; + + expect(decoded.transaction.length).toBe(0); + expect(decoded.senderAuthenticator.length).toBe(0); + }); + }); +}); diff --git a/typescript/packages/mechanisms/aptos/tsconfig.json b/typescript/packages/mechanisms/aptos/tsconfig.json new file mode 100644 index 0000000..f600458 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/typescript/packages/mechanisms/aptos/tsup.config.ts b/typescript/packages/mechanisms/aptos/tsup.config.ts new file mode 100644 index 0000000..1143bfe --- /dev/null +++ b/typescript/packages/mechanisms/aptos/tsup.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from "tsup"; + +const baseConfig = { + entry: { + index: "src/index.ts", + "exact/client/index": "src/exact/client/index.ts", + "exact/server/index": "src/exact/server/index.ts", + "exact/facilitator/index": "src/exact/facilitator/index.ts", + }, + dts: { + resolve: true, + }, + sourcemap: true, + target: "es2020", +}; + +export default defineConfig([ + { + ...baseConfig, + format: "esm", + outDir: "dist/esm", + clean: true, + }, + { + ...baseConfig, + format: "cjs", + outDir: "dist/cjs", + clean: false, + }, +]); diff --git a/typescript/packages/mechanisms/aptos/vitest.config.ts b/typescript/packages/mechanisms/aptos/vitest.config.ts new file mode 100644 index 0000000..ce2ed43 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/vitest.config.ts @@ -0,0 +1,15 @@ +import { loadEnv } from "vite"; +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + exclude: [ + "**/node_modules/**", + "**/dist/**", + "**/test/integrations/**", // Exclude integration tests from default run + ], + }, + plugins: [tsconfigPaths({ projects: ["."] })], +})); diff --git a/typescript/packages/mechanisms/aptos/vitest.integration.config.ts b/typescript/packages/mechanisms/aptos/vitest.integration.config.ts new file mode 100644 index 0000000..0dd9d40 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/vitest.integration.config.ts @@ -0,0 +1,11 @@ +import { loadEnv } from "vite"; +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + include: ["test/integrations/**/*.test.ts"], // Only include integration tests + }, + plugins: [tsconfigPaths({ projects: ["."] })], +})); diff --git a/typescript/packages/mechanisms/evm/CHANGELOG.md b/typescript/packages/mechanisms/evm/CHANGELOG.md index afd1191..d83a846 100644 --- a/typescript/packages/mechanisms/evm/CHANGELOG.md +++ b/typescript/packages/mechanisms/evm/CHANGELOG.md @@ -1,5 +1,102 @@ # @x402/evm Changelog +## 2.9.0 + +### Minor Changes + +- 8c80edd: Add Polygon mainnet (chain ID 137) support with USDC as the default stablecoin +- bbe45f5: Add Stable mainnet (chain ID 988) support with USDT0 as the default stablecoin +- bff876d: Add Stable testnet (chain ID 2201) support with USDT0 as the default stablecoin +- 2250cae: Migrated project from coinbase/x402 to x402-foundation/x402 organization +- d352574: Add upto payment scheme TypeScript SDK with client, facilitator, and server support for permit2-based "up to" payments on EVM chains. + +### Patch Changes + +- 9f52f9c: Add Arbitrum One (chain ID 42161) and Arbitrum Sepolid (chain ID 421614) support with USDC as the default stablecoin +- 011e680: Add Mezo Testnet (chain ID 31611) support with mUSD as the default stablecoin +- ad2658a: Updated x402UptoPermit2Proxy canonical address to 0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002, deployed with deterministic bytecode for reproducible cross-chain CREATE2 addresses +- Updated dependencies [8cf3fca] +- Updated dependencies [c0e3969] +- Updated dependencies [2250cae] +- Updated dependencies [d352574] + - @x402/core@2.9.0 + +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- 8b731cb: Replaced `sendRawApprovalAndSettle` with a generic `sendTransactions` signer method that accepts an array of pre-signed serialized transactions or unsigned call intents. The signer owns execution strategy (sequential, batched, or atomic bundling). Closed fail-open verification paths, aligned Permit2 amount check to exact match, and added `signerForNetwork` to the extensions package. + +### Patch Changes + +- d8e9f3f: Added simulation to permit2 verify and (optional) settle +- 1a6e08b: Simulate transaction in verify and (optional) settle; Added multicall utility for efficient rpc calls; Fixed undeployed smart wallet handling to prevent facilitator grieving and account for implementation dependent verifyTypedData +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- f431337: Added assetTransferMethod and supportsEip2612 flag to defaultAssets +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + +## 2.5.0 + +### Minor Changes + +- 7fe268f: Implemented the erc20 approval gas sponsorship extension +- 33a9cab: Update Permit2 witness struct (remove extra field), contract addresses, and error names for post-audit x402 proxy contracts on Base Sepolia + +### Patch Changes + +- 55a4396: Separated v1 legacy network name resolution from v2 CAIP-2 resolution; getEvmChainId now only accepts eip155:CHAIN_ID format, v1 code uses getEvmChainIdV1 from v1/index +- Updated dependencies [96a9db0] +- Updated dependencies [7fe268f] +- Updated dependencies [1ab1c86] +- Updated dependencies [d0a2b11] +- Updated dependencies + - @x402/core@2.5.0 + - @x402/extensions@2.5.0 + +## 2.4.0 + +### Minor Changes + +- 018181b: Implement EIP-2612 gasless Permit2 approval extension + + - Implemented EIP-2612 gas sponsoring for the exact EVM scheme — clients automatically sign EIP-2612 permits when Permit2 allowance is insufficient, and facilitators route to `settleWithPermit` when the extension is present + +### Patch Changes + +- Updated dependencies [57a5488] +- Updated dependencies [018181b] +- Updated dependencies [3fb55d7] + - @x402/core@2.4.0 + - @x402/extensions@2.4.0 + +## 2.3.1 + +### Patch Changes + +- 0c6064d: Add MegaETH mainnet (chain ID 4326) support with USDM as the default stablecoin +- Updated dependencies [9ec9f15] + - @x402/core@2.3.1 + ## 2.3.0 ### Minor Changes diff --git a/typescript/packages/mechanisms/evm/README.md b/typescript/packages/mechanisms/evm/README.md index 6c9fff9..7fb2af2 100644 --- a/typescript/packages/mechanisms/evm/README.md +++ b/typescript/packages/mechanisms/evm/README.md @@ -57,33 +57,6 @@ This package provides three main components for handling x402 payments on EVM-co ] ``` -### Client Builder (`@x402/evm/client`) - -**Convenience builder** for creating fully-configured EVM clients - -**Exports:** -- `createEvmClient(config)` - Creates x402Client with EVM support -- `EvmClientConfig` - Configuration interface - -**What it does:** -- Automatically registers V2 wildcard scheme (`eip155:*`) -- Automatically registers all V1 networks from `NETWORKS` -- Optionally applies payment policies -- Optionally uses custom payment selector - -**Example:** -```typescript -import { createEvmClient } from "@x402/evm/client"; -import { toClientEvmSigner } from "@x402/evm"; -import { privateKeyToAccount } from "viem/accounts"; - -const account = privateKeyToAccount("0x..."); -const signer = toClientEvmSigner(account); - -const client = createEvmClient({ signer }); -// Ready to use with both V1 and V2! -``` - ## Version Differences ### V2 (Main Package) @@ -102,17 +75,7 @@ const client = createEvmClient({ signer }); ## Usage Patterns -### 1. Using Pre-built Builder (Recommended) - -```typescript -import { createEvmClient } from "@x402/evm/client"; -import { wrapFetchWithPayment } from "@x402/fetch"; - -const client = createEvmClient({ signer: myEvmSigner }); -const paidFetch = wrapFetchWithPayment(fetch, client); -``` - -### 2. Direct Registration (Full Control) +### 1. Direct Registration (Full Control) ```typescript import { x402Client } from "@x402/core/client"; @@ -125,7 +88,31 @@ const client = new x402Client() .registerSchemeV1("base", new ExactEvmClientV1(signer)); ``` -### 3. Using Config (Flexible) +### Extension RPC Configuration (Optional) + +`ExactEvmClient` only requires signer support for `address` + `signTypedData`. +Permit2 extension enrichment (EIP-2612 / ERC-20 approval gas sponsoring) can +optionally use explicit RPC config when signer read/fee helpers are unavailable. + +No chain-default RPC fallback is applied by the SDK. + +```typescript +// Per-network explicit registration +const client = new x402Client() + .register("eip155:137", new ExactEvmClient(signer, { rpcUrl: polygonRpcUrl })) + .register("eip155:8453", new ExactEvmClient(signer, { rpcUrl: baseRpcUrl })); + +// Wildcard registration with chain-id keyed config map +const wildcardClient = new x402Client().register( + "eip155:*", + new ExactEvmClient(signer, { + 137: { rpcUrl: polygonRpcUrl }, + 8453: { rpcUrl: baseRpcUrl }, + }), +); +``` + +### 2. Using Config (Flexible) ```typescript import { x402Client } from "@x402/core/client"; @@ -154,10 +141,11 @@ See `NETWORKS` constant in `@x402/evm/v1` ## Asset Support -Supports any ERC-3009 compatible token: -- USDC (primary) -- EURC -- Any token implementing `transferWithAuthorization()` +Supports two asset transfer methods: +- **EIP-3009**: Tokens with native `transferWithAuthorization()` (e.g., USDC, EURC) — simplest, truly gasless +- **Permit2**: Any ERC-20 token — universal fallback, requires one-time approval + +See [DEFAULT_ASSET.md](src/exact/server/DEFAULT_ASSET.md) for the current list of configured chains and how to add new ones. ## Development @@ -181,3 +169,4 @@ npm run format - `@x402/core` - Core protocol types and client - `@x402/fetch` - HTTP wrapper with automatic payment handling - `@x402/svm` - Solana/SVM implementation +- `@x402/stellar` - Stellar implementation diff --git a/typescript/packages/mechanisms/evm/package.json b/typescript/packages/mechanisms/evm/package.json index 8aea4e8..9f60222 100644 --- a/typescript/packages/mechanisms/evm/package.json +++ b/typescript/packages/mechanisms/evm/package.json @@ -1,6 +1,6 @@ { "name": "@x402/evm", - "version": "2.3.0", + "version": "2.9.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/cjs/index.d.ts", @@ -24,8 +24,8 @@ "ethereum" ], "license": "Apache-2.0", - "author": "Coinbase Inc.", - "repository": "https://github.com/coinbase/x402", + "author": "x402 Foundation", + "repository": "https://github.com/x402-foundation/x402", "description": "x402 Payment Protocol EVM Implementation", "devDependencies": { "@eslint/js": "^9.24.0", @@ -120,6 +120,26 @@ "default": "./dist/cjs/exact/v1/facilitator/index.js" } }, + "./upto/client": { + "import": { + "types": "./dist/esm/upto/client/index.d.mts", + "default": "./dist/esm/upto/client/index.mjs" + }, + "require": { + "types": "./dist/cjs/upto/client/index.d.ts", + "default": "./dist/cjs/upto/client/index.js" + } + }, + "./upto/server": { + "import": { + "types": "./dist/esm/upto/server/index.d.mts", + "default": "./dist/esm/upto/server/index.mjs" + }, + "require": { + "types": "./dist/cjs/upto/server/index.d.ts", + "default": "./dist/cjs/upto/server/index.js" + } + }, "./upto/facilitator": { "import": { "types": "./dist/esm/upto/facilitator/index.d.mts", diff --git a/typescript/packages/mechanisms/evm/src/constants.ts b/typescript/packages/mechanisms/evm/src/constants.ts index 9ed5f33..f81adff 100644 --- a/typescript/packages/mechanisms/evm/src/constants.ts +++ b/typescript/packages/mechanisms/evm/src/constants.ts @@ -11,7 +11,7 @@ export const authorizationTypes = { } as const; /** - * Permit2 EIP-712 types for signing PermitWitnessTransferFrom. + * Permit2 EIP-712 types for signing PermitWitnessTransferFrom (exact scheme). * Must match the exact format expected by the Permit2 contract. * Note: Types must be in ALPHABETICAL order after the primary type (TokenPermissions < Witness). */ @@ -30,7 +30,31 @@ export const permit2WitnessTypes = { Witness: [ { name: "to", type: "address" }, { name: "validAfter", type: "uint256" }, - { name: "extra", type: "bytes" }, + ], +} as const; + +/** + * Permit2 EIP-712 types for signing PermitWitnessTransferFrom (upto scheme). + * The upto witness includes a `facilitator` field that the exact witness does not. + * This ensures only the authorized facilitator can settle the payment. + * Must match: Witness(address to,address facilitator,uint256 validAfter) + */ +export const uptoPermit2WitnessTypes = { + PermitWitnessTransferFrom: [ + { name: "permitted", type: "TokenPermissions" }, + { name: "spender", type: "address" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + { name: "witness", type: "Witness" }, + ], + TokenPermissions: [ + { name: "token", type: "address" }, + { name: "amount", type: "uint256" }, + ], + Witness: [ + { name: "to", type: "address" }, + { name: "facilitator", type: "address" }, + { name: "validAfter", type: "uint256" }, ], } as const; @@ -82,16 +106,94 @@ export const eip3009ABI = [ stateMutability: "view", type: "function", }, + { + inputs: [], + name: "name", + outputs: [{ name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { name: "authorizer", type: "address" }, + { name: "nonce", type: "bytes32" }, + ], + name: "authorizationState", + outputs: [{ name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, ] as const; +/** + * EIP-2612 Permit EIP-712 types for signing token.permit(). + */ +export const eip2612PermitTypes = { + Permit: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], +} as const; + +/** + * EIP-2612 nonces ABI for querying current nonce. + */ +export const eip2612NoncesAbi = [ + { + type: "function", + name: "nonces", + inputs: [{ name: "owner", type: "address" }], + outputs: [{ type: "uint256" }], + stateMutability: "view", + }, +] as const; + +/** ERC-20 approve(address,uint256) ABI for encoding/decoding approval calldata. */ +export const erc20ApproveAbi = [ + { + type: "function", + name: "approve", + inputs: [ + { name: "spender", type: "address" }, + { name: "amount", type: "uint256" }, + ], + outputs: [{ type: "bool" }], + stateMutability: "nonpayable", + }, +] as const; + +/** ERC-20 allowance(address,address) ABI for checking spender approval. */ +export const erc20AllowanceAbi = [ + { + type: "function", + name: "allowance", + inputs: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + ], + outputs: [{ type: "uint256" }], + stateMutability: "view", + }, +] as const; + +/** Gas limit for a standard ERC-20 approve() transaction. */ +export const ERC20_APPROVE_GAS_LIMIT = 70_000n; + +/** Fallback max fee per gas (1 gwei) when fee estimation fails. */ +export const DEFAULT_MAX_FEE_PER_GAS = 1_000_000_000n; + +/** Fallback max priority fee per gas (0.1 gwei) when fee estimation fails. */ +export const DEFAULT_MAX_PRIORITY_FEE_PER_GAS = 100_000_000n; + /** * Canonical Permit2 contract address. * Same address on all EVM chains via CREATE2 deployment. * * @see https://github.com/Uniswap/permit2 */ - -// TODO: revert after precompile upgrade export const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3" as const; /** @@ -102,7 +204,7 @@ export const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3" as c * - Vanity-mined salt for prefix 0x4020 and suffix 0001 * - Contract bytecode + constructor args (PERMIT2_ADDRESS) */ -export const x402ExactPermit2ProxyAddress = "0xBe08D629cc799E6C17200F454F68A61E017038C8" as const; +export const x402ExactPermit2ProxyAddress = "0x402085c248EeA27D92E8b30b2C58ed07f9E20001" as const; /** * x402UptoPermit2Proxy contract address. @@ -112,10 +214,30 @@ export const x402ExactPermit2ProxyAddress = "0xBe08D629cc799E6C17200F454F68A61E0 * - Vanity-mined salt for prefix 0x4020 and suffix 0002 * - Contract bytecode + constructor args (PERMIT2_ADDRESS) */ -export const x402UptoPermit2ProxyAddress = "0xBe08D629cc799E6C17200F454F68A61E017038C8" as const; +export const x402UptoPermit2ProxyAddress = "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002" as const; /** - * x402UptoPermit2Proxy ABI - settle function for upto payment scheme (variable amounts). + * ABI components for the exact Permit2 witness tuple: Witness(address to, uint256 validAfter). + */ +const permit2WitnessABIComponents = [ + { name: "to", type: "address", internalType: "address" }, + { name: "validAfter", type: "uint256", internalType: "uint256" }, +] as const; + +/** + * ABI components for the upto Permit2 witness tuple: + * Witness(address to, address facilitator, uint256 validAfter). + */ +const uptoPermit2WitnessABIComponents = [ + { name: "to", type: "address", internalType: "address" }, + { name: "facilitator", type: "address", internalType: "address" }, + { name: "validAfter", type: "uint256", internalType: "uint256" }, +] as const; + +/** + * x402UptoPermit2Proxy ABI — settle/settleWithPermit for the upto payment scheme. + * Key differences from exact: settle() takes a `uint256 amount` parameter, and the + * Witness struct includes an `address facilitator` field. */ export const x402UptoPermit2ProxyABI = [ { @@ -139,13 +261,6 @@ export const x402UptoPermit2ProxyABI = [ outputs: [{ name: "", type: "string", internalType: "string" }], stateMutability: "view", }, - { - type: "function", - name: "initialize", - inputs: [{ name: "_permit2", type: "address", internalType: "address" }], - outputs: [], - stateMutability: "nonpayable", - }, { type: "function", name: "settle", @@ -168,19 +283,15 @@ export const x402UptoPermit2ProxyABI = [ { name: "deadline", type: "uint256", internalType: "uint256" }, ], }, + { name: "amount", type: "uint256", internalType: "uint256" }, { name: "owner", type: "address", internalType: "address" }, { name: "witness", type: "tuple", - internalType: "struct x402BasePermit2Proxy.Witness", - components: [ - { name: "to", type: "address", internalType: "address" }, - { name: "validAfter", type: "uint256", internalType: "uint256" }, - { name: "extra", type: "bytes", internalType: "bytes" }, - ], + internalType: "struct x402UptoPermit2Proxy.Witness", + components: uptoPermit2WitnessABIComponents, }, { name: "signature", type: "bytes", internalType: "bytes" }, - { name: "actualAmount", type: "uint256", internalType: "uint256" }, ], outputs: [], stateMutability: "nonpayable", @@ -192,7 +303,7 @@ export const x402UptoPermit2ProxyABI = [ { name: "permit2612", type: "tuple", - internalType: "struct x402BasePermit2Proxy.EIP2612Permit", + internalType: "struct x402UptoPermit2Proxy.EIP2612Permit", components: [ { name: "value", type: "uint256", internalType: "uint256" }, { name: "deadline", type: "uint256", internalType: "uint256" }, @@ -219,16 +330,13 @@ export const x402UptoPermit2ProxyABI = [ { name: "deadline", type: "uint256", internalType: "uint256" }, ], }, + { name: "amount", type: "uint256", internalType: "uint256" }, { name: "owner", type: "address", internalType: "address" }, { name: "witness", type: "tuple", - internalType: "struct x402BasePermit2Proxy.Witness", - components: [ - { name: "to", type: "address", internalType: "address" }, - { name: "validAfter", type: "uint256", internalType: "uint256" }, - { name: "extra", type: "bytes", internalType: "bytes" }, - ], + internalType: "struct x402UptoPermit2Proxy.Witness", + components: uptoPermit2WitnessABIComponents, }, { name: "signature", type: "bytes", internalType: "bytes" }, ], @@ -237,13 +345,14 @@ export const x402UptoPermit2ProxyABI = [ }, { type: "event", name: "Settled", inputs: [], anonymous: false }, { type: "event", name: "SettledWithPermit", inputs: [], anonymous: false }, - { type: "error", name: "AlreadyInitialized", inputs: [] }, { type: "error", name: "AmountExceedsPermitted", inputs: [] }, { type: "error", name: "InvalidDestination", inputs: [] }, { type: "error", name: "InvalidOwner", inputs: [] }, { type: "error", name: "InvalidPermit2Address", inputs: [] }, { type: "error", name: "PaymentTooEarly", inputs: [] }, + { type: "error", name: "Permit2612AmountMismatch", inputs: [] }, { type: "error", name: "ReentrancyGuardReentrantCall", inputs: [] }, + { type: "error", name: "UnauthorizedFacilitator", inputs: [] }, ] as const; /** @@ -271,13 +380,6 @@ export const x402ExactPermit2ProxyABI = [ outputs: [{ name: "", type: "string", internalType: "string" }], stateMutability: "view", }, - { - type: "function", - name: "initialize", - inputs: [{ name: "_permit2", type: "address", internalType: "address" }], - outputs: [], - stateMutability: "nonpayable", - }, { type: "function", name: "settle", @@ -304,12 +406,8 @@ export const x402ExactPermit2ProxyABI = [ { name: "witness", type: "tuple", - internalType: "struct x402BasePermit2Proxy.Witness", - components: [ - { name: "to", type: "address", internalType: "address" }, - { name: "validAfter", type: "uint256", internalType: "uint256" }, - { name: "extra", type: "bytes", internalType: "bytes" }, - ], + internalType: "struct x402ExactPermit2Proxy.Witness", + components: permit2WitnessABIComponents, }, { name: "signature", type: "bytes", internalType: "bytes" }, ], @@ -323,7 +421,7 @@ export const x402ExactPermit2ProxyABI = [ { name: "permit2612", type: "tuple", - internalType: "struct x402BasePermit2Proxy.EIP2612Permit", + internalType: "struct x402ExactPermit2Proxy.EIP2612Permit", components: [ { name: "value", type: "uint256", internalType: "uint256" }, { name: "deadline", type: "uint256", internalType: "uint256" }, @@ -354,12 +452,8 @@ export const x402ExactPermit2ProxyABI = [ { name: "witness", type: "tuple", - internalType: "struct x402BasePermit2Proxy.Witness", - components: [ - { name: "to", type: "address", internalType: "address" }, - { name: "validAfter", type: "uint256", internalType: "uint256" }, - { name: "extra", type: "bytes", internalType: "bytes" }, - ], + internalType: "struct x402ExactPermit2Proxy.Witness", + components: permit2WitnessABIComponents, }, { name: "signature", type: "bytes", internalType: "bytes" }, ], @@ -368,10 +462,11 @@ export const x402ExactPermit2ProxyABI = [ }, { type: "event", name: "Settled", inputs: [], anonymous: false }, { type: "event", name: "SettledWithPermit", inputs: [], anonymous: false }, - { type: "error", name: "AlreadyInitialized", inputs: [] }, + { type: "error", name: "InvalidAmount", inputs: [] }, { type: "error", name: "InvalidDestination", inputs: [] }, { type: "error", name: "InvalidOwner", inputs: [] }, { type: "error", name: "InvalidPermit2Address", inputs: [] }, { type: "error", name: "PaymentTooEarly", inputs: [] }, + { type: "error", name: "Permit2612AmountMismatch", inputs: [] }, { type: "error", name: "ReentrancyGuardReentrantCall", inputs: [] }, ] as const; diff --git a/typescript/packages/mechanisms/evm/src/exact/client/eip2612.ts b/typescript/packages/mechanisms/evm/src/exact/client/eip2612.ts new file mode 100644 index 0000000..f2b86de --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/exact/client/eip2612.ts @@ -0,0 +1,84 @@ +import { getAddress } from "viem"; +import { eip2612PermitTypes, eip2612NoncesAbi, PERMIT2_ADDRESS } from "../../constants"; +import { ClientEvmSigner } from "../../signer"; +import type { Eip2612GasSponsoringInfo } from "../extensions"; + +export type Eip2612PermitSigner = Pick & { + readContract: NonNullable; +}; + +/** + * Signs an EIP-2612 permit authorizing the Permit2 contract to spend tokens. + * + * This creates a gasless off-chain signature that the facilitator can submit + * on-chain via `x402Permit2Proxy.settleWithPermit()`. + * + * The `permittedAmount` must match the Permit2 `permitted.amount` exactly, as the + * proxy contract enforces `permit2612.value == permittedAmount`. + * + * @param signer - The client EVM signer (must support readContract for nonce query) + * @param tokenAddress - The ERC-20 token contract address + * @param tokenName - The token name (from paymentRequirements.extra.name) + * @param tokenVersion - The token version (from paymentRequirements.extra.version) + * @param chainId - The chain ID + * @param deadline - The deadline for the permit (unix timestamp as string) + * @param permittedAmount - The Permit2 permitted amount (must match exactly) + * @returns The EIP-2612 gas sponsoring info object + */ +export async function signEip2612Permit( + signer: Eip2612PermitSigner, + tokenAddress: `0x${string}`, + tokenName: string, + tokenVersion: string, + chainId: number, + deadline: string, + permittedAmount: string, +): Promise { + const owner = signer.address; + const spender = getAddress(PERMIT2_ADDRESS); + + // Query the current EIP-2612 nonce from the token contract + const nonce = (await signer.readContract({ + address: tokenAddress, + abi: eip2612NoncesAbi, + functionName: "nonces", + args: [owner], + })) as bigint; + + // Construct EIP-712 domain for the token's permit function + const domain = { + name: tokenName, + version: tokenVersion, + chainId, + verifyingContract: tokenAddress, + }; + + const approvalAmount = BigInt(permittedAmount); + + const message = { + owner, + spender, + value: approvalAmount, + nonce, + deadline: BigInt(deadline), + }; + + // Sign the EIP-2612 permit + const signature = await signer.signTypedData({ + domain, + types: eip2612PermitTypes, + primaryType: "Permit", + message, + }); + + return { + from: owner, + asset: tokenAddress, + spender, + amount: approvalAmount.toString(), + nonce: nonce.toString(), + deadline, + signature, + version: "1", + }; +} diff --git a/typescript/packages/mechanisms/evm/src/exact/client/eip3009.ts b/typescript/packages/mechanisms/evm/src/exact/client/eip3009.ts index 70ee139..77b4403 100644 --- a/typescript/packages/mechanisms/evm/src/exact/client/eip3009.ts +++ b/typescript/packages/mechanisms/evm/src/exact/client/eip3009.ts @@ -3,7 +3,7 @@ import { getAddress } from "viem"; import { authorizationTypes } from "../../constants"; import { ClientEvmSigner } from "../../signer"; import { ExactEIP3009Payload } from "../../types"; -import { createNonce } from "../../utils"; +import { createNonce, getEvmChainId } from "../../utils"; /** * Creates an EIP-3009 (transferWithAuthorization) payload. @@ -56,7 +56,7 @@ async function signEIP3009Authorization( authorization: ExactEIP3009Payload["authorization"], requirements: PaymentRequirements, ): Promise<`0x${string}`> { - const chainId = parseInt(requirements.network.split(":")[1]); + const chainId = getEvmChainId(requirements.network); if (!requirements.extra?.name || !requirements.extra?.version) { throw new Error( diff --git a/typescript/packages/mechanisms/evm/src/exact/client/erc20approval.ts b/typescript/packages/mechanisms/evm/src/exact/client/erc20approval.ts new file mode 100644 index 0000000..78d24b5 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/exact/client/erc20approval.ts @@ -0,0 +1,87 @@ +import { encodeFunctionData, getAddress, maxUint256 } from "viem"; +import { + PERMIT2_ADDRESS, + erc20ApproveAbi, + ERC20_APPROVE_GAS_LIMIT, + DEFAULT_MAX_FEE_PER_GAS, + DEFAULT_MAX_PRIORITY_FEE_PER_GAS, +} from "../../constants"; +import { ClientEvmSigner } from "../../signer"; +import { + ERC20_APPROVAL_GAS_SPONSORING_VERSION, + type Erc20ApprovalGasSponsoringInfo, +} from "../extensions"; + +export type Erc20ApprovalTxSigner = Pick & { + signTransaction: NonNullable; + getTransactionCount: NonNullable; + estimateFeesPerGas?: NonNullable; +}; + +/** + * Signs an EIP-1559 `approve(Permit2, MaxUint256)` transaction for the given token. + * + * The signed transaction is NOT broadcast here — the facilitator broadcasts it + * atomically before settling the Permit2 payment. This enables Permit2 payments + * for generic ERC-20 tokens that do NOT implement EIP-2612. + * + * Always approves MaxUint256 regardless of the payment amount. + * + * @param signer - The client EVM signer (must support signTransaction, getTransactionCount) + * @param tokenAddress - The ERC-20 token contract address + * @param chainId - The chain ID + * @returns The ERC-20 approval gas sponsoring info object + */ +export async function signErc20ApprovalTransaction( + signer: Erc20ApprovalTxSigner, + tokenAddress: `0x${string}`, + chainId: number, +): Promise { + const from = signer.address; + const spender = getAddress(PERMIT2_ADDRESS); + + // Encode approve(PERMIT2_ADDRESS, MaxUint256) calldata + const data = encodeFunctionData({ + abi: erc20ApproveAbi, + functionName: "approve", + args: [spender, maxUint256], + }); + + // Get current nonce for the sender + const nonce = await signer.getTransactionCount({ address: from }); + + // Get current fee estimates, with fallback values + let maxFeePerGas: bigint; + let maxPriorityFeePerGas: bigint; + try { + const fees = await signer.estimateFeesPerGas?.(); + if (!fees) { + throw new Error("no fee estimates available"); + } + maxFeePerGas = fees.maxFeePerGas; + maxPriorityFeePerGas = fees.maxPriorityFeePerGas; + } catch { + maxFeePerGas = DEFAULT_MAX_FEE_PER_GAS; + maxPriorityFeePerGas = DEFAULT_MAX_PRIORITY_FEE_PER_GAS; + } + + // Sign the EIP-1559 transaction (not broadcast) + const signedTransaction = await signer.signTransaction({ + to: tokenAddress, + data, + nonce, + gas: ERC20_APPROVE_GAS_LIMIT, + maxFeePerGas, + maxPriorityFeePerGas, + chainId, + }); + + return { + from, + asset: tokenAddress, + spender, + amount: maxUint256.toString(), + signedTransaction, + version: ERC20_APPROVAL_GAS_SPONSORING_VERSION, + }; +} diff --git a/typescript/packages/mechanisms/evm/src/exact/client/index.ts b/typescript/packages/mechanisms/evm/src/exact/client/index.ts index 492a56d..6d0958b 100644 --- a/typescript/packages/mechanisms/evm/src/exact/client/index.ts +++ b/typescript/packages/mechanisms/evm/src/exact/client/index.ts @@ -1,9 +1,14 @@ export { ExactEvmScheme } from "./scheme"; export { registerExactEvmScheme } from "./register"; export type { EvmClientConfig } from "./register"; +export type { + ExactEvmSchemeConfig, + ExactEvmSchemeConfigByChainId, + ExactEvmSchemeOptions, +} from "./rpc"; export { createPermit2ApprovalTx, getPermit2AllowanceReadParams, - erc20AllowanceAbi, type Permit2AllowanceParams, } from "./permit2"; +export { erc20AllowanceAbi } from "../../constants"; diff --git a/typescript/packages/mechanisms/evm/src/exact/client/permit2.ts b/typescript/packages/mechanisms/evm/src/exact/client/permit2.ts index a0214a3..ba02333 100644 --- a/typescript/packages/mechanisms/evm/src/exact/client/permit2.ts +++ b/typescript/packages/mechanisms/evm/src/exact/client/permit2.ts @@ -1,13 +1,13 @@ import { PaymentRequirements, PaymentPayloadResult } from "@x402/core/types"; import { encodeFunctionData, getAddress } from "viem"; import { - permit2WitnessTypes, PERMIT2_ADDRESS, x402ExactPermit2ProxyAddress, + erc20ApproveAbi, + erc20AllowanceAbi, } from "../../constants"; import { ClientEvmSigner } from "../../signer"; -import { ExactPermit2Payload } from "../../types"; -import { createPermit2Nonce } from "../../utils"; +import { createPermit2PayloadForProxy } from "../../shared/permit2"; /** Maximum uint256 value for unlimited approval. */ const MAX_UINT256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); @@ -27,124 +27,14 @@ export async function createPermit2Payload( x402Version: number, paymentRequirements: PaymentRequirements, ): Promise { - const now = Math.floor(Date.now() / 1000); - const nonce = createPermit2Nonce(); - - // Lower time bound - allow some clock skew - const validAfter = (now - 600).toString(); - // Upper time bound is enforced by Permit2's deadline field - const deadline = (now + paymentRequirements.maxTimeoutSeconds).toString(); - - const permit2Authorization: ExactPermit2Payload["permit2Authorization"] = { - from: signer.address, - permitted: { - token: getAddress(paymentRequirements.asset), - amount: paymentRequirements.amount, - }, - spender: x402ExactPermit2ProxyAddress, - nonce, - deadline, - witness: { - to: getAddress(paymentRequirements.payTo), - validAfter, - extra: "0x", - }, - }; - - const signature = await signPermit2Authorization( + return createPermit2PayloadForProxy( + x402ExactPermit2ProxyAddress, signer, - permit2Authorization, + x402Version, paymentRequirements, ); - - const payload: ExactPermit2Payload = { - signature, - permit2Authorization, - }; - - return { - x402Version, - payload, - }; } -/** - * Sign the Permit2 authorization using EIP-712 with witness data. - * The signature authorizes the x402Permit2Proxy to transfer tokens on behalf of the signer. - * - * @param signer - The EVM signer - * @param permit2Authorization - The Permit2 authorization parameters - * @param requirements - The payment requirements - * @returns Promise resolving to the signature - */ -async function signPermit2Authorization( - signer: ClientEvmSigner, - permit2Authorization: ExactPermit2Payload["permit2Authorization"], - requirements: PaymentRequirements, -): Promise<`0x${string}`> { - const chainId = parseInt(requirements.network.split(":")[1]); - - const domain = { - name: "Permit2", - chainId, - verifyingContract: PERMIT2_ADDRESS, - }; - - const message = { - permitted: { - token: getAddress(permit2Authorization.permitted.token), - amount: BigInt(permit2Authorization.permitted.amount), - }, - spender: getAddress(permit2Authorization.spender), - nonce: BigInt(permit2Authorization.nonce), - deadline: BigInt(permit2Authorization.deadline), - witness: { - to: getAddress(permit2Authorization.witness.to), - validAfter: BigInt(permit2Authorization.witness.validAfter), - extra: permit2Authorization.witness.extra, - }, - }; - - return await signer.signTypedData({ - domain, - types: permit2WitnessTypes, - primaryType: "PermitWitnessTransferFrom", - message, - }); -} - -/** - * ERC20 approve ABI for encoding approval transactions. - */ -const erc20ApproveAbi = [ - { - type: "function", - name: "approve", - inputs: [ - { name: "spender", type: "address" }, - { name: "amount", type: "uint256" }, - ], - outputs: [{ type: "bool" }], - stateMutability: "nonpayable", - }, -] as const; - -/** - * ERC20 allowance ABI for checking approval status. - */ -export const erc20AllowanceAbi = [ - { - type: "function", - name: "allowance", - inputs: [ - { name: "owner", type: "address" }, - { name: "spender", type: "address" }, - ], - outputs: [{ type: "uint256" }], - stateMutability: "view", - }, -] as const; - /** * Creates transaction data to approve Permit2 to spend tokens. * The user sends this transaction (paying gas) before using Permit2 flow. diff --git a/typescript/packages/mechanisms/evm/src/exact/client/register.ts b/typescript/packages/mechanisms/evm/src/exact/client/register.ts index bbcc629..3e91e13 100644 --- a/typescript/packages/mechanisms/evm/src/exact/client/register.ts +++ b/typescript/packages/mechanisms/evm/src/exact/client/register.ts @@ -2,6 +2,7 @@ import { x402Client, SelectPaymentRequirements, PaymentPolicy } from "@x402/core import { Network } from "@x402/core/types"; import { ClientEvmSigner } from "../../signer"; import { ExactEvmScheme } from "./scheme"; +import { ExactEvmSchemeOptions } from "./rpc"; import { ExactEvmSchemeV1 } from "../v1/client/scheme"; import { NETWORKS } from "../../v1"; @@ -26,8 +27,15 @@ export interface EvmClientConfig { policies?: PaymentPolicy[]; /** - * Optional specific networks to register - * If not provided, registers wildcard support (eip155:*) + * Optional Exact EVM client scheme options. + * Supports either a single config ({ rpcUrl }) or per-chain configs + * keyed by EVM chain ID ({ 8453: { rpcUrl: "..." } }). + */ + schemeOptions?: ExactEvmSchemeOptions; + + /** + * Optional specific networks to register. + * If not provided, registers wildcard support (eip155:*). */ networks?: Network[]; } @@ -55,15 +63,19 @@ export interface EvmClientConfig { * ``` */ export function registerExactEvmScheme(client: x402Client, config: EvmClientConfig): x402Client { + const evmScheme = new ExactEvmScheme(config.signer, config.schemeOptions); + // Register V2 scheme + // EIP-2612 gas sponsoring is handled internally by the scheme when the + // server advertises support - no separate extension registration needed. if (config.networks && config.networks.length > 0) { // Register specific networks config.networks.forEach(network => { - client.register(network, new ExactEvmScheme(config.signer)); + client.register(network, evmScheme); }); } else { // Register wildcard for all EVM chains - client.register("eip155:*", new ExactEvmScheme(config.signer)); + client.register("eip155:*", evmScheme); } // Register all V1 networks diff --git a/typescript/packages/mechanisms/evm/src/exact/client/rpc.ts b/typescript/packages/mechanisms/evm/src/exact/client/rpc.ts new file mode 100644 index 0000000..7c43187 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/exact/client/rpc.ts @@ -0,0 +1,11 @@ +// Re-export from shared for backward compatibility +export { + type EvmSchemeConfig, + type EvmSchemeConfigByChainId, + type EvmSchemeOptions, + type ExactEvmSchemeConfig, + type ExactEvmSchemeConfigByChainId, + type ExactEvmSchemeOptions, + resolveExtensionRpcCapabilities, + resolveRpcUrl, +} from "../../shared/rpc"; diff --git a/typescript/packages/mechanisms/evm/src/exact/client/scheme.ts b/typescript/packages/mechanisms/evm/src/exact/client/scheme.ts index e0bc688..50202bd 100644 --- a/typescript/packages/mechanisms/evm/src/exact/client/scheme.ts +++ b/typescript/packages/mechanisms/evm/src/exact/client/scheme.ts @@ -1,8 +1,18 @@ -import { PaymentRequirements, SchemeNetworkClient, PaymentPayloadResult } from "@x402/core/types"; +import { + SchemeNetworkClient, + PaymentRequirements, + PaymentPayloadResult, + PaymentPayloadContext, +} from "@x402/core/types"; import { ClientEvmSigner } from "../../signer"; import { AssetTransferMethod } from "../../types"; import { createEIP3009Payload } from "./eip3009"; import { createPermit2Payload } from "./permit2"; +import { + trySignEip2612PermitExtension, + trySignErc20ApprovalExtension, +} from "../../shared/extensions"; +import { ExactEvmSchemeOptions } from "./rpc"; /** * EVM client implementation for the Exact payment scheme. @@ -11,6 +21,10 @@ import { createPermit2Payload } from "./permit2"; * Routes to the appropriate authorization method based on * `requirements.extra.assetTransferMethod`. Defaults to EIP-3009 * for backward compatibility with older facilitators. + * + * When the server advertises `eip2612GasSponsoring` and the asset transfer + * method is `permit2`, the scheme automatically signs an EIP-2612 permit + * if the user lacks Permit2 approval. This requires `readContract` on the signer. */ export class ExactEvmScheme implements SchemeNetworkClient { readonly scheme = "exact"; @@ -18,27 +32,70 @@ export class ExactEvmScheme implements SchemeNetworkClient { /** * Creates a new ExactEvmClient instance. * - * @param signer - The EVM signer for client operations + * @param signer - The EVM signer for client operations. + * Base flow only requires `address` + `signTypedData`. + * Extension enrichment (EIP-2612 / ERC-20 approval sponsoring) additionally + * requires optional capabilities like `readContract` and tx signing helpers. + * @param options - Optional RPC configuration used to backfill extension capabilities. */ - constructor(private readonly signer: ClientEvmSigner) {} + constructor( + private readonly signer: ClientEvmSigner, + private readonly options?: ExactEvmSchemeOptions, + ) {} /** * Creates a payment payload for the Exact scheme. * Routes to EIP-3009 or Permit2 based on requirements.extra.assetTransferMethod. * + * For Permit2 flows, if the server advertises `eip2612GasSponsoring` and the + * signer supports `readContract`, automatically signs an EIP-2612 permit + * when Permit2 allowance is insufficient. + * * @param x402Version - The x402 protocol version * @param paymentRequirements - The payment requirements - * @returns Promise resolving to a payment payload result + * @param context - Optional context with server-declared extensions + * @returns Promise resolving to a payment payload result (with optional extensions) */ async createPaymentPayload( x402Version: number, paymentRequirements: PaymentRequirements, + context?: PaymentPayloadContext, ): Promise { const assetTransferMethod = (paymentRequirements.extra?.assetTransferMethod as AssetTransferMethod) ?? "eip3009"; if (assetTransferMethod === "permit2") { - return createPermit2Payload(this.signer, x402Version, paymentRequirements); + const result = await createPermit2Payload(this.signer, x402Version, paymentRequirements); + + const eip2612Extensions = await trySignEip2612PermitExtension( + this.signer, + this.options, + paymentRequirements, + result, + context, + ); + + if (eip2612Extensions) { + return { + ...result, + extensions: eip2612Extensions, + }; + } + + const erc20Extensions = await trySignErc20ApprovalExtension( + this.signer, + this.options, + paymentRequirements, + context, + ); + if (erc20Extensions) { + return { + ...result, + extensions: erc20Extensions, + }; + } + + return result; } return createEIP3009Payload(this.signer, x402Version, paymentRequirements); diff --git a/typescript/packages/mechanisms/evm/src/exact/extensions.ts b/typescript/packages/mechanisms/evm/src/exact/extensions.ts new file mode 100644 index 0000000..d3e3fe4 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/exact/extensions.ts @@ -0,0 +1,177 @@ +import type { PaymentPayload } from "@x402/core/types"; +import type { FacilitatorEvmSigner } from "../signer"; + +export const EIP2612_GAS_SPONSORING_KEY = "eip2612GasSponsoring" as const; +export const ERC20_APPROVAL_GAS_SPONSORING_KEY = "erc20ApprovalGasSponsoring" as const; +export const ERC20_APPROVAL_GAS_SPONSORING_VERSION = "1" as const; + +export interface Eip2612GasSponsoringInfo { + [key: string]: unknown; + from: string; + asset: string; + spender: string; + amount: string; + nonce: string; + deadline: string; + signature: string; + version: string; +} + +export interface Erc20ApprovalGasSponsoringInfo { + [key: string]: unknown; + from: `0x${string}`; + asset: `0x${string}`; + spender: `0x${string}`; + amount: string; + signedTransaction: `0x${string}`; + version: string; +} + +/** + * A single transaction to be executed by the signer. + * - `0x${string}`: a pre-signed serialized transaction (broadcast as-is via sendRawTransaction) + * - `{ to, data, gas? }`: an unsigned call intent (signer signs and broadcasts) + */ +export type TransactionRequest = + | `0x${string}` + | { to: `0x${string}`; data: `0x${string}`; gas?: bigint }; + +export type Erc20ApprovalGasSponsoringSigner = FacilitatorEvmSigner & { + sendTransactions(transactions: TransactionRequest[]): Promise<`0x${string}`[]>; + simulateTransactions?(transactions: TransactionRequest[]): Promise; +}; + +export interface Erc20ApprovalGasSponsoringFacilitatorExtension { + key: typeof ERC20_APPROVAL_GAS_SPONSORING_KEY; + signer?: Erc20ApprovalGasSponsoringSigner; + signerForNetwork?: (network: string) => Erc20ApprovalGasSponsoringSigner | undefined; +} + +/** + * Extracts a typed `info` payload from an extension entry. + * + * @param payload - Payment payload containing optional extensions. + * @param extensionKey - Extension key to extract. + * @returns The extension `info` object when present; otherwise null. + */ +function _extractInfo( + payload: PaymentPayload, + extensionKey: string, +): Record | null { + const extensions = payload.extensions; + if (!extensions) return null; + const extension = extensions[extensionKey] as { info?: Record } | undefined; + if (!extension?.info) return null; + return extension.info; +} + +/** + * Extracts and validates required EIP-2612 gas sponsoring fields. + * + * @param payload - Payment payload returned by the client scheme. + * @returns Parsed EIP-2612 gas sponsoring info when available and complete. + */ +export function extractEip2612GasSponsoringInfo( + payload: PaymentPayload, +): Eip2612GasSponsoringInfo | null { + const info = _extractInfo(payload, EIP2612_GAS_SPONSORING_KEY); + if (!info) return null; + if ( + !info.from || + !info.asset || + !info.spender || + !info.amount || + !info.nonce || + !info.deadline || + !info.signature || + !info.version + ) { + return null; + } + return info as unknown as Eip2612GasSponsoringInfo; +} + +/** + * Validates the structure and formatting of EIP-2612 sponsoring info. + * + * @param info - EIP-2612 extension info to validate. + * @returns True when all required fields match expected patterns. + */ +export function validateEip2612GasSponsoringInfo(info: Eip2612GasSponsoringInfo): boolean { + const addressPattern = /^0x[a-fA-F0-9]{40}$/; + const numericPattern = /^[0-9]+$/; + const hexPattern = /^0x[a-fA-F0-9]+$/; + const versionPattern = /^[0-9]+(\.[0-9]+)*$/; + return ( + addressPattern.test(info.from) && + addressPattern.test(info.asset) && + addressPattern.test(info.spender) && + numericPattern.test(info.amount) && + numericPattern.test(info.nonce) && + numericPattern.test(info.deadline) && + hexPattern.test(info.signature) && + versionPattern.test(info.version) + ); +} + +/** + * Extracts and validates required ERC-20 approval sponsoring fields. + * + * @param payload - Payment payload returned by the client scheme. + * @returns Parsed ERC-20 approval sponsoring info when available and complete. + */ +export function extractErc20ApprovalGasSponsoringInfo( + payload: PaymentPayload, +): Erc20ApprovalGasSponsoringInfo | null { + const info = _extractInfo(payload, ERC20_APPROVAL_GAS_SPONSORING_KEY); + if (!info) return null; + if ( + !info.from || + !info.asset || + !info.spender || + !info.amount || + !info.signedTransaction || + !info.version + ) { + return null; + } + return info as unknown as Erc20ApprovalGasSponsoringInfo; +} + +/** + * Validates the structure and formatting of ERC-20 approval sponsoring info. + * + * @param info - ERC-20 approval extension info to validate. + * @returns True when all required fields match expected patterns. + */ +export function validateErc20ApprovalGasSponsoringInfo( + info: Erc20ApprovalGasSponsoringInfo, +): boolean { + const addressPattern = /^0x[a-fA-F0-9]{40}$/; + const numericPattern = /^[0-9]+$/; + const hexPattern = /^0x[a-fA-F0-9]+$/; + const versionPattern = /^[0-9]+(\.[0-9]+)*$/; + return ( + addressPattern.test(info.from) && + addressPattern.test(info.asset) && + addressPattern.test(info.spender) && + numericPattern.test(info.amount) && + hexPattern.test(info.signedTransaction) && + versionPattern.test(info.version) + ); +} + +/** + * Resolves the ERC-20 approval extension signer for a specific network. + * + * @param extension - Optional facilitator extension config. + * @param network - CAIP-2 network identifier. + * @returns A network-specific signer when available, else the default signer. + */ +export function resolveErc20ApprovalExtensionSigner( + extension: Erc20ApprovalGasSponsoringFacilitatorExtension | undefined, + network: string, +): Erc20ApprovalGasSponsoringSigner | undefined { + if (!extension) return undefined; + return extension.signerForNetwork?.(network) ?? extension.signer; +} diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009-utils.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009-utils.ts new file mode 100644 index 0000000..2d8f2c4 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009-utils.ts @@ -0,0 +1,240 @@ +import { PaymentRequirements, VerifyResponse } from "@x402/core/types"; +import { encodeFunctionData, getAddress, Hex, parseErc6492Signature, parseSignature } from "viem"; +import { eip3009ABI } from "../../constants"; +import { multicall, ContractCall, RawContractCall } from "../../multicall"; +import { FacilitatorEvmSigner } from "../../signer"; +import { ExactEIP3009Payload } from "../../types"; +import * as Errors from "./errors"; + +export interface Eip6492Deployment { + factoryAddress: `0x${string}`; + factoryCalldata: `0x${string}`; +} + +/** + * Simulates transferWithAuthorization via eth_call. + * Returns true if simulation succeeded, false if it failed. + * + * @param signer - EVM signer for contract reads + * @param erc20Address - ERC-20 token contract address + * @param payload - EIP-3009 transfer authorization payload + * @param eip6492Deployment - Optional EIP-6492 factory info for undeployed smart wallets + * + * @returns true if simulation succeeded, false if it failed + */ +export async function simulateEip3009Transfer( + signer: FacilitatorEvmSigner, + erc20Address: `0x${string}`, + payload: ExactEIP3009Payload, + eip6492Deployment?: Eip6492Deployment, +): Promise { + const auth = payload.authorization; + const transferArgs = [ + getAddress(auth.from), + getAddress(auth.to), + BigInt(auth.value), + BigInt(auth.validAfter), + BigInt(auth.validBefore), + auth.nonce, + ] as const; + + if (eip6492Deployment) { + const { signature: innerSignature } = parseErc6492Signature(payload.signature!); + const transferCalldata = encodeFunctionData({ + abi: eip3009ABI, + functionName: "transferWithAuthorization", + args: [...transferArgs, innerSignature], + }); + + try { + const results = await multicall(signer.readContract.bind(signer), [ + { + address: getAddress(eip6492Deployment.factoryAddress), + callData: eip6492Deployment.factoryCalldata, + } satisfies RawContractCall, + { + address: erc20Address, + callData: transferCalldata, + } satisfies RawContractCall, + ]); + + return results[1]?.status === "success"; + } catch { + return false; + } + } + + const sig = payload.signature!; + const sigLength = sig.startsWith("0x") ? sig.length - 2 : sig.length; + const isECDSA = sigLength === 130; + + try { + if (isECDSA) { + const parsedSig = parseSignature(sig); + await signer.readContract({ + address: erc20Address, + abi: eip3009ABI, + functionName: "transferWithAuthorization", + args: [ + ...transferArgs, + (parsedSig.v as number | undefined) ?? parsedSig.yParity, + parsedSig.r, + parsedSig.s, + ], + }); + } else { + await signer.readContract({ + address: erc20Address, + abi: eip3009ABI, + functionName: "transferWithAuthorization", + args: [...transferArgs, sig], + }); + } + return true; + } catch { + return false; + } +} + +/** + * After simulation fails, runs a single diagnostic multicall to determine the most specific error reason. + * Checks balanceOf, name, version and authorizationState in one RPC round-trip. + * + * @param signer - EVM signer used for the payment + * @param erc20Address - Address of the ERC-20 token contract + * @param payload - The EIP-3009 transfer authorization payload + * @param requirements - Payment requirements to validate against + * @param amountRequired - Required amount for the payment (balance check) + * + * @returns Promise resolving to the verification result with validity and optional invalid reason + */ +export async function diagnoseEip3009SimulationFailure( + signer: FacilitatorEvmSigner, + erc20Address: `0x${string}`, + payload: ExactEIP3009Payload, + requirements: PaymentRequirements, + amountRequired: string, +): Promise { + const payer = payload.authorization.from; + + const diagnosticCalls: ContractCall[] = [ + { + address: erc20Address, + abi: eip3009ABI, + functionName: "balanceOf", + args: [payload.authorization.from], + }, + { + address: erc20Address, + abi: eip3009ABI, + functionName: "name", + }, + { + address: erc20Address, + abi: eip3009ABI, + functionName: "version", + }, + { + address: erc20Address, + abi: eip3009ABI, + functionName: "authorizationState", + args: [payload.authorization.from, payload.authorization.nonce], + }, + ]; + + try { + const results = await multicall(signer.readContract.bind(signer), diagnosticCalls); + + const [balanceResult, nameResult, versionResult, authStateResult] = results; + + if (authStateResult.status === "failure") { + return { isValid: false, invalidReason: Errors.ErrEip3009NotSupported, payer }; + } + + if (authStateResult.status === "success" && authStateResult.result === true) { + return { isValid: false, invalidReason: Errors.ErrEip3009NonceAlreadyUsed, payer }; + } + + if ( + nameResult.status === "success" && + requirements.extra?.name && + nameResult.result !== requirements.extra.name + ) { + return { isValid: false, invalidReason: Errors.ErrEip3009TokenNameMismatch, payer }; + } + + if ( + versionResult.status === "success" && + requirements.extra?.version && + versionResult.result !== requirements.extra.version + ) { + return { isValid: false, invalidReason: Errors.ErrEip3009TokenVersionMismatch, payer }; + } + + if (balanceResult.status === "success") { + const balance = balanceResult.result as bigint; + if (balance < BigInt(amountRequired)) { + return { + isValid: false, + invalidReason: Errors.ErrEip3009InsufficientBalance, + payer, + }; + } + } + } catch { + // Diagnostic multicall failed — fall through to generic error + } + + return { isValid: false, invalidReason: Errors.ErrEip3009SimulationFailed, payer }; +} + +/** + * Executes transferWithAuthorization onchain. + * + * @param signer - EVM signer for contract writes + * @param erc20Address - ERC-20 token contract address + * @param payload - EIP-3009 transfer authorization payload + * + * @returns Transaction hash + */ +export async function executeTransferWithAuthorization( + signer: FacilitatorEvmSigner, + erc20Address: `0x${string}`, + payload: ExactEIP3009Payload, +): Promise { + const { signature } = parseErc6492Signature(payload.signature!); + const signatureLength = signature.startsWith("0x") ? signature.length - 2 : signature.length; + const isECDSA = signatureLength === 130; + + const auth = payload.authorization; + const baseArgs = [ + getAddress(auth.from), + getAddress(auth.to), + BigInt(auth.value), + BigInt(auth.validAfter), + BigInt(auth.validBefore), + auth.nonce, + ] as const; + + if (isECDSA) { + const parsedSig = parseSignature(signature); + return signer.writeContract({ + address: erc20Address, + abi: eip3009ABI, + functionName: "transferWithAuthorization", + args: [ + ...baseArgs, + (parsedSig.v as number | undefined) || parsedSig.yParity, + parsedSig.r, + parsedSig.s, + ], + }); + } + + return signer.writeContract({ + address: erc20Address, + abi: eip3009ABI, + functionName: "transferWithAuthorization", + args: [...baseArgs, signature], + }); +} diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts index e0f3f72..69056a9 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts @@ -4,10 +4,22 @@ import { SettleResponse, VerifyResponse, } from "@x402/core/types"; -import { getAddress, Hex, isAddressEqual, parseErc6492Signature, parseSignature } from "viem"; -import { authorizationTypes, eip3009ABI } from "../../constants"; +import { getAddress, Hex, isAddressEqual, parseErc6492Signature } from "viem"; +import { authorizationTypes } from "../../constants"; import { FacilitatorEvmSigner } from "../../signer"; +import { getEvmChainId } from "../../utils"; import { ExactEIP3009Payload } from "../../types"; +import * as Errors from "./errors"; +import { + diagnoseEip3009SimulationFailure, + executeTransferWithAuthorization, + simulateEip3009Transfer, +} from "./eip3009-utils"; + +export interface VerifyEIP3009Options { + /** Run onchain simulation. Defaults to true. */ + simulate?: boolean; +} export interface EIP3009FacilitatorConfig { /** @@ -17,6 +29,12 @@ export interface EIP3009FacilitatorConfig { * @default false */ deployERC4337WithEIP6492: boolean; + /** + * If enabled, simulates transaction before settling. Defaults to false, ie only simulate during verify. + * + * @default false + */ + simulateInSettle?: boolean; } /** @@ -26,6 +44,7 @@ export interface EIP3009FacilitatorConfig { * @param payload - The payment payload to verify * @param requirements - The payment requirements * @param eip3009Payload - The EIP-3009 specific payload + * @param options - Optional verification options * @returns Promise resolving to verification response */ export async function verifyEIP3009( @@ -33,14 +52,18 @@ export async function verifyEIP3009( payload: PaymentPayload, requirements: PaymentRequirements, eip3009Payload: ExactEIP3009Payload, + options?: VerifyEIP3009Options, ): Promise { const payer = eip3009Payload.authorization.from; + let eip6492Deployment: + | { factoryAddress: `0x${string}`; factoryCalldata: `0x${string}` } + | undefined; // Verify scheme matches if (payload.accepted.scheme !== "exact" || requirements.scheme !== "exact") { return { isValid: false, - invalidReason: "unsupported_scheme", + invalidReason: Errors.ErrInvalidScheme, payer, }; } @@ -49,7 +72,7 @@ export async function verifyEIP3009( if (!requirements.extra?.name || !requirements.extra?.version) { return { isValid: false, - invalidReason: "missing_eip712_domain", + invalidReason: Errors.ErrMissingEip712Domain, payer, }; } @@ -61,7 +84,7 @@ export async function verifyEIP3009( if (payload.accepted.network !== requirements.network) { return { isValid: false, - invalidReason: "network_mismatch", + invalidReason: Errors.ErrNetworkMismatch, payer, }; } @@ -73,7 +96,7 @@ export async function verifyEIP3009( domain: { name, version, - chainId: parseInt(requirements.network.split(":")[1]), + chainId: getEvmChainId(requirements.network), verifyingContract: erc20Address, }, message: { @@ -87,71 +110,70 @@ export async function verifyEIP3009( }; // Verify signature + // Note: verifyTypedData is implementation-dependent and pluggable on FacilitatorEvmSigner + // Some implementations only do EOA-style ECDSA recovery (e.g. viem/utils verifyTypedData, ethers.verifyTypedData) + // Viem's publicClient.verifyTypedData supports EOA and Smart Contract Account (ERC-1271 / ERC-6492) signature verification + let isValid = false; try { - const recoveredAddress = await signer.verifyTypedData({ + isValid = await signer.verifyTypedData({ address: eip3009Payload.authorization.from, ...permitTypedData, signature: eip3009Payload.signature!, }); + } catch { + isValid = false; + } + const signature = eip3009Payload.signature!; + const sigLen = signature.startsWith("0x") ? signature.length - 2 : signature.length; - if (!recoveredAddress) { + // Extract EIP-6492 deployment info (factory address + calldata) if present + const erc6492Data = parseErc6492Signature(signature); + const hasDeploymentInfo = + erc6492Data.address && + erc6492Data.data && + !isAddressEqual(erc6492Data.address, "0x0000000000000000000000000000000000000000"); + + if (hasDeploymentInfo) { + eip6492Deployment = { + factoryAddress: erc6492Data.address!, + factoryCalldata: erc6492Data.data!, + }; + } + + if (!isValid) { + // Check if signature is from a smart wallet + const isSmartWallet = sigLen > 130; // 65 bytes = 130 hex chars for EOA + + // EOA signature that failed verification — definitely invalid + if (!isSmartWallet) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_signature", + invalidReason: Errors.ErrInvalidSignature, payer, }; } - } catch { - // Signature verification failed - could be an undeployed smart wallet - // Check if smart wallet is deployed - const signature = eip3009Payload.signature!; - const signatureLength = signature.startsWith("0x") ? signature.length - 2 : signature.length; - const isSmartWallet = signatureLength > 130; // 65 bytes = 130 hex chars for EOA - if (isSmartWallet) { - const payerAddress = eip3009Payload.authorization.from; - const bytecode = await signer.getCode({ address: payerAddress }); - - if (!bytecode || bytecode === "0x") { - // Wallet is not deployed. Check if it's EIP-6492 with deployment info. - const erc6492Data = parseErc6492Signature(signature); - const hasDeploymentInfo = - erc6492Data.address && - erc6492Data.data && - !isAddressEqual(erc6492Data.address, "0x0000000000000000000000000000000000000000"); + // Smart wallet signature: check if deployed or has ERC-6492 deployment info + const bytecode = await signer.getCode({ address: payer }); + const isDeployed = bytecode && bytecode !== "0x"; - if (!hasDeploymentInfo) { - // Non-EIP-6492 undeployed smart wallet - will always fail at settlement - return { - isValid: false, - invalidReason: "invalid_exact_evm_payload_undeployed_smart_wallet", - payer: payerAddress, - }; - } - // EIP-6492 signature with deployment info - allow through - } else { - // Wallet is deployed but signature still failed - invalid signature - return { - isValid: false, - invalidReason: "invalid_exact_evm_payload_signature", - payer, - }; - } - } else { - // EOA signature failed + if (!isDeployed && !hasDeploymentInfo) { + // Undeployed smart wallet with no factory info return { isValid: false, - invalidReason: "invalid_exact_evm_payload_signature", + invalidReason: Errors.ErrUndeployedSmartWallet, payer, }; } + // Deployed smart wallet or undeployed with ERC-6492 factory info + // fall through to remaining field checks and onchain simulation } // Verify payment recipient matches if (getAddress(eip3009Payload.authorization.to) !== getAddress(requirements.payTo)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_recipient_mismatch", + invalidReason: Errors.ErrRecipientMismatch, payer, }; } @@ -161,7 +183,7 @@ export async function verifyEIP3009( if (BigInt(eip3009Payload.authorization.validBefore) < BigInt(now + 6)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_authorization_valid_before", + invalidReason: Errors.ErrValidBeforeExpired, payer, }; } @@ -170,41 +192,39 @@ export async function verifyEIP3009( if (BigInt(eip3009Payload.authorization.validAfter) > BigInt(now)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_authorization_valid_after", + invalidReason: Errors.ErrValidAfterInFuture, payer, }; } - // Check balance - try { - const balance = (await signer.readContract({ - address: erc20Address, - abi: eip3009ABI, - functionName: "balanceOf", - args: [eip3009Payload.authorization.from], - })) as bigint; - - if (BigInt(balance) < BigInt(requirements.amount)) { - return { - isValid: false, - invalidReason: "insufficient_funds", - invalidMessage: `Insufficient funds to complete the payment. Required: ${requirements.amount} ${requirements.asset}, Available: ${balance.toString()} ${requirements.asset}. Please add funds to your wallet and try again.`, - payer, - }; - } - } catch { - // If we can't check balance, continue with other validations - } - - // Verify amount is sufficient - if (BigInt(eip3009Payload.authorization.value) < BigInt(requirements.amount)) { + // Verify amount exactly matches requirements + if (BigInt(eip3009Payload.authorization.value) !== BigInt(requirements.amount)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_authorization_value", + invalidReason: Errors.ErrInvalidAuthorizationValue, payer, }; } + // Transaction simulation + if (options?.simulate !== false) { + const simulationSucceeded = await simulateEip3009Transfer( + signer, + erc20Address, + eip3009Payload, + eip6492Deployment, + ); + if (!simulationSucceeded) { + return diagnoseEip3009SimulationFailure( + signer, + erc20Address, + eip3009Payload, + requirements, + requirements.amount, + ); + } + } + return { isValid: true, invalidReason: undefined, @@ -232,21 +252,24 @@ export async function settleEIP3009( const payer = eip3009Payload.authorization.from; // Re-verify before settling - const valid = await verifyEIP3009(signer, payload, requirements, eip3009Payload); + const valid = await verifyEIP3009(signer, payload, requirements, eip3009Payload, { + simulate: config.simulateInSettle ?? false, + }); if (!valid.isValid) { return { success: false, network: payload.accepted.network, transaction: "", - errorReason: valid.invalidReason ?? "invalid_scheme", + errorReason: valid.invalidReason ?? Errors.ErrInvalidScheme, payer, }; } try { - // Parse ERC-6492 signature if applicable - const parseResult = parseErc6492Signature(eip3009Payload.signature!); - const { signature, address: factoryAddress, data: factoryCalldata } = parseResult; + // Parse ERC-6492 signature if applicable (for optional deployment) + const { address: factoryAddress, data: factoryCalldata } = parseErc6492Signature( + eip3009Payload.signature!, + ); // Deploy ERC-4337 smart wallet via EIP-6492 if configured and needed if ( @@ -270,57 +293,19 @@ export async function settleEIP3009( } } - // Determine if this is an ECDSA signature (EOA) or smart wallet signature - const signatureLength = signature.startsWith("0x") ? signature.length - 2 : signature.length; - const isECDSA = signatureLength === 130; - - let tx: Hex; - if (isECDSA) { - // For EOA wallets, parse signature into v, r, s and use that overload - const parsedSig = parseSignature(signature); - - tx = await signer.writeContract({ - address: getAddress(requirements.asset), - abi: eip3009ABI, - functionName: "transferWithAuthorization", - args: [ - getAddress(eip3009Payload.authorization.from), - getAddress(eip3009Payload.authorization.to), - BigInt(eip3009Payload.authorization.value), - BigInt(eip3009Payload.authorization.validAfter), - BigInt(eip3009Payload.authorization.validBefore), - eip3009Payload.authorization.nonce, - (parsedSig.v as number | undefined) || parsedSig.yParity, - parsedSig.r, - parsedSig.s, - ], - }); - } else { - // For smart wallets, use the bytes signature overload - tx = await signer.writeContract({ - address: getAddress(requirements.asset), - abi: eip3009ABI, - functionName: "transferWithAuthorization", - args: [ - getAddress(eip3009Payload.authorization.from), - getAddress(eip3009Payload.authorization.to), - BigInt(eip3009Payload.authorization.value), - BigInt(eip3009Payload.authorization.validAfter), - BigInt(eip3009Payload.authorization.validBefore), - eip3009Payload.authorization.nonce, - signature, - ], - }); - } + const tx = await executeTransferWithAuthorization( + signer, + getAddress(requirements.asset), + eip3009Payload, + ); // Wait for transaction confirmation const receipt = await signer.waitForTransactionReceipt({ hash: tx }); - console.log("receipt ", receipt); if (receipt.status !== "success") { return { success: false, - errorReason: "invalid_transaction_state", + errorReason: Errors.ErrTransactionFailed, transaction: tx, network: payload.accepted.network, payer, @@ -336,7 +321,7 @@ export async function settleEIP3009( } catch { return { success: false, - errorReason: "transaction_failed", + errorReason: Errors.ErrTransactionFailed, transaction: "", network: payload.accepted.network, payer, diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/erc20approval.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/erc20approval.ts new file mode 100644 index 0000000..eda6a04 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/erc20approval.ts @@ -0,0 +1,2 @@ +// Re-export from shared for backward compatibility +export { validateErc20ApprovalForPayment } from "../../shared/erc20approval"; diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/errors.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/errors.ts new file mode 100644 index 0000000..2573990 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/errors.ts @@ -0,0 +1,71 @@ +/** + * Named error reason constants for the exact EVM facilitator. + * + * These strings must be character-for-character identical to the Go constants in + * go/mechanisms/evm/exact/facilitator/errors.go to maintain cross-SDK parity. + */ + +export const ErrInvalidScheme = "invalid_exact_evm_scheme"; +export const ErrNetworkMismatch = "invalid_exact_evm_network_mismatch"; +export const ErrMissingEip712Domain = "invalid_exact_evm_missing_eip712_domain"; +export const ErrRecipientMismatch = "invalid_exact_evm_recipient_mismatch"; +export const ErrInvalidSignature = "invalid_exact_evm_signature"; +export const ErrValidBeforeExpired = "invalid_exact_evm_payload_authorization_valid_before"; +export const ErrValidAfterInFuture = "invalid_exact_evm_payload_authorization_valid_after"; +export const ErrInvalidAuthorizationValue = "invalid_exact_evm_authorization_value"; +export const ErrUndeployedSmartWallet = "invalid_exact_evm_payload_undeployed_smart_wallet"; +export const ErrTransactionFailed = "invalid_exact_evm_transaction_failed"; + +// EIP-3009 verify errors +export const ErrEip3009TokenNameMismatch = "invalid_exact_evm_token_name_mismatch"; +export const ErrEip3009TokenVersionMismatch = "invalid_exact_evm_token_version_mismatch"; +export const ErrEip3009NotSupported = "invalid_exact_evm_eip3009_not_supported"; +export const ErrEip3009NonceAlreadyUsed = "invalid_exact_evm_nonce_already_used"; +export const ErrEip3009InsufficientBalance = "invalid_exact_evm_insufficient_balance"; +export const ErrEip3009SimulationFailed = "invalid_exact_evm_transaction_simulation_failed"; + +// Permit2 verify errors +export const ErrPermit2InvalidSpender = "invalid_permit2_spender"; +export const ErrPermit2RecipientMismatch = "invalid_permit2_recipient_mismatch"; +export const ErrPermit2DeadlineExpired = "permit2_deadline_expired"; +export const ErrPermit2NotYetValid = "permit2_not_yet_valid"; +export const ErrPermit2AmountMismatch = "permit2_amount_mismatch"; +export const ErrPermit2TokenMismatch = "permit2_token_mismatch"; +export const ErrPermit2InvalidSignature = "invalid_permit2_signature"; +export const ErrPermit2AllowanceRequired = "permit2_allowance_required"; +export const ErrPermit2SimulationFailed = "permit2_simulation_failed"; +export const ErrPermit2InsufficientBalance = "permit2_insufficient_balance"; +export const ErrPermit2ProxyNotDeployed = "permit2_proxy_not_deployed"; + +// Permit2 settle errors (from contract reverts) +export const ErrPermit2InvalidAmount = "permit2_invalid_amount"; +export const ErrPermit2InvalidDestination = "permit2_invalid_destination"; +export const ErrPermit2InvalidOwner = "permit2_invalid_owner"; +export const ErrPermit2PaymentTooEarly = "permit2_payment_too_early"; +export const ErrPermit2InvalidNonce = "permit2_invalid_nonce"; +export const ErrPermit2612AmountMismatch = "permit2_2612_amount_mismatch"; + +// ERC-20 approval gas sponsoring verify errors +export const ErrErc20ApprovalInvalidFormat = "invalid_erc20_approval_extension_format"; +export const ErrErc20ApprovalFromMismatch = "erc20_approval_from_mismatch"; +export const ErrErc20ApprovalAssetMismatch = "erc20_approval_asset_mismatch"; +export const ErrErc20ApprovalSpenderNotPermit2 = "erc20_approval_spender_not_permit2"; +export const ErrErc20ApprovalTxWrongTarget = "erc20_approval_tx_wrong_target"; +export const ErrErc20ApprovalTxWrongSelector = "erc20_approval_tx_wrong_selector"; +export const ErrErc20ApprovalTxWrongSpender = "erc20_approval_tx_wrong_spender"; +export const ErrErc20ApprovalTxInvalidCalldata = "erc20_approval_tx_invalid_calldata"; +export const ErrErc20ApprovalTxSignerMismatch = "erc20_approval_tx_signer_mismatch"; +export const ErrErc20ApprovalTxInvalidSignature = "erc20_approval_tx_invalid_signature"; +export const ErrErc20ApprovalTxParseFailed = "erc20_approval_tx_parse_failed"; +export const ErrErc20ApprovalTxFailed = "erc20_approval_tx_failed"; + +// EIP-2612 gas sponsoring verify errors +export const ErrInvalidEip2612ExtensionFormat = "invalid_eip2612_extension_format"; +export const ErrEip2612FromMismatch = "eip2612_from_mismatch"; +export const ErrEip2612AssetMismatch = "eip2612_asset_mismatch"; +export const ErrEip2612SpenderNotPermit2 = "eip2612_spender_not_permit2"; +export const ErrEip2612DeadlineExpired = "eip2612_deadline_expired"; + +// Shared settle errors +export const ErrUnsupportedPayloadType = "unsupported_payload_type"; +export const ErrInvalidTransactionState = "invalid_transaction_state"; diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts index b627453..ef0132a 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts @@ -1,41 +1,79 @@ import { PaymentPayload, PaymentRequirements, + FacilitatorContext, SettleResponse, VerifyResponse, } from "@x402/core/types"; -import { getAddress } from "viem"; import { - eip3009ABI, + extractEip2612GasSponsoringInfo, + extractErc20ApprovalGasSponsoringInfo, + ERC20_APPROVAL_GAS_SPONSORING_KEY, + resolveErc20ApprovalExtensionSigner, + type Eip2612GasSponsoringInfo, + type Erc20ApprovalGasSponsoringFacilitatorExtension, + type Erc20ApprovalGasSponsoringSigner, +} from "../extensions"; +import { getAddress, encodeFunctionData } from "viem"; +import { PERMIT2_ADDRESS, permit2WitnessTypes, x402ExactPermit2ProxyABI, x402ExactPermit2ProxyAddress, } from "../../constants"; +import * as Errors from "./errors"; import { FacilitatorEvmSigner } from "../../signer"; import { ExactPermit2Payload } from "../../types"; +import { getEvmChainId } from "../../utils"; +import { validateErc20ApprovalForPayment } from "./erc20approval"; +import { + simulatePermit2Settle, + simulatePermit2SettleWithPermit, + simulatePermit2SettleWithErc20Approval, + diagnosePermit2SimulationFailure, + checkPermit2Prerequisites, + validateEip2612PermitForPayment, + buildExactPermit2SettleArgs, + splitEip2612Signature, + waitAndReturnSettleResponse, + mapSettleError, + type Permit2ProxyConfig, +} from "../../shared/permit2"; + +const exactProxyConfig: Permit2ProxyConfig = { + proxyAddress: x402ExactPermit2ProxyAddress, + proxyABI: x402ExactPermit2ProxyABI, +}; + +export interface VerifyPermit2Options { + /** Run onchain simulation. Defaults to true. */ + simulate?: boolean; +} -// ERC20 allowance ABI for checking Permit2 approval -const erc20AllowanceABI = [ - { - type: "function", - name: "allowance", - inputs: [ - { name: "owner", type: "address" }, - { name: "spender", type: "address" }, - ], - outputs: [{ type: "uint256" }], - stateMutability: "view", - }, -] as const; +export interface Permit2FacilitatorConfig { + /** + * If enabled, simulates transaction before settling. Defaults to false, + * i.e. only simulate during verify. + * + * @default false + */ + simulateInSettle?: boolean; +} /** * Verifies a Permit2 payment payload. * + * Handles all Permit2 verification paths: + * - Standard: checks on-chain Permit2 allowance + * - EIP-2612: validates the EIP-2612 permit extension when allowance is insufficient + * - ERC-20 approval: validates the pre-signed approve tx extension when allowance is insufficient + * * @param signer - The facilitator signer for contract reads * @param payload - The payment payload to verify * @param requirements - The payment requirements * @param permit2Payload - The Permit2 specific payload + * @param context - Optional facilitator context for extension-provided capabilities + * @param options - Optional verification options (e.g. simulate) * @returns Promise resolving to verification response */ export async function verifyPermit2( @@ -43,91 +81,87 @@ export async function verifyPermit2( payload: PaymentPayload, requirements: PaymentRequirements, permit2Payload: ExactPermit2Payload, + context?: FacilitatorContext, + options?: VerifyPermit2Options, ): Promise { const payer = permit2Payload.permit2Authorization.from; - // Verify scheme matches if (payload.accepted.scheme !== "exact" || requirements.scheme !== "exact") { return { isValid: false, - invalidReason: "unsupported_scheme", + invalidReason: Errors.ErrUnsupportedPayloadType, payer, }; } - // Verify network matches if (payload.accepted.network !== requirements.network) { return { isValid: false, - invalidReason: "network_mismatch", + invalidReason: Errors.ErrNetworkMismatch, payer, }; } - const chainId = parseInt(requirements.network.split(":")[1]); + const chainId = getEvmChainId(requirements.network); const tokenAddress = getAddress(requirements.asset); - // Verify spender is the x402ExactPermit2Proxy if ( getAddress(permit2Payload.permit2Authorization.spender) !== getAddress(x402ExactPermit2ProxyAddress) ) { return { isValid: false, - invalidReason: "invalid_permit2_spender", + invalidReason: Errors.ErrPermit2InvalidSpender, payer, }; } - // Verify witness.to matches payTo if ( getAddress(permit2Payload.permit2Authorization.witness.to) !== getAddress(requirements.payTo) ) { return { isValid: false, - invalidReason: "invalid_permit2_recipient_mismatch", + invalidReason: Errors.ErrPermit2RecipientMismatch, payer, }; } - // Verify deadline not expired (with 6 second buffer for block time) const now = Math.floor(Date.now() / 1000); if (BigInt(permit2Payload.permit2Authorization.deadline) < BigInt(now + 6)) { return { isValid: false, - invalidReason: "permit2_deadline_expired", + invalidReason: Errors.ErrPermit2DeadlineExpired, payer, }; } - // Verify validAfter is not in the future if (BigInt(permit2Payload.permit2Authorization.witness.validAfter) > BigInt(now)) { return { isValid: false, - invalidReason: "permit2_not_yet_valid", + invalidReason: Errors.ErrPermit2NotYetValid, payer, }; } - // Verify amount is sufficient - if (BigInt(permit2Payload.permit2Authorization.permitted.amount) < BigInt(requirements.amount)) { + // Verify amount exactly matches requirements + if ( + BigInt(permit2Payload.permit2Authorization.permitted.amount) !== BigInt(requirements.amount) + ) { return { isValid: false, - invalidReason: "permit2_insufficient_amount", + invalidReason: Errors.ErrPermit2AmountMismatch, payer, }; } - // Verify token matches if (getAddress(permit2Payload.permit2Authorization.permitted.token) !== tokenAddress) { return { isValid: false, - invalidReason: "permit2_token_mismatch", + invalidReason: Errors.ErrPermit2TokenMismatch, payer, }; } - // Build typed data for Permit2 signature verification const permit2TypedData = { types: permit2WitnessTypes, primaryType: "PermitWitnessTransferFrom" as const, @@ -147,89 +181,156 @@ export async function verifyPermit2( witness: { to: getAddress(permit2Payload.permit2Authorization.witness.to), validAfter: BigInt(permit2Payload.permit2Authorization.witness.validAfter), - extra: permit2Payload.permit2Authorization.witness.extra, }, }, }; // Verify signature + // Note: verifyTypedData is implementation-dependent and pluggable on FacilitatorEvmSigner + // Some implementations only do EOA-style ECDSA recovery (e.g. viem/utils verifyTypedData, ethers.verifyTypedData) + // Viem's publicClient.verifyTypedData supports EOA and Smart Contract Account (ERC-1271 / ERC-6492) signature verification + let signatureValid = false; try { - const isValid = await signer.verifyTypedData({ + signatureValid = await signer.verifyTypedData({ address: payer, ...permit2TypedData, signature: permit2Payload.signature, }); + } catch { + signatureValid = false; + } + + if (!signatureValid) { + // Check if the payer is a deployed smart contract + const bytecode = await signer.getCode({ address: payer }); + const isDeployedContract = bytecode && bytecode !== "0x"; - if (!isValid) { + if (!isDeployedContract) { return { isValid: false, - invalidReason: "invalid_permit2_signature", + invalidReason: Errors.ErrPermit2InvalidSignature, payer, }; } - } catch { - return { - isValid: false, - invalidReason: "invalid_permit2_signature", - payer, - }; + // Deployed smart contract: fall through to simulation } - // Check Permit2 allowance - try { - const allowance = (await signer.readContract({ - address: tokenAddress, - abi: erc20AllowanceABI, - functionName: "allowance", - args: [payer, PERMIT2_ADDRESS], - })) as bigint; - - if (allowance < BigInt(requirements.amount)) { - return { - isValid: false, - invalidReason: "permit2_allowance_required", - payer, - }; + // If simulation is disabled, return early + if (options?.simulate === false) { + return { isValid: true, invalidReason: undefined, payer }; + } + + // Branch: EIP-2612 gas sponsoring (atomic settleWithPermit via contract) + const eip2612Info = extractEip2612GasSponsoringInfo(payload); + if (eip2612Info) { + const fieldResult = validateEip2612PermitForPayment(eip2612Info, payer, tokenAddress); + if (!fieldResult.isValid) { + return { isValid: false, invalidReason: fieldResult.invalidReason!, payer }; } - } catch { - // If we can't check allowance, continue - settlement will fail if insufficient + + const exactSettleArgs = buildExactPermit2SettleArgs(permit2Payload); + const simulation = await simulatePermit2SettleWithPermit( + exactProxyConfig, + signer, + exactSettleArgs, + eip2612Info, + ); + if (!simulation.ok) { + return diagnosePermit2SimulationFailure( + exactProxyConfig, + signer, + tokenAddress, + permit2Payload, + requirements.amount, + simulation.error, + ); + } + + return { isValid: true, invalidReason: undefined, payer }; } - // Check balance - try { - const balance = (await signer.readContract({ - address: tokenAddress, - abi: eip3009ABI, - functionName: "balanceOf", - args: [payer], - })) as bigint; - - if (balance < BigInt(requirements.amount)) { - return { - isValid: false, - invalidReason: "insufficient_funds", - invalidMessage: `Insufficient funds to complete the payment. Required: ${requirements.amount} ${requirements.asset}, Available: ${balance.toString()} ${requirements.asset}. Please add funds to your wallet and try again.`, + // Branch: ERC-20 approval gas sponsoring (broadcast approval + settle via extension signer) + const erc20GasSponsorshipExtension = + context?.getExtension( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + ); + if (erc20GasSponsorshipExtension) { + const erc20Info = extractErc20ApprovalGasSponsoringInfo(payload); + if (erc20Info) { + const fieldResult = await validateErc20ApprovalForPayment(erc20Info, payer, tokenAddress); + if (!fieldResult.isValid) { + return { isValid: false, invalidReason: fieldResult.invalidReason!, payer }; + } + + const extensionSigner = resolveErc20ApprovalExtensionSigner( + erc20GasSponsorshipExtension, + requirements.network, + ); + + if (extensionSigner?.simulateTransactions) { + const simulation = await simulatePermit2SettleWithErc20Approval( + exactProxyConfig, + extensionSigner, + buildExactPermit2SettleArgs(permit2Payload), + erc20Info, + ); + if (!simulation.ok) { + return diagnosePermit2SimulationFailure( + exactProxyConfig, + signer, + tokenAddress, + permit2Payload, + requirements.amount, + simulation.error, + ); + } + return { isValid: true, invalidReason: undefined, payer }; + } + + // Fallback to prerequisite-only check if simulateTransactions is not available + return checkPermit2Prerequisites( + exactProxyConfig, + signer, + tokenAddress, payer, - }; + requirements.amount, + ); } - } catch { - // If we can't check balance, continue with other validations } - return { - isValid: true, - invalidReason: undefined, - payer, - }; + // Branch: standard settle (allowance already on-chain) + const simulation = await simulatePermit2Settle( + exactProxyConfig, + signer, + buildExactPermit2SettleArgs(permit2Payload), + ); + if (!simulation.ok) { + return diagnosePermit2SimulationFailure( + exactProxyConfig, + signer, + tokenAddress, + permit2Payload, + requirements.amount, + simulation.error, + ); + } + + return { isValid: true, invalidReason: undefined, payer }; } /** - * Settles a Permit2 payment by calling the x402ExactPermit2Proxy. + * Settles a Permit2 payment. Single entry point for all Permit2 settlement paths: * - * @param signer - The facilitator signer for contract writes + * 1. EIP-2612 extension present -> settleWithPermit (atomic single tx via contract) + * 2. ERC-20 approval extension present + extension signer -> broadcast approval + settle (via extension signer) + * 3. Standard -> settle directly (allowance already on-chain) + * + * @param signer - The base facilitator signer for contract writes * @param payload - The payment payload to settle * @param requirements - The payment requirements * @param permit2Payload - The Permit2 specific payload + * @param context - Optional facilitator context for extension-provided capabilities + * @param config - Optional facilitator config (simulateInSettle) * @returns Promise resolving to settlement response */ export async function settlePermit2( @@ -237,95 +338,169 @@ export async function settlePermit2( payload: PaymentPayload, requirements: PaymentRequirements, permit2Payload: ExactPermit2Payload, + context?: FacilitatorContext, + config?: Permit2FacilitatorConfig, ): Promise { const payer = permit2Payload.permit2Authorization.from; - // Re-verify before settling - const valid = await verifyPermit2(signer, payload, requirements, permit2Payload); + const valid = await verifyPermit2(signer, payload, requirements, permit2Payload, context, { + simulate: config?.simulateInSettle ?? false, + }); if (!valid.isValid) { return { success: false, network: payload.accepted.network, transaction: "", - errorReason: valid.invalidReason ?? "invalid_scheme", + errorReason: valid.invalidReason ?? Errors.ErrInvalidScheme, + errorMessage: valid.invalidMessage, payer, }; } + // Branch: EIP-2612 gas sponsoring (atomic settleWithPermit via contract) + const eip2612Info = extractEip2612GasSponsoringInfo(payload); + if (eip2612Info) { + return settlePermit2WithEIP2612(exactProxyConfig, signer, payload, permit2Payload, eip2612Info); + } + + // Branch: ERC-20 approval gas sponsoring (broadcast approval + settle via extension signer) + const erc20Info = extractErc20ApprovalGasSponsoringInfo(payload); + if (erc20Info) { + const erc20GasSponsorshipExtension = + context?.getExtension( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + ); + const extensionSigner = resolveErc20ApprovalExtensionSigner( + erc20GasSponsorshipExtension, + payload.accepted.network, + ); + if (extensionSigner) { + return settlePermit2WithERC20Approval( + exactProxyConfig, + extensionSigner, + payload, + permit2Payload, + erc20Info, + ); + } + } + + // Branch: standard settle (allowance already on-chain) + return settlePermit2Direct(exactProxyConfig, signer, payload, permit2Payload); +} + +// --------------------------------------------------------------------------- +// Exact-only settle helpers (not shared — upto has its own implementations) +// --------------------------------------------------------------------------- + +/** + * Settles a Permit2 payment via settleWithPermit, including the EIP-2612 permit atomically. + * + * @param config - The proxy contract configuration (address and ABI) + * @param signer - The facilitator signer for contract writes + * @param payload - The payment payload for network info + * @param permit2Payload - The Permit2 payload with authorization and signature + * @param eip2612Info - The EIP-2612 gas sponsoring info from the payload extension + * @returns Promise resolving to a settlement response + */ +async function settlePermit2WithEIP2612( + config: Permit2ProxyConfig, + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + permit2Payload: ExactPermit2Payload, + eip2612Info: Eip2612GasSponsoringInfo, +): Promise { + const payer = permit2Payload.permit2Authorization.from; try { - // Call x402ExactPermit2Proxy.settle() + const { v, r, s } = splitEip2612Signature(eip2612Info.signature); + const tx = await signer.writeContract({ - address: x402ExactPermit2ProxyAddress, - abi: x402ExactPermit2ProxyABI, - functionName: "settle", + address: config.proxyAddress, + abi: config.proxyABI, + functionName: "settleWithPermit", args: [ { - permitted: { - token: getAddress(permit2Payload.permit2Authorization.permitted.token), - amount: BigInt(permit2Payload.permit2Authorization.permitted.amount), - }, - nonce: BigInt(permit2Payload.permit2Authorization.nonce), - deadline: BigInt(permit2Payload.permit2Authorization.deadline), - }, - getAddress(payer), - { - to: getAddress(permit2Payload.permit2Authorization.witness.to), - validAfter: BigInt(permit2Payload.permit2Authorization.witness.validAfter), - extra: permit2Payload.permit2Authorization.witness.extra as `0x${string}`, + value: BigInt(eip2612Info.amount), + deadline: BigInt(eip2612Info.deadline), + r, + s, + v, }, - permit2Payload.signature, + ...buildExactPermit2SettleArgs(permit2Payload), ], }); - // Wait for transaction confirmation - const receipt = await signer.waitForTransactionReceipt({ hash: tx }); - console.log("receipt ", receipt); + return waitAndReturnSettleResponse(signer, tx, payload, payer); + } catch (error) { + return mapSettleError(error, payload, payer); + } +} - if (receipt.status !== "success") { - return { - success: false, - errorReason: "invalid_transaction_state", - transaction: tx, - network: payload.accepted.network, - payer, - }; - } +/** + * Settles a Permit2 payment using an ERC-20 approval gas sponsoring extension. + * + * @param config - The proxy contract configuration (address and ABI) + * @param extensionSigner - The extension signer with sendTransactions capability + * @param payload - The payment payload for network info + * @param permit2Payload - The Permit2 payload with authorization and signature + * @param erc20Info - Object containing the signed approval transaction + * @param erc20Info.signedTransaction - The RLP-encoded signed ERC-20 approve transaction + * @returns Promise resolving to a settlement response + */ +async function settlePermit2WithERC20Approval( + config: Permit2ProxyConfig, + extensionSigner: Erc20ApprovalGasSponsoringSigner, + payload: PaymentPayload, + permit2Payload: ExactPermit2Payload, + erc20Info: { signedTransaction: string }, +): Promise { + const payer = permit2Payload.permit2Authorization.from; - return { - success: true, - transaction: tx, - network: payload.accepted.network, - payer, - }; + try { + const settleData = encodeFunctionData({ + abi: config.proxyABI, + functionName: "settle", + args: buildExactPermit2SettleArgs(permit2Payload), + }); + + const txHashes = await extensionSigner.sendTransactions([ + erc20Info.signedTransaction as `0x${string}`, + { to: config.proxyAddress, data: settleData, gas: BigInt(300_000) }, + ]); + + const settleTxHash = txHashes[txHashes.length - 1]; + return waitAndReturnSettleResponse(extensionSigner, settleTxHash, payload, payer); } catch (error) { - // Extract meaningful error message from the contract revert - let errorReason = "transaction_failed"; - if (error instanceof Error) { - // Check for common contract revert patterns - const message = error.message; - if (message.includes("AmountExceedsPermitted")) { - errorReason = "permit2_amount_exceeds_permitted"; - } else if (message.includes("InvalidDestination")) { - errorReason = "permit2_invalid_destination"; - } else if (message.includes("InvalidOwner")) { - errorReason = "permit2_invalid_owner"; - } else if (message.includes("PaymentTooEarly")) { - errorReason = "permit2_payment_too_early"; - } else if (message.includes("InvalidSignature") || message.includes("SignatureExpired")) { - errorReason = "permit2_invalid_signature"; - } else if (message.includes("InvalidNonce")) { - errorReason = "permit2_invalid_nonce"; - } else { - // Include error message for debugging (longer for better visibility) - errorReason = `transaction_failed: ${message.slice(0, 500)}`; - } - } - return { - success: false, - errorReason, - transaction: "", - network: payload.accepted.network, - payer, - }; + return mapSettleError(error, payload, payer); + } +} + +/** + * Settles a Permit2 payment directly when Permit2 allowance is already on-chain. + * + * @param config - The proxy contract configuration (address and ABI) + * @param signer - The facilitator signer for contract writes + * @param payload - The payment payload for network info + * @param permit2Payload - The Permit2 payload with authorization and signature + * @returns Promise resolving to a settlement response + */ +async function settlePermit2Direct( + config: Permit2ProxyConfig, + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + permit2Payload: ExactPermit2Payload, +): Promise { + const payer = permit2Payload.permit2Authorization.from; + try { + const tx = await signer.writeContract({ + address: config.proxyAddress, + abi: config.proxyABI, + functionName: "settle", + args: buildExactPermit2SettleArgs(permit2Payload), + }); + + return waitAndReturnSettleResponse(signer, tx, payload, payer); + } catch (error) { + return mapSettleError(error, payload, payer); } } diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/register.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/register.ts index 61224d7..9ab0561 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/register.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/register.ts @@ -27,6 +27,13 @@ export interface EvmFacilitatorConfig { * @default false */ deployERC4337WithEIP6492?: boolean; + + /** + * If enabled, reruns on-chain simulation during settle's re-verify. + * + * @default false + */ + simulateInSettle?: boolean; } /** @@ -70,6 +77,7 @@ export function registerExactEvmScheme( config.networks, new ExactEvmScheme(config.signer, { deployERC4337WithEIP6492: config.deployERC4337WithEIP6492, + simulateInSettle: config.simulateInSettle, }), ); @@ -78,6 +86,7 @@ export function registerExactEvmScheme( NETWORKS as Network[], new ExactEvmSchemeV1(config.signer, { deployERC4337WithEIP6492: config.deployERC4337WithEIP6492, + simulateInSettle: config.simulateInSettle, }), ); diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/scheme.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/scheme.ts index a28fa87..d5bcd5a 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/scheme.ts @@ -2,6 +2,7 @@ import { PaymentPayload, PaymentRequirements, SchemeNetworkFacilitator, + FacilitatorContext, SettleResponse, VerifyResponse, } from "@x402/core/types"; @@ -18,11 +19,19 @@ export interface ExactEvmSchemeConfig { * @default false */ deployERC4337WithEIP6492?: boolean; + /** + * If enabled, run on-chain simulation during settle's re-verify. + * + * @default false + */ + simulateInSettle?: boolean; } /** * EVM facilitator implementation for the Exact payment scheme. - * Routes between EIP-3009 and Permit2 based on payload type. + * Thin router that delegates to EIP-3009 or Permit2 based on payload type. + * All extension handling (EIP-2612, ERC-20 approval gas sponsoring) is owned + * by the Permit2 functions via FacilitatorContext. */ export class ExactEvmScheme implements SchemeNetworkFacilitator { readonly scheme = "exact"; @@ -30,10 +39,10 @@ export class ExactEvmScheme implements SchemeNetworkFacilitator { private readonly config: Required; /** - * Creates a new ExactEvmFacilitator instance. + * Creates a new ExactEvmScheme facilitator instance. * * @param signer - The EVM signer for facilitator operations - * @param config - Optional configuration for the facilitator + * @param config - Optional configuration */ constructor( private readonly signer: FacilitatorEvmSigner, @@ -41,25 +50,24 @@ export class ExactEvmScheme implements SchemeNetworkFacilitator { ) { this.config = { deployERC4337WithEIP6492: config?.deployERC4337WithEIP6492 ?? false, + simulateInSettle: config?.simulateInSettle ?? false, }; } /** - * Get mechanism-specific extra data for the supported kinds endpoint. - * For EVM, no extra data is needed. + * Returns undefined — EVM has no mechanism-specific extra data. * - * @param _ - The network identifier (unused for EVM) - * @returns undefined (EVM has no extra data) + * @param _ - The network identifier (unused) + * @returns undefined */ getExtra(_: string): Record | undefined { return undefined; } /** - * Get signer addresses used by this facilitator. - * Returns all addresses this facilitator can use for signing/settling transactions. + * Returns facilitator wallet addresses for the supported response. * - * @param _ - The network identifier (unused for EVM, addresses are network-agnostic) + * @param _ - The network identifier (unused, addresses are network-agnostic) * @returns Array of facilitator wallet addresses */ getSigners(_: string): string[] { @@ -67,49 +75,51 @@ export class ExactEvmScheme implements SchemeNetworkFacilitator { } /** - * Verifies a payment payload. - * Routes to the appropriate verification logic based on payload type. + * Verifies a payment payload. Routes to Permit2 or EIP-3009 based on payload type. * * @param payload - The payment payload to verify * @param requirements - The payment requirements + * @param context - Optional facilitator context for extension capabilities * @returns Promise resolving to verification response */ async verify( payload: PaymentPayload, requirements: PaymentRequirements, + context?: FacilitatorContext, ): Promise { const rawPayload = payload.payload as ExactEvmPayloadV2; + const isPermit2 = isPermit2Payload(rawPayload); - // Route based on payload type - if (isPermit2Payload(rawPayload)) { - return verifyPermit2(this.signer, payload, requirements, rawPayload); + if (isPermit2) { + return verifyPermit2(this.signer, payload, requirements, rawPayload, context); } - // Type-narrowed to EIP-3009 payload const eip3009Payload: ExactEIP3009Payload = rawPayload; return verifyEIP3009(this.signer, payload, requirements, eip3009Payload); } /** - * Settles a payment by executing the transfer. - * Routes to the appropriate settlement logic based on payload type. + * Settles a payment. Routes to Permit2 or EIP-3009 based on payload type. * * @param payload - The payment payload to settle * @param requirements - The payment requirements + * @param context - Optional facilitator context for extension capabilities * @returns Promise resolving to settlement response */ async settle( payload: PaymentPayload, requirements: PaymentRequirements, + context?: FacilitatorContext, ): Promise { const rawPayload = payload.payload as ExactEvmPayloadV2; + const isPermit2 = isPermit2Payload(rawPayload); - // Route based on payload type - if (isPermit2Payload(rawPayload)) { - return settlePermit2(this.signer, payload, requirements, rawPayload); + if (isPermit2) { + return settlePermit2(this.signer, payload, requirements, rawPayload, context, { + simulateInSettle: this.config.simulateInSettle, + }); } - // Type-narrowed to EIP-3009 payload const eip3009Payload: ExactEIP3009Payload = rawPayload; return settleEIP3009(this.signer, payload, requirements, eip3009Payload, this.config); } diff --git a/typescript/packages/mechanisms/evm/src/exact/server/DEFAULT_ASSET.md b/typescript/packages/mechanisms/evm/src/exact/server/DEFAULT_ASSET.md index 2852c2f..7c03ba8 100644 --- a/typescript/packages/mechanisms/evm/src/exact/server/DEFAULT_ASSET.md +++ b/typescript/packages/mechanisms/evm/src/exact/server/DEFAULT_ASSET.md @@ -10,24 +10,26 @@ When a server uses `price: "$0.10"` syntax (USD string pricing), x402 needs to k To add support for a new EVM chain, add an entry to the `stablecoins` map in `getDefaultAsset()`: ```typescript -const stablecoins: Record = { +const stablecoins: Record = { "eip155:8453": { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", name: "USD Coin", version: "2", - decimals: 6 - }, // Base mainnet USDC + decimals: 6, + }, // Base mainnet USDC (EIP-3009) // Add your chain here: "eip155:YOUR_CHAIN_ID": { address: "0xYOUR_STABLECOIN_ADDRESS", - name: "Token Name", // Must match EIP-712 domain name - version: "1", // Must match EIP-712 domain version - decimals: 6, // Token decimals (typically 6 for USDC) + name: "Token Name", // Must match EIP-712 domain name + version: "1", // Must match EIP-712 domain version + decimals: 6, // Token decimals (typically 6 for USDC) + // assetTransferMethod: "permit2", // Uncomment if token doesn't support EIP-3009 + // supportsEip2612: true, // Set if permit2 token implements EIP-2612 permit() }, }; ``` -### Required Fields +### Fields | Field | Description | |-------|-------------| @@ -35,6 +37,8 @@ const stablecoins: Record **Note**: Default assets should support EIP-3009 for the best user experience (no approval required). Tokens requiring Permit2 can be added via `registerMoneyParser` as shown above. +1. Obtain the correct EIP-712 domain `name` and `version` from the token contract +2. Check whether the token supports EIP-3009 (`transferWithAuthorization`): + - If yes: add the entry without `assetTransferMethod` (EIP-3009 is the default) + - If no: add `assetTransferMethod: "permit2"` to the entry so the client uses Permit2 automatically +3. For permit2 tokens, check whether the token supports EIP-2612 (`permit()`): + - If yes: add `supportsEip2612: true` so clients can use gasless EIP-2612 permits for Permit2 approval + - If no: omit `supportsEip2612` — clients will fall back to ERC-20 approval gas sponsoring +4. Add the entry to `getDefaultAsset()` in `scheme.ts` +5. Submit a PR with the chain name and rationale for the asset selection + +## Cross-SDK Checklist + +When adding a new chain's default asset, update all three SDKs to maintain parity: + +| SDK | File to edit | What to add | +|-----|-------------|-------------| +| **Go** | `go/mechanisms/evm/constants.go` | Entry in `NetworkConfigs` map | +| **TypeScript** | `typescript/packages/mechanisms/evm/src/exact/server/scheme.ts` | Entry in `stablecoins` map inside `getDefaultAsset()` | +| **Python** | `python/x402/mechanisms/evm/constants.py` | Entry in `NETWORK_CONFIGS` dict | + +All three must use: +- The same CAIP-2 network key (e.g., `eip155:YOUR_CHAIN_ID`) +- The same token contract address +- The same EIP-712 domain `name` and `version` +- The same `decimals` value +- The same asset transfer method (EIP-3009 default, or Permit2 if specified) diff --git a/typescript/packages/mechanisms/evm/src/exact/server/scheme.ts b/typescript/packages/mechanisms/evm/src/exact/server/scheme.ts index e81c1bf..a8fbfbf 100644 --- a/typescript/packages/mechanisms/evm/src/exact/server/scheme.ts +++ b/typescript/packages/mechanisms/evm/src/exact/server/scheme.ts @@ -6,6 +6,7 @@ import { SchemeNetworkServer, MoneyParser, } from "@x402/core/types"; +import { getDefaultAsset, type ExactDefaultAssetInfo } from "../../shared/defaultAssets"; /** * EVM server implementation for the Exact payment scheme. @@ -39,6 +40,22 @@ export class ExactEvmScheme implements SchemeNetworkServer { return this; } + /** + * Returns the decimal precision of the default stablecoin for the given network. + * Implements the optional AssetDecimalsProvider interface used by resolveSettlementOverrideAmount. + * + * @param _asset - The asset symbol (unused; defaults to the network's default stablecoin) + * @param network - The network to look up the default asset for + * @returns The number of decimal places for the asset + */ + getAssetDecimals(_asset: string, network: Network): number { + try { + return getDefaultAsset(network).decimals; + } catch { + return 6; + } + } + /** * Parses a price into an asset amount. * If price is already an AssetAmount, returns it directly. @@ -129,91 +146,52 @@ export class ExactEvmScheme implements SchemeNetworkServer { } /** - * Default money conversion implementation. - * Converts decimal amount to the default stablecoin on the specified network. + * Converts a numeric dollar amount to an AssetAmount using the default token for the network. * - * @param amount - The decimal amount (e.g., 1.50) - * @param network - The network to use - * @returns The parsed asset amount in the default stablecoin + * @param amount - The dollar amount as a number + * @param network - The target network + * @returns The converted asset amount with token metadata */ private defaultMoneyConversion(amount: number, network: Network): AssetAmount { - const assetInfo = this.getDefaultAsset(network); + const assetInfo: ExactDefaultAssetInfo = getDefaultAsset(network); const tokenAmount = this.convertToTokenAmount(amount.toString(), assetInfo.decimals); + // EIP-3009 tokens always need name/version for their transferWithAuthorization domain. + // Permit2 tokens only need them if the token supports EIP-2612 (for gasless permit signing). + // Omitting name/version for permit2 tokens signals the client to skip EIP-2612 and use + // ERC-20 approval gas sponsoring instead. + const includeEip712Domain = !assetInfo.assetTransferMethod || assetInfo.supportsEip2612; + return { amount: tokenAmount, asset: assetInfo.address, extra: { - name: assetInfo.name, - version: assetInfo.version, + ...(includeEip712Domain && { + name: assetInfo.name, + version: assetInfo.version, + }), + ...(assetInfo.assetTransferMethod && { + assetTransferMethod: assetInfo.assetTransferMethod, + }), }, }; } /** - * Convert decimal amount to token units (e.g., 0.10 -> 100000 for 6-decimal tokens) + * Converts a decimal string amount to an integer token amount using the given decimals. * - * @param decimalAmount - The decimal amount to convert - * @param decimals - The number of decimals for the token - * @returns The token amount as a string + * @param decimalAmount - The amount as a decimal string (e.g. "1.5") + * @param decimals - The number of decimal places for the token + * @returns The token amount as an integer string in smallest units */ private convertToTokenAmount(decimalAmount: string, decimals: number): string { const amount = parseFloat(decimalAmount); if (isNaN(amount)) { throw new Error(`Invalid amount: ${decimalAmount}`); } - // Convert to smallest unit (e.g., for USDC with 6 decimals: 0.10 * 10^6 = 100000) const [intPart, decPart = ""] = String(amount).split("."); const paddedDec = decPart.padEnd(decimals, "0").slice(0, decimals); const tokenAmount = (intPart + paddedDec).replace(/^0+/, "") || "0"; return tokenAmount; } - - /** - * Get the default asset info for a network (typically USDC) - * - * @param network - The network to get asset info for - * @returns The asset information including address, name, version, and decimals - */ - private getDefaultAsset(network: Network): { - address: string; - name: string; - version: string; - decimals: number; - } { - // Map of network to USDC info including EIP-712 domain parameters - // Each network has the right to determine its own default stablecoin that can be expressed as a USD string by calling servers - // NOTE: Currently only EIP-3009 supporting stablecoins can be used with this scheme - // Generic ERC20 support via EIP-2612/permit2 is planned, but not yet implemented. - const stablecoins: Record< - string, - { address: string; name: string; version: string; decimals: number } - > = { - "eip155:8453": { - address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - name: "USD Coin", - version: "2", - decimals: 6, - }, // Base mainnet USDC - "eip155:84532": { - address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - name: "USDC", - version: "2", - decimals: 6, - }, // Base Sepolia USDC - "eip155:4326": { - address: "0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7", - name: "MegaUSD", - version: "1", - decimals: 18, - }, // MegaETH mainnet USDM - }; - - const assetInfo = stablecoins[network]; - if (!assetInfo) { - throw new Error(`No default asset configured for network ${network}`); - } - - return assetInfo; - } } diff --git a/typescript/packages/mechanisms/evm/src/exact/v1/client/scheme.ts b/typescript/packages/mechanisms/evm/src/exact/v1/client/scheme.ts index 4fe5701..3e9181a 100644 --- a/typescript/packages/mechanisms/evm/src/exact/v1/client/scheme.ts +++ b/typescript/packages/mechanisms/evm/src/exact/v1/client/scheme.ts @@ -9,8 +9,8 @@ import { getAddress } from "viem"; import { authorizationTypes } from "../../../constants"; import { ClientEvmSigner } from "../../../signer"; import { ExactEvmPayloadV1 } from "../../../types"; -import { createNonce, getEvmChainId } from "../../../utils"; -import { EvmNetworkV1 } from "../../../v1"; +import { createNonce } from "../../../utils"; +import { EvmNetworkV1, getEvmChainIdV1 } from "../../../v1"; /** * EVM client implementation for the Exact payment scheme (V1). @@ -78,7 +78,7 @@ export class ExactEvmSchemeV1 implements SchemeNetworkClient { authorization: ExactEvmPayloadV1["authorization"], requirements: PaymentRequirementsV1, ): Promise<`0x${string}`> { - const chainId = getEvmChainId(requirements.network as EvmNetworkV1); + const chainId = getEvmChainIdV1(requirements.network as EvmNetworkV1); if (!requirements.extra?.name || !requirements.extra?.version) { throw new Error( diff --git a/typescript/packages/mechanisms/evm/src/exact/v1/facilitator/scheme.ts b/typescript/packages/mechanisms/evm/src/exact/v1/facilitator/scheme.ts index 7f70ed5..381aa8c 100644 --- a/typescript/packages/mechanisms/evm/src/exact/v1/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/evm/src/exact/v1/facilitator/scheme.ts @@ -7,12 +7,22 @@ import { VerifyResponse, } from "@x402/core/types"; import { PaymentRequirementsV1 } from "@x402/core/types/v1"; -import { getAddress, Hex, isAddressEqual, parseErc6492Signature, parseSignature } from "viem"; -import { authorizationTypes, eip3009ABI } from "../../../constants"; +import { getAddress, Hex, isAddressEqual, parseErc6492Signature } from "viem"; +import { authorizationTypes } from "../../../constants"; import { FacilitatorEvmSigner } from "../../../signer"; import { ExactEvmPayloadV1 } from "../../../types"; -import { getEvmChainId } from "../../../utils"; -import { EvmNetworkV1 } from "../../../v1"; +import { EvmNetworkV1, getEvmChainIdV1 } from "../../../v1"; +import * as Errors from "../../facilitator/errors"; +import { + diagnoseEip3009SimulationFailure, + executeTransferWithAuthorization, + simulateEip3009Transfer, +} from "../../facilitator/eip3009-utils"; + +export interface VerifyV1Options { + /** Run onchain simulation. Defaults to true. */ + simulate?: boolean; +} export interface ExactEvmSchemeV1Config { /** @@ -22,6 +32,12 @@ export interface ExactEvmSchemeV1Config { * @default false */ deployERC4337WithEIP6492?: boolean; + /** + * If enabled, simulates transaction before settling. Defaults to false, ie only simulate during verify. + * + * @default false + */ + simulateInSettle?: boolean; } /** @@ -44,6 +60,7 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { ) { this.config = { deployERC4337WithEIP6492: config?.deployERC4337WithEIP6492 ?? false, + simulateInSettle: config?.simulateInSettle ?? false, }; } @@ -79,37 +96,151 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { async verify( payload: PaymentPayload, requirements: PaymentRequirements, + ): Promise { + return this._verify(payload, requirements); + } + + /** + * Settles a payment by executing the transfer (V1). + * + * @param payload - The payment payload to settle + * @param requirements - The payment requirements + * @returns Promise resolving to settlement response + */ + async settle( + payload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + const payloadV1 = payload as unknown as PaymentPayloadV1; + const exactEvmPayload = payload.payload as ExactEvmPayloadV1; + + // Re-verify before settling + const valid = await this._verify(payload, requirements, { + simulate: this.config.simulateInSettle ?? false, + }); + if (!valid.isValid) { + return { + success: false, + network: payloadV1.network, + transaction: "", + errorReason: valid.invalidReason ?? Errors.ErrInvalidScheme, + payer: exactEvmPayload.authorization.from, + }; + } + + try { + // Parse ERC-6492 signature if applicable (for optional deployment) + const { address: factoryAddress, data: factoryCalldata } = parseErc6492Signature( + exactEvmPayload.signature!, + ); + + // Deploy ERC-4337 smart wallet via EIP-6492 if configured and needed + if ( + this.config.deployERC4337WithEIP6492 && + factoryAddress && + factoryCalldata && + !isAddressEqual(factoryAddress, "0x0000000000000000000000000000000000000000") + ) { + // Check if smart wallet is already deployed + const payerAddress = exactEvmPayload.authorization.from; + const bytecode = await this.signer.getCode({ address: payerAddress }); + + if (!bytecode || bytecode === "0x") { + // Send the factory calldata directly as a transaction + // The factoryCalldata already contains the complete encoded function call + const deployTx = await this.signer.sendTransaction({ + to: factoryAddress as Hex, + data: factoryCalldata as Hex, + }); + + // Wait for deployment transaction + await this.signer.waitForTransactionReceipt({ hash: deployTx }); + } + } + + const tx = await executeTransferWithAuthorization( + this.signer, + getAddress(requirements.asset), + exactEvmPayload, + ); + + // Wait for transaction confirmation + const receipt = await this.signer.waitForTransactionReceipt({ hash: tx }); + + if (receipt.status !== "success") { + return { + success: false, + errorReason: Errors.ErrTransactionFailed, + transaction: tx, + network: payloadV1.network, + payer: exactEvmPayload.authorization.from, + }; + } + + return { + success: true, + transaction: tx, + network: payloadV1.network, + payer: exactEvmPayload.authorization.from, + }; + } catch (error) { + return { + success: false, + errorReason: error instanceof Error ? error.message : Errors.ErrTransactionFailed, + transaction: "", + network: payloadV1.network, + payer: exactEvmPayload.authorization.from, + }; + } + } + + /** + * Internal verify with optional simulation control. + * + * @param payload - The payment payload to verify + * @param requirements - The payment requirements + * @param options - Verification options (e.g. simulate) + * @returns Promise resolving to verification response + */ + private async _verify( + payload: PaymentPayload, + requirements: PaymentRequirements, + options?: VerifyV1Options, ): Promise { const requirementsV1 = requirements as unknown as PaymentRequirementsV1; const payloadV1 = payload as unknown as PaymentPayloadV1; const exactEvmPayload = payload.payload as ExactEvmPayloadV1; + const payer = exactEvmPayload.authorization.from; + let eip6492Deployment: + | { factoryAddress: `0x${string}`; factoryCalldata: `0x${string}` } + | undefined; // Verify scheme matches if (payloadV1.scheme !== "exact" || requirements.scheme !== "exact") { return { isValid: false, - invalidReason: "unsupported_scheme", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrInvalidScheme, + payer, }; } // Get chain configuration let chainId: number; try { - chainId = getEvmChainId(payloadV1.network as EvmNetworkV1); + chainId = getEvmChainIdV1(payloadV1.network as EvmNetworkV1); } catch { return { isValid: false, - invalidReason: `invalid_network`, - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrNetworkMismatch, + payer, }; } if (!requirements.extra?.name || !requirements.extra?.version) { return { isValid: false, - invalidReason: "missing_eip712_domain", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrMissingEip712Domain, + payer, }; } @@ -120,8 +251,8 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { if (payloadV1.network !== requirements.network) { return { isValid: false, - invalidReason: "network_mismatch", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrNetworkMismatch, + payer, }; } @@ -145,67 +276,54 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { }, }; - // Verify signature + // Verify signature (flatten EIP-6492 handling out of catch block) + let isValid = false; try { - const recoveredAddress = await this.signer.verifyTypedData({ - address: exactEvmPayload.authorization.from, + isValid = await this.signer.verifyTypedData({ + address: payer, ...permitTypedData, signature: exactEvmPayload.signature!, }); + } catch { + isValid = false; + } + + const signature = exactEvmPayload.signature!; + const sigLen = signature.startsWith("0x") ? signature.length - 2 : signature.length; - if (!recoveredAddress) { + // Extract EIP-6492 deployment info (factory address + calldata) if present + const erc6492Data = parseErc6492Signature(signature); + const hasDeploymentInfo = + erc6492Data.address && + erc6492Data.data && + !isAddressEqual(erc6492Data.address, "0x0000000000000000000000000000000000000000"); + + if (hasDeploymentInfo) { + eip6492Deployment = { + factoryAddress: erc6492Data.address!, + factoryCalldata: erc6492Data.data!, + }; + } + + if (!isValid) { + const isSmartWallet = sigLen > 130; // 65 bytes = 130 hex chars for EOA + + if (!isSmartWallet) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_signature", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrInvalidSignature, + payer, }; } - } catch { - // Signature verification failed - could be an undeployed smart wallet - // Check if smart wallet is deployed - const signature = exactEvmPayload.signature!; - const signatureLength = signature.startsWith("0x") ? signature.length - 2 : signature.length; - const isSmartWallet = signatureLength > 130; // 65 bytes = 130 hex chars for EOA - if (isSmartWallet) { - const payerAddress = exactEvmPayload.authorization.from; - const bytecode = await this.signer.getCode({ address: payerAddress }); + const bytecode = await this.signer.getCode({ address: payer }); + const isDeployed = bytecode && bytecode !== "0x"; - if (!bytecode || bytecode === "0x") { - // Wallet is not deployed. Check if it's EIP-6492 with deployment info. - // EIP-6492 signatures contain factory address and calldata needed for deployment. - // Non-EIP-6492 undeployed wallets cannot succeed (no way to deploy them). - const erc6492Data = parseErc6492Signature(signature); - const hasDeploymentInfo = - erc6492Data.address && - erc6492Data.data && - !isAddressEqual(erc6492Data.address, "0x0000000000000000000000000000000000000000"); - - if (!hasDeploymentInfo) { - // Non-EIP-6492 undeployed smart wallet - will always fail at settlement - // since EIP-3009 requires on-chain EIP-1271 validation - return { - isValid: false, - invalidReason: "invalid_exact_evm_payload_undeployed_smart_wallet", - payer: payerAddress, - }; - } - // EIP-6492 signature with deployment info - allow through - // Facilitators with sponsored deployment support can handle this in settle() - } else { - // Wallet is deployed but signature still failed - invalid signature - return { - isValid: false, - invalidReason: "invalid_exact_evm_payload_signature", - payer: exactEvmPayload.authorization.from, - }; - } - } else { - // EOA signature failed + if (!isDeployed && !hasDeploymentInfo) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_signature", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrUndeployedSmartWallet, + payer, }; } } @@ -214,8 +332,8 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { if (getAddress(exactEvmPayload.authorization.to) !== getAddress(requirements.payTo)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_recipient_mismatch", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrRecipientMismatch, + payer, }; } @@ -224,8 +342,8 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { if (BigInt(exactEvmPayload.authorization.validBefore) < BigInt(now + 6)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_authorization_valid_before", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrValidBeforeExpired, + payer, }; } @@ -233,189 +351,43 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { if (BigInt(exactEvmPayload.authorization.validAfter) > BigInt(now)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_authorization_valid_after", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrValidAfterInFuture, + payer, }; } - // Check balance - try { - const balance = (await this.signer.readContract({ - address: erc20Address, - abi: eip3009ABI, - functionName: "balanceOf", - args: [exactEvmPayload.authorization.from], - })) as bigint; - - if (BigInt(balance) < BigInt(requirementsV1.maxAmountRequired)) { - return { - isValid: false, - invalidReason: "insufficient_funds", - invalidMessage: `Insufficient funds to complete the payment. Required: ${requirementsV1.maxAmountRequired} ${requirements.asset}, Available: ${balance.toString()} ${requirements.asset}. Please add funds to your wallet and try again.`, - payer: exactEvmPayload.authorization.from, - }; - } - } catch { - // If we can't check balance, continue with other validations - } - - // Verify amount is sufficient - if (BigInt(exactEvmPayload.authorization.value) < BigInt(requirementsV1.maxAmountRequired)) { + // Verify amount exactly matches requirements + if (BigInt(exactEvmPayload.authorization.value) !== BigInt(requirementsV1.maxAmountRequired)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_authorization_value", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrInvalidAuthorizationValue, + payer, }; } + // Transaction simulation + if (options?.simulate !== false) { + const simulationSucceeded = await simulateEip3009Transfer( + this.signer, + erc20Address, + exactEvmPayload, + eip6492Deployment, + ); + if (!simulationSucceeded) { + return diagnoseEip3009SimulationFailure( + this.signer, + erc20Address, + exactEvmPayload, + requirements, + requirementsV1.maxAmountRequired, + ); + } + } + return { isValid: true, invalidReason: undefined, - payer: exactEvmPayload.authorization.from, + payer, }; } - - /** - * Settles a payment by executing the transfer (V1). - * - * @param payload - The payment payload to settle - * @param requirements - The payment requirements - * @returns Promise resolving to settlement response - */ - async settle( - payload: PaymentPayload, - requirements: PaymentRequirements, - ): Promise { - const payloadV1 = payload as unknown as PaymentPayloadV1; - const exactEvmPayload = payload.payload as ExactEvmPayloadV1; - - // Re-verify before settling - const valid = await this.verify(payload, requirements); - if (!valid.isValid) { - return { - success: false, - network: payloadV1.network, - transaction: "", - errorReason: valid.invalidReason ?? "invalid_scheme", - payer: exactEvmPayload.authorization.from, - }; - } - - try { - // Parse ERC-6492 signature if applicable - const parseResult = parseErc6492Signature(exactEvmPayload.signature!); - const { signature, address: factoryAddress, data: factoryCalldata } = parseResult; - - // Deploy ERC-4337 smart wallet via EIP-6492 if configured and needed - if ( - this.config.deployERC4337WithEIP6492 && - factoryAddress && - factoryCalldata && - !isAddressEqual(factoryAddress, "0x0000000000000000000000000000000000000000") - ) { - // Check if smart wallet is already deployed - const payerAddress = exactEvmPayload.authorization.from; - const bytecode = await this.signer.getCode({ address: payerAddress }); - - if (!bytecode || bytecode === "0x") { - // Wallet not deployed - attempt deployment - try { - console.log(`Deploying ERC-4337 smart wallet for ${payerAddress} via EIP-6492`); - - // Send the factory calldata directly as a transaction - // The factoryCalldata already contains the complete encoded function call - const deployTx = await this.signer.sendTransaction({ - to: factoryAddress as Hex, - data: factoryCalldata as Hex, - }); - - // Wait for deployment transaction - await this.signer.waitForTransactionReceipt({ hash: deployTx }); - console.log(`Successfully deployed smart wallet for ${payerAddress}`); - } catch (deployError) { - console.error("Smart wallet deployment failed:", deployError); - // Deployment failed - cannot proceed - throw deployError; - } - } else { - console.log(`Smart wallet for ${payerAddress} already deployed, skipping deployment`); - } - } - - // Determine if this is an ECDSA signature (EOA) or smart wallet signature - // ECDSA signatures are exactly 65 bytes (130 hex chars without 0x) - const signatureLength = signature.startsWith("0x") ? signature.length - 2 : signature.length; - const isECDSA = signatureLength === 130; - - let tx: Hex; - if (isECDSA) { - // For EOA wallets, parse signature into v, r, s and use that overload - const parsedSig = parseSignature(signature); - - tx = await this.signer.writeContract({ - address: getAddress(requirements.asset), - abi: eip3009ABI, - functionName: "transferWithAuthorization", - args: [ - getAddress(exactEvmPayload.authorization.from), - getAddress(exactEvmPayload.authorization.to), - BigInt(exactEvmPayload.authorization.value), - BigInt(exactEvmPayload.authorization.validAfter), - BigInt(exactEvmPayload.authorization.validBefore), - exactEvmPayload.authorization.nonce, - (parsedSig.v as number | undefined) || parsedSig.yParity, - parsedSig.r, - parsedSig.s, - ], - }); - } else { - // For smart wallets, use the bytes signature overload - // The signature contains WebAuthn/P256 or other ERC-1271 compatible signature data - tx = await this.signer.writeContract({ - address: getAddress(requirements.asset), - abi: eip3009ABI, - functionName: "transferWithAuthorization", - args: [ - getAddress(exactEvmPayload.authorization.from), - getAddress(exactEvmPayload.authorization.to), - BigInt(exactEvmPayload.authorization.value), - BigInt(exactEvmPayload.authorization.validAfter), - BigInt(exactEvmPayload.authorization.validBefore), - exactEvmPayload.authorization.nonce, - signature, - ], - }); - } - - // Wait for transaction confirmation - const receipt = await this.signer.waitForTransactionReceipt({ hash: tx }); - console.log("receipt ", receipt); - - if (receipt.status !== "success") { - return { - success: false, - errorReason: "invalid_transaction_state", - transaction: tx, - network: payloadV1.network, - payer: exactEvmPayload.authorization.from, - }; - } - - return { - success: true, - transaction: tx, - network: payloadV1.network, - payer: exactEvmPayload.authorization.from, - }; - } catch (error) { - console.error("Failed to settle transaction:", error); - return { - success: false, - errorReason: "transaction_failed", - transaction: "", - network: payloadV1.network, - payer: exactEvmPayload.authorization.from, - }; - } - } } diff --git a/typescript/packages/mechanisms/evm/src/index.ts b/typescript/packages/mechanisms/evm/src/index.ts index 380e530..38277af 100644 --- a/typescript/packages/mechanisms/evm/src/index.ts +++ b/typescript/packages/mechanisms/evm/src/index.ts @@ -29,13 +29,22 @@ export type { } from "./types"; export { isPermit2Payload, isEIP3009Payload } from "./types"; +// Upto scheme client +export { UptoEvmScheme } from "./upto"; + +// Upto types +export type { UptoPermit2Payload, UptoPermit2Witness, UptoPermit2Authorization } from "./types"; +export { isUptoPermit2Payload } from "./types"; + // Constants export { PERMIT2_ADDRESS, x402ExactPermit2ProxyAddress, x402UptoPermit2ProxyAddress, permit2WitnessTypes, + uptoPermit2WitnessTypes, authorizationTypes, eip3009ABI, x402ExactPermit2ProxyABI, + x402UptoPermit2ProxyABI, } from "./constants"; diff --git a/typescript/packages/mechanisms/evm/src/multicall.ts b/typescript/packages/mechanisms/evm/src/multicall.ts new file mode 100644 index 0000000..9e0534a --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/multicall.ts @@ -0,0 +1,140 @@ +import { encodeFunctionData, decodeFunctionResult } from "viem"; + +/** + * Multicall3 contract address. + * Same address on all EVM chains via CREATE2 deployment. + * + * @see https://github.com/mds1/multicall + */ +export const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11" as const; + +/** Multicall3 getEthBalance ABI for querying native token balance. */ +export const multicall3GetEthBalanceAbi = [ + { + name: "getEthBalance", + inputs: [{ name: "addr", type: "address" }], + outputs: [{ name: "balance", type: "uint256" }], + stateMutability: "view", + type: "function", + }, +] as const; + +/** Multicall3 tryAggregate ABI for batching calls. */ +const multicall3ABI = [ + { + inputs: [ + { name: "requireSuccess", type: "bool" }, + { + name: "calls", + type: "tuple[]", + components: [ + { name: "target", type: "address" }, + { name: "callData", type: "bytes" }, + ], + }, + ], + name: "tryAggregate", + outputs: [ + { + name: "returnData", + type: "tuple[]", + components: [ + { name: "success", type: "bool" }, + { name: "returnData", type: "bytes" }, + ], + }, + ], + stateMutability: "payable", + type: "function", + }, +] as const; + +export type ContractCall = { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; +}; + +export type RawContractCall = { + address: `0x${string}`; + callData: `0x${string}`; +}; + +export type MulticallSuccess = { status: "success"; result: unknown }; +export type MulticallFailure = { status: "failure"; error: Error }; +export type MulticallResult = MulticallSuccess | MulticallFailure; + +/** + * Batches contract calls via Multicall3 `tryAggregate(false, ...)`. + * + * Accepts a mix of typed ContractCall (ABI-encoded + decoded) and + * RawContractCall (pre-encoded calldata, no decoding) entries. + * Raw calls are useful for the EIP-6492 factory deployment case + * where calldata is pre-encoded with no ABI available. + */ +type ReadContractFn = (args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; +}) => Promise; + +/** + * Executes multiple contract read calls in a single RPC round-trip using Multicall3. + * + * @param readContract - Function that performs a single contract read (e.g. viem readContract) + * @param calls - Array of contract calls to batch (ContractCall or RawContractCall) + * @returns A promise that resolves to an array of decoded results, one per call + */ +export async function multicall( + readContract: ReadContractFn, + calls: ReadonlyArray, +): Promise { + const aggregateCalls = calls.map(call => { + if ("callData" in call) { + return { target: call.address, callData: call.callData }; + } + const callData = encodeFunctionData({ + abi: call.abi, + functionName: call.functionName, + args: call.args as unknown[], + }); + return { target: call.address, callData }; + }); + + const rawResults = (await readContract({ + address: MULTICALL3_ADDRESS, + abi: multicall3ABI, + functionName: "tryAggregate", + args: [false, aggregateCalls], + })) as { success: boolean; returnData: `0x${string}` }[]; + + return rawResults.map((raw, i) => { + if (!raw.success) { + return { + status: "failure" as const, + error: new Error(`multicall: call reverted (returnData: ${raw.returnData})`), + }; + } + + const call = calls[i]; + if ("callData" in call) { + return { status: "success" as const, result: undefined }; + } + + try { + const decoded = decodeFunctionResult({ + abi: call.abi, + functionName: call.functionName, + data: raw.returnData, + }); + return { status: "success" as const, result: decoded }; + } catch (err) { + return { + status: "failure" as const, + error: err instanceof Error ? err : new Error(String(err)), + }; + } + }); +} diff --git a/typescript/packages/mechanisms/evm/src/shared/defaultAssets.ts b/typescript/packages/mechanisms/evm/src/shared/defaultAssets.ts new file mode 100644 index 0000000..f62ecbd --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/shared/defaultAssets.ts @@ -0,0 +1,115 @@ +import type { Network } from "@x402/core/types"; + +/** + * Base stablecoin asset configuration shared across all EVM payment schemes. + * Contains the core fields needed to identify and convert tokens. + */ +export type DefaultAssetInfo = { + /** Token contract address */ + address: string; + /** EIP-712 domain name (must match the token's domain separator) */ + name: string; + /** EIP-712 domain version (must match the token's domain separator) */ + version: string; + /** Token decimal places (typically 6 for USDC) */ + decimals: number; +}; + +/** + * Extended asset configuration for the exact scheme. + * Includes transfer method hints that control client-side behaviour. + */ +export type ExactDefaultAssetInfo = DefaultAssetInfo & { + /** + * Transfer method override: `"permit2"` for tokens that don't support EIP-3009. + * Omit for EIP-3009 tokens (default behaviour). + */ + assetTransferMethod?: string; + /** + * Set to `true` for permit2 tokens that implement EIP-2612 `permit()`. + * Controls whether name/version are included in `extra` so the client can + * sign a gasless EIP-2612 permit for Permit2 approval. + */ + supportsEip2612?: boolean; +}; + +/** + * Default stablecoins indexed by CAIP-2 network identifier. + * + * Each network has the right to determine its own default stablecoin that can + * be expressed as a USD string by calling servers. See DEFAULT_ASSET.md in + * exact/server/ for how to add new chains. + */ +export const DEFAULT_STABLECOINS: Record = { + "eip155:8453": { + address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + name: "USD Coin", + version: "2", + decimals: 6, + }, // Base mainnet USDC + "eip155:84532": { + address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + name: "USDC", + version: "2", + decimals: 6, + }, // Base Sepolia USDC + "eip155:4326": { + address: "0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7", + name: "MegaUSD", + version: "1", + decimals: 18, + assetTransferMethod: "permit2", + supportsEip2612: true, + }, // MegaETH mainnet MegaUSD (no EIP-3009, supports EIP-2612) + "eip155:143": { + address: "0x754704Bc059F8C67012fEd69BC8A327a5aafb603", + name: "USD Coin", + version: "2", + decimals: 6, + }, // Monad mainnet USDC + "eip155:988": { + address: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", + name: "USDT0", + version: "1", + decimals: 6, + }, // Stable mainnet USDT0 + "eip155:2201": { + address: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", + name: "USDT0", + version: "1", + decimals: 6, + }, // Stable testnet USDT0 + "eip155:137": { + address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + name: "USD Coin", + version: "2", + decimals: 6, + }, // Polygon mainnet USDC + "eip155:42161": { + address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + name: "USD Coin", + version: "2", + decimals: 6, + }, // Arbitrum One USDC + "eip155:421614": { + address: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", + name: "USD Coin", + version: "2", + decimals: 6, + }, // Arbitrum Sepolia USDC +}; + +/** + * Look up the default stablecoin for a network. + * + * @param network - CAIP-2 network identifier (e.g. "eip155:8453") + * @returns The default asset info + * @throws If no default asset is configured for the network + */ +export function getDefaultAsset(network: Network): ExactDefaultAssetInfo { + const info = DEFAULT_STABLECOINS[network]; + if (!info) { + throw new Error(`No default asset configured for network ${network}`); + } + return info; +} diff --git a/typescript/packages/mechanisms/evm/src/shared/erc20approval.ts b/typescript/packages/mechanisms/evm/src/shared/erc20approval.ts new file mode 100644 index 0000000..0a020d9 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/shared/erc20approval.ts @@ -0,0 +1,154 @@ +import { + getAddress, + parseTransaction, + decodeFunctionData, + recoverTransactionAddress, + type TransactionSerialized, +} from "viem"; +import type { VerifyResponse } from "@x402/core/types"; +import { + validateErc20ApprovalGasSponsoringInfo, + type Erc20ApprovalGasSponsoringInfo, +} from "../exact/extensions"; +import { PERMIT2_ADDRESS, erc20ApproveAbi } from "../constants"; +import { + ErrErc20ApprovalInvalidFormat, + ErrErc20ApprovalFromMismatch, + ErrErc20ApprovalAssetMismatch, + ErrErc20ApprovalSpenderNotPermit2, + ErrErc20ApprovalTxWrongTarget, + ErrErc20ApprovalTxWrongSelector, + ErrErc20ApprovalTxWrongSpender, + ErrErc20ApprovalTxInvalidCalldata, + ErrErc20ApprovalTxSignerMismatch, + ErrErc20ApprovalTxInvalidSignature, + ErrErc20ApprovalTxParseFailed, +} from "../exact/facilitator/errors"; + +/** The approve(address,uint256) function selector */ +const APPROVE_SELECTOR = "0x095ea7b3"; + +/** + * Validates ERC-20 approval extension data for a Permit2 payment. + * + * Performs comprehensive validation: + * - Format validation via validateErc20ApprovalGasSponsoringInfo (JSON Schema) + * - `from` matches payer + * - `asset` matches token + * - `spender` is PERMIT2_ADDRESS + * - Transaction `to` matches token address + * - Transaction calldata is a valid approve(PERMIT2_ADDRESS, ...) call + * - Recovered transaction signer matches `from` + * + * @param info - The ERC-20 approval gas sponsoring info + * @param payer - The expected payer address + * @param tokenAddress - The expected token address + * @returns Validation result with invalidReason and invalidMessage on failure + */ +export async function validateErc20ApprovalForPayment( + info: Erc20ApprovalGasSponsoringInfo, + payer: `0x${string}`, + tokenAddress: `0x${string}`, +): Promise> { + if (!validateErc20ApprovalGasSponsoringInfo(info)) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalInvalidFormat, + invalidMessage: "ERC-20 approval extension info failed schema validation", + }; + } + + if (getAddress(info.from) !== getAddress(payer)) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalFromMismatch, + invalidMessage: `Expected from=${payer}, got ${info.from}`, + }; + } + + if (getAddress(info.asset) !== tokenAddress) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalAssetMismatch, + invalidMessage: `Expected asset=${tokenAddress}, got ${info.asset}`, + }; + } + + if (getAddress(info.spender) !== getAddress(PERMIT2_ADDRESS)) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalSpenderNotPermit2, + invalidMessage: `Expected spender=${PERMIT2_ADDRESS}, got ${info.spender}`, + }; + } + + try { + const serializedTx = info.signedTransaction as TransactionSerialized; + const tx = parseTransaction(serializedTx); + + if (!tx.to || getAddress(tx.to) !== tokenAddress) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxWrongTarget, + invalidMessage: `Transaction targets ${tx.to ?? "null"}, expected ${tokenAddress}`, + }; + } + + const data = tx.data ?? "0x"; + if (!data.startsWith(APPROVE_SELECTOR)) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxWrongSelector, + invalidMessage: `Transaction calldata does not start with approve() selector ${APPROVE_SELECTOR}`, + }; + } + + try { + const decoded = decodeFunctionData({ + abi: erc20ApproveAbi, + data: data as `0x${string}`, + }); + const calldataSpender = getAddress(decoded.args[0] as `0x${string}`); + if (calldataSpender !== getAddress(PERMIT2_ADDRESS)) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxWrongSpender, + invalidMessage: `approve() spender is ${calldataSpender}, expected Permit2 ${PERMIT2_ADDRESS}`, + }; + } + } catch { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxInvalidCalldata, + invalidMessage: "Failed to decode approve() calldata from the signed transaction", + }; + } + + try { + const recoveredAddress = await recoverTransactionAddress({ + serializedTransaction: serializedTx, + }); + if (getAddress(recoveredAddress) !== getAddress(payer)) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxSignerMismatch, + invalidMessage: `Transaction signed by ${recoveredAddress}, expected payer ${payer}`, + }; + } + } catch { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxInvalidSignature, + invalidMessage: "Failed to recover signer from the signed transaction", + }; + } + } catch { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxParseFailed, + invalidMessage: "Failed to parse the signed transaction", + }; + } + + return { isValid: true }; +} diff --git a/typescript/packages/mechanisms/evm/src/shared/extensions.ts b/typescript/packages/mechanisms/evm/src/shared/extensions.ts new file mode 100644 index 0000000..11baee6 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/shared/extensions.ts @@ -0,0 +1,147 @@ +import { PaymentRequirements, PaymentPayloadResult, PaymentPayloadContext } from "@x402/core/types"; +import { EIP2612_GAS_SPONSORING_KEY, ERC20_APPROVAL_GAS_SPONSORING_KEY } from "../exact/extensions"; +import { getAddress } from "viem"; +import { PERMIT2_ADDRESS, erc20AllowanceAbi } from "../constants"; +import { getEvmChainId } from "../utils"; +import { ClientEvmSigner } from "../signer"; +import { signEip2612Permit } from "../exact/client/eip2612"; +import { signErc20ApprovalTransaction } from "../exact/client/erc20approval"; +import { resolveExtensionRpcCapabilities, type ExactEvmSchemeOptions } from "./rpc"; + +/** + * Attempts to sign an EIP-2612 permit for gasless Permit2 approval. + * + * @param signer - The EVM client signer + * @param options - Optional RPC configuration for backfilling capabilities + * @param requirements - The payment requirements from the server + * @param result - The payment payload result from the scheme + * @param context - Optional context containing server extensions and metadata + * @returns Extension data for EIP-2612 gas sponsoring, or undefined if not applicable + */ +export async function trySignEip2612PermitExtension( + signer: ClientEvmSigner, + options: ExactEvmSchemeOptions | undefined, + requirements: PaymentRequirements, + result: PaymentPayloadResult, + context?: PaymentPayloadContext, +): Promise | undefined> { + const capabilities = resolveExtensionRpcCapabilities(requirements.network, signer, options); + + if (!capabilities.readContract) { + return undefined; + } + + if (!context?.extensions?.[EIP2612_GAS_SPONSORING_KEY]) { + return undefined; + } + + const tokenName = requirements.extra?.name as string | undefined; + const tokenVersion = requirements.extra?.version as string | undefined; + if (!tokenName || !tokenVersion) { + return undefined; + } + + const chainId = getEvmChainId(requirements.network); + const tokenAddress = getAddress(requirements.asset) as `0x${string}`; + + try { + const allowance = (await capabilities.readContract({ + address: tokenAddress, + abi: erc20AllowanceAbi, + functionName: "allowance", + args: [signer.address, PERMIT2_ADDRESS], + })) as bigint; + + if (allowance >= BigInt(requirements.amount)) { + return undefined; + } + } catch { + // Allowance check failed, proceed with signing + } + + const permit2Auth = result.payload?.permit2Authorization as Record | undefined; + const deadline = + (permit2Auth?.deadline as string) ?? + Math.floor(Date.now() / 1000 + requirements.maxTimeoutSeconds).toString(); + + const info = await signEip2612Permit( + { + address: signer.address, + signTypedData: msg => signer.signTypedData(msg), + readContract: capabilities.readContract, + }, + tokenAddress, + tokenName, + tokenVersion, + chainId, + deadline, + requirements.amount, + ); + + return { + [EIP2612_GAS_SPONSORING_KEY]: { info }, + }; +} + +/** + * Attempts to sign an ERC-20 approval transaction for gasless Permit2 approval. + * + * @param signer - The EVM client signer + * @param options - Optional RPC configuration for backfilling capabilities + * @param requirements - The payment requirements from the server + * @param context - Optional context containing server extensions and metadata + * @returns Extension data for ERC-20 approval gas sponsoring, or undefined if not applicable + */ +export async function trySignErc20ApprovalExtension( + signer: ClientEvmSigner, + options: ExactEvmSchemeOptions | undefined, + requirements: PaymentRequirements, + context?: PaymentPayloadContext, +): Promise | undefined> { + const capabilities = resolveExtensionRpcCapabilities(requirements.network, signer, options); + + if (!capabilities.readContract) { + return undefined; + } + + if (!context?.extensions?.[ERC20_APPROVAL_GAS_SPONSORING_KEY]) { + return undefined; + } + + if (!capabilities.signTransaction || !capabilities.getTransactionCount) { + return undefined; + } + + const chainId = getEvmChainId(requirements.network); + const tokenAddress = getAddress(requirements.asset) as `0x${string}`; + + try { + const allowance = (await capabilities.readContract({ + address: tokenAddress, + abi: erc20AllowanceAbi, + functionName: "allowance", + args: [signer.address, PERMIT2_ADDRESS], + })) as bigint; + + if (allowance >= BigInt(requirements.amount)) { + return undefined; + } + } catch { + // Allowance check failed, proceed with signing + } + + const info = await signErc20ApprovalTransaction( + { + address: signer.address, + signTransaction: capabilities.signTransaction, + getTransactionCount: capabilities.getTransactionCount, + estimateFeesPerGas: capabilities.estimateFeesPerGas, + }, + tokenAddress, + chainId, + ); + + return { + [ERC20_APPROVAL_GAS_SPONSORING_KEY]: { info }, + }; +} diff --git a/typescript/packages/mechanisms/evm/src/shared/permit2.ts b/typescript/packages/mechanisms/evm/src/shared/permit2.ts new file mode 100644 index 0000000..7f6d8af --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/shared/permit2.ts @@ -0,0 +1,813 @@ +import { + PaymentPayload, + PaymentPayloadResult, + PaymentRequirements, + FacilitatorContext, + SettleResponse, + VerifyResponse, +} from "@x402/core/types"; +import { + extractEip2612GasSponsoringInfo, + validateEip2612GasSponsoringInfo, + extractErc20ApprovalGasSponsoringInfo, + ERC20_APPROVAL_GAS_SPONSORING_KEY, + type Eip2612GasSponsoringInfo, + type Erc20ApprovalGasSponsoringFacilitatorExtension, + type Erc20ApprovalGasSponsoringSigner, +} from "../exact/extensions"; +import { getAddress, encodeFunctionData } from "viem"; +import { PERMIT2_ADDRESS, eip3009ABI, erc20AllowanceAbi, permit2WitnessTypes } from "../constants"; +import { multicall, ContractCall } from "../multicall"; +import { createPermit2Nonce, getEvmChainId } from "../utils"; +import { + ErrPermit2612AmountMismatch, + ErrPermit2InvalidAmount, + ErrPermit2InvalidDestination, + ErrPermit2InvalidNonce, + ErrPermit2InvalidOwner, + ErrPermit2InvalidSignature, + ErrPermit2PaymentTooEarly, + ErrPermit2AllowanceRequired, + ErrPermit2SimulationFailed, + ErrPermit2InsufficientBalance, + ErrPermit2ProxyNotDeployed, + ErrInvalidTransactionState, + ErrTransactionFailed, + ErrInvalidEip2612ExtensionFormat, + ErrEip2612FromMismatch, + ErrEip2612AssetMismatch, + ErrEip2612SpenderNotPermit2, + ErrEip2612DeadlineExpired, + ErrErc20ApprovalTxFailed, +} from "../exact/facilitator/errors"; +import { ClientEvmSigner, FacilitatorEvmSigner } from "../signer"; +import { ExactPermit2Payload, Permit2Authorization, UptoPermit2Payload } from "../types"; +import { validateErc20ApprovalForPayment } from "./erc20approval"; +import { + ErrUptoAmountExceedsPermitted, + ErrUptoUnauthorizedFacilitator, +} from "../upto/facilitator/errors"; + +/** + * Base type for Permit2 payloads shared between exact and upto schemes. + * Both {@link ExactPermit2Payload} and {@link UptoPermit2Payload} satisfy this type. + */ +export type Permit2PayloadBase = ExactPermit2Payload | UptoPermit2Payload; + +/** + * Configuration for the Permit2 proxy contract used during settlement. + * The exact and upto schemes use different proxy addresses and ABIs. + */ +export type Permit2ProxyConfig = { + /** The deployed proxy contract address. */ + proxyAddress: `0x${string}`; + /** The proxy contract ABI (must include settle and settleWithPermit functions). */ + proxyABI: readonly Record[]; +}; + +export type Permit2SimulationResult = + | { ok: true } + | { + ok: false; + error: unknown; + errorMessage?: string; + }; + +/** + * Checks Permit2 allowance and validates gas sponsoring extensions if allowance is insufficient. + * + * When the on-chain ERC-20 allowance to the Permit2 contract is below the required amount, + * this function falls back to validating EIP-2612 or ERC-20 approval gas sponsoring extensions + * attached to the payment payload. + * + * @param signer - The facilitator signer for on-chain reads + * @param payload - The payment payload + * @param requirements - The payment requirements + * @param payer - The payer address + * @param tokenAddress - The token contract address + * @param context - Optional facilitator context for extension lookup + * @returns A VerifyResponse if verification should stop (failure), or null to continue + */ +export async function verifyPermit2Allowance( + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + requirements: PaymentRequirements, + payer: `0x${string}`, + tokenAddress: `0x${string}`, + context?: FacilitatorContext, +): Promise { + try { + const allowance = (await signer.readContract({ + address: tokenAddress, + abi: erc20AllowanceAbi, + functionName: "allowance", + args: [payer, PERMIT2_ADDRESS], + })) as bigint; + + if (allowance >= BigInt(requirements.amount)) { + return null; // Sufficient allowance, continue verification + } + + // Allowance insufficient — try EIP-2612 gas sponsoring first + const eip2612Info = extractEip2612GasSponsoringInfo(payload); + if (eip2612Info) { + const result = validateEip2612PermitForPayment(eip2612Info, payer, tokenAddress); + if (!result.isValid) { + return { isValid: false, invalidReason: result.invalidReason!, payer }; + } + return null; // EIP-2612 is valid, allowance will be set atomically during settlement + } + + // Try ERC-20 approval gas sponsoring as fallback + const erc20GasSponsorshipExtension = + context?.getExtension( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + ); + if (erc20GasSponsorshipExtension) { + const erc20Info = extractErc20ApprovalGasSponsoringInfo(payload); + if (erc20Info) { + const result = await validateErc20ApprovalForPayment(erc20Info, payer, tokenAddress); + if (!result.isValid) { + return { isValid: false, invalidReason: result.invalidReason!, payer }; + } + return null; // ERC-20 approval is valid, will be broadcast before settlement + } + } + + return { isValid: false, invalidReason: "permit2_allowance_required", payer }; + } catch { + // Allowance check failed — validate extensions if present; fail closed if none valid + const eip2612Info = extractEip2612GasSponsoringInfo(payload); + if (eip2612Info) { + const result = validateEip2612PermitForPayment(eip2612Info, payer, tokenAddress); + if (!result.isValid) { + return { isValid: false, invalidReason: result.invalidReason!, payer }; + } + return null; + } + + const erc20GasSponsorshipExtension = + context?.getExtension( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + ); + if (erc20GasSponsorshipExtension) { + const erc20Info = extractErc20ApprovalGasSponsoringInfo(payload); + if (erc20Info) { + const result = await validateErc20ApprovalForPayment(erc20Info, payer, tokenAddress); + if (!result.isValid) { + return { isValid: false, invalidReason: result.invalidReason!, payer }; + } + return null; + } + } + + return { isValid: false, invalidReason: "permit2_allowance_required", payer }; + } +} + +/** + * Waits for a transaction receipt and returns the appropriate SettleResponse. + * + * @param signer - Signer with waitForTransactionReceipt capability + * @param tx - The transaction hash to wait for + * @param payload - The payment payload (for network info) + * @param payer - The payer address + * @returns Promise resolving to a settlement response indicating success or failure + */ +export async function waitAndReturnSettleResponse( + signer: Pick, + tx: `0x${string}`, + payload: PaymentPayload, + payer: `0x${string}`, +): Promise { + const receipt = await signer.waitForTransactionReceipt({ hash: tx }); + + if (receipt.status !== "success") { + return { + success: false, + errorReason: ErrInvalidTransactionState, + transaction: tx, + network: payload.accepted.network, + payer, + }; + } + + return { + success: true, + transaction: tx, + network: payload.accepted.network, + payer, + }; +} + +/** + * Maps contract revert errors to structured SettleResponse error reasons. + * + * Inspects the error message for known contract revert strings and maps them + * to the corresponding error reason constants. Falls back to a generic + * "transaction_failed" reason with truncated message for unrecognized errors. + * + * @param error - The caught error (typically from a contract write) + * @param payload - The payment payload (for network info) + * @param payer - The payer address + * @returns A failed SettleResponse with the mapped error reason + */ +export function mapSettleError( + error: unknown, + payload: PaymentPayload, + payer: `0x${string}`, +): SettleResponse { + let errorReason: string = ErrTransactionFailed; + if (error instanceof Error) { + const message = error.message; + if (message.includes("Permit2612AmountMismatch")) { + errorReason = ErrPermit2612AmountMismatch; + } else if (message.includes("InvalidAmount")) { + errorReason = ErrPermit2InvalidAmount; + } else if (message.includes("InvalidDestination")) { + errorReason = ErrPermit2InvalidDestination; + } else if (message.includes("InvalidOwner")) { + errorReason = ErrPermit2InvalidOwner; + } else if (message.includes("PaymentTooEarly")) { + errorReason = ErrPermit2PaymentTooEarly; + } else if (message.includes("InvalidSignature") || message.includes("SignatureExpired")) { + errorReason = ErrPermit2InvalidSignature; + } else if (message.includes("InvalidNonce")) { + errorReason = ErrPermit2InvalidNonce; + } else if (message.includes("erc20_approval_tx_failed")) { + errorReason = ErrErc20ApprovalTxFailed; + } else if (message.includes("AmountExceedsPermitted")) { + errorReason = ErrUptoAmountExceedsPermitted; + } else if (message.includes("UnauthorizedFacilitator")) { + errorReason = ErrUptoUnauthorizedFacilitator; + } else { + errorReason = `${ErrTransactionFailed}: ${message.slice(0, 500)}`; + } + } + return { + success: false, + errorReason, + transaction: "", + network: payload.accepted.network, + payer, + }; +} + +/** + * Validates EIP-2612 permit extension data for a Permit2 payment. + * + * Checks that the permit extension has a valid format and that the from address, + * asset address, spender address, and deadline all match expectations. + * + * @param info - The EIP-2612 gas sponsoring info extracted from the payment payload + * @param payer - The expected payer address + * @param tokenAddress - The expected token address + * @returns Validation result with isValid flag and optional invalidReason string + */ +export function validateEip2612PermitForPayment( + info: Eip2612GasSponsoringInfo, + payer: `0x${string}`, + tokenAddress: `0x${string}`, +): { isValid: boolean; invalidReason?: string } { + if (!validateEip2612GasSponsoringInfo(info)) { + return { isValid: false, invalidReason: ErrInvalidEip2612ExtensionFormat }; + } + + if (getAddress(info.from as `0x${string}`) !== getAddress(payer)) { + return { isValid: false, invalidReason: ErrEip2612FromMismatch }; + } + + if (getAddress(info.asset as `0x${string}`) !== tokenAddress) { + return { isValid: false, invalidReason: ErrEip2612AssetMismatch }; + } + + if (getAddress(info.spender as `0x${string}`) !== getAddress(PERMIT2_ADDRESS)) { + return { isValid: false, invalidReason: ErrEip2612SpenderNotPermit2 }; + } + + const now = Math.floor(Date.now() / 1000); + if (BigInt(info.deadline) < BigInt(now + 6)) { + return { isValid: false, invalidReason: ErrEip2612DeadlineExpired }; + } + + return { isValid: true }; +} + +// --------------------------------------------------------------------------- +// Simulation helpers (shared across exact and upto) +// --------------------------------------------------------------------------- + +/** + * Simulates settle() via eth_call (readContract). + * Returns true if simulation succeeded, false if it failed. + * + * @param config - The proxy contract configuration (address and ABI) + * @param signer - EVM signer for contract reads + * @param settleArgs - Pre-built settle function arguments (scheme-specific) + * @returns true if simulation succeeded, false if it failed + */ +export async function simulatePermit2Settle( + config: Permit2ProxyConfig, + signer: FacilitatorEvmSigner, + settleArgs: readonly unknown[], +): Promise { + try { + await signer.readContract({ + address: config.proxyAddress, + abi: config.proxyABI, + functionName: "settle", + args: settleArgs, + }); + return { ok: true }; + } catch (error) { + return { + ok: false, + error, + errorMessage: extractSimulationErrorMessage(error), + }; + } +} + +/** + * Simulates settleWithPermit() via eth_call (readContract). + * The contract atomically calls token.permit() then PERMIT2.permitTransferFrom(), + * so simulation covers allowance + balance + nonces. + * + * @param config - The proxy contract configuration (address and ABI) + * @param signer - EVM signer for contract reads + * @param settleArgs - Pre-built settle function arguments (scheme-specific) + * @param eip2612Info - EIP-2612 gas sponsoring info from the payload extension + * @returns true if simulation succeeded, false if it failed + */ +export async function simulatePermit2SettleWithPermit( + config: Permit2ProxyConfig, + signer: FacilitatorEvmSigner, + settleArgs: readonly unknown[], + eip2612Info: Eip2612GasSponsoringInfo, +): Promise { + try { + const { v, r, s } = splitEip2612Signature(eip2612Info.signature); + + await signer.readContract({ + address: config.proxyAddress, + abi: config.proxyABI, + functionName: "settleWithPermit", + args: [ + { + value: BigInt(eip2612Info.amount), + deadline: BigInt(eip2612Info.deadline), + r, + s, + v, + }, + ...settleArgs, + ], + }); + return { ok: true }; + } catch (error) { + return { + ok: false, + error, + errorMessage: extractSimulationErrorMessage(error), + }; + } +} + +/** + * Delegates the full approve+settle simulation flow to the extension signer via simulateTransactions. + * The signer owns execution strategy. + * + * @param config - The proxy contract configuration (address and ABI) + * @param extensionSigner - The extension signer with simulateTransactions capability + * @param settleArgs - Pre-built settle function arguments (scheme-specific) + * @param erc20Info - Object containing the signed approval transaction + * @param erc20Info.signedTransaction - The RLP-encoded signed ERC-20 approve transaction hex string + * @returns true if the bundle simulation succeeded, false otherwise + */ +export async function simulatePermit2SettleWithErc20Approval( + config: Permit2ProxyConfig, + extensionSigner: Erc20ApprovalGasSponsoringSigner, + settleArgs: readonly unknown[], + erc20Info: { signedTransaction: string }, +): Promise { + if (!extensionSigner.simulateTransactions) { + return { ok: false, error: new Error("simulateTransactions unavailable") }; + } + + try { + const settleData = encodeFunctionData({ + abi: config.proxyABI, + functionName: "settle", + args: settleArgs, + }); + + const ok = await extensionSigner.simulateTransactions([ + erc20Info.signedTransaction as `0x${string}`, + { to: config.proxyAddress, data: settleData, gas: BigInt(300_000) }, + ]); + return ok ? { ok: true } : { ok: false, error: new Error("bundle simulation failed") }; + } catch (error) { + return { + ok: false, + error, + errorMessage: extractSimulationErrorMessage(error), + }; + } +} + +/** + * Diagnoses a Permit2 simulation failure by performing a multicall to check the proxy deployment, balance and allowance. + * + * @param config - The proxy contract configuration (address and ABI) + * @param signer - EVM signer for contract reads + * @param tokenAddress - ERC-20 token contract address + * @param permit2Payload - The Permit2 authorization payload + * @param amountRequired - Required payment amount (as string) + * @returns VerifyResponse with the most specific failure reason + */ +export async function diagnosePermit2SimulationFailure( + config: Permit2ProxyConfig, + signer: FacilitatorEvmSigner, + tokenAddress: `0x${string}`, + permit2Payload: Permit2PayloadBase, + amountRequired: string, + simulationError?: unknown, +): Promise { + const payer = permit2Payload.permit2Authorization.from; + const mappedSimulationError = mapSimulationError(simulationError); + + if (mappedSimulationError) { + return { + isValid: false, + invalidReason: mappedSimulationError.invalidReason, + invalidMessage: mappedSimulationError.invalidMessage, + payer, + }; + } + + const diagnosticCalls: ContractCall[] = [ + { + address: config.proxyAddress, + abi: config.proxyABI, + functionName: "PERMIT2", + }, + { + address: tokenAddress, + abi: eip3009ABI, + functionName: "balanceOf", + args: [payer], + }, + { + address: tokenAddress, + abi: erc20AllowanceAbi, + functionName: "allowance", + args: [payer, PERMIT2_ADDRESS], + }, + ]; + + try { + const results = await multicall(signer.readContract.bind(signer), diagnosticCalls); + + const [proxyResult, balanceResult, allowanceResult] = results; + + if (proxyResult.status === "failure") { + return { isValid: false, invalidReason: ErrPermit2ProxyNotDeployed, payer }; + } + + if (balanceResult.status === "success") { + const balance = balanceResult.result as bigint; + if (balance < BigInt(amountRequired)) { + return { isValid: false, invalidReason: ErrPermit2InsufficientBalance, payer }; + } + } + + if (allowanceResult.status === "success") { + const allowance = allowanceResult.result as bigint; + if (allowance < BigInt(amountRequired)) { + return { isValid: false, invalidReason: ErrPermit2AllowanceRequired, payer }; + } + } + } catch { + // Diagnostic multicall itself failed — fall through to generic error + } + + return { isValid: false, invalidReason: ErrPermit2SimulationFailed, payer }; +} + +function extractSimulationErrorMessage(error: unknown): string | undefined { + if (error instanceof Error) { + return error.message; + } + if (typeof error === "string") { + return error; + } + return undefined; +} + +function mapSimulationError( + error: unknown, +): Pick | null { + const message = extractSimulationErrorMessage(error); + if (!message) { + return null; + } + + if (message.includes("Permit2612AmountMismatch")) { + return { + invalidReason: ErrPermit2612AmountMismatch, + invalidMessage: message, + }; + } + if (message.includes("InvalidAmount")) { + return { + invalidReason: ErrPermit2InvalidAmount, + invalidMessage: message, + }; + } + if (message.includes("InvalidDestination")) { + return { + invalidReason: ErrPermit2InvalidDestination, + invalidMessage: message, + }; + } + if (message.includes("InvalidOwner")) { + return { + invalidReason: ErrPermit2InvalidOwner, + invalidMessage: message, + }; + } + if (message.includes("PaymentTooEarly")) { + return { + invalidReason: ErrPermit2PaymentTooEarly, + invalidMessage: message, + }; + } + if (message.includes("InvalidSignature") || message.includes("SignatureExpired")) { + return { + invalidReason: ErrPermit2InvalidSignature, + invalidMessage: message, + }; + } + if (message.includes("InvalidNonce")) { + return { + invalidReason: ErrPermit2InvalidNonce, + invalidMessage: message, + }; + } + if (message.includes("AmountExceedsPermitted")) { + return { + invalidReason: ErrUptoAmountExceedsPermitted, + invalidMessage: message, + }; + } + if (message.includes("UnauthorizedFacilitator")) { + return { + invalidReason: ErrUptoUnauthorizedFacilitator, + invalidMessage: message, + }; + } + if (message.includes("permit")) { + return { + invalidReason: ErrPermit2SimulationFailed, + invalidMessage: message, + }; + } + + return { + invalidReason: ErrPermit2SimulationFailed, + invalidMessage: message, + }; +} + +/** + * Targeted multicall for the ERC-20 approval path where simulation cannot be used + * (the approval hasn't been broadcast yet, so settle() would fail for expected reasons). + * Checks proxy deployment, payer token balance and payer ETH balance for gas. + * + * @param config - The proxy contract configuration (address and ABI) + * @param signer - EVM signer for contract reads + * @param tokenAddress - ERC-20 token contract address + * @param payer - The payer address + * @param amountRequired - Required payment amount (as string) + * @returns VerifyResponse — valid if checks pass, otherwise the most specific failure + */ +export async function checkPermit2Prerequisites( + config: Permit2ProxyConfig, + signer: FacilitatorEvmSigner, + tokenAddress: `0x${string}`, + payer: `0x${string}`, + amountRequired: string, +): Promise { + const diagnosticCalls: ContractCall[] = [ + { + address: config.proxyAddress, + abi: config.proxyABI, + functionName: "PERMIT2", + }, + { + address: tokenAddress, + abi: eip3009ABI, + functionName: "balanceOf", + args: [payer], + }, + ]; + + try { + const results = await multicall(signer.readContract.bind(signer), diagnosticCalls); + + const [proxyResult, balanceResult] = results; + + if (proxyResult.status === "failure") { + return { isValid: false, invalidReason: ErrPermit2ProxyNotDeployed, payer }; + } + + if (balanceResult.status === "success") { + const balance = balanceResult.result as bigint; + if (balance < BigInt(amountRequired)) { + return { isValid: false, invalidReason: ErrPermit2InsufficientBalance, payer }; + } + } + } catch { + // Multicall failed — fall through to valid (fail open for prerequisites-only check) + } + + return { isValid: true, invalidReason: undefined, payer }; +} + +/** + * Builds args for exact settle(permit, owner, witness, signature). + * + * @param permit2Payload - The Permit2 payload containing authorization and signature data + * @returns Tuple of contract call arguments for the exact settle function + */ +export function buildExactPermit2SettleArgs(permit2Payload: Permit2PayloadBase) { + return [ + { + permitted: { + token: getAddress(permit2Payload.permit2Authorization.permitted.token), + amount: BigInt(permit2Payload.permit2Authorization.permitted.amount), + }, + nonce: BigInt(permit2Payload.permit2Authorization.nonce), + deadline: BigInt(permit2Payload.permit2Authorization.deadline), + }, + getAddress(permit2Payload.permit2Authorization.from), + { + to: getAddress(permit2Payload.permit2Authorization.witness.to), + validAfter: BigInt(permit2Payload.permit2Authorization.witness.validAfter), + }, + permit2Payload.signature, + ] as const; +} + +/** + * Builds args for upto settle(permit, amount, owner, witness, signature). + * The upto contract's settle() takes an additional `amount` parameter and the witness + * includes a `facilitator` field. + * + * @param permit2Payload - The upto Permit2 payload containing authorization and signature data + * @param settlementAmount - The amount to settle on-chain + * @param facilitatorAddress - The facilitator address authorized in the witness + * @returns Tuple of contract call arguments for the upto settle function + */ +export function buildUptoPermit2SettleArgs( + permit2Payload: UptoPermit2Payload, + settlementAmount: bigint, + facilitatorAddress: `0x${string}`, +) { + return [ + { + permitted: { + token: getAddress(permit2Payload.permit2Authorization.permitted.token), + amount: BigInt(permit2Payload.permit2Authorization.permitted.amount), + }, + nonce: BigInt(permit2Payload.permit2Authorization.nonce), + deadline: BigInt(permit2Payload.permit2Authorization.deadline), + }, + settlementAmount, + getAddress(permit2Payload.permit2Authorization.from), + { + to: getAddress(permit2Payload.permit2Authorization.witness.to), + facilitator: getAddress(facilitatorAddress), + validAfter: BigInt(permit2Payload.permit2Authorization.witness.validAfter), + }, + permit2Payload.signature, + ] as const; +} + +/** + * Splits a 65-byte EIP-2612 signature into v, r, s components. + * + * @param signature - The hex-encoded 65-byte signature (with or without 0x prefix) + * @returns Object with v (uint8), r (bytes32 hex), s (bytes32 hex) + * @throws Error if the signature is not exactly 65 bytes (130 hex chars) + */ +export function splitEip2612Signature(signature: string): { + v: number; + r: `0x${string}`; + s: `0x${string}`; +} { + const sig = signature.startsWith("0x") ? signature.slice(2) : signature; + + if (sig.length !== 130) { + throw new Error( + `invalid EIP-2612 signature length: expected 65 bytes (130 hex chars), got ${sig.length / 2} bytes`, + ); + } + + const r = `0x${sig.slice(0, 64)}` as `0x${string}`; + const s = `0x${sig.slice(64, 128)}` as `0x${string}`; + const v = parseInt(sig.slice(128, 130), 16); + + return { v, r, s }; +} + +// --------------------------------------------------------------------------- +// Client-side helpers +// --------------------------------------------------------------------------- + +/** + * Creates a Permit2 payload for any scheme (exact or upto). + * The only scheme-specific input is the proxy address used as the spender. + * + * @param proxyAddress - The x402 proxy contract address to set as spender + * @param signer - The EVM client signer + * @param x402Version - The x402 protocol version + * @param paymentRequirements - The payment requirements + * @returns Promise resolving to a payment payload result + */ +export async function createPermit2PayloadForProxy( + proxyAddress: `0x${string}`, + signer: ClientEvmSigner, + x402Version: number, + paymentRequirements: PaymentRequirements, +): Promise { + const now = Math.floor(Date.now() / 1000); + const nonce = createPermit2Nonce(); + + // Lower time bound - allow some clock skew + const validAfter = (now - 600).toString(); + // Upper time bound is enforced by Permit2's deadline field + const deadline = (now + paymentRequirements.maxTimeoutSeconds).toString(); + + const permit2Authorization: Permit2Authorization & { from: `0x${string}` } = { + from: signer.address, + permitted: { + token: getAddress(paymentRequirements.asset), + amount: paymentRequirements.amount, + }, + spender: proxyAddress, + nonce, + deadline, + witness: { + to: getAddress(paymentRequirements.payTo), + validAfter, + }, + }; + + const signature = await signPermit2Authorization( + signer, + permit2Authorization, + paymentRequirements, + ); + + return { + x402Version, + payload: { signature, permit2Authorization }, + }; +} + +/** + * Signs a Permit2 authorization using EIP-712 with witness data. + * The signature authorizes the proxy contract to transfer tokens on behalf of the signer. + * + * @param signer - The EVM client signer + * @param permit2Authorization - The Permit2 authorization parameters + * @param requirements - The payment requirements + * @returns Promise resolving to the hex-encoded signature + */ +async function signPermit2Authorization( + signer: ClientEvmSigner, + permit2Authorization: Permit2Authorization & { from: `0x${string}` }, + requirements: PaymentRequirements, +): Promise<`0x${string}`> { + const chainId = getEvmChainId(requirements.network); + + return await signer.signTypedData({ + domain: { name: "Permit2", chainId, verifyingContract: PERMIT2_ADDRESS }, + types: permit2WitnessTypes, + primaryType: "PermitWitnessTransferFrom", + message: { + permitted: { + token: getAddress(permit2Authorization.permitted.token), + amount: BigInt(permit2Authorization.permitted.amount), + }, + spender: getAddress(permit2Authorization.spender), + nonce: BigInt(permit2Authorization.nonce), + deadline: BigInt(permit2Authorization.deadline), + witness: { + to: getAddress(permit2Authorization.witness.to), + validAfter: BigInt(permit2Authorization.witness.validAfter), + }, + }, + }); +} diff --git a/typescript/packages/mechanisms/evm/src/shared/rpc.ts b/typescript/packages/mechanisms/evm/src/shared/rpc.ts new file mode 100644 index 0000000..1ad839e --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/shared/rpc.ts @@ -0,0 +1,123 @@ +import { createPublicClient, http } from "viem"; +import type { ClientEvmSigner } from "../signer"; +import { getEvmChainId } from "../utils"; + +export type EvmSchemeConfig = { + rpcUrl?: string; +}; + +export type EvmSchemeConfigByChainId = Record; + +export type EvmSchemeOptions = EvmSchemeConfig | EvmSchemeConfigByChainId; + +/** @deprecated Use EvmSchemeConfig */ +export type ExactEvmSchemeConfig = EvmSchemeConfig; +/** @deprecated Use EvmSchemeConfigByChainId */ +export type ExactEvmSchemeConfigByChainId = EvmSchemeConfigByChainId; +/** @deprecated Use EvmSchemeOptions */ +export type ExactEvmSchemeOptions = EvmSchemeOptions; + +type ExtensionRpcCapabilities = Pick< + ClientEvmSigner, + "readContract" | "signTransaction" | "getTransactionCount" | "estimateFeesPerGas" +>; + +const rpcClientCache = new Map>(); + +/** + * Check if options is a per-chain-id configuration map. + * + * @param options - The EVM scheme options to check + * @returns True if the options are keyed by chain ID + */ +function isConfigByChainId(options: EvmSchemeOptions): options is EvmSchemeConfigByChainId { + const keys = Object.keys(options); + return keys.length > 0 && keys.every(key => /^\d+$/.test(key)); +} + +/** + * Get or create a cached viem public client for the given RPC URL. + * + * @param rpcUrl - The JSON-RPC endpoint URL + * @returns A viem PublicClient instance + */ +function getRpcClient(rpcUrl: string): ReturnType { + const existing = rpcClientCache.get(rpcUrl); + if (existing) { + return existing; + } + + const client = createPublicClient({ + transport: http(rpcUrl), + }); + rpcClientCache.set(rpcUrl, client); + return client; +} + +/** + * Resolve an RPC URL from scheme options for the given network. + * + * @param network - The CAIP-2 network identifier + * @param options - Optional EVM scheme options (flat or per-chain-id) + * @returns The resolved RPC URL, or undefined if not configured + */ +export function resolveRpcUrl(network: string, options?: EvmSchemeOptions): string | undefined { + if (!options) { + return undefined; + } + + if (isConfigByChainId(options)) { + const chainId = getEvmChainId(network); + const optionsByChainId = options as EvmSchemeConfigByChainId; + return optionsByChainId[chainId]?.rpcUrl; + } + + return (options as EvmSchemeConfig).rpcUrl; +} + +/** + * Resolve RPC capabilities for extensions, backfilling from a public RPC client when the signer lacks them. + * + * @param network - The CAIP-2 network identifier + * @param signer - The client EVM signer + * @param options - Optional EVM scheme options for RPC URL resolution + * @returns Extension RPC capabilities (readContract, signTransaction, etc.) + */ +export function resolveExtensionRpcCapabilities( + network: string, + signer: ClientEvmSigner, + options?: EvmSchemeOptions, +): ExtensionRpcCapabilities { + const capabilities: ExtensionRpcCapabilities = { + signTransaction: signer.signTransaction, + readContract: signer.readContract, + getTransactionCount: signer.getTransactionCount, + estimateFeesPerGas: signer.estimateFeesPerGas, + }; + + const needsRpcBackfill = + !capabilities.readContract || + !capabilities.getTransactionCount || + !capabilities.estimateFeesPerGas; + if (!needsRpcBackfill) { + return capabilities; + } + + const rpcUrl = resolveRpcUrl(network, options); + if (!rpcUrl) { + return capabilities; + } + const rpcClient = getRpcClient(rpcUrl); + if (!capabilities.readContract) { + capabilities.readContract = args => rpcClient.readContract(args as never) as Promise; + } + if (!capabilities.getTransactionCount) { + capabilities.getTransactionCount = async args => + rpcClient.getTransactionCount({ address: args.address }); + } + if (!capabilities.estimateFeesPerGas) { + capabilities.estimateFeesPerGas = async () => rpcClient.estimateFeesPerGas(); + } + + return capabilities; +} diff --git a/typescript/packages/mechanisms/evm/src/signer.ts b/typescript/packages/mechanisms/evm/src/signer.ts index 926b0af..66ec3de 100644 --- a/typescript/packages/mechanisms/evm/src/signer.ts +++ b/typescript/packages/mechanisms/evm/src/signer.ts @@ -1,7 +1,16 @@ /** - * ClientEvmSigner - Used by x402 clients to sign payment authorizations - * This is typically a LocalAccount or wallet that holds private keys - * and can sign EIP-712 typed data for payment authorizations + * ClientEvmSigner - Used by x402 clients to sign payment authorizations. + * + * Typically a viem WalletClient extended with publicActions: + * ```typescript + * const client = createWalletClient({ + * account: privateKeyToAccount('0x...'), + * chain: baseSepolia, + * transport: http(), + * }).extend(publicActions); + * ``` + * + * Or composed via `toClientEvmSigner(account, publicClient)`. */ export type ClientEvmSigner = { readonly address: `0x${string}`; @@ -11,6 +20,39 @@ export type ClientEvmSigner = { primaryType: string; message: Record; }): Promise<`0x${string}`>; + /** + * Optional on-chain reads. + * Required only for extension enrichment (EIP-2612 / ERC-20 approval). + */ + readContract?(args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; + }): Promise; + /** + * Optional: Signs a raw EIP-1559 transaction without broadcasting. + * Required for ERC-20 approval gas sponsoring when the token lacks EIP-2612. + */ + signTransaction?(args: { + to: `0x${string}`; + data: `0x${string}`; + nonce: number; + gas: bigint; + maxFeePerGas: bigint; + maxPriorityFeePerGas: bigint; + chainId: number; + }): Promise<`0x${string}`>; + /** + * Optional: Gets the current transaction count (nonce) for an address. + * Required for ERC-20 approval gas sponsoring. + */ + getTransactionCount?(args: { address: `0x${string}` }): Promise; + /** + * Optional: Estimates current gas fees per gas. + * Required for ERC-20 approval gas sponsoring. + */ + estimateFeesPerGas?(): Promise<{ maxFeePerGas: bigint; maxPriorityFeePerGas: bigint }>; }; /** @@ -46,6 +88,8 @@ export type FacilitatorEvmSigner = { abi: readonly unknown[]; functionName: string; args: readonly unknown[]; + /** Optional gas limit. When provided, skips eth_estimateGas simulation. */ + gas?: bigint; }): Promise<`0x${string}`>; sendTransaction(args: { to: `0x${string}`; data: `0x${string}` }): Promise<`0x${string}`>; waitForTransactionReceipt(args: { hash: `0x${string}` }): Promise<{ status: string }>; @@ -53,13 +97,79 @@ export type FacilitatorEvmSigner = { }; /** - * Converts a signer to a ClientEvmSigner + * Composes a ClientEvmSigner from a local account and a public client. + * + * Use this when your signer (e.g., `privateKeyToAccount`) doesn't have + * `readContract`. The `publicClient` provides the on-chain read capability. * - * @param signer - The signer to convert to a ClientEvmSigner - * @returns The converted signer + * Alternatively, use a WalletClient extended with publicActions directly: + * ```typescript + * const signer = createWalletClient({ + * account: privateKeyToAccount('0x...'), + * chain: baseSepolia, + * transport: http(), + * }).extend(publicActions); + * ``` + * + * @param signer - A signer with `address` and `signTypedData` (and optionally `readContract`) + * @param publicClient - A client with optional read/nonce/fee helpers + * @param publicClient.readContract - The readContract method from the public client + * @param publicClient.getTransactionCount - Optional getTransactionCount for ERC-20 approval + * @param publicClient.estimateFeesPerGas - Optional estimateFeesPerGas for ERC-20 approval + * @returns A ClientEvmSigner with any available optional capabilities + * + * @example + * ```typescript + * const account = privateKeyToAccount("0x..."); + * const publicClient = createPublicClient({ chain: baseSepolia, transport: http() }); + * const signer = toClientEvmSigner(account, publicClient); + * ``` */ -export function toClientEvmSigner(signer: ClientEvmSigner): ClientEvmSigner { - return signer; +export function toClientEvmSigner( + signer: Omit & { + readContract?: ClientEvmSigner["readContract"]; + }, + publicClient?: { + readContract(args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; + }): Promise; + getTransactionCount?(args: { address: `0x${string}` }): Promise; + estimateFeesPerGas?(): Promise<{ maxFeePerGas: bigint; maxPriorityFeePerGas: bigint }>; + }, +): ClientEvmSigner { + const readContract = signer.readContract ?? publicClient?.readContract.bind(publicClient); + + const result: ClientEvmSigner = { + address: signer.address, + signTypedData: msg => signer.signTypedData(msg), + }; + + if (readContract) { + result.readContract = readContract; + } + + // Forward optional capabilities from signer or publicClient + const signTransaction = signer.signTransaction; + if (signTransaction) { + result.signTransaction = args => signTransaction(args); + } + + const getTransactionCount = + signer.getTransactionCount ?? publicClient?.getTransactionCount?.bind(publicClient); + if (getTransactionCount) { + result.getTransactionCount = args => getTransactionCount(args); + } + + const estimateFeesPerGas = + signer.estimateFeesPerGas ?? publicClient?.estimateFeesPerGas?.bind(publicClient); + if (estimateFeesPerGas) { + result.estimateFeesPerGas = () => estimateFeesPerGas(); + } + + return result; } /** diff --git a/typescript/packages/mechanisms/evm/src/types.ts b/typescript/packages/mechanisms/evm/src/types.ts index a668627..0463327 100644 --- a/typescript/packages/mechanisms/evm/src/types.ts +++ b/typescript/packages/mechanisms/evm/src/types.ts @@ -28,7 +28,6 @@ export type ExactEIP3009Payload = { export type Permit2Witness = { to: `0x${string}`; validAfter: string; - extra: `0x${string}`; }; /** @@ -81,3 +80,70 @@ export function isPermit2Payload(payload: ExactEvmPayloadV2): payload is ExactPe export function isEIP3009Payload(payload: ExactEvmPayloadV2): payload is ExactEIP3009Payload { return "authorization" in payload; } + +/** + * Upto Permit2 witness — includes `facilitator` field absent from exact witness. + * Only the address matching `witness.facilitator` can call settle() on-chain. + */ +export type UptoPermit2Witness = { + to: `0x${string}`; + facilitator: `0x${string}`; + validAfter: string; +}; + +export type UptoPermit2Authorization = { + permitted: { + token: `0x${string}`; + amount: string; + }; + spender: `0x${string}`; + nonce: string; + deadline: string; + witness: UptoPermit2Witness; +}; + +export type UptoPermit2Payload = { + signature: `0x${string}`; + permit2Authorization: UptoPermit2Authorization & { + from: `0x${string}`; + }; +}; + +/** + * Type guard to check if a payload is an upto Permit2 payload. + * Validates structural presence of all required fields: signature, permit2Authorization + * (with from, permitted, spender, nonce, deadline), and a witness containing facilitator. + * + * @param payload - The payload to check + * @returns True if the payload is an upto Permit2 payload, false otherwise + */ +export function isUptoPermit2Payload( + payload: Record, +): payload is UptoPermit2Payload { + if (typeof payload.signature !== "string") return false; + if (!("permit2Authorization" in payload)) return false; + + const auth = payload.permit2Authorization; + if (typeof auth !== "object" || auth === null) return false; + + const a = auth as Record; + if (typeof a.from !== "string") return false; + if (typeof a.spender !== "string") return false; + if (typeof a.nonce !== "string") return false; + if (typeof a.deadline !== "string") return false; + + const permitted = a.permitted; + if (typeof permitted !== "object" || permitted === null) return false; + const p = permitted as Record; + if (typeof p.token !== "string") return false; + if (typeof p.amount !== "string") return false; + + const witness = a.witness; + if (typeof witness !== "object" || witness === null) return false; + const w = witness as Record; + return ( + typeof w.facilitator === "string" && + typeof w.to === "string" && + typeof w.validAfter === "string" + ); +} diff --git a/typescript/packages/mechanisms/evm/src/upto/client/index.ts b/typescript/packages/mechanisms/evm/src/upto/client/index.ts new file mode 100644 index 0000000..e378396 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/client/index.ts @@ -0,0 +1,16 @@ +// Note: The upto scheme does not provide register.ts convenience helpers (unlike exact). +// The exact scheme's register helpers exist primarily for V1 backward compatibility, +// which is not needed for upto. Use direct class instantiation instead: +// client.register("eip155:*", new UptoEvmScheme(signer, options)) +export { UptoEvmScheme } from "./scheme"; +export type { + UptoEvmSchemeConfig, + UptoEvmSchemeConfigByChainId, + UptoEvmSchemeOptions, +} from "./rpc"; +export { + createPermit2ApprovalTx, + getPermit2AllowanceReadParams, + type Permit2AllowanceParams, +} from "./permit2"; +export { erc20AllowanceAbi } from "../../constants"; diff --git a/typescript/packages/mechanisms/evm/src/upto/client/permit2.ts b/typescript/packages/mechanisms/evm/src/upto/client/permit2.ts new file mode 100644 index 0000000..41d2817 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/client/permit2.ts @@ -0,0 +1,96 @@ +import { PaymentRequirements, PaymentPayloadResult } from "@x402/core/types"; +import { + PERMIT2_ADDRESS, + uptoPermit2WitnessTypes, + x402UptoPermit2ProxyAddress, +} from "../../constants"; +import { ClientEvmSigner } from "../../signer"; +import { UptoPermit2Authorization } from "../../types"; +import { createPermit2Nonce, getEvmChainId } from "../../utils"; +import { getAddress } from "viem"; + +// Re-export Permit2-generic approval helpers +export { createPermit2ApprovalTx, getPermit2AllowanceReadParams } from "../../exact/client/permit2"; +export type { Permit2AllowanceParams } from "../../exact/client/permit2"; + +/** + * Creates a signed upto Permit2 payment payload for the given requirements. + * + * Constructs a Permit2 authorization with an upto witness (including facilitator address) + * and signs it using EIP-712 typed data. + * + * @param signer - The EVM client signer for signing typed data + * @param x402Version - The x402 protocol version + * @param paymentRequirements - The payment requirements including asset, amount, and payTo + * @returns Promise resolving to a payment payload result with the signed authorization + */ +export async function createUptoPermit2Payload( + signer: ClientEvmSigner, + x402Version: number, + paymentRequirements: PaymentRequirements, +): Promise { + const facilitatorAddress = paymentRequirements.extra?.facilitatorAddress as + | `0x${string}` + | undefined; + if (!facilitatorAddress) { + throw new Error( + "upto scheme requires facilitatorAddress in paymentRequirements.extra. " + + "Ensure the server is configured with an upto facilitator that provides getExtra().", + ); + } + + const now = Math.floor(Date.now() / 1000); + const nonce = createPermit2Nonce(); + const validAfter = (now - 600).toString(); + const deadline = (now + paymentRequirements.maxTimeoutSeconds).toString(); + + if (BigInt(deadline) <= BigInt(validAfter)) { + throw new Error( + `Invalid time window: deadline (${deadline}) must be after validAfter (${validAfter}). ` + + `Check that maxTimeoutSeconds (${paymentRequirements.maxTimeoutSeconds}) is positive.`, + ); + } + + const permit2Authorization: UptoPermit2Authorization & { from: `0x${string}` } = { + from: signer.address, + permitted: { + token: getAddress(paymentRequirements.asset), + amount: paymentRequirements.amount, + }, + spender: x402UptoPermit2ProxyAddress, + nonce, + deadline, + witness: { + to: getAddress(paymentRequirements.payTo), + facilitator: getAddress(facilitatorAddress), + validAfter, + }, + }; + + const chainId = getEvmChainId(paymentRequirements.network); + + const signature = await signer.signTypedData({ + domain: { name: "Permit2", chainId, verifyingContract: PERMIT2_ADDRESS }, + types: uptoPermit2WitnessTypes, + primaryType: "PermitWitnessTransferFrom", + message: { + permitted: { + token: getAddress(permit2Authorization.permitted.token), + amount: BigInt(permit2Authorization.permitted.amount), + }, + spender: getAddress(permit2Authorization.spender), + nonce: BigInt(permit2Authorization.nonce), + deadline: BigInt(permit2Authorization.deadline), + witness: { + to: getAddress(permit2Authorization.witness.to), + facilitator: getAddress(permit2Authorization.witness.facilitator), + validAfter: BigInt(permit2Authorization.witness.validAfter), + }, + }, + }); + + return { + x402Version, + payload: { signature, permit2Authorization }, + }; +} diff --git a/typescript/packages/mechanisms/evm/src/upto/client/rpc.ts b/typescript/packages/mechanisms/evm/src/upto/client/rpc.ts new file mode 100644 index 0000000..1bba7da --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/client/rpc.ts @@ -0,0 +1,4 @@ +export type { EvmSchemeConfig as UptoEvmSchemeConfig } from "../../shared/rpc"; +export type { EvmSchemeConfigByChainId as UptoEvmSchemeConfigByChainId } from "../../shared/rpc"; +export type { EvmSchemeOptions as UptoEvmSchemeOptions } from "../../shared/rpc"; +export { resolveExtensionRpcCapabilities } from "../../shared/rpc"; diff --git a/typescript/packages/mechanisms/evm/src/upto/client/scheme.ts b/typescript/packages/mechanisms/evm/src/upto/client/scheme.ts new file mode 100644 index 0000000..d32e3c6 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/client/scheme.ts @@ -0,0 +1,71 @@ +import { + SchemeNetworkClient, + PaymentRequirements, + PaymentPayloadResult, + PaymentPayloadContext, +} from "@x402/core/types"; +import { ClientEvmSigner } from "../../signer"; +import { createUptoPermit2Payload } from "./permit2"; +import { + trySignEip2612PermitExtension, + trySignErc20ApprovalExtension, +} from "../../shared/extensions"; +import { UptoEvmSchemeOptions } from "./rpc"; + +/** + * EVM client implementation for the Upto payment scheme. + * Handles Permit2-based payment payload creation and gas-sponsoring extensions. + */ +export class UptoEvmScheme implements SchemeNetworkClient { + readonly scheme = "upto"; + + /** + * Creates a new UptoEvmScheme instance. + * + * @param signer - The EVM signer for client operations + * @param options - Optional RPC configuration + */ + constructor( + private readonly signer: ClientEvmSigner, + private readonly options?: UptoEvmSchemeOptions, + ) {} + + /** + * Creates a payment payload for the Upto scheme using Permit2. + * + * @param x402Version - The x402 protocol version + * @param paymentRequirements - The payment requirements + * @param context - Optional context with server-declared extensions + * @returns Promise resolving to a payment payload result + */ + async createPaymentPayload( + x402Version: number, + paymentRequirements: PaymentRequirements, + context?: PaymentPayloadContext, + ): Promise { + const result = await createUptoPermit2Payload(this.signer, x402Version, paymentRequirements); + + const eip2612Extensions = await trySignEip2612PermitExtension( + this.signer, + this.options, + paymentRequirements, + result, + context, + ); + if (eip2612Extensions) { + return { ...result, extensions: eip2612Extensions }; + } + + const erc20Extensions = await trySignErc20ApprovalExtension( + this.signer, + this.options, + paymentRequirements, + context, + ); + if (erc20Extensions) { + return { ...result, extensions: erc20Extensions }; + } + + return result; + } +} diff --git a/typescript/packages/mechanisms/evm/src/upto/extensions.ts b/typescript/packages/mechanisms/evm/src/upto/extensions.ts new file mode 100644 index 0000000..f02acd7 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/extensions.ts @@ -0,0 +1 @@ +export { EIP2612_GAS_SPONSORING_KEY, ERC20_APPROVAL_GAS_SPONSORING_KEY } from "../exact/extensions"; diff --git a/typescript/packages/mechanisms/evm/src/upto/facilitator/errors.ts b/typescript/packages/mechanisms/evm/src/upto/facilitator/errors.ts new file mode 100644 index 0000000..08034c0 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/facilitator/errors.ts @@ -0,0 +1,46 @@ +/** + * Named error reason constants for the upto EVM facilitator. + * + * Shared Permit2 errors are re-exported from exact/facilitator/errors.ts. + * Upto-specific errors are defined here. + * + * These strings must be character-for-character identical to the Go constants + * to maintain cross-SDK parity. + */ + +// Re-export shared Permit2 errors +export { + ErrPermit2InvalidSpender, + ErrPermit2RecipientMismatch, + ErrPermit2DeadlineExpired, + ErrPermit2NotYetValid, + ErrPermit2AmountMismatch, + ErrPermit2TokenMismatch, + ErrPermit2InvalidSignature, + ErrPermit2AllowanceRequired, + ErrPermit2InvalidAmount, + ErrPermit2InvalidDestination, + ErrPermit2InvalidOwner, + ErrPermit2PaymentTooEarly, + ErrPermit2InvalidNonce, + ErrPermit2612AmountMismatch, + ErrErc20ApprovalInvalidFormat, + ErrErc20ApprovalFromMismatch, + ErrErc20ApprovalAssetMismatch, + ErrErc20ApprovalSpenderNotPermit2, + ErrErc20ApprovalTxWrongTarget, + ErrErc20ApprovalTxWrongSelector, + ErrErc20ApprovalTxWrongSpender, + ErrErc20ApprovalTxInvalidCalldata, + ErrErc20ApprovalTxSignerMismatch, + ErrErc20ApprovalTxInvalidSignature, + ErrErc20ApprovalTxParseFailed, +} from "../../exact/facilitator/errors"; + +// Upto-specific errors +export const ErrUptoInvalidScheme = "invalid_upto_evm_scheme"; +export const ErrUptoNetworkMismatch = "invalid_upto_evm_network_mismatch"; +export const ErrUptoSettlementExceedsAmount = "invalid_upto_evm_payload_settlement_exceeds_amount"; +export const ErrUptoAmountExceedsPermitted = "upto_amount_exceeds_permitted"; +export const ErrUptoUnauthorizedFacilitator = "upto_unauthorized_facilitator"; +export const ErrUptoFacilitatorMismatch = "upto_facilitator_mismatch"; diff --git a/typescript/packages/mechanisms/evm/src/upto/facilitator/index.ts b/typescript/packages/mechanisms/evm/src/upto/facilitator/index.ts index f4e4ee7..c24e4af 100644 --- a/typescript/packages/mechanisms/evm/src/upto/facilitator/index.ts +++ b/typescript/packages/mechanisms/evm/src/upto/facilitator/index.ts @@ -1,2 +1,3 @@ +// Note: No register.ts helper — V1 backward compatibility is not needed for upto. +// Use direct class instantiation: facilitator.register("eip155:*", new UptoEvmScheme(signer)) export { UptoEvmScheme } from "./scheme"; -export type { UptoEvmSchemeConfig } from "./scheme"; diff --git a/typescript/packages/mechanisms/evm/src/upto/facilitator/permit2.ts b/typescript/packages/mechanisms/evm/src/upto/facilitator/permit2.ts index d372e53..05309a8 100644 --- a/typescript/packages/mechanisms/evm/src/upto/facilitator/permit2.ts +++ b/typescript/packages/mechanisms/evm/src/upto/facilitator/permit2.ts @@ -1,142 +1,180 @@ import { PaymentPayload, PaymentRequirements, + FacilitatorContext, SettleResponse, VerifyResponse, } from "@x402/core/types"; -import { getAddress } from "viem"; import { - eip3009ABI, + extractEip2612GasSponsoringInfo, + extractErc20ApprovalGasSponsoringInfo, + ERC20_APPROVAL_GAS_SPONSORING_KEY, + resolveErc20ApprovalExtensionSigner, + type Erc20ApprovalGasSponsoringFacilitatorExtension, + type Erc20ApprovalGasSponsoringSigner, +} from "../../exact/extensions"; +import { getAddress, encodeFunctionData } from "viem"; +import { PERMIT2_ADDRESS, - permit2WitnessTypes, + uptoPermit2WitnessTypes, x402UptoPermit2ProxyABI, x402UptoPermit2ProxyAddress, } from "../../constants"; +import { + ErrPermit2AmountMismatch, + ErrUptoSettlementExceedsAmount, + ErrUptoFacilitatorMismatch, + ErrUptoInvalidScheme, + ErrUptoNetworkMismatch, +} from "./errors"; import { FacilitatorEvmSigner } from "../../signer"; -import { ExactPermit2Payload } from "../../types"; - -// ERC20 allowance ABI for checking Permit2 approval -const erc20AllowanceABI = [ - { - type: "function", - name: "allowance", - inputs: [ - { name: "owner", type: "address" }, - { name: "spender", type: "address" }, - ], - outputs: [{ type: "uint256" }], - stateMutability: "view", - }, -] as const; +import { UptoPermit2Payload } from "../../types"; +import { getEvmChainId } from "../../utils"; +import { validateErc20ApprovalForPayment } from "../../shared/erc20approval"; +import { + buildUptoPermit2SettleArgs, + waitAndReturnSettleResponse, + mapSettleError, + splitEip2612Signature, + simulatePermit2Settle, + simulatePermit2SettleWithPermit, + simulatePermit2SettleWithErc20Approval, + diagnosePermit2SimulationFailure, + checkPermit2Prerequisites, + validateEip2612PermitForPayment, + type Permit2ProxyConfig, +} from "../../shared/permit2"; +import type { Eip2612GasSponsoringInfo } from "../../exact/extensions"; + +const uptoProxyConfig: Permit2ProxyConfig = { + proxyAddress: x402UptoPermit2ProxyAddress, + proxyABI: x402UptoPermit2ProxyABI, +}; + +export interface VerifyUptoPermit2Options { + simulate?: boolean; +} + +export interface UptoPermit2FacilitatorConfig { + simulateInSettle?: boolean; +} /** - * Verifies a Permit2 payment payload for the upto scheme. + * Verifies an upto Permit2 payment payload against the given requirements. * - * The upto scheme allows a single signature to cover multiple requests up to a spend cap. - * Verification checks that: - * - The spender is the x402UptoPermit2Proxy - * - The permitted amount (spend cap) is >= requirements amount - * - The signature is valid + * Validates scheme, network, spender, recipient, facilitator, deadline, amount, + * token, signature, Permit2 allowance, and payer balance. * - * @param signer - The facilitator signer for contract reads + * @param signer - The facilitator signer for contract reads and signature verification * @param payload - The payment payload to verify - * @param requirements - The payment requirements - * @param permit2Payload - The Permit2 specific payload - * @returns Promise resolving to verification response + * @param requirements - The payment requirements to verify against + * @param permit2Payload - The upto Permit2 specific payload with witness data + * @param context - Optional facilitator context for extension-provided capabilities + * @param options - Optional verification options (e.g., skip simulation) + * @returns Promise resolving to a verification response indicating validity */ export async function verifyUptoPermit2( signer: FacilitatorEvmSigner, payload: PaymentPayload, requirements: PaymentRequirements, - permit2Payload: ExactPermit2Payload, + permit2Payload: UptoPermit2Payload, + context?: FacilitatorContext, + options?: VerifyUptoPermit2Options, ): Promise { const payer = permit2Payload.permit2Authorization.from; - // Verify scheme matches if (payload.accepted.scheme !== "upto" || requirements.scheme !== "upto") { return { isValid: false, - invalidReason: "unsupported_scheme", + invalidReason: ErrUptoInvalidScheme, payer, }; } - // Verify network matches if (payload.accepted.network !== requirements.network) { return { isValid: false, - invalidReason: "network_mismatch", + invalidReason: ErrUptoNetworkMismatch, payer, }; } - const chainId = parseInt(requirements.network.split(":")[1]); + const chainId = getEvmChainId(requirements.network); const tokenAddress = getAddress(requirements.asset); - console.log("permit payload", permit2Payload); - // Verify spender is the x402UptoPermit2Proxy if ( getAddress(permit2Payload.permit2Authorization.spender) !== getAddress(x402UptoPermit2ProxyAddress) ) { return { isValid: false, - invalidReason: "invalid_upto_permit2_spender", + invalidReason: "invalid_permit2_spender", payer, }; } - // Verify witness.to matches payTo if ( getAddress(permit2Payload.permit2Authorization.witness.to) !== getAddress(requirements.payTo) ) { return { isValid: false, - invalidReason: "invalid_upto_recipient_mismatch", + invalidReason: "invalid_permit2_recipient_mismatch", + payer, + }; + } + + // Verify the facilitator address in the witness matches our own address + const facilitatorAddresses = signer.getAddresses(); + const witnessFacilitator = getAddress(permit2Payload.permit2Authorization.witness.facilitator); + const isFacilitatorMatch = facilitatorAddresses.some( + addr => getAddress(addr) === witnessFacilitator, + ); + if (!isFacilitatorMatch) { + return { + isValid: false, + invalidReason: ErrUptoFacilitatorMismatch, payer, }; } - // Verify deadline not expired (with 6 second buffer for block time) const now = Math.floor(Date.now() / 1000); if (BigInt(permit2Payload.permit2Authorization.deadline) < BigInt(now + 6)) { return { isValid: false, - invalidReason: "upto_permit2_deadline_expired", + invalidReason: "permit2_deadline_expired", payer, }; } - // Verify validAfter is not in the future if (BigInt(permit2Payload.permit2Authorization.witness.validAfter) > BigInt(now)) { return { isValid: false, - invalidReason: "upto_permit2_not_yet_valid", + invalidReason: "permit2_not_yet_valid", payer, }; } - // For upto: the permitted amount is the spend cap, must be >= requirements amount - if (BigInt(permit2Payload.permit2Authorization.permitted.amount) < BigInt(requirements.amount)) { + if ( + BigInt(permit2Payload.permit2Authorization.permitted.amount) !== BigInt(requirements.amount) + ) { return { isValid: false, - invalidReason: "upto_amount_exceeds_permitted", + invalidReason: ErrPermit2AmountMismatch, payer, }; } - // Verify token matches if (getAddress(permit2Payload.permit2Authorization.permitted.token) !== tokenAddress) { return { isValid: false, - invalidReason: "upto_permit2_token_mismatch", + invalidReason: "permit2_token_mismatch", payer, }; } - // Build typed data for Permit2 signature verification + // Verify signature using upto-specific witness types (includes facilitator) const permit2TypedData = { - types: permit2WitnessTypes, + types: uptoPermit2WitnessTypes, primaryType: "PermitWitnessTransferFrom" as const, domain: { name: "Permit2", @@ -153,74 +191,141 @@ export async function verifyUptoPermit2( deadline: BigInt(permit2Payload.permit2Authorization.deadline), witness: { to: getAddress(permit2Payload.permit2Authorization.witness.to), + facilitator: getAddress(permit2Payload.permit2Authorization.witness.facilitator), validAfter: BigInt(permit2Payload.permit2Authorization.witness.validAfter), - extra: permit2Payload.permit2Authorization.witness.extra, }, }, }; // Verify signature + // Note: verifyTypedData is implementation-dependent and pluggable on FacilitatorEvmSigner + // Some implementations only do EOA-style ECDSA recovery (e.g. viem/utils verifyTypedData, ethers.verifyTypedData) + // Viem's publicClient.verifyTypedData supports EOA and Smart Contract Account (ERC-1271 / ERC-6492) signature verification + let signatureValid = false; try { - const isValid = await signer.verifyTypedData({ + signatureValid = await signer.verifyTypedData({ address: payer, ...permit2TypedData, signature: permit2Payload.signature, }); + } catch { + signatureValid = false; + } + + if (!signatureValid) { + // Check if the payer is a deployed smart contract (ERC-1271 / ERC-6492) + const bytecode = await signer.getCode({ address: payer }); + const isDeployedContract = bytecode && bytecode !== "0x"; - if (!isValid) { + if (!isDeployedContract) { return { isValid: false, - invalidReason: "invalid_upto_permit2_signature", + invalidReason: "invalid_permit2_signature", payer, }; } - } catch { - return { - isValid: false, - invalidReason: "invalid_upto_permit2_signature", - payer, - }; + // Deployed smart contract: fall through to simulation } - // Check Permit2 allowance - try { - const allowance = (await signer.readContract({ - address: tokenAddress, - abi: erc20AllowanceABI, - functionName: "allowance", - args: [payer, PERMIT2_ADDRESS], - })) as bigint; - - if (allowance < BigInt(requirements.amount)) { - return { - isValid: false, - invalidReason: "upto_permit2_allowance_required", - payer, - }; + // If simulation is disabled, return early + if (options?.simulate === false) { + return { isValid: true, invalidReason: undefined, payer }; + } + + const facilitatorAddress = getAddress(permit2Payload.permit2Authorization.witness.facilitator); + // Per spec §Phase 3 Step 7: simulate with requirements.amount (the worst-case charge). + // At verify time, requirements.amount = max authorized amount. + // At settle time, requirements.amount = actual settlement amount (≤ max). + const uptoSettleArgs = buildUptoPermit2SettleArgs( + permit2Payload, + BigInt(requirements.amount), + facilitatorAddress, + ); + + const eip2612InfoForSim = extractEip2612GasSponsoringInfo(payload); + if (eip2612InfoForSim) { + const fieldResult = validateEip2612PermitForPayment(eip2612InfoForSim, payer, tokenAddress); + if (!fieldResult.isValid) { + return { isValid: false, invalidReason: fieldResult.invalidReason!, payer }; } - } catch { - // If we can't check allowance, continue - settlement will fail if insufficient + + const simulation = await simulatePermit2SettleWithPermit( + uptoProxyConfig, + signer, + uptoSettleArgs, + eip2612InfoForSim, + ); + if (!simulation.ok) { + return diagnosePermit2SimulationFailure( + uptoProxyConfig, + signer, + tokenAddress, + permit2Payload, + requirements.amount, + simulation.error, + ); + } + + return { isValid: true, invalidReason: undefined, payer }; } - // Check balance - try { - const balance = (await signer.readContract({ - address: tokenAddress, - abi: eip3009ABI, - functionName: "balanceOf", - args: [payer], - })) as bigint; - - if (balance < BigInt(requirements.amount)) { - return { - isValid: false, - invalidReason: "insufficient_funds", - invalidMessage: `Insufficient funds to complete the payment. Required: ${requirements.amount} ${requirements.asset}, Available: ${balance.toString()} ${requirements.asset}. Please add funds to your wallet and try again.`, + const erc20GasSponsorshipExtension = + context?.getExtension( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + ); + if (erc20GasSponsorshipExtension) { + const erc20Info = extractErc20ApprovalGasSponsoringInfo(payload); + if (erc20Info) { + const fieldResult = await validateErc20ApprovalForPayment(erc20Info, payer, tokenAddress); + if (!fieldResult.isValid) { + return { isValid: false, invalidReason: fieldResult.invalidReason!, payer }; + } + + const extensionSigner = resolveErc20ApprovalExtensionSigner( + erc20GasSponsorshipExtension, + requirements.network, + ); + + if (extensionSigner?.simulateTransactions) { + const simulation = await simulatePermit2SettleWithErc20Approval( + uptoProxyConfig, + extensionSigner, + uptoSettleArgs, + erc20Info, + ); + if (!simulation.ok) { + return diagnosePermit2SimulationFailure( + uptoProxyConfig, + signer, + tokenAddress, + permit2Payload, + requirements.amount, + simulation.error, + ); + } + return { isValid: true, invalidReason: undefined, payer }; + } + + return checkPermit2Prerequisites( + uptoProxyConfig, + signer, + tokenAddress, payer, - }; + requirements.amount, + ); } - } catch { - // If we can't check balance, continue with other validations + } + + const simulation = await simulatePermit2Settle(uptoProxyConfig, signer, uptoSettleArgs); + if (!simulation.ok) { + return diagnosePermit2SimulationFailure( + uptoProxyConfig, + signer, + tokenAddress, + permit2Payload, + requirements.amount, + simulation.error, + ); } return { @@ -231,111 +336,249 @@ export async function verifyUptoPermit2( } /** - * Settles an upto Permit2 payment by calling the x402UptoPermit2Proxy. + * Settles an upto Permit2 payment on-chain. * - * Key difference from exact: settles the ACTUAL amount (from requirements.amount) - * rather than the full permitted amount (spend cap). + * Verifies the payment first, then selects the appropriate settlement path: + * EIP-2612 atomic permit, ERC-20 approval extension, or direct settlement. * * @param signer - The facilitator signer for contract writes * @param payload - The payment payload to settle * @param requirements - The payment requirements - * @param permit2Payload - The Permit2 specific payload - * @returns Promise resolving to settlement response + * @param permit2Payload - The upto Permit2 specific payload with witness data + * @param context - Optional facilitator context for extension-provided capabilities + * @param config - Optional facilitator configuration (e.g., simulation settings for settle) + * @returns Promise resolving to a settlement response indicating success or failure */ export async function settleUptoPermit2( signer: FacilitatorEvmSigner, payload: PaymentPayload, requirements: PaymentRequirements, - permit2Payload: ExactPermit2Payload, + permit2Payload: UptoPermit2Payload, + context?: FacilitatorContext, + config?: UptoPermit2FacilitatorConfig, ): Promise { const payer = permit2Payload.permit2Authorization.from; + const settlementAmount = BigInt(requirements.amount); + + // Re-verify the signature before settling. We override `requirements.amount` + // with the *authorized maximum* (`permitted.amount`) — NOT the actual + // settlement amount — because `verifyUptoPermit2` performs strict equality + // (`permitted.amount === requirements.amount`) to confirm the payload matches + // what the client signed. The actual settlement amount, which may be lower + // than the authorized maximum, is validated separately in the guard below + // (`settlementAmount > permitted.amount`). + const verifyRequirements: PaymentRequirements = { + ...requirements, + amount: permit2Payload.permit2Authorization.permitted.amount, + }; - // Re-verify before settling - const valid = await verifyUptoPermit2(signer, payload, requirements, permit2Payload); + const valid = await verifyUptoPermit2( + signer, + payload, + verifyRequirements, + permit2Payload, + context, + { simulate: config?.simulateInSettle ?? true }, + ); if (!valid.isValid) { return { success: false, network: payload.accepted.network, transaction: "", errorReason: valid.invalidReason ?? "invalid_scheme", + errorMessage: valid.invalidMessage, payer, }; } + // Zero settlement — no on-chain tx needed + if (settlementAmount === 0n) { + return { + success: true, + transaction: "", + network: payload.accepted.network, + payer, + amount: "0", + }; + } + + if (settlementAmount > BigInt(permit2Payload.permit2Authorization.permitted.amount)) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: ErrUptoSettlementExceedsAmount, + payer, + }; + } + + const facilitatorAddress = getAddress(permit2Payload.permit2Authorization.witness.facilitator); + + // Branch: EIP-2612 gas sponsoring (atomic settleWithPermit via contract) + const eip2612Info = extractEip2612GasSponsoringInfo(payload); + if (eip2612Info) { + return settleUptoWithEIP2612( + signer, + payload, + permit2Payload, + eip2612Info, + settlementAmount, + facilitatorAddress, + ); + } + + // Branch: ERC-20 approval gas sponsoring (broadcast approval + settle via extension signer) + const erc20Info = extractErc20ApprovalGasSponsoringInfo(payload); + if (erc20Info) { + const erc20GasSponsorshipExtension = + context?.getExtension( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + ); + const extensionSigner = resolveErc20ApprovalExtensionSigner( + erc20GasSponsorshipExtension, + payload.accepted.network, + ); + if (extensionSigner) { + return settleUptoWithERC20Approval( + extensionSigner, + payload, + permit2Payload, + erc20Info, + settlementAmount, + facilitatorAddress, + ); + } + } + + // Branch: standard settle (allowance already on-chain) + return settleUptoDirect(signer, payload, permit2Payload, settlementAmount, facilitatorAddress); +} + +/** + * Settles an upto Permit2 payment via settleWithPermit, including the EIP-2612 permit atomically. + * + * @param signer - The facilitator signer for contract writes + * @param payload - The payment payload for network info + * @param permit2Payload - The upto Permit2 specific payload with authorization and signature + * @param eip2612Info - The EIP-2612 gas sponsoring info from the payload extension + * @param settlementAmount - The amount to settle on-chain + * @param facilitatorAddress - The facilitator address authorized in the witness + * @returns Promise resolving to a settlement response + */ +async function settleUptoWithEIP2612( + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + permit2Payload: UptoPermit2Payload, + eip2612Info: Eip2612GasSponsoringInfo, + settlementAmount: bigint, + facilitatorAddress: `0x${string}`, +): Promise { + const payer = permit2Payload.permit2Authorization.from; try { - // Call x402UptoPermit2Proxy.settle() - // The settle function uses the same ABI as exact but targets the upto proxy + const { v, r, s } = splitEip2612Signature(eip2612Info.signature); + const tx = await signer.writeContract({ - address: x402UptoPermit2ProxyAddress, - abi: x402UptoPermit2ProxyABI, - functionName: "settle", + address: uptoProxyConfig.proxyAddress, + abi: uptoProxyConfig.proxyABI, + functionName: "settleWithPermit", args: [ { - permitted: { - token: getAddress(permit2Payload.permit2Authorization.permitted.token), - amount: BigInt(permit2Payload.permit2Authorization.permitted.amount), - }, - nonce: BigInt(permit2Payload.permit2Authorization.nonce), - deadline: BigInt(permit2Payload.permit2Authorization.deadline), + value: BigInt(eip2612Info.amount), + deadline: BigInt(eip2612Info.deadline), + r, + s, + v, }, - getAddress(payer), - { - to: getAddress(permit2Payload.permit2Authorization.witness.to), - validAfter: BigInt(permit2Payload.permit2Authorization.witness.validAfter), - extra: permit2Payload.permit2Authorization.witness.extra as `0x${string}`, - }, - permit2Payload.signature, - BigInt(requirements.amount), + ...buildUptoPermit2SettleArgs(permit2Payload, settlementAmount, facilitatorAddress), ], }); - // Wait for transaction confirmation - const receipt = await signer.waitForTransactionReceipt({ hash: tx }); - console.log("receipt ", receipt); + const response = await waitAndReturnSettleResponse(signer, tx, payload, payer); + return { ...response, amount: settlementAmount.toString() }; + } catch (error) { + return mapSettleError(error, payload, payer); + } +} - if (receipt.status !== "success") { - return { - success: false, - errorReason: "invalid_transaction_state", - transaction: tx, - network: payload.accepted.network, - payer, - }; - } +/** + * Settles an upto Permit2 payment using an ERC-20 approval gas sponsoring extension. + * + * Broadcasts the pre-signed approval transaction followed by the settle transaction + * via the extension signer. + * + * @param extensionSigner - The extension signer with sendTransactions capability + * @param payload - The payment payload for network info + * @param permit2Payload - The upto Permit2 specific payload with authorization and signature + * @param erc20Info - Object containing the signed approval transaction + * @param erc20Info.signedTransaction - The RLP-encoded signed ERC-20 approve transaction hex string + * @param settlementAmount - The amount to settle on-chain + * @param facilitatorAddress - The facilitator address authorized in the witness + * @returns Promise resolving to a settlement response + */ +async function settleUptoWithERC20Approval( + extensionSigner: Erc20ApprovalGasSponsoringSigner, + payload: PaymentPayload, + permit2Payload: UptoPermit2Payload, + erc20Info: { signedTransaction: string }, + settlementAmount: bigint, + facilitatorAddress: `0x${string}`, +): Promise { + const payer = permit2Payload.permit2Authorization.from; - return { - success: true, - transaction: tx, - network: payload.accepted.network, + try { + const settleData = encodeFunctionData({ + abi: uptoProxyConfig.proxyABI, + functionName: "settle", + args: buildUptoPermit2SettleArgs(permit2Payload, settlementAmount, facilitatorAddress), + }); + + const txHashes = await extensionSigner.sendTransactions([ + erc20Info.signedTransaction as `0x${string}`, + { to: uptoProxyConfig.proxyAddress, data: settleData, gas: BigInt(300_000) }, + ]); + + const settleTxHash = txHashes[txHashes.length - 1]; + const response = await waitAndReturnSettleResponse( + extensionSigner, + settleTxHash, + payload, payer, - }; + ); + return { ...response, amount: settlementAmount.toString() }; } catch (error) { - // Extract meaningful error message from the contract revert - let errorReason = "transaction_failed"; - if (error instanceof Error) { - const message = error.message; - if (message.includes("AmountExceedsPermitted")) { - errorReason = "upto_amount_exceeds_permitted"; - } else if (message.includes("InvalidDestination")) { - errorReason = "upto_invalid_destination"; - } else if (message.includes("InvalidOwner")) { - errorReason = "upto_invalid_owner"; - } else if (message.includes("PaymentTooEarly")) { - errorReason = "upto_payment_too_early"; - } else if (message.includes("InvalidSignature") || message.includes("SignatureExpired")) { - errorReason = "upto_invalid_signature"; - } else if (message.includes("InvalidNonce")) { - errorReason = "upto_invalid_nonce"; - } else { - errorReason = `transaction_failed: ${message.slice(0, 500)}`; - } - } - return { - success: false, - errorReason, - transaction: "", - network: payload.accepted.network, - payer, - }; + return mapSettleError(error, payload, payer); + } +} + +/** + * Settles an upto Permit2 payment directly when Permit2 allowance is already on-chain. + * + * @param signer - The facilitator signer for contract writes + * @param payload - The payment payload for network info + * @param permit2Payload - The upto Permit2 specific payload with authorization and signature + * @param settlementAmount - The amount to settle on-chain + * @param facilitatorAddress - The facilitator address authorized in the witness + * @returns Promise resolving to a settlement response + */ +async function settleUptoDirect( + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + permit2Payload: UptoPermit2Payload, + settlementAmount: bigint, + facilitatorAddress: `0x${string}`, +): Promise { + const payer = permit2Payload.permit2Authorization.from; + try { + const tx = await signer.writeContract({ + address: uptoProxyConfig.proxyAddress, + abi: uptoProxyConfig.proxyABI, + functionName: "settle", + args: buildUptoPermit2SettleArgs(permit2Payload, settlementAmount, facilitatorAddress), + }); + + const response = await waitAndReturnSettleResponse(signer, tx, payload, payer); + return { ...response, amount: settlementAmount.toString() }; + } catch (error) { + return mapSettleError(error, payload, payer); } } diff --git a/typescript/packages/mechanisms/evm/src/upto/facilitator/scheme.ts b/typescript/packages/mechanisms/evm/src/upto/facilitator/scheme.ts index 9c49b81..b49eb9f 100644 --- a/typescript/packages/mechanisms/evm/src/upto/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/evm/src/upto/facilitator/scheme.ts @@ -2,101 +2,108 @@ import { PaymentPayload, PaymentRequirements, SchemeNetworkFacilitator, + FacilitatorContext, SettleResponse, VerifyResponse, } from "@x402/core/types"; import { FacilitatorEvmSigner } from "../../signer"; -import { ExactPermit2Payload } from "../../types"; +import { UptoPermit2Payload, isUptoPermit2Payload } from "../../types"; import { verifyUptoPermit2, settleUptoPermit2 } from "./permit2"; -export interface UptoEvmSchemeConfig { - /** - * If enabled, the facilitator will deploy ERC-4337 smart wallets - * via EIP-6492 when encountering undeployed contract signatures. - * - * @default false - */ - deployERC4337WithEIP6492?: boolean; -} - /** * EVM facilitator implementation for the Upto payment scheme. - * Uses Permit2 with x402UptoPermit2Proxy for variable-amount payments. - * - * The upto scheme allows a single signature to cover multiple requests - * up to a spend cap. Each settlement uses the actual accumulated amount - * rather than the full permitted amount. + * Handles verification and settlement of Permit2-based payments. */ export class UptoEvmScheme implements SchemeNetworkFacilitator { readonly scheme = "upto"; readonly caipFamily = "eip155:*"; - private readonly config: Required; /** - * Creates a new UptoEvmScheme instance. + * Creates a new UptoEvmScheme facilitator instance. * * @param signer - The EVM signer for facilitator operations - * @param config - Optional configuration for the facilitator */ - constructor( - private readonly signer: FacilitatorEvmSigner, - config?: UptoEvmSchemeConfig, - ) { - this.config = { - deployERC4337WithEIP6492: config?.deployERC4337WithEIP6492 ?? false, - }; - } + constructor(private readonly signer: FacilitatorEvmSigner) {} /** - * Get mechanism-specific extra data for the supported kinds endpoint. - * For EVM upto, no extra data is needed. + * Returns extra metadata required by the upto scheme, including the facilitator address. * - * @param _ - The network identifier (unused for EVM) - * @returns undefined (EVM has no extra data) + * @param _ - The network identifier (unused) + * @returns Object with facilitatorAddress, or undefined if no signer addresses are available */ getExtra(_: string): Record | undefined { - return undefined; + const addresses = this.signer.getAddresses(); + if (addresses.length === 0) { + return undefined; + } + return { facilitatorAddress: addresses[Math.floor(Math.random() * addresses.length)] }; } /** - * Get signer addresses used by this facilitator. + * Returns the list of facilitator signer addresses for the upto scheme. * - * @param _ - The network identifier (unused for EVM, addresses are network-agnostic) - * @returns Array of facilitator wallet addresses + * @param _ - The network identifier (unused) + * @returns Array of facilitator signer addresses */ getSigners(_: string): string[] { return [...this.signer.getAddresses()]; } /** - * Verifies an upto payment payload. - * All upto payloads use Permit2. + * Verifies an upto Permit2 payment payload against the given requirements. * * @param payload - The payment payload to verify - * @param requirements - The payment requirements - * @returns Promise resolving to verification response + * @param requirements - The payment requirements to verify against + * @param context - Optional facilitator context + * @returns Promise resolving to a verification response */ async verify( payload: PaymentPayload, requirements: PaymentRequirements, + context?: FacilitatorContext, ): Promise { - const rawPayload = payload.payload as ExactPermit2Payload; - return verifyUptoPermit2(this.signer, payload, requirements, rawPayload); + const rawPayload = payload.payload as Record; + if (!isUptoPermit2Payload(rawPayload)) { + return { isValid: false, invalidReason: "unsupported_payload_type", payer: "" }; + } + return verifyUptoPermit2( + this.signer, + payload, + requirements, + rawPayload as UptoPermit2Payload, + context, + ); } /** - * Settles an upto payment by executing the transfer. - * All upto payloads use Permit2 via x402UptoPermit2Proxy. + * Settles an upto Permit2 payment on-chain. * * @param payload - The payment payload to settle * @param requirements - The payment requirements - * @returns Promise resolving to settlement response + * @param context - Optional facilitator context + * @returns Promise resolving to a settlement response */ async settle( payload: PaymentPayload, requirements: PaymentRequirements, + context?: FacilitatorContext, ): Promise { - const rawPayload = payload.payload as ExactPermit2Payload; - return settleUptoPermit2(this.signer, payload, requirements, rawPayload); + const rawPayload = payload.payload as Record; + if (!isUptoPermit2Payload(rawPayload)) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "unsupported_payload_type", + payer: "", + }; + } + return settleUptoPermit2( + this.signer, + payload, + requirements, + rawPayload as UptoPermit2Payload, + context, + ); } } diff --git a/typescript/packages/mechanisms/evm/src/upto/index.ts b/typescript/packages/mechanisms/evm/src/upto/index.ts new file mode 100644 index 0000000..41d65cf --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/index.ts @@ -0,0 +1 @@ +export { UptoEvmScheme } from "./client/scheme"; diff --git a/typescript/packages/mechanisms/evm/src/upto/server/index.ts b/typescript/packages/mechanisms/evm/src/upto/server/index.ts new file mode 100644 index 0000000..01843a1 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/server/index.ts @@ -0,0 +1,3 @@ +// Note: No register.ts helper — V1 backward compatibility is not needed for upto. +// Use direct class instantiation: server.register("eip155:*", new UptoEvmScheme()) +export { UptoEvmScheme } from "./scheme"; diff --git a/typescript/packages/mechanisms/evm/src/upto/server/scheme.ts b/typescript/packages/mechanisms/evm/src/upto/server/scheme.ts new file mode 100644 index 0000000..7c3549c --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/server/scheme.ts @@ -0,0 +1,173 @@ +import { + AssetAmount, + Network, + PaymentRequirements, + Price, + SchemeNetworkServer, + MoneyParser, +} from "@x402/core/types"; +import { getAddress } from "viem"; +import { getDefaultAsset } from "../../shared/defaultAssets"; + +/** + * EVM server implementation for the Upto payment scheme. + * Handles price parsing, payment requirements enhancement, and default asset resolution. + */ +export class UptoEvmScheme implements SchemeNetworkServer { + readonly scheme = "upto"; + private moneyParsers: MoneyParser[] = []; + + /** + * Registers a custom money parser for converting prices to asset amounts. + * + * @param parser - The money parser function to register + * @returns This instance for chaining + */ + registerMoneyParser(parser: MoneyParser): UptoEvmScheme { + this.moneyParsers.push(parser); + return this; + } + + /** + * Returns the decimal precision of the default stablecoin for the given network. + * Implements the optional AssetDecimalsProvider interface used by resolveSettlementOverrideAmount. + * + * @param _asset - The asset symbol (unused; defaults to the network's default stablecoin) + * @param network - The network to look up the default asset for + * @returns The number of decimal places for the asset + */ + getAssetDecimals(_asset: string, network: Network): number { + try { + return getDefaultAsset(network).decimals; + } catch { + return 6; + } + } + + /** + * Parses a price into an asset amount for the given network. + * + * @param price - The price to parse (string, number, or AssetAmount) + * @param network - The target network + * @returns Promise resolving to an asset amount + */ + async parsePrice(price: Price, network: Network): Promise { + if (typeof price === "object" && price !== null && "amount" in price) { + if (!price.asset) { + throw new Error(`Asset address must be specified for AssetAmount on network ${network}`); + } + return { + amount: price.amount, + asset: price.asset, + extra: price.extra || {}, + }; + } + + const amount = this.parseMoneyToDecimal(price); + + for (const parser of this.moneyParsers) { + const result = await parser(amount, network); + if (result !== null) { + return result; + } + } + + return this.defaultMoneyConversion(amount, network); + } + + /** + * Enhances payment requirements with upto-specific metadata. + * + * @param paymentRequirements - The base payment requirements + * @param supportedKind - The supported scheme/network kind + * @param supportedKind.x402Version - The x402 protocol version + * @param supportedKind.scheme - The payment scheme name + * @param supportedKind.network - The target network + * @param supportedKind.extra - Optional extra metadata + * @param extensionKeys - Extension keys to include + * @returns Promise resolving to enhanced payment requirements + */ + enhancePaymentRequirements( + paymentRequirements: PaymentRequirements, + supportedKind: { + x402Version: number; + scheme: string; + network: Network; + extra?: Record; + }, + extensionKeys: string[], + ): Promise { + void extensionKeys; + return Promise.resolve({ + ...paymentRequirements, + extra: { + ...paymentRequirements.extra, + assetTransferMethod: "permit2", + ...(supportedKind.extra?.facilitatorAddress + ? { facilitatorAddress: getAddress(supportedKind.extra.facilitatorAddress as string) } + : {}), + }, + }); + } + + /** + * Parses a money string or number into a decimal value. + * + * @param money - The money value to parse + * @returns The parsed decimal amount + */ + private parseMoneyToDecimal(money: string | number): number { + if (typeof money === "number") { + return money; + } + + const cleanMoney = money.replace(/^\$/, "").trim(); + const amount = parseFloat(cleanMoney); + + if (isNaN(amount)) { + throw new Error(`Invalid money format: ${money}`); + } + + return amount; + } + + /** + * Converts a numeric dollar amount to an AssetAmount using the default token for the network. + * + * @param amount - The dollar amount as a number + * @param network - The target network + * @returns The converted asset amount with token metadata + */ + private defaultMoneyConversion(amount: number, network: Network): AssetAmount { + const assetInfo = getDefaultAsset(network); + const tokenAmount = this.convertToTokenAmount(amount.toString(), assetInfo.decimals); + + return { + amount: tokenAmount, + asset: assetInfo.address, + extra: { + name: assetInfo.name, + version: assetInfo.version, + assetTransferMethod: "permit2", + }, + }; + } + + /** + * Converts a decimal string amount to an integer token amount using the given decimals. + * + * @param decimalAmount - The amount as a decimal string (e.g. "1.5") + * @param decimals - The number of decimal places for the token + * @returns The token amount as an integer string in smallest units + */ + private convertToTokenAmount(decimalAmount: string, decimals: number): string { + const amount = parseFloat(decimalAmount); + if (isNaN(amount)) { + throw new Error(`Invalid amount: ${decimalAmount}`); + } + const [intPart, decPart = ""] = String(amount).split("."); + const paddedDec = decPart.padEnd(decimals, "0").slice(0, decimals); + const tokenAmount = (intPart + paddedDec).replace(/^0+/, "") || "0"; + return tokenAmount; + } +} diff --git a/typescript/packages/mechanisms/evm/src/utils.ts b/typescript/packages/mechanisms/evm/src/utils.ts index bbdd7c1..02abf07 100644 --- a/typescript/packages/mechanisms/evm/src/utils.ts +++ b/typescript/packages/mechanisms/evm/src/utils.ts @@ -1,20 +1,23 @@ import { toHex } from "viem"; -import { EVM_NETWORK_CHAIN_ID_MAP, EvmNetworkV1 } from "./v1"; /** - * Extract chain ID from network string (e.g., "base-sepolia" -> 84532) - * Used by v1 implementations + * Extract chain ID from a CAIP-2 network identifier (eip155:CHAIN_ID). * - * @param network - The network identifier + * @param network - The network identifier in CAIP-2 format (e.g., "eip155:8453") * @returns The numeric chain ID - * @throws Error if the network is not supported + * @throws Error if the network format is invalid */ -export function getEvmChainId(network: EvmNetworkV1): number { - const chainId = EVM_NETWORK_CHAIN_ID_MAP[network]; - if (!chainId) { - throw new Error(`Unsupported network: ${network}`); +export function getEvmChainId(network: string): number { + if (network.startsWith("eip155:")) { + const idStr = network.split(":")[1]; + const chainId = parseInt(idStr, 10); + if (isNaN(chainId)) { + throw new Error(`Invalid CAIP-2 chain ID: ${network}`); + } + return chainId; } - return chainId; + + throw new Error(`Unsupported network format: ${network} (expected eip155:CHAIN_ID)`); } /** diff --git a/typescript/packages/mechanisms/evm/src/v1/index.ts b/typescript/packages/mechanisms/evm/src/v1/index.ts index 1d1012a..1521a89 100644 --- a/typescript/packages/mechanisms/evm/src/v1/index.ts +++ b/typescript/packages/mechanisms/evm/src/v1/index.ts @@ -19,8 +19,26 @@ export const EVM_NETWORK_CHAIN_ID_MAP = { educhain: 41923, "skale-base-sepolia": 324705682, megaeth: 4326, + monad: 143, + stable: 988, + "stable-testnet": 2201, } as const; export type EvmNetworkV1 = keyof typeof EVM_NETWORK_CHAIN_ID_MAP; export const NETWORKS: string[] = Object.keys(EVM_NETWORK_CHAIN_ID_MAP); + +/** + * Extract chain ID from a v1 legacy network name. + * + * @param network - The v1 network name (e.g., "base-sepolia", "polygon") + * @returns The numeric chain ID + * @throws Error if the network name is not a known v1 network + */ +export function getEvmChainIdV1(network: string): number { + const chainId = EVM_NETWORK_CHAIN_ID_MAP[network as EvmNetworkV1]; + if (!chainId) { + throw new Error(`Unsupported v1 network: ${network}`); + } + return chainId; +} diff --git a/typescript/packages/mechanisms/evm/test/integrations/exact-evm.test.ts b/typescript/packages/mechanisms/evm/test/integrations/exact-evm.test.ts index 9596e2b..523d05a 100644 --- a/typescript/packages/mechanisms/evm/test/integrations/exact-evm.test.ts +++ b/typescript/packages/mechanisms/evm/test/integrations/exact-evm.test.ts @@ -172,60 +172,64 @@ describe("EVM Integration Tests", () => { await server.initialize(); // Initialize to fetch supported kinds }); - it("server should successfully verify and settle an EVM payment from a client", async () => { - // Server - builds PaymentRequired response - const accepts = [ - buildEvmPaymentRequirements( - "0x9876543210987654321098765432109876543210", - "1000", // 0.001 USDC - ), - ]; - const resource = { - url: "https://company.co", - description: "Company Co. resource", - mimeType: "application/json", - }; - const paymentRequired = server.createPaymentRequiredResponse(accepts, resource); - - // Client - responds with PaymentPayload response - const paymentPayload = await client.createPaymentPayload(paymentRequired); - - expect(paymentPayload).toBeDefined(); - expect(paymentPayload.x402Version).toBe(2); - expect(paymentPayload.accepted.scheme).toBe("exact"); - - // Verify the payload structure - const evmPayload = paymentPayload.payload as ExactEvmPayloadV2; - expect(evmPayload.authorization).toBeDefined(); - expect(evmPayload.authorization.from).toBe(clientAddress); - expect(evmPayload.authorization.to).toBe("0x9876543210987654321098765432109876543210"); - expect(evmPayload.signature).toBeDefined(); - - // Server - maps payment payload to payment requirements - const accepted = server.findMatchingRequirements(accepts, paymentPayload); - expect(accepted).toBeDefined(); - - const verifyResponse = await server.verifyPayment(paymentPayload, accepted!); - - if (!verifyResponse.isValid) { - console.log("❌ Verification failed!"); - console.log("Invalid reason:", verifyResponse.invalidReason); - console.log("Payer:", verifyResponse.payer); - console.log("Client address:", clientAddress); - console.log("Payload:", JSON.stringify(paymentPayload, null, 2)); - } + it( + "server should successfully verify and settle an EVM payment from a client", + { timeout: 30000 }, + async () => { + // Server - builds PaymentRequired response + const accepts = [ + buildEvmPaymentRequirements( + "0x9876543210987654321098765432109876543210", + "1000", // 0.001 USDC + ), + ]; + const resource = { + url: "https://company.co", + description: "Company Co. resource", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + // Client - responds with PaymentPayload response + const paymentPayload = await client.createPaymentPayload(paymentRequired); + + expect(paymentPayload).toBeDefined(); + expect(paymentPayload.x402Version).toBe(2); + expect(paymentPayload.accepted.scheme).toBe("exact"); + + // Verify the payload structure + const evmPayload = paymentPayload.payload as ExactEvmPayloadV2; + expect(evmPayload.authorization).toBeDefined(); + expect(evmPayload.authorization.from).toBe(clientAddress); + expect(evmPayload.authorization.to).toBe("0x9876543210987654321098765432109876543210"); + expect(evmPayload.signature).toBeDefined(); + + // Server - maps payment payload to payment requirements + const accepted = server.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + const verifyResponse = await server.verifyPayment(paymentPayload, accepted!); + + if (!verifyResponse.isValid) { + console.log("❌ Verification failed!"); + console.log("Invalid reason:", verifyResponse.invalidReason); + console.log("Payer:", verifyResponse.payer); + console.log("Client address:", clientAddress); + console.log("Payload:", JSON.stringify(paymentPayload, null, 2)); + } - expect(verifyResponse.isValid).toBe(true); - expect(verifyResponse.payer).toBe(clientAddress); + expect(verifyResponse.isValid).toBe(true); + expect(verifyResponse.payer).toBe(clientAddress); - // Server does work here + // Server does work here - const settleResponse = await server.settlePayment(paymentPayload, accepted!); - expect(settleResponse.success).toBe(true); - expect(settleResponse.network).toBe("eip155:84532"); - expect(settleResponse.transaction).toBeDefined(); - expect(settleResponse.payer).toBe(clientAddress); - }); + const settleResponse = await server.settlePayment(paymentPayload, accepted!); + expect(settleResponse.success).toBe(true); + expect(settleResponse.network).toBe("eip155:84532"); + expect(settleResponse.transaction).toBeDefined(); + expect(settleResponse.payer).toBe(clientAddress); + }, + ); }); describe("x402HTTPClient / x402HTTPResourceServer / x402Facilitator - EVM Flow", () => { diff --git a/typescript/packages/mechanisms/evm/test/unit/constants.test.ts b/typescript/packages/mechanisms/evm/test/unit/constants.test.ts index 2a74c23..2bd0305 100644 --- a/typescript/packages/mechanisms/evm/test/unit/constants.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/constants.test.ts @@ -1,5 +1,12 @@ import { describe, it, expect } from "vitest"; -import { authorizationTypes, eip3009ABI } from "../../src/constants"; +import { hashTypedData } from "viem"; +import { + authorizationTypes, + eip3009ABI, + permit2WitnessTypes, + x402ExactPermit2ProxyAddress, + PERMIT2_ADDRESS, +} from "../../src/constants"; describe("EVM Constants", () => { describe("authorizationTypes", () => { @@ -76,4 +83,98 @@ describe("EVM Constants", () => { expect(bytesSigFunction).toBeDefined(); }); }); + + describe("Permit2 witness types", () => { + it("Witness type must not contain 'extra' field (post-audit)", () => { + const witnessFields = permit2WitnessTypes.Witness; + const hasExtra = witnessFields.some(f => f.name === "extra"); + expect(hasExtra).toBe(false); + }); + + it("Witness type must have exactly 'to' and 'validAfter' fields", () => { + const witnessFields = permit2WitnessTypes.Witness; + expect(witnessFields).toHaveLength(2); + expect(witnessFields[0].name).toBe("to"); + expect(witnessFields[1].name).toBe("validAfter"); + }); + }); + + /** + * Cross-SDK EIP-712 hash test vector. + * + * The canonical input below must produce the same 32-byte hash in Go + * (see go/test/unit/evm_eip712_test.go, TestPermit2HashCrossSDKVector) + * and in TypeScript here. If both hashes are identical for this input, + * the two SDKs agree on the post-audit witness struct (no 'extra' field). + */ + describe("Cross-SDK Permit2 EIP-712 hash vector", () => { + const canonicalDomain = { + name: "Permit2", + chainId: 84532, // Base Sepolia + verifyingContract: PERMIT2_ADDRESS as `0x${string}`, + } as const; + + const canonicalMessage = { + permitted: { + token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" as `0x${string}`, + amount: 1000000n, + }, + spender: x402ExactPermit2ProxyAddress, + nonce: 1n, + deadline: 9999999999n, + witness: { + to: "0x9876543210987654321098765432109876543210" as `0x${string}`, + validAfter: 0n, + }, + } as const; + + it("should produce a 32-byte hash for the canonical input", () => { + const hash = hashTypedData({ + domain: canonicalDomain, + types: permit2WitnessTypes, + primaryType: "PermitWitnessTransferFrom", + message: canonicalMessage, + }); + // EIP-712 hash is always 32 bytes (returned as 0x-prefixed hex = 66 chars) + expect(hash).toMatch(/^0x[0-9a-f]{64}$/i); + }); + + it("hash should be deterministic", () => { + const hash1 = hashTypedData({ + domain: canonicalDomain, + types: permit2WitnessTypes, + primaryType: "PermitWitnessTransferFrom", + message: canonicalMessage, + }); + const hash2 = hashTypedData({ + domain: canonicalDomain, + types: permit2WitnessTypes, + primaryType: "PermitWitnessTransferFrom", + message: canonicalMessage, + }); + expect(hash1).toBe(hash2); + }); + + it("changing witness.to must produce a different hash (no extra field)", () => { + const hash1 = hashTypedData({ + domain: canonicalDomain, + types: permit2WitnessTypes, + primaryType: "PermitWitnessTransferFrom", + message: canonicalMessage, + }); + const hash2 = hashTypedData({ + domain: canonicalDomain, + types: permit2WitnessTypes, + primaryType: "PermitWitnessTransferFrom", + message: { + ...canonicalMessage, + witness: { + to: "0x0000000000000000000000000000000000000001" as `0x${string}`, + validAfter: 0n, + }, + }, + }); + expect(hash1).not.toBe(hash2); + }); + }); }); diff --git a/typescript/packages/mechanisms/evm/test/unit/exact/client.rpc.test.ts b/typescript/packages/mechanisms/evm/test/unit/exact/client.rpc.test.ts new file mode 100644 index 0000000..5143713 --- /dev/null +++ b/typescript/packages/mechanisms/evm/test/unit/exact/client.rpc.test.ts @@ -0,0 +1,123 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { ClientEvmSigner } from "../../../src/signer"; + +const { + mockReadContract, + mockGetTransactionCount, + mockEstimateFeesPerGas, + mockCreatePublicClient, + mockHttp, +} = vi.hoisted(() => { + const readContract = vi.fn(); + const getTransactionCount = vi.fn(); + const estimateFeesPerGas = vi.fn(); + return { + mockReadContract: readContract, + mockGetTransactionCount: getTransactionCount, + mockEstimateFeesPerGas: estimateFeesPerGas, + mockCreatePublicClient: vi.fn(() => ({ + readContract, + getTransactionCount, + estimateFeesPerGas, + })), + mockHttp: vi.fn((url: string) => ({ url })), + }; +}); + +vi.mock("viem", () => ({ + createPublicClient: mockCreatePublicClient, + http: mockHttp, +})); + +import { + resolveRpcUrl, + resolveExtensionRpcCapabilities, + type ExactEvmSchemeOptions, +} from "../../../src/exact/client/rpc"; + +describe("Exact EVM RPC resolver", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("resolves rpc url from single config", () => { + const options: ExactEvmSchemeOptions = { rpcUrl: "https://base.example" }; + expect(resolveRpcUrl("eip155:8453", options)).toBe("https://base.example"); + }); + + it("resolves rpc url from chain map", () => { + const options: ExactEvmSchemeOptions = { + 137: { rpcUrl: "https://polygon.example" }, + 8453: { rpcUrl: "https://base.example" }, + }; + expect(resolveRpcUrl("eip155:8453", options)).toBe("https://base.example"); + expect(resolveRpcUrl("eip155:137", options)).toBe("https://polygon.example"); + }); + + it("keeps signer capabilities as highest precedence", async () => { + const signerRead = vi.fn().mockResolvedValue(1n); + const signerGetTx = vi.fn().mockResolvedValue(7); + const signerFees = vi.fn().mockResolvedValue({ + maxFeePerGas: 100n, + maxPriorityFeePerGas: 10n, + }); + + const signer: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xabc"), + readContract: signerRead, + getTransactionCount: signerGetTx, + estimateFeesPerGas: signerFees, + }; + + const capabilities = resolveExtensionRpcCapabilities("eip155:8453", signer, { + rpcUrl: "https://base.example", + }); + await capabilities.readContract?.({ + address: "0x1234567890123456789012345678901234567890", + abi: [], + functionName: "allowance", + args: [], + }); + + expect(capabilities.readContract).toBe(signerRead); + expect(capabilities.getTransactionCount).toBe(signerGetTx); + expect(capabilities.estimateFeesPerGas).toBe(signerFees); + expect(mockCreatePublicClient).not.toHaveBeenCalled(); + }); + + it("backfills missing read and fee capabilities from rpc", async () => { + mockReadContract.mockResolvedValue(0n); + mockGetTransactionCount.mockResolvedValue(3); + mockEstimateFeesPerGas.mockResolvedValue({ + maxFeePerGas: 111n, + maxPriorityFeePerGas: 22n, + }); + + const signer: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xabc"), + }; + + const capabilities = resolveExtensionRpcCapabilities("eip155:8453", signer, { + rpcUrl: "https://base.example", + }); + + const allowance = await capabilities.readContract?.({ + address: "0x1234567890123456789012345678901234567890", + abi: [], + functionName: "allowance", + args: [], + }); + const nonce = await capabilities.getTransactionCount?.({ + address: "0x1234567890123456789012345678901234567890", + }); + const fees = await capabilities.estimateFeesPerGas?.(); + + expect(mockCreatePublicClient).toHaveBeenCalledTimes(1); + expect(mockHttp).toHaveBeenCalledWith("https://base.example"); + expect(allowance).toBe(0n); + expect(nonce).toBe(3); + expect(fees).toEqual({ maxFeePerGas: 111n, maxPriorityFeePerGas: 22n }); + }); +}); diff --git a/typescript/packages/mechanisms/evm/test/unit/exact/client.test.ts b/typescript/packages/mechanisms/evm/test/unit/exact/client.test.ts index 1ce2572..19c3114 100644 --- a/typescript/packages/mechanisms/evm/test/unit/exact/client.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/exact/client.test.ts @@ -14,10 +14,11 @@ describe("ExactEvmScheme (Client)", () => { let mockSigner: ClientEvmSigner; beforeEach(() => { - // Create mock signer + // Create mock signer with readContract (required for ClientEvmSigner) mockSigner = { address: "0x1234567890123456789012345678901234567890", signTypedData: vi.fn().mockResolvedValue("0xmocksignature123456789"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), }; client = new ExactEvmScheme(mockSigner); }); @@ -622,4 +623,269 @@ describe("Permit2 Approval Flow", () => { expect(result.payload.permit2Authorization).toBeDefined(); }); }); + + describe("EIP-2612 Gas Sponsoring (Scheme-level)", () => { + const permit2Requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 60, + extra: { + assetTransferMethod: "permit2", + name: "USDC", + version: "2", + }, + }; + + it("should not return extensions when eip2612GasSponsoring not in context", async () => { + const signer: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xmocksig"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), + }; + const scheme = new ExactEvmScheme(signer); + const result = await scheme.createPaymentPayload(2, permit2Requirements, { + extensions: {}, + }); + + expect(result.extensions).toBeUndefined(); + }); + + it("should not return extensions when allowance is sufficient", async () => { + const signerWithReader: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xmocksig"), + readContract: vi.fn().mockResolvedValue(BigInt("999999999999999999")), + }; + const schemeWithReader = new ExactEvmScheme(signerWithReader); + + const result = await schemeWithReader.createPaymentPayload(2, permit2Requirements, { + extensions: { + eip2612GasSponsoring: { info: { description: "test", version: "1" }, schema: {} }, + }, + }); + + expect(result.extensions).toBeUndefined(); + }); + + it("should return EIP-2612 extensions when allowance insufficient and extension advertised", async () => { + const signerWithReader: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue( + "0x" + "ab".repeat(32) + "cd".repeat(32) + "1b", // 65 byte sig + ), + readContract: vi + .fn() + .mockResolvedValueOnce(BigInt(0)) // allowance check returns 0 + .mockResolvedValueOnce(BigInt(5)), // nonce query returns 5 + }; + const schemeWithReader = new ExactEvmScheme(signerWithReader); + + const result = await schemeWithReader.createPaymentPayload(2, permit2Requirements, { + extensions: { + eip2612GasSponsoring: { info: { description: "test", version: "1" }, schema: {} }, + }, + }); + + expect(result.extensions).toBeDefined(); + expect(result.extensions!.eip2612GasSponsoring).toBeDefined(); + const ext = result.extensions!.eip2612GasSponsoring as Record; + const info = ext.info as Record; + expect(info.from).toBe("0x1234567890123456789012345678901234567890"); + expect(info.spender).toBeDefined(); + expect(info.signature).toBeDefined(); + expect(info.version).toBe("1"); + }); + + it("should not return extensions for EIP-3009 asset transfer method", async () => { + const signer: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xmocksig"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), + }; + const scheme = new ExactEvmScheme(signer); + const eip3009Requirements: PaymentRequirements = { + ...permit2Requirements, + extra: { + name: "USDC", + version: "2", + }, + }; + + const result = await scheme.createPaymentPayload(2, eip3009Requirements, { + extensions: { + eip2612GasSponsoring: { info: { description: "test", version: "1" }, schema: {} }, + }, + }); + + expect(result.extensions).toBeUndefined(); + }); + }); + + describe("ERC-20 Approval Gas Sponsoring (Scheme-level)", () => { + const erc20Requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000", + asset: "0xeED520980fC7C7B4eB379B96d61CEdea2423005a", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 60, + extra: { + assetTransferMethod: "permit2", + // No name/version - generic ERC-20 without EIP-2612 + }, + }; + + it("should not return extensions when erc20ApprovalGasSponsoring not in context", async () => { + const signer: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xmocksig"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), + signTransaction: vi.fn().mockResolvedValue("0x02ab"), + getTransactionCount: vi.fn().mockResolvedValue(0), + estimateFeesPerGas: vi + .fn() + .mockResolvedValue({ maxFeePerGas: 1n, maxPriorityFeePerGas: 1n }), + }; + const scheme = new ExactEvmScheme(signer); + const result = await scheme.createPaymentPayload(2, erc20Requirements, { + extensions: {}, // No erc20ApprovalGasSponsoring + }); + + expect(result.extensions).toBeUndefined(); + }); + + it("should not return extensions when allowance is sufficient", async () => { + const signer: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xmocksig"), + readContract: vi.fn().mockResolvedValue(BigInt("999999999999999999")), + signTransaction: vi.fn().mockResolvedValue("0x02ab"), + getTransactionCount: vi.fn().mockResolvedValue(0), + estimateFeesPerGas: vi + .fn() + .mockResolvedValue({ maxFeePerGas: 1n, maxPriorityFeePerGas: 1n }), + }; + const scheme = new ExactEvmScheme(signer); + const result = await scheme.createPaymentPayload(2, erc20Requirements, { + extensions: { + erc20ApprovalGasSponsoring: { info: { description: "test", version: "1" }, schema: {} }, + }, + }); + + expect(result.extensions).toBeUndefined(); + }); + + it("should not return extensions when signer lacks signTransaction capability", async () => { + const signer: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xmocksig"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), + // No signTransaction, getTransactionCount, estimateFeesPerGas + }; + const scheme = new ExactEvmScheme(signer); + const result = await scheme.createPaymentPayload(2, erc20Requirements, { + extensions: { + erc20ApprovalGasSponsoring: { info: { description: "test", version: "1" }, schema: {} }, + }, + }); + + expect(result.extensions).toBeUndefined(); + }); + + it("should return ERC-20 extensions when allowance insufficient + extension advertised + signer capable", async () => { + const mockSignedTx = "0x02f8ab" as `0x${string}`; + const signer: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xmocksig"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), // zero allowance + signTransaction: vi.fn().mockResolvedValue(mockSignedTx), + getTransactionCount: vi.fn().mockResolvedValue(5), + estimateFeesPerGas: vi + .fn() + .mockResolvedValue({ maxFeePerGas: 1_000_000_000n, maxPriorityFeePerGas: 100_000_000n }), + }; + const scheme = new ExactEvmScheme(signer); + const result = await scheme.createPaymentPayload(2, erc20Requirements, { + extensions: { + erc20ApprovalGasSponsoring: { info: { description: "test", version: "1" }, schema: {} }, + }, + }); + + expect(result.extensions).toBeDefined(); + expect(result.extensions!.erc20ApprovalGasSponsoring).toBeDefined(); + const ext = result.extensions!.erc20ApprovalGasSponsoring as Record; + const info = ext.info as Record; + expect(info.from).toBe("0x1234567890123456789012345678901234567890"); + expect(info.spender).toBeDefined(); + expect(info.signedTransaction).toBe(mockSignedTx); + expect(info.version).toBe("1"); + }); + + it("should use EIP-2612 over ERC-20 approval when token has EIP-2612 support", async () => { + const eip2612CompatibleRequirements: PaymentRequirements = { + ...erc20Requirements, + extra: { + assetTransferMethod: "permit2", + name: "TOKEN", + version: "1", + }, + }; + + const signer: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0x" + "ab".repeat(32) + "cd".repeat(32) + "1b"), + readContract: vi + .fn() + .mockResolvedValueOnce(BigInt(0)) // allowance check + .mockResolvedValueOnce(BigInt(3)), // nonce for EIP-2612 + signTransaction: vi.fn(), // Should NOT be called + getTransactionCount: vi.fn(), + estimateFeesPerGas: vi.fn(), + }; + const scheme = new ExactEvmScheme(signer); + + const result = await scheme.createPaymentPayload(2, eip2612CompatibleRequirements, { + extensions: { + eip2612GasSponsoring: { info: { description: "test", version: "1" }, schema: {} }, + erc20ApprovalGasSponsoring: { info: { description: "test", version: "1" }, schema: {} }, + }, + }); + + // Should have EIP-2612 extension (not ERC-20 approval) + expect(result.extensions).toBeDefined(); + expect(result.extensions!.eip2612GasSponsoring).toBeDefined(); + expect(result.extensions!.erc20ApprovalGasSponsoring).toBeUndefined(); + // signTransaction should NOT have been called + expect(signer.signTransaction).not.toHaveBeenCalled(); + }); + + it("should use ERC-20 approval when EIP-2612 not advertised but ERC-20 is", async () => { + const mockSignedTx = "0x02f8ab" as `0x${string}`; + const signer: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xmocksig"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), // zero allowance + signTransaction: vi.fn().mockResolvedValue(mockSignedTx), + getTransactionCount: vi.fn().mockResolvedValue(0), + estimateFeesPerGas: vi + .fn() + .mockResolvedValue({ maxFeePerGas: 1n, maxPriorityFeePerGas: 1n }), + }; + const scheme = new ExactEvmScheme(signer); + + const result = await scheme.createPaymentPayload(2, erc20Requirements, { + extensions: { + // Only ERC-20, no EIP-2612 + erc20ApprovalGasSponsoring: { info: { description: "test", version: "1" }, schema: {} }, + }, + }); + + expect(result.extensions).toBeDefined(); + expect(result.extensions!.erc20ApprovalGasSponsoring).toBeDefined(); + expect(result.extensions!.eip2612GasSponsoring).toBeUndefined(); + }); + }); }); diff --git a/typescript/packages/mechanisms/evm/test/unit/exact/facilitator.test.ts b/typescript/packages/mechanisms/evm/test/unit/exact/facilitator.test.ts index 487040a..fc824b2 100644 --- a/typescript/packages/mechanisms/evm/test/unit/exact/facilitator.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/exact/facilitator.test.ts @@ -3,7 +3,22 @@ import { ExactEvmScheme } from "../../../src/exact/facilitator/scheme"; import { ExactEvmScheme as ClientExactEvmScheme } from "../../../src/exact/client/scheme"; import type { ClientEvmSigner, FacilitatorEvmSigner } from "../../../src/signer"; import { PaymentRequirements, PaymentPayload } from "@x402/core/types"; -import { x402ExactPermit2ProxyAddress } from "../../../src/constants"; +import { x402ExactPermit2ProxyAddress, PERMIT2_ADDRESS } from "../../../src/constants"; +import { ERC20_APPROVAL_GAS_SPONSORING_KEY } from "../../../src/exact/extensions"; +import { MULTICALL3_ADDRESS } from "../../../src/multicall"; +import { concat, encodeAbiParameters } from "viem"; +import * as Errors from "../../../src/exact/facilitator/errors"; + +// Mock viem's transaction parsing utilities for ERC-20 approval tests +// Uses importOriginal to preserve all other viem exports (getAddress, etc.) +vi.mock("viem", async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + parseTransaction: vi.fn(), + recoverTransactionAddress: vi.fn(), + }; +}); describe("ExactEvmScheme (Facilitator)", () => { let facilitator: ExactEvmScheme; @@ -16,6 +31,7 @@ describe("ExactEvmScheme (Facilitator)", () => { mockClientSigner = { address: "0x1234567890123456789012345678901234567890", signTypedData: vi.fn().mockResolvedValue("0xmocksignature"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), }; client = new ClientExactEvmScheme(mockClientSigner); @@ -100,7 +116,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const result = await facilitator.verify(payload, requirements); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("unsupported_scheme"); + expect(result.invalidReason).toBe(Errors.ErrInvalidScheme); }); it("should reject if missing EIP-712 domain parameters", async () => { @@ -128,7 +144,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const result = await facilitator.verify(fullPayload, requirements); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("missing_eip712_domain"); + expect(result.invalidReason).toBe(Errors.ErrMissingEip712Domain); }); it("should reject if network doesn't match", async () => { @@ -146,7 +162,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const fullPayload: PaymentPayload = { ...paymentPayload, - accepted: { ...requirements, network: "eip155:1" }, // Wrong network in accepted + accepted: requirements, resource: { url: "", description: "", mimeType: "" }, }; @@ -155,7 +171,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const result = await facilitator.verify(fullPayload, wrongNetworkRequirements); expect(result.isValid).toBe(false); - // Verification should fail (network mismatch or other validation error) + expect(result.invalidReason).toBe(Errors.ErrNetworkMismatch); }); it("should reject if recipient doesn't match payTo", async () => { @@ -186,7 +202,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const result = await facilitator.verify(fullPayload, modifiedRequirements); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("invalid_exact_evm_payload_recipient_mismatch"); + expect(result.invalidReason).toBe(Errors.ErrRecipientMismatch); }); it("should reject if amount doesn't match", async () => { @@ -246,7 +262,7 @@ describe("ExactEvmScheme (Facilitator)", () => { }); describe("Permit2 payload verification", () => { - it("should verify Permit2 payloads with valid signature and allowance", async () => { + it("should verify Permit2 payloads with valid signature and simulation success", async () => { const requirements: PaymentRequirements = { scheme: "exact", network: "eip155:84532", @@ -257,8 +273,8 @@ describe("ExactEvmScheme (Facilitator)", () => { extra: { name: "USDC", version: "2", assetTransferMethod: "permit2" }, }; - // Mock readContract to return sufficient allowance and balance - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(BigInt("10000000000")); + // Simulation of settle() on the proxy succeeds (readContract doesn't throw) + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const permit2Payload: PaymentPayload = { x402Version: 2, @@ -276,7 +292,6 @@ describe("ExactEvmScheme (Facilitator)", () => { witness: { to: requirements.payTo, validAfter: "0", - extra: "0x", }, }, }, @@ -290,7 +305,7 @@ describe("ExactEvmScheme (Facilitator)", () => { expect(result.payer).toBe(mockClientSigner.address); }); - it("should reject Permit2 payloads with insufficient allowance", async () => { + it("should reject Permit2 payloads when simulation fails and allowance is insufficient", async () => { const requirements: PaymentRequirements = { scheme: "exact", network: "eip155:84532", @@ -301,8 +316,31 @@ describe("ExactEvmScheme (Facilitator)", () => { extra: { name: "USDC", version: "2", assetTransferMethod: "permit2" }, }; - // Mock readContract to return zero allowance - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(BigInt(0)); + // Simulation fails (settle throws), diagnostic multicall returns proxy OK, balance OK, allowance 0 + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === x402ExactPermit2ProxyAddress) { + return Promise.reject(new Error("execution reverted")); + } + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { + success: true, + returnData: "0x000000000000000000000000000000000022D473030F116dDEE9F6B43aC78BA3", + }, + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + ]); + } + return Promise.resolve(BigInt(0)); + }); const permit2Payload: PaymentPayload = { x402Version: 2, @@ -320,7 +358,6 @@ describe("ExactEvmScheme (Facilitator)", () => { witness: { to: requirements.payTo, validAfter: "0", - extra: "0x", }, }, }, @@ -362,7 +399,6 @@ describe("ExactEvmScheme (Facilitator)", () => { witness: { to: requirements.payTo, validAfter: "0", - extra: "0x", }, }, }, @@ -404,7 +440,6 @@ describe("ExactEvmScheme (Facilitator)", () => { witness: { to: requirements.payTo, validAfter: "0", - extra: "0x", }, }, }, @@ -446,7 +481,6 @@ describe("ExactEvmScheme (Facilitator)", () => { witness: { to: "0x0000000000000000000000000000000000000001", // Wrong recipient validAfter: "0", - extra: "0x", }, }, }, @@ -474,8 +508,8 @@ describe("ExactEvmScheme (Facilitator)", () => { extra: { name: "USDC", version: "2", assetTransferMethod: "permit2" }, }; - // Mock readContract to return sufficient allowance and balance - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(BigInt("10000000000")); + // settle's re-verify has simulate=false (default), so no simulation readContract needed + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const permit2Payload: PaymentPayload = { x402Version: 2, @@ -493,7 +527,6 @@ describe("ExactEvmScheme (Facilitator)", () => { witness: { to: requirements.payTo, validAfter: "0", - extra: "0x", }, }, }, @@ -509,7 +542,7 @@ describe("ExactEvmScheme (Facilitator)", () => { expect(mockFacilitatorSigner.writeContract).toHaveBeenCalled(); }); - it("should fail Permit2 settlement when verification fails", async () => { + it("should fail Permit2 settlement when signature verification fails", async () => { const requirements: PaymentRequirements = { scheme: "exact", network: "eip155:84532", @@ -520,8 +553,8 @@ describe("ExactEvmScheme (Facilitator)", () => { extra: { name: "USDC", version: "2", assetTransferMethod: "permit2" }, }; - // Mock readContract to return zero allowance - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(BigInt(0)); + // Signature verification fails + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(false); const permit2Payload: PaymentPayload = { x402Version: 2, @@ -539,7 +572,6 @@ describe("ExactEvmScheme (Facilitator)", () => { witness: { to: requirements.payTo, validAfter: "0", - extra: "0x", }, }, }, @@ -550,7 +582,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const result = await facilitator.settle(permit2Payload, requirements); expect(result.success).toBe(false); - expect(result.errorReason).toBe("permit2_allowance_required"); + expect(result.errorReason).toBe("invalid_permit2_signature"); expect(result.payer).toBe(mockClientSigner.address); }); }); @@ -590,7 +622,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const result = await facilitator.verify(payload, requirements); expect(result.isValid).toBe(false); - expect(result.invalidReason).toContain("invalid_exact_evm_payload_signature"); + expect(result.invalidReason).toBe(Errors.ErrInvalidSignature); }); it("should normalize addresses (case-insensitive)", async () => { @@ -619,4 +651,1117 @@ describe("ExactEvmScheme (Facilitator)", () => { expect(result).toBeDefined(); }); }); + + describe("EIP-2612 Gas Sponsoring - Verify", () => { + it("should accept valid EIP-2612 extension when settleWithPermit simulation succeeds", async () => { + // Simulation of settleWithPermit on proxy succeeds + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const permit2Requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 60, + extra: { assetTransferMethod: "permit2", name: "USDC", version: "2" }, + }; + + const permit2ClientSigner: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0x" + "ab".repeat(32) + "cd".repeat(32) + "1b"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), + }; + const permit2Client = new ClientExactEvmScheme(permit2ClientSigner); + const paymentPayload = await permit2Client.createPaymentPayload(2, permit2Requirements); + + const now = Math.floor(Date.now() / 1000); + const fullPayload: PaymentPayload = { + ...paymentPayload, + accepted: permit2Requirements, + resource: { url: "https://test.com", description: "", mimeType: "" }, + extensions: { + eip2612GasSponsoring: { + info: { + from: "0x1234567890123456789012345678901234567890", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + nonce: "0", + deadline: (now + 300).toString(), + signature: "0x" + "ab".repeat(32) + "cd".repeat(32) + "1b", + version: "1", + }, + schema: {}, + }, + }, + }; + + const result = await facilitator.verify(fullPayload, permit2Requirements); + expect(result).toBeDefined(); + if (!result.isValid) { + expect(result.invalidReason).not.toBe("permit2_allowance_required"); + } + }); + + it("should reject when simulation fails and no extension present (allowance insufficient)", async () => { + // Simulation fails, diagnostic multicall returns low allowance + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === x402ExactPermit2ProxyAddress) { + return Promise.reject(new Error("execution reverted")); + } + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { + success: true, + returnData: "0x000000000000000000000000000000000022D473030F116dDEE9F6B43aC78BA3", + }, + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + ]); + } + return Promise.resolve(BigInt(0)); + }); + + const permit2Requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 60, + extra: { assetTransferMethod: "permit2", name: "USDC", version: "2" }, + }; + + const permit2ClientSigner: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0x" + "ab".repeat(32) + "cd".repeat(32) + "1b"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), + }; + const permit2Client = new ClientExactEvmScheme(permit2ClientSigner); + const paymentPayload = await permit2Client.createPaymentPayload(2, permit2Requirements); + + const fullPayload: PaymentPayload = { + ...paymentPayload, + accepted: permit2Requirements, + resource: { url: "https://test.com", description: "", mimeType: "" }, + }; + + const result = await facilitator.verify(fullPayload, permit2Requirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("permit2_allowance_required"); + }); + + it("should reject EIP-2612 extension with wrong spender", async () => { + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const permit2Requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 60, + extra: { assetTransferMethod: "permit2", name: "USDC", version: "2" }, + }; + + const permit2ClientSigner: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0x" + "ab".repeat(32) + "cd".repeat(32) + "1b"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), + }; + const permit2Client = new ClientExactEvmScheme(permit2ClientSigner); + const paymentPayload = await permit2Client.createPaymentPayload(2, permit2Requirements); + + const now = Math.floor(Date.now() / 1000); + const fullPayload: PaymentPayload = { + ...paymentPayload, + accepted: permit2Requirements, + resource: { url: "https://test.com", description: "", mimeType: "" }, + extensions: { + eip2612GasSponsoring: { + info: { + from: "0x1234567890123456789012345678901234567890", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + spender: "0x0000000000000000000000000000000000000000", // WRONG spender + amount: + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + nonce: "0", + deadline: (now + 300).toString(), + signature: "0x" + "ab".repeat(32) + "cd".repeat(32) + "1b", + version: "1", + }, + schema: {}, + }, + }, + }; + + const result = await facilitator.verify(fullPayload, permit2Requirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("eip2612_spender_not_permit2"); + }); + }); + + describe("ERC-6492 counterfactual signature verification", () => { + const ERC6492_MAGIC = "0x6492649264926492649264926492649264926492649264926492649264926492"; + + function makeERC6492Sig( + factory: `0x${string}`, + calldata: `0x${string}`, + innerSig: `0x${string}`, + ): `0x${string}` { + const encoded = encodeAbiParameters( + [{ type: "address" }, { type: "bytes" }, { type: "bytes" }], + [factory, calldata, innerSig], + ); + return concat([encoded, ERC6492_MAGIC]) as `0x${string}`; + } + + const erc6492Requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { name: "USDC", version: "2" }, + }; + + const erc6492Payer = "0x1234567890123456789012345678901234567890"; + const factory = "0x1111111111111111111111111111111111111111" as `0x${string}`; + const factoryCalldata = "0xdeadbeef" as `0x${string}`; + const garbageInnerSig = ("0x" + "00".repeat(65)) as `0x${string}`; + const erc6492Sig = makeERC6492Sig(factory, factoryCalldata, garbageInnerSig); + + function makeERC6492Payload(sig: `0x${string}`): PaymentPayload { + return { + x402Version: 2, + payload: { + authorization: { + from: erc6492Payer, + to: erc6492Requirements.payTo, + value: erc6492Requirements.amount, + validAfter: "0", + validBefore: "999999999999", + nonce: "0x0000000000000000000000000000000000000000000000000000000000000001", + }, + signature: sig, + }, + accepted: erc6492Requirements, + resource: { url: "", description: "", mimeType: "" }, + }; + } + + it("should accept ERC-6492 when verifyTypedData returns true and simulation passes", async () => { + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(true); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { success: true, returnData: "0x" }, + { success: true, returnData: "0x" }, + ]); + } + return Promise.resolve(BigInt("10000000")); + }); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(true); + expect(result.payer).toBe(erc6492Payer); + }); + + it("should accept ERC-6492 when verifyTypedData fails but simulation passes (EOA-only signer)", async () => { + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { success: true, returnData: "0x" }, + { success: true, returnData: "0x" }, + ]); + } + return Promise.resolve(BigInt("10000000")); + }); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(true); + expect(result.payer).toBe(erc6492Payer); + }); + + it("should accept ERC-6492 when verifyTypedData throws but simulation passes", async () => { + mockFacilitatorSigner.verifyTypedData = vi + .fn() + .mockRejectedValue(new Error("invalid signature length")); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { success: true, returnData: "0x" }, + { success: true, returnData: "0x" }, + ]); + } + return Promise.resolve(BigInt("10000000")); + }); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(true); + expect(result.payer).toBe(erc6492Payer); + }); + + it("should reject ERC-6492 when simulation fails (multicall transfer reverts)", async () => { + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(true); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { success: true, returnData: "0x" }, + { success: false, returnData: "0x" }, + ]); + } + return Promise.resolve([ + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + { success: true, returnData: "0x" }, + { success: true, returnData: "0x" }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + ]); + }); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(false); + }); + + it("should reject forged ERC-6492 when verifyTypedData fails and simulation fails", async () => { + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { success: true, returnData: "0x" }, + { success: false, returnData: "0x" }, + ]); + } + return Promise.resolve([ + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + { success: true, returnData: "0x" }, + { success: true, returnData: "0x" }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + ]); + }); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(false); + expect(result.payer).toBe(erc6492Payer); + }); + + it("should reject undeployed smart wallet without ERC-6492 deployment info", async () => { + const longNonERC6492Sig = ("0x" + "ab".repeat(100)) as `0x${string}`; + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x"); + + const result = await facilitator.verify( + makeERC6492Payload(longNonERC6492Sig), + erc6492Requirements, + ); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_evm_payload_undeployed_smart_wallet"); + expect(result.payer).toBe(erc6492Payer); + }); + + it("should accept deployed smart wallet when verifyTypedData fails but simulation passes (ERC-1271)", async () => { + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x6080604052"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { success: true, returnData: "0x" }, + { success: true, returnData: "0x" }, + ]); + } + return Promise.resolve(undefined); + }); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(true); + expect(result.payer).toBe(erc6492Payer); + }); + + it("should reject deployed smart wallet when both verifyTypedData and simulation fail", async () => { + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x6080604052"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockRejectedValue(new Error("execution reverted")); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(Errors.ErrEip3009SimulationFailed); + }); + }); + + describe("EIP-2612 Gas Sponsoring - Settlement", () => { + const permit2Requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 60, + extra: { assetTransferMethod: "permit2", name: "USDC", version: "2" }, + }; + + function makePermit2Payload(extensions?: Record): PaymentPayload { + const now = Math.floor(Date.now() / 1000); + return { + x402Version: 2, + payload: { + signature: "0x" + "ab".repeat(32) + "cd".repeat(32) + "1b", + permit2Authorization: { + from: "0x1234567890123456789012345678901234567890", + permitted: { + token: permit2Requirements.asset, + amount: permit2Requirements.amount, + }, + spender: x402ExactPermit2ProxyAddress, + nonce: "12345", + deadline: (now + 300).toString(), + witness: { + to: permit2Requirements.payTo, + validAfter: "0", + }, + }, + }, + accepted: permit2Requirements, + resource: { url: "https://test.com", description: "", mimeType: "" }, + ...(extensions ? { extensions } : {}), + }; + } + + function makeEip2612Extension() { + const now = Math.floor(Date.now() / 1000); + return { + eip2612GasSponsoring: { + info: { + from: "0x1234567890123456789012345678901234567890", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + nonce: "0", + deadline: (now + 300).toString(), + signature: "0x" + "ab".repeat(32) + "cd".repeat(32) + "1b", + version: "1", + }, + schema: {}, + }, + }; + } + + it("should call settleWithPermit when EIP-2612 extension is present", async () => { + // settle's re-verify has simulate=false, so readContract is not called for simulation + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makePermit2Payload(makeEip2612Extension()); + const result = await facilitator.settle(payload, permit2Requirements); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("0xtxhash"); + + const writeCall = (mockFacilitatorSigner.writeContract as ReturnType).mock + .calls[0][0]; + expect(writeCall.functionName).toBe("settleWithPermit"); + }); + + it("should call settle (not settleWithPermit) when no EIP-2612 extension", async () => { + // settle's re-verify has simulate=false + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makePermit2Payload(); + const result = await facilitator.settle(payload, permit2Requirements); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("0xtxhash"); + + const writeCall = (mockFacilitatorSigner.writeContract as ReturnType).mock + .calls[0][0]; + expect(writeCall.functionName).toBe("settle"); + }); + + it("should map Permit2612AmountMismatch contract revert to permit2_2612_amount_mismatch", async () => { + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + mockFacilitatorSigner.writeContract = vi + .fn() + .mockRejectedValue(new Error("execution reverted: Permit2612AmountMismatch()")); + + const payload = makePermit2Payload(); + const result = await facilitator.settle(payload, permit2Requirements); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe("permit2_2612_amount_mismatch"); + }); + + it("should map InvalidAmount contract revert to permit2_invalid_amount", async () => { + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + mockFacilitatorSigner.writeContract = vi + .fn() + .mockRejectedValue(new Error("execution reverted: InvalidAmount()")); + + const payload = makePermit2Payload(); + const result = await facilitator.settle(payload, permit2Requirements); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe("permit2_invalid_amount"); + }); + + it("should map InvalidNonce contract revert to permit2_invalid_nonce", async () => { + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + mockFacilitatorSigner.writeContract = vi + .fn() + .mockRejectedValue(new Error("execution reverted: InvalidNonce()")); + + const payload = makePermit2Payload(); + const result = await facilitator.settle(payload, permit2Requirements); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe("permit2_invalid_nonce"); + }); + + it("should pass correct EIP-2612 permit struct to settleWithPermit", async () => { + // settle's re-verify has simulate=false + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const extensions = makeEip2612Extension(); + const payload = makePermit2Payload(extensions); + await facilitator.settle(payload, permit2Requirements); + + const writeCall = (mockFacilitatorSigner.writeContract as ReturnType).mock + .calls[0][0]; + expect(writeCall.functionName).toBe("settleWithPermit"); + + const permit2612Struct = writeCall.args[0]; + expect(permit2612Struct.value).toBeDefined(); + expect(permit2612Struct.deadline).toBeDefined(); + expect(permit2612Struct.r).toBeDefined(); + expect(permit2612Struct.s).toBeDefined(); + expect(permit2612Struct.v).toBeDefined(); + expect(typeof permit2612Struct.v).toBe("number"); + }); + }); + + describe("ERC-20 Approval Gas Sponsoring - Verify", () => { + const PAYER = "0x1234567890123456789012345678901234567890" as `0x${string}`; + const TOKEN_ADDRESS = "0xeED520980fC7C7B4eB379B96d61CEdea2423005a" as `0x${string}`; + const MOCK_SIGNED_TX = "0x02f8ab0102030405060708" as `0x${string}`; + + // Approve calldata: approve(PERMIT2_ADDRESS, MaxUint256) + const APPROVE_CALLDATA = + `0x095ea7b3000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3` + + `ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`; + + const erc20Requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000", + asset: TOKEN_ADDRESS, + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 60, + extra: { assetTransferMethod: "permit2" }, + }; + + function makeErc20Permit2Payload(extensions?: Record): PaymentPayload { + const now = Math.floor(Date.now() / 1000); + return { + x402Version: 2, + payload: { + signature: "0x" + "ab".repeat(32) + "cd".repeat(32) + "1b", + permit2Authorization: { + from: PAYER, + permitted: { + token: TOKEN_ADDRESS, + amount: erc20Requirements.amount, + }, + spender: x402ExactPermit2ProxyAddress, + nonce: "99999", + deadline: (now + 300).toString(), + witness: { + to: erc20Requirements.payTo, + validAfter: "0", + }, + }, + }, + accepted: erc20Requirements, + resource: { url: "https://test.com", description: "", mimeType: "" }, + ...(extensions ? { extensions } : {}), + }; + } + + function makeValidErc20Extension() { + return { + erc20ApprovalGasSponsoring: { + info: { + from: PAYER, + asset: TOKEN_ADDRESS, + spender: PERMIT2_ADDRESS, + amount: + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }; + } + + /** Creates a mock FacilitatorContext with the ERC-20 extension registered. */ + function makeErc20Context() { + return { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { key: ERC20_APPROVAL_GAS_SPONSORING_KEY }; + } + return undefined; + }), + }; + } + + it("should reject when simulation fails and no ERC-20 extension (no context)", async () => { + // Simulation of settle() fails, diagnostic multicall shows low allowance + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === x402ExactPermit2ProxyAddress) { + return Promise.reject(new Error("execution reverted")); + } + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { + success: true, + returnData: "0x000000000000000000000000000000000022D473030F116dDEE9F6B43aC78BA3", + }, + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + ]); + } + return Promise.resolve(BigInt(0)); + }); + + const payload = makeErc20Permit2Payload(); + const result = await facilitator.verify(payload, erc20Requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("permit2_allowance_required"); + }); + + it("should reject when ERC-20 extension has invalid format (bad address)", async () => { + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makeErc20Permit2Payload({ + erc20ApprovalGasSponsoring: { + info: { + from: "not-an-address", // invalid + asset: TOKEN_ADDRESS, + spender: PERMIT2_ADDRESS, + amount: "100", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }); + + const result = await facilitator.verify(payload, erc20Requirements, makeErc20Context()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_erc20_approval_extension_format"); + }); + + it("should reject when ERC-20 extension `from` doesn't match payer", async () => { + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makeErc20Permit2Payload({ + erc20ApprovalGasSponsoring: { + info: { + from: "0x0000000000000000000000000000000000000001", // wrong address + asset: TOKEN_ADDRESS, + spender: PERMIT2_ADDRESS, + amount: "100", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }); + + const result = await facilitator.verify(payload, erc20Requirements, makeErc20Context()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("erc20_approval_from_mismatch"); + }); + + it("should reject when ERC-20 extension `asset` doesn't match token", async () => { + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makeErc20Permit2Payload({ + erc20ApprovalGasSponsoring: { + info: { + from: PAYER, + asset: "0x0000000000000000000000000000000000000002", // wrong token + spender: PERMIT2_ADDRESS, + amount: "100", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }); + + const result = await facilitator.verify(payload, erc20Requirements, makeErc20Context()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("erc20_approval_asset_mismatch"); + }); + + it("should reject when ERC-20 extension spender is not PERMIT2_ADDRESS", async () => { + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makeErc20Permit2Payload({ + erc20ApprovalGasSponsoring: { + info: { + from: PAYER, + asset: TOKEN_ADDRESS, + spender: "0x0000000000000000000000000000000000000003", // not Permit2 + amount: "100", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }); + + const result = await facilitator.verify(payload, erc20Requirements, makeErc20Context()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("erc20_approval_spender_not_permit2"); + }); + + it("should accept when valid ERC-20 extension present and prerequisites pass", async () => { + // checkPermit2Prerequisites multicall: proxy deployed + sufficient token balance + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { + success: true, + returnData: "0x000000000000000000000000000000000022D473030F116dDEE9F6B43aC78BA3", + }, + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + ]); + } + return Promise.resolve(undefined); + }); + + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + const payload = makeErc20Permit2Payload(makeValidErc20Extension()); + const result = await facilitator.verify(payload, erc20Requirements, makeErc20Context()); + + if (!result.isValid) { + expect(result.invalidReason).not.toBe("permit2_allowance_required"); + } + }); + + it("should reject when calldata targets wrong address (not PERMIT2_ADDRESS)", async () => { + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const wrongSpenderCalldata = + "0x095ea7b3" + + "0000000000000000000000000000000000000000000000000000000000000001" + // wrong spender + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: wrongSpenderCalldata as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + const payload = makeErc20Permit2Payload(makeValidErc20Extension()); + const result = await facilitator.verify(payload, erc20Requirements, makeErc20Context()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("erc20_approval_tx_wrong_spender"); + }); + + it("Path 2 simulation: should accept when extension signer simulateTransactions returns true", async () => { + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const mockSimulateTransactions = vi.fn().mockResolvedValue(true); + + const mockContext = { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, + signer: { + ...mockFacilitatorSigner, + sendTransactions: vi.fn(), + simulateTransactions: mockSimulateTransactions, + }, + }; + } + return undefined; + }), + }; + + const payload = makeErc20Permit2Payload(makeValidErc20Extension()); + const result = await facilitator.verify(payload, erc20Requirements, mockContext); + + expect(mockSimulateTransactions).toHaveBeenCalledOnce(); + const bundle = mockSimulateTransactions.mock.calls[0][0]; + expect(bundle[0]).toBe(MOCK_SIGNED_TX); + expect(bundle[1]).toHaveProperty("to"); + expect(bundle[1]).toHaveProperty("data"); + expect(result.isValid).toBe(true); + }); + + it("Path 2 simulation: should reject with diagnostic reason when simulateTransactions returns false", async () => { + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + // diagnostic multicall: proxy deployed, balance insufficient + return Promise.resolve([ + { + success: true, + returnData: "0x000000000000000000000000000000000022D473030F116dDEE9F6B43aC78BA3", + }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000001", + }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + ]); + } + return Promise.resolve(undefined); + }); + + const mockSimulateTransactions = vi.fn().mockResolvedValue(false); + + const mockContext = { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, + signer: { + ...mockFacilitatorSigner, + sendTransactions: vi.fn(), + simulateTransactions: mockSimulateTransactions, + }, + }; + } + return undefined; + }), + }; + + const payload = makeErc20Permit2Payload(makeValidErc20Extension()); + const result = await facilitator.verify(payload, erc20Requirements, mockContext); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(Errors.ErrPermit2InsufficientBalance); + }); + + it("Path 2 simulation: should fall back to checkPermit2Prerequisites when simulateTransactions is absent", async () => { + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + // prerequisites pass: proxy deployed + sufficient token balance + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { + success: true, + returnData: "0x000000000000000000000000000000000022D473030F116dDEE9F6B43aC78BA3", + }, + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + ]); + } + return Promise.resolve(undefined); + }); + + // signer has sendTransactions but no simulateTransactions (legacy) + const mockContext = { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, + signer: { + ...mockFacilitatorSigner, + sendTransactions: vi.fn(), + }, + }; + } + return undefined; + }), + }; + + const payload = makeErc20Permit2Payload(makeValidErc20Extension()); + const result = await facilitator.verify(payload, erc20Requirements, mockContext); + + expect(result.isValid).toBe(true); + }); + }); + + describe("ERC-20 Approval Gas Sponsoring - Settlement", () => { + const PAYER = "0x1234567890123456789012345678901234567890" as `0x${string}`; + const TOKEN_ADDRESS = "0xeED520980fC7C7B4eB379B96d61CEdea2423005a" as `0x${string}`; + const MOCK_SIGNED_TX = "0x02f8ab0102030405060708" as `0x${string}`; + + const APPROVE_CALLDATA = + `0x095ea7b3000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3` + + `ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`; + + const erc20Requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000", + asset: TOKEN_ADDRESS, + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 60, + extra: { assetTransferMethod: "permit2" }, + }; + + function makeErc20Permit2Payload(extensions?: Record): PaymentPayload { + const now = Math.floor(Date.now() / 1000); + return { + x402Version: 2, + payload: { + signature: "0x" + "ab".repeat(32) + "cd".repeat(32) + "1b", + permit2Authorization: { + from: PAYER, + permitted: { + token: TOKEN_ADDRESS, + amount: erc20Requirements.amount, + }, + spender: x402ExactPermit2ProxyAddress, + nonce: "99999", + deadline: (now + 300).toString(), + witness: { + to: erc20Requirements.payTo, + validAfter: "0", + }, + }, + }, + accepted: erc20Requirements, + resource: { url: "https://test.com", description: "", mimeType: "" }, + ...(extensions ? { extensions } : {}), + }; + } + + function makeValidErc20Extension() { + return { + erc20ApprovalGasSponsoring: { + info: { + from: PAYER, + asset: TOKEN_ADDRESS, + spender: PERMIT2_ADDRESS, + amount: + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }; + } + + it("should broadcast approval tx via extension signer then settle via extension signer", async () => { + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + // settle's re-verify has simulate=false, so no simulation calls + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const SETTLE_TX_HASH = "0xsettle_tx_hash_mock" as `0x${string}`; + const mockSendTransactions = vi.fn().mockResolvedValue([SETTLE_TX_HASH]); + const mockExtWaitForReceipt = vi.fn().mockResolvedValue({ status: "success" }); + + // Extension signer has all FacilitatorEvmSigner methods + sendTransactions + const mockContext = { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, + signer: { + getAddresses: vi.fn().mockReturnValue([PAYER]), + readContract: mockFacilitatorSigner.readContract, + verifyTypedData: mockFacilitatorSigner.verifyTypedData, + writeContract: vi.fn(), + sendTransaction: vi.fn(), + waitForTransactionReceipt: mockExtWaitForReceipt, + getCode: vi.fn().mockResolvedValue("0x"), + sendTransactions: mockSendTransactions, + }, + }; + } + return undefined; + }), + }; + + const payload = makeErc20Permit2Payload(makeValidErc20Extension()); + const result = await facilitator.settle(payload, erc20Requirements, mockContext); + + // Extension signer called sendTransactions with [approvalTx, settleCall] + expect(mockSendTransactions).toHaveBeenCalled(); + const transactions = mockSendTransactions.mock.calls[0][0]; + expect(transactions[0]).toBe(MOCK_SIGNED_TX); + expect(transactions[1]).toHaveProperty("to"); + expect(transactions[1]).toHaveProperty("data"); + + // Base signer's writeContract should NOT have been called + expect(mockFacilitatorSigner.writeContract).not.toHaveBeenCalled(); + + expect(result.success).toBe(true); + }); + + it("should resolve extension signer by network when signerForNetwork is present", async () => { + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + // settle's re-verify has simulate=false + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const selectedSignerSendTransactions = vi + .fn() + .mockResolvedValue(["0xsettle_hash" as `0x${string}`]); + const selectedSignerWait = vi.fn().mockResolvedValue({ status: "success" }); + const fallbackSignerSendTransactions = vi.fn(); + + const mockContext = { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key !== ERC20_APPROVAL_GAS_SPONSORING_KEY) return undefined; + return { + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, + signer: { + getAddresses: vi.fn().mockReturnValue([PAYER]), + readContract: mockFacilitatorSigner.readContract, + verifyTypedData: mockFacilitatorSigner.verifyTypedData, + writeContract: vi.fn(), + sendTransaction: vi.fn(), + waitForTransactionReceipt: selectedSignerWait, + getCode: vi.fn().mockResolvedValue("0x"), + sendTransactions: fallbackSignerSendTransactions, + }, + signerForNetwork: (network: string) => { + if (network !== "eip155:84532") return undefined; + return { + getAddresses: vi.fn().mockReturnValue([PAYER]), + readContract: mockFacilitatorSigner.readContract, + verifyTypedData: mockFacilitatorSigner.verifyTypedData, + writeContract: vi.fn(), + sendTransaction: vi.fn(), + waitForTransactionReceipt: selectedSignerWait, + getCode: vi.fn().mockResolvedValue("0x"), + sendTransactions: selectedSignerSendTransactions, + }; + }, + }; + }), + }; + + const payload = makeErc20Permit2Payload(makeValidErc20Extension()); + await facilitator.settle(payload, erc20Requirements, mockContext); + + expect(selectedSignerSendTransactions).toHaveBeenCalled(); + expect(fallbackSignerSendTransactions).not.toHaveBeenCalled(); + }); + }); }); diff --git a/typescript/packages/mechanisms/evm/test/unit/server.test.ts b/typescript/packages/mechanisms/evm/test/unit/server.test.ts index 892abfd..dec6b5f 100644 --- a/typescript/packages/mechanisms/evm/test/unit/server.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/server.test.ts @@ -13,6 +13,7 @@ describe("ExactEvmScheme (Server)", () => { expect(result.amount).toBe("100000"); // 0.10 USDC = 100000 smallest units expect(result.asset).toBe("0x036CbD53842c5426634e7929541eC2318f3dCF7e"); expect(result.extra).toEqual({ name: "USDC", version: "2" }); + expect(result.extra).not.toHaveProperty("assetTransferMethod"); }); it("should parse simple number string prices", async () => { @@ -51,6 +52,28 @@ describe("ExactEvmScheme (Server)", () => { expect(result.asset).toBe("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"); expect(result.amount).toBe("1000000"); expect(result.extra).toEqual({ name: "USD Coin", version: "2" }); + expect(result.extra).not.toHaveProperty("assetTransferMethod"); + }); + }); + + describe("MegaETH network", () => { + const network = "eip155:4326"; + + it("should parse dollar string and include assetTransferMethod permit2", async () => { + const result = await server.parsePrice("$0.10", network); + expect(result.asset).toBe("0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7"); + expect(result.amount).toBe("100000000000000000"); // 0.10 * 10^18 + expect(result.extra).toEqual({ + name: "MegaUSD", + version: "1", + assetTransferMethod: "permit2", + }); + }); + + it("should produce correct 18-decimal amount", async () => { + const result = await server.parsePrice("1.00", network); + expect(result.amount).toBe("1000000000000000000"); // 1.00 * 10^18 + expect(result.extra).toHaveProperty("assetTransferMethod", "permit2"); }); }); diff --git a/typescript/packages/mechanisms/evm/test/unit/signer.test.ts b/typescript/packages/mechanisms/evm/test/unit/signer.test.ts index 649cbaf..2eaf573 100644 --- a/typescript/packages/mechanisms/evm/test/unit/signer.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/signer.test.ts @@ -4,15 +4,42 @@ import type { ClientEvmSigner } from "../../src/signer"; describe("EVM Signer Converters", () => { describe("toClientEvmSigner", () => { - it("should return the same signer (identity function)", () => { + it("should return a composed signer when signer already has readContract", () => { const mockSigner: ClientEvmSigner = { address: "0x1234567890123456789012345678901234567890", signTypedData: async () => "0xsignature" as `0x${string}`, + readContract: async () => BigInt(0), }; const result = toClientEvmSigner(mockSigner); - expect(result).toBe(mockSigner); expect(result.address).toBe(mockSigner.address); + expect(result.readContract).toBeDefined(); + }); + + it("should compose a signer with readContract from publicClient", () => { + const mockAccount = { + address: "0x1234567890123456789012345678901234567890" as `0x${string}`, + signTypedData: async () => "0xsignature" as `0x${string}`, + }; + + const mockPublicClient = { + readContract: async () => BigInt(42), + }; + + const result = toClientEvmSigner(mockAccount, mockPublicClient); + expect(result.address).toBe(mockAccount.address); + expect(result.readContract).toBeDefined(); + }); + + it("should return minimal signer when no readContract exists", () => { + const mockAccount = { + address: "0x1234567890123456789012345678901234567890" as `0x${string}`, + signTypedData: async () => "0xsignature" as `0x${string}`, + }; + + const result = toClientEvmSigner(mockAccount); + expect(result.address).toBe(mockAccount.address); + expect(result.readContract).toBeUndefined(); }); }); diff --git a/typescript/packages/mechanisms/evm/test/unit/types.test.ts b/typescript/packages/mechanisms/evm/test/unit/types.test.ts index 15f46b0..77f07b0 100644 --- a/typescript/packages/mechanisms/evm/test/unit/types.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/types.test.ts @@ -78,7 +78,6 @@ describe("EVM Types", () => { witness: { to: "0x9876543210987654321098765432109876543210", validAfter: "1234567000", - extra: "0x", }, }, }; @@ -116,7 +115,6 @@ describe("EVM Types", () => { witness: { to: "0x9876543210987654321098765432109876543210", validAfter: "1234567000", - extra: "0x", }, }, }; diff --git a/typescript/packages/mechanisms/evm/test/unit/upto/client.test.ts b/typescript/packages/mechanisms/evm/test/unit/upto/client.test.ts new file mode 100644 index 0000000..e0e3099 --- /dev/null +++ b/typescript/packages/mechanisms/evm/test/unit/upto/client.test.ts @@ -0,0 +1,305 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { UptoEvmScheme } from "../../../src/upto/client/scheme"; +import { + createPermit2ApprovalTx, + getPermit2AllowanceReadParams, +} from "../../../src/upto/client/permit2"; +import { createUptoPermit2Payload } from "../../../src/upto/client/permit2"; +import type { ClientEvmSigner } from "../../../src/signer"; +import { PaymentRequirements } from "@x402/core/types"; +import { PERMIT2_ADDRESS, x402UptoPermit2ProxyAddress } from "../../../src/constants"; +import { isUptoPermit2Payload } from "../../../src/types"; + +const FACILITATOR_ADDRESS = "0xFAC11174700123456789012345678901234aBCDe" as `0x${string}`; + +describe("UptoEvmScheme (Client)", () => { + let client: UptoEvmScheme; + let mockSigner: ClientEvmSigner; + + beforeEach(() => { + mockSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xmocksignature123456789"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), + }; + client = new UptoEvmScheme(mockSigner); + }); + + function makeRequirements(overrides?: Partial): PaymentRequirements { + return { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { assetTransferMethod: "permit2", facilitatorAddress: FACILITATOR_ADDRESS }, + ...overrides, + }; + } + + describe("Construction", () => { + it("should create instance with signer", () => { + expect(client).toBeDefined(); + expect(client.scheme).toBe("upto"); + }); + }); + + describe("createPaymentPayload", () => { + it("should create Permit2 payload with correct structure", async () => { + const result = await client.createPaymentPayload(2, makeRequirements()); + const payload = result.payload; + + expect(result.x402Version).toBe(2); + expect(payload.signature).toBeDefined(); + expect(payload.permit2Authorization).toBeDefined(); + expect(isUptoPermit2Payload(payload)).toBe(true); + }); + + it("should set spender to x402UptoPermit2ProxyAddress", async () => { + const result = await client.createPaymentPayload(2, makeRequirements()); + + expect(result.payload.permit2Authorization.spender).toBe(x402UptoPermit2ProxyAddress); + }); + + it("should set witness.to to payTo address", async () => { + const payToAddress = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0"; + const result = await client.createPaymentPayload(2, makeRequirements()); + + expect(result.payload.permit2Authorization.witness.to.toLowerCase()).toBe( + payToAddress.toLowerCase(), + ); + }); + + it("should set witness.facilitator to facilitatorAddress from extra", async () => { + const result = await client.createPaymentPayload(2, makeRequirements()); + + expect(result.payload.permit2Authorization.witness.facilitator.toLowerCase()).toBe( + FACILITATOR_ADDRESS.toLowerCase(), + ); + }); + + it("should throw if facilitatorAddress is missing from extra", async () => { + const requirements = makeRequirements({ + extra: { assetTransferMethod: "permit2" }, + }); + + await expect(client.createPaymentPayload(2, requirements)).rejects.toThrow( + "upto scheme requires facilitatorAddress", + ); + }); + + it("should use requirements.amount as permitted amount", async () => { + const requirements = makeRequirements({ amount: "2500000" }); + + const result = await client.createPaymentPayload(2, requirements); + + expect(result.payload.permit2Authorization.permitted.amount).toBe("2500000"); + }); + + it("should use signer's address as from", async () => { + const result = await client.createPaymentPayload(2, makeRequirements()); + + expect(result.payload.permit2Authorization.from).toBe(mockSigner.address); + }); + + it("should use Permit2 EIP-712 domain for signing", async () => { + await client.createPaymentPayload(2, makeRequirements()); + + const callArgs = (mockSigner.signTypedData as ReturnType).mock.calls[0][0]; + expect(callArgs.domain.name).toBe("Permit2"); + expect(callArgs.domain.verifyingContract).toBe(PERMIT2_ADDRESS); + expect(callArgs.primaryType).toBe("PermitWitnessTransferFrom"); + }); + + it("should use uptoPermit2WitnessTypes with facilitator in Witness", async () => { + await client.createPaymentPayload(2, makeRequirements()); + + const callArgs = (mockSigner.signTypedData as ReturnType).mock.calls[0][0]; + const witnessType = callArgs.types.Witness; + expect(witnessType).toEqual([ + { name: "to", type: "address" }, + { name: "facilitator", type: "address" }, + { name: "validAfter", type: "uint256" }, + ]); + }); + + it("should set deadline in the future based on maxTimeoutSeconds", async () => { + const fakeNow = 1700000000000; + vi.useFakeTimers(); + vi.setSystemTime(fakeNow); + + try { + const requirements = makeRequirements({ maxTimeoutSeconds: 600 }); + const result = await client.createPaymentPayload(2, requirements); + const deadline = parseInt(result.payload.permit2Authorization.deadline); + const expectedDeadline = Math.floor(fakeNow / 1000) + 600; + + expect(deadline).toBe(expectedDeadline); + } finally { + vi.useRealTimers(); + } + }); + + it("should set validAfter to 10 minutes before current time", async () => { + const fakeNow = 1700000000000; + vi.useFakeTimers(); + vi.setSystemTime(fakeNow); + + try { + const result = await client.createPaymentPayload(2, makeRequirements()); + const validAfter = parseInt(result.payload.permit2Authorization.witness.validAfter); + const expectedValidAfter = Math.floor(fakeNow / 1000) - 600; + + expect(validAfter).toBe(expectedValidAfter); + } finally { + vi.useRealTimers(); + } + }); + + it("should generate unique nonces across calls", async () => { + const requirements = makeRequirements(); + + const result1 = await client.createPaymentPayload(2, requirements); + const result2 = await client.createPaymentPayload(2, requirements); + + expect(result1.payload.permit2Authorization.nonce).not.toBe( + result2.payload.permit2Authorization.nonce, + ); + }); + + it("should handle different networks", async () => { + const ethereumRequirements = makeRequirements({ + network: "eip155:1", + asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + }); + + const result = await client.createPaymentPayload(2, ethereumRequirements); + + expect(result.x402Version).toBe(2); + expect(result.payload.permit2Authorization).toBeDefined(); + }); + + it("should call signTypedData on signer", async () => { + const result = await client.createPaymentPayload(2, makeRequirements()); + + expect(mockSigner.signTypedData).toHaveBeenCalled(); + expect(result.payload.signature).toBeDefined(); + }); + }); +}); + +describe("Permit2 Approval Helpers", () => { + describe("createPermit2ApprovalTx", () => { + it("should create approval transaction data", () => { + const tokenAddress = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as `0x${string}`; + const tx = createPermit2ApprovalTx(tokenAddress); + + expect(tx.to.toLowerCase()).toBe(tokenAddress.toLowerCase()); + expect(tx.data).toBeDefined(); + expect(tx.data).toMatch(/^0x/); + }); + + it("should encode approve function call", () => { + const tokenAddress = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as `0x${string}`; + const tx = createPermit2ApprovalTx(tokenAddress); + + // approve(address,uint256) selector is 0x095ea7b3 + expect(tx.data.startsWith("0x095ea7b3")).toBe(true); + }); + }); + + describe("getPermit2AllowanceReadParams", () => { + it("should return correct read parameters", () => { + const params = getPermit2AllowanceReadParams({ + tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + ownerAddress: "0x1234567890123456789012345678901234567890", + }); + + expect(params.address.toLowerCase()).toBe( + "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913".toLowerCase(), + ); + expect(params.functionName).toBe("allowance"); + expect(params.args[0].toLowerCase()).toBe( + "0x1234567890123456789012345678901234567890".toLowerCase(), + ); + expect(params.args[1]).toBe(PERMIT2_ADDRESS); + }); + + it("should include allowance ABI", () => { + const params = getPermit2AllowanceReadParams({ + tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + ownerAddress: "0x1234567890123456789012345678901234567890", + }); + + expect(params.abi).toBeDefined(); + expect(params.abi[0].name).toBe("allowance"); + }); + }); +}); + +describe("createUptoPermit2Payload (direct)", () => { + let mockSigner: ClientEvmSigner; + + beforeEach(() => { + mockSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xmocksignature123456789"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), + }; + }); + + it("should throw when facilitatorAddress is missing from extra", async () => { + const requirements: PaymentRequirements = { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { assetTransferMethod: "permit2" }, + }; + + await expect(createUptoPermit2Payload(mockSigner, 2, requirements)).rejects.toThrow( + "upto scheme requires facilitatorAddress", + ); + }); + + it("should throw when extra is undefined", async () => { + const requirements: PaymentRequirements = { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + }; + + await expect(createUptoPermit2Payload(mockSigner, 2, requirements)).rejects.toThrow( + "upto scheme requires facilitatorAddress", + ); + }); + + it("should succeed when facilitatorAddress is provided", async () => { + const requirements: PaymentRequirements = { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { + assetTransferMethod: "permit2", + facilitatorAddress: "0xFAC11174700123456789012345678901234aBCDe", + }, + }; + + const result = await createUptoPermit2Payload(mockSigner, 2, requirements); + + expect(result.x402Version).toBe(2); + expect(result.payload.signature).toBeDefined(); + expect(result.payload.permit2Authorization.witness.facilitator.toLowerCase()).toBe( + "0xFAC11174700123456789012345678901234aBCDe".toLowerCase(), + ); + }); +}); diff --git a/typescript/packages/mechanisms/evm/test/unit/upto/facilitator.test.ts b/typescript/packages/mechanisms/evm/test/unit/upto/facilitator.test.ts new file mode 100644 index 0000000..315fb8f --- /dev/null +++ b/typescript/packages/mechanisms/evm/test/unit/upto/facilitator.test.ts @@ -0,0 +1,908 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { UptoEvmScheme } from "../../../src/upto/facilitator/scheme"; +import { verifyUptoPermit2, settleUptoPermit2 } from "../../../src/upto/facilitator/permit2"; +import type { FacilitatorEvmSigner } from "../../../src/signer"; +import { PaymentPayload, PaymentRequirements } from "@x402/core/types"; +import { x402UptoPermit2ProxyAddress } from "../../../src/constants"; +import { + ErrPermit2AmountMismatch, + ErrUptoAmountExceedsPermitted, + ErrUptoFacilitatorMismatch, + ErrUptoSettlementExceedsAmount, + ErrUptoUnauthorizedFacilitator, + ErrUptoInvalidScheme, + ErrUptoNetworkMismatch, +} from "../../../src/upto/facilitator/errors"; +import type { UptoPermit2Payload } from "../../../src/types"; +import { ERC20_APPROVAL_GAS_SPONSORING_KEY } from "../../../src/upto/extensions"; + +vi.mock("viem", async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + parseTransaction: vi.fn(), + recoverTransactionAddress: vi.fn(), + }; +}); + +const FACILITATOR_ADDRESS = "0xFAC11174700123456789012345678901234aBCDe" as `0x${string}`; + +const now = () => Math.floor(Date.now() / 1000); + +function makePermit2Payload(overrides?: Partial): UptoPermit2Payload { + const base: UptoPermit2Payload = { + signature: "0xmocksig" as `0x${string}`, + permit2Authorization: { + from: "0x1234567890123456789012345678901234567890", + permitted: { + token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + amount: "1000000", + }, + spender: x402UptoPermit2ProxyAddress, + nonce: "12345", + deadline: (now() + 3600).toString(), + witness: { + to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + facilitator: FACILITATOR_ADDRESS, + validAfter: (now() - 600).toString(), + }, + }, + }; + return { ...base, ...overrides }; +} + +function makePayload( + permit2?: UptoPermit2Payload, + acceptedOverrides?: Record, +): PaymentPayload { + const p2 = permit2 ?? makePermit2Payload(); + return { + x402Version: 2, + accepted: { scheme: "upto", network: "eip155:8453", ...acceptedOverrides }, + payload: p2, + } as PaymentPayload; +} + +function makeRequirements(overrides?: Partial): PaymentRequirements { + return { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { assetTransferMethod: "permit2", facilitatorAddress: FACILITATOR_ADDRESS }, + ...overrides, + }; +} + +describe("UptoEvmScheme (Facilitator)", () => { + let mockSigner: FacilitatorEvmSigner; + let scheme: UptoEvmScheme; + + beforeEach(() => { + mockSigner = { + getAddresses: () => [FACILITATOR_ADDRESS], + readContract: vi.fn().mockResolvedValue(BigInt("999999999999999999")), + verifyTypedData: vi.fn().mockResolvedValue(true), + writeContract: vi.fn().mockResolvedValue("0xtxhash1234" as `0x${string}`), + sendTransaction: vi.fn(), + waitForTransactionReceipt: vi.fn().mockResolvedValue({ status: "success" }), + getCode: vi.fn(), + }; + scheme = new UptoEvmScheme(mockSigner); + }); + + describe("Construction", () => { + it("should create instance with scheme=upto", () => { + expect(scheme).toBeDefined(); + expect(scheme.scheme).toBe("upto"); + }); + }); + + describe("getExtra", () => { + it("should return facilitatorAddress from signer", () => { + const extra = scheme.getExtra("eip155:8453"); + expect(extra).toEqual({ facilitatorAddress: FACILITATOR_ADDRESS }); + }); + }); + + describe("verify", () => { + it("should return isValid=true for a valid payload", async () => { + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(true); + expect(result.payer).toBe("0x1234567890123456789012345678901234567890"); + expect(mockSigner.verifyTypedData).toHaveBeenCalled(); + }); + + it("should verify with uptoPermit2WitnessTypes containing facilitator", async () => { + await scheme.verify(makePayload(), makeRequirements()); + + const callArgs = (mockSigner.verifyTypedData as ReturnType).mock.calls[0][0]; + const witnessType = callArgs.types.Witness; + expect(witnessType).toEqual([ + { name: "to", type: "address" }, + { name: "facilitator", type: "address" }, + { name: "validAfter", type: "uint256" }, + ]); + }); + + it("should reject if scheme is not upto", async () => { + const payload = makePayload(undefined, { scheme: "exact" }); + const requirements = makeRequirements({ scheme: "exact" as any }); + + const result = await scheme.verify(payload, requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(ErrUptoInvalidScheme); + }); + + it("should reject if network mismatches", async () => { + const payload = makePayload(undefined, { network: "eip155:1" }); + const requirements = makeRequirements({ network: "eip155:8453" as any }); + + const result = await scheme.verify(payload, requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(ErrUptoNetworkMismatch); + }); + + it("should reject if spender is not x402UptoPermit2ProxyAddress", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.spender = "0x0000000000000000000000000000000000000001"; + const payload = makePayload(p2); + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_permit2_spender"); + }); + + it("should reject if facilitator in witness does not match signer", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.witness.facilitator = "0x0000000000000000000000000000000000000099"; + const payload = makePayload(p2); + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(ErrUptoFacilitatorMismatch); + }); + + it("should reject if deadline is expired", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.deadline = "1"; + const payload = makePayload(p2); + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("permit2_deadline_expired"); + }); + + it("should reject if validAfter is in the future", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.witness.validAfter = (now() + 3600).toString(); + const payload = makePayload(p2); + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("permit2_not_yet_valid"); + }); + + it("should reject if token mismatches", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.permitted.token = "0x0000000000000000000000000000000000000099"; + const payload = makePayload(p2); + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("permit2_token_mismatch"); + }); + + it("should reject if witness.to doesn't match payTo", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.witness.to = "0x0000000000000000000000000000000000000001"; + const payload = makePayload(p2); + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_permit2_recipient_mismatch"); + }); + + it("should PASS when permitted.amount equals requirements.amount", async () => { + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(true); + }); + + it("should FAIL when permitted.amount !== requirements.amount (too low)", async () => { + const requirements = makeRequirements({ amount: "5000000" }); + + const result = await scheme.verify(makePayload(), requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(ErrPermit2AmountMismatch); + }); + + it("should FAIL when permitted.amount !== requirements.amount (too high)", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.permitted.amount = "2000000"; + const result = await scheme.verify(makePayload(p2), makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(ErrPermit2AmountMismatch); + }); + + it("should reject if signature is invalid", async () => { + mockSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_permit2_signature"); + }); + + it("should reject non-Permit2 payload via scheme wrapper with unsupported_payload_type", async () => { + const payload: PaymentPayload = { + x402Version: 2, + accepted: { scheme: "upto", network: "eip155:8453" }, + payload: { + authorization: { + from: "0x1234567890123456789012345678901234567890", + to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + value: "1000000", + validAfter: "0", + validBefore: "999999999999", + nonce: "0x00", + }, + signature: "0x", + }, + } as PaymentPayload; + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("unsupported_payload_type"); + }); + }); + + describe("settle", () => { + it("should settle successfully and return tx hash", async () => { + const result = await scheme.settle(makePayload(), makeRequirements()); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("0xtxhash1234"); + expect(result.payer).toBe("0x1234567890123456789012345678901234567890"); + expect(mockSigner.writeContract).toHaveBeenCalled(); + }); + + it("should pass settlement amount to settle call", async () => { + await scheme.settle(makePayload(), makeRequirements({ amount: "500000" })); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + expect(writeCall.functionName).toBe("settle"); + // args: [permit, amount, owner, witness, signature] + expect(writeCall.args[1]).toBe(BigInt("500000")); + }); + + it("should include facilitator in witness for settle call", async () => { + await scheme.settle(makePayload(), makeRequirements()); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + // args[3] is the witness struct + expect(writeCall.args[3].facilitator.toLowerCase()).toBe(FACILITATOR_ADDRESS.toLowerCase()); + }); + + it("should return success with empty tx for zero settlement amount", async () => { + const requirements = makeRequirements({ amount: "0" }); + + const result = await scheme.settle(makePayload(), requirements); + + expect(result.success).toBe(true); + expect(result.transaction).toBe(""); + expect(mockSigner.writeContract).not.toHaveBeenCalled(); + }); + + it("should succeed when settlement amount < permitted amount (upto core feature)", async () => { + const result = await scheme.settle(makePayload(), makeRequirements({ amount: "500000" })); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("0xtxhash1234"); + expect(mockSigner.writeContract).toHaveBeenCalled(); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + expect(writeCall.functionName).toBe("settle"); + expect(writeCall.args[1]).toBe(BigInt("500000")); + }); + + it("should fail when settlement exceeds permitted amount", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.permitted.amount = "1000000"; + const payload = makePayload(p2); + const requirements = makeRequirements({ amount: "2000000" }); + + const result = await scheme.settle(payload, requirements); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe(ErrUptoSettlementExceedsAmount); + }); + + it("should reject non-Permit2 payload via scheme wrapper with unsupported_payload_type", async () => { + const payload: PaymentPayload = { + x402Version: 2, + accepted: { scheme: "upto", network: "eip155:8453" }, + payload: { + authorization: { + from: "0x1234567890123456789012345678901234567890", + to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + value: "1000000", + validAfter: "0", + validBefore: "999999999999", + nonce: "0x00", + }, + signature: "0x", + }, + } as PaymentPayload; + + const result = await scheme.settle(payload, makeRequirements()); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe("unsupported_payload_type"); + }); + }); + + describe("settle error mapping", () => { + it("should map Permit2612AmountMismatch revert", async () => { + mockSigner.writeContract = vi + .fn() + .mockRejectedValue(new Error("execution reverted: Permit2612AmountMismatch()")); + + const result = await scheme.settle(makePayload(), makeRequirements()); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe("permit2_2612_amount_mismatch"); + }); + + it("should map InvalidNonce revert", async () => { + mockSigner.writeContract = vi + .fn() + .mockRejectedValue(new Error("execution reverted: InvalidNonce()")); + + const result = await scheme.settle(makePayload(), makeRequirements()); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe("permit2_invalid_nonce"); + }); + + it("should map AmountExceedsPermitted revert", async () => { + mockSigner.writeContract = vi + .fn() + .mockRejectedValue(new Error("execution reverted: AmountExceedsPermitted()")); + + const result = await scheme.settle(makePayload(), makeRequirements()); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe(ErrUptoAmountExceedsPermitted); + }); + + it("should map UnauthorizedFacilitator revert", async () => { + mockSigner.writeContract = vi + .fn() + .mockRejectedValue(new Error("execution reverted: UnauthorizedFacilitator()")); + + const result = await scheme.settle(makePayload(), makeRequirements()); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe(ErrUptoUnauthorizedFacilitator); + }); + }); + + describe("direct function calls (verifyUptoPermit2 / settleUptoPermit2)", () => { + it("verifyUptoPermit2 returns isValid=true for valid input", async () => { + const p2 = makePermit2Payload(); + const result = await verifyUptoPermit2(mockSigner, makePayload(p2), makeRequirements(), p2); + + expect(result.isValid).toBe(true); + }); + + it("settleUptoPermit2 returns success for zero amount", async () => { + const p2 = makePermit2Payload(); + const result = await settleUptoPermit2( + mockSigner, + makePayload(p2), + makeRequirements({ amount: "0" }), + p2, + ); + + expect(result.success).toBe(true); + expect(result.transaction).toBe(""); + expect(result.amount).toBe("0"); + expect(mockSigner.writeContract).not.toHaveBeenCalled(); + }); + + it("settleUptoPermit2 rejects when settlement exceeds permitted", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.permitted.amount = "500000"; + const result = await settleUptoPermit2( + mockSigner, + makePayload(p2), + makeRequirements({ amount: "1000000" }), + p2, + ); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe(ErrUptoSettlementExceedsAmount); + }); + }); + + describe("getSigners", () => { + it("should return facilitator addresses from signer", () => { + const signers = scheme.getSigners("eip155:8453"); + expect(signers).toEqual([FACILITATOR_ADDRESS]); + }); + }); + + describe("verify edge cases", () => { + it("should handle verifyTypedData throwing an exception", async () => { + mockSigner.verifyTypedData = vi.fn().mockRejectedValue(new Error("RPC unavailable")); + + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_permit2_signature"); + }); + }); + + describe("ERC-6492 / smart contract wallet signature fallback", () => { + it("should reject undeployed EOA with invalid signature", async () => { + mockSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockSigner.getCode = vi.fn().mockResolvedValue("0x"); + + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_permit2_signature"); + }); + + it("should fall through to simulation for deployed smart contract when verifyTypedData returns false", async () => { + mockSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockSigner.getCode = vi.fn().mockResolvedValue("0x608060405234"); + + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(true); + }); + + it("should fall through to simulation for deployed smart contract when verifyTypedData throws", async () => { + mockSigner.verifyTypedData = vi.fn().mockRejectedValue(new Error("unsupported")); + mockSigner.getCode = vi.fn().mockResolvedValue("0x608060405234"); + + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(true); + }); + + it("should reject undeployed contract when verifyTypedData throws", async () => { + mockSigner.verifyTypedData = vi.fn().mockRejectedValue(new Error("unsupported")); + mockSigner.getCode = vi.fn().mockResolvedValue("0x"); + + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_permit2_signature"); + }); + }); + + describe("settle receipt handling", () => { + it("should fail when transaction receipt returns reverted status", async () => { + mockSigner.waitForTransactionReceipt = vi.fn().mockResolvedValue({ status: "reverted" }); + + const result = await scheme.settle(makePayload(), makeRequirements()); + + expect(result.success).toBe(false); + }); + }); + + describe("EIP-2612 Gas Sponsoring - Settlement", () => { + const eip2612Requirements = makeRequirements(); + + function makeEip2612Extension() { + const ts = Math.floor(Date.now() / 1000); + return { + eip2612GasSponsoring: { + info: { + from: "0x1234567890123456789012345678901234567890", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + nonce: "0", + deadline: (ts + 300).toString(), + signature: "0x" + "ab".repeat(32) + "cd".repeat(32) + "1b", + version: "1", + }, + schema: {}, + }, + }; + } + + function makePayloadWithExtensions(extensions?: Record): PaymentPayload { + const p2 = makePermit2Payload(); + return { + x402Version: 2, + accepted: { scheme: "upto", network: "eip155:8453" }, + payload: p2, + resource: { url: "https://test.com", description: "", mimeType: "" }, + ...(extensions ? { extensions } : {}), + } as PaymentPayload; + } + + it("should call settleWithPermit when EIP-2612 extension is present", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makePayloadWithExtensions(makeEip2612Extension()); + const result = await scheme.settle(payload, eip2612Requirements); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("0xtxhash1234"); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + expect(writeCall.functionName).toBe("settleWithPermit"); + }); + + it("should call settle (not settleWithPermit) when no EIP-2612 extension", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(BigInt("999999999999999999")); + + const payload = makePayloadWithExtensions(); + const result = await scheme.settle(payload, eip2612Requirements); + + expect(result.success).toBe(true); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + expect(writeCall.functionName).toBe("settle"); + }); + + it("should pass correct EIP-2612 permit struct to settleWithPermit", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makePayloadWithExtensions(makeEip2612Extension()); + await scheme.settle(payload, eip2612Requirements); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + expect(writeCall.functionName).toBe("settleWithPermit"); + + const permit2612Struct = writeCall.args[0]; + expect(permit2612Struct.value).toBeDefined(); + expect(permit2612Struct.deadline).toBeDefined(); + expect(permit2612Struct.r).toBeDefined(); + expect(permit2612Struct.s).toBeDefined(); + expect(permit2612Struct.v).toBeDefined(); + expect(typeof permit2612Struct.v).toBe("number"); + }); + + it("should include settlement amount in settleWithPermit args", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makePayloadWithExtensions(makeEip2612Extension()); + await scheme.settle(payload, makeRequirements({ amount: "500000" })); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + expect(writeCall.functionName).toBe("settleWithPermit"); + // settleWithPermit args: [permit2612Struct, permit, amount, owner, witness, signature] + expect(writeCall.args[2]).toBe(BigInt("500000")); + }); + }); + + describe("ERC-20 Approval Gas Sponsoring - Verify", () => { + const PAYER = "0x1234567890123456789012345678901234567890" as `0x${string}`; + const TOKEN_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as `0x${string}`; + const MOCK_SIGNED_TX = "0x02f8ab0102030405060708" as `0x${string}`; + + const APPROVE_CALLDATA = + `0x095ea7b3000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3` + + `ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`; + + const erc20VerifyRequirements: PaymentRequirements = { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: TOKEN_ADDRESS, + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { assetTransferMethod: "permit2", facilitatorAddress: FACILITATOR_ADDRESS }, + }; + + function makeErc20UptoPayload(extensions?: Record): PaymentPayload { + const ts = Math.floor(Date.now() / 1000); + return { + x402Version: 2, + payload: { + signature: "0x" + "ab".repeat(32) + "cd".repeat(32) + "1b", + permit2Authorization: { + from: PAYER, + permitted: { + token: TOKEN_ADDRESS, + amount: erc20VerifyRequirements.amount, + }, + spender: x402UptoPermit2ProxyAddress, + nonce: "99999", + deadline: (ts + 300).toString(), + witness: { + to: erc20VerifyRequirements.payTo, + facilitator: FACILITATOR_ADDRESS, + validAfter: (ts - 600).toString(), + }, + }, + } as UptoPermit2Payload, + accepted: { scheme: "upto", network: "eip155:8453" }, + resource: { url: "https://test.com", description: "", mimeType: "" }, + ...(extensions ? { extensions } : {}), + } as PaymentPayload; + } + + function makeValidErc20Extension() { + return { + erc20ApprovalGasSponsoring: { + info: { + from: PAYER, + asset: TOKEN_ADDRESS, + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }; + } + + function makeErc20Context() { + return { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { key: ERC20_APPROVAL_GAS_SPONSORING_KEY }; + } + return undefined; + }), + }; + } + + it("should reject when ERC-20 extension has invalid format (bad address)", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makeErc20UptoPayload({ + erc20ApprovalGasSponsoring: { + info: { + from: "not-an-address", + asset: TOKEN_ADDRESS, + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "100", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }); + + const result = await scheme.verify(payload, erc20VerifyRequirements, makeErc20Context()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_erc20_approval_extension_format"); + }); + + it("should reject when ERC-20 extension from doesn't match payer", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makeErc20UptoPayload({ + erc20ApprovalGasSponsoring: { + info: { + from: "0x0000000000000000000000000000000000000001", + asset: TOKEN_ADDRESS, + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "100", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }); + + const result = await scheme.verify(payload, erc20VerifyRequirements, makeErc20Context()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("erc20_approval_from_mismatch"); + }); + + it("should accept when valid ERC-20 extension present and simulation succeeds", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + const mockSimulateTransactions = vi.fn().mockResolvedValue(true); + + const mockContext = { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, + signer: { + ...mockSigner, + sendTransactions: vi.fn(), + simulateTransactions: mockSimulateTransactions, + }, + }; + } + return undefined; + }), + }; + + const result = await scheme.verify( + makeErc20UptoPayload(makeValidErc20Extension()), + erc20VerifyRequirements, + mockContext, + ); + + expect(result.isValid).toBe(true); + }); + }); + + describe("ERC-20 Approval Gas Sponsoring - Settlement", () => { + const PAYER = "0x1234567890123456789012345678901234567890" as `0x${string}`; + const TOKEN_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as `0x${string}`; + const MOCK_SIGNED_TX = "0x02f8ab0102030405060708" as `0x${string}`; + + const APPROVE_CALLDATA = + `0x095ea7b3000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3` + + `ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`; + + const erc20SettleRequirements: PaymentRequirements = { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: TOKEN_ADDRESS, + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { assetTransferMethod: "permit2", facilitatorAddress: FACILITATOR_ADDRESS }, + }; + + function makeErc20UptoPayload(extensions?: Record): PaymentPayload { + const ts = Math.floor(Date.now() / 1000); + return { + x402Version: 2, + payload: { + signature: "0x" + "ab".repeat(32) + "cd".repeat(32) + "1b", + permit2Authorization: { + from: PAYER, + permitted: { + token: TOKEN_ADDRESS, + amount: erc20SettleRequirements.amount, + }, + spender: x402UptoPermit2ProxyAddress, + nonce: "99999", + deadline: (ts + 300).toString(), + witness: { + to: erc20SettleRequirements.payTo, + facilitator: FACILITATOR_ADDRESS, + validAfter: (ts - 600).toString(), + }, + }, + } as UptoPermit2Payload, + accepted: { scheme: "upto", network: "eip155:8453" }, + resource: { url: "https://test.com", description: "", mimeType: "" }, + ...(extensions ? { extensions } : {}), + } as PaymentPayload; + } + + function makeValidErc20Extension() { + return { + erc20ApprovalGasSponsoring: { + info: { + from: PAYER, + asset: TOKEN_ADDRESS, + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }; + } + + function makeErc20SettleContext() { + const SETTLE_TX_HASH = "0xsettle_tx_hash_mock" as `0x${string}`; + const mockSendTransactions = vi.fn().mockResolvedValue([SETTLE_TX_HASH]); + const mockExtWaitForReceipt = vi.fn().mockResolvedValue({ status: "success" }); + + const mockContext = { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, + signer: { + getAddresses: () => [FACILITATOR_ADDRESS], + readContract: mockSigner.readContract, + verifyTypedData: mockSigner.verifyTypedData, + writeContract: vi.fn(), + sendTransaction: vi.fn(), + waitForTransactionReceipt: mockExtWaitForReceipt, + getCode: vi.fn().mockResolvedValue("0x"), + sendTransactions: mockSendTransactions, + }, + }; + } + return undefined; + }), + }; + + return { mockContext, mockSendTransactions }; + } + + it("should broadcast approval tx via extension signer then settle", async () => { + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const { mockContext, mockSendTransactions } = makeErc20SettleContext(); + + const result = await scheme.settle( + makeErc20UptoPayload(makeValidErc20Extension()), + erc20SettleRequirements, + mockContext, + ); + + expect(mockSendTransactions).toHaveBeenCalled(); + const transactions = mockSendTransactions.mock.calls[0][0]; + expect(transactions[0]).toBe(MOCK_SIGNED_TX); + expect(transactions[1]).toHaveProperty("to"); + expect(transactions[1]).toHaveProperty("data"); + + expect(mockSigner.writeContract).not.toHaveBeenCalled(); + + expect(result.success).toBe(true); + }); + + it("should include settlement amount in ERC-20 approval settle response", async () => { + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const { mockContext } = makeErc20SettleContext(); + + const result = await scheme.settle( + makeErc20UptoPayload(makeValidErc20Extension()), + makeRequirements({ + amount: "750000", + asset: TOKEN_ADDRESS, + extra: { assetTransferMethod: "permit2", facilitatorAddress: FACILITATOR_ADDRESS }, + }), + mockContext, + ); + + expect(result.success).toBe(true); + expect(result.amount).toBe("750000"); + }); + }); +}); diff --git a/typescript/packages/mechanisms/evm/test/unit/upto/server.test.ts b/typescript/packages/mechanisms/evm/test/unit/upto/server.test.ts new file mode 100644 index 0000000..938a622 --- /dev/null +++ b/typescript/packages/mechanisms/evm/test/unit/upto/server.test.ts @@ -0,0 +1,247 @@ +import { describe, it, expect } from "vitest"; +import { UptoEvmScheme } from "../../../src/upto/server/scheme"; +import type { PaymentRequirements } from "@x402/core/types"; + +const FACILITATOR_ADDRESS = "0xFAC11174700123456789012345678901234aBCDe"; + +describe("UptoEvmScheme (Server)", () => { + const server = new UptoEvmScheme(); + + describe("parsePrice", () => { + describe("Base Sepolia network", () => { + const network = "eip155:84532"; + + it("should parse dollar string prices", async () => { + const result = await server.parsePrice("$0.10", network); + expect(result.amount).toBe("100000"); + expect(result.asset).toBe("0x036CbD53842c5426634e7929541eC2318f3dCF7e"); + expect(result.extra).toEqual({ + name: "USDC", + version: "2", + assetTransferMethod: "permit2", + }); + }); + + it("should parse simple number string prices", async () => { + const result = await server.parsePrice("0.10", network); + expect(result.amount).toBe("100000"); + expect(result.asset).toBe("0x036CbD53842c5426634e7929541eC2318f3dCF7e"); + }); + + it("should parse number prices", async () => { + const result = await server.parsePrice(0.1, network); + expect(result.amount).toBe("100000"); + }); + + it("should handle larger amounts", async () => { + const result = await server.parsePrice("100.50", network); + expect(result.amount).toBe("100500000"); + }); + + it("should handle whole numbers", async () => { + const result = await server.parsePrice("1", network); + expect(result.amount).toBe("1000000"); + }); + + it("should avoid floating-point rounding error", async () => { + const result = await server.parsePrice("$4.02", network); + expect(result.amount).toBe("4020000"); + }); + + it("should always include assetTransferMethod=permit2 in extra", async () => { + const result = await server.parsePrice("$1.00", network); + expect(result.extra).toHaveProperty("assetTransferMethod", "permit2"); + }); + }); + + describe("Base mainnet network", () => { + const network = "eip155:8453"; + + it("should use Base mainnet USDC address with permit2", async () => { + const result = await server.parsePrice("1.00", network); + expect(result.asset).toBe("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"); + expect(result.amount).toBe("1000000"); + expect(result.extra).toEqual({ + name: "USD Coin", + version: "2", + assetTransferMethod: "permit2", + }); + }); + }); + + describe("MegaETH network", () => { + const network = "eip155:4326"; + + it("should parse dollar string for 18-decimal token", async () => { + const result = await server.parsePrice("$0.10", network); + expect(result.asset).toBe("0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7"); + expect(result.amount).toBe("100000000000000000"); + expect(result.extra).toEqual({ + name: "MegaUSD", + version: "1", + assetTransferMethod: "permit2", + }); + }); + }); + + describe("pre-parsed price objects", () => { + it("should handle pre-parsed price objects with asset", async () => { + const result = await server.parsePrice( + { + amount: "123456", + asset: "0x1234567890123456789012345678901234567890", + extra: { foo: "bar" }, + }, + "eip155:84532", + ); + expect(result.amount).toBe("123456"); + expect(result.asset).toBe("0x1234567890123456789012345678901234567890"); + expect(result.extra).toEqual({ foo: "bar" }); + }); + + it("should throw for price objects without asset", async () => { + await expect( + async () => await server.parsePrice({ amount: "123456" } as never, "eip155:84532"), + ).rejects.toThrow("Asset address must be specified"); + }); + }); + + describe("custom money parser", () => { + it("should use custom parser when it returns a result", async () => { + const customServer = new UptoEvmScheme(); + + customServer.registerMoneyParser(async (amount, network) => { + if (network === "eip155:84532" && amount > 0) { + return { + amount: (amount * 1e18).toString(), + asset: "0xPermit2OnlyToken123456789012345678901234", + extra: { assetTransferMethod: "permit2" }, + }; + } + return null; + }); + + const result = await customServer.parsePrice("1.00", "eip155:84532"); + expect(result.amount).toBe("1000000000000000000"); + expect(result.asset).toBe("0xPermit2OnlyToken123456789012345678901234"); + }); + + it("should fall back to default when custom parser returns null", async () => { + const customServer = new UptoEvmScheme(); + + customServer.registerMoneyParser(async (_amount, network) => { + if (network === "eip155:42161") { + return { amount: "1", asset: "0xArb", extra: {} }; + } + return null; + }); + + const result = await customServer.parsePrice("1.00", "eip155:84532"); + expect(result.asset).toBe("0x036CbD53842c5426634e7929541eC2318f3dCF7e"); + }); + }); + + describe("error cases", () => { + it("should throw for unsupported networks", async () => { + await expect(async () => await server.parsePrice("1.00", "eip155:999999")).rejects.toThrow( + "No default asset configured", + ); + }); + + it("should throw for invalid money formats", async () => { + await expect( + async () => await server.parsePrice("not-a-price!", "eip155:84532"), + ).rejects.toThrow("Invalid money format"); + }); + }); + }); + + describe("enhancePaymentRequirements", () => { + const baseRequirements: PaymentRequirements = { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { name: "USD Coin", version: "2" }, + }; + + it("should always set assetTransferMethod=permit2 in extra", async () => { + const result = await server.enhancePaymentRequirements( + baseRequirements, + { x402Version: 2, scheme: "upto", network: "eip155:8453" }, + [], + ); + + expect(result.extra?.assetTransferMethod).toBe("permit2"); + }); + + it("should inject facilitatorAddress from supportedKind.extra", async () => { + const result = await server.enhancePaymentRequirements( + baseRequirements, + { + x402Version: 2, + scheme: "upto", + network: "eip155:8453", + extra: { facilitatorAddress: FACILITATOR_ADDRESS }, + }, + [], + ); + + expect(result.extra?.facilitatorAddress).toBe(FACILITATOR_ADDRESS); + }); + + it("should preserve existing extra fields", async () => { + const result = await server.enhancePaymentRequirements( + baseRequirements, + { x402Version: 2, scheme: "upto", network: "eip155:8453" }, + [], + ); + + expect(result.extra?.name).toBe("USD Coin"); + expect(result.extra?.version).toBe("2"); + }); + + it("should not include facilitatorAddress when not provided", async () => { + const result = await server.enhancePaymentRequirements( + baseRequirements, + { x402Version: 2, scheme: "upto", network: "eip155:8453" }, + [], + ); + + expect(result.extra?.facilitatorAddress).toBeUndefined(); + }); + + it("should checksum-validate facilitatorAddress via getAddress", async () => { + const lowercaseAddress = "0xfac11174700123456789012345678901234abcde"; + const result = await server.enhancePaymentRequirements( + baseRequirements, + { + x402Version: 2, + scheme: "upto", + network: "eip155:8453", + extra: { facilitatorAddress: lowercaseAddress }, + }, + [], + ); + + expect(result.extra?.facilitatorAddress).toBe("0xFAC11174700123456789012345678901234aBCDe"); + }); + + it("should throw for invalid facilitatorAddress", () => { + expect(() => + server.enhancePaymentRequirements( + baseRequirements, + { + x402Version: 2, + scheme: "upto", + network: "eip155:8453", + extra: { facilitatorAddress: "not-an-address" }, + }, + [], + ), + ).toThrow(); + }); + }); +}); diff --git a/typescript/packages/mechanisms/evm/test/unit/upto/types.test.ts b/typescript/packages/mechanisms/evm/test/unit/upto/types.test.ts new file mode 100644 index 0000000..e27dea1 --- /dev/null +++ b/typescript/packages/mechanisms/evm/test/unit/upto/types.test.ts @@ -0,0 +1,237 @@ +import { describe, it, expect } from "vitest"; +import { isUptoPermit2Payload } from "../../../src/types"; +import { buildUptoPermit2SettleArgs } from "../../../src/shared/permit2"; +import type { UptoPermit2Payload } from "../../../src/types"; +import { getAddress } from "viem"; + +const VALID_PAYLOAD = { + signature: "0xmocksig" as `0x${string}`, + permit2Authorization: { + from: "0x1234567890123456789012345678901234567890", + permitted: { + token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + amount: "1000000", + }, + spender: "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002", + nonce: "12345", + deadline: "1700000000", + witness: { + to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + facilitator: "0xFAC11174700123456789012345678901234aBCDe", + validAfter: "1699999400", + }, + }, +}; + +describe("isUptoPermit2Payload", () => { + it("should return true for a valid payload", () => { + expect(isUptoPermit2Payload(VALID_PAYLOAD)).toBe(true); + }); + + it("should return false when signature is missing", () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { signature: _signature, ...rest } = VALID_PAYLOAD; + expect(isUptoPermit2Payload(rest as Record)).toBe(false); + }); + + it("should return false when signature is not a string", () => { + expect(isUptoPermit2Payload({ ...VALID_PAYLOAD, signature: 123 })).toBe(false); + }); + + it("should return false when permit2Authorization is missing", () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { permit2Authorization: _permit2Authorization, ...rest } = VALID_PAYLOAD; + expect(isUptoPermit2Payload(rest as Record)).toBe(false); + }); + + it("should return false when permit2Authorization is null", () => { + expect(isUptoPermit2Payload({ ...VALID_PAYLOAD, permit2Authorization: null })).toBe(false); + }); + + it("should return false when permit2Authorization is not an object", () => { + expect(isUptoPermit2Payload({ ...VALID_PAYLOAD, permit2Authorization: "bad" })).toBe(false); + }); + + it("should return false when from is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization as Record).from; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when from is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization as Record).from = 42; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when spender is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization as Record).spender; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when nonce is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization as Record).nonce = 12345; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when deadline is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization as Record).deadline = 1700000000; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when permitted is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization as Record).permitted; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when permitted is null", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization as Record).permitted = null; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when permitted.token is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization.permitted as Record).token = 0x833589; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when permitted.amount is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization.permitted as Record).amount = 1000000; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization as Record).witness; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness is null", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization as Record).witness = null; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness.facilitator is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization.witness as Record).facilitator; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness.facilitator is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization.witness as Record).facilitator = 123; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness.to is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization.witness as Record).to; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness.to is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization.witness as Record).to = 42; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness.validAfter is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization.witness as Record).validAfter; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness.validAfter is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization.witness as Record).validAfter = 1699999400; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false for an empty object", () => { + expect(isUptoPermit2Payload({})).toBe(false); + }); + + it("should return false for an exact scheme payload (no facilitator in witness)", () => { + const exactPayload = { + signature: "0xsig", + permit2Authorization: { + from: "0x1234567890123456789012345678901234567890", + permitted: { token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", amount: "1000000" }, + spender: "0x402085c248EeA27D92E8b30b2C58ed07f9E20001", + nonce: "1", + deadline: "999999999", + witness: { to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", validAfter: "0" }, + }, + }; + expect(isUptoPermit2Payload(exactPayload as Record)).toBe(false); + }); +}); + +describe("buildUptoPermit2SettleArgs", () => { + const FACILITATOR = "0xFAC11174700123456789012345678901234aBCDe" as `0x${string}`; + const payload: UptoPermit2Payload = { + signature: "0xdeadbeef" as `0x${string}`, + permit2Authorization: { + from: "0x1234567890123456789012345678901234567890", + permitted: { + token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + amount: "5000000", + }, + spender: "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002", + nonce: "99", + deadline: "1700000000", + witness: { + to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + facilitator: FACILITATOR, + validAfter: "1699999400", + }, + }, + }; + + it("should return a 5-element tuple with correct types", () => { + const args = buildUptoPermit2SettleArgs(payload, 1000000n, FACILITATOR); + expect(args).toHaveLength(5); + }); + + it("should place the settlement amount as the second element", () => { + const args = buildUptoPermit2SettleArgs(payload, 750000n, FACILITATOR); + expect(args[1]).toBe(750000n); + }); + + it("should convert permitted.amount to BigInt", () => { + const args = buildUptoPermit2SettleArgs(payload, 1000000n, FACILITATOR); + expect(args[0].permitted.amount).toBe(5000000n); + }); + + it("should checksum all addresses", () => { + const args = buildUptoPermit2SettleArgs(payload, 1000000n, FACILITATOR); + const checksummedToken = getAddress(payload.permit2Authorization.permitted.token); + const checksummedTo = getAddress(payload.permit2Authorization.witness.to); + const checksummedFacilitator = getAddress(FACILITATOR); + const checksummedFrom = getAddress(payload.permit2Authorization.from); + + expect(args[0].permitted.token).toBe(checksummedToken); + expect(args[2]).toBe(checksummedFrom); + expect(args[3].to).toBe(checksummedTo); + expect(args[3].facilitator).toBe(checksummedFacilitator); + }); + + it("should convert nonce, deadline, and validAfter to BigInt", () => { + const args = buildUptoPermit2SettleArgs(payload, 1000000n, FACILITATOR); + expect(args[0].nonce).toBe(99n); + expect(args[0].deadline).toBe(1700000000n); + expect(args[3].validAfter).toBe(1699999400n); + }); + + it("should pass through the signature unchanged", () => { + const args = buildUptoPermit2SettleArgs(payload, 1000000n, FACILITATOR); + expect(args[4]).toBe("0xdeadbeef"); + }); +}); diff --git a/typescript/packages/mechanisms/evm/test/unit/utils.test.ts b/typescript/packages/mechanisms/evm/test/unit/utils.test.ts index 0490254..af189f1 100644 --- a/typescript/packages/mechanisms/evm/test/unit/utils.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/utils.test.ts @@ -1,81 +1,85 @@ import { describe, it, expect } from "vitest"; import { getEvmChainId, createNonce } from "../../src/utils"; -import { EvmNetworkV1 } from "../../src/v1"; +import { getEvmChainIdV1 } from "../../src/v1"; describe("EVM Utils", () => { - describe("getEvmChainId", () => { - it("should return correct chain ID for Base", () => { - expect(getEvmChainId("base")).toBe(8453); + describe("getEvmChainId (CAIP-2 only)", () => { + it("should return correct chain ID for CAIP-2 Base", () => { + expect(getEvmChainId("eip155:8453")).toBe(8453); }); - it("should return correct chain ID for Base Sepolia", () => { - expect(getEvmChainId("base-sepolia")).toBe(84532); + it("should return correct chain ID for CAIP-2 Base Sepolia", () => { + expect(getEvmChainId("eip155:84532")).toBe(84532); }); - it("should return correct chain ID for Ethereum mainnet", () => { - expect(getEvmChainId("ethereum")).toBe(1); + it("should return correct chain ID for CAIP-2 Ethereum", () => { + expect(getEvmChainId("eip155:1")).toBe(1); }); - it("should return correct chain ID for Sepolia", () => { - expect(getEvmChainId("sepolia")).toBe(11155111); + it("should return correct chain ID for CAIP-2 Polygon", () => { + expect(getEvmChainId("eip155:137")).toBe(137); }); - it("should return correct chain ID for Polygon", () => { - expect(getEvmChainId("polygon")).toBe(137); + it("should return correct chain ID for arbitrary CAIP-2 chain", () => { + expect(getEvmChainId("eip155:999999")).toBe(999999); }); - it("should return correct chain ID for Polygon Amoy", () => { - expect(getEvmChainId("polygon-amoy")).toBe(80002); + it("should throw for legacy network names", () => { + expect(() => getEvmChainId("base")).toThrow(); }); - it("should return correct chain ID for Abstract", () => { - expect(getEvmChainId("abstract")).toBe(2741); + it("should throw for unsupported format", () => { + expect(() => getEvmChainId("unknown-network")).toThrow(); }); - it("should return correct chain ID for Abstract Testnet", () => { - expect(getEvmChainId("abstract-testnet")).toBe(11124); + it("should throw for invalid CAIP-2 chain ID", () => { + expect(() => getEvmChainId("eip155:abc")).toThrow("Invalid CAIP-2 chain ID"); }); + }); - it("should return correct chain ID for Avalanche Fuji", () => { - expect(getEvmChainId("avalanche-fuji")).toBe(43113); + describe("getEvmChainIdV1 (legacy names)", () => { + it("should return correct chain ID for Base", () => { + expect(getEvmChainIdV1("base")).toBe(8453); }); - it("should return correct chain ID for Avalanche", () => { - expect(getEvmChainId("avalanche")).toBe(43114); + it("should return correct chain ID for Base Sepolia", () => { + expect(getEvmChainIdV1("base-sepolia")).toBe(84532); }); - it("should return correct chain ID for IoTeX", () => { - expect(getEvmChainId("iotex")).toBe(4689); + it("should return correct chain ID for Ethereum", () => { + expect(getEvmChainIdV1("ethereum")).toBe(1); }); - it("should return correct chain ID for Sei", () => { - expect(getEvmChainId("sei")).toBe(1329); + it("should return correct chain ID for Polygon", () => { + expect(getEvmChainIdV1("polygon")).toBe(137); }); - it("should return correct chain ID for Sei Testnet", () => { - expect(getEvmChainId("sei-testnet")).toBe(1328); + it("should return correct chain ID for Polygon Amoy", () => { + expect(getEvmChainIdV1("polygon-amoy")).toBe(80002); }); - it("should return correct chain ID for Peaq", () => { - expect(getEvmChainId("peaq")).toBe(3338); + it("should return correct chain ID for Abstract", () => { + expect(getEvmChainIdV1("abstract")).toBe(2741); + }); + + it("should return correct chain ID for Avalanche", () => { + expect(getEvmChainIdV1("avalanche")).toBe(43114); }); - it("should return correct chain ID for Story", () => { - expect(getEvmChainId("story")).toBe(1514); + it("should return correct chain ID for MegaETH", () => { + expect(getEvmChainIdV1("megaeth")).toBe(4326); }); - it("should return correct chain ID for Educhain", () => { - expect(getEvmChainId("educhain")).toBe(41923); + it("should return correct chain ID for Monad", () => { + expect(getEvmChainIdV1("monad")).toBe(143); }); - it("should return correct chain ID for Skale Base Sepolia", () => { - expect(getEvmChainId("skale-base-sepolia")).toBe(324705682); + it("should throw for CAIP-2 format", () => { + expect(() => getEvmChainIdV1("eip155:8453")).toThrow("Unsupported v1 network"); }); - it("should throw for unsupported network", () => { - expect(() => getEvmChainId("unknown-network" as EvmNetworkV1)).toThrow( - "Unsupported network: unknown-network", - ); + it("should throw for unknown network", () => { + expect(() => getEvmChainIdV1("unknown-network")).toThrow("Unsupported v1 network"); }); }); diff --git a/typescript/packages/mechanisms/evm/test/unit/v1/client.test.ts b/typescript/packages/mechanisms/evm/test/unit/v1/client.test.ts index 00c84a8..09c97c2 100644 --- a/typescript/packages/mechanisms/evm/test/unit/v1/client.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/v1/client.test.ts @@ -202,7 +202,7 @@ describe("ExactEvmSchemeV1", () => { }; await expect(client.createPaymentPayload(1, requirements as never)).rejects.toThrow( - "Unsupported network: unknown-network", + "Unsupported v1 network: unknown-network", ); }); }); diff --git a/typescript/packages/mechanisms/evm/test/unit/v1/facilitator.test.ts b/typescript/packages/mechanisms/evm/test/unit/v1/facilitator.test.ts index b430491..b6171af 100644 --- a/typescript/packages/mechanisms/evm/test/unit/v1/facilitator.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/v1/facilitator.test.ts @@ -3,6 +3,7 @@ import { ExactEvmSchemeV1 } from "../../../src/exact/v1/facilitator/scheme"; import type { FacilitatorEvmSigner } from "../../../src/signer"; import type { PaymentRequirementsV1 } from "@x402/core/types/v1"; import type { PaymentPayloadV1 } from "@x402/core/types/v1"; +import * as Errors from "../../../src/exact/facilitator/errors"; describe("ExactEvmSchemeV1", () => { let mockSigner: FacilitatorEvmSigner; @@ -97,7 +98,7 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.verify(payload as never, requirements as never); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("unsupported_scheme"); + expect(result.invalidReason).toBe(Errors.ErrInvalidScheme); }); it("should reject if network does not match", async () => { @@ -133,7 +134,7 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.verify(payload as never, requirements as never); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("network_mismatch"); + expect(result.invalidReason).toBe(Errors.ErrNetworkMismatch); }); it("should reject if amount is insufficient (maxAmountRequired)", async () => { @@ -169,11 +170,12 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.verify(payload as never, requirements as never); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("invalid_exact_evm_payload_authorization_value"); + expect(result.invalidReason).toBe(Errors.ErrInvalidAuthorizationValue); }); it("should reject if balance is insufficient", async () => { - mockSigner.readContract = vi.fn().mockResolvedValue(BigInt("50000")); // Low balance + // Simulation fails (transfer would revert due to insufficient balance) + mockSigner.readContract = vi.fn().mockRejectedValue(new Error("simulation reverted")); const facilitator = new ExactEvmSchemeV1(mockSigner); @@ -207,7 +209,7 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.verify(payload as never, requirements as never); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("insufficient_funds"); + expect(result.invalidReason).toBe("invalid_exact_evm_transaction_simulation_failed"); }); it("should reject if recipient does not match", async () => { @@ -243,7 +245,7 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.verify(payload as never, requirements as never); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("invalid_exact_evm_payload_recipient_mismatch"); + expect(result.invalidReason).toBe(Errors.ErrRecipientMismatch); }); it("should reject if network not supported", async () => { @@ -278,7 +280,7 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.verify(payload as never, requirements as never); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("invalid_network"); + expect(result.invalidReason).toBe(Errors.ErrNetworkMismatch); }); }); @@ -359,7 +361,7 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.settle(payload as never, requirements as never); expect(result.success).toBe(false); - expect(result.errorReason).toBe("invalid_exact_evm_payload_signature"); + expect(result.errorReason).toBe(Errors.ErrInvalidSignature); }); }); }); diff --git a/typescript/packages/mechanisms/evm/tsup.config.ts b/typescript/packages/mechanisms/evm/tsup.config.ts index c014a50..db132c6 100644 --- a/typescript/packages/mechanisms/evm/tsup.config.ts +++ b/typescript/packages/mechanisms/evm/tsup.config.ts @@ -9,6 +9,8 @@ const baseConfig = { "exact/facilitator/index": "src/exact/facilitator/index.ts", "exact/v1/client/index": "src/exact/v1/client/index.ts", "exact/v1/facilitator/index": "src/exact/v1/facilitator/index.ts", + "upto/client/index": "src/upto/client/index.ts", + "upto/server/index": "src/upto/server/index.ts", "upto/facilitator/index": "src/upto/facilitator/index.ts", }, dts: { diff --git a/typescript/packages/mechanisms/stellar/.prettierignore b/typescript/packages/mechanisms/stellar/.prettierignore new file mode 100644 index 0000000..9fd1bad --- /dev/null +++ b/typescript/packages/mechanisms/stellar/.prettierignore @@ -0,0 +1,7 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +**/**/*.json +*.md diff --git a/typescript/packages/mechanisms/stellar/.prettierrc b/typescript/packages/mechanisms/stellar/.prettierrc new file mode 100644 index 0000000..ffb416b --- /dev/null +++ b/typescript/packages/mechanisms/stellar/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/packages/mechanisms/stellar/CHANGELOG.md b/typescript/packages/mechanisms/stellar/CHANGELOG.md new file mode 100644 index 0000000..f7511b4 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/CHANGELOG.md @@ -0,0 +1,36 @@ +# @x402/stellar Changelog + +## 2.9.0 + +### Minor Changes + +- 2250cae: Migrated project from coinbase/x402 to x402-foundation/x402 organization + +### Patch Changes + +- Updated dependencies [8cf3fca] +- Updated dependencies [c0e3969] +- Updated dependencies [2250cae] +- Updated dependencies [d352574] + - @x402/core@2.9.0 + +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- c92c0d1: Bump "@stellar/stellar-sdk" dependency and refactor API call for better performance +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +- Implement x402 v2 protocol support for the Stellar mechanism (exact scheme). diff --git a/typescript/packages/mechanisms/stellar/README.md b/typescript/packages/mechanisms/stellar/README.md new file mode 100644 index 0000000..efffd23 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/README.md @@ -0,0 +1,168 @@ +# @x402/stellar + +Stellar implementation of the x402 payment protocol using the **Exact** payment scheme with [Soroban token](https://stellar.org/protocol/sep-41) transfers. + +## Installation + +```bash +npm install @x402/stellar +``` + +## Overview + +This package provides three main components for handling x402 payments on Stellar: + +- **Client** - For applications that need to make payments (have wallets/signers) +- **Facilitator** - For payment processors that verify and execute on-chain transactions +- **Server** - For resource servers that accept payments and build payment requirements + +**Key Differences from EVM/SVM:** + +- **Ledger-based expiration** (not timestamps) - default ~12 ledgers ≈ 60 seconds +- **Auth entry signing** - client signs authorization entries only, facilitator rebuilds and submits transaction +- **Mainnet requires custom RPC URL** (see [Stellar RPC Providers](https://developers.stellar.org/docs/data/apis/rpc/providers)) + +## Package Exports + +### Main Package (`@x402/stellar`) + +**V2 Protocol Support** - x402 v2 protocol with CAIP-2 network identifiers + +**Client:** + +- `ExactStellarScheme` - Client implementation using Soroban token transfers +- `createEd25519Signer(privateKey, defaultNetwork)` - Creates a Stellar signer from private key that implements `SignAuthEntry` and `SignTransaction` according to [SEP-43](https://stellar.org/protocol/sep-43) +- `ClientStellarSigner` - TypeScript type for client signers + +**Facilitator:** + +- `ExactStellarScheme` - Facilitator for payment verification and settlement +- `FacilitatorStellarSigner` - TypeScript type for facilitator signers + +> [!NOTE] +> Facilitators currently always sponsor transaction fees (`areFeesSponsored: true`). A non-sponsored flow will be added later. See [spec](../../../specs/schemes/exact/scheme_exact_stellar.md#paymentrequirements-for-exact) for details. + +**Server:** + +- `ExactStellarScheme` - Server for building payment requirements + +**Utilities:** + +- `getRpcUrl(network, config?)` - Get RPC URL for a network +- `getRpcClient(network, config?)` - Create Soroban RPC client +- `getNetworkPassphrase(network)` - Get network passphrase +- `validateStellarDestinationAddress(address)` - Validate destination address +- `validateStellarAssetAddress(address)` - Validate asset/contract address +- `convertToTokenAmount(amount, decimals)` - Convert decimal to token units +- `getUsdcAddress(network)` - Get USDC contract address + +**Constants:** + +- `STELLAR_PUBNET_CAIP2` = `"stellar:pubnet"` +- `STELLAR_TESTNET_CAIP2` = `"stellar:testnet"` +- `USDC_PUBNET_ADDRESS` - USDC contract on mainnet +- `USDC_TESTNET_ADDRESS` - USDC contract on testnet +- `DEFAULT_TOKEN_DECIMALS` = `7` + +### Subpath Exports + +- `@x402/stellar/exact/client` - `ExactStellarScheme` (client) +- `@x402/stellar/exact/server` - `ExactStellarScheme` (server) +- `@x402/stellar/exact/facilitator` - `ExactStellarScheme` (facilitator) + +## Supported Networks + +**V2 Networks** (via [CAIP-28](https://namespaces.chainagnostic.org/stellar/caip2)): + +- `stellar:pubnet` - Mainnet (requires custom RPC URL) +- `stellar:testnet` - Testnet (default: [https://soroban-testnet.stellar.org](https://soroban-testnet.stellar.org)) +- `stellar:*` - Wildcard (matches all Stellar networks) + +## Asset Support + +Supports Soroban tokens implementing [SEP-41](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md): + +- Any Soroban token contract with `transfer(from, to, amount)` function +- Default asset is USDC (primary, 7 decimals) + +> **For detailed protocol flow, transaction structure, and verification rules, see the [Exact Scheme Specification](../../../specs/schemes/exact/scheme_exact_stellar.md).** + +## Usage Patterns + +### 1. Direct Registration (Recommended) + +```typescript +import { x402Client } from "@x402/core/client"; +import { createEd25519Signer } from "@x402/stellar"; +import { ExactStellarScheme } from "@x402/stellar/exact/client"; + +const signer = createEd25519Signer(privateKey, "stellar:testnet"); +const client = new x402Client().register("stellar:*", new ExactStellarScheme(signer)); +``` + +### 2. Custom Configuration + +```typescript +// Client with custom RPC +const client = new x402Client().register( + "stellar:*", + new ExactStellarScheme(signer, { url: "https://custom-rpc.example.com" }), +); + +// Server with custom money parser +const scheme = new ExactStellarScheme().registerMoneyParser(async (amount, network) => ({ + amount: customConvert(amount), + asset: "TOKEN_ADDRESS", + extra: {}, +})); + +// Facilitator +const facilitator = new x402Facilitator().register( + "stellar:testnet", + new ExactStellarScheme([signer]), +); +``` + +## Development + +```bash +# Build +pnpm build + +# Test +pnpm test + +# Integration tests +pnpm test:integration + +# Lint & Format +pnpm lint +pnpm format +``` + +## Integration Tests + +Integration tests require four funded Stellar testnet accounts: + +```bash +CLIENT_PRIVATE_KEY=S... # Client's secret key +FACILITATOR_PRIVATE_KEY=S... # Facilitator's secret key +FACILITATOR_ADDRESS=G... # Facilitator's public address +RESOURCE_SERVER_ADDRESS=G... # Resource server's public address +``` + +### Stellar Testnet Account Setup + +1. Go to [Stellar Laboratory](https://lab.stellar.org/account/create) ➡️ Generate keypair ➡️ Fund account with Friendbot, then copy the `Secret` and `Public` keys so you can use them. +2. Add USDC trustline (required for client and resource server): go to [Fund Account](https://lab.stellar.org/account/fund) ➡️ Paste your `Public Key` ➡️ Add USDC Trustline ➡️ paste your `Secret key` ➡️ Sign transaction ➡️ Add Trustline. +3. Get testnet USDC from [Circle Faucet](https://faucet.circle.com/) (select Stellar network). + +> [!NOTE] +> The facilitator account only needs XLM (step 1). Client and resource server accounts need all three steps. + +## Related Packages + +- `@x402/core` - Core protocol types and client +- `@x402/fetch` - HTTP wrapper with automatic payment handling +- `@x402/evm` - EVM/Ethereum implementation +- `@x402/svm` - Solana/SVM implementation diff --git a/typescript/packages/mechanisms/stellar/eslint.config.js b/typescript/packages/mechanisms/stellar/eslint.config.js new file mode 100644 index 0000000..8aecb38 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/eslint.config.js @@ -0,0 +1,131 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts", "**/*.tsx"], + ignores: ["**/*.test.ts", "test/**/*"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "import/order": [ + "error", + { + groups: [ + "builtin", + "external", + "internal", + ["parent", "sibling"], + "index", + "object", + "type", + ], + "newlines-between": "never", + alphabetize: { + order: "asc", + caseInsensitive: true, + }, + }, + ], + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, + { + files: ["**/*.test.ts", "test/**/*"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + import: importPlugin, + }, + rules: { + "import/first": "error", + "import/order": [ + "error", + { + groups: [ + "builtin", + "external", + "internal", + ["parent", "sibling"], + "index", + "object", + "type", + ], + "newlines-between": "never", + alphabetize: { + order: "asc", + caseInsensitive: true, + }, + }, + ], + "prettier/prettier": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/member-ordering": "off", + }, + }, +]; diff --git a/typescript/packages/mechanisms/stellar/package.json b/typescript/packages/mechanisms/stellar/package.json new file mode 100644 index 0000000..1a46ee1 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/package.json @@ -0,0 +1,97 @@ +{ + "name": "@x402/stellar", + "version": "2.9.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/cjs/index.d.ts", + "scripts": { + "start": "tsx --env-file=.env index.ts", + "build": "tsup", + "test": "vitest run", + "test:integration": "vitest run --config vitest.integration.config.ts", + "test:all": "vitest run --config vitest.config.ts && vitest run --config vitest.integration.config.ts", + "test:watch": "vitest", + "watch": "tsc --watch", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "keywords": [ + "x402", + "payment", + "protocol", + "stellar", + "soroban" + ], + "license": "Apache-2.0", + "author": "x402 Foundation", + "repository": "https://github.com/x402-foundation/x402", + "description": "x402 Payment Protocol Stellar Implementation", + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/node": "^22.13.4", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsup": "^8.4.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vite": "^6.2.6", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.5" + }, + "dependencies": { + "@stellar/stellar-sdk": "^14.6.1", + "@x402/core": "workspace:*" + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./exact/client": { + "import": { + "types": "./dist/esm/exact/client/index.d.mts", + "default": "./dist/esm/exact/client/index.mjs" + }, + "require": { + "types": "./dist/cjs/exact/client/index.d.ts", + "default": "./dist/cjs/exact/client/index.js" + } + }, + "./exact/server": { + "import": { + "types": "./dist/esm/exact/server/index.d.mts", + "default": "./dist/esm/exact/server/index.mjs" + }, + "require": { + "types": "./dist/cjs/exact/server/index.d.ts", + "default": "./dist/cjs/exact/server/index.js" + } + }, + "./exact/facilitator": { + "import": { + "types": "./dist/esm/exact/facilitator/index.d.mts", + "default": "./dist/esm/exact/facilitator/index.mjs" + }, + "require": { + "types": "./dist/cjs/exact/facilitator/index.d.ts", + "default": "./dist/cjs/exact/facilitator/index.js" + } + } + }, + "files": [ + "dist" + ] +} diff --git a/typescript/packages/mechanisms/stellar/src/constants.ts b/typescript/packages/mechanisms/stellar/src/constants.ts new file mode 100644 index 0000000..e3ef921 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/constants.ts @@ -0,0 +1,39 @@ +/** + * CAIP-2 network identifiers for Stellar (V2) + */ +export const STELLAR_PUBNET_CAIP2 = "stellar:pubnet"; +export const STELLAR_TESTNET_CAIP2 = "stellar:testnet"; +export const STELLAR_WILDCARD_CAIP2 = "stellar:*"; + +/** + * Default testnet RPC URL + */ +export const DEFAULT_TESTNET_RPC_URL = "https://soroban-testnet.stellar.org"; + +/** + * Default Horizon API URLs + */ +export const DEFAULT_TESTNET_HORIZON_URL = "https://horizon-testnet.stellar.org"; +export const DEFAULT_PUBNET_HORIZON_URL = "https://horizon.stellar.org"; + +/** + * Stellar validation regex for destination and asset addresses + */ +export const STELLAR_DESTINATION_ADDRESS_REGEX = /^(?:[GC][ABCD][A-Z2-7]{54}|M[ABCD][A-Z2-7]{67})$/; // Stellar address: G-account (56 chars), C-account (56 chars), or M-account (69 chars, muxed) +export const STELLAR_ASSET_ADDRESS_REGEX = /^(?:[C][ABCD][A-Z2-7]{54})$/; // Stellar token contract address: C-account (56 chars) + +/** + * USDC contract addresses (default stablecoin) + */ +export const USDC_PUBNET_ADDRESS = "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75"; +export const USDC_TESTNET_ADDRESS = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; + +export const STELLAR_NETWORK_TO_PASSPHRASE: ReadonlyMap = new Map([ + [STELLAR_PUBNET_CAIP2, "Public Global Stellar Network ; September 2015"], + [STELLAR_TESTNET_CAIP2, "Test SDF Network ; September 2015"], +]); + +/** + * Default token decimals + */ +export const DEFAULT_TOKEN_DECIMALS = 7; diff --git a/typescript/packages/mechanisms/stellar/src/exact/client/index.ts b/typescript/packages/mechanisms/stellar/src/exact/client/index.ts new file mode 100644 index 0000000..9e91e06 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/client/index.ts @@ -0,0 +1 @@ +export { ExactStellarScheme } from "./scheme"; diff --git a/typescript/packages/mechanisms/stellar/src/exact/client/scheme.ts b/typescript/packages/mechanisms/stellar/src/exact/client/scheme.ts new file mode 100644 index 0000000..2724fca --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/client/scheme.ts @@ -0,0 +1,138 @@ +import { nativeToScVal, contract } from "@stellar/stellar-sdk"; +import { handleSimulationResult } from "../../shared"; +import { + getEstimatedLedgerCloseTimeSeconds, + getNetworkPassphrase, + getRpcClient, + getRpcUrl, + isStellarNetwork, + RpcConfig, + validateStellarAssetAddress, + validateStellarDestinationAddress, +} from "../../utils"; +import type { ClientStellarSigner } from "../../signer"; +import type { PaymentPayload, PaymentRequirements, SchemeNetworkClient } from "@x402/core/types"; + +/** + * Stellar client implementation for the Exact payment scheme. + */ +export class ExactStellarScheme implements SchemeNetworkClient { + readonly scheme = "exact"; + + /** + * Creates a new ExactStellarScheme instance. + * + * @param signer - The Stellar signer for client operations + * @param rpcConfig - Optional configuration with custom RPC URL + * @returns ExactStellarScheme instance + */ + constructor( + private readonly signer: ClientStellarSigner, + private readonly rpcConfig?: RpcConfig, + ) {} + + /** + * Creates a payment payload for the Exact scheme. + * + * @param x402Version - The x402 protocol version + * @param paymentRequirements - The payment requirements + * @returns Promise resolving to a payment payload + */ + async createPaymentPayload( + x402Version: number, + paymentRequirements: PaymentRequirements, + ): Promise> { + try { + this.validateCreateAndSignPaymentInput(paymentRequirements); + } catch (error) { + throw new Error(`Invalid input parameters for creating Stellar payment, cause: ${error}`); + } + + const sourcePublicKey = this.signer.address; + const { network, payTo, asset, amount, extra, maxTimeoutSeconds } = paymentRequirements; + const networkPassphrase = getNetworkPassphrase(network); + const rpcUrl = getRpcUrl(network, this.rpcConfig); + + if (!extra.areFeesSponsored) { + throw new Error(`Exact scheme requires areFeesSponsored to be true`); + } + + // Fetch current ledger and calculate maxLedger + const rpcServer = getRpcClient(network, this.rpcConfig); + const latestLedger = await rpcServer.getLatestLedger(); + const currentLedger = latestLedger.sequence; + const estimatedLedgerSeconds = await getEstimatedLedgerCloseTimeSeconds(network); + const maxLedger = currentLedger + Math.ceil(maxTimeoutSeconds / estimatedLedgerSeconds); + + const tx = await contract.AssembledTransaction.build({ + contractId: asset, + method: "transfer", + args: [ + // SEP-41 spec: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md#interface + nativeToScVal(sourcePublicKey, { type: "address" }), // from + nativeToScVal(payTo, { type: "address" }), // to + nativeToScVal(amount, { type: "i128" }), // amount + ], + networkPassphrase, + rpcUrl, + parseResultXdr: result => result, + }); + handleSimulationResult(tx.simulation); + + let missingSigners = tx.needsNonInvokerSigningBy(); + if (!missingSigners.includes(sourcePublicKey) || missingSigners.length > 1) { + throw new Error( + `Expected to sign with [${sourcePublicKey}], but got [${missingSigners.join(", ")}]`, + ); + } + await tx.signAuthEntries({ + address: sourcePublicKey, + signAuthEntry: this.signer.signAuthEntry, + expiration: maxLedger, + }); + + await tx.simulate(); + handleSimulationResult(tx.simulation); + + missingSigners = tx.needsNonInvokerSigningBy(); + if (missingSigners.length > 0) { + throw new Error(`unexpected signer(s) required: [${missingSigners.join(", ")}]`); + } + + return { + x402Version, + payload: { + transaction: tx.built!.toXDR(), + }, + }; + } + + /** + * Validates the input parameters for the createAndSignPayment function. + * + * @param paymentRequirements - Payment requirements + * @throws Error if validation fails + */ + private validateCreateAndSignPaymentInput(paymentRequirements: PaymentRequirements): void { + const { scheme, network, payTo, asset, amount } = paymentRequirements; + if (typeof amount !== "string" || !Number.isInteger(Number(amount)) || Number(amount) <= 0) { + throw new Error(`Invalid amount: ${amount}. Amount must be a positive integer.`); + } + + if (scheme !== "exact") { + throw new Error(`Unsupported scheme: ${scheme}`); + } + + if (!isStellarNetwork(network)) { + throw new Error(`Unsupported Stellar network: ${network}`); + } + + if (!validateStellarDestinationAddress(payTo)) { + throw new Error(`Invalid Stellar destination address: ${payTo}`); + } + + if (!validateStellarAssetAddress(asset)) { + throw new Error(`Invalid Stellar asset address: ${asset}`); + } + } +} diff --git a/typescript/packages/mechanisms/stellar/src/exact/facilitator/index.ts b/typescript/packages/mechanisms/stellar/src/exact/facilitator/index.ts new file mode 100644 index 0000000..9e91e06 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/facilitator/index.ts @@ -0,0 +1 @@ +export { ExactStellarScheme } from "./scheme"; diff --git a/typescript/packages/mechanisms/stellar/src/exact/facilitator/scheme.ts b/typescript/packages/mechanisms/stellar/src/exact/facilitator/scheme.ts new file mode 100644 index 0000000..bac8c87 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/facilitator/scheme.ts @@ -0,0 +1,765 @@ +import { + scValToNative, + Transaction, + FeeBumpTransaction, + Address, + Operation, + xdr, + rpc, + TransactionBuilder, +} from "@stellar/stellar-sdk"; +import { Api } from "@stellar/stellar-sdk/rpc"; +import { STELLAR_WILDCARD_CAIP2 } from "../../constants"; +import { gatherAuthEntrySignatureStatus } from "../../shared"; +import { ExactStellarPayloadV2 } from "../../types"; +import { + getEstimatedLedgerCloseTimeSeconds, + getRpcClient, + getNetworkPassphrase, + isStellarNetwork, + RpcConfig, +} from "../../utils"; +import type { FacilitatorStellarSigner } from "../../signer"; +import type { + Network, + PaymentPayload, + PaymentRequirements, + SchemeNetworkFacilitator, + SettleResponse, + VerifyResponse, +} from "@x402/core/types"; + +const DEFAULT_TIMEOUT_SECONDS = 60; +const SUPPORTED_X402_VERSION = 2; +const DEFAULT_MAX_TRANSACTION_FEE_STROOPS = 50_000; +const SIGNATURE_EXPIRATION_LEDGER_TOLERANCE = 2; + +/** + * Returns a round-robin selector for choosing which signer to use. + * Each invocation returns a new selector with its own counter. + * + * @returns A function that selects the next address from the given array on each call + */ +const roundRobinSelectSigner = (): ((addresses: readonly string[]) => string) => { + let index = 0; + return addrs => addrs[index++ % addrs.length]; +}; + +/** + * Helper to create a `VerifyResponse` with `isValid: false`. + * + * @param reason - The error reason code + * @param payer - Optional payer address + * @returns a `VerifyResponse` with `isValid: false` and the provided reason and (optional) payer + */ +export function invalidVerifyResponse(reason: string, payer?: string): VerifyResponse { + return { isValid: false, invalidReason: reason, payer }; +} + +/** + * Helper to create a `VerifyResponse` with `isValid: true`. + * + * @param payer - The payer address + * @returns a `VerifyResponse` with `isValid: true` and the provided payer + */ +export function validVerifyResponse(payer: string): VerifyResponse { + return { isValid: true, payer }; +} + +/** + * Stellar facilitator implementation for the Exact payment scheme. + */ +export class ExactStellarScheme implements SchemeNetworkFacilitator { + readonly scheme = "exact"; + readonly caipFamily = STELLAR_WILDCARD_CAIP2; + + public readonly signingAddresses: ReadonlySet; + public readonly areFeesSponsored: boolean; + public readonly rpcConfig?: RpcConfig; + public readonly maxTransactionFeeStroops: number; + public readonly feeBumpSigner?: FacilitatorStellarSigner; + private readonly signerMap: Map; + private readonly selectSigner: (addresses: readonly string[]) => string; + + /** + * Creates a new ExactStellarScheme instance. + * + * @param signers - One or more Stellar signers managed by the facilitator for settlement + * @param options - Configuration options + * @param options.rpcConfig - Optional RPC configuration with custom RPC URL + * @param options.areFeesSponsored - Indicates if fees are sponsored (default: true) + * @param options.maxTransactionFeeStroops - Maximum fee in stroops the facilitator will pay (default: 50_000) + * @param options.selectSigner - Callback to select which signer to use (default: round-robin) + * @param options.feeBumpSigner - Optional signer used as fee source in a fee bump transaction wrapper. + * When provided, settle() wraps the inner transaction (signed by the selected signer) in a + * FeeBumpTransaction where the feeBumpSigner pays the fees, decoupling fee payment from sequence number management. + * @returns ExactStellarScheme instance + */ + constructor( + signers: FacilitatorStellarSigner[], + { + rpcConfig, + areFeesSponsored = true, + maxTransactionFeeStroops = DEFAULT_MAX_TRANSACTION_FEE_STROOPS, + selectSigner = roundRobinSelectSigner(), + feeBumpSigner, + }: { + /** Optional RPC configuration with custom RPC URL */ + rpcConfig?: RpcConfig; + /** Indicates if fees are sponsored (default: true) */ + areFeesSponsored?: boolean; + /** Maximum fee in stroops the facilitator will pay (default: 50_000) */ + maxTransactionFeeStroops?: number; + /** Optional callback to select which signer to use. Receives addresses array, returns selected address. Defaults to round-robin. */ + selectSigner?: (addresses: readonly string[]) => string; + /** Optional signer used as fee source in a fee bump transaction wrapper. Decouples fee payment from sequence number management. */ + feeBumpSigner?: FacilitatorStellarSigner; + } = {}, + ) { + // Validate signers and store their data + if (!signers || signers.length === 0) { + throw new Error("At least one signer is required"); + } + this.signerMap = new Map(signers.map(s => [s.address, s])); + this.signingAddresses = new Set(this.signerMap.keys()); + + // Apply configuration options (with defaults) + this.rpcConfig = rpcConfig; + this.areFeesSponsored = areFeesSponsored ?? true; + this.maxTransactionFeeStroops = maxTransactionFeeStroops ?? DEFAULT_MAX_TRANSACTION_FEE_STROOPS; + this.selectSigner = selectSigner ?? roundRobinSelectSigner(); + this.feeBumpSigner = feeBumpSigner; + } + + /** + * Get mechanism-specific extra data for the supported kinds endpoint. + * For Stellar, returns `areFeesSponsored` indicating to clients if they can expect fees to be sponsored. + * As of now, the spec only supports `areFeesSponsored: true`. + * + * @param _ - The network identifier (unused, offset is network-agnostic) + * @returns Extra data with the `areFeesSponsored` flag + */ + getExtra(_: Network): Record | undefined { + return { + areFeesSponsored: this.areFeesSponsored, + }; + } + + /** + * Get signer addresses used by this facilitator. + * For Stellar, returns all facilitator addresses including the fee bump signer when configured. + * + * @param _ - The network identifier (unused for Stellar) + * @returns Array containing all facilitator addresses + */ + getSigners(_: string): string[] { + const signers = [...this.signingAddresses]; + if (this.feeBumpSigner && !this.signingAddresses.has(this.feeBumpSigner.address)) { + signers.push(this.feeBumpSigner.address); + } + return signers; + } + + /** + * Verifies a payment payload. + * + * @param payload - The payment payload to verify + * @param requirements - The payment requirements + * @returns Promise resolving to verification response + */ + async verify( + payload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + let fromAddress: string | undefined; + try { + // Step 1: Validate protocol version, scheme, and network + if (payload.x402Version !== SUPPORTED_X402_VERSION) { + return invalidVerifyResponse("invalid_x402_version"); + } + + if (payload.accepted.scheme !== "exact" || requirements.scheme !== "exact") { + return invalidVerifyResponse("unsupported_scheme"); + } + + if (requirements.network !== payload.accepted.network) { + return invalidVerifyResponse("network_mismatch"); + } + if (!isStellarNetwork(requirements.network)) { + return invalidVerifyResponse("invalid_network"); + } + + const networkPassphrase = getNetworkPassphrase(requirements.network); + const server = getRpcClient(requirements.network, this.rpcConfig); + + // Step 2: Parse and decode transaction + const stellarPayload = payload.payload as ExactStellarPayloadV2; + if (!stellarPayload || typeof stellarPayload.transaction !== "string") { + return invalidVerifyResponse("invalid_exact_stellar_payload_malformed"); + } + + let transaction: Transaction; + try { + transaction = new Transaction(stellarPayload.transaction, networkPassphrase); + } catch (error) { + console.error("Error parsing transaction:", error); + return invalidVerifyResponse("invalid_exact_stellar_payload_malformed"); + } + + // Step 3: Validate transaction structure + if (transaction.operations.length !== 1) { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_operation"); + } + + const operation = transaction.operations[0]; + if (operation.type !== "invokeHostFunction") { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_operation"); + } + + if ( + this.signingAddresses.has(operation.source ?? "") || + this.signingAddresses.has(transaction.source) + ) { + return invalidVerifyResponse("invalid_exact_stellar_payload_unsafe_tx_or_op_source"); + } + + // Step 4: Extract and validate contract invocation details + const invokeOp = operation as Operation.InvokeHostFunction; + const func = invokeOp.func; + + if (!func || func.switch().name !== "hostFunctionTypeInvokeContract") { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_operation"); + } + + // Step 5: Validate contract address and function name + const invokeContractArgs = func.invokeContract(); + const contractAddress = Address.fromScAddress( + invokeContractArgs.contractAddress(), + ).toString(); + const functionName = invokeContractArgs.functionName().toString(); + + const args = invokeContractArgs.args(); + if (contractAddress !== requirements.asset) { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_asset"); + } + + if (functionName !== "transfer" || args.length !== 3) { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_function_name"); + } + + // Step 6: Extract and validate transfer arguments + fromAddress = scValToNative(args[0]) as string; + const toAddress = scValToNative(args[1]) as string; + const amount = scValToNative(args[2]) as bigint; + + if (this.signingAddresses.has(fromAddress)) { + return invalidVerifyResponse("invalid_exact_stellar_payload_facilitator_is_payer"); + } + + if (toAddress !== requirements.payTo) { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_recipient", fromAddress); + } + + const expectedAmount = BigInt(requirements.amount); + if (amount !== expectedAmount) { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_amount", fromAddress); + } + + // Step 7: Re-simulate to ensure transaction will succeed + const simResponse = await server.simulateTransaction(transaction); + if (!Api.isSimulationSuccess(simResponse)) { + const errorMsg = simResponse.error ? `: ${simResponse.error}` : ""; + console.error("Simulation error:", errorMsg); + return invalidVerifyResponse( + "invalid_exact_stellar_payload_simulation_failed", + fromAddress, + ); + } + + // Step 8: Validate if the resource fees are within acceptable bounds + const clientFeeStroops = parseInt(transaction.fee, 10); + const minResourceFee = parseInt(simResponse.minResourceFee, 10); + + // Fee must be at least the minimum required by simulation + if (clientFeeStroops < minResourceFee) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_fee_below_minimum", + fromAddress, + ); + } + + // Fee must not exceed the facilitator's maximum + if (clientFeeStroops > this.maxTransactionFeeStroops) { + return invalidVerifyResponse("invalid_exact_stellar_payload_fee_exceeds_maximum"); + } + + // Step 9: Validate simulation events for expected transfer only. + const eventValidation = this.validateSimulationEvents( + simResponse.events, + fromAddress, + requirements.payTo, + expectedAmount, + requirements.asset, + ); + if (eventValidation) { + return eventValidation; + } + + const latestLedger = await server.getLatestLedger(); + const currentLedger = latestLedger.sequence; + const maxTimeoutSeconds = requirements.maxTimeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS; + const estimatedLedgerSeconds = await getEstimatedLedgerCloseTimeSeconds(requirements.network); + const maxLedgerOffset = Math.ceil(maxTimeoutSeconds / estimatedLedgerSeconds); + const maxLedger = currentLedger + maxLedgerOffset; + + // Step 10: Validate auth entries (structure, credential type, expiration, facilitator safety, and signature status). + const authValidation = this.validateAuthEntries( + invokeOp, + this.signingAddresses, + fromAddress, + maxLedger, + transaction, + simResponse, + ); + if (authValidation) { + return authValidation; + } + + return validVerifyResponse(fromAddress); + } catch (error) { + console.error("Unexpected verification error:", error); + return invalidVerifyResponse("unexpected_verify_error", fromAddress); + } + } + + /** + * Settles a payment by submitting the transaction on-chain. + * + * @param payload - The payment payload to settle + * @param requirements - The payment requirements + * @returns Promise resolving to settlement response + */ + async settle( + payload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + const server = getRpcClient(requirements.network, this.rpcConfig); + const networkPassphrase = getNetworkPassphrase(requirements.network); + let payer: string | undefined; + let txHash: string | undefined; + + try { + // Step 1: Verify payment before settlement + const verifyResult = await this.verify(payload, requirements); + + if (!verifyResult.isValid) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: verifyResult.invalidReason ?? "verification_failed", + payer: verifyResult.payer, + }; + } + + payer = verifyResult.payer!; + + // Step 2: Parse transaction envelope once to extract both transaction and Soroban data + const stellarPayload = payload.payload as ExactStellarPayloadV2; + const txEnvelope = xdr.TransactionEnvelope.fromXDR(stellarPayload.transaction, "base64"); + const transaction = new Transaction(stellarPayload.transaction, networkPassphrase); + const sorobanData = txEnvelope.v1()?.tx()?.ext()?.sorobanData() || undefined; + + // Validate Soroban data is present for Soroban transactions + if (!sorobanData) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "invalid_exact_stellar_payload_malformed", + payer, + }; + } + + // Step 3: Extract operation + const invokeOp = transaction.operations[0] as Operation.InvokeHostFunction; + + // Step 4: Rebuild transaction with facilitator as source and facilitator-chosen fee + const signer = this.signerMap.get(this.selectSigner([...this.signingAddresses])); + if (!signer) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "settle_exact_stellar_signer_selection_failed", + payer, + }; + } + const facilitatorAccount = await server.getAccount(signer.address); + + // Use the minimum of the client's fee and the maximum cap + const clientFeeStroops = parseInt(transaction.fee, 10); + const maxFeeStroops = Math.min(clientFeeStroops, this.maxTransactionFeeStroops); + + const rebuiltTx = new TransactionBuilder(facilitatorAccount, { + fee: maxFeeStroops.toString(), + networkPassphrase, + ledgerbounds: transaction.ledgerBounds, + memo: transaction.memo, + minAccountSequence: transaction.minAccountSequence, + minAccountSequenceAge: transaction.minAccountSequenceAge, + minAccountSequenceLedgerGap: transaction.minAccountSequenceLedgerGap, + extraSigners: transaction.extraSigners, + sorobanData, + }) + .setTimeout(requirements.maxTimeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) + .addOperation(Operation.invokeHostFunction(invokeOp)) + .build(); + + // Step 5: Sign inner transaction with the selected signer's key + const { signedTxXdr, error: signError } = await signer.signTransaction(rebuiltTx.toXDR(), { + networkPassphrase, + }); + + if (signError) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "settle_exact_stellar_transaction_signing_failed", + payer, + }; + } + + // Step 6: Optionally wrap in a fee bump transaction + let txToSubmit: Transaction | FeeBumpTransaction; + + if (this.feeBumpSigner) { + const signedInnerTx = TransactionBuilder.fromXDR( + signedTxXdr, + networkPassphrase, + ) as Transaction; + + const feeBumpTx = TransactionBuilder.buildFeeBumpTransaction( + this.feeBumpSigner.address, + maxFeeStroops.toString(), // Same as the inner transaction fee + signedInnerTx, + networkPassphrase, + ); + + const { signedTxXdr: signedFeeBumpXdr, error: feeBumpSignError } = + await this.feeBumpSigner.signTransaction(feeBumpTx.toXDR(), { networkPassphrase }); + + if (feeBumpSignError) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "settle_exact_stellar_fee_bump_signing_failed", + payer, + }; + } + + txToSubmit = TransactionBuilder.fromXDR( + signedFeeBumpXdr, + networkPassphrase, + ) as FeeBumpTransaction; + } else { + txToSubmit = TransactionBuilder.fromXDR(signedTxXdr, networkPassphrase) as Transaction; + } + + // Step 7: Submit transaction to network + const sendResult = await server.sendTransaction(txToSubmit); + + if (sendResult.status !== "PENDING") { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "settle_exact_stellar_transaction_submission_failed", + payer, + }; + } + + // Step 8: Poll for transaction confirmation + txHash = sendResult.hash; + const maxPollAttempts = requirements.maxTimeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS; + const confirmResult = await this.pollForTransaction(server, txHash, maxPollAttempts); + + if (!confirmResult.success) { + return { + success: false, + network: payload.accepted.network, + transaction: txHash, + errorReason: "settle_exact_stellar_transaction_failed", + payer, + }; + } + + // Step 9: Return success + return { + success: true, + transaction: txHash, + network: payload.accepted.network, + payer: payer, + }; + } catch (error) { + console.error("Unexpected settlement error:", error); + return { + success: false, + network: payload.accepted.network, + transaction: txHash || "", + errorReason: "unexpected_settle_error", + payer, + }; + } + } + + /** + * Polls for transaction confirmation on Soroban. + * + * @param server - Soroban RPC server + * @param txHash - Transaction hash to poll for + * @param maxPollAttempts - Maximum number of polling attempts (default: 15) + * @param delayMs - Delay between attempts in milliseconds (default: 1000) + * @returns Result with success status + */ + private async pollForTransaction( + server: rpc.Server, + txHash: string, + maxPollAttempts = 15, + delayMs = 1000, + ): Promise<{ success: boolean }> { + for (let i = 0; i < maxPollAttempts; i++) { + try { + const txResult = await server.getTransaction(txHash); + + if (txResult.status === "SUCCESS") { + return { success: true }; + } else if (txResult.status === "FAILED") { + return { success: false }; + } + + // Transaction still pending, wait and retry + await new Promise(resolve => setTimeout(resolve, delayMs)); + } catch (error: unknown) { + if (error instanceof Error && !error.message.includes("NOT_FOUND")) { + console.warn(`Poll attempt ${i} failed:`, error); + } + await new Promise(resolve => setTimeout(resolve, delayMs)); + } + } + + // Timeout + return { success: false }; + } + + /** + * Validates simulation events for transfer correctness. + * Ensures there is exactly one token transfer event, the transfer matches the + * expected sender, recipient, amount, and asset (contract address), and the + * facilitator address is not involved in the transfer. + * + * @param events - The array of DiagnosticEvent objects from the simulation + * @param fromAddress - The payer's address + * @param toAddress - The recipient's address + * @param expectedAmount - The expected transfer amount + * @param expectedAsset - The expected token contract address + * @returns undefined if the validation succeeds, otherwise an invalid VerifyResponse + */ + private validateSimulationEvents( + events: xdr.DiagnosticEvent[], + fromAddress: string, + toAddress: string, + expectedAmount: bigint, + expectedAsset: string, + ): VerifyResponse | undefined { + // Soroban token transfer events follow the [CAP-46](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-06.md) format: + // Topic: ["transfer", from, to], Data: amount + const transferEvents: Array<{ + from: string; + to: string; + amount: bigint; + }> = []; + + // Parse events into + for (const diagnosticEvent of events) { + try { + const event = diagnosticEvent.event(); + + // Skip non-contract events + if (event.type().name !== "contract") { + continue; + } + + const body = event.body().v0(); + const topics = body.topics(); + + // Check if this is a transfer event (first topic is "transfer" symbol) + if (topics.length < 3) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_event_not_transfer", + fromAddress, + ); + } + + const topicType = topics[0].switch().name; + if (topicType !== "scvSymbol") { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_event_not_transfer", + fromAddress, + ); + } + + const symbol = topics[0].sym().toString(); + if (symbol !== "transfer") { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_event_not_transfer", + fromAddress, + ); + } + + const contractIdHash = event.contractId(); + if (!contractIdHash) + return invalidVerifyResponse( + "invalid_exact_stellar_payload_event_missing_contract_id", + fromAddress, + ); + const eventContractAddress = Address.fromScAddress( + xdr.ScAddress.scAddressTypeContract(contractIdHash), + ).toString(); + if (eventContractAddress !== expectedAsset) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_event_wrong_asset", + fromAddress, + ); + } + + // Extract from, to, and amount + const from = scValToNative(topics[1]) as string; + const to = scValToNative(topics[2]) as string; + const amount = scValToNative(body.data()) as bigint; + + transferEvents.push({ from, to, amount }); + } catch (error: unknown) { + if (error instanceof Error) { + console.error("Error parsing diagnostic event:", error.message); + } else { + console.error("Error parsing diagnostic event:", String(error)); + } + return invalidVerifyResponse("unexpected_verify_error", fromAddress); + } + } + + // If no transfer events are present, reject. + if (transferEvents.length === 0) { + return invalidVerifyResponse("invalid_exact_stellar_payload_no_transfer_events", fromAddress); + } + + if (transferEvents.length > 1) { + return invalidVerifyResponse("invalid_exact_stellar_payload_multiple_transfers", fromAddress); + } + + const transferEvent = transferEvents[0]; + + // Validate the transfer matches the expected sender, recipient, and amount + if (transferEvent.from !== fromAddress) { + return invalidVerifyResponse("invalid_exact_stellar_payload_event_wrong_from", fromAddress); + } + if (transferEvent.to !== toAddress) { + return invalidVerifyResponse("invalid_exact_stellar_payload_event_wrong_to", fromAddress); + } + if (transferEvent.amount !== expectedAmount) { + return invalidVerifyResponse("invalid_exact_stellar_payload_event_wrong_amount", fromAddress); + } + + return undefined; + } + + /** + * Validates authorization entries: structure, credential type, expiration, + * facilitator safety, no sub-invocations, and that the payer has signed and + * no other signatures are pending (per simulation). + * + * @param invokeOp - The invoke host function operation + * @param facilitatorAddresses - Set of all facilitator addresses + * @param fromAddress - The payer's address (for error reporting) + * @param maxLedger - The maximum allowed expiration ledger + * @param transaction - The full transaction (for signature status) + * @param simResponse - The simulation result (used to interpret auth entry signatures) + * @returns Invalid VerifyResponse when validation fails + */ + private validateAuthEntries( + invokeOp: Operation.InvokeHostFunction, + facilitatorAddresses: ReadonlySet, + fromAddress: string, + maxLedger: number, + transaction: Transaction, + simResponse: Api.SimulateTransactionSuccessResponse, + ): VerifyResponse | undefined { + if (!invokeOp.auth || invokeOp.auth.length === 0) { + return invalidVerifyResponse("invalid_exact_stellar_payload_no_auth_entries", fromAddress); + } + + for (const auth of invokeOp.auth) { + const credentialsType = auth.credentials().switch(); + + // Only address-based credentials are allowed + if (credentialsType !== xdr.SorobanCredentialsType.sorobanCredentialsAddress()) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_unsupported_credential_type", + fromAddress, + ); + } + + // Extract address from credentials + const addressCredentials = auth.credentials().address(); + const authAddress = Address.fromScAddress(addressCredentials.address()).toString(); + + // Facilitator must not appear in auth entries + if (facilitatorAddresses.has(authAddress)) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_facilitator_in_auth", + fromAddress, + ); + } + + // Check signature expiration is within allowed window (with ledger tolerance for RPC skew) + const expirationLedger = addressCredentials.signatureExpirationLedger(); + if (expirationLedger > maxLedger + SIGNATURE_EXPIRATION_LEDGER_TOLERANCE) { + return invalidVerifyResponse( + "invalid_exact_stellar_signature_expiration_too_far", + fromAddress, + ); + } + + // No sub-invocations allowed + const rootInvocation = auth.rootInvocation(); + if (rootInvocation.subInvocations().length > 0) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_has_subinvocations", + fromAddress, + ); + } + } + + const authStatus = gatherAuthEntrySignatureStatus({ + transaction, + simulationResponse: simResponse, + }); + if (!authStatus.alreadySigned.includes(fromAddress)) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_missing_payer_signature", + fromAddress, + ); + } + if (authStatus.pendingSignature.length > 0) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_unexpected_pending_signatures", + fromAddress, + ); + } + + return undefined; + } +} diff --git a/typescript/packages/mechanisms/stellar/src/exact/index.ts b/typescript/packages/mechanisms/stellar/src/exact/index.ts new file mode 100644 index 0000000..028d97c --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/index.ts @@ -0,0 +1 @@ +export { ExactStellarScheme } from "./client/scheme"; diff --git a/typescript/packages/mechanisms/stellar/src/exact/server/index.ts b/typescript/packages/mechanisms/stellar/src/exact/server/index.ts new file mode 100644 index 0000000..9e91e06 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/server/index.ts @@ -0,0 +1 @@ +export { ExactStellarScheme } from "./scheme"; diff --git a/typescript/packages/mechanisms/stellar/src/exact/server/scheme.ts b/typescript/packages/mechanisms/stellar/src/exact/server/scheme.ts new file mode 100644 index 0000000..df9e6df --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/server/scheme.ts @@ -0,0 +1,150 @@ +import { DEFAULT_TOKEN_DECIMALS } from "../../constants"; +import { convertToTokenAmount, getUsdcAddress } from "../../utils"; +import type { + AssetAmount, + Network, + PaymentRequirements, + Price, + SchemeNetworkServer, + MoneyParser, +} from "@x402/core/types"; + +/** + * Stellar server implementation for the Exact payment scheme. + */ +export class ExactStellarScheme implements SchemeNetworkServer { + readonly scheme = "exact"; + private moneyParsers: MoneyParser[] = []; + + /** + * Register a custom money parser in the parser chain. + * Multiple parsers can be registered - they will be tried in registration order. + * Each parser receives a decimal amount (e.g., 1.50 for $1.50). + * If a parser returns null, the next parser in the chain will be tried. + * The default parser is always the final fallback. + * + * @param parser - Custom function to convert amount to AssetAmount (or null to skip) + * @returns The service instance for chaining + */ + registerMoneyParser(parser: MoneyParser): ExactStellarScheme { + this.moneyParsers.push(parser); + return this; + } + + /** + * Parses a price into `AssetAmount`. + * If price is already an `AssetAmount`, returns it directly. + * If price is `Money` (string | number), parses to decimal and tries custom parsers. + * If no custom parsers return a valid `AssetAmount`, falls back to default conversion, assuming USDC token contract. + * + * @param price - The `Price` to parse + * @param network - The `Network` to use + * @returns Promise that resolves to the parsed `AssetAmount` + */ + async parsePrice(price: Price, network: Network): Promise { + // Attempt 1: if already an AssetAmount, return it directly + if (typeof price === "object" && price !== null && "amount" in price) { + if (!price.asset) { + throw new Error(`Asset address must be specified for AssetAmount on network ${network}`); + } + return { + amount: price.amount, + asset: price.asset, + extra: price.extra || {}, + }; + } + + // Parse Money to decimal number + const amount = this.parseMoneyToDecimal(price); + + // Attempt 2: try each custom money parser in order + for (const parser of this.moneyParsers) { + const result = await parser(amount, network); + if (result !== null) { + return result; + } + } + + // Attempt 3: fallback to default conversion, assuming USDC token contract. + return this.defaultMoneyConversion(amount, network); + } + + /** + * Build payment requirements for this scheme/network combination + * + * @param paymentRequirements - The base payment requirements + * @param supportedKind - The supported kind configuration + * @param supportedKind.x402Version - The x402 protocol version + * @param supportedKind.scheme - The payment scheme + * @param supportedKind.network - The network identifier + * @param supportedKind.extra - Extra metadata including `areFeesSponsored` from facilitator + * @param extensionKeys - Extension keys supported by the facilitator + * @returns Enhanced payment requirements with `areFeesSponsored` in extra + */ + enhancePaymentRequirements( + paymentRequirements: PaymentRequirements, + supportedKind: { + x402Version: number; + scheme: string; + network: Network; + extra?: Record; + }, + extensionKeys: string[], + ): Promise { + // Mark unused parameters to satisfy linter + void extensionKeys; + + // Add `areFeesSponsored` from supportedKind.extra to payment requirements + // The facilitator provides `areFeesSponsored` which clients use to determine if fees are sponsored + const areFeesSponsored = supportedKind.extra?.areFeesSponsored; + return Promise.resolve({ + ...paymentRequirements, + extra: { + ...paymentRequirements.extra, + ...(typeof areFeesSponsored === "boolean" && { areFeesSponsored }), + }, + }); + } + + /** + * Parse Money (string | number) to a decimal number. + * Handles formats like "$1.50", "1.50", 1.50, etc. + * + * @param money - The money value to parse + * @returns Decimal number + */ + private parseMoneyToDecimal(money: string | number): number { + if (typeof money === "number") { + return money; + } + + // Remove $ sign and whitespace, then parse + const cleanMoney = money.replace(/^\$/, "").trim(); + const amount = parseFloat(cleanMoney); + + if (isNaN(amount)) { + throw new Error(`Invalid money format: ${money}`); + } + + return amount; + } + + /** + * Default money conversion implementation. + * Converts decimal amount to USDC on the specified network. + * + * @param amount - The decimal amount (e.g., 1.50) + * @param network - The network to use + * @returns The parsed asset amount in USDC + */ + private defaultMoneyConversion(amount: number, network: Network): AssetAmount { + // Convert decimal amount to token amount (USDC on Stellar has 7 decimals) + const tokenAmount = convertToTokenAmount(amount.toString(), DEFAULT_TOKEN_DECIMALS); + + return { + amount: tokenAmount, + asset: getUsdcAddress(network), + extra: {}, + }; + } +} diff --git a/typescript/packages/mechanisms/stellar/src/index.ts b/typescript/packages/mechanisms/stellar/src/index.ts new file mode 100644 index 0000000..31c40bc --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/index.ts @@ -0,0 +1,24 @@ +/** + * Stellar blockchain support for x402 protocol. + * + * This package provides Stellar network support for the x402 payment protocol, + * including client signing, server validation, and facilitator settlement. + * + * @module + */ + +// Exact scheme client +export { ExactStellarScheme } from "./exact"; + +// Types +export * from "./types"; + +// Constants +export * from "./constants"; + +// Signers +export * from "./signer"; + +// Utilities +export * from "./utils"; +export * from "./shared"; diff --git a/typescript/packages/mechanisms/stellar/src/shared.ts b/typescript/packages/mechanisms/stellar/src/shared.ts new file mode 100644 index 0000000..7cbb83b --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/shared.ts @@ -0,0 +1,134 @@ +import { Transaction, Address, Operation, xdr } from "@stellar/stellar-sdk"; +import { Api, assembleTransaction } from "@stellar/stellar-sdk/rpc"; + +/** + * Handles the simulation result of a Stellar transaction. + * + * @param simulation - The simulation result to handle + * @throws An error if the simulation result is of type "RESTORE" or "ERROR" + */ +export function handleSimulationResult(simulation?: Api.SimulateTransactionResponse) { + if (!simulation) { + throw new Error("Simulation result is undefined"); + } + + if (Api.isSimulationRestore(simulation)) { + throw new Error( + `Stellar simulation result has type "RESTORE" with restorePreamble: ${simulation.restorePreamble}`, + ); + } + + if (Api.isSimulationError(simulation)) { + const msg = `Stellar simulation failed${simulation.error ? ` with error message: ${simulation.error}` : ""}`; + + throw new Error(msg); + } +} + +/** + * Analysis result of transaction signers + */ +export type ContractSigners = { + /** Accounts that have already signed auth entries */ + alreadySigned: string[]; + /** Accounts that still need to sign auth entries */ + pendingSignature: string[]; +}; + +/** + * Input parameters for gathering auth entry signature status + */ +export type GatherAuthEntrySignatureStatusInput = { + /** The transaction to analyze */ + transaction: Transaction; + /** Optional simulation response to assemble with transaction before analysis */ + simulationResponse?: Api.SimulateTransactionResponse; + /** Whether to simulate/assemble the transaction with simulation data (default: true if simulationResponse was not provided) */ + simulate?: boolean; +}; + +/** + * Gathers the signature status of auth entries in a Stellar transaction. + * + * This function inspects the auth entries in the transaction's InvokeHostFunction + * operation and categorizes them based on their signature status. + * + * @param input - Input containing transaction and optional simulation data + * @param input.transaction - The transaction to analyze + * @param input.simulationResponse - Optional simulation response to assemble with transaction before analysis + * @param input.simulate - Whether to simulate/assemble the transaction with simulation data (default: true if simulationResponse was not provided) + * @returns ContractSigners with arrays of signed and pending signer addresses + * @throws Error if transaction doesn't have exactly one InvokeHostFunction operation + * + * @example + * ```ts + * const status = gatherAuthEntrySignatureStatus({ + * transaction: tx, + * simulationResponse: simResult + * }); + * console.log('Already signed:', status.alreadySigned); + * console.log('Pending:', status.pendingSignature); + * ``` + */ +export function gatherAuthEntrySignatureStatus({ + transaction, + simulationResponse, + simulate, +}: GatherAuthEntrySignatureStatusInput): ContractSigners { + // Determine if we should assemble with simulation + const shouldAssemble = simulate ?? simulationResponse !== undefined; + let assembledTx = transaction; + + // Assemble transaction with simulation if requested + if (shouldAssemble && simulationResponse) { + const assembledTxBuilder = assembleTransaction(transaction, simulationResponse); + assembledTx = assembledTxBuilder.build(); + } + + // Validate transaction structure + if (assembledTx.operations.length !== 1) { + throw new Error( + `Expected transaction with exactly one operation, got ${assembledTx.operations.length}`, + ); + } + + const operation = assembledTx.operations[0]; + if (operation.type !== "invokeHostFunction") { + throw new Error(`Expected InvokeHostFunction operation, got ${operation.type}`); + } + + const invokeOp = operation as Operation.InvokeHostFunction; + + const alreadySigned: string[] = []; + const pendingSignature: string[] = []; + + for (const entry of invokeOp.auth ?? []) { + const credentialsType = entry.credentials().switch(); + + // Skip source account credentials - these use the transaction source + if (credentialsType === xdr.SorobanCredentialsType.sorobanCredentialsSourceAccount()) { + continue; + } + + // Handle address-based credentials + if (credentialsType === xdr.SorobanCredentialsType.sorobanCredentialsAddress()) { + const addressCredentials = entry.credentials().address(); + const address = Address.fromScAddress(addressCredentials.address()).toString(); + const signature = addressCredentials.signature(); + + // Check if already signed (signature is not scvVoid) + const isSigned = signature.switch().name !== "scvVoid"; + + if (isSigned) { + alreadySigned.push(address); + } else { + pendingSignature.push(address); + } + } + } + + return { + alreadySigned: [...new Set(alreadySigned)], // Remove duplicates + pendingSignature: [...new Set(pendingSignature)], + }; +} diff --git a/typescript/packages/mechanisms/stellar/src/signer.ts b/typescript/packages/mechanisms/stellar/src/signer.ts new file mode 100644 index 0000000..9a6498b --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/signer.ts @@ -0,0 +1,102 @@ +import { Keypair } from "@stellar/stellar-sdk"; +import { basicNodeSigner, SignAuthEntry, SignTransaction } from "@stellar/stellar-sdk/contract"; +import { STELLAR_TESTNET_CAIP2 } from "./constants"; +import { getNetworkPassphrase } from "./utils"; +import type { Network } from "@x402/core/types"; + +/** + * Ed25519 signer for Stellar transactions and auth entries. + * + * Implements SEP-43 interface (except signMessage). + * + * @see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0043.md + */ +export type Ed25519Signer = { + address: string; + signAuthEntry: SignAuthEntry; + signTransaction: SignTransaction; +}; + +/** + * Facilitator signer for Stellar transactions. + * + * Alias for Ed25519Signer. Used by x402 facilitators to verify and settle payments. + * + * @see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0043.md + */ +export type FacilitatorStellarSigner = Ed25519Signer; + +/** + * Client signer for Stellar transactions. + * + * Used by x402 clients to sign auth entries. Supports both classic (G) and contract (C) accounts. + * signTransaction is optional for client signers. + * + * @see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0043.md + */ +export type ClientStellarSigner = { + address: string; + signAuthEntry: SignAuthEntry; + signTransaction?: SignTransaction; +}; + +/** + * Creates an Ed25519 signer for the given Stellar network. + * + * @param privateKey - Stellar classic (G) account private key + * @param defaultNetwork - Is the network the signTransactiopn method will default to if no network is provided. Must use the CAIP-2 format identifier. + * @returns Ed25519 signer implementing SEP-43 interface (except signMessage) + * @see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0043.md + */ +export function createEd25519Signer( + privateKey: string, + defaultNetwork: Network = STELLAR_TESTNET_CAIP2, +): Ed25519Signer { + const kp = Keypair.fromSecret(privateKey); + const networkPassphrase = getNetworkPassphrase(defaultNetwork); + + const address = kp.publicKey(); + const { signAuthEntry, signTransaction } = basicNodeSigner(kp, networkPassphrase); + + return { + address, + signAuthEntry, + signTransaction, + }; +} + +/** + * Type guard for FacilitatorStellarSigner. + * + * Checks for required methods: address, signAuthEntry, signTransaction. + * + * @param signer - Value to check + * @returns `true` if signer is a FacilitatorStellarSigner + */ +export function isFacilitatorStellarSigner(signer: unknown): signer is FacilitatorStellarSigner { + if (typeof signer !== "object" || signer === null) return false; + const s = signer as Record; + return ( + typeof s.address === "string" && + typeof s.signAuthEntry === "function" && + typeof s.signTransaction === "function" + ); +} + +/** + * Type guard for ClientStellarSigner. + * + * Checks for required methods: address, signAuthEntry. signTransaction is optional. + * + * @param signer - Value to check + * @returns `true` if signer is a ClientStellarSigner + */ +export function isClientStellarSigner(signer: unknown): signer is ClientStellarSigner { + if (typeof signer !== "object" || signer === null) return false; + const s = signer as Record; + return ( + typeof s.address === "string" && + typeof s.signAuthEntry === "function" && + (s.signTransaction === undefined || typeof s.signTransaction === "function") + ); +} diff --git a/typescript/packages/mechanisms/stellar/src/types.ts b/typescript/packages/mechanisms/stellar/src/types.ts new file mode 100644 index 0000000..0349610 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/types.ts @@ -0,0 +1,9 @@ +/** + * Exact Stellar payload structure containing a base64 encoded Stellar transaction + */ +export type ExactStellarPayloadV2 = { + /** + * Base64 encoded Stellar transaction + */ + transaction: string; +}; diff --git a/typescript/packages/mechanisms/stellar/src/utils.ts b/typescript/packages/mechanisms/stellar/src/utils.ts new file mode 100644 index 0000000..2005cc2 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/utils.ts @@ -0,0 +1,215 @@ +import { Horizon, rpc } from "@stellar/stellar-sdk"; +import { + DEFAULT_PUBNET_HORIZON_URL, + DEFAULT_TESTNET_HORIZON_URL, + DEFAULT_TESTNET_RPC_URL, + DEFAULT_TOKEN_DECIMALS, + STELLAR_ASSET_ADDRESS_REGEX, + STELLAR_DESTINATION_ADDRESS_REGEX, + STELLAR_NETWORK_TO_PASSPHRASE, + STELLAR_PUBNET_CAIP2, + STELLAR_TESTNET_CAIP2, + USDC_PUBNET_ADDRESS, + USDC_TESTNET_ADDRESS, +} from "./constants"; +import type { Network } from "@x402/core/types"; + +export const DEFAULT_ESTIMATED_LEDGER_SECONDS = 5; +const HORIZON_LEDGERS_SAMPLE_SIZE = 20; + +/** + * Configuration for RPC client connections + */ +export interface RpcConfig { + /** Custom RPC URL to use instead of defaults */ + url?: string; +} + +/** + * Checks if a network is a Stellar network + * + * @param network - The CAIP-2 network identifier + * @returns `true` if the network is a Stellar network, `false` otherwise + */ +export function isStellarNetwork(network: Network): boolean { + return STELLAR_NETWORK_TO_PASSPHRASE.has(network); +} + +/** + * Validates a Stellar destination address (G-account, C-account, or M-account) + * + * @param address - Stellar destination address to validate + * @returns `true` if the address is valid, `false` otherwise + */ +export function validateStellarDestinationAddress(address: string): boolean { + return STELLAR_DESTINATION_ADDRESS_REGEX.test(address); +} + +/** + * Validates a Stellar asset/contract address (C-account only) + * + * @param address - Stellar asset address to validate + * @returns `true` if the address is valid, `false` otherwise + */ +export function validateStellarAssetAddress(address: string): boolean { + return STELLAR_ASSET_ADDRESS_REGEX.test(address); +} + +/** + * Gets the network passphrase for a given Stellar network + * + * @param network - The CAIP-2 network identifier + * @returns The network passphrase string + * @throws {Error} If the network is not a known Stellar network + */ +export function getNetworkPassphrase(network: Network): string { + const networkPassphrase = STELLAR_NETWORK_TO_PASSPHRASE.get(network); + if (!networkPassphrase) { + throw new Error(`Unknown Stellar network: ${network}`); + } + return networkPassphrase; +} + +/** + * Gets the RPC URL for a given Stellar network + * + * @param network - The CAIP-2 network identifier + * @param rpcConfig - Optional RPC configuration with custom URL + * @returns The RPC URL string + * @throws {Error} If the network is unknown or mainnet RPC URL is not provided + */ +export function getRpcUrl(network: Network, rpcConfig?: RpcConfig): string { + const customRpcUrl = rpcConfig?.url; + switch (network) { + case STELLAR_TESTNET_CAIP2: + return customRpcUrl || DEFAULT_TESTNET_RPC_URL; + case STELLAR_PUBNET_CAIP2: + if (!customRpcUrl) { + throw new Error( + "Stellar mainnet requires a non-empty rpcUrl. For a list of RPC providers, see https://developers.stellar.org/docs/data/apis/rpc/providers#publicly-accessible-apis", + ); + } + return customRpcUrl; + default: + throw new Error(`Unknown Stellar network: ${network}`); + } +} + +/** + * Creates a Soroban RPC client for the given network + * + * @param network - The CAIP-2 network identifier + * @param rpcConfig - Optional RPC configuration with custom URL + * @returns A configured Soroban RPC Server instance + * @throws {Error} If the network is not a valid Stellar network + */ +export function getRpcClient(network: Network, rpcConfig?: RpcConfig): rpc.Server { + const rpcUrl = getRpcUrl(network, rpcConfig); + return new rpc.Server(rpcUrl, { + allowHttp: network === STELLAR_TESTNET_CAIP2, // Allow HTTP for testnet + }); +} + +/** + * Creates a Horizon SDK client for the given network. + * + * @param network - The CAIP-2 network identifier + * @returns A configured Horizon.Server instance + * @throws {Error} If the network is unknown + */ +export function getHorizonClient(network: Network): Horizon.Server { + switch (network) { + case STELLAR_TESTNET_CAIP2: + return new Horizon.Server(DEFAULT_TESTNET_HORIZON_URL); + case STELLAR_PUBNET_CAIP2: + return new Horizon.Server(DEFAULT_PUBNET_HORIZON_URL); + default: + throw new Error(`Unknown Stellar network: ${network}`); + } +} + +/** + * Estimates ledger close time by fetching the most recent ledgers from Horizon. + * + * Uses the Horizon SDK's ledger query builder which is significantly faster + * than the Soroban RPC `getLedgers` method for this purpose. + * + * @param network - The CAIP-2 network identifier + * @returns Estimated seconds per ledger, or DEFAULT_ESTIMATED_LEDGER_SECONDS (5) on error + */ +export async function getEstimatedLedgerCloseTimeSeconds(network: Network): Promise { + try { + const horizon = getHorizonClient(network); + const page = await horizon.ledgers().limit(HORIZON_LEDGERS_SAMPLE_SIZE).order("desc").call(); + const records = page.records; + if (!records || records.length < 2) return DEFAULT_ESTIMATED_LEDGER_SECONDS; + + const newestTs = new Date(records[0].closed_at).getTime() / 1000; + const oldestTs = new Date(records[records.length - 1].closed_at).getTime() / 1000; + const intervals = records.length - 1; + return Math.ceil((newestTs - oldestTs) / intervals); + } catch { + return DEFAULT_ESTIMATED_LEDGER_SECONDS; + } +} + +/** + * Gets the default USDC contract address for a network + * + * @param network - The CAIP-2 network identifier + * @returns The USDC contract address for the network + * @throws {Error} If the network doesn't have a configured USDC address + */ +export function getUsdcAddress(network: Network): string { + switch (network) { + case STELLAR_PUBNET_CAIP2: + return USDC_PUBNET_ADDRESS; + case STELLAR_TESTNET_CAIP2: + return USDC_TESTNET_ADDRESS; + default: + throw new Error(`No USDC address configured for network: ${network}`); + } +} + +/** + * Converts a decimal amount to token smallest units + * + * Handles both regular decimal strings (e.g., "0.10") and scientific notation (e.g., "1e-7"). + * The result is truncated (not rounded) to the specified number of decimal places. + * + * @param decimalAmount - The decimal amount as a string + * @param decimals - Number of decimal places for the token (default: 7 for USDC) + * @returns The amount in smallest units as a string with leading zeros removed + * @throws {Error} If the amount is invalid or decimals is out of range + * + * @example + * ```ts + * convertToTokenAmount("0.1", 7) // "1000000" + * convertToTokenAmount("1.5", 7) // "15000000" + * convertToTokenAmount("1e-7", 7) // "1" + * convertToTokenAmount("1.5", 0) // "1" (truncated) + * ``` + */ +export function convertToTokenAmount( + decimalAmount: string, + decimals: number = DEFAULT_TOKEN_DECIMALS, +): string { + const amount = parseFloat(decimalAmount); + if (isNaN(amount)) { + throw new Error(`Invalid amount: ${decimalAmount}`); + } + + if (decimals < 0 || decimals > 20) { + throw new Error(`Decimals must be between 0 and 20, got ${decimals}`); + } + + // Normalize scientific notation to fixed decimal string + const normalizedDecimal = /[eE]/.test(decimalAmount) + ? amount.toFixed(Math.max(decimals, 20)) + : decimalAmount; + + const [intPart, decPart = ""] = normalizedDecimal.split("."); + const paddedDec = decPart.padEnd(decimals, "0").slice(0, decimals); + + return (intPart + paddedDec).replace(/^0+/, "") || "0"; +} diff --git a/typescript/packages/mechanisms/stellar/test/integrations/exact-stellar.test.ts b/typescript/packages/mechanisms/stellar/test/integrations/exact-stellar.test.ts new file mode 100644 index 0000000..5a65262 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/integrations/exact-stellar.test.ts @@ -0,0 +1,596 @@ +import { x402Client, x402HTTPClient } from "@x402/core/client"; +import { x402Facilitator } from "@x402/core/facilitator"; +import { + HTTPAdapter, + HTTPResponseInstructions, + x402HTTPResourceServer, + x402ResourceServer, + FacilitatorClient, +} from "@x402/core/server"; +import { + AssetAmount, + Network, + PaymentPayload, + PaymentRequirements, + VerifyResponse, + SettleResponse, + SupportedResponse, +} from "@x402/core/types"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { createEd25519Signer, Ed25519Signer, STELLAR_TESTNET_CAIP2 } from "../../src"; +import { ExactStellarScheme as ExactStellarClient } from "../../src/exact/client"; +import { ExactStellarScheme as ExactStellarFacilitator } from "../../src/exact/facilitator"; +import { ExactStellarScheme as ExactStellarServer } from "../../src/exact/server"; +import type { ExactStellarPayloadV2 } from "../../src/types"; + +// Load private keys and addresses from environment +const CLIENT_PRIVATE_KEY = process.env.CLIENT_PRIVATE_KEY; +const FACILITATOR_PRIVATE_KEY = process.env.FACILITATOR_PRIVATE_KEY; +const FACILITATOR_ADDRESS = process.env.FACILITATOR_ADDRESS; +const RESOURCE_SERVER_ADDRESS = process.env.RESOURCE_SERVER_ADDRESS; +const XLM_TESTNET_ASSET = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; + +async function xlmFallbackParser(amount: number, network: string): Promise { + if (network === STELLAR_TESTNET_CAIP2) { + return { + amount: Math.round(amount * 1e7).toString(), + asset: XLM_TESTNET_ASSET, + extra: {}, + }; + } + return null; +} + +const missingEnvVars = + !CLIENT_PRIVATE_KEY || + !FACILITATOR_PRIVATE_KEY || + !FACILITATOR_ADDRESS || + !RESOURCE_SERVER_ADDRESS; + +const HORIZON_TESTNET = "https://horizon-testnet.stellar.org"; +const FRIENDBOT_URL = "https://friendbot.stellar.org"; +const STELLAR_EXPERT_TESTNET_TX = "https://stellar.expert/explorer/testnet/tx"; + +function logStellarExpertTxUrl(txHash: string): void { + console.log(`Stellar Expert (testnet): ${STELLAR_EXPERT_TESTNET_TX}/${txHash}`); +} + +async function fundOneAccount(address: string): Promise { + const res = await fetch(`${HORIZON_TESTNET}/accounts/${address}`); + if (res.status === 404) { + console.log(`Account ${address} not found, funding with Friendbot\n`); + const fb = await fetch(`${FRIENDBOT_URL}?addr=${encodeURIComponent(address)}`); + if (!fb.ok) { + const body = await fb.text(); + throw new Error(`Friendbot failed for ${address}: ${fb.status} ${body}`); + } + console.log(`Account ${address} funded with Friendbot\n`); + } else if (!res.ok) { + throw new Error(`Horizon account check failed for ${address}: ${res.status}`); + } +} + +async function ensureAccountsFunded(addresses: string[]): Promise { + await Promise.all(addresses.map(fundOneAccount)); +} + +/** + * Stellar Facilitator Client wrapper + * Wraps the x402Facilitator for use with x402ResourceServer + */ +class StellarFacilitatorClient implements FacilitatorClient { + readonly scheme = "exact"; + readonly network = STELLAR_TESTNET_CAIP2; + readonly x402Version = 2; + + /** + * Creates a new StellarFacilitatorClient instance + * + * @param facilitator - The x402 facilitator to wrap + */ + constructor(private readonly facilitator: x402Facilitator) {} + + /** + * Verifies a payment payload + * + * @param paymentPayload - The payment payload to verify + * @param paymentRequirements - The payment requirements + * @returns Promise resolving to verification response + */ + verify( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise { + return this.facilitator.verify(paymentPayload, paymentRequirements); + } + + /** + * Settles a payment + * + * @param paymentPayload - The payment payload to settle + * @param paymentRequirements - The payment requirements + * @returns Promise resolving to settlement response + */ + settle( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise { + return this.facilitator.settle(paymentPayload, paymentRequirements); + } + + /** + * Gets supported payment kinds + * + * @returns Promise resolving to supported response + */ + getSupported(): Promise { + // Delegate to actual facilitator to get real supported kinds + return Promise.resolve(this.facilitator.getSupported() as SupportedResponse); + } +} + +/** + * Build Stellar payment requirements for testing + * + * @param payTo - The recipient address + * @param amount - The payment amount in smallest units + * @param network - The network identifier (defaults to Stellar Testnet) + * @returns Payment requirements object + */ +function buildStellarPaymentRequirements( + payTo: string, + amount: string, + network: Network = STELLAR_TESTNET_CAIP2, +): PaymentRequirements { + return { + scheme: "exact", + network, + asset: XLM_TESTNET_ASSET, + amount, + payTo, + maxTimeoutSeconds: 3600, + extra: { areFeesSponsored: true }, + }; +} + +/** + * Helper to check if an error is due to insufficient balance + */ +function isInsufficientBalanceError(error: unknown): boolean { + if (error instanceof Error) { + return ( + error.message.includes("resulting balance is not within the allowed range") || + error.message.includes("insufficient balance") || + error.message.includes("Error(Contract, #10)") + ); + } + return false; +} + +describe.skipIf(missingEnvVars)("Stellar Integration Tests", () => { + let clientAddress: string; + let clientSigner: Ed25519Signer; + let facilitatorSigner: Ed25519Signer; + beforeAll(async () => { + clientSigner = createEd25519Signer(CLIENT_PRIVATE_KEY!, STELLAR_TESTNET_CAIP2); + clientAddress = clientSigner.address; + + facilitatorSigner = createEd25519Signer(FACILITATOR_PRIVATE_KEY, STELLAR_TESTNET_CAIP2); + + await ensureAccountsFunded([FACILITATOR_ADDRESS, RESOURCE_SERVER_ADDRESS, clientAddress]); + }); + + describe("x402Client / x402ResourceServer / x402Facilitator - Stellar Flow", () => { + let client: x402Client; + let server: x402ResourceServer; + let facilitatorClient: StellarFacilitatorClient; + + beforeEach(async () => { + const stellarClient = new ExactStellarClient(clientSigner); + client = new x402Client().register(STELLAR_TESTNET_CAIP2, stellarClient); + + const stellarFacilitator = new ExactStellarFacilitator([facilitatorSigner]); + const facilitator = new x402Facilitator().register(STELLAR_TESTNET_CAIP2, stellarFacilitator); + + facilitatorClient = new StellarFacilitatorClient(facilitator); + server = new x402ResourceServer(facilitatorClient); + server.register(STELLAR_TESTNET_CAIP2, new ExactStellarServer()); + await server.initialize(); + }); + + it("server should successfully verify and settle a Stellar payment from a client", async () => { + // Server - builds PaymentRequired response + const accepts = [buildStellarPaymentRequirements(RESOURCE_SERVER_ADDRESS, "1000")]; + const resource = { + url: "https://company.co", + description: "Company Co. resource", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + // Client - responds with PaymentPayload response + let paymentPayload: PaymentPayload; + try { + paymentPayload = await client.createPaymentPayload(paymentRequired); + } catch (error) { + if (isInsufficientBalanceError(error)) { + throw new Error( + `Insufficient balance on testnet account ${clientAddress}. ` + + `Asset: ${XLM_TESTNET_ASSET}. Ensure the account is funded (e.g. via Friendbot).`, + ); + } + throw error; + } + + expect(paymentPayload).toBeDefined(); + expect(paymentPayload.x402Version).toBe(2); + expect(paymentPayload.accepted.scheme).toBe("exact"); + + // Verify the payload structure + const stellarPayload = paymentPayload.payload as ExactStellarPayloadV2; + expect(stellarPayload.transaction).toBeDefined(); + expect(typeof stellarPayload.transaction).toBe("string"); + expect(stellarPayload.transaction.length).toBeGreaterThan(0); + + // Server - maps payment payload to payment requirements + const accepted = server.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + const verifyResponse = await server.verifyPayment(paymentPayload, accepted!); + + expect(verifyResponse.isValid).toBe(true); + expect(verifyResponse.payer).toBe(clientAddress); + + // Server does work here + const settleResponse = await server.settlePayment(paymentPayload, accepted!); + expect(settleResponse.success).toBe(true); + expect(settleResponse.network).toBe(STELLAR_TESTNET_CAIP2); + expect(settleResponse.transaction).toBeDefined(); + expect(settleResponse.payer).toBe(clientAddress); + logStellarExpertTxUrl(settleResponse.transaction); + }); + }); + + describe("x402HTTPClient / x402HTTPResourceServer / x402Facilitator - Stellar Flow", () => { + let client: x402HTTPClient; + let httpServer: x402HTTPResourceServer; + + const routes = { + "/api/protected": { + accepts: { + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: { amount: "1000", asset: XLM_TESTNET_ASSET }, + network: STELLAR_TESTNET_CAIP2 as Network, + }, + description: "Access to protected API", + mimeType: "application/json", + }, + }; + + const mockAdapter: HTTPAdapter = { + getHeader: () => { + return undefined; + }, + getMethod: () => "GET", + getPath: () => "/api/protected", + getUrl: () => "https://example.com/api/protected", + getAcceptHeader: () => "application/json", + getUserAgent: () => "TestClient/1.0", + }; + + beforeEach(async () => { + const stellarFacilitator = new ExactStellarFacilitator([facilitatorSigner]); + const facilitator = new x402Facilitator().register(STELLAR_TESTNET_CAIP2, stellarFacilitator); + + const facilitatorClient = new StellarFacilitatorClient(facilitator); + + const stellarClient = new ExactStellarClient(clientSigner); + const paymentClient = new x402Client().register(STELLAR_TESTNET_CAIP2, stellarClient); + client = new x402HTTPClient(paymentClient) as x402HTTPClient; + + // Create resource server and register schemes (composition pattern) + const ResourceServer = new x402ResourceServer(facilitatorClient); + ResourceServer.register(STELLAR_TESTNET_CAIP2, new ExactStellarServer()); + await ResourceServer.initialize(); // Initialize to fetch supported kinds + + httpServer = new x402HTTPResourceServer(ResourceServer, routes); + }); + + it("middleware should successfully verify and settle a Stellar payment from an http client", async () => { + // Middleware creates a PaymentRequired response + const context = { + adapter: mockAdapter, + path: "/api/protected", + method: "GET", + }; + + // No payment made, get PaymentRequired response & header + const httpProcessResult = (await httpServer.processHTTPRequest(context))!; + expect(httpProcessResult.type).toBe("payment-error"); + + const initial402Response = ( + httpProcessResult as { type: "payment-error"; response: HTTPResponseInstructions } + ).response; + + expect(initial402Response).toBeDefined(); + expect(initial402Response.status).toBe(402); + expect(initial402Response.headers).toBeDefined(); + expect(initial402Response.headers["PAYMENT-REQUIRED"]).toBeDefined(); + + // Client responds to PaymentRequired and submits a request with a PaymentPayload + const paymentRequired = client.getPaymentRequiredResponse( + name => initial402Response.headers[name], + initial402Response.body, + ); + let paymentPayload: PaymentPayload; + try { + paymentPayload = await client.createPaymentPayload(paymentRequired); + } catch (error) { + if (isInsufficientBalanceError(error)) { + throw new Error( + `Insufficient balance on testnet account ${clientAddress}. ` + + `Asset: ${XLM_TESTNET_ASSET}. Ensure the account is funded (e.g. via Friendbot).`, + ); + } + throw error; + } + + expect(paymentPayload).toBeDefined(); + expect(paymentPayload.accepted.scheme).toBe("exact"); + + const requestHeaders = await client.encodePaymentSignatureHeader(paymentPayload); + + // Middleware handles PAYMENT-SIGNATURE request + mockAdapter.getHeader = (name: string) => { + if (name === "PAYMENT-SIGNATURE") { + return requestHeaders["PAYMENT-SIGNATURE"]; + } + return undefined; + }; + + const httpProcessResult2 = await httpServer.processHTTPRequest(context); + + // No need to respond, can continue with request + expect(httpProcessResult2.type).toBe("payment-verified"); + const { + paymentPayload: verifiedPaymentPayload, + paymentRequirements: verifiedPaymentRequirements, + } = httpProcessResult2 as { + type: "payment-verified"; + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + }; + + expect(verifiedPaymentPayload).toBeDefined(); + expect(verifiedPaymentRequirements).toBeDefined(); + + const settlementResult = await httpServer.processSettlement( + verifiedPaymentPayload, + verifiedPaymentRequirements, + ); + + expect(settlementResult).toBeDefined(); + expect(settlementResult.success).toBe(true); + + if (settlementResult.success) { + expect(settlementResult.headers).toBeDefined(); + expect(settlementResult.headers["PAYMENT-RESPONSE"]).toBeDefined(); + logStellarExpertTxUrl(settlementResult.transaction); + } + }); + }); + + describe("Price Parsing Integration", () => { + let server: x402ResourceServer; + let stellarServer: ExactStellarServer; + + beforeEach(async () => { + const facilitator = new x402Facilitator().register( + STELLAR_TESTNET_CAIP2, + new ExactStellarFacilitator([facilitatorSigner]), + ); + + const facilitatorClient = new StellarFacilitatorClient(facilitator); + server = new x402ResourceServer(facilitatorClient); + + stellarServer = new ExactStellarServer(); + server.register(STELLAR_TESTNET_CAIP2, stellarServer); + await server.initialize(); + }); + + it("should parse Money formats and build payment requirements", async () => { + stellarServer.registerMoneyParser(xlmFallbackParser); + + // Test different Money formats + const testCases = [ + { input: "$1.00", expectedAmount: "10000000" }, + { input: "1.50", expectedAmount: "15000000" }, + { input: 2.5, expectedAmount: "25000000" }, + ]; + + for (const testCase of testCases) { + const requirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: testCase.input, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + + expect(requirements).toHaveLength(1); + expect(requirements[0].amount).toBe(testCase.expectedAmount); + expect(requirements[0].asset).toBe(XLM_TESTNET_ASSET); + } + }); + + it("should handle AssetAmount pass-through", async () => { + const customAsset = { + amount: "50000000", + asset: "CUSTOMTOKENMINT111111111111111111111111111111", + extra: { foo: "bar" }, + }; + + const requirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: customAsset, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + + expect(requirements).toHaveLength(1); + expect(requirements[0].amount).toBe("50000000"); + expect(requirements[0].asset).toBe("CUSTOMTOKENMINT111111111111111111111111111111"); + expect(requirements[0].extra?.foo).toBe("bar"); + }); + + it("should use registerMoneyParser for custom conversion", async () => { + stellarServer + .registerMoneyParser(async (amount, _network) => { + if (amount > 100) { + return { + amount: (amount * 1e7).toString(), + asset: "CUSTOMLARGETOKENMINT111111111111111111111", + extra: { token: "CUSTOM", tier: "large" }, + }; + } + return null; + }) + .registerMoneyParser(xlmFallbackParser); + + // Test large amount - should use custom parser + const largeRequirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: 150, // Large amount + network: STELLAR_TESTNET_CAIP2 as Network, + }); + + expect(largeRequirements[0].amount).toBe((150 * 1e7).toString()); + expect(largeRequirements[0].asset).toBe("CUSTOMLARGETOKENMINT111111111111111111111"); + expect(largeRequirements[0].extra?.token).toBe("CUSTOM"); + expect(largeRequirements[0].extra?.tier).toBe("large"); + + // Test small amount - should use default (XLM) + const smallRequirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: 50, // Small amount + network: STELLAR_TESTNET_CAIP2 as Network, + }); + + expect(smallRequirements[0].amount).toBe("500000000"); // 50 * 1e7 (7 decimals) + expect(smallRequirements[0].asset).toBe(XLM_TESTNET_ASSET); + }); + + it("should support multiple MoneyParser in chain", async () => { + stellarServer + .registerMoneyParser(async amount => { + if (amount > 1000) { + return { + amount: (amount * 1e7).toString(), + asset: "VIPTOKENMINT111111111111111111111111111111", + extra: { tier: "vip" }, + }; + } + return null; + }) + .registerMoneyParser(async amount => { + if (amount > 100) { + return { + amount: (amount * 1e7).toString(), + asset: "PREMIUMTOKENMINT1111111111111111111111111", + extra: { tier: "premium" }, + }; + } + return null; + }) + .registerMoneyParser(xlmFallbackParser); + // < 100 uses XLM fallback + + // VIP tier + const vipReq = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: 2000, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + expect(vipReq[0].extra?.tier).toBe("vip"); + expect(vipReq[0].asset).toBe("VIPTOKENMINT111111111111111111111111111111"); + + // Premium tier + const premiumReq = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: 500, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + expect(premiumReq[0].extra?.tier).toBe("premium"); + expect(premiumReq[0].asset).toBe("PREMIUMTOKENMINT1111111111111111111111111"); + + // Standard tier (default) + const standardReq = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: 50, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + expect(standardReq[0].asset).toBe(XLM_TESTNET_ASSET); + }); + + it("should work with async MoneyParser (e.g., exchange rate lookup)", async () => { + const mockExchangeRate = 0.98; + + stellarServer.registerMoneyParser(async (amount, _network) => { + await new Promise(resolve => setTimeout(resolve, 10)); + + const convertedAmount = amount * mockExchangeRate; + return { + amount: Math.floor(convertedAmount * 1e7).toString(), + asset: XLM_TESTNET_ASSET, + extra: { + exchangeRate: mockExchangeRate, + originalUSD: amount, + }, + }; + }); + + const requirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: 100, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + + // 100 * 0.98 = 98 (XLM, 7 decimals) + expect(requirements[0].amount).toBe("980000000"); + expect(requirements[0].extra?.exchangeRate).toBe(0.98); + expect(requirements[0].extra?.originalUSD).toBe(100); + }); + + it("should avoid floating-point rounding error", async () => { + stellarServer.registerMoneyParser(xlmFallbackParser); + + // Test different Money formats + const testCases = [ + { input: "$4.02", expectedAmount: "40200000" }, + { input: "4.02", expectedAmount: "40200000" }, + { input: "4.02 XLM", expectedAmount: "40200000" }, + { input: "4.02 USD", expectedAmount: "40200000" }, + { input: 4.02, expectedAmount: "40200000" }, + ]; + + for (const testCase of testCases) { + const requirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: testCase.input, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + + expect(requirements).toHaveLength(1); + expect(requirements[0].amount).toBe(testCase.expectedAmount); + expect(requirements[0].asset).toBe(XLM_TESTNET_ASSET); + } + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/client.test.ts b/typescript/packages/mechanisms/stellar/test/unit/client.test.ts new file mode 100644 index 0000000..4467c67 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/client.test.ts @@ -0,0 +1,262 @@ +import { nativeToScVal } from "@stellar/stellar-sdk"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { STELLAR_PUBNET_CAIP2, STELLAR_TESTNET_CAIP2 } from "../../src/constants"; +import { ExactStellarScheme } from "../../src/exact/client/scheme"; +import * as stellarUtils from "../../src/utils"; +import type { ClientStellarSigner } from "../../src/signer"; +import type { RpcConfig } from "../../src/utils"; +import type { PaymentRequirements } from "@x402/core/types"; + +const { mockAssembledTransactionBuild } = vi.hoisted(() => ({ + mockAssembledTransactionBuild: vi.fn(), +})); + +vi.mock("@stellar/stellar-sdk", async () => { + const actual = + await vi.importActual("@stellar/stellar-sdk"); + return { + ...actual, + contract: { + ...actual.contract, + AssembledTransaction: { + ...actual.contract.AssembledTransaction, + build: mockAssembledTransactionBuild, + }, + }, + }; +}); + +vi.mock("../../src/utils", async () => { + const actual = await vi.importActual("../../src/utils"); + return { + ...actual, + getEstimatedLedgerCloseTimeSeconds: vi.fn().mockResolvedValue(5), + getRpcUrl: vi.fn(), + getRpcClient: vi.fn(), + isStellarNetwork: vi.fn(), + validateStellarAssetAddress: vi.fn(), + validateStellarDestinationAddress: vi.fn(), + }; +}); + +describe("ExactStellarScheme", () => { + const mockSignerAddress = "GBBO4ZDDZTSM2IUKQYBAST3CFHNPFXECGEFTGWTA2WELR2BIWDK57UVE"; + const mockSigner: ClientStellarSigner = { + address: mockSignerAddress, + signAuthEntry: vi.fn().mockResolvedValue({ signedAuthEntry: "signed" }), + }; + + const validPaymentReq: PaymentRequirements = { + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + amount: "1000000", + payTo: "GCHEI4PQEFJOA27MNZRPQNLGURS6KASW76X5UZCUZIXCOJLKXYCXOR2W", + maxTimeoutSeconds: 60, + asset: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", + extra: { areFeesSponsored: true }, + }; + + const mockTransaction = { + simulation: {}, + needsNonInvokerSigningBy: vi.fn(), + signAuthEntries: vi.fn(), + simulate: vi.fn(), + built: { toXDR: vi.fn().mockReturnValue("mock-xdr") }, + }; + + const setupSuccessfulTransaction = () => { + mockTransaction.needsNonInvokerSigningBy.mockReturnValueOnce([mockSignerAddress]); + mockTransaction.needsNonInvokerSigningBy.mockReturnValueOnce([]); + }; + + const mockRpcServer = { + getLatestLedger: vi.fn().mockResolvedValue({ sequence: 100000 }), + }; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(stellarUtils.isStellarNetwork).mockReturnValue(true); + vi.mocked(stellarUtils.validateStellarAssetAddress).mockReturnValue(true); + vi.mocked(stellarUtils.validateStellarDestinationAddress).mockReturnValue(true); + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue("https://soroban-testnet.stellar.org"); + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockRpcServer as never); + mockAssembledTransactionBuild.mockResolvedValue(mockTransaction); + }); + + describe("constructor", () => { + it("should create instance with correct scheme and accept optional rpcConfig", () => { + expect(new ExactStellarScheme(mockSigner).scheme).toBe("exact"); + expect( + new ExactStellarScheme(mockSigner, { url: "https://custom-rpc.example.com" }).scheme, + ).toBe("exact"); + }); + }); + + describe("createPaymentPayload", () => { + it.each([ + ["unsupported scheme", { scheme: "invalid" }, "Unsupported scheme: invalid"], + ["unsupported network", { network: "base-sepolia" as never }, "Unsupported Stellar network"], + ["invalid payTo", { payTo: "invalid-address" }, "Invalid Stellar destination address"], + ["invalid asset", { asset: "invalid-asset" }, "Invalid Stellar asset address"], + ["invalid amount (negative)", { amount: "-100" }, "Invalid amount"], + ["invalid amount (zero)", { amount: "0" }, "Invalid amount"], + ["invalid amount (non-integer)", { amount: "100.5" }, "Invalid amount"], + ["invalid amount (empty string)", { amount: "" }, "Invalid amount"], + ["invalid amount (non-numeric)", { amount: "abc" }, "Invalid amount"], + ])("should throw for %s", async (_, overrides, expectedError) => { + const client = new ExactStellarScheme(mockSigner); + if ("network" in overrides && overrides.network) { + vi.mocked(stellarUtils.isStellarNetwork).mockReturnValue(false); + } + if ("payTo" in overrides && overrides.payTo) { + vi.mocked(stellarUtils.validateStellarDestinationAddress).mockReturnValue(false); + } + if ("asset" in overrides && overrides.asset) { + vi.mocked(stellarUtils.validateStellarAssetAddress).mockReturnValue(false); + } + + await expect( + client.createPaymentPayload(2, { ...validPaymentReq, ...overrides } as PaymentRequirements), + ).rejects.toThrow(expectedError); + }); + + it("should work with both TESTNET and PUBNET networks", async () => { + const client = new ExactStellarScheme(mockSigner); + setupSuccessfulTransaction(); + + await expect(client.createPaymentPayload(2, validPaymentReq)).resolves.toBeDefined(); + + const pubnetReq = { + ...validPaymentReq, + network: STELLAR_PUBNET_CAIP2, + } as PaymentRequirements; + vi.mocked(stellarUtils.getRpcUrl).mockReturnValueOnce("https://mainnet-rpc.example.com"); + setupSuccessfulTransaction(); + + await expect(client.createPaymentPayload(2, pubnetReq)).resolves.toBeDefined(); + }); + + it("should accept G, C, or M addresses for payTo", async () => { + const client = new ExactStellarScheme(mockSigner); + const addresses = [ + validPaymentReq.payTo, // G address + "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", // C address + "MA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KAAAAAAAAAAAAFKBA", // M address + ]; + + for (const address of addresses) { + setupSuccessfulTransaction(); + await expect( + client.createPaymentPayload(2, { ...validPaymentReq, payTo: address }), + ).resolves.toBeDefined(); + } + }); + + it("should use custom RPC URL from rpcConfig", async () => { + const client = new ExactStellarScheme(mockSigner, { url: "https://custom-rpc.example.com" }); + setupSuccessfulTransaction(); + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue("https://custom-rpc.example.com"); + + await client.createPaymentPayload(2, validPaymentReq); + + expect(stellarUtils.getRpcUrl).toHaveBeenCalledWith( + STELLAR_TESTNET_CAIP2, + expect.objectContaining({ url: "https://custom-rpc.example.com" }), + ); + }); + + it("should throw for PUBNET without custom RPC URL", async () => { + const client = new ExactStellarScheme(mockSigner); + const pubnetReq = { + ...validPaymentReq, + network: STELLAR_PUBNET_CAIP2, + } as PaymentRequirements; + vi.mocked(stellarUtils.getRpcUrl).mockImplementation( + (network: string, rpcConfig?: RpcConfig) => { + if (network === STELLAR_PUBNET_CAIP2 && !rpcConfig?.url) { + throw new Error( + "Stellar mainnet requires a non-empty rpcUrl. For a list of RPC providers, see https://developers.stellar.org/docs/data/apis/rpc/providers#publicly-accessible-apis", + ); + } + return "https://soroban-testnet.stellar.org"; + }, + ); + + await expect(client.createPaymentPayload(2, pubnetReq)).rejects.toThrow( + /Stellar mainnet requires a non-empty rpcUrl/, + ); + }); + + it.each([ + ["wrong signer", ["DIFFERENT_ADDRESS"]], + ["multiple signers", [mockSignerAddress, "ANOTHER_ADDRESS"]], + ])("should throw if %s is needed", async (_, signers) => { + const client = new ExactStellarScheme(mockSigner); + mockTransaction.needsNonInvokerSigningBy.mockReturnValueOnce(signers); + await expect(client.createPaymentPayload(2, validPaymentReq)).rejects.toThrow( + /Expected to sign with/, + ); + }); + + it("should throw if signers still missing after signing", async () => { + const client = new ExactStellarScheme(mockSigner); + mockTransaction.needsNonInvokerSigningBy.mockReturnValueOnce([mockSignerAddress]); + mockTransaction.needsNonInvokerSigningBy.mockReturnValueOnce(["STILL_MISSING"]); + + await expect(client.createPaymentPayload(2, validPaymentReq)).rejects.toThrow( + /unexpected signer\(s\) required/, + ); + }); + + it.each([ + [ + "TESTNET", + STELLAR_TESTNET_CAIP2, + "Test SDF Network ; September 2015", + "https://soroban-testnet.stellar.org", + undefined, + ], + [ + "PUBNET", + STELLAR_PUBNET_CAIP2, + "Public Global Stellar Network ; September 2015", + "https://mainnet-rpc.example.com", + { url: "https://mainnet-rpc.example.com" }, + ], + ])( + "should build, sign, and return correct payment for %s", + async (_, network, passphrase, rpcUrl, rpcConfig) => { + const client = new ExactStellarScheme(mockSigner, rpcConfig); + setupSuccessfulTransaction(); + const req = { ...validPaymentReq, network } as PaymentRequirements; + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue(rpcUrl); + + const result = await client.createPaymentPayload(2, req); + + expect(mockAssembledTransactionBuild).toHaveBeenCalledWith({ + contractId: req.asset, + method: "transfer", + args: [ + nativeToScVal(mockSignerAddress, { type: "address" }), + nativeToScVal(req.payTo, { type: "address" }), + nativeToScVal(req.amount, { type: "i128" }), + ], + networkPassphrase: passphrase, + rpcUrl, + parseResultXdr: expect.any(Function), + }); + // Expiration is calculated as currentLedger (100000) + ceil(maxTimeoutSeconds / 5) = 100012 + expect(mockTransaction.signAuthEntries).toHaveBeenCalledWith({ + address: mockSignerAddress, + signAuthEntry: mockSigner.signAuthEntry, + expiration: 100012, + }); + expect(mockTransaction.simulate).toHaveBeenCalled(); + expect(result).toEqual({ + x402Version: 2, + payload: { transaction: "mock-xdr" }, + }); + }, + ); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/constants.test.ts b/typescript/packages/mechanisms/stellar/test/unit/constants.test.ts new file mode 100644 index 0000000..27300f9 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/constants.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect } from "vitest"; +import { + STELLAR_ASSET_ADDRESS_REGEX, + STELLAR_DESTINATION_ADDRESS_REGEX, +} from "../../src/constants"; + +describe("STELLAR_DESTINATION_ADDRESS_REGEX", () => { + it("should match valid G-accounts (56 characters)", () => { + const validGAccounts = [ + "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + "G" + "A".repeat(27) + "2".repeat(28), + ]; + + validGAccounts.forEach(address => { + expect(STELLAR_DESTINATION_ADDRESS_REGEX.test(address)).toBe(true); + }); + }); + + it("should match valid C-accounts (56 characters)", () => { + const validCAccounts = [ + "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75", + "C" + "B".repeat(27) + "2".repeat(28), + ]; + + validCAccounts.forEach(address => { + expect(STELLAR_DESTINATION_ADDRESS_REGEX.test(address)).toBe(true); + }); + }); + + it("should match valid M-accounts (69 characters)", () => { + const validMAccounts = [ + "MA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KAAAAAAAAAAAAFKBA", + "M" + "C".repeat(34) + "3".repeat(34), + ]; + + validMAccounts.forEach(address => { + expect(STELLAR_DESTINATION_ADDRESS_REGEX.test(address)).toBe(true); + }); + }); + + it("should reject invalid Stellar addresses", () => { + const invalidAddresses = [ + "", // Empty string + "G", // Just prefix (too short) + "C", // Just prefix (too short) + "M", // Just prefix (too short) + "G" + "A".repeat(56), // G-account too long (57 chars) + "GA" + "2".repeat(53), // G-account too short (55 chars) + "C" + "B".repeat(56), // C-account too long (57 chars) + "CA" + "2".repeat(53), // C-account too short (55 chars) + "M" + "C".repeat(69), // M-account too long (70 chars) + "MA" + "3".repeat(66), // M-account too short (68 chars) + "XA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // Invalid prefix 'X' + "GE5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // Invalid second character + "gA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // Lowercase prefix + "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN ", // Space character + "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN-", // Hyphen character + "0xGA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // EVM-style prefix + "ME5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KAAAAAAAAAAAAFKBA", // invalid second character in M-account + ]; + + invalidAddresses.forEach(address => { + expect(STELLAR_DESTINATION_ADDRESS_REGEX.test(address)).toBe(false); + }); + }); +}); + +describe("STELLAR_ASSET_ADDRESS_REGEX", () => { + it("should match valid C-accounts (56 characters)", () => { + const validCAccounts = [ + "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75", + "C" + "B".repeat(27) + "2".repeat(28), + ]; + + validCAccounts.forEach(address => { + expect(STELLAR_ASSET_ADDRESS_REGEX.test(address)).toBe(true); + }); + }); + + it("should reject invalid Stellar addresses", () => { + const invalidAddresses = [ + "", // Empty string + "C", // Just prefix (too short) + "C" + "B".repeat(56), // C-account too long (57 chars) + "CA" + "2".repeat(53), // C-account too short (55 chars) + "XA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // Invalid prefix 'X' + "CE5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // Invalid second character + "cA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // Lowercase prefix + "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // G-account + "MA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KAAAAAAAAAAAAFKBA", // M-account + "0xGA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // EVM-style prefix + ]; + + invalidAddresses.forEach(address => { + expect(STELLAR_ASSET_ADDRESS_REGEX.test(address)).toBe(false); + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/facilitator-accessors.test.ts b/typescript/packages/mechanisms/stellar/test/unit/facilitator-accessors.test.ts new file mode 100644 index 0000000..dbe5356 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/facilitator-accessors.test.ts @@ -0,0 +1,197 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { STELLAR_TESTNET_CAIP2 } from "../../src/constants"; +import { ExactStellarScheme } from "../../src/exact/facilitator/scheme"; +import { createEd25519Signer } from "../../src/signer"; +import * as stellarUtils from "../../src/utils"; + +vi.mock("../../src/utils", async () => { + const actual = await vi.importActual("../../src/utils"); + return { + ...actual, + getRpcClient: vi.fn(), + }; +}); + +describe("ExactStellarScheme - getExtra", () => { + const mockRpcClient = { + getLatestLedger: vi.fn(), + }; + let scheme: ExactStellarScheme; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockRpcClient as never); + }); + + it("should return areFeesSponsored", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + + scheme = new ExactStellarScheme([signer]); + + const result = scheme.getExtra(STELLAR_TESTNET_CAIP2); + + expect(result).toEqual({ areFeesSponsored: true }); + expect(mockRpcClient.getLatestLedger).not.toHaveBeenCalled(); + }); + + it("should return consistent areFeesSponsored on each call", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + + scheme = new ExactStellarScheme([signer]); + + const result1 = scheme.getExtra(STELLAR_TESTNET_CAIP2); + expect(result1).toEqual({ areFeesSponsored: true }); + + const result2 = scheme.getExtra(STELLAR_TESTNET_CAIP2); + expect(result2).toEqual({ areFeesSponsored: true }); + + expect(mockRpcClient.getLatestLedger).not.toHaveBeenCalled(); + }); + + it("should use custom areFeesSponsored", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + + scheme = new ExactStellarScheme([signer], { areFeesSponsored: false }); + + const result = scheme.getExtra(STELLAR_TESTNET_CAIP2); + expect(result).toEqual({ areFeesSponsored: false }); + }); + + it("should return consistent areFeesSponsored with multiple signers", () => { + const signer1 = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + const signer2 = createEd25519Signer( + "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK", + STELLAR_TESTNET_CAIP2, + ); + + scheme = new ExactStellarScheme([signer1, signer2]); + + // Call getExtra multiple times to ensure consistency + for (let i = 0; i < 10; i++) { + const result = scheme.getExtra(STELLAR_TESTNET_CAIP2); + expect(result).toEqual({ areFeesSponsored: true }); + } + + expect(mockRpcClient.getLatestLedger).not.toHaveBeenCalled(); + }); +}); + +describe("ExactStellarScheme - getSigners", () => { + const mockRpcClient = { + getLatestLedger: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockRpcClient as never); + }); + + it("should return all signer addresses with a single signer", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + + const scheme = new ExactStellarScheme([signer]); + const signers = scheme.getSigners(STELLAR_TESTNET_CAIP2); + + expect(signers).toEqual([signer.address]); + }); + + it("should return all signer addresses with multiple signers", () => { + const signer1 = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + const signer2 = createEd25519Signer( + "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK", + STELLAR_TESTNET_CAIP2, + ); + + const scheme = new ExactStellarScheme([signer1, signer2]); + const signers = scheme.getSigners(STELLAR_TESTNET_CAIP2); + + expect(signers).toHaveLength(2); + expect(signers).toContain(signer1.address); + expect(signers).toContain(signer2.address); + }); + + it("should include feeBumpSigner address when configured", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + const feeBumpSigner = createEd25519Signer( + "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK", + STELLAR_TESTNET_CAIP2, + ); + + const scheme = new ExactStellarScheme([signer], { feeBumpSigner }); + const signers = scheme.getSigners(STELLAR_TESTNET_CAIP2); + + expect(signers).toHaveLength(2); + expect(signers).toContain(signer.address); + expect(signers).toContain(feeBumpSigner.address); + }); + + it("should not duplicate feeBumpSigner if it is also a regular signer", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + + const scheme = new ExactStellarScheme([signer], { feeBumpSigner: signer }); + const signers = scheme.getSigners(STELLAR_TESTNET_CAIP2); + + expect(signers).toHaveLength(1); + expect(signers).toEqual([signer.address]); + }); + + it("should include feeBumpSigner with multiple regular signers", () => { + const signer1 = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + const signer2 = createEd25519Signer( + "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK", + STELLAR_TESTNET_CAIP2, + ); + const feeBumpSigner = createEd25519Signer( + "SACGSSH2Y7Q6P6BK3BBKGH5Z2RDSQQGD2XHOCDYQN7N6BU37HE2OLKMD", + STELLAR_TESTNET_CAIP2, + ); + + const scheme = new ExactStellarScheme([signer1, signer2], { feeBumpSigner }); + const signers = scheme.getSigners(STELLAR_TESTNET_CAIP2); + + expect(signers).toHaveLength(3); + expect(signers).toContain(signer1.address); + expect(signers).toContain(signer2.address); + expect(signers).toContain(feeBumpSigner.address); + }); + + it("should not include feeBumpSigner when not configured", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + + const scheme = new ExactStellarScheme([signer]); + const signers = scheme.getSigners(STELLAR_TESTNET_CAIP2); + + expect(signers).toHaveLength(1); + expect(signers).toEqual([signer.address]); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/facilitator-settle.test.ts b/typescript/packages/mechanisms/stellar/test/unit/facilitator-settle.test.ts new file mode 100644 index 0000000..804d81d --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/facilitator-settle.test.ts @@ -0,0 +1,546 @@ +import { Buffer } from "buffer"; +import { + Networks as StellarNetworks, + rpc, + Account, + SorobanDataBuilder, + TransactionBuilder, + FeeBumpTransaction, +} from "@stellar/stellar-sdk"; +import { Api } from "@stellar/stellar-sdk/rpc"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { STELLAR_TESTNET_CAIP2 } from "../../src/constants"; +import { ExactStellarScheme } from "../../src/exact/facilitator/scheme"; +import { createEd25519Signer } from "../../src/signer"; +import * as stellarUtils from "../../src/utils"; +import type { FacilitatorStellarSigner } from "../../src/signer"; +import type { PaymentPayload, PaymentRequirements } from "@x402/core/types"; + +vi.mock("../../src/utils", async () => { + const actual = await vi.importActual("../../src/utils"); + return { + ...actual, + getNetworkPassphrase: vi.fn(), + getRpcUrl: vi.fn(), + getRpcClient: vi.fn(), + }; +}); + +describe("ExactStellarScheme - Settle (randomly using 1-2 facilitator signers)", () => { + const CLIENT_PUBLIC = "GBBO4ZDDZTSM2IUKQYBAST3CFHNPFXECGEFTGWTA2WELR2BIWDK57UVE"; + const FACILITATOR_SECRET_1 = "SCKB3ECHCPVM4HJPNCQWTQWJJ5XRL6UNKLTTCIH4B7TB22NKJ5GUFMIV"; + const FACILITATOR_PUBLIC_1 = "GCQAXB2D77Y4C66CTGVH25H2RMUKMQJGOWUPK7UXGG5MAQBONUEKFQ4P"; + const FACILITATOR_SECRET_2 = "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK"; + const FACILITATOR_PUBLIC_2 = "GDEDUVINLPX4AN7HYK3MZGY6YDQSNVJT657CVWHEM3QMAH4QHSGLIHVI"; + // The transaction's recipient (different from facilitator signer address) + const TRANSACTION_RECIPIENT = "GCHEI4PQEFJOA27MNZRPQNLGURS6KASW76X5UZCUZIXCOJLKXYCXOR2W"; + const ASSET = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; + + const validRequirements: PaymentRequirements = { + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + amount: "10000", // Extracted from transaction XDR + payTo: TRANSACTION_RECIPIENT, // Must match transaction's recipient + maxTimeoutSeconds: 60, + asset: ASSET, + extra: { + areFeesSponsored: true, + }, + }; + + const facilitatorSigner1 = createEd25519Signer(FACILITATOR_SECRET_1, STELLAR_TESTNET_CAIP2); + const facilitatorSigner2 = createEd25519Signer(FACILITATOR_SECRET_2, STELLAR_TESTNET_CAIP2); + + let facilitatorSigners: FacilitatorStellarSigner[]; + let validPayload: PaymentPayload; + let facilitator: ExactStellarScheme; + let mockSignedTxXdr: string; + let mockServer: rpc.Server; + + // Use a real transaction XDR from shared test (base64 encoded JSON with tx field) + const signedTxJson = + "eyJtZXRob2QiOiJ0cmFuc2ZlciIsInR4IjoiQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQURsM0lBQUFBQUFBQUFBUUFBQUFFQUFBQUFBQUFBQUFBQUFBQnBGcEdGQUFBQUFBQUFBQUVBQUFBQUFBQUFHQUFBQUFBQUFBQUJVRVhOWHNCeW1uYVAxYTBDVUZoUzMwOENqYzZERGxyRklnbTZTRWc3THdFQUFBQUlkSEpoYm5ObVpYSUFBQUFEQUFBQUVnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBU0FBQUFBQUFBQUFDT1JISHdJVkxnYSt4dVl2ZzFacVJsNVFKVy82L2FaRlRLTGljbGFyNEZkd0FBQUFvQUFBQUFBQUFBQUFBQUFBQUFBQ2NRQUFBQUFRQUFBQUVBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZlh4amsrOHlZOGhnQUk4ck9BQUFBRUFBQUFBRUFBQUFCQUFBQUVRQUFBQUVBQUFBQ0FBQUFEd0FBQUFwd2RXSnNhV05mYTJWNUFBQUFBQUFOQUFBQUlFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUR3QUFBQWx6YVdkdVlYUjFjbVVBQUFBQUFBQU5BQUFBUUl2bjJjU3VLbFl5TU96T0pTWnkwc0VaN3dkN1QwYmdSQ0ZxZjg1M3VXQXFVcjE1ZUpycXNqVjROUVpTQW05WXNWbHZEcEUrSFRLc3pUQUVBaTJBRkFnQUFBQUFBQUFBQVZCRnpWN0FjcHAyajlXdEFsQllVdDlQQW8zT2d3NWF4U0lKdWtoSU95OEJBQUFBQ0hSeVlXNXpabVZ5QUFBQUF3QUFBQklBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFFZ0FBQUFBQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUtBQUFBQUFBQUFBQUFBQUFBQUFBbkVBQUFBQUFBQUFBQkFBQUFBQUFBQUFJQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBQmdBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFCUUFBQUFCQUFBQUF3QUFBQUVBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFFQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUJWVk5FUXdBQUFBQkNQbjBGOHV5dnYrd1pLeUZhUHh2cGF1MjQyT2NDVkt2alFUNENCOTVXc2dBQUFBWUFBQUFBQUFBQUFFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUZWOFk1UHZNbVBJWUFBQUFBQUFMNVRFQUFBRjRBQUFCTkFBQUFBQUFBNWNPQUFBQUFBPT0iLCJzaW11bGF0aW9uUmVzdWx0Ijp7ImF1dGgiOlsiQUFBQUFRQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDlmR09UN3pKanlHQUFBQUFBQUFBQUJBQUFBQUFBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFBaDBjbUZ1YzJabGNnQUFBQU1BQUFBU0FBQUFBQUFBQUFCQzdtUmp6T1ROSW9xR0FnbFBZaW5hOHR5Q01Rc3pXbURWaUxqb0tMRFYzd0FBQUJJQUFBQUFBQUFBQUk1RWNmQWhVdUJyN0c1aStEVm1wR1hsQWxiL3I5cGtWTW91SnlWcXZnVjNBQUFBQ2dBQUFBQUFBQUFBQUFBQUFBQUFKeEFBQUFBQSJdLCJyZXR2YWwiOiJBQUFBQVE9PSJ9LCJzaW11bGF0aW9uVHJhbnNhY3Rpb25EYXRhIjoiQUFBQUFBQUFBQUlBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFCZ0FBQUFGUVJjMWV3SEthZG8vVnJRSlFXRkxmVHdLTnpvTU9Xc1VpQ2JwSVNEc3ZBUUFBQUJRQUFBQUJBQUFBQXdBQUFBRUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOEFBQUFCVlZORVF3QUFBQUJDUG4wRjh1eXZ2K3daS3lGYVB4dnBhdTI0Mk9jQ1ZLdmpRVDRDQjk1V3NnQUFBQUVBQUFBQWprUng4Q0ZTNEd2c2JtTDROV2FrWmVVQ1Z2K3YybVJVeWk0bkpXcStCWGNBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFZQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBRlY4WTVQdk1tUElZQUFBQUFBQUw1VEVBQUFGNEFBQUJOQUFBQUFBQUE1Y08ifQ=="; + const { tx: mockTransactionXDR } = JSON.parse( + Buffer.from(signedTxJson, "base64").toString("utf8"), + ); + + beforeAll(async () => { + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue("https://soroban-testnet.stellar.org"); + + // Build full V2 PaymentPayload with mocked transaction + validPayload = { + x402Version: 2, + resource: { + url: "https://example.com/resource", + description: "Test payment", + mimeType: "application/json", + }, + accepted: validRequirements, + payload: { + transaction: mockTransactionXDR, + }, + }; + + // Use the same XDR for signed transaction + mockSignedTxXdr = mockTransactionXDR; + }); + + beforeEach(() => { + // Random selection for 1-2 facilitators + const useTwoFacilitators = Math.random() > 0.5; + facilitatorSigners = useTwoFacilitators + ? [facilitatorSigner1, facilitatorSigner2] + : [facilitatorSigner1]; + facilitator = new ExactStellarScheme(facilitatorSigners); + + // Create a fresh mock server for each test + mockServer = { + getAccount: vi.fn().mockImplementation(async addr => new Account(addr, "100")), + sendTransaction: vi.fn().mockResolvedValue({ + status: "PENDING", + hash: "test-tx-hash-123", + } as Api.SendTransactionResponse), + getTransaction: vi + .fn() + .mockResolvedValue({ status: "SUCCESS" } as Api.GetTransactionResponse), + getLatestLedger: vi.fn().mockResolvedValue({ sequence: 100000 }), + simulateTransaction: vi.fn().mockResolvedValue({ + id: "test", + latestLedger: 123, + events: [], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse), + } as unknown as rpc.Server; + + vi.clearAllMocks(); + + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockServer); + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue("https://soroban-testnet.stellar.org"); + + // Mock verify to pass for settle tests (verify is tested separately) + // The expiration check may reject the test transaction, so we mock verify for settle tests + // Note: This is reset in tests that need to test actual verify behavior + vi.spyOn(facilitator, "verify").mockImplementation(async () => ({ + isValid: true, + payer: CLIENT_PUBLIC, + })); + + // Mock signTransaction for all signers to return the mock signed XDR + vi.spyOn(facilitatorSigner1, "signTransaction").mockResolvedValue({ + signedTxXdr: mockSignedTxXdr, + error: undefined, + }); + vi.spyOn(facilitatorSigner2, "signTransaction").mockResolvedValue({ + signedTxXdr: mockSignedTxXdr, + error: undefined, + }); + }); + + describe("settlement failures", () => { + it("should return error when verify fails", async () => { + vi.spyOn(facilitator, "verify").mockRestore(); + // Use requirements with wrong amount to make verify fail + const invalidRequirements = { + ...validRequirements, + amount: "9999", // Wrong amount (transaction has 10000) + }; + + const result = await facilitator.settle(validPayload, invalidRequirements); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe("invalid_exact_stellar_payload_wrong_amount"); + expect(result.payer).toBe(CLIENT_PUBLIC); + expect(result.network).toBe(STELLAR_TESTNET_CAIP2); + expect(result.transaction).toBe(""); + expect(mockServer.sendTransaction).not.toHaveBeenCalled(); + }); + + it("should return error when signing fails", async () => { + vi.spyOn(facilitatorSigner1, "signTransaction").mockResolvedValue({ + signedTxXdr: "", + error: { code: 1, message: "Signing failed" }, + }); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result).toEqual({ + success: false, + errorReason: "settle_exact_stellar_transaction_signing_failed", + payer: CLIENT_PUBLIC, + network: STELLAR_TESTNET_CAIP2, + transaction: "", + }); + }); + + it("should return error when transaction submission returns non-PENDING status", async () => { + vi.mocked(mockServer.sendTransaction).mockResolvedValue({ + status: "TRY_AGAIN_LATER", + hash: "", + } as Api.SendTransactionResponse); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result).toEqual({ + success: false, + errorReason: "settle_exact_stellar_transaction_submission_failed", + payer: CLIENT_PUBLIC, + network: STELLAR_TESTNET_CAIP2, + transaction: "", + }); + }); + + it("should return error when transaction confirmation fails", async () => { + vi.mocked(mockServer.sendTransaction).mockResolvedValue({ + status: "PENDING", + hash: "test-tx-hash-123", + } as Api.SendTransactionResponse); + vi.mocked(mockServer.getTransaction).mockResolvedValue({ + status: "FAILED", + } as Api.GetTransactionResponse); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result).toEqual({ + success: false, + errorReason: "settle_exact_stellar_transaction_failed", + payer: CLIENT_PUBLIC, + network: STELLAR_TESTNET_CAIP2, + transaction: "test-tx-hash-123", + }); + }); + + it("should return error when transaction confirmation times out", async () => { + vi.mocked(mockServer.sendTransaction).mockResolvedValue({ + status: "PENDING", + hash: "test-tx-hash-123", + } as Api.SendTransactionResponse); + vi.mocked(mockServer.getTransaction).mockResolvedValue({ + status: "NOT_FOUND", + } as Api.GetTransactionResponse); + + const result = await facilitator.settle(validPayload, { + ...validRequirements, + maxTimeoutSeconds: 1, + }); + + expect(result).toEqual({ + success: false, + errorReason: "settle_exact_stellar_transaction_failed", + payer: CLIENT_PUBLIC, + network: STELLAR_TESTNET_CAIP2, + transaction: "test-tx-hash-123", + }); + }); + + it("should handle unexpected errors during account fetch", async () => { + vi.mocked(mockServer.getAccount).mockRejectedValue(new Error("Network error")); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result).toEqual({ + success: false, + errorReason: "unexpected_settle_error", + payer: CLIENT_PUBLIC, + network: STELLAR_TESTNET_CAIP2, + transaction: "", + }); + }); + }); + + describe("successful settlement", () => { + it("should successfully settle valid payment", async () => { + vi.mocked(mockServer.sendTransaction).mockResolvedValue({ + status: "PENDING", + hash: "test-tx-hash-123", + } as Api.SendTransactionResponse); + vi.mocked(mockServer.getTransaction).mockResolvedValue({ + status: "SUCCESS", + } as Api.GetTransactionResponse); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result).toEqual({ + success: true, + transaction: "test-tx-hash-123", + payer: CLIENT_PUBLIC, + network: STELLAR_TESTNET_CAIP2, + }); + + // Verify getAccount was called with one of the facilitator addresses + expect(mockServer.getAccount).toHaveBeenCalledTimes(1); + const calledWithAddress = vi.mocked(mockServer.getAccount).mock.calls[0][0]; + expect(facilitator.signingAddresses).toContain(calledWithAddress); + expect(mockServer.sendTransaction).toHaveBeenCalled(); + expect(mockServer.getTransaction).toHaveBeenCalledWith("test-tx-hash-123"); + }); + + it("should poll until transaction succeeds", async () => { + let callCount = 0; + vi.mocked(mockServer.getTransaction).mockImplementation(async () => { + callCount++; + if (callCount < 3) { + return { status: "NOT_FOUND" } as Api.GetTransactionResponse; + } + return { status: "SUCCESS" } as Api.GetTransactionResponse; + }); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result.success).toBe(true); + expect(mockServer.getTransaction).toHaveBeenCalledTimes(3); + }); + + it("should continue polling on errors", async () => { + let callCount = 0; + vi.mocked(mockServer.getTransaction).mockImplementation(async () => { + callCount++; + if (callCount === 1) { + throw new Error("Temporary network error"); + } + return { status: "SUCCESS" } as Api.GetTransactionResponse; + }); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result.success).toBe(true); + expect(mockServer.getTransaction).toHaveBeenCalledTimes(2); + }); + }); + + describe("multi-signer tests", () => { + const multiSigners: FacilitatorStellarSigner[] = [facilitatorSigner1, facilitatorSigner2]; + + it("should use custom selectSigner callback", async () => { + const customFacilitator = new ExactStellarScheme(multiSigners, { + selectSigner: addrs => addrs[1], + }); + + vi.spyOn(customFacilitator, "verify").mockResolvedValue({ + isValid: true, + payer: CLIENT_PUBLIC, + }); + + const result = await customFacilitator.settle(validPayload, validRequirements); + + expect(result.success).toBe(true); + expect(mockServer.getAccount).toHaveBeenCalledWith(FACILITATOR_PUBLIC_2); + expect(facilitatorSigner2.signTransaction).toHaveBeenCalled(); + expect(facilitatorSigner1.signTransaction).not.toHaveBeenCalled(); + }); + + it("should use round-robin by default with multiple signers", async () => { + const roundRobinFacilitator = new ExactStellarScheme(multiSigners); + + vi.spyOn(roundRobinFacilitator, "verify").mockResolvedValue({ + isValid: true, + payer: CLIENT_PUBLIC, + }); + + const calledAddresses: string[] = []; + + for (let i = 0; i < 4; i++) { + await roundRobinFacilitator.settle(validPayload, validRequirements); + const callArgs = vi.mocked(mockServer.getAccount).mock.calls[i]; + calledAddresses.push(callArgs[0]); + } + + // Verify round-robin pattern: [addr1, addr2, addr1, addr2] + expect(calledAddresses).toEqual([ + FACILITATOR_PUBLIC_1, + FACILITATOR_PUBLIC_2, + FACILITATOR_PUBLIC_1, + FACILITATOR_PUBLIC_2, + ]); + }); + }); +}); + +describe("ExactStellarScheme - Settle with feeBumpSigner", () => { + const CLIENT_PUBLIC = "GBBO4ZDDZTSM2IUKQYBAST3CFHNPFXECGEFTGWTA2WELR2BIWDK57UVE"; + const FACILITATOR_SECRET_1 = "SCKB3ECHCPVM4HJPNCQWTQWJJ5XRL6UNKLTTCIH4B7TB22NKJ5GUFMIV"; + const FACILITATOR_SECRET_2 = "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK"; + // A separate keypair for the fee bump signer + const FEE_BUMP_SECRET = "SACGSSH2Y7Q6P6BK3BBKGH5Z2RDSQQGD2XHOCDYQN7N6BU37HE2OLKMD"; + const FEE_BUMP_PUBLIC = "GDBNQWJ67SGPPEZ2GALX5W5YGT2NACBJQDK64T6WXDGJNAA4IPWIULMV"; + const TRANSACTION_RECIPIENT = "GCHEI4PQEFJOA27MNZRPQNLGURS6KASW76X5UZCUZIXCOJLKXYCXOR2W"; + const ASSET = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; + + const validRequirements: PaymentRequirements = { + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + amount: "10000", + payTo: TRANSACTION_RECIPIENT, + maxTimeoutSeconds: 60, + asset: ASSET, + extra: { + areFeesSponsored: true, + }, + }; + + const facilitatorSigner1 = createEd25519Signer(FACILITATOR_SECRET_1, STELLAR_TESTNET_CAIP2); + const facilitatorSigner2 = createEd25519Signer(FACILITATOR_SECRET_2, STELLAR_TESTNET_CAIP2); + const feeBumpSigner = createEd25519Signer(FEE_BUMP_SECRET, STELLAR_TESTNET_CAIP2); + + let validPayload: PaymentPayload; + let mockSignedTxXdr: string; + let mockServer: rpc.Server; + + const signedTxJson = + "eyJtZXRob2QiOiJ0cmFuc2ZlciIsInR4IjoiQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQURsM0lBQUFBQUFBQUFBUUFBQUFFQUFBQUFBQUFBQUFBQUFBQnBGcEdGQUFBQUFBQUFBQUVBQUFBQUFBQUFHQUFBQUFBQUFBQUJVRVhOWHNCeW1uYVAxYTBDVUZoUzMwOENqYzZERGxyRklnbTZTRWc3THdFQUFBQUlkSEpoYm5ObVpYSUFBQUFEQUFBQUVnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBU0FBQUFBQUFBQUFDT1JISHdJVkxnYSt4dVl2ZzFacVJsNVFKVy82L2FaRlRLTGljbGFyNEZkd0FBQUFvQUFBQUFBQUFBQUFBQUFBQUFBQ2NRQUFBQUFRQUFBQUVBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZlh4amsrOHlZOGhnQUk4ck9BQUFBRUFBQUFBRUFBQUFCQUFBQUVRQUFBQUVBQUFBQ0FBQUFEd0FBQUFwd2RXSnNhV05mYTJWNUFBQUFBQUFOQUFBQUlFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUR3QUFBQWx6YVdkdVlYUjFjbVVBQUFBQUFBQU5BQUFBUUl2bjJjU3VLbFl5TU96T0pTWnkwc0VaN3dkN1QwYmdSQ0ZxZjg1M3VXQXFVcjE1ZUpycXNqVjROUVpTQW05WXNWbHZEcEUrSFRLc3pUQUVBaTJBRkFnQUFBQUFBQUFBQVZCRnpWN0FjcHAyajlXdEFsQllVdDlQQW8zT2d3NWF4U0lKdWtoSU95OEJBQUFBQ0hSeVlXNXpabVZ5QUFBQUF3QUFBQklBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFFZ0FBQUFBQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUtBQUFBQUFBQUFBQUFBQUFBQUFBbkVBQUFBQUFBQUFBQkFBQUFBQUFBQUFJQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBQmdBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFCUUFBQUFCQUFBQUF3QUFBQUVBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFFQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUJWVk5FUXdBQUFBQkNQbjBGOHV5dnYrd1pLeUZhUHh2cGF1MjQyT2NDVkt2alFUNENCOTVXc2dBQUFBWUFBQUFBQUFBQUFFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUZWOFk1UHZNbVBJWUFBQUFBQUFMNVRFQUFBRjRBQUFCTkFBQUFBQUFBNWNPQUFBQUFBPT0iLCJzaW11bGF0aW9uUmVzdWx0Ijp7ImF1dGgiOlsiQUFBQUFRQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDlmR09UN3pKanlHQUFBQUFBQUFBQUJBQUFBQUFBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFBaDBjbUZ1YzJabGNnQUFBQU1BQUFBU0FBQUFBQUFBQUFCQzdtUmp6T1ROSW9xR0FnbFBZaW5hOHR5Q01Rc3pXbURWaUxqb0tMRFYzd0FBQUJJQUFBQUFBQUFBQUk1RWNmQWhVdUJyN0c1aStEVm1wR1hsQWxiL3I5cGtWTW91SnlWcXZnVjNBQUFBQ2dBQUFBQUFBQUFBQUFBQUFBQUFKeEFBQUFBQSJdLCJyZXR2YWwiOiJBQUFBQVE9PSJ9LCJzaW11bGF0aW9uVHJhbnNhY3Rpb25EYXRhIjoiQUFBQUFBQUFBQUlBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFCZ0FBQUFGUVJjMWV3SEthZG8vVnJRSlFXRkxmVHdLTnpvTU9Xc1VpQ2JwSVNEc3ZBUUFBQUJRQUFBQUJBQUFBQXdBQUFBRUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOEFBQUFCVlZORVF3QUFBQUJDUG4wRjh1eXZ2K3daS3lGYVB4dnBhdTI0Mk9jQ1ZLdmpRVDRDQjk1V3NnQUFBQUVBQUFBQWprUng4Q0ZTNEd2c2JtTDROV2FrWmVVQ1Z2K3YybVJVeWk0bkpXcStCWGNBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFZQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBRlY4WTVQdk1tUElZQUFBQUFBQUw1VEVBQUFGNEFBQUJOQUFBQUFBQUE1Y08ifQ=="; + const { tx: mockTransactionXDR } = JSON.parse( + Buffer.from(signedTxJson, "base64").toString("utf8"), + ); + + beforeAll(async () => { + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue("https://soroban-testnet.stellar.org"); + + validPayload = { + x402Version: 2, + resource: { + url: "https://example.com/resource", + description: "Test payment", + mimeType: "application/json", + }, + accepted: validRequirements, + payload: { + transaction: mockTransactionXDR, + }, + }; + + mockSignedTxXdr = mockTransactionXDR; + }); + + beforeEach(() => { + mockServer = { + getAccount: vi.fn().mockImplementation(async addr => new Account(addr, "100")), + sendTransaction: vi.fn().mockResolvedValue({ + status: "PENDING", + hash: "test-tx-hash-fee-bump", + } as Api.SendTransactionResponse), + getTransaction: vi + .fn() + .mockResolvedValue({ status: "SUCCESS" } as Api.GetTransactionResponse), + getLatestLedger: vi.fn().mockResolvedValue({ sequence: 100000 }), + simulateTransaction: vi.fn().mockResolvedValue({ + id: "test", + latestLedger: 123, + events: [], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse), + } as unknown as rpc.Server; + + vi.clearAllMocks(); + + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockServer); + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue("https://soroban-testnet.stellar.org"); + + // Mock signTransaction for all signers to return the mock signed XDR + vi.spyOn(facilitatorSigner1, "signTransaction").mockResolvedValue({ + signedTxXdr: mockSignedTxXdr, + error: undefined, + }); + vi.spyOn(facilitatorSigner2, "signTransaction").mockResolvedValue({ + signedTxXdr: mockSignedTxXdr, + error: undefined, + }); + }); + + it("should wrap inner tx in FeeBumpTransaction when feeBumpSigner is set", async () => { + // Mock the fee bump signer to return a properly signed fee bump XDR + const feeBumpSignSpy = vi + .spyOn(feeBumpSigner, "signTransaction") + .mockImplementation(async (txXdr, opts) => { + // Verify the input is a fee bump transaction + const parsed = TransactionBuilder.fromXDR( + txXdr, + opts?.networkPassphrase ?? StellarNetworks.TESTNET, + ); + expect(parsed).toBeInstanceOf(FeeBumpTransaction); + const fbTx = parsed as FeeBumpTransaction; + expect(fbTx.feeSource).toBe(FEE_BUMP_PUBLIC); + // Return the same XDR (mock — no real signature needed for send mock) + return { signedTxXdr: txXdr, error: undefined }; + }); + + const facilitator = new ExactStellarScheme([facilitatorSigner1], { feeBumpSigner }); + vi.spyOn(facilitator, "verify").mockResolvedValue({ isValid: true, payer: CLIENT_PUBLIC }); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("test-tx-hash-fee-bump"); + // Inner tx signer called first + expect(facilitatorSigner1.signTransaction).toHaveBeenCalledTimes(1); + // Fee bump signer called second + expect(feeBumpSignSpy).toHaveBeenCalledTimes(1); + // The submitted transaction should be a FeeBumpTransaction + const submitCall = vi.mocked(mockServer.sendTransaction).mock.calls[0][0]; + expect(submitCall).toBeInstanceOf(FeeBumpTransaction); + }); + + it("should return error when fee bump signing fails", async () => { + vi.spyOn(feeBumpSigner, "signTransaction").mockResolvedValue({ + signedTxXdr: "", + error: { code: 1, message: "Fee bump signing failed" }, + }); + + const facilitator = new ExactStellarScheme([facilitatorSigner1], { feeBumpSigner }); + vi.spyOn(facilitator, "verify").mockResolvedValue({ isValid: true, payer: CLIENT_PUBLIC }); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe("settle_exact_stellar_fee_bump_signing_failed"); + expect(result.payer).toBe(CLIENT_PUBLIC); + // Inner tx signing should have succeeded + expect(facilitatorSigner1.signTransaction).toHaveBeenCalledTimes(1); + // Transaction should not have been submitted + expect(mockServer.sendTransaction).not.toHaveBeenCalled(); + }); + + it("should not use fee bump when feeBumpSigner is not set", async () => { + const facilitator = new ExactStellarScheme([facilitatorSigner1]); + vi.spyOn(facilitator, "verify").mockResolvedValue({ isValid: true, payer: CLIENT_PUBLIC }); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result.success).toBe(true); + // The submitted transaction should be a regular Transaction, not FeeBumpTransaction + const submitCall = vi.mocked(mockServer.sendTransaction).mock.calls[0][0]; + expect(submitCall).not.toBeInstanceOf(FeeBumpTransaction); + }); + + it("should use round-robin signer for inner tx while fee bump signer pays fees", async () => { + vi.spyOn(feeBumpSigner, "signTransaction").mockImplementation(async txXdr => { + return { signedTxXdr: txXdr, error: undefined }; + }); + + const facilitator = new ExactStellarScheme([facilitatorSigner1, facilitatorSigner2], { + feeBumpSigner, + }); + vi.spyOn(facilitator, "verify").mockResolvedValue({ isValid: true, payer: CLIENT_PUBLIC }); + + // First settle — should use signer1 for inner tx + await facilitator.settle(validPayload, validRequirements); + expect(mockServer.getAccount).toHaveBeenLastCalledWith(facilitatorSigner1.address); + + // Second settle — should use signer2 for inner tx (round robin) + await facilitator.settle(validPayload, validRequirements); + expect(mockServer.getAccount).toHaveBeenLastCalledWith(facilitatorSigner2.address); + + // Fee bump signer should have been called for both settlements + expect(feeBumpSigner.signTransaction).toHaveBeenCalledTimes(2); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/facilitator-verify.test.ts b/typescript/packages/mechanisms/stellar/test/unit/facilitator-verify.test.ts new file mode 100644 index 0000000..9bd84a8 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/facilitator-verify.test.ts @@ -0,0 +1,1037 @@ +import { Buffer } from "buffer"; +import { + Address, + Networks as StellarNetworks, + SorobanDataBuilder, + rpc, + Transaction, + TransactionBuilder, + Operation, + Account, + xdr, + Keypair, + Asset, +} from "@stellar/stellar-sdk"; +import { Api } from "@stellar/stellar-sdk/rpc"; +import { beforeEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { STELLAR_TESTNET_CAIP2 } from "../../src/constants"; +import { + ExactStellarScheme, + invalidVerifyResponse, + validVerifyResponse, +} from "../../src/exact/facilitator/scheme"; +import { createEd25519Signer } from "../../src/signer"; +import * as stellarUtils from "../../src/utils"; +import type { FacilitatorStellarSigner } from "../../src/signer"; +import type { PaymentPayload, PaymentRequirements } from "@x402/core/types"; + +/** + * Creates a mock transfer event for testing event validation. + * Follows CAP-46-06 format: Topic: ["transfer", from, to, ...], Data: amount + * @see https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-06.md + * + * @param params - The parameters object + * @param params.from - The sender address + * @param params.to - The recipient address + * @param params.amount - The transfer amount as bigint + * @param params.fnName - The function name (defaults to "transfer") + * @returns A DiagnosticEvent XDR object + */ +function createMockContractEvent({ + from, + to, + amount, + fnName = "transfer", + contractId, +}: { + from: string; + to: string; + amount: bigint; + fnName?: string; + contractId?: xdr.Hash | null; +}): xdr.DiagnosticEvent { + // symbol for the function name + const transferSymbol = xdr.ScVal.scvSymbol(fnName); + + const fromKeypair = Keypair.fromPublicKey(from); + const fromScAddress = xdr.ScVal.scvAddress( + xdr.ScAddress.scAddressTypeAccount( + xdr.PublicKey.publicKeyTypeEd25519(fromKeypair.rawPublicKey()), + ), + ); + + const toKeypair = Keypair.fromPublicKey(to); + const toScAddress = xdr.ScVal.scvAddress( + xdr.ScAddress.scAddressTypeAccount( + xdr.PublicKey.publicKeyTypeEd25519(toKeypair.rawPublicKey()), + ), + ); + + const amountScVal = xdr.ScVal.scvI128( + new xdr.Int128Parts({ + lo: xdr.Uint64.fromString(amount.toString()), + hi: xdr.Int64.fromString("0"), + }), + ); + + const contractEventV0 = new xdr.ContractEventV0({ + topics: [transferSymbol, fromScAddress, toScAddress], + data: amountScVal, + }); + return createMockDiagnosticEvent( + contractEventV0, + xdr.ContractEventType.contract(), + contractId ?? null, + ); +} + +/** + * Creates a mock system diagnostic event (should be ignored by event validator). + */ +function createMockSystemEvent(): xdr.DiagnosticEvent { + return createMockDiagnosticEvent( + new xdr.ContractEventV0({ topics: [], data: xdr.ScVal.scvVoid() }), + xdr.ContractEventType.system(), + ); +} + +/** + * Creates a mock diagnostic event from a ContractEventV0 and event type. + * This helper function constructs the proper XDR structure for testing + * contract event validation in the facilitator verification process. + * + * @param v0 - The ContractEventV0 containing the event data + * @param eventType - The contract event type (contract or system) + * @param contractId - Optional contract ID (null = event has no contract; omit for backward compat) + * @returns A properly formatted DiagnosticEvent for testing + */ +function createMockDiagnosticEvent( + v0: xdr.ContractEventV0, + eventType: ReturnType = xdr.ContractEventType.contract(), + contractId: xdr.Hash | null = null, +): xdr.DiagnosticEvent { + const eventBodyXdr = xdr.ContractEventBody.toXDR( + xdr.ContractEventBody.fromXDR( + Buffer.concat([Buffer.from([0, 0, 0, 0]), xdr.ContractEventV0.toXDR(v0)]), + ), + ); + const contractEvent = new xdr.ContractEvent({ + ext: xdr.ExtensionPoint.fromXDR(Buffer.from([0, 0, 0, 0])), + contractId, + type: eventType, + body: xdr.ContractEventBody.fromXDR(eventBodyXdr), + }); + return new xdr.DiagnosticEvent({ + inSuccessfulContractCall: true, + event: contractEvent, + }); +} + +vi.mock("../../src/utils", async () => { + const actual = await vi.importActual("../../src/utils"); + return { + ...actual, + getEstimatedLedgerCloseTimeSeconds: vi.fn().mockResolvedValue(5), + getNetworkPassphrase: vi.fn(), + getRpcClient: vi.fn(), + }; +}); + +describe("ExactStellarScheme#Verify (randomly using 1-2 facilitator signers)", () => { + const mockServer = { + simulateTransaction: vi.fn(), + getLatestLedger: vi.fn(), + } as unknown as rpc.Server; + + const CLIENT_PUBLIC = "GBBO4ZDDZTSM2IUKQYBAST3CFHNPFXECGEFTGWTA2WELR2BIWDK57UVE"; + const FACILITATOR_PUBLIC = "GCQAXB2D77Y4C66CTGVH25H2RMUKMQJGOWUPK7UXGG5MAQBONUEKFQ4P"; + const TRANSACTION_RECIPIENT = "GCHEI4PQEFJOA27MNZRPQNLGURS6KASW76X5UZCUZIXCOJLKXYCXOR2W"; + const ASSET = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; + const networkPassphrase = StellarNetworks.TESTNET; + const account = new Account(CLIENT_PUBLIC, "100"); + + const facilitatorSigner1 = createEd25519Signer( + "SCKB3ECHCPVM4HJPNCQWTQWJJ5XRL6UNKLTTCIH4B7TB22NKJ5GUFMIV", + STELLAR_TESTNET_CAIP2, + ); + const facilitatorSigner2 = createEd25519Signer( + "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK", + STELLAR_TESTNET_CAIP2, + ); + + let stellarPayload: { transaction: string }; + let baseTransaction: Transaction; + let baseSorobanData: xdr.SorobanTransactionData | undefined; + let baseOperation: Operation.InvokeHostFunction; + let baseFunc: xdr.HostFunction; + let baseInvokeContractArgs: xdr.InvokeContractArgs; + let facilitatorSigners: FacilitatorStellarSigner[]; + let facilitator: ExactStellarScheme; + let validPayload: PaymentPayload; + let validRequirements: PaymentRequirements; + + // Use a real transaction XDR from shared test (base64 encoded JSON with tx field) + const signedTxJson = + "eyJtZXRob2QiOiJ0cmFuc2ZlciIsInR4IjoiQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQURsM0lBQUFBQUFBQUFBUUFBQUFFQUFBQUFBQUFBQUFBQUFBQnBGcEdGQUFBQUFBQUFBQUVBQUFBQUFBQUFHQUFBQUFBQUFBQUJVRVhOWHNCeW1uYVAxYTBDVUZoUzMwOENqYzZERGxyRklnbTZTRWc3THdFQUFBQUlkSEpoYm5ObVpYSUFBQUFEQUFBQUVnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBU0FBQUFBQUFBQUFDT1JISHdJVkxnYSt4dVl2ZzFacVJsNVFKVy82L2FaRlRLTGljbGFyNEZkd0FBQUFvQUFBQUFBQUFBQUFBQUFBQUFBQ2NRQUFBQUFRQUFBQUVBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZlh4amsrOHlZOGhnQUk4ck9BQUFBRUFBQUFBRUFBQUFCQUFBQUVRQUFBQUVBQUFBQ0FBQUFEd0FBQUFwd2RXSnNhV05mYTJWNUFBQUFBQUFOQUFBQUlFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUR3QUFBQWx6YVdkdVlYUjFjbVVBQUFBQUFBQU5BQUFBUUl2bjJjU3VLbFl5TU96T0pTWnkwc0VaN3dkN1QwYmdSQ0ZxZjg1M3VXQXFVcjE1ZUpycXNqVjROUVpTQW05WXNWbHZEcEUrSFRLc3pUQUVBaTJBRkFnQUFBQUFBQUFBQVZCRnpWN0FjcHAyajlXdEFsQllVdDlQQW8zT2d3NWF4U0lKdWtoSU95OEJBQUFBQ0hSeVlXNXpabVZ5QUFBQUF3QUFBQklBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFFZ0FBQUFBQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUtBQUFBQUFBQUFBQUFBQUFBQUFBbkVBQUFBQUFBQUFBQkFBQUFBQUFBQUFJQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBQmdBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFCUUFBQUFCQUFBQUF3QUFBQUVBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFFQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUJWVk5FUXdBQUFBQkNQbjBGOHV5dnYrd1pLeUZhUHh2cGF1MjQyT2NDVkt2alFUNENCOTVXc2dBQUFBWUFBQUFBQUFBQUFFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUZWOFk1UHZNbVBJWUFBQUFBQUFMNVRFQUFBRjRBQUFCTkFBQUFBQUFBNWNPQUFBQUFBPT0iLCJzaW11bGF0aW9uUmVzdWx0Ijp7ImF1dGgiOlsiQUFBQUFRQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDlmR09UN3pKanlHQUFBQUFBQUFBQUJBQUFBQUFBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFBaDBjbUZ1YzJabGNnQUFBQU1BQUFBU0FBQUFBQUFBQUFCQzdtUmp6T1ROSW9xR0FnbFBZaW5hOHR5Q01Rc3pXbURWaUxqb0tMRFYzd0FBQUJJQUFBQUFBQUFBQUk1RWNmQWhVdUJyN0c1aStEVm1wR1hsQWxiL3I5cGtWTW91SnlWcXZnVjNBQUFBQ2dBQUFBQUFBQUFBQUFBQUFBQUFKeEFBQUFBQSJdLCJyZXR2YWwiOiJBQUFBQVE9PSJ9LCJzaW11bGF0aW9uVHJhbnNhY3Rpb25EYXRhIjoiQUFBQUFBQUFBQUlBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFCZ0FBQUFGUVJjMWV3SEthZG8vVnJRSlFXRkxmVHdLTnpvTU9Xc1VpQ2JwSVNEc3ZBUUFBQUJRQUFBQUJBQUFBQXdBQUFBRUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOEFBQUFCVlZORVF3QUFBQUJDUG4wRjh1eXZ2K3daS3lGYVB4dnBhdTI0Mk9jQ1ZLdmpRVDRDQjk1V3NnQUFBQUVBQUFBQWprUng4Q0ZTNEd2c2JtTDROV2FrWmVVQ1Z2K3YybVJVeWk0bkpXcStCWGNBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFZQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBRlY4WTVQdk1tUElZQUFBQUFBQUw1VEVBQUFGNEFBQUJOQUFBQUFBQUE1Y08ifQ=="; + const txSignatureExpiration = 2345678; + const { tx: baseTransactionXDR } = JSON.parse( + Buffer.from(signedTxJson, "base64").toString("utf8"), + ); + + beforeAll(async () => { + // Set up mocks + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockServer); + vi.mocked(mockServer.getLatestLedger).mockResolvedValue({ + sequence: txSignatureExpiration - 10, + } as Api.GetLatestLedgerResponse); + + // Create valid requirements (V2 format) + // Note: Values must match the transaction XDR from shared test + validRequirements = { + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + amount: "10000", // Extracted from transaction XDR + payTo: TRANSACTION_RECIPIENT, // Must match transaction's recipient + maxTimeoutSeconds: 60, + asset: ASSET, + extra: { + maxLedgerOffset: 12, + }, + }; + + // Build full V2 PaymentPayload with mocked transaction + validPayload = { + x402Version: 2, + resource: { + url: "https://example.com/resource", + description: "Test payment", + mimeType: "application/json", + }, + accepted: validRequirements, + payload: { + transaction: baseTransactionXDR, + }, + }; + + stellarPayload = validPayload.payload as { transaction: string }; + const txEnvelope = xdr.TransactionEnvelope.fromXDR(stellarPayload.transaction, "base64"); + baseTransaction = new Transaction(stellarPayload.transaction, networkPassphrase); + baseSorobanData = txEnvelope.v1()?.tx()?.ext()?.sorobanData() || undefined; + baseOperation = baseTransaction.operations[0] as Operation.InvokeHostFunction; + baseFunc = baseOperation.func; + baseInvokeContractArgs = baseFunc.invokeContract(); + }); + + /** + * Builds a modified transaction and wraps it in a PaymentPayload. + */ + function buildStellarPayloadFromOp( + operation: xdr.Operation, + options?: { includeSorobanData?: boolean }, + ): PaymentPayload { + const modifiedTx = new TransactionBuilder(account, { + fee: baseTransaction.fee, + networkPassphrase, + ledgerbounds: baseTransaction.ledgerBounds, + ...(options?.includeSorobanData !== false && + baseSorobanData && { sorobanData: baseSorobanData }), + }) + .addOperation(operation) + .setTimeout(validRequirements.maxTimeoutSeconds) + .build(); + return { + ...validPayload, + payload: { transaction: modifiedTx.toXDR() }, + }; + } + + beforeEach(() => { + // Random selection for 1-2 facilitators + const useTwoFacilitators = Math.random() > 0.5; + facilitatorSigners = useTwoFacilitators + ? [facilitatorSigner1, facilitatorSigner2] + : [facilitatorSigner1]; + + // Use a high max fee for tests to avoid fee validation errors in tests that check other validations + facilitator = new ExactStellarScheme(facilitatorSigners, { + areFeesSponsored: true, + maxTransactionFeeStroops: 1_000_000, + }); + + const expectedAssetHash = new Address(ASSET).toScAddress().contractId(); + const defaultTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt(validRequirements.amount), + contractId: expectedAssetHash, + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValue({ + id: "test", + latestLedger: 123, + events: [defaultTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + }); + + describe("validation errors", () => { + it("should reject invalid x402 version, scheme, and network mismatch", async () => { + let result = await facilitator.verify( + { ...validPayload, x402Version: 9 }, // ❌ unsupported x402 version + validRequirements, + ); + expect(result).toEqual(invalidVerifyResponse("invalid_x402_version")); + + result = await facilitator.verify( + { + ...validPayload, + accepted: { ...validPayload.accepted, scheme: "invalid" }, // ❌ wrong scheme + }, + validRequirements, + ); + expect(result).toEqual(invalidVerifyResponse("unsupported_scheme")); + + result = await facilitator.verify( + { + ...validPayload, + accepted: { ...validPayload.accepted, network: "foo:bar" }, // ❌ wrong network + }, + validRequirements, + ); + expect(result).toEqual(invalidVerifyResponse("network_mismatch")); + }); + + it("should reject transactions with fees exceeding the maximum", async () => { + const lowMaxFeeFacilitator = new ExactStellarScheme(facilitatorSigners, { + areFeesSponsored: true, + maxTransactionFeeStroops: 1000, // 1000 stroops max + }); + + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockServer as rpc.Server); + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + + const result = await lowMaxFeeFacilitator.verify(validPayload, validRequirements); + expect(result).toEqual( + invalidVerifyResponse("invalid_exact_stellar_payload_fee_exceeds_maximum"), + ); + }); + + it("should reject transactions with fees below simulation minimum", async () => { + const expectedAssetHashForFeeTest = new Address(ASSET).toScAddress().contractId(); + const mockTransferEventForFeeTest = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt("10000"), + contractId: expectedAssetHashForFeeTest, + }); + + const originalSimulate = vi.mocked(stellarUtils.getRpcClient).getMockImplementation(); + const mockServerWithHighMinFee = { + ...mockServer, + simulateTransaction: vi.fn().mockResolvedValue({ + id: "test", + latestLedger: 123, + events: [mockTransferEventForFeeTest], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "999999999", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse), + }; + + vi.mocked(stellarUtils.getRpcClient).mockReturnValue( + mockServerWithHighMinFee as unknown as rpc.Server, + ); + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + + try { + const result = await facilitator.verify(validPayload, validRequirements); + expect(result).toEqual( + invalidVerifyResponse("invalid_exact_stellar_payload_fee_below_minimum", CLIENT_PUBLIC), + ); + } finally { + vi.mocked(stellarUtils.getRpcClient).mockImplementation(originalSimulate!); + } + }); + + describe("mismatching networks", () => { + it("should reject mismatching requirement<>payload networks", async () => { + const requirements: PaymentRequirements = { + ...validRequirements, + network: "eip155:84532" as never, // ❌ requirements network != payload accepted + }; + const result = await facilitator.verify(validPayload, requirements); + expect(result).toEqual(invalidVerifyResponse("network_mismatch")); + }); + + it("should reject when network is not a Stellar network", async () => { + const wrongNetwork = "eip155:84532" as never; // ❌ non-Stellar network + const requirements: PaymentRequirements = { + ...validRequirements, + network: wrongNetwork, + }; + const payload: PaymentPayload = { + ...validPayload, + accepted: { ...validPayload.accepted, network: wrongNetwork }, + }; + const result = await facilitator.verify(payload, requirements); + expect(result).toEqual(invalidVerifyResponse("invalid_network")); + }); + }); + + it("should reject malformed transaction XDR", async () => { + const payload = { + ...validPayload, + payload: { transaction: "AAAA" }, // ❌ Invalid XDR + }; + const result = await facilitator.verify(payload, validRequirements); + expect(result).toEqual(invalidVerifyResponse("invalid_exact_stellar_payload_malformed")); + }); + + it("should reject wrong operation count", async () => { + expect(baseSorobanData).toBeDefined(); + + const parsedOperation = Operation.invokeHostFunction(baseOperation); + const modifiedTx = new TransactionBuilder(account, { + fee: baseTransaction.fee, + networkPassphrase, + ledgerbounds: baseTransaction.ledgerBounds, + sorobanData: baseSorobanData, + }) + .addOperation(parsedOperation) + .addOperation(parsedOperation) // ❌ Multiple operations are forbidden + .setTimeout(validRequirements.maxTimeoutSeconds) + .build(); + + const modifiedStellarPayload: PaymentPayload = { + ...validPayload, + payload: { transaction: modifiedTx.toXDR() }, + }; + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_wrong_operation"); + }); + + it("should reject wrong operation type", async () => { + const paymentOp = Operation.payment({ + // ❌ operation of unsupported type (MUST be invokeHostFunction) + destination: CLIENT_PUBLIC, + asset: Asset.native(), + amount: "1", + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(paymentOp, { + includeSorobanData: false, + }); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_wrong_operation"); + }); + + it("should reject wrong contract function name", async () => { + const wrongFuncArgs = new xdr.InvokeContractArgs({ + contractAddress: baseInvokeContractArgs.contractAddress(), + functionName: "mint", // ❌ function name MUST be "transfer" + args: baseInvokeContractArgs.args(), + }); + const modifiedFunc = xdr.HostFunction.hostFunctionTypeInvokeContract(wrongFuncArgs); + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + func: modifiedFunc, + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_wrong_function_name"); + }); + + it("should reject wrong asset, recipient, or amount", async () => { + let result = await facilitator.verify(validPayload, { + ...validRequirements, + asset: "CDNVQW44C3HALYNVQ4SOBXY5EWYTGVYXX6JPESOLQDABJI5FC5LTRRUE", // ❌ wrong asset + }); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_wrong_asset"); + + result = await facilitator.verify(validPayload, { + ...validRequirements, + payTo: "GAHPYWLK6YRN7CVYZOO4H3VDRZ7PVF5UJGLZCSPAEIKJE2XSWF5LAGER", // ❌ wrong recipient + }); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_wrong_recipient"); + expect(result.payer).toBe(CLIENT_PUBLIC); + + result = await facilitator.verify(validPayload, { + ...validRequirements, + amount: "10001", // ❌ wrong amount + }); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_wrong_amount"); + expect(result.payer).toBe(CLIENT_PUBLIC); + }); + + describe("Authorization entries and facilitator safety", () => { + it("should reject when facilitator is the payer (from address)", async () => { + const facilitatorAddress = facilitatorSigner1.address; + + if (!baseSorobanData || !baseOperation.auth?.length) { + throw new Error("Missing sorobanData or auth in test transaction"); + } + + const originalArgs = baseInvokeContractArgs.args(); + const facilitatorKeypair = Keypair.fromPublicKey(facilitatorAddress); + const facilitatorScAddress = xdr.ScVal.scvAddress( + xdr.ScAddress.scAddressTypeAccount( + xdr.PublicKey.publicKeyTypeEd25519(facilitatorKeypair.rawPublicKey()), + ), + ); + + const modifiedInvokeContractArgs = new xdr.InvokeContractArgs({ + contractAddress: baseInvokeContractArgs.contractAddress(), + functionName: baseInvokeContractArgs.functionName(), + args: [ + facilitatorScAddress, // ❌ facilitator CANNOT be the payer + originalArgs[1], + originalArgs[2], + ], + }); + const modifiedFunc = xdr.HostFunction.hostFunctionTypeInvokeContract( + modifiedInvokeContractArgs, + ); + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + func: modifiedFunc, + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_facilitator_is_payer"); + }); + + it("should reject empty auth entries array", async () => { + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [], // ❌ Empty auth array + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_no_auth_entries"); + }); + + it("should reject missing payer signature", async () => { + if (!baseSorobanData || !baseOperation.auth || baseOperation.auth.length === 0) { + throw new Error("Missing sorobanData or auth in test transaction"); + } + + const originalAuth = baseOperation.auth[0]; + const originalCreds = originalAuth.credentials().address(); + + const unsignedCreds = new xdr.SorobanAddressCredentials({ + address: originalCreds.address(), + nonce: originalCreds.nonce(), + signatureExpirationLedger: originalCreds.signatureExpirationLedger(), + signature: xdr.ScVal.scvVoid(), // ❌ payer signature is missing + }); + + const authWithoutSignature = new xdr.SorobanAuthorizationEntry({ + credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(unsignedCreds), + rootInvocation: originalAuth.rootInvocation(), + }); + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [authWithoutSignature], + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_missing_payer_signature"); + expect(result.payer).toBe(CLIENT_PUBLIC); + }); + + it("should reject unexpected pending signatures", async () => { + if (!baseSorobanData || !baseOperation.auth || baseOperation.auth.length === 0) { + throw new Error("Missing sorobanData or auth in test transaction"); + } + + const originalAuth = baseOperation.auth[0]; + const originalCreds = originalAuth.credentials().address(); + + const otherKeypair = Keypair.random(); + const otherAddressScVal = xdr.ScAddress.scAddressTypeAccount( + xdr.PublicKey.publicKeyTypeEd25519(otherKeypair.rawPublicKey()), + ); + + const pendingCreds = new xdr.SorobanAddressCredentials({ + address: otherAddressScVal, + nonce: originalCreds.nonce(), + signatureExpirationLedger: originalCreds.signatureExpirationLedger(), + signature: xdr.ScVal.scvVoid(), + }); + + const pendingAuth = new xdr.SorobanAuthorizationEntry({ + credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(pendingCreds), + rootInvocation: originalAuth.rootInvocation(), + }); + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [originalAuth, pendingAuth], // ❌ unexpected pending signature(s) + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe( + "invalid_exact_stellar_payload_unexpected_pending_signatures", + ); + expect(result.payer).toBe(CLIENT_PUBLIC); + }); + + it("should reject expiration ledger too far in the future", async () => { + if (!baseSorobanData || !baseOperation.auth || baseOperation.auth.length === 0) { + throw new Error("Missing sorobanData or auth in test transaction"); + } + + const originalAuth = baseOperation.auth[0]; + const originalCreds = originalAuth.credentials().address(); + const farFuture = (txSignatureExpiration + 10_000).toString(); + + const farFutureCreds = new xdr.SorobanAddressCredentials({ + address: originalCreds.address(), + nonce: originalCreds.nonce(), + signatureExpirationLedger: Number(farFuture), // ❌ Signature expiration too far + signature: originalCreds.signature(), + }); + + const authWithFarExpiration = new xdr.SorobanAuthorizationEntry({ + credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(farFutureCreds), + rootInvocation: originalAuth.rootInvocation(), + }); + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [authWithFarExpiration], + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_signature_expiration_too_far"); + expect(result.payer).toBe(CLIENT_PUBLIC); + }); + + describe("credential type validation", () => { + it("should reject auth entries with non-sorobanCredentialsAddress credentials", async () => { + if (!baseSorobanData) { + throw new Error("Missing sorobanData in test transaction"); + } + + const sourceAccountAuth = xdr.SorobanAuthorizationEntry.fromXDR( + xdr.SorobanAuthorizationEntry.toXDR( + new xdr.SorobanAuthorizationEntry({ + credentials: xdr.SorobanCredentials.sorobanCredentialsSourceAccount(), // ❌ not sorobanCredentialsAddress + rootInvocation: baseOperation.auth![0].rootInvocation(), + }), + ), + ); + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [sourceAccountAuth], + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe( + "invalid_exact_stellar_payload_unsupported_credential_type", + ); + }); + }); + + describe("sub-invocation validation", () => { + it("should reject auth entries with sub-invocations", async () => { + if (!baseSorobanData || !baseOperation.auth || baseOperation.auth.length === 0) { + throw new Error("Missing sorobanData or auth in test transaction"); + } + + const originalAuth = baseOperation.auth[0]; + const originalRootInvocation = originalAuth.rootInvocation(); + + const subInvocation = new xdr.SorobanAuthorizedInvocation({ + function: originalRootInvocation.function(), + subInvocations: [], + }); + + const rootWithSubInvocations = new xdr.SorobanAuthorizedInvocation({ + function: originalRootInvocation.function(), + subInvocations: [subInvocation], // ❌ sub-invocations not allowed + }); + + const authWithSubInvocations = new xdr.SorobanAuthorizationEntry({ + credentials: originalAuth.credentials(), + rootInvocation: rootWithSubInvocations, + }); + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [authWithSubInvocations], + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_has_subinvocations"); + }); + }); + + describe("facilitator in auth entries validation", () => { + it("should reject when facilitator address is in auth entries", async () => { + if (!baseSorobanData || !baseOperation.auth || baseOperation.auth.length === 0) { + throw new Error("Missing sorobanData or auth in test transaction"); + } + + const facilitatorAddress = facilitatorSigner1.address; + const originalAuth = baseOperation.auth[0]; + const originalAddressCredentials = originalAuth.credentials().address(); + + const facilitatorKeypair = Keypair.fromPublicKey(facilitatorAddress); + const facilitatorAddressScVal = xdr.ScAddress.scAddressTypeAccount( + xdr.PublicKey.publicKeyTypeEd25519(facilitatorKeypair.rawPublicKey()), + ); + + const facilitatorCredentials = new xdr.SorobanAddressCredentials({ + address: facilitatorAddressScVal, // ❌ facilitator address in auth entry + nonce: originalAddressCredentials.nonce(), + signatureExpirationLedger: originalAddressCredentials.signatureExpirationLedger(), + signature: originalAddressCredentials.signature(), + }); + + const authWithFacilitator = new xdr.SorobanAuthorizationEntry({ + credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(facilitatorCredentials), + rootInvocation: originalAuth.rootInvocation(), + }); + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [authWithFacilitator], + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_facilitator_in_auth"); + }); + }); + + describe("should reject when source is unauthorized", () => { + it("should reject operation.source == facilitatorAccount", async () => { + const facilitatorAddress = facilitatorSigner1.address; + + if (!baseSorobanData) { + throw new Error("Missing sorobanData in test transaction"); + } + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + source: facilitatorAddress, // ❌ operation source is facilitator + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_unsafe_tx_or_op_source"); + }); + + it("should reject transaction.source == facilitatorAccount", async () => { + const facilitatorAddress = facilitatorSigner1.address; + + if (!baseSorobanData) { + throw new Error("Missing sorobanData in test transaction"); + } + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + }); + const facilitatorAccount = new Account(facilitatorAddress, "100"); // ❌ transaction source is facilitator + const modifiedTx = new TransactionBuilder(facilitatorAccount, { + fee: baseTransaction.fee, + networkPassphrase, + ledgerbounds: baseTransaction.ledgerBounds, + sorobanData: baseSorobanData, + }) + .addOperation(modifiedOperation) + .setTimeout(validRequirements.maxTimeoutSeconds) + .build(); + + const modifiedStellarPayload: PaymentPayload = { + ...validPayload, + payload: { + transaction: modifiedTx.toXDR(), + }, + }; + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_unsafe_tx_or_op_source"); + }); + }); + }); + + it("should reject simulation failure", async () => { + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + error: "Simulation failed", // ❌ Simulation error + events: [], + id: "test", + latestLedger: 123, + _parsed: true, + } as Api.SimulateTransactionErrorResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_simulation_failed"); + expect(result.payer).toBe(CLIENT_PUBLIC); + }); + + // Event-based balance change validation + describe("simulation event validation", () => { + const expectedAssetHash = () => new Address(ASSET).toScAddress().contractId(); + + it("should reject when simulation shows multiple transfer events", async () => { + const mockTransferEvent1 = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt(10000), + contractId: expectedAssetHash(), + }); + const mockTransferEvent2 = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: "GAHPYWLK6YRN7CVYZOO4H3VDRZ7PVF5UJGLZCSPAEIKJE2XSWF5LAGER", + amount: BigInt(5000), + contractId: expectedAssetHash(), + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockTransferEvent1, mockTransferEvent2], // ❌ multiple transfer events + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_multiple_transfers"); + }); + + it("should reject when transfer event has wrong from address", async () => { + const mockTransferEvent = createMockContractEvent({ + from: "GAHPYWLK6YRN7CVYZOO4H3VDRZ7PVF5UJGLZCSPAEIKJE2XSWF5LAGER", // ❌ wrong `from` address + to: FACILITATOR_PUBLIC, + amount: BigInt(10000), + contractId: expectedAssetHash(), + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_event_wrong_from"); + }); + + it("should reject when transfer event has wrong to address", async () => { + const mockTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: "GAHPYWLK6YRN7CVYZOO4H3VDRZ7PVF5UJGLZCSPAEIKJE2XSWF5LAGER", // ❌ wrong `to` address + amount: BigInt(10000), + contractId: expectedAssetHash(), + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_event_wrong_to"); + }); + + it("should reject when transfer event has wrong amount", async () => { + const mockTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt(5000), // ❌ wrong `amount` (expected 10000) + contractId: expectedAssetHash(), + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_event_wrong_amount"); + }); + + it("should reject when transfer event has null contractId", async () => { + const mockTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt(10000), + contractId: null, + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe( + "invalid_exact_stellar_payload_event_missing_contract_id", + ); + }); + + it("should reject when transfer event has wrong asset (contract address)", async () => { + const wrongAsset = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; + const mockTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt(10000), + contractId: new Address(wrongAsset).toScAddress().contractId(), + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_event_wrong_asset"); + }); + + it("should reject when no transfer events are present", async () => { + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [], // ❌ no transfer events + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_no_transfer_events"); + }); + + it("should reject when a contract event is not a transfer", async () => { + const mockNonTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: FACILITATOR_PUBLIC, + amount: BigInt(10000), + fnName: "mint", // ❌ event is not "transfer" + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockNonTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_event_not_transfer"); + }); + + it("should ignore non-contract events and still accept valid transfer", async () => { + const mockTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt(10000), + contractId: expectedAssetHash(), + }); + const nonContractEvent = createMockSystemEvent(); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [nonContractEvent, mockTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(true); + }); + }); + }); + + describe("🎉 Successful verification", () => { + it("should verify valid payment", async () => { + const result = await facilitator.verify(validPayload, validRequirements); + expect(result).toEqual(validVerifyResponse(CLIENT_PUBLIC)); + expect(stellarUtils.getRpcClient).toHaveBeenCalledWith(STELLAR_TESTNET_CAIP2, undefined); + expect(mockServer.simulateTransaction).toHaveBeenCalled(); + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/server.test.ts b/typescript/packages/mechanisms/stellar/test/unit/server.test.ts new file mode 100644 index 0000000..3cc2814 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/server.test.ts @@ -0,0 +1,156 @@ +import { describe, it, expect } from "vitest"; +import { + USDC_PUBNET_ADDRESS, + USDC_TESTNET_ADDRESS, + STELLAR_PUBNET_CAIP2, + STELLAR_TESTNET_CAIP2, +} from "../../src/constants"; +import { ExactStellarScheme } from "../../src/exact/server/scheme"; + +describe("ExactStellarScheme", () => { + const server = new ExactStellarScheme(); + + describe("parsePrice", () => { + describe("Stellar Pubnet network", () => { + const network = STELLAR_PUBNET_CAIP2; + + it("should parse dollar string prices", async () => { + const result = await server.parsePrice("$0.10", network); + expect(result.amount).toBe("1000000"); // 0.10 USDC = 1000000 smallest units (7 decimals) + expect(result.asset).toBe(USDC_PUBNET_ADDRESS); + expect(result.extra).toEqual({}); + }); + + it("should parse simple number string prices", async () => { + const result = await server.parsePrice("0.10", network); + expect(result.amount).toBe("1000000"); + expect(result.asset).toBe(USDC_PUBNET_ADDRESS); + }); + + it("should parse number prices", async () => { + const result = await server.parsePrice(0.1, network); + expect(result.amount).toBe("1000000"); + expect(result.asset).toBe(USDC_PUBNET_ADDRESS); + }); + + it("should handle larger amounts", async () => { + const result = await server.parsePrice("100.50", network); + expect(result.amount).toBe("1005000000"); // 100.50 USDC + }); + + it("should handle whole numbers", async () => { + const result = await server.parsePrice("1", network); + expect(result.amount).toBe("10000000"); // 1 USDC (7 decimals) + }); + + it("should avoid floating-point rounding error", async () => { + const result = await server.parsePrice("$4.02", network); + expect(result.amount).toBe("40200000"); // 4.02 USDC + }); + }); + + describe("Stellar Testnet network", () => { + const network = STELLAR_TESTNET_CAIP2; + + it("should use Testnet USDC address", async () => { + const result = await server.parsePrice("1.00", network); + expect(result.asset).toBe(USDC_TESTNET_ADDRESS); + expect(result.amount).toBe("10000000"); + }); + }); + + describe("pre-parsed price objects", () => { + it("should handle pre-parsed price objects with asset", async () => { + const result = await server.parsePrice( + { + amount: "123456", + asset: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", + extra: { foo: "bar" }, + }, + STELLAR_PUBNET_CAIP2, + ); + expect(result.amount).toBe("123456"); + expect(result.asset).toBe("CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"); + expect(result.extra).toEqual({ foo: "bar" }); + }); + + it("should throw for price objects without asset", async () => { + await expect( + async () => await server.parsePrice({ amount: "123456" } as never, STELLAR_PUBNET_CAIP2), + ).rejects.toThrow("Asset address must be specified"); + }); + }); + + describe("error cases", () => { + it("should throw for invalid money formats", async () => { + await expect( + async () => await server.parsePrice("not-a-price!", STELLAR_PUBNET_CAIP2), + ).rejects.toThrow("Invalid money format"); + }); + + it("should throw for invalid amounts", async () => { + await expect( + async () => await server.parsePrice("abc", STELLAR_PUBNET_CAIP2), + ).rejects.toThrow("Invalid money format"); + }); + }); + }); + + describe("enhancePaymentRequirements", () => { + it("should add areFeesSponsored from facilitator to payment requirements", async () => { + const requirements = { + scheme: "exact", + network: STELLAR_PUBNET_CAIP2, + asset: USDC_PUBNET_ADDRESS, + amount: "1000000", + payTo: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + maxTimeoutSeconds: 3600, + extra: {}, + }; + + const result = await server.enhancePaymentRequirements( + requirements as never, + { + x402Version: 2, + scheme: "exact", + network: STELLAR_PUBNET_CAIP2, + extra: { areFeesSponsored: true }, + }, + [], + ); + + expect(result).toEqual({ + ...requirements, + extra: { areFeesSponsored: true }, + }); + }); + + it("should preserve existing extra fields", async () => { + const requirements = { + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + asset: USDC_TESTNET_ADDRESS, + amount: "1000000", + payTo: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + maxTimeoutSeconds: 3600, + extra: { custom: "value" }, + }; + + const result = await server.enhancePaymentRequirements( + requirements as never, + { + x402Version: 2, + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + extra: { areFeesSponsored: true }, + }, + [], + ); + + expect(result.extra).toEqual({ + areFeesSponsored: true, + custom: "value", + }); + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/shared.test.ts b/typescript/packages/mechanisms/stellar/test/unit/shared.test.ts new file mode 100644 index 0000000..c8beb44 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/shared.test.ts @@ -0,0 +1,155 @@ +import { + SorobanDataBuilder, + xdr, + Networks as StellarNetworks, + Transaction, +} from "@stellar/stellar-sdk"; +import { AssembledTransaction } from "@stellar/stellar-sdk/contract"; +import { Api } from "@stellar/stellar-sdk/rpc"; +import { beforeEach, describe, it, expect, vi } from "vitest"; +import { STELLAR_TESTNET_CAIP2 } from "../../src/constants"; +import { ExactStellarScheme } from "../../src/exact/client/scheme"; +import { gatherAuthEntrySignatureStatus, handleSimulationResult } from "../../src/shared"; +import { createEd25519Signer } from "../../src/signer"; +import * as stellarUtils from "../../src/utils"; +import type { PaymentRequirements } from "@x402/core/types"; + +vi.mock("../../src/utils", async () => { + const actual = await vi.importActual("../../src/utils"); + return { + ...actual, + getEstimatedLedgerCloseTimeSeconds: vi.fn().mockResolvedValue(5), + getRpcClient: vi.fn(), + }; +}); + +describe("Stellar Shared Utilities", () => { + describe("handleSimulationResult", () => { + it("should throw error when simulation is undefined", () => { + expect(() => handleSimulationResult(undefined)).toThrow("Simulation result is undefined"); + }); + + it("should throw error when simulation has type RESTORE", () => { + const mockRestoreSimulation: Api.SimulateTransactionResponse = { + id: "test-id", + latestLedger: 12345, + events: [], + _parsed: true, + result: { + auth: [], + retval: xdr.ScVal.scvVoid(), + }, + restorePreamble: { + minResourceFee: "100", + transactionData: new SorobanDataBuilder(), + }, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + } as Api.SimulateTransactionRestoreResponse; + + expect(() => handleSimulationResult(mockRestoreSimulation)).toThrow( + /Stellar simulation result has type "RESTORE"/, + ); + }); + + it("should throw error when simulation has type ERROR", () => { + const mockErrorSimulation: Api.SimulateTransactionResponse = { + id: "test-id", + latestLedger: 12345, + _parsed: true, + error: "Transaction simulation failed: insufficient balance", + } as Api.SimulateTransactionErrorResponse; + + expect(() => handleSimulationResult(mockErrorSimulation)).toThrow( + /Stellar simulation failed with error message: Transaction simulation failed: insufficient balance/, + ); + }); + + it("should handle simulation with empty error message", () => { + const mockErrorSimulation: Api.SimulateTransactionResponse = { + id: "test-id", + latestLedger: 12345, + _parsed: true, + error: "", + } as Api.SimulateTransactionErrorResponse; + + expect(() => handleSimulationResult(mockErrorSimulation)).toThrow( + /Stellar simulation failed/, + ); + }); + + it("should not throw error when simulation is successful", () => { + const mockSuccessSimulation: Api.SimulateTransactionResponse = { + id: "test-id", + latestLedger: 12345, + events: [], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + } as Api.SimulateTransactionSuccessResponse; + + expect(() => handleSimulationResult(mockSuccessSimulation)).not.toThrow(); + }); + }); + + describe("gatherAuthEntrySignatureStatus", () => { + const CLIENT_SECRET = "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK"; + const CLIENT_PUBLIC = "GBBO4ZDDZTSM2IUKQYBAST3CFHNPFXECGEFTGWTA2WELR2BIWDK57UVE"; + + const mockRpcServer = { + getLatestLedger: vi.fn().mockResolvedValue({ sequence: 100000 }), + }; + + beforeEach(() => { + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockRpcServer as never); + }); + + // paymenrRequirements is used to create a valid payload for the test + const paymentRequirements: PaymentRequirements = { + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + asset: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", + amount: "1000000", + payTo: "GCHEI4PQEFJOA27MNZRPQNLGURS6KASW76X5UZCUZIXCOJLKXYCXOR2W", + maxTimeoutSeconds: 60, + extra: { + areFeesSponsored: true, + }, + }; + + it("should identify signed accounts and no pending signatures", async () => { + const signer = createEd25519Signer(CLIENT_SECRET, STELLAR_TESTNET_CAIP2); + const signedTxJson = + "eyJtZXRob2QiOiJ0cmFuc2ZlciIsInR4IjoiQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQURsM0lBQUFBQUFBQUFBUUFBQUFFQUFBQUFBQUFBQUFBQUFBQnBGcEdGQUFBQUFBQUFBQUVBQUFBQUFBQUFHQUFBQUFBQUFBQUJVRVhOWHNCeW1uYVAxYTBDVUZoUzMwOENqYzZERGxyRklnbTZTRWc3THdFQUFBQUlkSEpoYm5ObVpYSUFBQUFEQUFBQUVnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBU0FBQUFBQUFBQUFDT1JISHdJVkxnYSt4dVl2ZzFacVJsNVFKVy82L2FaRlRLTGljbGFyNEZkd0FBQUFvQUFBQUFBQUFBQUFBQUFBQUFBQ2NRQUFBQUFRQUFBQUVBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZlh4amsrOHlZOGhnQUk4ck9BQUFBRUFBQUFBRUFBQUFCQUFBQUVRQUFBQUVBQUFBQ0FBQUFEd0FBQUFwd2RXSnNhV05mYTJWNUFBQUFBQUFOQUFBQUlFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUR3QUFBQWx6YVdkdVlYUjFjbVVBQUFBQUFBQU5BQUFBUUl2bjJjU3VLbFl5TU96T0pTWnkwc0VaN3dkN1QwYmdSQ0ZxZjg1M3VXQXFVcjE1ZUpycXNqVjROUVpTQW05WXNWbHZEcEUrSFRLc3pUQUVBaTJBRkFnQUFBQUFBQUFBQVZCRnpWN0FjcHAyajlXdEFsQllVdDlQQW8zT2d3NWF4U0lKdWtoSU95OEJBQUFBQ0hSeVlXNXpabVZ5QUFBQUF3QUFBQklBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFFZ0FBQUFBQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUtBQUFBQUFBQUFBQUFBQUFBQUFBbkVBQUFBQUFBQUFBQkFBQUFBQUFBQUFJQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBQmdBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFCUUFBQUFCQUFBQUF3QUFBQUVBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFFQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUJWVk5FUXdBQUFBQkNQbjBGOHV5dnYrd1pLeUZhUHh2cGF1MjQyT2NDVkt2alFUNENCOTVXc2dBQUFBWUFBQUFBQUFBQUFFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUZWOFk1UHZNbVBJWUFBQUFBQUFMNVRFQUFBRjRBQUFCTkFBQUFBQUFBNWNPQUFBQUFBPT0iLCJzaW11bGF0aW9uUmVzdWx0Ijp7ImF1dGgiOlsiQUFBQUFRQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDlmR09UN3pKanlHQUFBQUFBQUFBQUJBQUFBQUFBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFBaDBjbUZ1YzJabGNnQUFBQU1BQUFBU0FBQUFBQUFBQUFCQzdtUmp6T1ROSW9xR0FnbFBZaW5hOHR5Q01Rc3pXbURWaUxqb0tMRFYzd0FBQUJJQUFBQUFBQUFBQUk1RWNmQWhVdUJyN0c1aStEVm1wR1hsQWxiL3I5cGtWTW91SnlWcXZnVjNBQUFBQ2dBQUFBQUFBQUFBQUFBQUFBQUFKeEFBQUFBQSJdLCJyZXR2YWwiOiJBQUFBQVE9PSJ9LCJzaW11bGF0aW9uVHJhbnNhY3Rpb25EYXRhIjoiQUFBQUFBQUFBQUlBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFCZ0FBQUFGUVJjMWV3SEthZG8vVnJRSlFXRkxmVHdLTnpvTU9Xc1VpQ2JwSVNEc3ZBUUFBQUJRQUFBQUJBQUFBQXdBQUFBRUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOEFBQUFCVlZORVF3QUFBQUJDUG4wRjh1eXZ2K3daS3lGYVB4dnBhdTI0Mk9jQ1ZLdmpRVDRDQjk1V3NnQUFBQUVBQUFBQWprUng4Q0ZTNEd2c2JtTDROV2FrWmVVQ1Z2K3YybVJVeWk0bkpXcStCWGNBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFZQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBRlY4WTVQdk1tUElZQUFBQUFBQUw1VEVBQUFGNEFBQUJOQUFBQUFBQUE1Y08ifQ=="; + const { tx: transactionXDR } = JSON.parse( + Buffer.from(signedTxJson, "base64").toString("utf8"), + ); + + let needsSigning: string[] = [CLIENT_PUBLIC]; + vi.spyOn(AssembledTransaction, "build").mockResolvedValue({ + simulation: {} as Api.SimulateTransactionSuccessResponse, + needsNonInvokerSigningBy: vi.fn(() => { + const result = needsSigning; + needsSigning = []; + return result; + }), + signAuthEntries: vi.fn().mockResolvedValue(undefined), + simulate: vi.fn().mockResolvedValue(undefined), + built: { toXDR: () => transactionXDR }, + } as unknown as AssembledTransaction); + + const scheme = new ExactStellarScheme(signer); + const payload = await scheme.createPaymentPayload(1, paymentRequirements); + + if (!("transaction" in payload.payload)) { + throw new Error("Expected Stellar payload with transaction property"); + } + + const tx = new Transaction(payload.payload.transaction as string, StellarNetworks.TESTNET); + const status = gatherAuthEntrySignatureStatus({ transaction: tx }); + + expect(status.alreadySigned).toContain(CLIENT_PUBLIC); + expect(status.pendingSignature).toHaveLength(0); + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/signer.test.ts b/typescript/packages/mechanisms/stellar/test/unit/signer.test.ts new file mode 100644 index 0000000..3ebe6d6 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/signer.test.ts @@ -0,0 +1,148 @@ +import { Keypair, Networks as StellarNetworks } from "@stellar/stellar-sdk"; +import { AssembledTransaction } from "@stellar/stellar-sdk/contract"; +import { describe, expect, it } from "vitest"; +import { DEFAULT_TESTNET_RPC_URL, STELLAR_TESTNET_CAIP2 } from "../../src/constants"; +import { + createEd25519Signer, + isClientStellarSigner, + isFacilitatorStellarSigner, +} from "../../src/signer"; + +describe("Stellar Ed25519 Signer", () => { + const validSecret = "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK"; + const validPublicKey = "GBBO4ZDDZTSM2IUKQYBAST3CFHNPFXECGEFTGWTA2WELR2BIWDK57UVE"; + + describe("createEd25519Signer", () => { + it("should create signer with all required methods", async () => { + const signer = createEd25519Signer(validSecret, STELLAR_TESTNET_CAIP2); + + expect(signer.address).toBe(validPublicKey); + expect(signer.signAuthEntry).toBeInstanceOf(Function); + expect(signer.signTransaction).toBeInstanceOf(Function); + }); + + it("should create different signers for different keys", async () => { + const secret1 = "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK"; + const secret2 = "SBFCBBFETW6U5HSOADUUXTQMUEXK7DIQLLATM6OVKKBQNG3I3EWIYJAW"; + + const signer1 = createEd25519Signer(secret1, STELLAR_TESTNET_CAIP2); + const signer2 = createEd25519Signer(secret2, STELLAR_TESTNET_CAIP2); + + expect(signer1.address).not.toBe(signer2.address); + }); + + it("should throw for invalid secret key format", () => { + expect(() => createEd25519Signer("INVALID_SECRET_KEY", STELLAR_TESTNET_CAIP2)).toThrow(); + }); + + it("should throw for empty string", () => { + expect(() => createEd25519Signer("", STELLAR_TESTNET_CAIP2)).toThrow(); + }); + + it("should throw for public key instead of secret", () => { + expect(() => createEd25519Signer(validPublicKey, STELLAR_TESTNET_CAIP2)).toThrow(); + }); + + it("should throw for invalid network", () => { + expect(() => createEd25519Signer(validSecret, "invalid:network")).toThrow( + "Unknown Stellar network: invalid:network", + ); + }); + + it("should create a signer that can sign auth entries", async () => { + const unsignedTxXDR = + "eyJtZXRob2QiOiJ0cmFuc2ZlciIsInR4IjoiQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQURsM0lBQUFBQUFBQUFBUUFBQUFFQUFBQUFBQUFBQUFBQUFBQnBGcEdGQUFBQUFBQUFBQUVBQUFBQUFBQUFHQUFBQUFBQUFBQUJVRVhOWHNCeW1uYVAxYTBDVUZoUzMwOENqYzZERGxyRklnbTZTRWc3THdFQUFBQUlkSEpoYm5ObVpYSUFBQUFEQUFBQUVnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBU0FBQUFBQUFBQUFDT1JISHdJVkxnYSt4dVl2ZzFacVJsNVFKVy82L2FaRlRLTGljbGFyNEZkd0FBQUFvQUFBQUFBQUFBQUFBQUFBQUFBQ2NRQUFBQUFRQUFBQUVBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZlh4amsrOHlZOGhnQUFBQUFBQUFBQVFBQUFBQUFBQUFCVUVYTlhzQnltbmFQMWEwQ1VGaFMzMDhDamM2RERsckZJZ202U0VnN0x3RUFBQUFJZEhKaGJuTm1aWElBQUFBREFBQUFFZ0FBQUFBQUFBQUFRdTVrWTh6a3pTS0toZ0lKVDJJcDJ2TGNnakVMTTFwZzFZaTQ2Q2l3MWQ4QUFBQVNBQUFBQUFBQUFBQ09SSEh3SVZMZ2EreHVZdmcxWnFSbDVRSlcvNi9hWkZUS0xpY2xhcjRGZHdBQUFBb0FBQUFBQUFBQUFBQUFBQUFBQUNjUUFBQUFBQUFBQUFFQUFBQUFBQUFBQWdBQUFBQUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOEFBQUFHQUFBQUFWQkZ6VjdBY3BwMmo5V3RBbEJZVXQ5UEFvM09ndzVheFNJSnVraElPeThCQUFBQUZBQUFBQUVBQUFBREFBQUFBUUFBQUFCQzdtUmp6T1ROSW9xR0FnbFBZaW5hOHR5Q01Rc3pXbURWaUxqb0tMRFYzd0FBQUFGVlUwUkRBQUFBQUVJK2ZRWHk3SysvN0JrcklWby9HK2xxN2JqWTV3SlVxK05CUGdJSDNsYXlBQUFBQVFBQUFBQ09SSEh3SVZMZ2EreHVZdmcxWnFSbDVRSlcvNi9hWkZUS0xpY2xhcjRGZHdBQUFBRlZVMFJEQUFBQUFFSStmUVh5N0srLzdCa3JJVm8vRytscTdialk1d0pVcStOQlBnSUgzbGF5QUFBQUJnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBVlh4amsrOHlZOGhnQUFBQUFBQXZsTVFBQUFYZ0FBQUUwQUFBQUFBQURsdzRBQUFBQSIsInNpbXVsYXRpb25SZXN1bHQiOnsiYXV0aCI6WyJBQUFBQVFBQUFBQUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOWZHT1Q3ekpqeUdBQUFBQUFBQUFBQkFBQUFBQUFBQUFGUVJjMWV3SEthZG8vVnJRSlFXRkxmVHdLTnpvTU9Xc1VpQ2JwSVNEc3ZBUUFBQUFoMGNtRnVjMlpsY2dBQUFBTUFBQUFTQUFBQUFBQUFBQUJDN21SanpPVE5Jb3FHQWdsUFlpbmE4dHlDTVFzeldtRFZpTGpvS0xEVjN3QUFBQklBQUFBQUFBQUFBSTVFY2ZBaFV1QnI3RzVpK0RWbXBHWGxBbGIvcjlwa1ZNb3VKeVZxdmdWM0FBQUFDZ0FBQUFBQUFBQUFBQUFBQUFBQUp4QUFBQUFBIl0sInJldHZhbCI6IkFBQUFBUT09In0sInNpbXVsYXRpb25UcmFuc2FjdGlvbkRhdGEiOiJBQUFBQUFBQUFBSUFBQUFBQUFBQUFFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUJnQUFBQUZRUmMxZXdIS2Fkby9WclFKUVdGTGZUd0tOem9NT1dzVWlDYnBJU0RzdkFRQUFBQlFBQUFBQkFBQUFBd0FBQUFFQUFBQUFRdTVrWTh6a3pTS0toZ0lKVDJJcDJ2TGNnakVMTTFwZzFZaTQ2Q2l3MWQ4QUFBQUJWVk5FUXdBQUFBQkNQbjBGOHV5dnYrd1pLeUZhUHh2cGF1MjQyT2NDVkt2alFUNENCOTVXc2dBQUFBRUFBQUFBamtSeDhDRlM0R3ZzYm1MNE5XYWtaZVVDVnYrdjJtUlV5aTRuSldxK0JYY0FBQUFCVlZORVF3QUFBQUJDUG4wRjh1eXZ2K3daS3lGYVB4dnBhdTI0Mk9jQ1ZLdmpRVDRDQjk1V3NnQUFBQVlBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFGVjhZNVB2TW1QSVlBQUFBQUFBTDVURUFBQUY0QUFBQk5BQUFBQUFBQTVjTyJ9"; + const expectedSignedTxXDR = + "eyJtZXRob2QiOiJ0cmFuc2ZlciIsInR4IjoiQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQURsM0lBQUFBQUFBQUFBUUFBQUFFQUFBQUFBQUFBQUFBQUFBQnBGcEdGQUFBQUFBQUFBQUVBQUFBQUFBQUFHQUFBQUFBQUFBQUJVRVhOWHNCeW1uYVAxYTBDVUZoUzMwOENqYzZERGxyRklnbTZTRWc3THdFQUFBQUlkSEpoYm5ObVpYSUFBQUFEQUFBQUVnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBU0FBQUFBQUFBQUFDT1JISHdJVkxnYSt4dVl2ZzFacVJsNVFKVy82L2FaRlRLTGljbGFyNEZkd0FBQUFvQUFBQUFBQUFBQUFBQUFBQUFBQ2NRQUFBQUFRQUFBQUVBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZlh4amsrOHlZOGhnQUk4ck9BQUFBRUFBQUFBRUFBQUFCQUFBQUVRQUFBQUVBQUFBQ0FBQUFEd0FBQUFwd2RXSnNhV05mYTJWNUFBQUFBQUFOQUFBQUlFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUR3QUFBQWx6YVdkdVlYUjFjbVVBQUFBQUFBQU5BQUFBUUl2bjJjU3VLbFl5TU96T0pTWnkwc0VaN3dkN1QwYmdSQ0ZxZjg1M3VXQXFVcjE1ZUpycXNqVjROUVpTQW05WXNWbHZEcEUrSFRLc3pUQUVBaTJBRkFnQUFBQUFBQUFBQVZCRnpWN0FjcHAyajlXdEFsQllVdDlQQW8zT2d3NWF4U0lKdWtoSU95OEJBQUFBQ0hSeVlXNXpabVZ5QUFBQUF3QUFBQklBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFFZ0FBQUFBQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUtBQUFBQUFBQUFBQUFBQUFBQUFBbkVBQUFBQUFBQUFBQkFBQUFBQUFBQUFJQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBQmdBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFCUUFBQUFCQUFBQUF3QUFBQUVBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFFQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUJWVk5FUXdBQUFBQkNQbjBGOHV5dnYrd1pLeUZhUHh2cGF1MjQyT2NDVkt2alFUNENCOTVXc2dBQUFBWUFBQUFBQUFBQUFFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUZWOFk1UHZNbVBJWUFBQUFBQUFMNVRFQUFBRjRBQUFCTkFBQUFBQUFBNWNPQUFBQUFBPT0iLCJzaW11bGF0aW9uUmVzdWx0Ijp7ImF1dGgiOlsiQUFBQUFRQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDlmR09UN3pKanlHQUFBQUFBQUFBQUJBQUFBQUFBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFBaDBjbUZ1YzJabGNnQUFBQU1BQUFBU0FBQUFBQUFBQUFCQzdtUmp6T1ROSW9xR0FnbFBZaW5hOHR5Q01Rc3pXbURWaUxqb0tMRFYzd0FBQUJJQUFBQUFBQUFBQUk1RWNmQWhVdUJyN0c1aStEVm1wR1hsQWxiL3I5cGtWTW91SnlWcXZnVjNBQUFBQ2dBQUFBQUFBQUFBQUFBQUFBQUFKeEFBQUFBQSJdLCJyZXR2YWwiOiJBQUFBQVE9PSJ9LCJzaW11bGF0aW9uVHJhbnNhY3Rpb25EYXRhIjoiQUFBQUFBQUFBQUlBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFCZ0FBQUFGUVJjMWV3SEthZG8vVnJRSlFXRkxmVHdLTnpvTU9Xc1VpQ2JwSVNEc3ZBUUFBQUJRQUFBQUJBQUFBQXdBQUFBRUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOEFBQUFCVlZORVF3QUFBQUJDUG4wRjh1eXZ2K3daS3lGYVB4dnBhdTI0Mk9jQ1ZLdmpRVDRDQjk1V3NnQUFBQUVBQUFBQWprUng4Q0ZTNEd2c2JtTDROV2FrWmVVQ1Z2K3YybVJVeWk0bkpXcStCWGNBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFZQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBRlY4WTVQdk1tUElZQUFBQUFBQUw1VEVBQUFGNEFBQUJOQUFBQUFBQUE1Y08ifQ=="; + + const signer = createEd25519Signer(validSecret, STELLAR_TESTNET_CAIP2); + expect(signer.address).toBe(validPublicKey); + + // parse the unsigned tx XDR into AssembledTransaction + const { method, tx, simulationResult, simulationTransactionData } = JSON.parse( + Buffer.from(unsignedTxXDR, "base64").toString("utf8"), + ); + const recoveredTx = AssembledTransaction.fromJSON( + { + contractId: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", + networkPassphrase: StellarNetworks.TESTNET, + rpcUrl: DEFAULT_TESTNET_RPC_URL, + method, + parseResultXdr: result => result, + }, + { tx, simulationResult, simulationTransactionData }, + ); + + // ensure the tx is the same + const recoveredTxXDR = Buffer.from(recoveredTx.toJSON()).toString("base64"); + expect(recoveredTxXDR).toBe(unsignedTxXDR); + + // ensure the Tx is missing the signer we have + let missingSigners = recoveredTx.needsNonInvokerSigningBy(); + expect(missingSigners).toEqual([validPublicKey]); + + // ensure the tx is signed successfully + await recoveredTx.signAuthEntries({ + address: validPublicKey, + signAuthEntry: signer.signAuthEntry, + expiration: 2345678, + }); + missingSigners = recoveredTx.needsNonInvokerSigningBy(); + expect(missingSigners).toHaveLength(0); + + // ensure the result of the signed tx is the same as the expected signed tx + const signedTxXDR = Buffer.from(recoveredTx.toJSON()).toString("base64"); + expect(signedTxXDR).toBe(expectedSignedTxXDR); + }); + }); + + describe("is*StellarSigner", () => { + it("should return true for both when signer has all methods", () => { + const signer = createEd25519Signer(validSecret, STELLAR_TESTNET_CAIP2); + expect(isFacilitatorStellarSigner(signer)).toBe(true); + expect(isClientStellarSigner(signer)).toBe(true); + }); + + it("should return true for client but false for facilitator when signTransaction missing", () => { + const mockSigner = { + address: validPublicKey, + signAuthEntry: async () => ({ signedAuthEntry: "" }), + }; + expect(isFacilitatorStellarSigner(mockSigner)).toBe(false); + expect(isClientStellarSigner(mockSigner)).toBe(true); + }); + + it("should return false for invalid types", () => { + // false for null + expect(isFacilitatorStellarSigner(null)).toBe(false); + expect(isClientStellarSigner(null)).toBe(false); + // false for undefined + expect(isFacilitatorStellarSigner(undefined)).toBe(false); + expect(isClientStellarSigner(undefined)).toBe(false); + // false for string + expect(isFacilitatorStellarSigner("string")).toBe(false); + expect(isClientStellarSigner("string")).toBe(false); + // false for number + expect(isFacilitatorStellarSigner(123)).toBe(false); + expect(isClientStellarSigner(123)).toBe(false); + // false for empty object + expect(isFacilitatorStellarSigner({})).toBe(false); + expect(isClientStellarSigner({})).toBe(false); + // false for object incomplete object + expect(isFacilitatorStellarSigner({ address: "" })).toBe(false); + expect(isClientStellarSigner({ address: "" })).toBe(false); + // false for similar object with non-string address + const invalidSigner = { + address: 123, + signAuthEntry: () => {}, + signTransaction: () => {}, + }; + expect(isFacilitatorStellarSigner(invalidSigner)).toBe(false); + expect(isClientStellarSigner(invalidSigner)).toBe(false); + // false for Stellar keypair + const keypair = Keypair.fromSecret(validSecret); + expect(isFacilitatorStellarSigner(keypair)).toBe(false); + expect(isClientStellarSigner(keypair)).toBe(false); + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/utils.test.ts b/typescript/packages/mechanisms/stellar/test/unit/utils.test.ts new file mode 100644 index 0000000..4adaf1a --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/utils.test.ts @@ -0,0 +1,398 @@ +import { Horizon, rpc } from "@stellar/stellar-sdk"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { STELLAR_PUBNET_CAIP2, STELLAR_TESTNET_CAIP2 } from "../../src"; +import { + convertToTokenAmount, + DEFAULT_ESTIMATED_LEDGER_SECONDS, + getEstimatedLedgerCloseTimeSeconds, + getNetworkPassphrase, + getRpcClient, + getRpcUrl, + getUsdcAddress, + isStellarNetwork, + RpcConfig, + validateStellarAssetAddress, + validateStellarDestinationAddress, +} from "../../src/utils"; + +// Mock the Stellar SDK +vi.mock("@stellar/stellar-sdk", () => ({ + Horizon: { + Server: vi.fn(), + }, + rpc: { + Server: vi.fn(), + }, +})); + +describe("Stellar RPC Helper Functions", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("validateStellarDestinationAddress", () => { + it("should return true for valid addresses", () => { + expect( + validateStellarDestinationAddress( + "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + ), + ).toBe(true); + }); + + it("should return false for invalid addresses", () => { + expect(validateStellarDestinationAddress("")).toBe(false); + expect(validateStellarDestinationAddress("invalid")).toBe(false); + }); + }); + + describe("validateStellarAssetAddress", () => { + it("should return true for valid C-accounts", () => { + expect( + validateStellarAssetAddress("CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75"), + ).toBe(true); + }); + + it("should return false for invalid addresses", () => { + expect(validateStellarAssetAddress("")).toBe(false); + expect(validateStellarAssetAddress("invalid")).toBe(false); + }); + }); + + describe("isStellarNetwork", () => { + it("should return true for Stellar pubnet", () => { + expect(isStellarNetwork(STELLAR_PUBNET_CAIP2)).toBe(true); + }); + + it("should return true for Stellar testnet", () => { + expect(isStellarNetwork(STELLAR_TESTNET_CAIP2)).toBe(true); + }); + + it("should return false for invalid networks", () => { + expect(isStellarNetwork("invalid-network" as any)).toBe(false); + expect(isStellarNetwork("" as any)).toBe(false); + }); + + it("should return false for non-Stellar CAIP-2 networks", () => { + expect(isStellarNetwork("eip155:1" as any)).toBe(false); + expect(isStellarNetwork("eip155:8453" as any)).toBe(false); + expect(isStellarNetwork("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" as any)).toBe(false); + }); + }); + + describe("getNetworkPassphrase", () => { + it("should return the correct passphrase for stellar (mainnet)", () => { + const result = getNetworkPassphrase(STELLAR_PUBNET_CAIP2); + expect(result).toBe("Public Global Stellar Network ; September 2015"); + }); + + it("should return the correct passphrase for stellar testnet", () => { + const result = getNetworkPassphrase(STELLAR_TESTNET_CAIP2); + expect(result).toBe("Test SDF Network ; September 2015"); + }); + + it("should throw error for unknown network", () => { + expect(() => getNetworkPassphrase("invalid-network" as any)).toThrow( + "Unknown Stellar network: invalid-network", + ); + }); + }); + + describe("getRpcUrl", () => { + describe(STELLAR_TESTNET_CAIP2, () => { + it("should return default testnet URL when no config provided", () => { + const result = getRpcUrl(STELLAR_TESTNET_CAIP2); + expect(result).toBe("https://soroban-testnet.stellar.org"); + }); + + it("should return custom URL when provided in rpcConfig", () => { + const customUrl = "https://custom-stellar-testnet-rpc.example.com"; + const rpcConfig: RpcConfig = { url: customUrl }; + const result = getRpcUrl(STELLAR_TESTNET_CAIP2, rpcConfig); + expect(result).toBe(customUrl); + }); + }); + + describe("stellar mainnet", () => { + it("should throw error when no config provided for mainnet", () => { + expect(() => getRpcUrl(STELLAR_PUBNET_CAIP2)).toThrow( + "Stellar mainnet requires a non-empty rpcUrl. For a list of RPC providers, see https://developers.stellar.org/docs/data/apis/rpc/providers#publicly-accessible-apis", + ); + }); + + it("should throw error when rpcConfig provided without url for mainnet", () => { + const rpcConfig: RpcConfig = {}; + expect(() => getRpcUrl(STELLAR_PUBNET_CAIP2, rpcConfig)).toThrow( + "Stellar mainnet requires a non-empty rpcUrl. For a list of RPC providers, see https://developers.stellar.org/docs/data/apis/rpc/providers#publicly-accessible-apis", + ); + }); + + it("should return custom URL when provided in rpcConfig for mainnet", () => { + const customUrl = "https://custom-stellar-mainnet-rpc.example.com"; + const config: RpcConfig = { url: customUrl }; + const result = getRpcUrl(STELLAR_PUBNET_CAIP2, config); + expect(result).toBe(customUrl); + }); + }); + + describe("invalid networks", () => { + it("should throw error for unknown network", () => { + expect(() => getRpcUrl("invalid-network" as any)).toThrow( + "Unknown Stellar network: invalid-network", + ); + }); + }); + }); + + describe("getRpcClient", () => { + describe(STELLAR_TESTNET_CAIP2, () => { + it("should create RPC client with default testnet URL when no config provided", () => { + const mockServer = { mock: "testnet-server" }; + vi.mocked(rpc.Server).mockReturnValue(mockServer as any); + + const result = getRpcClient(STELLAR_TESTNET_CAIP2); + + expect(rpc.Server).toHaveBeenCalledWith("https://soroban-testnet.stellar.org", { + allowHttp: true, + }); + expect(result).toBe(mockServer); + }); + + it("should create RPC client with custom URL when provided in rpcConfig", () => { + const customUrl = "https://custom-testnet-rpc.com"; + const mockServer = { mock: "testnet-server-custom" }; + vi.mocked(rpc.Server).mockReturnValue(mockServer as any); + + const rpcConfig: RpcConfig = { url: customUrl }; + const result = getRpcClient(STELLAR_TESTNET_CAIP2, rpcConfig); + + expect(rpc.Server).toHaveBeenCalledWith(customUrl, { + allowHttp: true, + }); + expect(result).toBe(mockServer); + }); + + it("should allow HTTP for testnet", () => { + const mockServer = { mock: "testnet-server" }; + vi.mocked(rpc.Server).mockReturnValue(mockServer as any); + + getRpcClient(STELLAR_TESTNET_CAIP2); + + expect(rpc.Server).toHaveBeenCalledWith(expect.any(String), { + allowHttp: true, + }); + }); + }); + + describe("stellar mainnet", () => { + it("should throw error when no config provided for mainnet", () => { + expect(() => getRpcClient(STELLAR_PUBNET_CAIP2)).toThrow( + "Stellar mainnet requires a non-empty rpcUrl. For a list of RPC providers, see https://developers.stellar.org/docs/data/apis/rpc/providers#publicly-accessible-apis", + ); + }); + + it("should create RPC client with custom URL for mainnet", () => { + const customUrl = "https://custom-mainnet-rpc.com"; + const mockServer = { mock: "mainnet-server" }; + vi.mocked(rpc.Server).mockReturnValue(mockServer as any); + + const rpcConfig: RpcConfig = { url: customUrl }; + const result = getRpcClient(STELLAR_PUBNET_CAIP2, rpcConfig); + + expect(rpc.Server).toHaveBeenCalledWith(customUrl, { + allowHttp: false, + }); + expect(result).toBe(mockServer); + }); + + it("should not allow HTTP for mainnet", () => { + const customUrl = "https://custom-mainnet-rpc.com"; + const mockServer = { mock: "mainnet-server" }; + vi.mocked(rpc.Server).mockReturnValue(mockServer as any); + + const rpcConfig: RpcConfig = { url: customUrl }; + getRpcClient(STELLAR_PUBNET_CAIP2, rpcConfig); + expect(rpc.Server).toHaveBeenCalledWith(expect.any(String), { + allowHttp: false, + }); + }); + }); + + describe("invalid networks", () => { + it("should throw error for unknown network", () => { + expect(() => getRpcClient("invalid-network" as any)).toThrow( + "Unknown Stellar network: invalid-network", + ); + }); + + it("should throw error for non-Stellar network", () => { + expect(() => getRpcClient("base" as any)).toThrow("Unknown Stellar network: base"); + }); + }); + }); + + describe("getEstimatedLedgerCloseTimeSeconds", () => { + function mockHorizonServer(records: Array<{ closed_at: string; sequence: number }>) { + const mockCall = vi.fn().mockResolvedValue({ records }); + const mockOrder = vi.fn().mockReturnValue({ call: mockCall }); + const mockLimit = vi.fn().mockReturnValue({ order: mockOrder }); + const mockLedgers = vi.fn().mockReturnValue({ limit: mockLimit }); + vi.mocked(Horizon.Server).mockImplementation(() => ({ ledgers: mockLedgers }) as any); + return { mockCall, mockOrder, mockLimit, mockLedgers }; + } + + it("should compute seconds per ledger from Horizon SDK ledgers response", async () => { + const baseTs = 1734032457; + const records = [105, 104, 103, 102, 101, 100].map((seq, i) => ({ + sequence: seq, + closed_at: new Date((baseTs + (5 - i) * 3) * 1000).toISOString(), + })); + const { mockLedgers, mockLimit, mockOrder } = mockHorizonServer(records); + + const result = await getEstimatedLedgerCloseTimeSeconds(STELLAR_TESTNET_CAIP2); + + expect(result).toBe(3); + expect(Horizon.Server).toHaveBeenCalledWith("https://horizon-testnet.stellar.org"); + expect(mockLedgers).toHaveBeenCalled(); + expect(mockLimit).toHaveBeenCalledWith(20); + expect(mockOrder).toHaveBeenCalledWith("desc"); + }); + + it("should return DEFAULT_ESTIMATED_LEDGER_SECONDS when SDK call throws", async () => { + const mockCall = vi.fn().mockRejectedValue(new Error("Network error")); + const mockOrder = vi.fn().mockReturnValue({ call: mockCall }); + const mockLimit = vi.fn().mockReturnValue({ order: mockOrder }); + const mockLedgers = vi.fn().mockReturnValue({ limit: mockLimit }); + vi.mocked(Horizon.Server).mockImplementation(() => ({ ledgers: mockLedgers }) as any); + + const result = await getEstimatedLedgerCloseTimeSeconds(STELLAR_TESTNET_CAIP2); + + expect(result).toBe(DEFAULT_ESTIMATED_LEDGER_SECONDS); + }); + + it("should return DEFAULT_ESTIMATED_LEDGER_SECONDS when fewer than 2 records", async () => { + mockHorizonServer([{ sequence: 100, closed_at: "2024-12-13T00:00:57Z" }]); + + const result = await getEstimatedLedgerCloseTimeSeconds(STELLAR_TESTNET_CAIP2); + + expect(result).toBe(DEFAULT_ESTIMATED_LEDGER_SECONDS); + }); + + it("should use pubnet Horizon URL for pubnet network", async () => { + const baseTs = 1734032457; + const records = [102, 101, 100].map((seq, i) => ({ + sequence: seq, + closed_at: new Date((baseTs + (2 - i) * 6) * 1000).toISOString(), + })); + mockHorizonServer(records); + + const result = await getEstimatedLedgerCloseTimeSeconds(STELLAR_PUBNET_CAIP2); + + expect(result).toBe(6); + expect(Horizon.Server).toHaveBeenCalledWith("https://horizon.stellar.org"); + }); + }); + + describe("getUsdcAddress", () => { + it("should return USDC address for mainnet", () => { + const result = getUsdcAddress(STELLAR_PUBNET_CAIP2); + expect(result).toBe("CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75"); + }); + + it("should return USDC address for testnet", () => { + const result = getUsdcAddress(STELLAR_TESTNET_CAIP2); + expect(result).toBe("CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"); + }); + + it("should throw error for unknown network", () => { + expect(() => getUsdcAddress("invalid-network" as any)).toThrow( + "No USDC address configured for network: invalid-network", + ); + }); + }); + + describe("convertToTokenAmount", () => { + describe("with default 7 decimals", () => { + it("should convert decimal amount correctly", () => { + expect(convertToTokenAmount("0.1")).toBe("1000000"); + expect(convertToTokenAmount("1.5")).toBe("15000000"); + expect(convertToTokenAmount("0.1234567")).toBe("1234567"); + }); + + it("should convert integer amount correctly", () => { + expect(convertToTokenAmount("1")).toBe("10000000"); + expect(convertToTokenAmount("10")).toBe("100000000"); + expect(convertToTokenAmount("0")).toBe("0"); + }); + + it("should handle amounts with trailing zeros", () => { + expect(convertToTokenAmount("1.0")).toBe("10000000"); + expect(convertToTokenAmount("0.1000000")).toBe("1000000"); + }); + + it("should handle very small amounts", () => { + expect(convertToTokenAmount("0.0000001")).toBe("1"); + expect(convertToTokenAmount("0.00000001")).toBe("0"); + }); + + it("should truncate excess decimal places", () => { + expect(convertToTokenAmount("1.12345678")).toBe("11234567"); + }); + }); + + describe("with custom decimals", () => { + it("should convert with 6 decimals", () => { + expect(convertToTokenAmount("1.5", 6)).toBe("1500000"); + expect(convertToTokenAmount("0.1", 6)).toBe("100000"); + }); + + it("should convert with 18 decimals", () => { + expect(convertToTokenAmount("1.0", 18)).toBe("1000000000000000000"); + expect(convertToTokenAmount("0.5", 18)).toBe("500000000000000000"); + }); + + it("should convert with 0 decimals", () => { + expect(convertToTokenAmount("1.5", 0)).toBe("1"); + expect(convertToTokenAmount("2.9", 0)).toBe("2"); + }); + }); + + describe("special cases", () => { + it("should handle negative numbers", () => { + expect(convertToTokenAmount("-1.5")).toBe("-15000000"); + }); + + it("should handle scientific notation input", () => { + // Scientific notation should be correctly converted + // 1e-7 = 0.0000001, which with 7 decimals = 1 + expect(convertToTokenAmount("1e-7")).toBe("1"); + expect(convertToTokenAmount("1e-6")).toBe("10"); + expect(convertToTokenAmount("1.5e-6")).toBe("15"); + }); + + it("should handle very large numbers", () => { + expect(convertToTokenAmount("999999999.9999999")).toBe("9999999999999999"); + }); + }); + + describe("error cases", () => { + it("should throw error for invalid amount", () => { + expect(() => convertToTokenAmount("invalid")).toThrow("Invalid amount: invalid"); + expect(() => convertToTokenAmount("abc")).toThrow("Invalid amount: abc"); + expect(() => convertToTokenAmount("")).toThrow("Invalid amount: "); + }); + + it("should throw error for NaN", () => { + expect(() => convertToTokenAmount("NaN")).toThrow("Invalid amount: NaN"); + }); + + it("should throw error for invalid decimals", () => { + expect(() => convertToTokenAmount("1.5", -1)).toThrow( + "Decimals must be between 0 and 20, got -1", + ); + expect(() => convertToTokenAmount("1.5", 21)).toThrow( + "Decimals must be between 0 and 20, got 21", + ); + }); + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/tsconfig.json b/typescript/packages/mechanisms/stellar/tsconfig.json new file mode 100644 index 0000000..19ba0fb --- /dev/null +++ b/typescript/packages/mechanisms/stellar/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2020"], + "allowJs": false, + "checkJs": false + }, + "include": ["src", "test"] +} diff --git a/typescript/packages/mechanisms/stellar/tsup.config.ts b/typescript/packages/mechanisms/stellar/tsup.config.ts new file mode 100644 index 0000000..1143bfe --- /dev/null +++ b/typescript/packages/mechanisms/stellar/tsup.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from "tsup"; + +const baseConfig = { + entry: { + index: "src/index.ts", + "exact/client/index": "src/exact/client/index.ts", + "exact/server/index": "src/exact/server/index.ts", + "exact/facilitator/index": "src/exact/facilitator/index.ts", + }, + dts: { + resolve: true, + }, + sourcemap: true, + target: "es2020", +}; + +export default defineConfig([ + { + ...baseConfig, + format: "esm", + outDir: "dist/esm", + clean: true, + }, + { + ...baseConfig, + format: "cjs", + outDir: "dist/cjs", + clean: false, + }, +]); diff --git a/typescript/packages/mechanisms/stellar/vitest.config.ts b/typescript/packages/mechanisms/stellar/vitest.config.ts new file mode 100644 index 0000000..1d5c581 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/vitest.config.ts @@ -0,0 +1,15 @@ +import { loadEnv } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + exclude: [ + "**/node_modules/**", + "**/dist/**", + "**/test/integrations/**", // Exclude integration tests from default run + ], + }, + plugins: [tsconfigPaths({ projects: ["."] })], +})); diff --git a/typescript/packages/mechanisms/stellar/vitest.integration.config.ts b/typescript/packages/mechanisms/stellar/vitest.integration.config.ts new file mode 100644 index 0000000..a0e4a65 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/vitest.integration.config.ts @@ -0,0 +1,13 @@ +import { loadEnv } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + include: ["test/integrations/**/*.test.ts"], // Only include integration tests + testTimeout: 20000, + hookTimeout: 20000, + }, + plugins: [tsconfigPaths({ projects: ["."] })], +})); diff --git a/typescript/packages/mechanisms/svm/CHANGELOG.md b/typescript/packages/mechanisms/svm/CHANGELOG.md index 2c43a14..1ed9e6c 100644 --- a/typescript/packages/mechanisms/svm/CHANGELOG.md +++ b/typescript/packages/mechanisms/svm/CHANGELOG.md @@ -1,5 +1,65 @@ # @x402/svm Changelog +## 2.9.0 + +### Minor Changes + +- 2250cae: Migrated project from coinbase/x402 to x402-foundation/x402 organization + +### Patch Changes + +- Updated dependencies [8cf3fca] +- Updated dependencies [c0e3969] +- Updated dependencies [2250cae] +- Updated dependencies [d352574] + - @x402/core@2.9.0 + +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- 7cd93d8: Add in-memory SettlementCache to prevent duplicate SVM transaction settlement during on-chain confirmation window +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + +## 2.5.0 + +### Minor Changes + +- Updated dependencies [96a9db0] +- Updated dependencies [d0a2b11] +- Updated dependencies + - @x402/core@2.5.0 + +## 2.4.0 + +### Minor Changes + +- Updated dependencies [57a5488] +- Updated dependencies [018181b] +- Updated dependencies [3fb55d7] + - @x402/core@2.4.0 + ## 2.3.0 ### Minor Changes diff --git a/typescript/packages/mechanisms/svm/README.md b/typescript/packages/mechanisms/svm/README.md index 5c9b975..01601da 100644 --- a/typescript/packages/mechanisms/svm/README.md +++ b/typescript/packages/mechanisms/svm/README.md @@ -174,6 +174,16 @@ The exact payment scheme uses SPL Token `TransferChecked` instruction with: - Source/destination ATAs (Associated Token Accounts) - Partial signing (client signs, facilitator completes and submits) +## Duplicate Settlement Protection + +This package includes a built-in `SettlementCache` that prevents a known race condition on Solana where the same payment transaction could be settled multiple times before on-chain confirmation. When the facilitator scheme is registered via `registerExactSvmScheme`, a single `SettlementCache` instance is automatically shared across both V1 and V2 scheme versions. + +The cache rejects concurrent `/settle` calls that carry the same transaction payload, returning a `duplicate_settlement` error for the second and subsequent attempts. Entries are automatically evicted after 120 seconds (approximately twice the Solana blockhash lifetime). + +**No additional configuration is required** — duplicate settlement protection is enabled by default when using the standard registration helpers. + +For full details on the race condition and mitigation strategy, see the [Exact SVM Scheme Specification](../../../../specs/schemes/exact/scheme_exact_svm.md#duplicate-settlement-mitigation-recommended). + ## Development ```bash @@ -196,5 +206,5 @@ pnpm format - `@x402/core` - Core protocol types and client - `@x402/fetch` - HTTP wrapper with automatic payment handling - `@x402/evm` - EVM/Ethereum implementation +- `@x402/stellar` - Stellar implementation - `@solana/web3.js` - Solana JavaScript SDK (peer dependency) - diff --git a/typescript/packages/mechanisms/svm/package.json b/typescript/packages/mechanisms/svm/package.json index 8fdfe73..0839884 100644 --- a/typescript/packages/mechanisms/svm/package.json +++ b/typescript/packages/mechanisms/svm/package.json @@ -1,6 +1,6 @@ { "name": "@x402/svm", - "version": "2.3.0", + "version": "2.9.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/cjs/index.d.ts", @@ -24,8 +24,8 @@ "solana" ], "license": "Apache-2.0", - "author": "Coinbase Inc.", - "repository": "https://github.com/coinbase/x402", + "author": "x402 Foundation", + "repository": "https://github.com/x402-foundation/x402", "description": "x402 Payment Protocol SVM Implementation", "devDependencies": { "@eslint/js": "^9.24.0", @@ -49,8 +49,10 @@ "@x402/core": "workspace:~", "@solana-program/compute-budget": "^0.11.0", "@solana-program/token": "^0.9.0", - "@solana-program/token-2022": "^0.6.1", - "@solana/kit": "^5.1.0" + "@solana-program/token-2022": "^0.6.1" + }, + "peerDependencies": { + "@solana/kit": ">=5.1.0" }, "exports": { ".": { diff --git a/typescript/packages/mechanisms/svm/src/constants.ts b/typescript/packages/mechanisms/svm/src/constants.ts index 15a5463..508f430 100644 --- a/typescript/packages/mechanisms/svm/src/constants.ts +++ b/typescript/packages/mechanisms/svm/src/constants.ts @@ -13,7 +13,7 @@ export const MEMO_PROGRAM_ADDRESS = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr * - Phantom adds 1 Lighthouse instruction (4th instruction) * - Solflare adds 2 Lighthouse instructions (4th and 5th instructions) * We allow these as optional instructions to support these wallets. - * See: https://github.com/coinbase/x402/issues/828 + * See: https://github.com/x402-foundation/x402/issues/828 */ export const LIGHTHOUSE_PROGRAM_ADDRESS = "L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95"; @@ -42,6 +42,12 @@ export const DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1; export const MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 5_000_000; // 5 lamports export const DEFAULT_COMPUTE_UNIT_LIMIT = 20_000; +/** + * How long a transaction is held in the duplicate settlement cache (ms). + * Covers the Solana blockhash lifetime (~60-90s) with margin. + */ +export const SETTLEMENT_TTL_MS = 120_000; + /** * Solana address validation regex (base58, 32-44 characters) */ diff --git a/typescript/packages/mechanisms/svm/src/exact/facilitator/register.ts b/typescript/packages/mechanisms/svm/src/exact/facilitator/register.ts index 7055142..25d18c6 100644 --- a/typescript/packages/mechanisms/svm/src/exact/facilitator/register.ts +++ b/typescript/packages/mechanisms/svm/src/exact/facilitator/register.ts @@ -1,5 +1,6 @@ import { x402Facilitator } from "@x402/core/facilitator"; import { Network } from "@x402/core/types"; +import { SettlementCache } from "../../settlement-cache"; import { FacilitatorSvmSigner } from "../../signer"; import { ExactSvmScheme } from "./scheme"; import { ExactSvmSchemeV1 } from "../v1/facilitator/scheme"; @@ -47,11 +48,18 @@ export function registerExactSvmScheme( facilitator: x402Facilitator, config: SvmFacilitatorConfig, ): x402Facilitator { + // Share a single settlement cache across V1 and V2 so that a duplicate + // transaction submitted through one protocol version is also caught by the other. + const settlementCache = new SettlementCache(); + // Register V2 scheme with specified networks - facilitator.register(config.networks, new ExactSvmScheme(config.signer)); + facilitator.register(config.networks, new ExactSvmScheme(config.signer, settlementCache)); // Register all V1 networks - facilitator.registerV1(NETWORKS as Network[], new ExactSvmSchemeV1(config.signer)); + facilitator.registerV1( + NETWORKS as Network[], + new ExactSvmSchemeV1(config.signer, settlementCache), + ); return facilitator; } diff --git a/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts b/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts index 0aa5406..1e81bb8 100644 --- a/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts @@ -29,6 +29,7 @@ import { MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS, MEMO_PROGRAM_ADDRESS, } from "../../constants"; +import { SettlementCache } from "../../settlement-cache"; import type { FacilitatorSvmSigner } from "../../signer"; import type { ExactSvmPayloadV2 } from "../../types"; import { decodeTransactionFromPayload, getTokenPayerFromTransaction } from "../../utils"; @@ -40,13 +41,21 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator { readonly scheme = "exact"; readonly caipFamily = "solana:*"; + private readonly settlementCache: SettlementCache; + /** * Creates a new ExactSvmFacilitator instance. * * @param signer - The SVM RPC client for facilitator operations + * @param settlementCache - Optional shared settlement cache (one is created if omitted) * @returns ExactSvmFacilitator instance */ - constructor(private readonly signer: FacilitatorSvmSigner) {} + constructor( + private readonly signer: FacilitatorSvmSigner, + settlementCache?: SettlementCache, + ) { + this.settlementCache = settlementCache ?? new SettlementCache(); + } /** * Get mechanism-specific extra data for the supported kinds endpoint. @@ -146,7 +155,7 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator { // - 4 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse or Memo // - 5 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse + Lighthouse or Memo // - 6 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse + Lighthouse + Memo - // See: https://github.com/coinbase/x402/issues/828 + // See: https://github.com/x402-foundation/x402/issues/828 if (instructions.length < 3 || instructions.length > 6) { return { isValid: false, @@ -258,10 +267,10 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator { // Verify transfer amount meets requirements const amount = parsedTransfer.data.amount; - if (amount < BigInt(requirements.amount)) { + if (amount !== BigInt(requirements.amount)) { return { isValid: false, - invalidReason: "invalid_exact_svm_payload_amount_insufficient", + invalidReason: "invalid_exact_svm_payload_amount_mismatch", payer, }; } @@ -348,6 +357,19 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator { }; } + // Duplicate settlement check: reject if this transaction is already being settled. + // Must occur before any async work so concurrent calls for the same tx are caught. + const txKey = exactSvmPayload.transaction; + if (this.settlementCache.isDuplicate(txKey)) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "duplicate_settlement", + payer: valid.payer || "", + }; + } + try { // Extract feePayer from requirements (already validated in verify) const feePayer = requirements.extra.feePayer as Address; diff --git a/typescript/packages/mechanisms/svm/src/exact/v1/facilitator/scheme.ts b/typescript/packages/mechanisms/svm/src/exact/v1/facilitator/scheme.ts index 16d0f46..7d0dee6 100644 --- a/typescript/packages/mechanisms/svm/src/exact/v1/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/svm/src/exact/v1/facilitator/scheme.ts @@ -30,6 +30,7 @@ import { MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS, MEMO_PROGRAM_ADDRESS, } from "../../../constants"; +import { SettlementCache } from "../../../settlement-cache"; import type { FacilitatorSvmSigner } from "../../../signer"; import type { ExactSvmPayloadV1 } from "../../../types"; import { decodeTransactionFromPayload, getTokenPayerFromTransaction } from "../../../utils"; @@ -41,13 +42,21 @@ export class ExactSvmSchemeV1 implements SchemeNetworkFacilitator { readonly scheme = "exact"; readonly caipFamily = "solana:*"; + private readonly settlementCache: SettlementCache; + /** * Creates a new ExactSvmFacilitatorV1 instance. * * @param signer - The SVM RPC client for facilitator operations + * @param settlementCache - Optional shared settlement cache (one is created if omitted) * @returns ExactSvmFacilitatorV1 instance */ - constructor(private readonly signer: FacilitatorSvmSigner) {} + constructor( + private readonly signer: FacilitatorSvmSigner, + settlementCache?: SettlementCache, + ) { + this.settlementCache = settlementCache ?? new SettlementCache(); + } /** * Get mechanism-specific extra data for the supported kinds endpoint. @@ -149,7 +158,7 @@ export class ExactSvmSchemeV1 implements SchemeNetworkFacilitator { // - 4 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse or Memo // - 5 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse + Lighthouse or Memo // - 6 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse + Lighthouse + Memo - // See: https://github.com/coinbase/x402/issues/828 + // See: https://github.com/x402-foundation/x402/issues/828 if (instructions.length < 3 || instructions.length > 6) { return { isValid: false, @@ -259,12 +268,12 @@ export class ExactSvmSchemeV1 implements SchemeNetworkFacilitator { }; } - // Verify transfer amount meets requirements + // Verify transfer amount exactly matches requirements const amount = parsedTransfer.data.amount; - if (amount < BigInt(requirementsV1.maxAmountRequired)) { + if (amount !== BigInt(requirementsV1.maxAmountRequired)) { return { isValid: false, - invalidReason: "invalid_exact_svm_payload_amount_insufficient", + invalidReason: "invalid_exact_svm_payload_amount_mismatch", payer, }; } @@ -352,6 +361,19 @@ export class ExactSvmSchemeV1 implements SchemeNetworkFacilitator { }; } + // Duplicate settlement check: reject if this transaction is already being settled. + // Must occur before any async work so concurrent calls for the same tx are caught. + const txKey = exactSvmPayload.transaction; + if (this.settlementCache.isDuplicate(txKey)) { + return { + success: false, + network: payloadV1.network, + transaction: "", + errorReason: "duplicate_settlement", + payer: valid.payer || "", + }; + } + try { // Extract feePayer from requirements (already validated in verify) const feePayer = requirements.extra.feePayer as Address; diff --git a/typescript/packages/mechanisms/svm/src/index.ts b/typescript/packages/mechanisms/svm/src/index.ts index 3f1e736..ef5eca0 100644 --- a/typescript/packages/mechanisms/svm/src/index.ts +++ b/typescript/packages/mechanisms/svm/src/index.ts @@ -20,6 +20,9 @@ export type { // Export payload types export type { ExactSvmPayloadV1, ExactSvmPayloadV2 } from "./types"; +// Export settlement cache (shared across V1/V2 facilitator instances) +export { SettlementCache } from "./settlement-cache"; + // Export constants export * from "./constants"; diff --git a/typescript/packages/mechanisms/svm/src/settlement-cache.ts b/typescript/packages/mechanisms/svm/src/settlement-cache.ts new file mode 100644 index 0000000..a8941b4 --- /dev/null +++ b/typescript/packages/mechanisms/svm/src/settlement-cache.ts @@ -0,0 +1,46 @@ +import { SETTLEMENT_TTL_MS } from "./constants"; + +/** + * In-memory cache for deduplicating concurrent settlement requests. + * + * A single instance should be shared across V1 and V2 facilitator scheme + * instances so that a transaction submitted through one protocol version is + * also blocked on the other. Because Node.js is single-threaded, no lock + * is required — the cache check + insert must simply occur before the first + * `await` in the settle path. + */ +export class SettlementCache { + private readonly entries = new Map(); + + /** + * Returns `true` if `key` is already pending settlement (duplicate), + * or `false` after recording it as newly pending. + * + * Callers should reject the settlement when this returns `true`. + * + * @param key - The unique identifier for the settlement (typically the transaction signature). + * @returns `true` if the key was already present (duplicate); `false` otherwise. + */ + isDuplicate(key: string): boolean { + this.prune(); + if (this.entries.has(key)) { + return true; + } + this.entries.set(key, Date.now()); + return false; + } + + /** + * Remove entries older than the settlement TTL. + */ + private prune(): void { + const cutoff = Date.now() - SETTLEMENT_TTL_MS; + for (const [key, timestamp] of this.entries) { + if (timestamp < cutoff) { + this.entries.delete(key); + } else { + break; + } + } + } +} diff --git a/typescript/packages/mechanisms/svm/test/integrations/exact-svm.test.ts b/typescript/packages/mechanisms/svm/test/integrations/exact-svm.test.ts index 190e631..5ab872e 100644 --- a/typescript/packages/mechanisms/svm/test/integrations/exact-svm.test.ts +++ b/typescript/packages/mechanisms/svm/test/integrations/exact-svm.test.ts @@ -161,7 +161,7 @@ describe("SVM Integration Tests", () => { description: "Company Co. resource", mimeType: "application/json", }; - const paymentRequired = server.createPaymentRequiredResponse(accepts, resource); + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); const paymentPayload = await client.createPaymentPayload(paymentRequired); diff --git a/typescript/packages/mechanisms/svm/test/unit/facilitator.test.ts b/typescript/packages/mechanisms/svm/test/unit/facilitator.test.ts index 5d27bb5..cb7fe1f 100644 --- a/typescript/packages/mechanisms/svm/test/unit/facilitator.test.ts +++ b/typescript/packages/mechanisms/svm/test/unit/facilitator.test.ts @@ -1,7 +1,10 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { ExactSvmScheme } from "../../src/exact/facilitator/scheme"; +import { ExactSvmSchemeV1 } from "../../src/exact/v1/facilitator/scheme"; +import { SettlementCache } from "../../src/settlement-cache"; import type { FacilitatorSvmSigner } from "../../src/signer"; import type { PaymentRequirements, PaymentPayload } from "@x402/core/types"; +import type { PaymentPayloadV1, PaymentRequirementsV1 } from "@x402/core/types/v1"; import { USDC_DEVNET_ADDRESS, SOLANA_DEVNET_CAIP2 } from "../../src/constants"; describe("ExactSvmScheme", () => { @@ -255,4 +258,211 @@ describe("ExactSvmScheme", () => { expect(result.network).toBe(SOLANA_DEVNET_CAIP2); }); }); + + describe("duplicate settlement cache", () => { + function makePayload(transaction: string): PaymentPayload { + return { + x402Version: 2, + resource: { + url: "http://example.com/protected", + description: "Test resource", + mimeType: "application/json", + }, + accepted: { + scheme: "exact", + network: SOLANA_DEVNET_CAIP2, + asset: USDC_DEVNET_ADDRESS, + amount: "100000", + payTo: "PayToAddress11111111111111111111111111", + maxTimeoutSeconds: 3600, + extra: { feePayer: "FeePayer1111111111111111111111111111" }, + }, + payload: { transaction }, + }; + } + + const requirements: PaymentRequirements = { + scheme: "exact", + network: SOLANA_DEVNET_CAIP2, + asset: USDC_DEVNET_ADDRESS, + amount: "100000", + payTo: "PayToAddress11111111111111111111111111", + maxTimeoutSeconds: 3600, + extra: { feePayer: "FeePayer1111111111111111111111111111" }, + }; + + function setupSettleMocks(facilitator: ExactSvmScheme) { + vi.spyOn(facilitator, "verify").mockResolvedValue({ + isValid: true, + payer: "PayerAddress", + }); + (mockSigner as Record).signTransaction = vi + .fn() + .mockResolvedValue("signedTx"); + (mockSigner as Record).sendTransaction = vi + .fn() + .mockResolvedValue("txSignature123"); + (mockSigner as Record).confirmTransaction = vi + .fn() + .mockResolvedValue(undefined); + } + + it("should reject duplicate settlement of the same transaction", async () => { + const facilitator = new ExactSvmScheme(mockSigner); + setupSettleMocks(facilitator); + + const payload = makePayload("sameTransactionBase64=="); + + const result1 = await facilitator.settle(payload, requirements); + expect(result1.success).toBe(true); + + const result2 = await facilitator.settle(payload, requirements); + expect(result2.success).toBe(false); + expect(result2.errorReason).toBe("duplicate_settlement"); + }); + + it("should allow settlement of distinct transactions", async () => { + const facilitator = new ExactSvmScheme(mockSigner); + setupSettleMocks(facilitator); + + const result1 = await facilitator.settle(makePayload("transactionA=="), requirements); + expect(result1.success).toBe(true); + + const result2 = await facilitator.settle(makePayload("transactionB=="), requirements); + expect(result2.success).toBe(true); + }); + + it("should evict cache entries after TTL", async () => { + vi.useFakeTimers(); + try { + const facilitator = new ExactSvmScheme(mockSigner); + setupSettleMocks(facilitator); + + const payload = makePayload("expiringTransaction=="); + + const result1 = await facilitator.settle(payload, requirements); + expect(result1.success).toBe(true); + + // Advance past the 120s TTL + vi.advanceTimersByTime(121_000); + + const result2 = await facilitator.settle(payload, requirements); + expect(result2.success).toBe(true); + } finally { + vi.useRealTimers(); + } + }); + + it("should block cross-version duplicate when sharing a cache", async () => { + const sharedCache = new SettlementCache(); + const v2 = new ExactSvmScheme(mockSigner, sharedCache); + const v1 = new ExactSvmSchemeV1(mockSigner, sharedCache); + + // Mock V2 settle flow + vi.spyOn(v2, "verify").mockResolvedValue({ + isValid: true, + payer: "PayerAddress", + }); + (mockSigner as Record).signTransaction = vi + .fn() + .mockResolvedValue("signedTx"); + (mockSigner as Record).sendTransaction = vi + .fn() + .mockResolvedValue("txSignature123"); + (mockSigner as Record).confirmTransaction = vi + .fn() + .mockResolvedValue(undefined); + + // Settle via V2 first + const v2Result = await v2.settle(makePayload("crossVersionTx=="), requirements); + expect(v2Result.success).toBe(true); + + // Mock V1 verify + vi.spyOn(v1, "verify").mockResolvedValue({ + isValid: true, + payer: "PayerAddress", + }); + + // Same tx via V1 should be rejected + const v1Payload: PaymentPayloadV1 = { + x402Version: 1, + scheme: "exact", + network: "solana-devnet", + payload: { transaction: "crossVersionTx==" }, + }; + const v1Requirements: PaymentRequirementsV1 = { + scheme: "exact", + network: "solana-devnet", + asset: USDC_DEVNET_ADDRESS, + maxAmountRequired: "100000", + payTo: "PayToAddress11111111111111111111111111", + maxTimeoutSeconds: 3600, + extra: { feePayer: "FeePayer1111111111111111111111111111" }, + }; + + const v1Result = await v1.settle(v1Payload as never, v1Requirements as never); + expect(v1Result.success).toBe(false); + expect(v1Result.errorReason).toBe("duplicate_settlement"); + }); + }); +}); + +describe("SettlementCache prune optimization", () => { + it("should prune only expired entries and preserve non-expired ones", () => { + vi.useFakeTimers(); + try { + const cache = new SettlementCache(); + + // Insert three entries 10s apart + cache.isDuplicate("tx-a"); + vi.advanceTimersByTime(10_000); + cache.isDuplicate("tx-b"); + vi.advanceTimersByTime(10_000); + cache.isDuplicate("tx-c"); + + // Advance so tx-a and tx-b are expired (> 120s old) but tx-c is not + vi.advanceTimersByTime(101_000); // total: tx-a=121s, tx-b=111s, tx-c=101s + + // tx-a should be expired, tx-b and tx-c should still be cached + // Trigger prune via a new isDuplicate call + expect(cache.isDuplicate("tx-a")).toBe(false); // expired, re-inserted as new + expect(cache.isDuplicate("tx-b")).toBe(true); // still cached + expect(cache.isDuplicate("tx-c")).toBe(true); // still cached + } finally { + vi.useRealTimers(); + } + }); + + it("should prune all entries when all are expired", () => { + vi.useFakeTimers(); + try { + const cache = new SettlementCache(); + + cache.isDuplicate("tx-1"); + cache.isDuplicate("tx-2"); + cache.isDuplicate("tx-3"); + + vi.advanceTimersByTime(121_000); + + // All expired — none should be detected as duplicates + expect(cache.isDuplicate("tx-1")).toBe(false); + expect(cache.isDuplicate("tx-2")).toBe(false); + expect(cache.isDuplicate("tx-3")).toBe(false); + } finally { + vi.useRealTimers(); + } + }); + + it("should not prune any entries when none are expired", () => { + const cache = new SettlementCache(); + + cache.isDuplicate("tx-x"); + cache.isDuplicate("tx-y"); + cache.isDuplicate("tx-z"); + + // All still fresh — all should be detected as duplicates + expect(cache.isDuplicate("tx-x")).toBe(true); + expect(cache.isDuplicate("tx-y")).toBe(true); + expect(cache.isDuplicate("tx-z")).toBe(true); + }); }); diff --git a/typescript/packages/mechanisms/svm/test/unit/v1/facilitator.test.ts b/typescript/packages/mechanisms/svm/test/unit/v1/facilitator.test.ts index ac5a1c3..0502996 100644 --- a/typescript/packages/mechanisms/svm/test/unit/v1/facilitator.test.ts +++ b/typescript/packages/mechanisms/svm/test/unit/v1/facilitator.test.ts @@ -204,4 +204,93 @@ describe("ExactSvmSchemeV1", () => { expect(result.network).toBe("solana-devnet"); }); }); + + describe("duplicate settlement cache", () => { + function makePayload(transaction: string): PaymentPayloadV1 { + return { + x402Version: 1, + scheme: "exact", + network: "solana-devnet", + payload: { transaction }, + }; + } + + const requirements: PaymentRequirementsV1 = { + scheme: "exact", + network: "solana-devnet", + asset: USDC_DEVNET_ADDRESS, + maxAmountRequired: "100000", + payTo: "PayToAddress11111111111111111111111111", + maxTimeoutSeconds: 3600, + extra: { feePayer: "FeePayer1111111111111111111111111111" }, + }; + + function setupSettleMocks(facilitator: ExactSvmSchemeV1) { + vi.spyOn(facilitator, "verify").mockResolvedValue({ + isValid: true, + payer: "PayerAddress", + }); + (mockSigner as Record).signTransaction = vi + .fn() + .mockResolvedValue("signedTx"); + (mockSigner as Record).sendTransaction = vi + .fn() + .mockResolvedValue("txSignature123"); + (mockSigner as Record).confirmTransaction = vi + .fn() + .mockResolvedValue(undefined); + } + + it("should reject duplicate settlement of the same transaction", async () => { + const facilitator = new ExactSvmSchemeV1(mockSigner); + setupSettleMocks(facilitator); + + const payload = makePayload("sameTransactionBase64=="); + + const result1 = await facilitator.settle(payload as never, requirements as never); + expect(result1.success).toBe(true); + + const result2 = await facilitator.settle(payload as never, requirements as never); + expect(result2.success).toBe(false); + expect(result2.errorReason).toBe("duplicate_settlement"); + }); + + it("should allow settlement of distinct transactions", async () => { + const facilitator = new ExactSvmSchemeV1(mockSigner); + setupSettleMocks(facilitator); + + const result1 = await facilitator.settle( + makePayload("transactionA==") as never, + requirements as never, + ); + expect(result1.success).toBe(true); + + const result2 = await facilitator.settle( + makePayload("transactionB==") as never, + requirements as never, + ); + expect(result2.success).toBe(true); + }); + + it("should evict cache entries after TTL", async () => { + vi.useFakeTimers(); + try { + const facilitator = new ExactSvmSchemeV1(mockSigner); + setupSettleMocks(facilitator); + + const payload = makePayload("expiringTransaction=="); + + const result1 = await facilitator.settle(payload as never, requirements as never); + expect(result1.success).toBe(true); + + // Advance past the 120s TTL + vi.advanceTimersByTime(121_000); + + const result2 = await facilitator.settle(payload as never, requirements as never); + expect(result2.success).toBe(true); + } finally { + vi.useRealTimers(); + } + }); + }); }); diff --git a/typescript/pnpm-lock.yaml b/typescript/pnpm-lock.yaml index 80ca002..8e92494 100644 --- a/typescript/pnpm-lock.yaml +++ b/typescript/pnpm-lock.yaml @@ -81,6 +81,9 @@ importers: packages/extensions: dependencies: + '@noble/curves': + specifier: ^1.9.0 + version: 1.9.7 '@scure/base': specifier: ^1.2.6 version: 1.2.6 @@ -90,6 +93,9 @@ importers: ajv: specifier: ^8.17.1 version: 8.17.1 + jose: + specifier: ^5.9.6 + version: 5.10.0 siwe: specifier: ^2.3.2 version: 2.3.2(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -209,12 +215,6 @@ importers: packages/http/express: dependencies: - '@coinbase/cdp-sdk': - specifier: ^1.22.0 - version: 1.44.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@solana/kit': - specifier: ^2.1.1 - version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@x402/core': specifier: workspace:~ version: link:../../core @@ -222,7 +222,7 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall viem: specifier: ^2.39.3 @@ -283,6 +283,67 @@ importers: specifier: ^3.0.5 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.5)(yaml@2.8.1) + packages/http/fastify: + dependencies: + '@x402/core': + specifier: workspace:~ + version: link:../../core + '@x402/extensions': + specifier: workspace:~ + version: link:../../extensions + '@x402/paywall': + specifier: workspace:* + version: link:../paywall + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.34.0 + '@types/node': + specifier: ^22.13.4 + version: 22.18.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.9.2))(eslint@9.34.0(jiti@1.21.7))(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.42.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.9.2) + eslint: + specifier: ^9.24.0 + version: 9.34.0(jiti@1.21.7) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.9.2))(eslint@9.34.0(jiti@1.21.7)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.34.0(jiti@1.21.7)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.34.0(jiti@1.21.7))(prettier@3.5.2) + fastify: + specifier: ^5.0.0 + version: 5.8.4 + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) + tsx: + specifier: ^4.19.2 + version: 4.20.5 + typescript: + specifier: ^5.7.3 + version: 5.9.2 + vite: + specifier: ^6.2.6 + version: 6.3.5(@types/node@22.18.0)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.5)(yaml@2.8.1) + packages/http/fetch: dependencies: '@x402/core': @@ -350,7 +411,7 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall zod: specifier: ^3.24.2 @@ -407,9 +468,6 @@ importers: packages/http/next: dependencies: - '@coinbase/cdp-sdk': - specifier: ^1.22.0 - version: 1.44.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@x402/core': specifier: workspace:~ version: link:../../core @@ -417,7 +475,7 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall next: specifier: ^16.0.10 @@ -474,27 +532,6 @@ importers: packages/http/paywall: dependencies: - '@scure/base': - specifier: ^1.2.6 - version: 1.2.6 - '@solana-program/compute-budget': - specifier: ^0.8.0 - version: 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': - specifier: ^0.5.1 - version: 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token-2022': - specifier: ^0.4.2 - version: 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) - '@solana/kit': - specifier: ^2.1.1 - version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-confirmation': - specifier: ^2.1.1 - version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/wallet-standard-features': - specifier: ^1.3.0 - version: 1.3.0 '@tanstack/react-query': specifier: ^5.90.7 version: 5.90.20(react@19.1.1) @@ -550,9 +587,6 @@ importers: '@x402/evm': specifier: workspace:~ version: link:../../mechanisms/evm - '@x402/svm': - specifier: workspace:~ - version: link:../../mechanisms/svm buffer: specifier: ^6.0.3 version: 6.0.3 @@ -599,17 +633,14 @@ importers: specifier: ^3.0.5 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.5)(yaml@2.8.1) - packages/mcp: + packages/mechanisms/aptos: dependencies: - '@modelcontextprotocol/sdk': - specifier: ^1.12.1 - version: 1.26.0(zod@3.25.76) + '@aptos-labs/ts-sdk': + specifier: ^5.2.1 + version: 5.2.1(got@11.8.6) '@x402/core': - specifier: workspace:~ - version: link:../core - zod: - specifier: ^3.24.2 - version: 3.25.76 + specifier: workspace:* + version: link:../../core devDependencies: '@eslint/js': specifier: ^9.24.0 @@ -623,9 +654,6 @@ importers: '@typescript-eslint/parser': specifier: ^8.29.1 version: 8.42.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.9.2) - '@x402/evm': - specifier: workspace:~ - version: link:../mechanisms/evm eslint: specifier: ^9.24.0 version: 9.34.0(jiti@1.21.7) @@ -638,9 +666,6 @@ importers: eslint-plugin-prettier: specifier: ^5.2.6 version: 5.5.4(eslint@9.34.0(jiti@1.21.7))(prettier@3.5.2) - express: - specifier: ^4.21.2 - version: 4.22.1 prettier: specifier: 3.5.2 version: 3.5.2 @@ -653,9 +678,6 @@ importers: typescript: specifier: ^5.7.3 version: 5.9.2 - viem: - specifier: ^2.27.2 - version: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) vite: specifier: ^6.2.6 version: 6.3.5(@types/node@22.18.0)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) @@ -724,30 +746,18 @@ importers: specifier: ^3.0.5 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.5)(yaml@2.8.1) - packages/mechanisms/svm: + packages/mechanisms/stellar: dependencies: - '@solana-program/compute-budget': - specifier: ^0.11.0 - version: 0.11.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)) - '@solana-program/token': - specifier: ^0.9.0 - version: 0.9.0(@solana/kit@5.5.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)) - '@solana-program/token-2022': - specifier: ^0.6.1 - version: 0.6.1(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) - '@solana/kit': - specifier: ^5.1.0 - version: 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@stellar/stellar-sdk': + specifier: ^14.6.1 + version: 14.6.1 '@x402/core': - specifier: workspace:~ + specifier: workspace:* version: link:../../core devDependencies: '@eslint/js': specifier: ^9.24.0 version: 9.34.0 - '@scure/base': - specifier: ^1.2.6 - version: 1.2.6 '@types/node': specifier: ^22.13.4 version: 22.18.0 @@ -799,6 +809,20 @@ packages: '@adraffy/ens-normalize@1.11.0': resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==} + '@aptos-labs/aptos-cli@1.1.1': + resolution: {integrity: sha512-sB7CokCM6s76SLJmccysbnFR+MDik6udKfj2+9ZsmTLV0/t73veIeCDKbvWJmbW267ibx4HiGbPI7L+1+yjEbQ==} + hasBin: true + + '@aptos-labs/aptos-client@2.2.0': + resolution: {integrity: sha512-lYgHI8ehgD+Ykhix0IwzLaTCknHp1KNmExbq2bPZk8IeTwQg79D5BOkD46MjW0jGbJbl+J/RBtVF9vM7Te/hWA==} + engines: {node: '>=20.0.0'} + peerDependencies: + got: ^11.8.6 + + '@aptos-labs/ts-sdk@5.2.1': + resolution: {integrity: sha512-kazYjqfsPCBx2UJI+nYUOb6Ov7q7brSgYEfxp2sP27IeJWdDNa50lfs0WIpDJ92kQxdtlm9q3ZWw7Toh9f1gxQ==} + engines: {node: '>=20.0.0'} + '@asamuzakjp/css-color@4.1.2': resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} @@ -1142,6 +1166,24 @@ packages: resolution: {integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==} engines: {node: '>=14'} + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@gemini-wallet/core@0.2.0': resolution: {integrity: sha512-vv9aozWnKrrPWQ3vIFcWk7yta4hQW1Ie0fsNNPeXnjAxkbXr2hqMagEptLuMxpEP2W3mnRu05VDNKzcvAuuZDw==} peerDependencies: @@ -1152,12 +1194,6 @@ packages: peerDependencies: viem: '>=2.0.0' - '@hono/node-server@1.19.9': - resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4 - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1447,16 +1483,6 @@ packages: resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} engines: {node: '>=16.0.0'} - '@modelcontextprotocol/sdk@1.26.0': - resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - '@next/env@16.1.6': resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} @@ -1574,6 +1600,9 @@ packages: resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} deprecated: 'The package is now available as "qr": npm install qr' + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1753,52 +1782,23 @@ packages: '@scure/bip39@1.6.0': resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} - '@solana-program/compute-budget@0.11.0': - resolution: {integrity: sha512-7f1ePqB/eURkTwTOO9TNIdUXZcyrZoX3Uy2hNo7cXMfNhPFWp9AVgIyRNBc2jf15sdUa9gNpW+PfP2iV8AYAaw==} - peerDependencies: - '@solana/kit': ^5.0 - - '@solana-program/compute-budget@0.8.0': - resolution: {integrity: sha512-qPKxdxaEsFxebZ4K5RPuy7VQIm/tfJLa1+Nlt3KNA8EYQkz9Xm8htdoEaXVrer9kpgzzp9R3I3Bh6omwCM06tQ==} - peerDependencies: - '@solana/kit': ^2.1.0 - '@solana-program/system@0.10.0': resolution: {integrity: sha512-Go+LOEZmqmNlfr+Gjy5ZWAdY5HbYzk2RBewD9QinEU/bBSzpFfzqDRT55JjFRBGJUvMgf3C2vfXEGT4i8DSI4g==} peerDependencies: '@solana/kit': ^5.0 - '@solana-program/token-2022@0.4.2': - resolution: {integrity: sha512-zIpR5t4s9qEU3hZKupzIBxJ6nUV5/UVyIT400tu9vT1HMs5JHxaTTsb5GUhYjiiTvNwU0MQavbwc4Dl29L0Xvw==} - peerDependencies: - '@solana/kit': ^2.1.0 - '@solana/sysvars': ^2.1.0 - - '@solana-program/token-2022@0.6.1': - resolution: {integrity: sha512-Ex02cruDMGfBMvZZCrggVR45vdQQSI/unHVpt/7HPt/IwFYB4eTlXtO8otYZyqV/ce5GqZ8S6uwyRf0zy6fdbA==} - peerDependencies: - '@solana/kit': ^5.0 - '@solana/sysvars': ^5.0 - - '@solana-program/token@0.5.1': - resolution: {integrity: sha512-bJvynW5q9SFuVOZ5vqGVkmaPGA0MCC+m9jgJj1nk5m20I389/ms69ASnhWGoOPNcie7S9OwBX0gTj2fiyWpfag==} - peerDependencies: - '@solana/kit': ^2.1.0 - '@solana-program/token@0.9.0': resolution: {integrity: sha512-vnZxndd4ED4Fc56sw93cWZ2djEeeOFxtaPS8SPf5+a+JZjKA/EnKqzbE1y04FuMhIVrLERQ8uR8H2h72eZzlsA==} peerDependencies: '@solana/kit': ^5.0 - '@solana/accounts@2.3.0': - resolution: {integrity: sha512-QgQTj404Z6PXNOyzaOpSzjgMOuGwG8vC66jSDB+3zHaRcEPRVRd2sVSrd1U6sHtnV3aiaS6YyDuPQMheg4K2jw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/accounts@5.5.1': resolution: {integrity: sha512-TfOY9xixg5rizABuLVuZ9XI2x2tmWUC/OoN556xwfDlhBHBjKfszicYYOyD6nbFmwTGYarCmyGIdteXxTXIdhQ==} engines: {node: '>=20.18.0'} @@ -1808,12 +1808,6 @@ packages: typescript: optional: true - '@solana/addresses@2.3.0': - resolution: {integrity: sha512-ypTNkY2ZaRFpHLnHAgaW8a83N0/WoqdFvCqf4CQmnMdFsZSdC7qOwcbd7YzdaQn9dy+P2hybewzB+KP7LutxGA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/addresses@5.5.1': resolution: {integrity: sha512-5xoah3Q9G30HQghu/9BiHLb5pzlPKRC3zydQDmE3O9H//WfayxTFppsUDCL6FjYUHqj/wzK6CWHySglc2RkpdA==} engines: {node: '>=20.18.0'} @@ -1823,12 +1817,6 @@ packages: typescript: optional: true - '@solana/assertions@2.3.0': - resolution: {integrity: sha512-Ekoet3khNg3XFLN7MIz8W31wPQISpKUGDGTylLptI+JjCDWx3PIa88xjEMqFo02WJ8sBj2NLV64Xg1sBcsHjZQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/assertions@5.5.1': resolution: {integrity: sha512-YTCSWAlGwSlVPnWtWLm3ukz81wH4j2YaCveK+TjpvUU88hTy6fmUqxi0+hvAMAe4zKXpJyj3Az7BrLJRxbIm4Q==} engines: {node: '>=20.18.0'} @@ -1857,12 +1845,6 @@ packages: typescript: optional: true - '@solana/codecs-data-structures@2.3.0': - resolution: {integrity: sha512-qvU5LE5DqEdYMYgELRHv+HMOx73sSoV1ZZkwIrclwUmwTbTaH8QAJURBj0RhQ/zCne7VuLLOZFFGv6jGigWhSw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/codecs-data-structures@5.5.1': resolution: {integrity: sha512-97bJWGyUY9WvBz3mX1UV3YPWGDTez6btCfD0ip3UVEXJbItVuUiOkzcO5iFDUtQT5riKT6xC+Mzl+0nO76gd0w==} engines: {node: '>=20.18.0'} @@ -1887,13 +1869,6 @@ packages: typescript: optional: true - '@solana/codecs-strings@2.3.0': - resolution: {integrity: sha512-y5pSBYwzVziXu521hh+VxqUtp0hYGTl1eWGoc1W+8mdvBdC1kTqm/X7aYQw33J42hw03JjryvYOvmGgk3Qz/Ug==} - engines: {node: '>=20.18.0'} - peerDependencies: - fastestsmallesttextencoderdecoder: ^1.0.22 - typescript: '>=5.3.3' - '@solana/codecs-strings@5.5.1': resolution: {integrity: sha512-7klX4AhfHYA+uKKC/nxRGP2MntbYQCR3N6+v7bk1W/rSxYuhNmt+FN8aoThSZtWIKwN6BEyR1167ka8Co1+E7A==} engines: {node: '>=20.18.0'} @@ -1906,12 +1881,6 @@ packages: typescript: optional: true - '@solana/codecs@2.3.0': - resolution: {integrity: sha512-JVqGPkzoeyU262hJGdH64kNLH0M+Oew2CIPOa/9tR3++q2pEd4jU2Rxdfye9sd0Ce3XJrR5AIa8ZfbyQXzjh+g==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/codecs@5.5.1': resolution: {integrity: sha512-Vea29nJub/bXjfzEV7ZZQ/PWr1pYLZo3z0qW0LQL37uKKVzVFRQlwetd7INk3YtTD3xm9WUYr7bCvYUk3uKy2g==} engines: {node: '>=20.18.0'} @@ -1938,12 +1907,6 @@ packages: typescript: optional: true - '@solana/fast-stable-stringify@2.3.0': - resolution: {integrity: sha512-KfJPrMEieUg6D3hfQACoPy0ukrAV8Kio883llt/8chPEG3FVTX9z/Zuf4O01a15xZmBbmQ7toil2Dp0sxMJSxw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/fast-stable-stringify@5.5.1': resolution: {integrity: sha512-Ni7s2FN33zTzhTFgRjEbOVFO+UAmK8qi3Iu0/GRFYK4jN696OjKHnboSQH/EacQ+yGqS54bfxf409wU5dsLLCw==} engines: {node: '>=20.18.0'} @@ -1953,12 +1916,6 @@ packages: typescript: optional: true - '@solana/functional@2.3.0': - resolution: {integrity: sha512-AgsPh3W3tE+nK3eEw/W9qiSfTGwLYEvl0rWaxHht/lRcuDVwfKRzeSa5G79eioWFFqr+pTtoCr3D3OLkwKz02Q==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/functional@5.5.1': resolution: {integrity: sha512-tTHoJcEQq3gQx5qsdsDJ0LEJeFzwNpXD80xApW9o/PPoCNimI3SALkZl+zNW8VnxRrV3l3yYvfHWBKe/X3WG3w==} engines: {node: '>=20.18.0'} @@ -1977,12 +1934,6 @@ packages: typescript: optional: true - '@solana/instructions@2.3.0': - resolution: {integrity: sha512-PLMsmaIKu7hEAzyElrk2T7JJx4D+9eRwebhFZpy2PXziNSmFF929eRHKUsKqBFM3cYR1Yy3m6roBZfA+bGE/oQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/instructions@5.5.1': resolution: {integrity: sha512-h0G1CG6S+gUUSt0eo6rOtsaXRBwCq1+Js2a+Ps9Bzk9q7YHNFA75/X0NWugWLgC92waRp66hrjMTiYYnLBoWOQ==} engines: {node: '>=20.18.0'} @@ -1992,12 +1943,6 @@ packages: typescript: optional: true - '@solana/keys@2.3.0': - resolution: {integrity: sha512-ZVVdga79pNH+2pVcm6fr2sWz9HTwfopDVhYb0Lh3dh+WBmJjwkabXEIHey2rUES7NjFa/G7sV8lrUn/v8LDCCQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/keys@5.5.1': resolution: {integrity: sha512-KRD61cL7CRL+b4r/eB9dEoVxIf/2EJ1Pm1DmRYhtSUAJD2dJ5Xw8QFuehobOGm9URqQ7gaQl+Fkc1qvDlsWqKg==} engines: {node: '>=20.18.0'} @@ -2007,12 +1952,6 @@ packages: typescript: optional: true - '@solana/kit@2.3.0': - resolution: {integrity: sha512-sb6PgwoW2LjE5oTFu4lhlS/cGt/NB3YrShEyx7JgWFWysfgLdJnhwWThgwy/4HjNsmtMrQGWVls0yVBHcMvlMQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/kit@5.5.1': resolution: {integrity: sha512-irKUGiV2yRoyf+4eGQ/ZeCRxa43yjFEL1DUI5B0DkcfZw3cr0VJtVJnrG8OtVF01vT0OUfYOcUn6zJW5TROHvQ==} engines: {node: '>=20.18.0'} @@ -2022,12 +1961,6 @@ packages: typescript: optional: true - '@solana/nominal-types@2.3.0': - resolution: {integrity: sha512-uKlMnlP4PWW5UTXlhKM8lcgIaNj8dvd8xO4Y9l+FVvh9RvW2TO0GwUO6JCo7JBzCB0PSqRJdWWaQ8pu1Ti/OkA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/nominal-types@5.5.1': resolution: {integrity: sha512-I1ImR+kfrLFxN5z22UDiTWLdRZeKtU0J/pkWkO8qm/8WxveiwdIv4hooi8pb6JnlR4mSrWhq0pCIOxDYrL9GIQ==} engines: {node: '>=20.18.0'} @@ -2046,12 +1979,6 @@ packages: typescript: optional: true - '@solana/options@2.3.0': - resolution: {integrity: sha512-PPnnZBRCWWoZQ11exPxf//DRzN2C6AoFsDI/u2AsQfYih434/7Kp4XLpfOMT/XESi+gdBMFNNfbES5zg3wAIkw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/options@5.5.1': resolution: {integrity: sha512-eo971c9iLNLmk+yOFyo7yKIJzJ/zou6uKpy6mBuyb/thKtS/haiKIc3VLhyTXty3OH2PW8yOlORJnv4DexJB8A==} engines: {node: '>=20.18.0'} @@ -2070,12 +1997,6 @@ packages: typescript: optional: true - '@solana/programs@2.3.0': - resolution: {integrity: sha512-UXKujV71VCI5uPs+cFdwxybtHZAIZyQkqDiDnmK+DawtOO9mBn4Nimdb/6RjR2CXT78mzO9ZCZ3qfyX+ydcB7w==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/programs@5.5.1': resolution: {integrity: sha512-7U9kn0Jsx1NuBLn5HRTFYh78MV4XN145Yc3WP/q5BlqAVNlMoU9coG5IUTJIG847TUqC1lRto3Dnpwm6T4YRpA==} engines: {node: '>=20.18.0'} @@ -2085,12 +2006,6 @@ packages: typescript: optional: true - '@solana/promises@2.3.0': - resolution: {integrity: sha512-GjVgutZKXVuojd9rWy1PuLnfcRfqsaCm7InCiZc8bqmJpoghlyluweNc7ml9Y5yQn1P2IOyzh9+p/77vIyNybQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/promises@5.5.1': resolution: {integrity: sha512-T9lfuUYkGykJmppEcssNiCf6yiYQxJkhiLPP+pyAc2z84/7r3UVIb2tNJk4A9sucS66pzJnVHZKcZVGUUp6wzA==} engines: {node: '>=20.18.0'} @@ -2100,12 +2015,6 @@ packages: typescript: optional: true - '@solana/rpc-api@2.3.0': - resolution: {integrity: sha512-UUdiRfWoyYhJL9PPvFeJr4aJ554ob2jXcpn4vKmRVn9ire0sCbpQKYx6K8eEKHZWXKrDW8IDspgTl0gT/aJWVg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/rpc-api@5.5.1': resolution: {integrity: sha512-XWOQQPhKl06Vj0xi3RYHAc6oEQd8B82okYJ04K7N0Vvy3J4PN2cxeK7klwkjgavdcN9EVkYCChm2ADAtnztKnA==} engines: {node: '>=20.18.0'} @@ -2115,12 +2024,6 @@ packages: typescript: optional: true - '@solana/rpc-parsed-types@2.3.0': - resolution: {integrity: sha512-B5pHzyEIbBJf9KHej+zdr5ZNAdSvu7WLU2lOUPh81KHdHQs6dEb310LGxcpCc7HVE8IEdO20AbckewDiAN6OCg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/rpc-parsed-types@5.5.1': resolution: {integrity: sha512-HEi3G2nZqGEsa3vX6U0FrXLaqnUCg4SKIUrOe8CezD+cSFbRTOn3rCLrUmJrhVyXlHoQVaRO9mmeovk31jWxJg==} engines: {node: '>=20.18.0'} @@ -2130,12 +2033,6 @@ packages: typescript: optional: true - '@solana/rpc-spec-types@2.3.0': - resolution: {integrity: sha512-xQsb65lahjr8Wc9dMtP7xa0ZmDS8dOE2ncYjlvfyw/h4mpdXTUdrSMi6RtFwX33/rGuztQ7Hwaid5xLNSLvsFQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/rpc-spec-types@5.5.1': resolution: {integrity: sha512-6OFKtRpIEJQs8Jb2C4OO8KyP2h2Hy1MFhatMAoXA+0Ik8S3H+CicIuMZvGZ91mIu/tXicuOOsNNLu3HAkrakrw==} engines: {node: '>=20.18.0'} @@ -2145,12 +2042,6 @@ packages: typescript: optional: true - '@solana/rpc-spec@2.3.0': - resolution: {integrity: sha512-fA2LMX4BMixCrNB2n6T83AvjZ3oUQTu7qyPLyt8gHQaoEAXs8k6GZmu6iYcr+FboQCjUmRPgMaABbcr9j2J9Sw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/rpc-spec@5.5.1': resolution: {integrity: sha512-m3LX2bChm3E3by4mQrH4YwCAFY57QBzuUSWqlUw7ChuZ+oLLOq7b2czi4i6L4Vna67j3eCmB3e+4tqy1j5wy7Q==} engines: {node: '>=20.18.0'} @@ -2160,12 +2051,6 @@ packages: typescript: optional: true - '@solana/rpc-subscriptions-api@2.3.0': - resolution: {integrity: sha512-9mCjVbum2Hg9KGX3LKsrI5Xs0KX390lS+Z8qB80bxhar6MJPugqIPH8uRgLhCW9GN3JprAfjRNl7our8CPvsPQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/rpc-subscriptions-api@5.5.1': resolution: {integrity: sha512-5Oi7k+GdeS8xR2ly1iuSFkAv6CZqwG0Z6b1QZKbEgxadE1XGSDrhM2cn59l+bqCozUWCqh4c/A2znU/qQjROlw==} engines: {node: '>=20.18.0'} @@ -2175,13 +2060,6 @@ packages: typescript: optional: true - '@solana/rpc-subscriptions-channel-websocket@2.3.0': - resolution: {integrity: sha512-2oL6ceFwejIgeWzbNiUHI2tZZnaOxNTSerszcin7wYQwijxtpVgUHiuItM/Y70DQmH9sKhmikQp+dqeGalaJxw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - ws: ^8.18.0 - '@solana/rpc-subscriptions-channel-websocket@5.5.1': resolution: {integrity: sha512-7tGfBBrYY8TrngOyxSHoCU5shy86iA9SRMRrPSyBhEaZRAk6dnbdpmUTez7gtdVo0BCvh9nzQtUycKWSS7PnFQ==} engines: {node: '>=20.18.0'} @@ -2191,12 +2069,6 @@ packages: typescript: optional: true - '@solana/rpc-subscriptions-spec@2.3.0': - resolution: {integrity: sha512-rdmVcl4PvNKQeA2l8DorIeALCgJEMSu7U8AXJS1PICeb2lQuMeaR+6cs/iowjvIB0lMVjYN2sFf6Q3dJPu6wWg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/rpc-subscriptions-spec@5.5.1': resolution: {integrity: sha512-iq+rGq5fMKP3/mKHPNB6MC8IbVW41KGZg83Us/+LE3AWOTWV1WT20KT2iH1F1ik9roi42COv/TpoZZvhKj45XQ==} engines: {node: '>=20.18.0'} @@ -2206,12 +2078,6 @@ packages: typescript: optional: true - '@solana/rpc-subscriptions@2.3.0': - resolution: {integrity: sha512-Uyr10nZKGVzvCOqwCZgwYrzuoDyUdwtgQRefh13pXIrdo4wYjVmoLykH49Omt6abwStB0a4UL5gX9V4mFdDJZg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/rpc-subscriptions@5.5.1': resolution: {integrity: sha512-CTMy5bt/6mDh4tc6vUJms9EcuZj3xvK0/xq8IQ90rhkpYvate91RjBP+egvjgSayUg9yucU9vNuUpEjz4spM7w==} engines: {node: '>=20.18.0'} @@ -2221,12 +2087,6 @@ packages: typescript: optional: true - '@solana/rpc-transformers@2.3.0': - resolution: {integrity: sha512-UuHYK3XEpo9nMXdjyGKkPCOr7WsZsxs7zLYDO1A5ELH3P3JoehvrDegYRAGzBS2VKsfApZ86ZpJToP0K3PhmMA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/rpc-transformers@5.5.1': resolution: {integrity: sha512-OsWqLCQdcrRJKvHiMmwFhp9noNZ4FARuMkHT5us3ustDLXaxOjF0gfqZLnMkulSLcKt7TGXqMhBV+HCo7z5M8Q==} engines: {node: '>=20.18.0'} @@ -2236,12 +2096,6 @@ packages: typescript: optional: true - '@solana/rpc-transport-http@2.3.0': - resolution: {integrity: sha512-HFKydmxGw8nAF5N+S0NLnPBDCe5oMDtI2RAmW8DMqP4U3Zxt2XWhvV1SNkAldT5tF0U1vP+is6fHxyhk4xqEvg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/rpc-transport-http@5.5.1': resolution: {integrity: sha512-yv8GoVSHqEV0kUJEIhkdOVkR2SvJ6yoWC51cJn2rSV7plr6huLGe0JgujCmB7uZhhaLbcbP3zxXxu9sOjsi7Fg==} engines: {node: '>=20.18.0'} @@ -2251,12 +2105,6 @@ packages: typescript: optional: true - '@solana/rpc-types@2.3.0': - resolution: {integrity: sha512-O09YX2hED2QUyGxrMOxQ9GzH1LlEwwZWu69QbL4oYmIf6P5dzEEHcqRY6L1LsDVqc/dzAdEs/E1FaPrcIaIIPw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/rpc-types@5.5.1': resolution: {integrity: sha512-bibTFQ7PbHJJjGJPmfYC2I+/5CRFS4O2p9WwbFraX1Keeel+nRrt/NBXIy8veP5AEn2sVJIyJPpWBRpCx1oATA==} engines: {node: '>=20.18.0'} @@ -2266,12 +2114,6 @@ packages: typescript: optional: true - '@solana/rpc@2.3.0': - resolution: {integrity: sha512-ZWN76iNQAOCpYC7yKfb3UNLIMZf603JckLKOOLTHuy9MZnTN8XV6uwvDFhf42XvhglgUjGCEnbUqWtxQ9pa/pQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/rpc@5.5.1': resolution: {integrity: sha512-ku8zTUMrkCWci66PRIBC+1mXepEnZH/q1f3ck0kJZ95a06bOTl5KU7HeXWtskkyefzARJ5zvCs54AD5nxjQJ+A==} engines: {node: '>=20.18.0'} @@ -2281,12 +2123,6 @@ packages: typescript: optional: true - '@solana/signers@2.3.0': - resolution: {integrity: sha512-OSv6fGr/MFRx6J+ZChQMRqKNPGGmdjkqarKkRzkwmv7v8quWsIRnJT5EV8tBy3LI4DLO/A8vKiNSPzvm1TdaiQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/signers@5.5.1': resolution: {integrity: sha512-FY0IVaBT2kCAze55vEieR6hag4coqcuJ31Aw3hqRH7mv6sV8oqwuJmUrx+uFwOp1gwd5OEAzlv6N4hOOple4sQ==} engines: {node: '>=20.18.0'} @@ -2296,12 +2132,6 @@ packages: typescript: optional: true - '@solana/subscribable@2.3.0': - resolution: {integrity: sha512-DkgohEDbMkdTWiKAoatY02Njr56WXx9e/dKKfmne8/Ad6/2llUIrax78nCdlvZW9quXMaXPTxZvdQqo9N669Og==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/subscribable@5.5.1': resolution: {integrity: sha512-9K0PsynFq0CsmK1CDi5Y2vUIJpCqkgSS5yfDN0eKPgHqEptLEaia09Kaxc90cSZDZU5mKY/zv1NBmB6Aro9zQQ==} engines: {node: '>=20.18.0'} @@ -2311,12 +2141,6 @@ packages: typescript: optional: true - '@solana/sysvars@2.3.0': - resolution: {integrity: sha512-LvjADZrpZ+CnhlHqfI5cmsRzX9Rpyb1Ox2dMHnbsRNzeKAMhu9w4ZBIaeTdO322zsTr509G1B+k2ABD3whvUBA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/sysvars@5.5.1': resolution: {integrity: sha512-k3Quq87Mm+geGUu1GWv6knPk0ALsfY6EKSJGw9xUJDHzY/RkYSBnh0RiOrUhtFm2TDNjOailg8/m0VHmi3reFA==} engines: {node: '>=20.18.0'} @@ -2326,12 +2150,6 @@ packages: typescript: optional: true - '@solana/transaction-confirmation@2.3.0': - resolution: {integrity: sha512-UiEuiHCfAAZEKdfne/XljFNJbsKAe701UQHKXEInYzIgBjRbvaeYZlBmkkqtxwcasgBTOmEaEKT44J14N9VZDw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/transaction-confirmation@5.5.1': resolution: {integrity: sha512-j4mKlYPHEyu+OD7MBt3jRoX4ScFgkhZC6H65on4Fux6LMScgivPJlwnKoZMnsgxFgWds0pl+BYzSiALDsXlYtw==} engines: {node: '>=20.18.0'} @@ -2341,12 +2159,6 @@ packages: typescript: optional: true - '@solana/transaction-messages@2.3.0': - resolution: {integrity: sha512-bgqvWuy3MqKS5JdNLH649q+ngiyOu5rGS3DizSnWwYUd76RxZl1kN6CoqHSrrMzFMvis6sck/yPGG3wqrMlAww==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/transaction-messages@5.5.1': resolution: {integrity: sha512-aXyhMCEaAp3M/4fP0akwBBQkFPr4pfwoC5CLDq999r/FUwDax2RE/h4Ic7h2Xk+JdcUwsb+rLq85Y52hq84XvQ==} engines: {node: '>=20.18.0'} @@ -2356,12 +2168,6 @@ packages: typescript: optional: true - '@solana/transactions@2.3.0': - resolution: {integrity: sha512-LnTvdi8QnrQtuEZor5Msje61sDpPstTVwKg4y81tNxDhiyomjuvnSNLAq6QsB9gIxUqbNzPZgOG9IU4I4/Uaug==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/transactions@5.5.1': resolution: {integrity: sha512-8hHtDxtqalZ157pnx6p8k10D7J/KY/biLzfgh9R09VNLLY3Fqi7kJvJCr7M2ik3oRll56pxhraAGCC9yIT6eOA==} engines: {node: '>=20.18.0'} @@ -2371,10 +2177,6 @@ packages: typescript: optional: true - '@solana/wallet-standard-features@1.3.0': - resolution: {integrity: sha512-ZhpZtD+4VArf6RPitsVExvgkF+nGghd1rzPjd97GmBximpnt1rsUxMOEyoIEuH3XBxPyNB6Us7ha7RHWQR+abg==} - engines: {node: '>=16'} - '@solana/web3.js@1.98.4': resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} @@ -2393,12 +2195,28 @@ packages: '@stablelib/wipe@1.0.1': resolution: {integrity: sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==} + '@stellar/js-xdr@3.1.2': + resolution: {integrity: sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==} + + '@stellar/stellar-base@14.1.0': + resolution: {integrity: sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==} + engines: {node: '>=20.0.0'} + + '@stellar/stellar-sdk@14.6.1': + resolution: {integrity: sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==} + engines: {node: '>=20.0.0'} + hasBin: true + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + '@tanstack/query-core@5.90.20': resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} @@ -2410,6 +2228,9 @@ packages: '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -2431,6 +2252,9 @@ packages: '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} @@ -2440,6 +2264,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/lodash@4.17.20': resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} @@ -2469,6 +2296,9 @@ packages: '@types/react@19.1.12': resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==} + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} @@ -2781,14 +2611,13 @@ packages: zod: optional: true + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2917,6 +2746,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + axios-retry@4.5.0: resolution: {integrity: sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==} peerDependencies: @@ -2934,6 +2766,10 @@ packages: base-x@5.0.1: resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + base32.js@0.1.0: + resolution: {integrity: sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==} + engines: {node: '>=0.12.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2948,6 +2784,9 @@ packages: big.js@6.2.2: resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + bn.js@5.2.2: resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} @@ -2955,10 +2794,6 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} - engines: {node: '>=18'} - borsh@0.7.0: resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} @@ -3002,6 +2837,14 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -3065,6 +2908,9 @@ packages: cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + clsx@1.2.1: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} @@ -3084,6 +2930,10 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@14.0.0: resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} engines: {node: '>=20'} @@ -3117,10 +2967,6 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} - content-disposition@1.0.1: - resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} - engines: {node: '>=18'} - content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -3131,21 +2977,17 @@ packages: cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -3267,6 +3109,10 @@ packages: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -3274,6 +3120,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -3301,6 +3151,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + derive-valtio@0.1.0: resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==} peerDependencies: @@ -3578,32 +3432,18 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - eventsource-parser@3.0.6: - resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} - engines: {node: '>=18.0.0'} - - eventsource@3.0.7: - resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} - engines: {node: '>=18.0.0'} + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} - express-rate-limit@8.2.1: - resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - express@4.22.1: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -3615,6 +3455,9 @@ packages: resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} engines: {node: '> 0.1.90'} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3628,9 +3471,15 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + fast-redact@3.5.0: resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} engines: {node: '>=6'} @@ -3647,6 +3496,9 @@ packages: fastestsmallesttextencoderdecoder@1.0.22: resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} + fastify@5.8.4: + resolution: {integrity: sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -3659,6 +3511,9 @@ packages: picomatch: optional: true + feaxios@0.0.23: + resolution: {integrity: sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -3675,9 +3530,9 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} + find-my-way@9.5.0: + resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + engines: {node: '>=20'} find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} @@ -3726,10 +3581,6 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -3765,6 +3616,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -3782,6 +3637,7 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@14.0.0: @@ -3803,6 +3659,10 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -3847,6 +3707,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -3855,6 +3718,10 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -3914,14 +3781,14 @@ packages: resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} engines: {node: '>=12.22.0'} - ip-address@10.0.1: - resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} - engines: {node: '>= 12'} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -4003,9 +3870,6 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -4014,6 +3878,10 @@ packages: resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} engines: {node: '>=10'} + is-retry-allowed@3.0.0: + resolution: {integrity: sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==} + engines: {node: '>=12'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -4094,6 +3962,9 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} @@ -4101,6 +3972,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} @@ -4139,15 +4013,15 @@ packages: json-rpc-random-id@1.0.1: resolution: {integrity: sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==} + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-schema-typed@8.0.2: - resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} - json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -4161,6 +4035,10 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + keccak@3.0.4: resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==} engines: {node: '>=10.0.0'} @@ -4175,6 +4053,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -4224,6 +4105,10 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -4248,17 +4133,9 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -4278,23 +4155,23 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} hasBin: true + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -4351,10 +4228,6 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - next@16.1.6: resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} engines: {node: '>=20.9.0'} @@ -4402,6 +4275,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + nwsapi@2.2.21: resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==} @@ -4442,6 +4319,10 @@ packages: on-exit-leak-free@0.2.0: resolution: {integrity: sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -4498,6 +4379,10 @@ packages: typescript: optional: true + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -4567,9 +4452,6 @@ packages: path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} - path-to-regexp@8.3.0: - resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -4607,9 +4489,19 @@ packages: pino-abstract-transport@0.5.0: resolution: {integrity: sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + pino-std-serializers@4.0.0: resolution: {integrity: sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==} + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + pino@7.11.0: resolution: {integrity: sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==} hasBin: true @@ -4618,10 +4510,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pkce-challenge@5.0.1: - resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} - engines: {node: '>=16.20.0'} - pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -4665,6 +4553,9 @@ packages: wagmi: optional: true + poseidon-lite@0.2.1: + resolution: {integrity: sha512-xIr+G6HeYfOhCuswdqcFpSX47SPhm0EpisWJ6h7fHlWwaVIvH3dLnejpatrtw6Xc6HaLrpq05y7VRfvDmDGIog==} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -4725,6 +4616,12 @@ packages: process-warning@1.0.0: resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -4764,9 +4661,16 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -4775,10 +4679,6 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - react-dom@19.1.1: resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} peerDependencies: @@ -4807,6 +4707,10 @@ packages: resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==} engines: {node: '>= 12.13.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -4834,6 +4738,9 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4850,19 +4757,25 @@ packages: engines: {node: '>= 0.4'} hasBin: true + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup@4.50.0: resolution: {integrity: sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - rpc-websockets@9.1.3: resolution: {integrity: sha512-I+kNjW0udB4Fetr3vvtRuYZJS0PcSPyyvBcH5sDdoV8DFs5E4W2pTr7aiMlKfPxANTClP9RlqCPolj9dd5MsEA==} @@ -4890,6 +4803,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-regex2@5.1.0: + resolution: {integrity: sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==} + hasBin: true + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -4904,6 +4821,9 @@ packages: scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4922,21 +4842,16 @@ packages: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - serve-static@1.16.3: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -5012,6 +4927,9 @@ packages: sonic-boom@2.8.0: resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -5177,6 +5095,10 @@ packages: thread-stream@0.15.2: resolution: {integrity: sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -5214,10 +5136,17 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -5336,10 +5265,6 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -5380,9 +5305,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.15.0: - resolution: {integrity: sha512-Xyn5T99wU4kPhLZMm+ElE6M+IoSeG8Se7eG9xoZ82ZgVHJ07wb/IWcDZeXe2GOPkavcJ8ko5oSlXMDRl/QgY9Q==} - undici-types@7.21.0: resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} @@ -5459,6 +5381,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urijs@1.19.11: + resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} + use-sync-external-store@1.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: @@ -5797,11 +5722,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod-to-json-schema@3.25.1: - resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} - peerDependencies: - zod: ^3.25 || ^4 - zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} @@ -5853,6 +5773,29 @@ snapshots: '@adraffy/ens-normalize@1.11.0': {} + '@aptos-labs/aptos-cli@1.1.1': + dependencies: + commander: 12.1.0 + + '@aptos-labs/aptos-client@2.2.0(got@11.8.6)': + dependencies: + got: 11.8.6 + + '@aptos-labs/ts-sdk@5.2.1(got@11.8.6)': + dependencies: + '@aptos-labs/aptos-cli': 1.1.1 + '@aptos-labs/aptos-client': 2.2.0(got@11.8.6) + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + eventemitter3: 5.0.1 + js-base64: 3.7.8 + jwt-decode: 4.0.0 + poseidon-lite: 0.2.1 + transitivePeerDependencies: + - got + '@asamuzakjp/css-color@4.1.2': dependencies: '@csstools/css-calc': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) @@ -6068,8 +6011,8 @@ snapshots: '@coinbase/cdp-sdk@1.44.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: - '@solana-program/system': 0.10.0(@solana/kit@5.5.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)) - '@solana-program/token': 0.9.0(@solana/kit@5.5.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)) + '@solana-program/system': 0.10.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)) + '@solana-program/token': 0.9.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)) '@solana/kit': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) abitype: 1.0.6(typescript@5.9.2)(zod@3.25.76) @@ -6314,6 +6257,29 @@ snapshots: ethereum-cryptography: 2.2.1 micro-ftch: 0.3.1 + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.3.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + '@gemini-wallet/core@0.2.0(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@metamask/rpc-errors': 7.0.2 @@ -6330,10 +6296,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@hono/node-server@1.19.9(hono@4.11.9)': - dependencies: - hono: 4.11.9 - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -6685,7 +6647,7 @@ snapshots: debug: 4.4.1 lodash: 4.17.21 pony-cause: 2.1.11 - semver: 7.7.2 + semver: 7.7.4 uuid: 9.0.1 transitivePeerDependencies: - supports-color @@ -6694,8 +6656,8 @@ snapshots: dependencies: '@ethereumjs/tx': 4.2.0 '@types/debug': 4.1.12 - debug: 4.4.1 - semver: 7.7.2 + debug: 4.4.3 + semver: 7.7.4 superstruct: 1.0.4 transitivePeerDependencies: - supports-color @@ -6709,7 +6671,7 @@ snapshots: '@types/debug': 4.1.12 debug: 4.4.1 pony-cause: 2.1.11 - semver: 7.7.2 + semver: 7.7.4 uuid: 9.0.1 transitivePeerDependencies: - supports-color @@ -6723,33 +6685,11 @@ snapshots: '@types/debug': 4.1.12 debug: 4.4.1 pony-cause: 2.1.11 - semver: 7.7.2 + semver: 7.7.4 uuid: 9.0.1 transitivePeerDependencies: - supports-color - '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': - dependencies: - '@hono/node-server': 1.19.9(hono@4.11.9) - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - content-type: 1.0.5 - cors: 2.8.6 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.6 - express: 5.2.1 - express-rate-limit: 8.2.1(express@5.2.1) - hono: 4.11.9 - jose: 6.1.3 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) - transitivePeerDependencies: - - supports-color - '@next/env@16.1.6': {} '@next/swc-darwin-arm64@16.1.6': @@ -6828,6 +6768,8 @@ snapshots: '@paulmillr/qr@0.2.1': {} + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -7218,50 +7160,18 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 - '@socket.io/component-emitter@3.1.2': {} - - '@solana-program/compute-budget@0.11.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10))': - dependencies: - '@solana/kit': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) - - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - - '@solana-program/system@0.10.0(@solana/kit@5.5.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))': - dependencies: - '@solana/kit': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@sindresorhus/is@4.6.0': {} - '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': - dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/sysvars': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@socket.io/component-emitter@3.1.2': {} - '@solana-program/token-2022@0.6.1(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': + '@solana-program/system@0.10.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10))': dependencies: '@solana/kit': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@solana/sysvars': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - - '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/token@0.9.0(@solana/kit@5.5.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))': + '@solana-program/token@0.9.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10))': dependencies: '@solana/kit': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec': 2.3.0(typescript@5.9.2) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/accounts@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -7275,17 +7185,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/addresses@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/assertions': 2.3.0(typescript@5.9.2) - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/nominal-types': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/addresses@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/assertions': 5.5.1(typescript@5.9.2) @@ -7298,11 +7197,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/assertions@2.3.0(typescript@5.9.2)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - '@solana/assertions@5.5.1(typescript@5.9.2)': dependencies: '@solana/errors': 5.5.1(typescript@5.9.2) @@ -7324,13 +7218,6 @@ snapshots: optionalDependencies: typescript: 5.9.2 - '@solana/codecs-data-structures@2.3.0(typescript@5.9.2)': - dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - '@solana/codecs-data-structures@5.5.1(typescript@5.9.2)': dependencies: '@solana/codecs-core': 5.5.1(typescript@5.9.2) @@ -7352,14 +7239,6 @@ snapshots: optionalDependencies: typescript: 5.9.2 - '@solana/codecs-strings@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - fastestsmallesttextencoderdecoder: 1.0.22 - typescript: 5.9.2 - '@solana/codecs-strings@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/codecs-core': 5.5.1(typescript@5.9.2) @@ -7369,17 +7248,6 @@ snapshots: fastestsmallesttextencoderdecoder: 1.0.22 typescript: 5.9.2 - '@solana/codecs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/options': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/codecs@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/codecs-core': 5.5.1(typescript@5.9.2) @@ -7405,18 +7273,10 @@ snapshots: optionalDependencies: typescript: 5.9.2 - '@solana/fast-stable-stringify@2.3.0(typescript@5.9.2)': - dependencies: - typescript: 5.9.2 - '@solana/fast-stable-stringify@5.5.1(typescript@5.9.2)': optionalDependencies: typescript: 5.9.2 - '@solana/functional@2.3.0(typescript@5.9.2)': - dependencies: - typescript: 5.9.2 - '@solana/functional@5.5.1(typescript@5.9.2)': optionalDependencies: typescript: 5.9.2 @@ -7434,12 +7294,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/instructions@2.3.0(typescript@5.9.2)': - dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - '@solana/instructions@5.5.1(typescript@5.9.2)': dependencies: '@solana/codecs-core': 5.5.1(typescript@5.9.2) @@ -7447,17 +7301,6 @@ snapshots: optionalDependencies: typescript: 5.9.2 - '@solana/keys@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/assertions': 2.3.0(typescript@5.9.2) - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/nominal-types': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/keys@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/assertions': 5.5.1(typescript@5.9.2) @@ -7470,56 +7313,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/instructions': 2.3.0(typescript@5.9.2) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/instructions': 2.3.0(typescript@5.9.2) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - '@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: '@solana/accounts': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -7551,10 +7344,6 @@ snapshots: - fastestsmallesttextencoderdecoder - utf-8-validate - '@solana/nominal-types@2.3.0(typescript@5.9.2)': - dependencies: - typescript: 5.9.2 - '@solana/nominal-types@5.5.1(typescript@5.9.2)': optionalDependencies: typescript: 5.9.2 @@ -7574,17 +7363,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/options@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/options@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/codecs-core': 5.5.1(typescript@5.9.2) @@ -7601,14 +7379,6 @@ snapshots: optionalDependencies: typescript: 5.9.2 - '@solana/programs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/programs@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -7618,31 +7388,10 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/promises@2.3.0(typescript@5.9.2)': - dependencies: - typescript: 5.9.2 - '@solana/promises@5.5.1(typescript@5.9.2)': optionalDependencies: typescript: 5.9.2 - '@solana/rpc-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec': 2.3.0(typescript@5.9.2) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/rpc-api@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -7661,28 +7410,14 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-parsed-types@2.3.0(typescript@5.9.2)': - dependencies: - typescript: 5.9.2 - '@solana/rpc-parsed-types@5.5.1(typescript@5.9.2)': optionalDependencies: typescript: 5.9.2 - '@solana/rpc-spec-types@2.3.0(typescript@5.9.2)': - dependencies: - typescript: 5.9.2 - '@solana/rpc-spec-types@5.5.1(typescript@5.9.2)': optionalDependencies: typescript: 5.9.2 - '@solana/rpc-spec@2.3.0(typescript@5.9.2)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - '@solana/rpc-spec@5.5.1(typescript@5.9.2)': dependencies: '@solana/errors': 5.5.1(typescript@5.9.2) @@ -7690,19 +7425,6 @@ snapshots: optionalDependencies: typescript: 5.9.2 - '@solana/rpc-subscriptions-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-api@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -7717,24 +7439,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) - '@solana/subscribable': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) - - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) - '@solana/subscribable': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-channel-websocket@5.5.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: '@solana/errors': 5.5.1(typescript@5.9.2) @@ -7748,14 +7452,6 @@ snapshots: - bufferutil - utf-8-validate - '@solana/rpc-subscriptions-spec@2.3.0(typescript@5.9.2)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/promises': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/subscribable': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - '@solana/rpc-subscriptions-spec@5.5.1(typescript@5.9.2)': dependencies: '@solana/errors': 5.5.1(typescript@5.9.2) @@ -7765,42 +7461,6 @@ snapshots: optionalDependencies: typescript: 5.9.2 - '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/promises': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/subscribable': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/promises': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/subscribable': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - '@solana/rpc-subscriptions@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: '@solana/errors': 5.5.1(typescript@5.9.2) @@ -7821,17 +7481,6 @@ snapshots: - fastestsmallesttextencoderdecoder - utf-8-validate - '@solana/rpc-transformers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/nominal-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/rpc-transformers@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/errors': 5.5.1(typescript@5.9.2) @@ -7844,14 +7493,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-transport-http@2.3.0(typescript@5.9.2)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - undici-types: 7.15.0 - '@solana/rpc-transport-http@5.5.1(typescript@5.9.2)': dependencies: '@solana/errors': 5.5.1(typescript@5.9.2) @@ -7861,18 +7502,6 @@ snapshots: optionalDependencies: typescript: 5.9.2 - '@solana/rpc-types@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/nominal-types': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/rpc-types@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -7886,21 +7515,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/rpc-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-spec': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-transport-http': 2.3.0(typescript@5.9.2) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/rpc@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/errors': 5.5.1(typescript@5.9.2) @@ -7917,20 +7531,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/signers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/instructions': 2.3.0(typescript@5.9.2) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/nominal-types': 2.3.0(typescript@5.9.2) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/signers@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -7939,79 +7539,30 @@ snapshots: '@solana/instructions': 5.5.1(typescript@5.9.2) '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/nominal-types': 5.5.1(typescript@5.9.2) - '@solana/offchain-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - optionalDependencies: - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/subscribable@2.3.0(typescript@5.9.2)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - - '@solana/subscribable@5.5.1(typescript@5.9.2)': - dependencies: - '@solana/errors': 5.5.1(typescript@5.9.2) - optionalDependencies: - typescript: 5.9.2 - - '@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/accounts': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 5.5.1(typescript@5.9.2) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/offchain-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/subscribable@5.5.1(typescript@5.9.2)': dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/promises': 2.3.0(typescript@5.9.2) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.5.1(typescript@5.9.2) + optionalDependencies: typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/promises': 2.3.0(typescript@5.9.2) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/accounts': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.5.1(typescript@5.9.2) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - - ws '@solana/transaction-confirmation@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: @@ -8032,21 +7583,6 @@ snapshots: - fastestsmallesttextencoderdecoder - utf-8-validate - '@solana/transaction-messages@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/instructions': 2.3.0(typescript@5.9.2) - '@solana/nominal-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/transaction-messages@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -8063,24 +7599,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transactions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/instructions': 2.3.0(typescript@5.9.2) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/nominal-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/transactions@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -8100,11 +7618,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/wallet-standard-features@1.3.0': - dependencies: - '@wallet-standard/base': 1.1.0 - '@wallet-standard/features': 1.1.0 - '@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.28.3 @@ -8148,6 +7661,31 @@ snapshots: '@stablelib/wipe@1.0.1': {} + '@stellar/js-xdr@3.1.2': {} + + '@stellar/stellar-base@14.1.0': + dependencies: + '@noble/curves': 1.9.7 + '@stellar/js-xdr': 3.1.2 + base32.js: 0.1.0 + bignumber.js: 9.3.1 + buffer: 6.0.3 + sha.js: 2.4.12 + + '@stellar/stellar-sdk@14.6.1': + dependencies: + '@stellar/stellar-base': 14.1.0 + axios: 1.13.5 + bignumber.js: 9.3.1 + commander: 14.0.2 + eventsource: 2.0.2 + feaxios: 0.0.23 + randombytes: 2.1.0 + toml: 3.0.0 + urijs: 1.19.11 + transitivePeerDependencies: + - debug + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -8156,6 +7694,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + '@tanstack/query-core@5.90.20': {} '@tanstack/react-query@5.90.20(react@19.1.1)': @@ -8168,6 +7710,13 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 22.18.0 + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 22.18.0 + '@types/responselike': 1.0.3 + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -8197,12 +7746,18 @@ snapshots: '@types/express-serve-static-core': 5.1.1 '@types/serve-static': 2.2.0 + '@types/http-cache-semantics@4.2.0': {} + '@types/http-errors@2.0.5': {} '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} + '@types/keyv@3.1.4': + dependencies: + '@types/node': 22.18.0 + '@types/lodash@4.17.20': {} '@types/ms@2.1.0': {} @@ -8229,6 +7784,10 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/responselike@1.0.3': + dependencies: + '@types/node': 22.18.0 + '@types/send@1.2.1': dependencies: '@types/node': 22.18.0 @@ -9100,16 +8659,13 @@ snapshots: typescript: 5.9.2 zod: 3.25.76 + abstract-logging@2.0.1: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -9241,6 +8797,11 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + avvio@9.2.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.19.1 + axios-retry@4.5.0(axios@1.13.5): dependencies: axios: 1.13.5 @@ -9262,6 +8823,8 @@ snapshots: base-x@5.0.1: {} + base32.js@0.1.0: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.9.19: {} @@ -9272,6 +8835,8 @@ snapshots: big.js@6.2.2: {} + bignumber.js@9.3.1: {} + bn.js@5.2.2: {} body-parser@1.20.4: @@ -9291,20 +8856,6 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.2.2: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - on-finished: 2.4.1 - qs: 6.14.1 - raw-body: 3.0.2 - type-is: 2.0.1 - transitivePeerDependencies: - - supports-color - borsh@0.7.0: dependencies: bn.js: 5.2.2 @@ -9352,6 +8903,18 @@ snapshots: cac@6.7.14: {} + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -9412,6 +8975,10 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 6.2.0 + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + clsx@1.2.1: {} cluster-key-slot@1.1.2: @@ -9427,6 +8994,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@12.1.0: {} + commander@14.0.0: {} commander@14.0.2: {} @@ -9447,24 +9016,17 @@ snapshots: dependencies: safe-buffer: 5.2.1 - content-disposition@1.0.1: {} - content-type@1.0.5: {} cookie-es@1.2.2: {} cookie-signature@1.0.7: {} - cookie-signature@1.2.2: {} - cookie@0.7.2: {} - core-util-is@1.0.3: {} + cookie@1.1.1: {} - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 + core-util-is@1.0.3: {} crc-32@1.2.2: {} @@ -9567,10 +9129,16 @@ snapshots: decode-uri-component@0.2.2: {} + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + deep-eql@5.0.2: {} deep-is@0.1.4: {} + defer-to-connect@2.0.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -9594,6 +9162,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + derive-valtio@0.1.0(valtio@1.13.2(@types/react@19.1.12)(react@19.1.1)): dependencies: valtio: 1.13.2(@types/react@19.1.12)(react@19.1.1) @@ -10003,19 +9573,10 @@ snapshots: events@3.3.0: {} - eventsource-parser@3.0.6: {} - - eventsource@3.0.7: - dependencies: - eventsource-parser: 3.0.6 + eventsource@2.0.2: {} expect-type@1.2.2: {} - express-rate-limit@8.2.1(express@5.2.1): - dependencies: - express: 5.2.1 - ip-address: 10.0.1 - express@4.22.1: dependencies: accepts: 1.3.8 @@ -10052,39 +9613,6 @@ snapshots: transitivePeerDependencies: - supports-color - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.2.2 - content-disposition: 1.0.1 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.1 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.1 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 - statuses: 2.0.2 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - extendable-error@0.1.7: {} extension-port-stream@3.0.0: @@ -10094,6 +9622,8 @@ snapshots: eyes@0.1.8: {} + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -10108,8 +9638,21 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.3.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + fast-redact@3.5.0: {} fast-safe-stringify@2.1.1: {} @@ -10118,7 +9661,26 @@ snapshots: fast-uri@3.1.0: {} - fastestsmallesttextencoderdecoder@1.0.22: {} + fastestsmallesttextencoderdecoder@1.0.22: + optional: true + + fastify@5.8.4: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.2.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.5.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.4 + toad-cache: 3.7.0 fastq@1.19.1: dependencies: @@ -10128,6 +9690,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + feaxios@0.0.23: + dependencies: + is-retry-allowed: 3.0.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -10150,16 +9716,11 @@ snapshots: transitivePeerDependencies: - supports-color - finalhandler@2.1.1: + find-my-way@9.5.0: dependencies: - debug: 4.4.1 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.0 find-up@4.1.0: dependencies: @@ -10207,8 +9768,6 @@ snapshots: fresh@0.5.2: {} - fresh@2.0.0: {} - fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -10257,6 +9816,10 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -10304,6 +9867,20 @@ snapshots: gopd@1.2.0: {} + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -10348,6 +9925,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + http-cache-semantics@4.2.0: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -10363,6 +9942,11 @@ snapshots: transitivePeerDependencies: - supports-color + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -10417,7 +10001,7 @@ snapshots: dependencies: '@ioredis/commands': 1.4.0 cluster-key-slot: 1.1.2 - debug: 4.4.1 + debug: 4.4.3 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -10428,10 +10012,10 @@ snapshots: - supports-color optional: true - ip-address@10.0.1: {} - ipaddr.js@1.9.1: {} + ipaddr.js@2.3.0: {} + iron-webcrypto@1.2.1: {} is-arguments@1.2.0: @@ -10513,8 +10097,6 @@ snapshots: is-potential-custom-element-name@1.0.1: {} - is-promise@4.0.0: {} - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -10524,6 +10106,8 @@ snapshots: is-retry-allowed@2.2.0: {} + is-retry-allowed@3.0.0: {} + is-set@2.0.3: {} is-shared-array-buffer@1.0.4: @@ -10609,10 +10193,14 @@ snapshots: jiti@1.21.7: optional: true + jose@5.10.0: {} + jose@6.1.3: {} joycon@3.1.1: {} + js-base64@3.7.8: {} + js-tokens@9.0.1: {} js-yaml@3.14.2: @@ -10666,12 +10254,14 @@ snapshots: json-rpc-random-id@1.0.1: {} + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} - json-schema-typed@8.0.2: {} - json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-safe@5.0.1: {} @@ -10684,6 +10274,8 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jwt-decode@4.0.0: {} + keccak@3.0.4: dependencies: node-addon-api: 2.0.2 @@ -10701,6 +10293,12 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -10747,6 +10345,8 @@ snapshots: loupe@3.2.1: {} + lowercase-keys@2.0.0: {} + lru-cache@10.4.3: {} lru-cache@11.2.5: {} @@ -10767,12 +10367,8 @@ snapshots: media-typer@0.3.0: {} - media-typer@1.1.0: {} - merge-descriptors@1.0.3: {} - merge-descriptors@2.0.0: {} - merge2@1.4.1: {} methods@1.1.2: {} @@ -10786,18 +10382,16 @@ snapshots: mime-db@1.52.0: {} - mime-db@1.54.0: {} - mime-types@2.1.35: dependencies: mime-db: 1.52.0 - mime-types@3.0.2: - dependencies: - mime-db: 1.54.0 - mime@1.6.0: {} + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -10843,8 +10437,6 @@ snapshots: negotiator@0.6.3: {} - negotiator@1.0.0: {} - next@16.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: '@next/env': 16.1.6 @@ -10883,6 +10475,8 @@ snapshots: normalize-path@3.0.0: {} + normalize-url@6.1.0: {} + nwsapi@2.2.21: {} obj-multiplex@1.0.0: @@ -10934,6 +10528,8 @@ snapshots: on-exit-leak-free@0.2.0: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -11038,6 +10634,8 @@ snapshots: transitivePeerDependencies: - zod + p-cancelable@2.1.1: {} + p-filter@2.1.0: dependencies: p-map: 2.1.0 @@ -11097,8 +10695,6 @@ snapshots: path-to-regexp@0.1.12: {} - path-to-regexp@8.3.0: {} - path-type@4.0.0: {} pathe@2.0.3: {} @@ -11122,8 +10718,28 @@ snapshots: duplexify: 4.1.3 split2: 4.2.0 + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + pino-std-serializers@4.0.0: {} + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + pino@7.11.0: dependencies: atomic-sleep: 1.0.0 @@ -11140,8 +10756,6 @@ snapshots: pirates@4.0.7: {} - pkce-challenge@5.0.1: {} - pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -11172,6 +10786,8 @@ snapshots: - immer - use-sync-external-store + poseidon-lite@0.2.1: {} + possible-typed-array-names@1.1.0: {} postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.5)(yaml@2.8.1): @@ -11213,6 +10829,10 @@ snapshots: process-warning@1.0.0: {} + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -11253,8 +10873,14 @@ snapshots: quick-format-unescaped@4.0.4: {} + quick-lru@5.1.1: {} + radix3@1.1.2: {} + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + range-parser@1.2.1: {} raw-body@2.5.3: @@ -11264,13 +10890,6 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - unpipe: 1.0.0 - react-dom@19.1.1(react@19.1.1): dependencies: react: 19.1.1 @@ -11305,6 +10924,8 @@ snapshots: real-require@0.1.0: {} + real-require@0.2.0: {} + redis-errors@1.2.0: optional: true @@ -11339,6 +10960,8 @@ snapshots: require-main-filename@2.0.0: {} + resolve-alpn@1.2.1: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -11351,8 +10974,16 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + ret@0.5.0: {} + reusify@1.1.0: {} + rfdc@1.4.1: {} + rollup@4.50.0: dependencies: '@types/estree': 1.0.8 @@ -11380,16 +11011,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.50.0 fsevents: 2.3.3 - router@2.2.0: - dependencies: - debug: 4.4.1 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.3.0 - transitivePeerDependencies: - - supports-color - rpc-websockets@9.1.3: dependencies: '@swc/helpers': 0.5.17 @@ -11432,6 +11053,10 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-regex2@5.1.0: + dependencies: + ret: 0.5.0 + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -11442,12 +11067,13 @@ snapshots: scheduler@0.26.0: {} + secure-json-parse@4.1.0: {} + semver@6.3.1: {} semver@7.7.2: {} - semver@7.7.4: - optional: true + semver@7.7.4: {} send@0.19.2: dependencies: @@ -11467,22 +11093,6 @@ snapshots: transitivePeerDependencies: - supports-color - send@1.2.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - serve-static@1.16.3: dependencies: encodeurl: 2.0.0 @@ -11492,17 +11102,10 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@2.2.1: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1 - transitivePeerDependencies: - - supports-color - set-blocking@2.0.0: {} + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -11635,6 +11238,10 @@ snapshots: dependencies: atomic-sleep: 1.0.0 + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} source-map@0.8.0-beta.0: @@ -11791,6 +11398,10 @@ snapshots: dependencies: real-require: 0.1.0 + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -11822,8 +11433,12 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toidentifier@1.0.1: {} + toml@3.0.0: {} + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -11936,12 +11551,6 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 - type-is@2.0.1: - dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.2 - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -11996,8 +11605,6 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.15.0: {} - undici-types@7.21.0: {} universalify@0.1.2: {} @@ -12023,6 +11630,8 @@ snapshots: dependencies: punycode: 2.3.1 + urijs@1.19.11: {} + use-sync-external-store@1.2.0(react@19.1.1): dependencies: react: 19.1.1 @@ -12417,10 +12026,6 @@ snapshots: yocto-queue@0.1.0: {} - zod-to-json-schema@3.25.1(zod@3.25.76): - dependencies: - zod: 3.25.76 - zod@3.22.4: {} zod@3.25.76: {} diff --git a/typescript/pnpm-workspace.yaml b/typescript/pnpm-workspace.yaml index d46c0d7..0906454 100644 --- a/typescript/pnpm-workspace.yaml +++ b/typescript/pnpm-workspace.yaml @@ -4,6 +4,7 @@ packages: - packages/mcp - packages/http/* - packages/mechanisms/* + - "!packages/mechanisms/svm" - packages/legacy/* - examples/facilitator - examples/**/*