From 9a2231a0da81d75d4894f76658fa09e54fa46ebb Mon Sep 17 00:00:00 2001 From: bdj Date: Wed, 1 Apr 2026 11:37:33 -0700 Subject: [PATCH 1/3] feat: authorize() takes protocols[] array instead of single protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each account implementation picks the first protocol it supports from the array. ATXPAccount calls /authorize/auto — accounts resolves the protocol via feature flag server-side. Local accounts (Base, Solana, etc.) filter to their supported protocols. PaymentClient simplified — no more protocolFlag, accountsServer, or fetchFn. Just passes protocols[] through to account.authorize(). Protocol handlers pass single-element arrays (['x402'], ['mpp']) since they know what the server requires. Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 470 +----------------- packages/atxp-base/src/baseAccount.ts | 8 +- packages/atxp-base/src/baseAppAccount.ts | 38 +- .../atxp-client/src/mppProtocolHandler.ts | 15 +- .../atxp-client/src/paymentClient.test.ts | 153 +----- packages/atxp-client/src/paymentClient.ts | 38 +- .../atxp-client/src/protocolHandler.test.ts | 10 +- .../atxp-client/src/x402ProtocolHandler.ts | 10 +- packages/atxp-common/src/atxpAccount.test.ts | 68 +-- packages/atxp-common/src/atxpAccount.ts | 68 +-- packages/atxp-common/src/types.ts | 7 +- .../atxp-polygon/src/polygonBrowserAccount.ts | 38 +- .../atxp-polygon/src/polygonServerAccount.ts | 38 +- packages/atxp-solana/src/solanaAccount.ts | 36 +- packages/atxp-tempo/src/tempoAccount.ts | 36 +- .../atxp-worldchain/src/worldchainAccount.ts | 38 +- 16 files changed, 222 insertions(+), 849 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5bc0bd0..8f18d9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25552,8 +25552,8 @@ "license": "MIT", "dependencies": { "@account-kit/infra": "^4.81.3", - "@atxp/client": "0.10.11", - "@atxp/common": "0.10.11", + "@atxp/client": "0.10.12", + "@atxp/common": "0.10.12", "bignumber.js": "^9.3.0", "viem": "^2.34.0" }, @@ -25572,37 +25572,6 @@ "vitest": "^4.0.16" } }, - "packages/atxp-base/node_modules/@atxp/client": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/client/-/client-0.10.11.tgz", - "integrity": "sha512-dH2BHqr3uQKfHBlLJM75EReY3LGZBBwOYwGf7p87pkRpYgxcxlKlGOcDLWINLZptO5I7JfcgSCT5NAKNWtm3Ww==", - "license": "MIT", - "dependencies": { - "@atxp/common": "0.10.11", - "@atxp/mpp": "0.10.7", - "@modelcontextprotocol/sdk": "^1.15.0", - "bignumber.js": "^9.3.0", - "oauth4webapi": "^3.8.3", - "x402": "^1.1.0" - }, - "peerDependencies": { - "expo-crypto": ">=14.0.0", - "react-native-url-polyfill": "^3.0.0" - } - }, - "packages/atxp-base/node_modules/@atxp/common": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/common/-/common-0.10.11.tgz", - "integrity": "sha512-n2CzUq0KzxfvWn/EnsD3bAidtqqOaGy7cfYGTs4PDxq9YJeBKydW5SYk5LIYUy8W6cHW/hfrzqAaCnCs8AON5g==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.3.0", - "jose": "^6.0.11", - "oauth4webapi": "^3.8.3", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, "packages/atxp-base/node_modules/@atxp/mpp": { "version": "0.10.7", "resolved": "https://registry.npmjs.org/@atxp/mpp/-/mpp-0.10.7.tgz", @@ -25635,8 +25604,8 @@ "version": "0.10.12", "license": "MIT", "dependencies": { - "@atxp/common": "0.10.11", - "@atxp/mpp": "0.10.11", + "@atxp/common": "0.10.12", + "@atxp/mpp": "0.10.12", "@modelcontextprotocol/sdk": "^1.15.0", "bignumber.js": "^9.3.0", "oauth4webapi": "^3.8.3", @@ -25661,25 +25630,6 @@ "react-native-url-polyfill": "^3.0.0" } }, - "packages/atxp-client/node_modules/@atxp/common": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/common/-/common-0.10.11.tgz", - "integrity": "sha512-n2CzUq0KzxfvWn/EnsD3bAidtqqOaGy7cfYGTs4PDxq9YJeBKydW5SYk5LIYUy8W6cHW/hfrzqAaCnCs8AON5g==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.3.0", - "jose": "^6.0.11", - "oauth4webapi": "^3.8.3", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, - "packages/atxp-client/node_modules/@atxp/mpp": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/mpp/-/mpp-0.10.11.tgz", - "integrity": "sha512-IqF0MhGsbh6t4X/d24d5kxGFUgp1GZQ9Qzbuyf9pnkR7aeqcVc8B+8FS2q3YWAAPtnSu1OPeKSPhQKJqTCOWfw==", - "license": "MIT" - }, "packages/atxp-client/node_modules/x402": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/x402/-/x402-1.1.0.tgz", @@ -25706,8 +25656,8 @@ "version": "0.10.12", "license": "MIT", "dependencies": { - "@atxp/common": "0.10.11", - "@atxp/server": "0.10.11" + "@atxp/common": "0.10.12", + "@atxp/server": "0.10.12" }, "devDependencies": { "@cloudflare/workers-types": "^4.20241022.0", @@ -25724,38 +25674,11 @@ "agents": "^0.3.3" } }, - "packages/atxp-cloudflare/node_modules/@atxp/common": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/common/-/common-0.10.11.tgz", - "integrity": "sha512-n2CzUq0KzxfvWn/EnsD3bAidtqqOaGy7cfYGTs4PDxq9YJeBKydW5SYk5LIYUy8W6cHW/hfrzqAaCnCs8AON5g==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.3.0", - "jose": "^6.0.11", - "oauth4webapi": "^3.8.3", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, - "packages/atxp-cloudflare/node_modules/@atxp/server": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/server/-/server-0.10.11.tgz", - "integrity": "sha512-6JD+it/0A+tMInpfsh9fvTQi91LZtoheJ/magypwMkc5647Wk4Qg+AAXkQMlwmWrb0rhh19u4wMEWQBjFey0Lw==", - "license": "MIT", - "dependencies": { - "@atxp/common": "0.10.10", - "@modelcontextprotocol/sdk": "^1.15.0", - "bignumber.js": "^9.3.0", - "oauth4webapi": "^3.8.3" - }, - "peerDependencies": { - "content-type": "^1.0.5" - } - }, "packages/atxp-cloudflare/node_modules/@atxp/server/node_modules/@atxp/common": { "version": "0.10.10", "resolved": "https://registry.npmjs.org/@atxp/common/-/common-0.10.10.tgz", "integrity": "sha512-w9TqXeoackIK3TqPaMF/Gd41/d3puZVAV2e7nXKaqBMpqQSyTjEsYRRvQW0UHLTnwHz31rw3kRCfp3G+CKL+Dg==", + "extraneous": true, "license": "MIT", "dependencies": { "bignumber.js": "^9.3.0", @@ -25791,7 +25714,7 @@ "version": "0.10.12", "license": "MIT", "dependencies": { - "@atxp/server": "0.10.11" + "@atxp/server": "0.10.12" }, "devDependencies": { "@types/express": "^5.0.0", @@ -25809,34 +25732,6 @@ "express": "^5.0.0" } }, - "packages/atxp-express/node_modules/@atxp/common": { - "version": "0.10.10", - "resolved": "https://registry.npmjs.org/@atxp/common/-/common-0.10.10.tgz", - "integrity": "sha512-w9TqXeoackIK3TqPaMF/Gd41/d3puZVAV2e7nXKaqBMpqQSyTjEsYRRvQW0UHLTnwHz31rw3kRCfp3G+CKL+Dg==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.3.0", - "jose": "^6.0.11", - "oauth4webapi": "^3.8.3", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, - "packages/atxp-express/node_modules/@atxp/server": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/server/-/server-0.10.11.tgz", - "integrity": "sha512-6JD+it/0A+tMInpfsh9fvTQi91LZtoheJ/magypwMkc5647Wk4Qg+AAXkQMlwmWrb0rhh19u4wMEWQBjFey0Lw==", - "license": "MIT", - "dependencies": { - "@atxp/common": "0.10.10", - "@modelcontextprotocol/sdk": "^1.15.0", - "bignumber.js": "^9.3.0", - "oauth4webapi": "^3.8.3" - }, - "peerDependencies": { - "content-type": "^1.0.5" - } - }, "packages/atxp-mpp": { "name": "@atxp/mpp", "version": "0.10.12", @@ -25855,8 +25750,8 @@ "version": "0.10.12", "license": "MIT", "dependencies": { - "@atxp/client": "0.10.11", - "@atxp/common": "0.10.11", + "@atxp/client": "0.10.12", + "@atxp/common": "0.10.12", "bignumber.js": "^9.3.0", "viem": "^2.34.0" }, @@ -25875,70 +25770,12 @@ "vitest": "^4.0.16" } }, - "packages/atxp-polygon/node_modules/@atxp/client": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/client/-/client-0.10.11.tgz", - "integrity": "sha512-dH2BHqr3uQKfHBlLJM75EReY3LGZBBwOYwGf7p87pkRpYgxcxlKlGOcDLWINLZptO5I7JfcgSCT5NAKNWtm3Ww==", - "license": "MIT", - "dependencies": { - "@atxp/common": "0.10.11", - "@atxp/mpp": "0.10.7", - "@modelcontextprotocol/sdk": "^1.15.0", - "bignumber.js": "^9.3.0", - "oauth4webapi": "^3.8.3", - "x402": "^1.1.0" - }, - "peerDependencies": { - "expo-crypto": ">=14.0.0", - "react-native-url-polyfill": "^3.0.0" - } - }, - "packages/atxp-polygon/node_modules/@atxp/common": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/common/-/common-0.10.11.tgz", - "integrity": "sha512-n2CzUq0KzxfvWn/EnsD3bAidtqqOaGy7cfYGTs4PDxq9YJeBKydW5SYk5LIYUy8W6cHW/hfrzqAaCnCs8AON5g==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.3.0", - "jose": "^6.0.11", - "oauth4webapi": "^3.8.3", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, - "packages/atxp-polygon/node_modules/@atxp/mpp": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/@atxp/mpp/-/mpp-0.10.7.tgz", - "integrity": "sha512-PXpCBPPB5EOeNgW8DMQI/8PtGypgKu0cYr7BdOQFGrRxwsCtWRP33PTfSwRFeyN/pmy2M92Ygtmjlbz88Nzgew==", - "license": "MIT" - }, - "packages/atxp-polygon/node_modules/x402": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/x402/-/x402-1.1.0.tgz", - "integrity": "sha512-v8nIpw/RCfUc5FFCp274Tya5+ro3bt3pJRS6wOCPoifhdzqMVP8pLmqplg6Ll/grwG8QJUNUTcd+GhmVb6aKUA==", - "license": "Apache-2.0", - "dependencies": { - "@scure/base": "^1.2.6", - "@solana-program/compute-budget": "^0.11.0", - "@solana-program/token": "^0.9.0", - "@solana-program/token-2022": "^0.6.1", - "@solana/kit": "^5.0.0", - "@solana/transaction-confirmation": "^5.0.0", - "@solana/wallet-standard-features": "^1.3.0", - "@wallet-standard/app": "^1.1.0", - "@wallet-standard/base": "^1.1.0", - "@wallet-standard/features": "^1.1.0", - "viem": "^2.21.26", - "wagmi": "^2.15.6", - "zod": "^3.24.2" - } - }, "packages/atxp-redis": { "name": "@atxp/redis", "version": "0.10.12", "license": "MIT", "dependencies": { - "@atxp/common": "0.10.11", + "@atxp/common": "0.10.12", "ioredis": "^5.7.0" }, "devDependencies": { @@ -25950,25 +25787,12 @@ "vitest": "^4.0.16" } }, - "packages/atxp-redis/node_modules/@atxp/common": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/common/-/common-0.10.11.tgz", - "integrity": "sha512-n2CzUq0KzxfvWn/EnsD3bAidtqqOaGy7cfYGTs4PDxq9YJeBKydW5SYk5LIYUy8W6cHW/hfrzqAaCnCs8AON5g==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.3.0", - "jose": "^6.0.11", - "oauth4webapi": "^3.8.3", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, "packages/atxp-server": { "name": "@atxp/server", "version": "0.10.12", "license": "MIT", "dependencies": { - "@atxp/common": "0.10.11", + "@atxp/common": "0.10.12", "@modelcontextprotocol/sdk": "^1.15.0", "bignumber.js": "^9.3.0", "oauth4webapi": "^3.8.3" @@ -25986,26 +25810,13 @@ "content-type": "^1.0.5" } }, - "packages/atxp-server/node_modules/@atxp/common": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/common/-/common-0.10.11.tgz", - "integrity": "sha512-n2CzUq0KzxfvWn/EnsD3bAidtqqOaGy7cfYGTs4PDxq9YJeBKydW5SYk5LIYUy8W6cHW/hfrzqAaCnCs8AON5g==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.3.0", - "jose": "^6.0.11", - "oauth4webapi": "^3.8.3", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, "packages/atxp-solana": { "name": "@atxp/solana", "version": "0.10.12", "license": "MIT", "dependencies": { - "@atxp/client": "0.10.11", - "@atxp/common": "0.10.11", + "@atxp/client": "0.10.12", + "@atxp/common": "0.10.12", "@solana/pay": "^0.2.5", "@solana/web3.js": "^1.98.1", "bignumber.js": "^9.3.0", @@ -26020,70 +25831,12 @@ "vitest": "^4.0.16" } }, - "packages/atxp-solana/node_modules/@atxp/client": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/client/-/client-0.10.11.tgz", - "integrity": "sha512-dH2BHqr3uQKfHBlLJM75EReY3LGZBBwOYwGf7p87pkRpYgxcxlKlGOcDLWINLZptO5I7JfcgSCT5NAKNWtm3Ww==", - "license": "MIT", - "dependencies": { - "@atxp/common": "0.10.11", - "@atxp/mpp": "0.10.7", - "@modelcontextprotocol/sdk": "^1.15.0", - "bignumber.js": "^9.3.0", - "oauth4webapi": "^3.8.3", - "x402": "^1.1.0" - }, - "peerDependencies": { - "expo-crypto": ">=14.0.0", - "react-native-url-polyfill": "^3.0.0" - } - }, - "packages/atxp-solana/node_modules/@atxp/common": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/common/-/common-0.10.11.tgz", - "integrity": "sha512-n2CzUq0KzxfvWn/EnsD3bAidtqqOaGy7cfYGTs4PDxq9YJeBKydW5SYk5LIYUy8W6cHW/hfrzqAaCnCs8AON5g==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.3.0", - "jose": "^6.0.11", - "oauth4webapi": "^3.8.3", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, - "packages/atxp-solana/node_modules/@atxp/mpp": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/@atxp/mpp/-/mpp-0.10.7.tgz", - "integrity": "sha512-PXpCBPPB5EOeNgW8DMQI/8PtGypgKu0cYr7BdOQFGrRxwsCtWRP33PTfSwRFeyN/pmy2M92Ygtmjlbz88Nzgew==", - "license": "MIT" - }, - "packages/atxp-solana/node_modules/x402": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/x402/-/x402-1.1.0.tgz", - "integrity": "sha512-v8nIpw/RCfUc5FFCp274Tya5+ro3bt3pJRS6wOCPoifhdzqMVP8pLmqplg6Ll/grwG8QJUNUTcd+GhmVb6aKUA==", - "license": "Apache-2.0", - "dependencies": { - "@scure/base": "^1.2.6", - "@solana-program/compute-budget": "^0.11.0", - "@solana-program/token": "^0.9.0", - "@solana-program/token-2022": "^0.6.1", - "@solana/kit": "^5.0.0", - "@solana/transaction-confirmation": "^5.0.0", - "@solana/wallet-standard-features": "^1.3.0", - "@wallet-standard/app": "^1.1.0", - "@wallet-standard/base": "^1.1.0", - "@wallet-standard/features": "^1.1.0", - "viem": "^2.21.26", - "wagmi": "^2.15.6", - "zod": "^3.24.2" - } - }, "packages/atxp-sqlite": { "name": "@atxp/sqlite", "version": "0.10.12", "license": "MIT", "dependencies": { - "@atxp/common": "0.10.11", + "@atxp/common": "0.10.12", "better-sqlite3": "^12.2.0" }, "devDependencies": { @@ -26096,26 +25849,13 @@ "vitest": "^4.0.16" } }, - "packages/atxp-sqlite/node_modules/@atxp/common": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/common/-/common-0.10.11.tgz", - "integrity": "sha512-n2CzUq0KzxfvWn/EnsD3bAidtqqOaGy7cfYGTs4PDxq9YJeBKydW5SYk5LIYUy8W6cHW/hfrzqAaCnCs8AON5g==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.3.0", - "jose": "^6.0.11", - "oauth4webapi": "^3.8.3", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, "packages/atxp-tempo": { "name": "@atxp/tempo", "version": "0.10.12", "license": "MIT", "dependencies": { - "@atxp/client": "0.10.11", - "@atxp/common": "0.10.11", + "@atxp/client": "0.10.12", + "@atxp/common": "0.10.12", "bignumber.js": "^9.3.0", "viem": "^2.34.0" }, @@ -26128,71 +25868,13 @@ "vitest": "^4.0.16" } }, - "packages/atxp-tempo/node_modules/@atxp/client": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/client/-/client-0.10.11.tgz", - "integrity": "sha512-dH2BHqr3uQKfHBlLJM75EReY3LGZBBwOYwGf7p87pkRpYgxcxlKlGOcDLWINLZptO5I7JfcgSCT5NAKNWtm3Ww==", - "license": "MIT", - "dependencies": { - "@atxp/common": "0.10.11", - "@atxp/mpp": "0.10.7", - "@modelcontextprotocol/sdk": "^1.15.0", - "bignumber.js": "^9.3.0", - "oauth4webapi": "^3.8.3", - "x402": "^1.1.0" - }, - "peerDependencies": { - "expo-crypto": ">=14.0.0", - "react-native-url-polyfill": "^3.0.0" - } - }, - "packages/atxp-tempo/node_modules/@atxp/common": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/common/-/common-0.10.11.tgz", - "integrity": "sha512-n2CzUq0KzxfvWn/EnsD3bAidtqqOaGy7cfYGTs4PDxq9YJeBKydW5SYk5LIYUy8W6cHW/hfrzqAaCnCs8AON5g==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.3.0", - "jose": "^6.0.11", - "oauth4webapi": "^3.8.3", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, - "packages/atxp-tempo/node_modules/@atxp/mpp": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/@atxp/mpp/-/mpp-0.10.7.tgz", - "integrity": "sha512-PXpCBPPB5EOeNgW8DMQI/8PtGypgKu0cYr7BdOQFGrRxwsCtWRP33PTfSwRFeyN/pmy2M92Ygtmjlbz88Nzgew==", - "license": "MIT" - }, - "packages/atxp-tempo/node_modules/x402": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/x402/-/x402-1.1.0.tgz", - "integrity": "sha512-v8nIpw/RCfUc5FFCp274Tya5+ro3bt3pJRS6wOCPoifhdzqMVP8pLmqplg6Ll/grwG8QJUNUTcd+GhmVb6aKUA==", - "license": "Apache-2.0", - "dependencies": { - "@scure/base": "^1.2.6", - "@solana-program/compute-budget": "^0.11.0", - "@solana-program/token": "^0.9.0", - "@solana-program/token-2022": "^0.6.1", - "@solana/kit": "^5.0.0", - "@solana/transaction-confirmation": "^5.0.0", - "@solana/wallet-standard-features": "^1.3.0", - "@wallet-standard/app": "^1.1.0", - "@wallet-standard/base": "^1.1.0", - "@wallet-standard/features": "^1.1.0", - "viem": "^2.21.26", - "wagmi": "^2.15.6", - "zod": "^3.24.2" - } - }, "packages/atxp-worldchain": { "name": "@atxp/worldchain", "version": "0.10.12", "license": "MIT", "dependencies": { - "@atxp/client": "0.10.11", - "@atxp/common": "0.10.11", + "@atxp/client": "0.10.12", + "@atxp/common": "0.10.12", "@worldcoin/minikit-js": "^1.9.6", "bignumber.js": "^9.3.0", "viem": "^2.34.0" @@ -26212,72 +25894,14 @@ "vitest": "^4.0.16" } }, - "packages/atxp-worldchain/node_modules/@atxp/client": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/client/-/client-0.10.11.tgz", - "integrity": "sha512-dH2BHqr3uQKfHBlLJM75EReY3LGZBBwOYwGf7p87pkRpYgxcxlKlGOcDLWINLZptO5I7JfcgSCT5NAKNWtm3Ww==", - "license": "MIT", - "dependencies": { - "@atxp/common": "0.10.11", - "@atxp/mpp": "0.10.7", - "@modelcontextprotocol/sdk": "^1.15.0", - "bignumber.js": "^9.3.0", - "oauth4webapi": "^3.8.3", - "x402": "^1.1.0" - }, - "peerDependencies": { - "expo-crypto": ">=14.0.0", - "react-native-url-polyfill": "^3.0.0" - } - }, - "packages/atxp-worldchain/node_modules/@atxp/common": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/common/-/common-0.10.11.tgz", - "integrity": "sha512-n2CzUq0KzxfvWn/EnsD3bAidtqqOaGy7cfYGTs4PDxq9YJeBKydW5SYk5LIYUy8W6cHW/hfrzqAaCnCs8AON5g==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.3.0", - "jose": "^6.0.11", - "oauth4webapi": "^3.8.3", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, - "packages/atxp-worldchain/node_modules/@atxp/mpp": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/@atxp/mpp/-/mpp-0.10.7.tgz", - "integrity": "sha512-PXpCBPPB5EOeNgW8DMQI/8PtGypgKu0cYr7BdOQFGrRxwsCtWRP33PTfSwRFeyN/pmy2M92Ygtmjlbz88Nzgew==", - "license": "MIT" - }, - "packages/atxp-worldchain/node_modules/x402": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/x402/-/x402-1.1.0.tgz", - "integrity": "sha512-v8nIpw/RCfUc5FFCp274Tya5+ro3bt3pJRS6wOCPoifhdzqMVP8pLmqplg6Ll/grwG8QJUNUTcd+GhmVb6aKUA==", - "license": "Apache-2.0", - "dependencies": { - "@scure/base": "^1.2.6", - "@solana-program/compute-budget": "^0.11.0", - "@solana-program/token": "^0.9.0", - "@solana-program/token-2022": "^0.6.1", - "@solana/kit": "^5.0.0", - "@solana/transaction-confirmation": "^5.0.0", - "@solana/wallet-standard-features": "^1.3.0", - "@wallet-standard/app": "^1.1.0", - "@wallet-standard/base": "^1.1.0", - "@wallet-standard/features": "^1.1.0", - "viem": "^2.21.26", - "wagmi": "^2.15.6", - "zod": "^3.24.2" - } - }, "packages/atxp-x402": { "name": "@atxp/x402", "version": "0.10.12", "license": "MIT", "dependencies": { - "@atxp/base": "0.10.11", - "@atxp/client": "0.10.11", - "@atxp/common": "0.10.11", + "@atxp/base": "0.10.12", + "@atxp/client": "0.10.12", + "@atxp/common": "0.10.12", "bignumber.js": "^9.1.2", "viem": "^2.21.54", "x402": "^1.1.0" @@ -26291,56 +25915,6 @@ "vitest": "^4.0.16" } }, - "packages/atxp-x402/node_modules/@atxp/base": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/base/-/base-0.10.11.tgz", - "integrity": "sha512-1jTjDQDF/UR+lO7bjFlgiWomTKLHawc2DcklfNE/2BbFCdFoa4pnTVjdM6SqAaF8Mh7aADm/Mgdwt9Q1w/gsrQ==", - "license": "MIT", - "dependencies": { - "@account-kit/infra": "^4.81.3", - "@atxp/client": "0.10.11", - "@atxp/common": "0.10.11", - "bignumber.js": "^9.3.0", - "viem": "^2.34.0" - } - }, - "packages/atxp-x402/node_modules/@atxp/client": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/client/-/client-0.10.11.tgz", - "integrity": "sha512-dH2BHqr3uQKfHBlLJM75EReY3LGZBBwOYwGf7p87pkRpYgxcxlKlGOcDLWINLZptO5I7JfcgSCT5NAKNWtm3Ww==", - "license": "MIT", - "dependencies": { - "@atxp/common": "0.10.11", - "@atxp/mpp": "0.10.7", - "@modelcontextprotocol/sdk": "^1.15.0", - "bignumber.js": "^9.3.0", - "oauth4webapi": "^3.8.3", - "x402": "^1.1.0" - }, - "peerDependencies": { - "expo-crypto": ">=14.0.0", - "react-native-url-polyfill": "^3.0.0" - } - }, - "packages/atxp-x402/node_modules/@atxp/common": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@atxp/common/-/common-0.10.11.tgz", - "integrity": "sha512-n2CzUq0KzxfvWn/EnsD3bAidtqqOaGy7cfYGTs4PDxq9YJeBKydW5SYk5LIYUy8W6cHW/hfrzqAaCnCs8AON5g==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.3.0", - "jose": "^6.0.11", - "oauth4webapi": "^3.8.3", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, - "packages/atxp-x402/node_modules/@atxp/mpp": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/@atxp/mpp/-/mpp-0.10.7.tgz", - "integrity": "sha512-PXpCBPPB5EOeNgW8DMQI/8PtGypgKu0cYr7BdOQFGrRxwsCtWRP33PTfSwRFeyN/pmy2M92Ygtmjlbz88Nzgew==", - "license": "MIT" - }, "packages/atxp-x402/node_modules/x402": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/x402/-/x402-1.1.0.tgz", diff --git a/packages/atxp-base/src/baseAccount.ts b/packages/atxp-base/src/baseAccount.ts index a62011c..4c018d7 100644 --- a/packages/atxp-base/src/baseAccount.ts +++ b/packages/atxp-base/src/baseAccount.ts @@ -72,7 +72,11 @@ export class BaseAccount implements Account { * Authorize a payment through the appropriate channel for Base accounts. */ async authorize(params: AuthorizeParams): Promise { - const { protocol } = params; + const supported: string[] = ['x402', 'atxp']; + const protocol = params.protocols.find(p => supported.includes(p)); + if (!protocol) { + throw new Error(`BaseAccount does not support any of: ${params.protocols.join(', ')}`); + } switch (protocol) { case 'x402': { @@ -103,8 +107,6 @@ export class BaseAccount implements Account { } return { protocol, credential: JSON.stringify({ transactionId: result.transactionId, chain: result.chain, currency: result.currency }) }; } - case 'mpp': - throw new Error('BaseAccount does not support MPP protocol'); default: throw new Error(`BaseAccount: unsupported protocol '${protocol}'`); } diff --git a/packages/atxp-base/src/baseAppAccount.ts b/packages/atxp-base/src/baseAppAccount.ts index 17e5b5c..e98ad12 100644 --- a/packages/atxp-base/src/baseAppAccount.ts +++ b/packages/atxp-base/src/baseAppAccount.ts @@ -229,29 +229,23 @@ export class BaseAppAccount implements Account { * Authorize a payment through the appropriate channel for Base browser accounts. */ async authorize(params: AuthorizeParams): Promise { - const { protocol } = params; + const supported: string[] = ['atxp']; + const protocol = params.protocols.find(p => supported.includes(p)); + if (!protocol) { + throw new Error(`BaseAppAccount does not support any of: ${params.protocols.join(', ')}`); + } - switch (protocol) { - case 'atxp': { - const chain = this.chainId === 84532 ? ChainEnum.BaseSepolia : ChainEnum.Base; - const destination: Destination = { - chain, - currency: 'USDC', - address: params.destination, - amount: new BigNumber(params.amount), - }; - const result = await this.paymentMakers[0].makePayment([destination], params.memo || ''); - if (!result) { - throw new Error('BaseAppAccount: payment execution returned no result'); - } - return { protocol, credential: JSON.stringify(result) }; - } - case 'x402': - throw new Error('BaseAppAccount does not support x402 protocol'); - case 'mpp': - throw new Error('BaseAppAccount does not support MPP protocol'); - default: - throw new Error(`BaseAppAccount: unsupported protocol '${protocol}'`); + const chain = this.chainId === 84532 ? ChainEnum.BaseSepolia : ChainEnum.Base; + const destination: Destination = { + chain, + currency: 'USDC', + address: params.destination, + amount: new BigNumber(params.amount), + }; + const result = await this.paymentMakers[0].makePayment([destination], params.memo || ''); + if (!result) { + throw new Error('BaseAppAccount: payment execution returned no result'); } + return { protocol, credential: JSON.stringify(result) }; } } \ No newline at end of file diff --git a/packages/atxp-client/src/mppProtocolHandler.ts b/packages/atxp-client/src/mppProtocolHandler.ts index 99232d4..5223b33 100644 --- a/packages/atxp-client/src/mppProtocolHandler.ts +++ b/packages/atxp-client/src/mppProtocolHandler.ts @@ -17,6 +17,7 @@ import { PaymentClient, buildPaymentHeaders } from './paymentClient.js'; * Configuration for MPP protocol handler. */ export interface MPPProtocolHandlerConfig { + /** @deprecated No longer used */ accountsServer?: string; } @@ -27,15 +28,13 @@ export interface MPPProtocolHandlerConfig { * 1. HTTP level: HTTP 402 with WWW-Authenticate: Payment header * 2. MCP level: JSON-RPC error with code -32042 containing MPP data * - * Handles the challenge by calling /authorize/mpp on the accounts service + * Handles the challenge by calling /authorize/auto on the accounts service * and retrying with an Authorization: Payment header. */ export class MPPProtocolHandler implements ProtocolHandler { readonly protocol = 'mpp'; - private accountsServer: string; - constructor(config?: MPPProtocolHandlerConfig) { - this.accountsServer = config?.accountsServer ?? 'https://accounts.atxp.ai'; + constructor(_config?: MPPProtocolHandlerConfig) { } async canHandle(response: Response): Promise { @@ -173,21 +172,17 @@ export class MPPProtocolHandler implements ProtocolHandler { const { account, logger, fetchFn, onPayment } = config; try { - logger.debug('MPP: calling /authorize/mpp on accounts service'); + logger.debug('MPP: calling /authorize/auto on accounts service'); const client = new PaymentClient({ - accountsServer: this.accountsServer, logger, - fetchFn, }); - const accountId = await account.getAccountId(); let authorizeResult; try { authorizeResult = await client.authorize({ account, - userId: accountId, + protocols: ['mpp'], destination: typeof originalRequest.url === 'string' ? originalRequest.url : originalRequest.url.toString(), - protocol: 'mpp', challenge, }); } catch (authorizeError) { diff --git a/packages/atxp-client/src/paymentClient.test.ts b/packages/atxp-client/src/paymentClient.test.ts index e52e9c9..d21a9f5 100644 --- a/packages/atxp-client/src/paymentClient.test.ts +++ b/packages/atxp-client/src/paymentClient.test.ts @@ -68,32 +68,24 @@ describe('buildPaymentHeaders', () => { }); describe('PaymentClient', () => { - let mockFetch: ReturnType; - beforeEach(() => { vi.clearAllMocks(); - mockFetch = vi.fn(); }); describe('authorize', () => { - it('should delegate to account.authorize with x402 protocol', async () => { + it('should delegate to account.authorize with protocols array', async () => { const mockAuthorize = vi.fn().mockResolvedValue({ protocol: 'x402', credential: 'x402-payment-header', }); const account = createMockAccount(mockAuthorize); - const client = new PaymentClient({ - accountsServer: 'https://accounts.test.com', - logger, - fetchFn: mockFetch, - }); + const client = new PaymentClient({ logger }); const result = await client.authorize({ account, - userId: 'base:0xtest', + protocols: ['x402'], destination: 'https://example.com/api', - protocol: 'x402', paymentRequirements: { network: 'base', scheme: 'exact' }, }); @@ -102,7 +94,7 @@ describe('PaymentClient', () => { // Verify account.authorize was called with correct params expect(mockAuthorize).toHaveBeenCalledWith({ - protocol: 'x402', + protocols: ['x402'], amount: undefined, destination: 'https://example.com/api', memo: undefined, @@ -118,18 +110,13 @@ describe('PaymentClient', () => { }); const account = createMockAccount(mockAuthorize); - const client = new PaymentClient({ - accountsServer: 'https://accounts.test.com', - logger, - fetchFn: mockFetch, - }); + const client = new PaymentClient({ logger }); const challenge = { id: 'ch_1', method: 'tempo', amount: '1000000' }; const result = await client.authorize({ account, - userId: 'base:0xtest', + protocols: ['mpp'], destination: 'https://example.com/api', - protocol: 'mpp', challenge, }); @@ -137,7 +124,7 @@ describe('PaymentClient', () => { expect(result.credential).toBe('mpp-credential-value'); expect(mockAuthorize).toHaveBeenCalledWith( - expect.objectContaining({ protocol: 'mpp', challenge }) + expect.objectContaining({ protocols: ['mpp'], challenge }) ); }); @@ -149,17 +136,12 @@ describe('PaymentClient', () => { }); const account = createMockAccount(mockAuthorize); - const client = new PaymentClient({ - accountsServer: 'https://accounts.test.com', - logger, - fetchFn: mockFetch, - }); + const client = new PaymentClient({ logger }); const result = await client.authorize({ account, - userId: 'base:0xtest', + protocols: ['atxp'], destination: '0xrecipient', - protocol: 'atxp', amount: new BigNumber('1.5'), memo: 'test payment', }); @@ -168,7 +150,7 @@ describe('PaymentClient', () => { expect(result.credential).toBe(JSON.stringify(responseBody)); expect(mockAuthorize).toHaveBeenCalledWith({ - protocol: 'atxp', + protocols: ['atxp'], amount: new BigNumber('1.5'), destination: '0xrecipient', memo: 'test payment', @@ -177,126 +159,44 @@ describe('PaymentClient', () => { }); }); - it('should use protocolFlag when no explicit protocol is provided', async () => { + it('should pass multiple protocols through to account.authorize', async () => { const mockAuthorize = vi.fn().mockResolvedValue({ - protocol: 'mpp', - credential: 'flag-cred', + protocol: 'x402', + credential: 'multi-cred', }); const account = createMockAccount(mockAuthorize); - const client = new PaymentClient({ - accountsServer: 'https://accounts.test.com', - protocolFlag: (_userId, _dest) => 'mpp', - logger, - fetchFn: mockFetch, - }); + const client = new PaymentClient({ logger }); const result = await client.authorize({ account, - userId: 'base:0xtest', + protocols: ['x402', 'atxp'], destination: 'https://example.com/api', - challenge: { id: 'ch_1' }, + paymentRequirements: { network: 'base' }, }); - expect(result.protocol).toBe('mpp'); - expect(mockAuthorize).toHaveBeenCalledWith( - expect.objectContaining({ protocol: 'mpp' }) - ); - }); - - it('should default to atxp when no protocol or protocolFlag', async () => { - const mockAuthorize = vi.fn().mockResolvedValue({ - protocol: 'atxp', - credential: '{"status":"ok"}', - }); - const account = createMockAccount(mockAuthorize); - - const client = new PaymentClient({ - accountsServer: 'https://accounts.test.com', - logger, - fetchFn: mockFetch, - }); - - const result = await client.authorize({ - account, - userId: 'base:0xtest', - destination: '0xrecipient', - amount: new BigNumber('1'), - }); - - expect(result.protocol).toBe('atxp'); + expect(result.protocol).toBe('x402'); expect(mockAuthorize).toHaveBeenCalledWith( - expect.objectContaining({ protocol: 'atxp' }) + expect.objectContaining({ protocols: ['x402', 'atxp'] }) ); }); it('should propagate errors from account.authorize', async () => { const mockAuthorize = vi.fn().mockRejectedValue( - new Error('ATXPAccount: /authorize/x402 failed (404): Not Found') + new Error('ATXPAccount: /authorize/auto failed (404): Not Found') ); const account = createMockAccount(mockAuthorize); - const client = new PaymentClient({ - accountsServer: 'https://accounts.test.com', - logger, - fetchFn: mockFetch, - }); - - await expect( - client.authorize({ - account, - userId: 'base:0xtest', - destination: 'https://example.com/api', - protocol: 'x402', - paymentRequirements: {}, - }) - ).rejects.toThrow('/authorize/x402 failed (404)'); - }); - - it('should propagate missing paymentHeader errors from account.authorize', async () => { - const mockAuthorize = vi.fn().mockRejectedValue( - new Error('ATXPAccount: /authorize/x402 response missing or invalid paymentHeader') - ); - const account = createMockAccount(mockAuthorize); - - const client = new PaymentClient({ - accountsServer: 'https://accounts.test.com', - logger, - fetchFn: mockFetch, - }); + const client = new PaymentClient({ logger }); await expect( client.authorize({ account, - userId: 'base:0xtest', + protocols: ['x402'], destination: 'https://example.com/api', - protocol: 'x402', paymentRequirements: {}, }) - ).rejects.toThrow('missing or invalid paymentHeader'); - }); - - it('should propagate missing credential errors from account.authorize', async () => { - const mockAuthorize = vi.fn().mockRejectedValue( - new Error('ATXPAccount: /authorize/mpp response missing or invalid credential') - ); - const account = createMockAccount(mockAuthorize); - - const client = new PaymentClient({ - accountsServer: 'https://accounts.test.com', - logger, - fetchFn: mockFetch, - }); - - await expect( - client.authorize({ - account, - userId: 'base:0xtest', - destination: 'https://example.com/api', - protocol: 'mpp', - challenge: {}, - }) - ).rejects.toThrow('missing or invalid credential'); + ).rejects.toThrow('/authorize/auto failed (404)'); }); it('should work with accounts that have no token property', async () => { @@ -313,17 +213,12 @@ describe('PaymentClient', () => { authorize: mockAuthorize, }; - const client = new PaymentClient({ - accountsServer: 'https://accounts.test.com', - logger, - fetchFn: mockFetch, - }); + const client = new PaymentClient({ logger }); const result = await client.authorize({ account: accountNoToken, - userId: 'base:0xtest', + protocols: ['x402'], destination: 'https://example.com/api', - protocol: 'x402', paymentRequirements: {}, }); diff --git a/packages/atxp-client/src/paymentClient.ts b/packages/atxp-client/src/paymentClient.ts index ee8bd91..e94d35d 100644 --- a/packages/atxp-client/src/paymentClient.ts +++ b/packages/atxp-client/src/paymentClient.ts @@ -1,4 +1,4 @@ -import type { PaymentProtocol, ProtocolFlag, FetchLike, Logger, Account, AuthorizeResult } from '@atxp/common'; +import type { PaymentProtocol, Logger, Account, AuthorizeResult } from '@atxp/common'; import { BigNumber } from 'bignumber.js'; // Re-export AuthorizeResult from common so existing imports keep working @@ -40,35 +40,27 @@ export function buildPaymentHeaders(result: AuthorizeResult, originalHeaders?: H /** * Client for authorizing payments. * - * Resolves the payment protocol via protocolFlag, then delegates to - * account.authorize() for the actual authorization logic. + * Passes protocols through to account.authorize() which handles + * protocol selection and authorization logic. */ export class PaymentClient { - private protocolFlag?: ProtocolFlag; private logger: Logger; constructor(config: { - protocolFlag?: ProtocolFlag; logger: Logger; - /** @deprecated No longer used — authorization delegates to account.authorize() */ - accountsServer?: string; - /** @deprecated No longer used — authorization delegates to account.authorize() */ - fetchFn?: FetchLike; }) { - this.protocolFlag = config.protocolFlag; this.logger = config.logger; } /** * Authorize a payment by delegating to the account's authorize method. * - * PaymentClient resolves the protocol (via explicit param or protocolFlag), - * then delegates all protocol-specific logic to account.authorize(). + * PaymentClient passes the protocols array through to account.authorize(), + * which selects the appropriate protocol and handles authorization. * * @param params.account - The account to authorize the payment through - * @param params.userId - Passed to protocolFlag for protocol selection + * @param params.protocols - Payment protocols the server/caller supports * @param params.destination - Payment destination address - * @param params.protocol - Explicit protocol override (skips protocolFlag) * @param params.amount - Payment amount * @param params.memo - Payment memo * @param params.paymentRequirements - X402 payment requirements @@ -77,25 +69,17 @@ export class PaymentClient { */ async authorize(params: { account: Account; - userId: string; - destination: string; - protocol?: PaymentProtocol; + protocols: PaymentProtocol[]; amount?: BigNumber; + destination: string; memo?: string; paymentRequirements?: unknown; challenge?: unknown; }): Promise { - const { account, userId, destination } = params; - - // Determine protocol - const protocol: PaymentProtocol = params.protocol - ?? (this.protocolFlag ? this.protocolFlag(userId, destination) : 'atxp'); - - // Delegate to the account's authorize method - return account.authorize({ - protocol, + return params.account.authorize({ + protocols: params.protocols, amount: params.amount!, - destination, + destination: params.destination, memo: params.memo, paymentRequirements: params.paymentRequirements, challenge: params.challenge, diff --git a/packages/atxp-client/src/protocolHandler.test.ts b/packages/atxp-client/src/protocolHandler.test.ts index c1727ff..d959949 100644 --- a/packages/atxp-client/src/protocolHandler.test.ts +++ b/packages/atxp-client/src/protocolHandler.test.ts @@ -151,7 +151,7 @@ describe('X402ProtocolHandler', () => { // Verify account.authorize was called with x402 protocol expect(mockAccount.authorize).toHaveBeenCalledWith( - expect.objectContaining({ protocol: 'x402' }) + expect.objectContaining({ protocols: ['x402'] }) ); // Verify retry included X-PAYMENT header @@ -368,7 +368,7 @@ describe('ATXPFetcher with protocol handlers', () => { // Verify account.authorize was called (X402 handler was used) expect(account.authorize).toHaveBeenCalledWith( - expect.objectContaining({ protocol: 'x402' }) + expect.objectContaining({ protocols: ['x402'] }) ); }); @@ -551,7 +551,7 @@ describe('MPPProtocolHandler', () => { // Verify account.authorize was called with mpp protocol expect(mockAccount.authorize).toHaveBeenCalledWith( - expect.objectContaining({ protocol: 'mpp' }) + expect.objectContaining({ protocols: ['mpp'] }) ); // Verify retry included Authorization: Payment header @@ -821,7 +821,7 @@ describe('ATXPFetcher with MPP handler', () => { // Verify account.authorize was called (MPP handler was used, not X402) expect(account.authorize).toHaveBeenCalledWith( - expect.objectContaining({ protocol: 'mpp' }) + expect.objectContaining({ protocols: ['mpp'] }) ); }); @@ -924,7 +924,7 @@ describe('ATXPFetcher with MPP handler', () => { // Verify account.authorize was called expect(account.authorize).toHaveBeenCalledWith( - expect.objectContaining({ protocol: 'mpp' }) + expect.objectContaining({ protocols: ['mpp'] }) ); }); }); diff --git a/packages/atxp-client/src/x402ProtocolHandler.ts b/packages/atxp-client/src/x402ProtocolHandler.ts index 4b51bf3..94781f8 100644 --- a/packages/atxp-client/src/x402ProtocolHandler.ts +++ b/packages/atxp-client/src/x402ProtocolHandler.ts @@ -28,6 +28,7 @@ function isX402Challenge(obj: unknown): obj is X402Challenge { } export interface X402ProtocolHandlerConfig { + /** @deprecated No longer used */ accountsServer?: string; } @@ -39,10 +40,8 @@ export interface X402ProtocolHandlerConfig { */ export class X402ProtocolHandler implements ProtocolHandler { readonly protocol = 'x402'; - private accountsServer: string; - constructor(config?: X402ProtocolHandlerConfig) { - this.accountsServer = config?.accountsServer ?? 'https://accounts.atxp.ai'; + constructor(_config?: X402ProtocolHandlerConfig) { } async canHandle(response: Response): Promise { @@ -127,16 +126,13 @@ export class X402ProtocolHandler implements ProtocolHandler { // service, BaseAccount signs locally. No fallback — each account type // handles authorization according to its capabilities. const client = new PaymentClient({ - accountsServer: this.accountsServer, logger, - fetchFn, }); const authorizeResult = await client.authorize({ account, - userId: accountId, + protocols: ['x402'], destination: url, - protocol: 'x402', paymentRequirements: selectedPaymentRequirements, }); const paymentHeader = authorizeResult.credential; diff --git a/packages/atxp-common/src/atxpAccount.test.ts b/packages/atxp-common/src/atxpAccount.test.ts index a6e100d..2fc400c 100644 --- a/packages/atxp-common/src/atxpAccount.test.ts +++ b/packages/atxp-common/src/atxpAccount.test.ts @@ -236,11 +236,11 @@ describe('ATXPAccount', () => { mockFetch = vi.fn(); }); - it('should call /authorize/atxp with correct body and inject sourceAccountToken', async () => { - const responseBody = { authorized: true, sourceAccountId: 'acct_123', options: {} }; + it('should call /authorize/auto with correct body and inject sourceAccountToken for atxp', async () => { + const credentialObj = { authorized: true, sourceAccountId: 'acct_123', options: {} }; mockFetch.mockResolvedValue({ ok: true, - json: async () => ({ ...responseBody }), + json: async () => ({ protocol: 'atxp', credential: JSON.stringify(credentialObj) }), }); const account = new ATXPAccount( @@ -249,7 +249,7 @@ describe('ATXPAccount', () => { ); const result = await account.authorize({ - protocol: 'atxp', + protocols: ['atxp'], amount: new BigNumber('2.5'), destination: '0xrecipient', memo: 'test memo', @@ -261,14 +261,17 @@ describe('ATXPAccount', () => { expect(parsed.sourceAccountToken).toBe('ct_abc123'); expect(mockFetch).toHaveBeenCalledWith( - 'https://accounts.example.com/authorize/atxp', + 'https://accounts.example.com/authorize/auto', expect.objectContaining({ method: 'POST', body: JSON.stringify({ + protocols: ['atxp'], amount: '2.5', currency: 'USDC', receiver: '0xrecipient', memo: 'test memo', + paymentRequirements: undefined, + challenge: undefined, }), }) ); @@ -279,10 +282,10 @@ describe('ATXPAccount', () => { expect(callHeaders['Authorization']).toBe(expectedAuth); }); - it('should call /authorize/x402 and return paymentHeader as credential', async () => { + it('should call /authorize/auto and return x402 credential as-is', async () => { mockFetch.mockResolvedValue({ ok: true, - json: async () => ({ paymentHeader: 'x402-header-value' }), + json: async () => ({ protocol: 'x402', credential: 'x402-header-value' }), }); const account = new ATXPAccount( @@ -291,7 +294,7 @@ describe('ATXPAccount', () => { ); const result = await account.authorize({ - protocol: 'x402', + protocols: ['x402'], amount: new BigNumber('1'), destination: 'https://example.com', paymentRequirements: { network: 'base' }, @@ -301,10 +304,10 @@ describe('ATXPAccount', () => { expect(result.credential).toBe('x402-header-value'); }); - it('should call /authorize/mpp and return credential', async () => { + it('should call /authorize/auto and return mpp credential as-is', async () => { mockFetch.mockResolvedValue({ ok: true, - json: async () => ({ credential: 'mpp-cred-value' }), + json: async () => ({ protocol: 'mpp', credential: 'mpp-cred-value' }), }); const account = new ATXPAccount( @@ -313,7 +316,7 @@ describe('ATXPAccount', () => { ); const result = await account.authorize({ - protocol: 'mpp', + protocols: ['mpp'], amount: new BigNumber('1'), destination: 'https://example.com', challenge: { id: 'ch_1' }, @@ -337,17 +340,17 @@ describe('ATXPAccount', () => { await expect( account.authorize({ - protocol: 'atxp', + protocols: ['atxp'], amount: new BigNumber('1'), destination: '0xrecipient', }) - ).rejects.toThrow('/authorize/atxp failed (500)'); + ).rejects.toThrow('/authorize/auto failed (500)'); }); - it('should throw when x402 response missing paymentHeader', async () => { + it('should send multiple protocols in the request', async () => { mockFetch.mockResolvedValue({ ok: true, - json: async () => ({ invalid: 'response' }), + json: async () => ({ protocol: 'x402', credential: 'x402-header' }), }); const account = new ATXPAccount( @@ -355,35 +358,18 @@ describe('ATXPAccount', () => { { fetchFn: mockFetch } ); - await expect( - account.authorize({ - protocol: 'x402', - amount: new BigNumber('1'), - destination: 'https://example.com', - paymentRequirements: {}, - }) - ).rejects.toThrow('missing or invalid paymentHeader'); - }); - - it('should throw when mpp response missing credential', async () => { - mockFetch.mockResolvedValue({ - ok: true, - json: async () => ({ somethingElse: 'value' }), + const result = await account.authorize({ + protocols: ['x402', 'atxp'], + amount: new BigNumber('1'), + destination: 'https://example.com', + paymentRequirements: { network: 'base' }, }); - const account = new ATXPAccount( - 'https://accounts.example.com?connection_token=ct_abc123&account_id=atxp_acct_xyz', - { fetchFn: mockFetch } - ); + expect(result.protocol).toBe('x402'); + expect(result.credential).toBe('x402-header'); - await expect( - account.authorize({ - protocol: 'mpp', - amount: new BigNumber('1'), - destination: 'https://example.com', - challenge: {}, - }) - ).rejects.toThrow('missing or invalid credential'); + const sentBody = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(sentBody.protocols).toEqual(['x402', 'atxp']); }); }); }); diff --git a/packages/atxp-common/src/atxpAccount.ts b/packages/atxp-common/src/atxpAccount.ts index 4a46b8b..1bad46c 100644 --- a/packages/atxp-common/src/atxpAccount.ts +++ b/packages/atxp-common/src/atxpAccount.ts @@ -1,4 +1,4 @@ -import type { Account, PaymentMaker, MeResponse, AuthorizeParams, AuthorizeResult } from './types.js'; +import type { Account, PaymentMaker, MeResponse, AuthorizeParams, AuthorizeResult, PaymentProtocol } from './types.js'; import type { FetchLike, Currency, AccountId, PaymentIdentifier, Destination, Chain, Source } from './types.js'; import { AuthorizationError } from './types.js'; import BigNumber from 'bignumber.js'; @@ -313,40 +313,29 @@ export class ATXPAccount implements Account { /** * Authorize a payment through the accounts service. - * Calls /authorize/{protocol} and returns an opaque credential. + * Calls /authorize/auto and returns an opaque credential. */ async authorize(params: AuthorizeParams): Promise { - const { protocol } = params; const authHeaders: Record = { 'Content-Type': 'application/json', 'Authorization': toBasicAuth(this.token), }; - let body: Record; - switch (protocol) { - case 'atxp': - body = { - amount: params.amount.toString(), - currency: 'USDC', - receiver: params.destination, - memo: params.memo, - }; - break; - case 'x402': - body = { paymentRequirements: params.paymentRequirements }; - break; - case 'mpp': - body = { challenge: params.challenge }; - break; - default: - throw new Error(`ATXPAccount: unsupported protocol '${protocol}'`); - } + const body: Record = { + protocols: params.protocols, + amount: params.amount.toString(), + currency: 'USDC', + receiver: params.destination, + memo: params.memo, + paymentRequirements: params.paymentRequirements, + challenge: params.challenge, + }; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); let response: Response; try { - response = await this.fetchFn(`${this.origin}/authorize/${protocol}`, { + response = await this.fetchFn(`${this.origin}/authorize/auto`, { method: 'POST', headers: authHeaders, body: JSON.stringify(body), @@ -364,36 +353,23 @@ export class ATXPAccount implements Account { errorCode = parsed.error || errorCode; } catch { /* not JSON */ } throw new AuthorizationError( - `ATXPAccount: /authorize/${protocol} failed (${response.status}): ${errorText}`, + `ATXPAccount: /authorize/auto failed (${response.status}): ${errorText}`, response.status, errorCode, ); } - const responseBody = await response.json() as Record; + const responseBody = await response.json() as { protocol: string; credential: string }; + const protocol = responseBody.protocol as PaymentProtocol; let credential: string; - switch (protocol) { - case 'atxp': { - // Inject the connection token so the credential is self-contained - responseBody.sourceAccountToken = this.token; - credential = JSON.stringify(responseBody); - break; - } - case 'x402': - if (!responseBody.paymentHeader || typeof responseBody.paymentHeader !== 'string') { - throw new Error('ATXPAccount: /authorize/x402 response missing or invalid paymentHeader'); - } - credential = responseBody.paymentHeader; - break; - case 'mpp': - if (!responseBody.credential || typeof responseBody.credential !== 'string') { - throw new Error('ATXPAccount: /authorize/mpp response missing or invalid credential'); - } - credential = responseBody.credential; - break; - default: - throw new Error(`ATXPAccount: unsupported protocol '${protocol}'`); + if (protocol === 'atxp') { + // Inject the connection token so the credential is self-contained + const credentialObj = JSON.parse(responseBody.credential); + credentialObj.sourceAccountToken = this.token; + credential = JSON.stringify(credentialObj); + } else { + credential = responseBody.credential; } return { protocol, credential }; diff --git a/packages/atxp-common/src/types.ts b/packages/atxp-common/src/types.ts index fd5bca0..b674763 100644 --- a/packages/atxp-common/src/types.ts +++ b/packages/atxp-common/src/types.ts @@ -197,7 +197,7 @@ export interface PaymentDestination { } export interface AuthorizeParams { - protocol: PaymentProtocol; + protocols: PaymentProtocol[]; amount: BigNumber; destination: string; memo?: string; @@ -246,8 +246,9 @@ export type Account = PaymentDestination & { /** * Authorize a payment through the appropriate channel for this account type. * - * For ATXPAccount: calls /authorize/{protocol} on the accounts service (pre-check only, no payment execution). - * For local-key accounts (Base, Solana, etc.): signs locally and/or executes the payment, returning evidence. + * For ATXPAccount: calls /authorize/auto on the accounts service (pre-check only, no payment execution). + * For local-key accounts (Base, Solana, etc.): picks the first supported protocol from + * params.protocols and signs locally and/or executes the payment, returning evidence. * * Returns an opaque credential that can be passed to ProtocolSettlement.settle(). */ diff --git a/packages/atxp-polygon/src/polygonBrowserAccount.ts b/packages/atxp-polygon/src/polygonBrowserAccount.ts index 23a2ec3..1a0b2f6 100644 --- a/packages/atxp-polygon/src/polygonBrowserAccount.ts +++ b/packages/atxp-polygon/src/polygonBrowserAccount.ts @@ -124,28 +124,22 @@ export class PolygonBrowserAccount implements Account { * Authorize a payment through the appropriate channel for Polygon browser accounts. */ async authorize(params: AuthorizeParams): Promise { - const { protocol } = params; - - switch (protocol) { - case 'atxp': { - const destination: Destination = { - chain: ChainEnum.Polygon, - currency: 'USDC', - address: params.destination, - amount: new BigNumber(params.amount), - }; - const result = await this.paymentMakers[0].makePayment([destination], params.memo || ''); - if (!result) { - throw new Error('PolygonBrowserAccount: payment execution returned no result'); - } - return { protocol, credential: JSON.stringify(result) }; - } - case 'x402': - throw new Error('PolygonBrowserAccount does not support x402 protocol'); - case 'mpp': - throw new Error('PolygonBrowserAccount does not support MPP protocol'); - default: - throw new Error(`PolygonBrowserAccount: unsupported protocol '${protocol}'`); + const supported: string[] = ['atxp']; + const protocol = params.protocols.find(p => supported.includes(p)); + if (!protocol) { + throw new Error(`PolygonBrowserAccount does not support any of: ${params.protocols.join(', ')}`); } + + const destination: Destination = { + chain: ChainEnum.Polygon, + currency: 'USDC', + address: params.destination, + amount: new BigNumber(params.amount), + }; + const result = await this.paymentMakers[0].makePayment([destination], params.memo || ''); + if (!result) { + throw new Error('PolygonBrowserAccount: payment execution returned no result'); + } + return { protocol, credential: JSON.stringify(result) }; } } diff --git a/packages/atxp-polygon/src/polygonServerAccount.ts b/packages/atxp-polygon/src/polygonServerAccount.ts index b1e92cc..3dc4568 100644 --- a/packages/atxp-polygon/src/polygonServerAccount.ts +++ b/packages/atxp-polygon/src/polygonServerAccount.ts @@ -110,29 +110,23 @@ export class PolygonServerAccount implements Account { * Authorize a payment through the appropriate channel for Polygon server accounts. */ async authorize(params: AuthorizeParams): Promise { - const { protocol } = params; + const supported: string[] = ['atxp']; + const protocol = params.protocols.find(p => supported.includes(p)); + if (!protocol) { + throw new Error(`PolygonServerAccount does not support any of: ${params.protocols.join(', ')}`); + } - switch (protocol) { - case 'atxp': { - const chain = this.chainId === 137 ? ChainEnum.Polygon : ChainEnum.PolygonAmoy; - const destination: Destination = { - chain, - currency: 'USDC', - address: params.destination, - amount: new BigNumber(params.amount), - }; - const result = await this.paymentMakers[0].makePayment([destination], params.memo || ''); - if (!result) { - throw new Error('PolygonServerAccount: payment execution returned no result'); - } - return { protocol, credential: JSON.stringify(result) }; - } - case 'x402': - throw new Error('PolygonServerAccount does not support x402 protocol'); - case 'mpp': - throw new Error('PolygonServerAccount does not support MPP protocol'); - default: - throw new Error(`PolygonServerAccount: unsupported protocol '${protocol}'`); + const chain = this.chainId === 137 ? ChainEnum.Polygon : ChainEnum.PolygonAmoy; + const destination: Destination = { + chain, + currency: 'USDC', + address: params.destination, + amount: new BigNumber(params.amount), + }; + const result = await this.paymentMakers[0].makePayment([destination], params.memo || ''); + if (!result) { + throw new Error('PolygonServerAccount: payment execution returned no result'); } + return { protocol, credential: JSON.stringify(result) }; } } diff --git a/packages/atxp-solana/src/solanaAccount.ts b/packages/atxp-solana/src/solanaAccount.ts index ad407b3..70736bf 100644 --- a/packages/atxp-solana/src/solanaAccount.ts +++ b/packages/atxp-solana/src/solanaAccount.ts @@ -57,28 +57,22 @@ export class SolanaAccount implements Account { * Authorize a payment through the appropriate channel for Solana accounts. */ async authorize(params: AuthorizeParams): Promise { - const { protocol } = params; + const supported: string[] = ['atxp']; + const protocol = params.protocols.find(p => supported.includes(p)); + if (!protocol) { + throw new Error(`SolanaAccount does not support any of: ${params.protocols.join(', ')}`); + } - switch (protocol) { - case 'atxp': { - const destination: Destination = { - chain: 'solana', - currency: 'USDC', - address: params.destination, - amount: new BigNumber(params.amount), - }; - const result = await this.paymentMakers[0].makePayment([destination], params.memo || ''); - if (!result) { - throw new Error('SolanaAccount: payment execution returned no result'); - } - return { protocol, credential: JSON.stringify(result) }; - } - case 'x402': - throw new Error('SolanaAccount does not support x402 protocol'); - case 'mpp': - throw new Error('SolanaAccount does not support MPP protocol'); - default: - throw new Error(`SolanaAccount: unsupported protocol '${protocol}'`); + const destination: Destination = { + chain: 'solana', + currency: 'USDC', + address: params.destination, + amount: new BigNumber(params.amount), + }; + const result = await this.paymentMakers[0].makePayment([destination], params.memo || ''); + if (!result) { + throw new Error('SolanaAccount: payment execution returned no result'); } + return { protocol, credential: JSON.stringify(result) }; } } \ No newline at end of file diff --git a/packages/atxp-tempo/src/tempoAccount.ts b/packages/atxp-tempo/src/tempoAccount.ts index 28605d8..01d55da 100644 --- a/packages/atxp-tempo/src/tempoAccount.ts +++ b/packages/atxp-tempo/src/tempoAccount.ts @@ -76,28 +76,22 @@ export class TempoAccount implements Account { * Authorize a payment through the appropriate channel for Tempo accounts. */ async authorize(params: AuthorizeParams): Promise { - const { protocol } = params; + const supported: string[] = ['mpp']; + const protocol = params.protocols.find(p => supported.includes(p)); + if (!protocol) { + throw new Error(`TempoAccount does not support any of: ${params.protocols.join(', ')}`); + } - switch (protocol) { - case 'mpp': { - const destination: Destination = { - chain: 'tempo', - currency: 'USDC', - address: params.destination, - amount: new BigNumber(params.amount), - }; - const result = await this.paymentMakers[0].makePayment([destination], params.memo || ''); - if (!result) { - throw new Error('TempoAccount: payment execution returned no result'); - } - return { protocol, credential: JSON.stringify(result) }; - } - case 'atxp': - throw new Error('TempoAccount does not support ATXP protocol'); - case 'x402': - throw new Error('TempoAccount does not support x402 protocol'); - default: - throw new Error(`TempoAccount: unsupported protocol '${protocol}'`); + const destination: Destination = { + chain: 'tempo', + currency: 'USDC', + address: params.destination, + amount: new BigNumber(params.amount), + }; + const result = await this.paymentMakers[0].makePayment([destination], params.memo || ''); + if (!result) { + throw new Error('TempoAccount: payment execution returned no result'); } + return { protocol, credential: JSON.stringify(result) }; } } diff --git a/packages/atxp-worldchain/src/worldchainAccount.ts b/packages/atxp-worldchain/src/worldchainAccount.ts index 6a1ada1..038687f 100644 --- a/packages/atxp-worldchain/src/worldchainAccount.ts +++ b/packages/atxp-worldchain/src/worldchainAccount.ts @@ -232,29 +232,23 @@ export class WorldchainAccount implements Account { * Authorize a payment through the appropriate channel for World Chain accounts. */ async authorize(params: AuthorizeParams): Promise { - const { protocol } = params; + const supported: string[] = ['atxp']; + const protocol = params.protocols.find(p => supported.includes(p)); + if (!protocol) { + throw new Error(`WorldchainAccount does not support any of: ${params.protocols.join(', ')}`); + } - switch (protocol) { - case 'atxp': { - const chain = this.chainId === 11155420 ? ChainEnum.WorldSepolia : ChainEnum.World; - const destination: Destination = { - chain, - currency: 'USDC', - address: params.destination, - amount: new BigNumber(params.amount), - }; - const result = await this.paymentMakers[0].makePayment([destination], params.memo || ''); - if (!result) { - throw new Error('WorldchainAccount: payment execution returned no result'); - } - return { protocol, credential: JSON.stringify(result) }; - } - case 'x402': - throw new Error('WorldchainAccount does not support x402 protocol'); - case 'mpp': - throw new Error('WorldchainAccount does not support MPP protocol'); - default: - throw new Error(`WorldchainAccount: unsupported protocol '${protocol}'`); + const chain = this.chainId === 11155420 ? ChainEnum.WorldSepolia : ChainEnum.World; + const destination: Destination = { + chain, + currency: 'USDC', + address: params.destination, + amount: new BigNumber(params.amount), + }; + const result = await this.paymentMakers[0].makePayment([destination], params.memo || ''); + if (!result) { + throw new Error('WorldchainAccount: payment execution returned no result'); } + return { protocol, credential: JSON.stringify(result) }; } } \ No newline at end of file From abda073c209f7a9f0675d5fac84261da5fd27d1c Mon Sep 17 00:00:00 2001 From: bdj Date: Wed, 1 Apr 2026 11:53:34 -0700 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?remove=20PaymentClient,=20fix=20bugs,=20add=20guards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove PaymentClient class (was pure passthrough to account.authorize) - Keep buildPaymentHeaders in new paymentHeaders.ts - Fix amount.toString() crash: conditionally include fields in body - Make amount/destination optional in AuthorizeParams - Add response validation for /authorize/auto (protocol + credential) - Type supported arrays as PaymentProtocol[] (not string[]) - Remove deprecated accountsServer from handler configs - Guard against empty protocols array in all account implementations - Add 4 new error-path tests for malformed responses - Fix missing trailing newlines Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/atxp-base/src/baseAccount.ts | 7 +- packages/atxp-base/src/baseAppAccount.ts | 9 +- packages/atxp-client/src/index.ts | 5 +- .../atxp-client/src/mppProtocolHandler.ts | 19 +- .../atxp-client/src/paymentClient.test.ts | 229 ------------------ packages/atxp-client/src/paymentClient.ts | 88 ------- packages/atxp-client/src/paymentHeaders.ts | 37 +++ .../atxp-client/src/protocolHandler.test.ts | 14 +- .../atxp-client/src/x402ProtocolHandler.ts | 17 +- packages/atxp-common/src/atxpAccount.test.ts | 81 ++++++- packages/atxp-common/src/atxpAccount.ts | 27 ++- packages/atxp-common/src/types.ts | 4 +- .../atxp-polygon/src/polygonBrowserAccount.ts | 7 +- .../atxp-polygon/src/polygonServerAccount.ts | 7 +- packages/atxp-solana/src/solanaAccount.ts | 9 +- packages/atxp-tempo/src/tempoAccount.ts | 7 +- .../atxp-worldchain/src/worldchainAccount.ts | 9 +- 17 files changed, 189 insertions(+), 387 deletions(-) delete mode 100644 packages/atxp-client/src/paymentClient.test.ts delete mode 100644 packages/atxp-client/src/paymentClient.ts create mode 100644 packages/atxp-client/src/paymentHeaders.ts diff --git a/packages/atxp-base/src/baseAccount.ts b/packages/atxp-base/src/baseAccount.ts index 4c018d7..4df90f8 100644 --- a/packages/atxp-base/src/baseAccount.ts +++ b/packages/atxp-base/src/baseAccount.ts @@ -1,5 +1,5 @@ import type { Account, PaymentMaker, Hex } from '@atxp/client'; -import type { AccountId, Source, AuthorizeParams, AuthorizeResult, Destination } from '@atxp/common'; +import type { AccountId, Source, AuthorizeParams, AuthorizeResult, Destination, PaymentProtocol } from '@atxp/common'; import { BigNumber } from 'bignumber.js'; import { privateKeyToAccount, PrivateKeyAccount } from 'viem/accounts'; import { BasePaymentMaker } from './basePaymentMaker.js'; @@ -72,7 +72,10 @@ export class BaseAccount implements Account { * Authorize a payment through the appropriate channel for Base accounts. */ async authorize(params: AuthorizeParams): Promise { - const supported: string[] = ['x402', 'atxp']; + if (!params.protocols || params.protocols.length === 0) { + throw new Error('BaseAccount: protocols array must not be empty'); + } + const supported: PaymentProtocol[] = ['x402', 'atxp']; const protocol = params.protocols.find(p => supported.includes(p)); if (!protocol) { throw new Error(`BaseAccount does not support any of: ${params.protocols.join(', ')}`); diff --git a/packages/atxp-base/src/baseAppAccount.ts b/packages/atxp-base/src/baseAppAccount.ts index e98ad12..e97104d 100644 --- a/packages/atxp-base/src/baseAppAccount.ts +++ b/packages/atxp-base/src/baseAppAccount.ts @@ -1,4 +1,4 @@ -import type { Account, PaymentMaker, AccountId, Source, AuthorizeParams, AuthorizeResult, Destination } from '@atxp/common'; +import type { Account, PaymentMaker, AccountId, Source, AuthorizeParams, AuthorizeResult, Destination, PaymentProtocol } from '@atxp/common'; import { WalletTypeEnum, ChainEnum } from '@atxp/common'; import { BigNumber } from 'bignumber.js'; import { getBaseUSDCAddress } from './baseConstants.js'; @@ -229,7 +229,10 @@ export class BaseAppAccount implements Account { * Authorize a payment through the appropriate channel for Base browser accounts. */ async authorize(params: AuthorizeParams): Promise { - const supported: string[] = ['atxp']; + if (!params.protocols || params.protocols.length === 0) { + throw new Error('BaseAppAccount: protocols array must not be empty'); + } + const supported: PaymentProtocol[] = ['atxp']; const protocol = params.protocols.find(p => supported.includes(p)); if (!protocol) { throw new Error(`BaseAppAccount does not support any of: ${params.protocols.join(', ')}`); @@ -248,4 +251,4 @@ export class BaseAppAccount implements Account { } return { protocol, credential: JSON.stringify(result) }; } -} \ No newline at end of file +} diff --git a/packages/atxp-client/src/index.ts b/packages/atxp-client/src/index.ts index d60ea56..c6cc541 100644 --- a/packages/atxp-client/src/index.ts +++ b/packages/atxp-client/src/index.ts @@ -105,10 +105,9 @@ export { MPPProtocolHandler } from './mppProtocolHandler.js'; -// Payment client for centralized authorize flow +// Payment header utilities for protocol-specific headers export { - PaymentClient, buildPaymentHeaders, type AuthorizeResult -} from './paymentClient.js'; +} from './paymentHeaders.js'; diff --git a/packages/atxp-client/src/mppProtocolHandler.ts b/packages/atxp-client/src/mppProtocolHandler.ts index 5223b33..1d14472 100644 --- a/packages/atxp-client/src/mppProtocolHandler.ts +++ b/packages/atxp-client/src/mppProtocolHandler.ts @@ -11,15 +11,7 @@ import { hasMPPMCPError, } from '@atxp/mpp'; import { BigNumber } from 'bignumber.js'; -import { PaymentClient, buildPaymentHeaders } from './paymentClient.js'; - -/** - * Configuration for MPP protocol handler. - */ -export interface MPPProtocolHandlerConfig { - /** @deprecated No longer used */ - accountsServer?: string; -} +import { buildPaymentHeaders } from './paymentHeaders.js'; /** * Protocol handler for MPP (Machine Payments Protocol) payment challenges. @@ -34,9 +26,6 @@ export interface MPPProtocolHandlerConfig { export class MPPProtocolHandler implements ProtocolHandler { readonly protocol = 'mpp'; - constructor(_config?: MPPProtocolHandlerConfig) { - } - async canHandle(response: Response): Promise { if (hasMPPChallenge(response)) return true; return hasMPPMCPError(response); @@ -173,14 +162,10 @@ export class MPPProtocolHandler implements ProtocolHandler { try { logger.debug('MPP: calling /authorize/auto on accounts service'); - const client = new PaymentClient({ - logger, - }); let authorizeResult; try { - authorizeResult = await client.authorize({ - account, + authorizeResult = await account.authorize({ protocols: ['mpp'], destination: typeof originalRequest.url === 'string' ? originalRequest.url : originalRequest.url.toString(), challenge, diff --git a/packages/atxp-client/src/paymentClient.test.ts b/packages/atxp-client/src/paymentClient.test.ts deleted file mode 100644 index d21a9f5..0000000 --- a/packages/atxp-client/src/paymentClient.test.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { ConsoleLogger, LogLevel } from '@atxp/common'; -import type { AuthorizeResult } from '@atxp/common'; -import { PaymentClient, buildPaymentHeaders } from './paymentClient.js'; -import { BigNumber } from 'bignumber.js'; - -function createMockAccount(authorizeImpl?: (params: any) => Promise) { - return { - token: 'test-token-123', - getAccountId: async () => 'base:0xtest' as any, - paymentMakers: [], - getSources: async () => [], - createSpendPermission: async () => null, - authorize: authorizeImpl ?? vi.fn().mockResolvedValue({ protocol: 'atxp', credential: '{}' }), - }; -} - -const logger = new ConsoleLogger({ prefix: '[Test]', level: LogLevel.ERROR }); - -describe('buildPaymentHeaders', () => { - it('should set X-PAYMENT header for x402 protocol', () => { - const result: AuthorizeResult = { protocol: 'x402', credential: 'x402-cred' }; - const headers = buildPaymentHeaders(result); - - expect(headers.get('X-PAYMENT')).toBe('x402-cred'); - expect(headers.get('Access-Control-Expose-Headers')).toBe('X-PAYMENT-RESPONSE'); - }); - - it('should set Authorization: Payment header for mpp protocol', () => { - const result: AuthorizeResult = { protocol: 'mpp', credential: 'mpp-cred' }; - const headers = buildPaymentHeaders(result); - - expect(headers.get('Authorization')).toBe('Payment mpp-cred'); - }); - - it('should not add any special headers for atxp protocol', () => { - const result: AuthorizeResult = { protocol: 'atxp', credential: '{"foo":"bar"}' }; - const headers = buildPaymentHeaders(result); - - expect(headers.get('X-PAYMENT')).toBeNull(); - expect(headers.get('Authorization')).toBeNull(); - }); - - it('should preserve original headers when provided as Headers object', () => { - const original = new Headers({ 'X-Custom': 'value', 'Accept': 'application/json' }); - const result: AuthorizeResult = { protocol: 'x402', credential: 'cred' }; - const headers = buildPaymentHeaders(result, original); - - expect(headers.get('X-Custom')).toBe('value'); - expect(headers.get('Accept')).toBe('application/json'); - expect(headers.get('X-PAYMENT')).toBe('cred'); - }); - - it('should preserve original headers when provided as plain object', () => { - const result: AuthorizeResult = { protocol: 'mpp', credential: 'cred' }; - const headers = buildPaymentHeaders(result, { 'X-Custom': 'value' }); - - expect(headers.get('X-Custom')).toBe('value'); - expect(headers.get('Authorization')).toBe('Payment cred'); - }); - - it('should handle undefined original headers', () => { - const result: AuthorizeResult = { protocol: 'x402', credential: 'cred' }; - const headers = buildPaymentHeaders(result, undefined); - - expect(headers.get('X-PAYMENT')).toBe('cred'); - }); -}); - -describe('PaymentClient', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('authorize', () => { - it('should delegate to account.authorize with protocols array', async () => { - const mockAuthorize = vi.fn().mockResolvedValue({ - protocol: 'x402', - credential: 'x402-payment-header', - }); - const account = createMockAccount(mockAuthorize); - - const client = new PaymentClient({ logger }); - - const result = await client.authorize({ - account, - protocols: ['x402'], - destination: 'https://example.com/api', - paymentRequirements: { network: 'base', scheme: 'exact' }, - }); - - expect(result.protocol).toBe('x402'); - expect(result.credential).toBe('x402-payment-header'); - - // Verify account.authorize was called with correct params - expect(mockAuthorize).toHaveBeenCalledWith({ - protocols: ['x402'], - amount: undefined, - destination: 'https://example.com/api', - memo: undefined, - paymentRequirements: { network: 'base', scheme: 'exact' }, - challenge: undefined, - }); - }); - - it('should delegate to account.authorize with mpp protocol', async () => { - const mockAuthorize = vi.fn().mockResolvedValue({ - protocol: 'mpp', - credential: 'mpp-credential-value', - }); - const account = createMockAccount(mockAuthorize); - - const client = new PaymentClient({ logger }); - - const challenge = { id: 'ch_1', method: 'tempo', amount: '1000000' }; - const result = await client.authorize({ - account, - protocols: ['mpp'], - destination: 'https://example.com/api', - challenge, - }); - - expect(result.protocol).toBe('mpp'); - expect(result.credential).toBe('mpp-credential-value'); - - expect(mockAuthorize).toHaveBeenCalledWith( - expect.objectContaining({ protocols: ['mpp'], challenge }) - ); - }); - - it('should delegate to account.authorize with atxp protocol', async () => { - const responseBody = { transactionId: 'tx_123', status: 'completed', sourceAccountToken: 'test-token-123' }; - const mockAuthorize = vi.fn().mockResolvedValue({ - protocol: 'atxp', - credential: JSON.stringify(responseBody), - }); - const account = createMockAccount(mockAuthorize); - - const client = new PaymentClient({ logger }); - - const result = await client.authorize({ - account, - protocols: ['atxp'], - destination: '0xrecipient', - amount: new BigNumber('1.5'), - memo: 'test payment', - }); - - expect(result.protocol).toBe('atxp'); - expect(result.credential).toBe(JSON.stringify(responseBody)); - - expect(mockAuthorize).toHaveBeenCalledWith({ - protocols: ['atxp'], - amount: new BigNumber('1.5'), - destination: '0xrecipient', - memo: 'test payment', - paymentRequirements: undefined, - challenge: undefined, - }); - }); - - it('should pass multiple protocols through to account.authorize', async () => { - const mockAuthorize = vi.fn().mockResolvedValue({ - protocol: 'x402', - credential: 'multi-cred', - }); - const account = createMockAccount(mockAuthorize); - - const client = new PaymentClient({ logger }); - - const result = await client.authorize({ - account, - protocols: ['x402', 'atxp'], - destination: 'https://example.com/api', - paymentRequirements: { network: 'base' }, - }); - - expect(result.protocol).toBe('x402'); - expect(mockAuthorize).toHaveBeenCalledWith( - expect.objectContaining({ protocols: ['x402', 'atxp'] }) - ); - }); - - it('should propagate errors from account.authorize', async () => { - const mockAuthorize = vi.fn().mockRejectedValue( - new Error('ATXPAccount: /authorize/auto failed (404): Not Found') - ); - const account = createMockAccount(mockAuthorize); - - const client = new PaymentClient({ logger }); - - await expect( - client.authorize({ - account, - protocols: ['x402'], - destination: 'https://example.com/api', - paymentRequirements: {}, - }) - ).rejects.toThrow('/authorize/auto failed (404)'); - }); - - it('should work with accounts that have no token property', async () => { - const mockAuthorize = vi.fn().mockResolvedValue({ - protocol: 'x402', - credential: 'cred', - }); - - const accountNoToken = { - getAccountId: async () => 'base:0xtest' as any, - paymentMakers: [], - getSources: async () => [], - createSpendPermission: async () => null, - authorize: mockAuthorize, - }; - - const client = new PaymentClient({ logger }); - - const result = await client.authorize({ - account: accountNoToken, - protocols: ['x402'], - destination: 'https://example.com/api', - paymentRequirements: {}, - }); - - expect(result.credential).toBe('cred'); - expect(mockAuthorize).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/atxp-client/src/paymentClient.ts b/packages/atxp-client/src/paymentClient.ts deleted file mode 100644 index e94d35d..0000000 --- a/packages/atxp-client/src/paymentClient.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { PaymentProtocol, Logger, Account, AuthorizeResult } from '@atxp/common'; -import { BigNumber } from 'bignumber.js'; - -// Re-export AuthorizeResult from common so existing imports keep working -export type { AuthorizeResult } from '@atxp/common'; - -/** - * Build protocol-specific payment headers for retrying a request after authorization. - * - * @param result - The authorization result containing protocol and credential - * @param originalHeaders - Optional original request headers to preserve - * @returns New Headers object with protocol-specific payment headers added - */ -export function buildPaymentHeaders(result: AuthorizeResult, originalHeaders?: HeadersInit): Headers { - let headers: Headers; - if (originalHeaders instanceof Headers) { - headers = new Headers(originalHeaders); - } else if (originalHeaders) { - headers = new Headers(originalHeaders as HeadersInit); - } else { - headers = new Headers(); - } - - switch (result.protocol) { - case 'x402': - headers.set('X-PAYMENT', result.credential); - headers.set('Access-Control-Expose-Headers', 'X-PAYMENT-RESPONSE'); - break; - case 'mpp': - headers.set('Authorization', `Payment ${result.credential}`); - break; - case 'atxp': - // ATXP uses the existing OAuth flow, not a payment header - break; - } - - return headers; -} - -/** - * Client for authorizing payments. - * - * Passes protocols through to account.authorize() which handles - * protocol selection and authorization logic. - */ -export class PaymentClient { - private logger: Logger; - - constructor(config: { - logger: Logger; - }) { - this.logger = config.logger; - } - - /** - * Authorize a payment by delegating to the account's authorize method. - * - * PaymentClient passes the protocols array through to account.authorize(), - * which selects the appropriate protocol and handles authorization. - * - * @param params.account - The account to authorize the payment through - * @param params.protocols - Payment protocols the server/caller supports - * @param params.destination - Payment destination address - * @param params.amount - Payment amount - * @param params.memo - Payment memo - * @param params.paymentRequirements - X402 payment requirements - * @param params.challenge - MPP challenge object - * @returns AuthorizeResult with protocol and opaque credential - */ - async authorize(params: { - account: Account; - protocols: PaymentProtocol[]; - amount?: BigNumber; - destination: string; - memo?: string; - paymentRequirements?: unknown; - challenge?: unknown; - }): Promise { - return params.account.authorize({ - protocols: params.protocols, - amount: params.amount!, - destination: params.destination, - memo: params.memo, - paymentRequirements: params.paymentRequirements, - challenge: params.challenge, - }); - } -} diff --git a/packages/atxp-client/src/paymentHeaders.ts b/packages/atxp-client/src/paymentHeaders.ts new file mode 100644 index 0000000..c4a4e9d --- /dev/null +++ b/packages/atxp-client/src/paymentHeaders.ts @@ -0,0 +1,37 @@ +import type { AuthorizeResult } from '@atxp/common'; + +// Re-export AuthorizeResult from common so existing imports keep working +export type { AuthorizeResult } from '@atxp/common'; + +/** + * Build protocol-specific payment headers for retrying a request after authorization. + * + * @param result - The authorization result containing protocol and credential + * @param originalHeaders - Optional original request headers to preserve + * @returns New Headers object with protocol-specific payment headers added + */ +export function buildPaymentHeaders(result: AuthorizeResult, originalHeaders?: HeadersInit): Headers { + let headers: Headers; + if (originalHeaders instanceof Headers) { + headers = new Headers(originalHeaders); + } else if (originalHeaders) { + headers = new Headers(originalHeaders as HeadersInit); + } else { + headers = new Headers(); + } + + switch (result.protocol) { + case 'x402': + headers.set('X-PAYMENT', result.credential); + headers.set('Access-Control-Expose-Headers', 'X-PAYMENT-RESPONSE'); + break; + case 'mpp': + headers.set('Authorization', `Payment ${result.credential}`); + break; + case 'atxp': + // ATXP uses the existing OAuth flow, not a payment header + break; + } + + return headers; +} diff --git a/packages/atxp-client/src/protocolHandler.test.ts b/packages/atxp-client/src/protocolHandler.test.ts index d959949..9e088cd 100644 --- a/packages/atxp-client/src/protocolHandler.test.ts +++ b/packages/atxp-client/src/protocolHandler.test.ts @@ -92,7 +92,7 @@ describe('X402ProtocolHandler', () => { beforeEach(() => { vi.clearAllMocks(); - handler = new X402ProtocolHandler({ accountsServer: 'https://accounts.test.com' }); + handler = new X402ProtocolHandler(); }); describe('canHandle', () => { @@ -331,7 +331,7 @@ describe('ATXPFetcher with protocol handlers', () => { } it('should use X402 handler when protocolFlag returns x402 for omni-challenge', async () => { - const x402Handler = new X402ProtocolHandler({ accountsServer: 'https://accounts.test.com' }); + const x402Handler = new X402ProtocolHandler(); const atxpHandler = new ATXPProtocolHandler(); const mockFetch = vi.fn(); @@ -406,7 +406,7 @@ describe('ATXPFetcher with protocol handlers', () => { }); it('should auto-detect protocol when only one handler matches', async () => { - const x402Handler = new X402ProtocolHandler({ accountsServer: 'https://accounts.test.com' }); + const x402Handler = new X402ProtocolHandler(); const mockFetch = vi.fn(); // Initial request returns X402 challenge @@ -475,7 +475,7 @@ describe('MPPProtocolHandler', () => { beforeEach(() => { vi.clearAllMocks(); - handler = new MPPProtocolHandler({ accountsServer: 'https://accounts.test.com' }); + handler = new MPPProtocolHandler(); }); describe('canHandle', () => { @@ -780,8 +780,8 @@ describe('ATXPFetcher with MPP handler', () => { } it('should use MPP handler when protocolFlag returns mpp for omni-challenge', async () => { - const mppHandler = new MPPProtocolHandler({ accountsServer: 'https://accounts.test.com' }); - const x402Handler = new X402ProtocolHandler({ accountsServer: 'https://accounts.test.com' }); + const mppHandler = new MPPProtocolHandler(); + const x402Handler = new X402ProtocolHandler(); const mockFetch = vi.fn(); @@ -886,7 +886,7 @@ describe('ATXPFetcher with MPP handler', () => { }); it('should auto-detect MPP from external server (WWW-Authenticate header)', async () => { - const mppHandler = new MPPProtocolHandler({ accountsServer: 'https://accounts.test.com' }); + const mppHandler = new MPPProtocolHandler(); const mockFetch = vi.fn(); // External server returns 402 with MPP header diff --git a/packages/atxp-client/src/x402ProtocolHandler.ts b/packages/atxp-client/src/x402ProtocolHandler.ts index 94781f8..43ec024 100644 --- a/packages/atxp-client/src/x402ProtocolHandler.ts +++ b/packages/atxp-client/src/x402ProtocolHandler.ts @@ -2,7 +2,7 @@ import type { ProtocolHandler, ProtocolConfig } from './protocolHandler.js'; import type { ProspectivePayment } from './types.js'; import { ATXPPaymentError } from './errors.js'; import { BigNumber } from 'bignumber.js'; -import { PaymentClient, buildPaymentHeaders } from './paymentClient.js'; +import { buildPaymentHeaders } from './paymentHeaders.js'; /** * Type guard for X402 challenge body. @@ -27,11 +27,6 @@ function isX402Challenge(obj: unknown): obj is X402Challenge { ); } -export interface X402ProtocolHandlerConfig { - /** @deprecated No longer used */ - accountsServer?: string; -} - /** * Protocol handler for X402 payment challenges. * @@ -41,9 +36,6 @@ export interface X402ProtocolHandlerConfig { export class X402ProtocolHandler implements ProtocolHandler { readonly protocol = 'x402'; - constructor(_config?: X402ProtocolHandlerConfig) { - } - async canHandle(response: Response): Promise { if (response.status !== 402) return false; @@ -125,12 +117,7 @@ export class X402ProtocolHandler implements ProtocolHandler { // Authorize via account.authorize() — ATXPAccount calls the accounts // service, BaseAccount signs locally. No fallback — each account type // handles authorization according to its capabilities. - const client = new PaymentClient({ - logger, - }); - - const authorizeResult = await client.authorize({ - account, + const authorizeResult = await account.authorize({ protocols: ['x402'], destination: url, paymentRequirements: selectedPaymentRequirements, diff --git a/packages/atxp-common/src/atxpAccount.test.ts b/packages/atxp-common/src/atxpAccount.test.ts index 2fc400c..6807915 100644 --- a/packages/atxp-common/src/atxpAccount.test.ts +++ b/packages/atxp-common/src/atxpAccount.test.ts @@ -267,11 +267,9 @@ describe('ATXPAccount', () => { body: JSON.stringify({ protocols: ['atxp'], amount: '2.5', - currency: 'USDC', receiver: '0xrecipient', memo: 'test memo', - paymentRequirements: undefined, - challenge: undefined, + currency: 'USDC', }), }) ); @@ -371,5 +369,82 @@ describe('ATXPAccount', () => { const sentBody = JSON.parse(mockFetch.mock.calls[0][1].body); expect(sentBody.protocols).toEqual(['x402', 'atxp']); }); + + it('should not include amount in body when undefined (x402/mpp paths)', async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({ protocol: 'x402', credential: 'x402-cred' }), + }); + + const account = new ATXPAccount( + 'https://accounts.example.com?connection_token=ct_abc123&account_id=atxp_acct_xyz', + { fetchFn: mockFetch } + ); + + await account.authorize({ + protocols: ['x402'], + paymentRequirements: { network: 'base' }, + }); + + const sentBody = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(sentBody.amount).toBeUndefined(); + expect(sentBody.receiver).toBeUndefined(); + expect(sentBody.currency).toBe('USDC'); + expect(sentBody.paymentRequirements).toEqual({ network: 'base' }); + }); + + it('should throw when protocols array is empty', async () => { + const account = new ATXPAccount( + 'https://accounts.example.com?connection_token=ct_abc123&account_id=atxp_acct_xyz', + { fetchFn: mockFetch } + ); + + await expect( + account.authorize({ + protocols: [], + destination: '0xrecipient', + }) + ).rejects.toThrow('protocols array must not be empty'); + }); + + it('should throw AuthorizationError when response is missing protocol field', async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({ credential: 'some-credential' }), + }); + + const account = new ATXPAccount( + 'https://accounts.example.com?connection_token=ct_abc123&account_id=atxp_acct_xyz', + { fetchFn: mockFetch } + ); + + await expect( + account.authorize({ + protocols: ['atxp'], + amount: new BigNumber('1'), + destination: '0xrecipient', + }) + ).rejects.toThrow('response missing protocol or credential'); + }); + + it('should throw AuthorizationError when response is missing credential field', async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({ protocol: 'atxp' }), + }); + + const account = new ATXPAccount( + 'https://accounts.example.com?connection_token=ct_abc123&account_id=atxp_acct_xyz', + { fetchFn: mockFetch } + ); + + await expect( + account.authorize({ + protocols: ['atxp'], + amount: new BigNumber('1'), + destination: '0xrecipient', + }) + ).rejects.toThrow('response missing protocol or credential'); + }); }); }); diff --git a/packages/atxp-common/src/atxpAccount.ts b/packages/atxp-common/src/atxpAccount.ts index 1bad46c..1dccbc6 100644 --- a/packages/atxp-common/src/atxpAccount.ts +++ b/packages/atxp-common/src/atxpAccount.ts @@ -316,6 +316,10 @@ export class ATXPAccount implements Account { * Calls /authorize/auto and returns an opaque credential. */ async authorize(params: AuthorizeParams): Promise { + if (!params.protocols || params.protocols.length === 0) { + throw new Error('ATXPAccount: protocols array must not be empty'); + } + const authHeaders: Record = { 'Content-Type': 'application/json', 'Authorization': toBasicAuth(this.token), @@ -323,13 +327,16 @@ export class ATXPAccount implements Account { const body: Record = { protocols: params.protocols, - amount: params.amount.toString(), - currency: 'USDC', - receiver: params.destination, - memo: params.memo, - paymentRequirements: params.paymentRequirements, - challenge: params.challenge, }; + // ATXP fields + if (params.amount) body.amount = params.amount.toString(); + if (params.destination) body.receiver = params.destination; + if (params.memo) body.memo = params.memo; + body.currency = 'USDC'; + // X402 fields + if (params.paymentRequirements) body.paymentRequirements = params.paymentRequirements; + // MPP fields + if (params.challenge) body.challenge = params.challenge; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); @@ -360,6 +367,14 @@ export class ATXPAccount implements Account { } const responseBody = await response.json() as { protocol: string; credential: string }; + + if (!responseBody || typeof responseBody.protocol !== 'string' || typeof responseBody.credential !== 'string') { + throw new AuthorizationError( + 'ATXPAccount: /authorize/auto response missing protocol or credential', + 500, 'malformed_response' + ); + } + const protocol = responseBody.protocol as PaymentProtocol; let credential: string; diff --git a/packages/atxp-common/src/types.ts b/packages/atxp-common/src/types.ts index b674763..cdffe9b 100644 --- a/packages/atxp-common/src/types.ts +++ b/packages/atxp-common/src/types.ts @@ -198,8 +198,8 @@ export interface PaymentDestination { export interface AuthorizeParams { protocols: PaymentProtocol[]; - amount: BigNumber; - destination: string; + amount?: BigNumber; + destination?: string; memo?: string; /** X402: payment requirements from server challenge */ paymentRequirements?: unknown; diff --git a/packages/atxp-polygon/src/polygonBrowserAccount.ts b/packages/atxp-polygon/src/polygonBrowserAccount.ts index 1a0b2f6..95dd7b3 100644 --- a/packages/atxp-polygon/src/polygonBrowserAccount.ts +++ b/packages/atxp-polygon/src/polygonBrowserAccount.ts @@ -1,4 +1,4 @@ -import type { Account, PaymentMaker, AccountId, Source, AuthorizeParams, AuthorizeResult, Destination } from '@atxp/common'; +import type { Account, PaymentMaker, AccountId, Source, AuthorizeParams, AuthorizeResult, Destination, PaymentProtocol } from '@atxp/common'; import { WalletTypeEnum, ChainEnum } from '@atxp/common'; import { BigNumber } from 'bignumber.js'; import { DirectWalletPaymentMaker, type MainWalletProvider } from './directWalletPaymentMaker.js'; @@ -124,7 +124,10 @@ export class PolygonBrowserAccount implements Account { * Authorize a payment through the appropriate channel for Polygon browser accounts. */ async authorize(params: AuthorizeParams): Promise { - const supported: string[] = ['atxp']; + if (!params.protocols || params.protocols.length === 0) { + throw new Error('PolygonBrowserAccount: protocols array must not be empty'); + } + const supported: PaymentProtocol[] = ['atxp']; const protocol = params.protocols.find(p => supported.includes(p)); if (!protocol) { throw new Error(`PolygonBrowserAccount does not support any of: ${params.protocols.join(', ')}`); diff --git a/packages/atxp-polygon/src/polygonServerAccount.ts b/packages/atxp-polygon/src/polygonServerAccount.ts index 3dc4568..a6e6a16 100644 --- a/packages/atxp-polygon/src/polygonServerAccount.ts +++ b/packages/atxp-polygon/src/polygonServerAccount.ts @@ -1,4 +1,4 @@ -import type { Account, PaymentMaker, Source, AuthorizeParams, AuthorizeResult, Destination } from '@atxp/common'; +import type { Account, PaymentMaker, Source, AuthorizeParams, AuthorizeResult, Destination, PaymentProtocol } from '@atxp/common'; import type { AccountId } from '@atxp/common'; import { ChainEnum, WalletTypeEnum } from '@atxp/common'; import { BigNumber } from 'bignumber.js'; @@ -110,7 +110,10 @@ export class PolygonServerAccount implements Account { * Authorize a payment through the appropriate channel for Polygon server accounts. */ async authorize(params: AuthorizeParams): Promise { - const supported: string[] = ['atxp']; + if (!params.protocols || params.protocols.length === 0) { + throw new Error('PolygonServerAccount: protocols array must not be empty'); + } + const supported: PaymentProtocol[] = ['atxp']; const protocol = params.protocols.find(p => supported.includes(p)); if (!protocol) { throw new Error(`PolygonServerAccount does not support any of: ${params.protocols.join(', ')}`); diff --git a/packages/atxp-solana/src/solanaAccount.ts b/packages/atxp-solana/src/solanaAccount.ts index 70736bf..66ffaec 100644 --- a/packages/atxp-solana/src/solanaAccount.ts +++ b/packages/atxp-solana/src/solanaAccount.ts @@ -1,5 +1,5 @@ import type { Account, PaymentMaker } from '@atxp/client'; -import type { AccountId, Source, AuthorizeParams, AuthorizeResult, Destination } from '@atxp/common'; +import type { AccountId, Source, AuthorizeParams, AuthorizeResult, Destination, PaymentProtocol } from '@atxp/common'; import { BigNumber } from 'bignumber.js'; import { SolanaPaymentMaker } from './solanaPaymentMaker.js'; import { Keypair } from "@solana/web3.js"; @@ -57,7 +57,10 @@ export class SolanaAccount implements Account { * Authorize a payment through the appropriate channel for Solana accounts. */ async authorize(params: AuthorizeParams): Promise { - const supported: string[] = ['atxp']; + if (!params.protocols || params.protocols.length === 0) { + throw new Error('SolanaAccount: protocols array must not be empty'); + } + const supported: PaymentProtocol[] = ['atxp']; const protocol = params.protocols.find(p => supported.includes(p)); if (!protocol) { throw new Error(`SolanaAccount does not support any of: ${params.protocols.join(', ')}`); @@ -75,4 +78,4 @@ export class SolanaAccount implements Account { } return { protocol, credential: JSON.stringify(result) }; } -} \ No newline at end of file +} diff --git a/packages/atxp-tempo/src/tempoAccount.ts b/packages/atxp-tempo/src/tempoAccount.ts index 01d55da..6042110 100644 --- a/packages/atxp-tempo/src/tempoAccount.ts +++ b/packages/atxp-tempo/src/tempoAccount.ts @@ -1,5 +1,5 @@ import type { Account, PaymentMaker, Hex } from '@atxp/client'; -import type { AccountId, Source, AuthorizeParams, AuthorizeResult, Destination } from '@atxp/common'; +import type { AccountId, Source, AuthorizeParams, AuthorizeResult, Destination, PaymentProtocol } from '@atxp/common'; import { BigNumber } from 'bignumber.js'; import { privateKeyToAccount, PrivateKeyAccount } from 'viem/accounts'; import { TempoPaymentMaker } from './tempoPaymentMaker.js'; @@ -76,7 +76,10 @@ export class TempoAccount implements Account { * Authorize a payment through the appropriate channel for Tempo accounts. */ async authorize(params: AuthorizeParams): Promise { - const supported: string[] = ['mpp']; + if (!params.protocols || params.protocols.length === 0) { + throw new Error('TempoAccount: protocols array must not be empty'); + } + const supported: PaymentProtocol[] = ['mpp']; const protocol = params.protocols.find(p => supported.includes(p)); if (!protocol) { throw new Error(`TempoAccount does not support any of: ${params.protocols.join(', ')}`); diff --git a/packages/atxp-worldchain/src/worldchainAccount.ts b/packages/atxp-worldchain/src/worldchainAccount.ts index 038687f..13d10a2 100644 --- a/packages/atxp-worldchain/src/worldchainAccount.ts +++ b/packages/atxp-worldchain/src/worldchainAccount.ts @@ -1,4 +1,4 @@ -import type { Account, PaymentMaker, AccountId, Source, AuthorizeParams, AuthorizeResult, Destination } from '@atxp/common'; +import type { Account, PaymentMaker, AccountId, Source, AuthorizeParams, AuthorizeResult, Destination, PaymentProtocol } from '@atxp/common'; import { WalletTypeEnum, ChainEnum } from '@atxp/common'; import { BigNumber } from 'bignumber.js'; import { @@ -232,7 +232,10 @@ export class WorldchainAccount implements Account { * Authorize a payment through the appropriate channel for World Chain accounts. */ async authorize(params: AuthorizeParams): Promise { - const supported: string[] = ['atxp']; + if (!params.protocols || params.protocols.length === 0) { + throw new Error('WorldchainAccount: protocols array must not be empty'); + } + const supported: PaymentProtocol[] = ['atxp']; const protocol = params.protocols.find(p => supported.includes(p)); if (!protocol) { throw new Error(`WorldchainAccount does not support any of: ${params.protocols.join(', ')}`); @@ -251,4 +254,4 @@ export class WorldchainAccount implements Account { } return { protocol, credential: JSON.stringify(result) }; } -} \ No newline at end of file +} From 761934bf54824bdc9e9f29296bde3fdb05fb6616 Mon Sep 17 00:00:00 2001 From: bdj Date: Wed, 1 Apr 2026 14:08:59 -0700 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20review=20round=202=20=E2=80=94=20tes?= =?UTF-8?q?ts,=20validation,=20stale=20references?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add paymentHeaders.test.ts with 6 tests for buildPaymentHeaders - Validate amount/destination in all local account authorize() paths (prevents silent NaN from new BigNumber(undefined)) - Fix stale PaymentClient comment in protocol.ts - Fix stale /authorize/mpp log message in mppProtocolHandler.ts Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/atxp-base/src/baseAccount.ts | 6 ++ packages/atxp-base/src/baseAppAccount.ts | 6 ++ .../atxp-client/src/mppProtocolHandler.ts | 2 +- .../atxp-client/src/paymentHeaders.test.ts | 57 +++++++++++++++++++ .../atxp-polygon/src/polygonBrowserAccount.ts | 6 ++ .../atxp-polygon/src/polygonServerAccount.ts | 6 ++ packages/atxp-server/src/protocol.ts | 2 +- packages/atxp-solana/src/solanaAccount.ts | 6 ++ packages/atxp-tempo/src/tempoAccount.ts | 6 ++ .../atxp-worldchain/src/worldchainAccount.ts | 6 ++ 10 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 packages/atxp-client/src/paymentHeaders.test.ts diff --git a/packages/atxp-base/src/baseAccount.ts b/packages/atxp-base/src/baseAccount.ts index 4df90f8..35b2c74 100644 --- a/packages/atxp-base/src/baseAccount.ts +++ b/packages/atxp-base/src/baseAccount.ts @@ -98,6 +98,12 @@ export class BaseAccount implements Account { return { protocol, credential: paymentHeader as string }; } case 'atxp': { + if (!params.amount) { + throw new Error('BaseAccount: amount is required for atxp authorize'); + } + if (!params.destination) { + throw new Error('BaseAccount: destination is required for atxp authorize'); + } const destination: Destination = { chain: 'base', currency: 'USDC', diff --git a/packages/atxp-base/src/baseAppAccount.ts b/packages/atxp-base/src/baseAppAccount.ts index e97104d..6284f1c 100644 --- a/packages/atxp-base/src/baseAppAccount.ts +++ b/packages/atxp-base/src/baseAppAccount.ts @@ -238,6 +238,12 @@ export class BaseAppAccount implements Account { throw new Error(`BaseAppAccount does not support any of: ${params.protocols.join(', ')}`); } + if (!params.amount) { + throw new Error('BaseAppAccount: amount is required for atxp authorize'); + } + if (!params.destination) { + throw new Error('BaseAppAccount: destination is required for atxp authorize'); + } const chain = this.chainId === 84532 ? ChainEnum.BaseSepolia : ChainEnum.Base; const destination: Destination = { chain, diff --git a/packages/atxp-client/src/mppProtocolHandler.ts b/packages/atxp-client/src/mppProtocolHandler.ts index 1d14472..250ef1e 100644 --- a/packages/atxp-client/src/mppProtocolHandler.ts +++ b/packages/atxp-client/src/mppProtocolHandler.ts @@ -174,7 +174,7 @@ export class MPPProtocolHandler implements ProtocolHandler { // AuthorizationError = server rejected the request (HTTP error from accounts) // Other errors = data validation or network failure if (authorizeError instanceof AuthorizationError) { - logger.debug(`MPP: /authorize/mpp rejected (${authorizeError.statusCode}), returning original response`); + logger.debug(`MPP: authorize rejected (${authorizeError.statusCode}), returning original response`); return this.reconstructResponse(bodyText, originalResponse); } throw authorizeError; diff --git a/packages/atxp-client/src/paymentHeaders.test.ts b/packages/atxp-client/src/paymentHeaders.test.ts new file mode 100644 index 0000000..9261332 --- /dev/null +++ b/packages/atxp-client/src/paymentHeaders.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest'; +import { buildPaymentHeaders } from './paymentHeaders.js'; +import type { AuthorizeResult } from './paymentHeaders.js'; + +describe('buildPaymentHeaders', () => { + it('x402: sets X-PAYMENT header and Access-Control-Expose-Headers', () => { + const result: AuthorizeResult = { protocol: 'x402', credential: 'x402-cred-abc' }; + const headers = buildPaymentHeaders(result); + + expect(headers.get('X-PAYMENT')).toBe('x402-cred-abc'); + expect(headers.get('Access-Control-Expose-Headers')).toBe('X-PAYMENT-RESPONSE'); + }); + + it('mpp: sets Authorization: Payment header', () => { + const result: AuthorizeResult = { protocol: 'mpp', credential: 'mpp-token-123' }; + const headers = buildPaymentHeaders(result); + + expect(headers.get('Authorization')).toBe('Payment mpp-token-123'); + }); + + it('atxp: does not set payment headers (no-op)', () => { + const result: AuthorizeResult = { protocol: 'atxp', credential: 'atxp-cred' }; + const headers = buildPaymentHeaders(result); + + expect(headers.get('X-PAYMENT')).toBeNull(); + expect(headers.get('Authorization')).toBeNull(); + expect(headers.get('Access-Control-Expose-Headers')).toBeNull(); + }); + + it('preserves existing Headers object', () => { + const original = new Headers({ 'Content-Type': 'application/json', 'X-Custom': 'keep-me' }); + const result: AuthorizeResult = { protocol: 'x402', credential: 'cred' }; + const headers = buildPaymentHeaders(result, original); + + expect(headers.get('Content-Type')).toBe('application/json'); + expect(headers.get('X-Custom')).toBe('keep-me'); + expect(headers.get('X-PAYMENT')).toBe('cred'); + }); + + it('preserves plain object headers', () => { + const original = { 'Content-Type': 'text/plain', 'Accept': 'application/json' }; + const result: AuthorizeResult = { protocol: 'mpp', credential: 'tok' }; + const headers = buildPaymentHeaders(result, original); + + expect(headers.get('Content-Type')).toBe('text/plain'); + expect(headers.get('Accept')).toBe('application/json'); + expect(headers.get('Authorization')).toBe('Payment tok'); + }); + + it('handles undefined original headers', () => { + const result: AuthorizeResult = { protocol: 'x402', credential: 'val' }; + const headers = buildPaymentHeaders(result, undefined); + + expect(headers.get('X-PAYMENT')).toBe('val'); + expect(headers.get('Access-Control-Expose-Headers')).toBe('X-PAYMENT-RESPONSE'); + }); +}); diff --git a/packages/atxp-polygon/src/polygonBrowserAccount.ts b/packages/atxp-polygon/src/polygonBrowserAccount.ts index 95dd7b3..9c36644 100644 --- a/packages/atxp-polygon/src/polygonBrowserAccount.ts +++ b/packages/atxp-polygon/src/polygonBrowserAccount.ts @@ -133,6 +133,12 @@ export class PolygonBrowserAccount implements Account { throw new Error(`PolygonBrowserAccount does not support any of: ${params.protocols.join(', ')}`); } + if (!params.amount) { + throw new Error('PolygonBrowserAccount: amount is required for atxp authorize'); + } + if (!params.destination) { + throw new Error('PolygonBrowserAccount: destination is required for atxp authorize'); + } const destination: Destination = { chain: ChainEnum.Polygon, currency: 'USDC', diff --git a/packages/atxp-polygon/src/polygonServerAccount.ts b/packages/atxp-polygon/src/polygonServerAccount.ts index a6e6a16..a53fb17 100644 --- a/packages/atxp-polygon/src/polygonServerAccount.ts +++ b/packages/atxp-polygon/src/polygonServerAccount.ts @@ -119,6 +119,12 @@ export class PolygonServerAccount implements Account { throw new Error(`PolygonServerAccount does not support any of: ${params.protocols.join(', ')}`); } + if (!params.amount) { + throw new Error('PolygonServerAccount: amount is required for atxp authorize'); + } + if (!params.destination) { + throw new Error('PolygonServerAccount: destination is required for atxp authorize'); + } const chain = this.chainId === 137 ? ChainEnum.Polygon : ChainEnum.PolygonAmoy; const destination: Destination = { chain, diff --git a/packages/atxp-server/src/protocol.ts b/packages/atxp-server/src/protocol.ts index 078f71c..6c72e4c 100644 --- a/packages/atxp-server/src/protocol.ts +++ b/packages/atxp-server/src/protocol.ts @@ -188,7 +188,7 @@ export class ProtocolSettlement { } // ATXP: auth expects { sourceAccountId, destinationAccountId, sourceAccountToken, options } - // The credential is a self-contained JSON string from PaymentClient/ATXPAccount.authorize() + // The credential is a self-contained JSON string from ATXPAccount.authorize() // containing sourceAccountId, sourceAccountToken, and options. // destinationAccountId comes from this instance's config (it's the server's own account). let parsed: Record = {}; diff --git a/packages/atxp-solana/src/solanaAccount.ts b/packages/atxp-solana/src/solanaAccount.ts index 66ffaec..aeb01e9 100644 --- a/packages/atxp-solana/src/solanaAccount.ts +++ b/packages/atxp-solana/src/solanaAccount.ts @@ -66,6 +66,12 @@ export class SolanaAccount implements Account { throw new Error(`SolanaAccount does not support any of: ${params.protocols.join(', ')}`); } + if (!params.amount) { + throw new Error('SolanaAccount: amount is required for atxp authorize'); + } + if (!params.destination) { + throw new Error('SolanaAccount: destination is required for atxp authorize'); + } const destination: Destination = { chain: 'solana', currency: 'USDC', diff --git a/packages/atxp-tempo/src/tempoAccount.ts b/packages/atxp-tempo/src/tempoAccount.ts index 6042110..294b33c 100644 --- a/packages/atxp-tempo/src/tempoAccount.ts +++ b/packages/atxp-tempo/src/tempoAccount.ts @@ -85,6 +85,12 @@ export class TempoAccount implements Account { throw new Error(`TempoAccount does not support any of: ${params.protocols.join(', ')}`); } + if (!params.amount) { + throw new Error('TempoAccount: amount is required for mpp authorize'); + } + if (!params.destination) { + throw new Error('TempoAccount: destination is required for mpp authorize'); + } const destination: Destination = { chain: 'tempo', currency: 'USDC', diff --git a/packages/atxp-worldchain/src/worldchainAccount.ts b/packages/atxp-worldchain/src/worldchainAccount.ts index 13d10a2..9939984 100644 --- a/packages/atxp-worldchain/src/worldchainAccount.ts +++ b/packages/atxp-worldchain/src/worldchainAccount.ts @@ -241,6 +241,12 @@ export class WorldchainAccount implements Account { throw new Error(`WorldchainAccount does not support any of: ${params.protocols.join(', ')}`); } + if (!params.amount) { + throw new Error('WorldchainAccount: amount is required for atxp authorize'); + } + if (!params.destination) { + throw new Error('WorldchainAccount: destination is required for atxp authorize'); + } const chain = this.chainId === 11155420 ? ChainEnum.WorldSepolia : ChainEnum.World; const destination: Destination = { chain,