@agirails/sdk 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +12 -14
- package/dist/ACTPClient.d.ts +8 -11
- package/dist/ACTPClient.d.ts.map +1 -1
- package/dist/ACTPClient.js +79 -20
- package/dist/ACTPClient.js.map +1 -1
- package/dist/__tests__/helpers/mockX402Server.d.ts +67 -0
- package/dist/__tests__/helpers/mockX402Server.d.ts.map +1 -0
- package/dist/__tests__/helpers/mockX402Server.js +121 -0
- package/dist/__tests__/helpers/mockX402Server.js.map +1 -0
- package/dist/adapters/BaseAdapter.d.ts +7 -1
- package/dist/adapters/BaseAdapter.d.ts.map +1 -1
- package/dist/adapters/BaseAdapter.js +11 -6
- package/dist/adapters/BaseAdapter.js.map +1 -1
- package/dist/adapters/BasicAdapter.d.ts.map +1 -1
- package/dist/adapters/BasicAdapter.js +12 -2
- package/dist/adapters/BasicAdapter.js.map +1 -1
- package/dist/adapters/StandardAdapter.d.ts.map +1 -1
- package/dist/adapters/StandardAdapter.js +12 -2
- package/dist/adapters/StandardAdapter.js.map +1 -1
- package/dist/adapters/X402Adapter.d.ts +161 -199
- package/dist/adapters/X402Adapter.d.ts.map +1 -1
- package/dist/adapters/X402Adapter.js +603 -414
- package/dist/adapters/X402Adapter.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/builders/DeliveryProofBuilder.d.ts.map +1 -1
- package/dist/builders/DeliveryProofBuilder.js +3 -2
- package/dist/builders/DeliveryProofBuilder.js.map +1 -1
- package/dist/cli/agirails.js +12 -4
- package/dist/cli/agirails.js.map +1 -1
- package/dist/cli/commands/autopublish.js +9 -1
- package/dist/cli/commands/autopublish.js.map +1 -1
- package/dist/cli/commands/config.js +1 -12
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/deploy-env.js +1 -1
- package/dist/cli/commands/deploy-env.js.map +1 -1
- package/dist/cli/commands/diff.js +38 -4
- package/dist/cli/commands/diff.js.map +1 -1
- package/dist/cli/commands/health.js +3 -1
- package/dist/cli/commands/health.js.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +75 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/pay.d.ts.map +1 -1
- package/dist/cli/commands/pay.js +23 -0
- package/dist/cli/commands/pay.js.map +1 -1
- package/dist/cli/commands/publish.js +15 -8
- package/dist/cli/commands/publish.js.map +1 -1
- package/dist/cli/commands/pull.js +3 -1
- package/dist/cli/commands/pull.js.map +1 -1
- package/dist/cli/commands/receipt.d.ts +17 -3
- package/dist/cli/commands/receipt.d.ts.map +1 -1
- package/dist/cli/commands/receipt.js +95 -33
- package/dist/cli/commands/receipt.js.map +1 -1
- package/dist/cli/commands/test.d.ts.map +1 -1
- package/dist/cli/commands/test.js +222 -60
- package/dist/cli/commands/test.js.map +1 -1
- package/dist/cli/commands/tx.js +13 -0
- package/dist/cli/commands/tx.js.map +1 -1
- package/dist/cli/commands/verify.d.ts +14 -0
- package/dist/cli/commands/verify.d.ts.map +1 -0
- package/dist/cli/commands/verify.js +253 -0
- package/dist/cli/commands/verify.js.map +1 -0
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/receiptUpload.d.ts +52 -0
- package/dist/cli/receiptUpload.d.ts.map +1 -0
- package/dist/cli/receiptUpload.js +134 -0
- package/dist/cli/receiptUpload.js.map +1 -0
- package/dist/cli/utils/banner.d.ts +31 -0
- package/dist/cli/utils/banner.d.ts.map +1 -0
- package/dist/cli/utils/banner.js +92 -0
- package/dist/cli/utils/banner.js.map +1 -0
- package/dist/cli/utils/config.d.ts +0 -2
- package/dist/cli/utils/config.d.ts.map +1 -1
- package/dist/cli/utils/config.js +40 -25
- package/dist/cli/utils/config.js.map +1 -1
- package/dist/cli/utils/output.d.ts +2 -0
- package/dist/cli/utils/output.d.ts.map +1 -1
- package/dist/cli/utils/output.js +7 -1
- package/dist/cli/utils/output.js.map +1 -1
- package/dist/cli/utils/share.d.ts +51 -0
- package/dist/cli/utils/share.d.ts.map +1 -0
- package/dist/cli/utils/share.js +133 -0
- package/dist/cli/utils/share.js.map +1 -0
- package/dist/config/agirailsmd.d.ts.map +1 -1
- package/dist/config/agirailsmd.js +2 -1
- package/dist/config/agirailsmd.js.map +1 -1
- package/dist/config/defaults.d.ts +2 -2
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +9 -3
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/networks.d.ts +7 -0
- package/dist/config/networks.d.ts.map +1 -1
- package/dist/config/networks.js +20 -11
- package/dist/config/networks.js.map +1 -1
- package/dist/config/pendingPublish.d.ts.map +1 -1
- package/dist/config/pendingPublish.js +10 -3
- package/dist/config/pendingPublish.js.map +1 -1
- package/dist/config/syncOperations.d.ts.map +1 -1
- package/dist/config/syncOperations.js +4 -2
- package/dist/config/syncOperations.js.map +1 -1
- package/dist/erc8004/ERC8004Bridge.d.ts.map +1 -1
- package/dist/erc8004/ERC8004Bridge.js +0 -1
- package/dist/erc8004/ERC8004Bridge.js.map +1 -1
- package/dist/errors/ACTPError.d.ts +24 -0
- package/dist/errors/ACTPError.d.ts.map +1 -0
- package/dist/errors/ACTPError.js +35 -0
- package/dist/errors/ACTPError.js.map +1 -0
- package/dist/errors/X402Errors.d.ts +106 -0
- package/dist/errors/X402Errors.d.ts.map +1 -0
- package/dist/errors/X402Errors.js +160 -0
- package/dist/errors/X402Errors.js.map +1 -0
- package/dist/errors/index.d.ts +3 -9
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +38 -33
- package/dist/errors/index.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/level0/Provider.d.ts +5 -0
- package/dist/level0/Provider.d.ts.map +1 -1
- package/dist/level0/ServiceDirectory.d.ts.map +1 -1
- package/dist/level0/ServiceDirectory.js +3 -2
- package/dist/level0/ServiceDirectory.js.map +1 -1
- package/dist/level0/provide.d.ts.map +1 -1
- package/dist/level0/provide.js +11 -8
- package/dist/level0/provide.js.map +1 -1
- package/dist/level0/request.d.ts.map +1 -1
- package/dist/level0/request.js +14 -6
- package/dist/level0/request.js.map +1 -1
- package/dist/level1/Agent.d.ts +1 -1
- package/dist/level1/Agent.d.ts.map +1 -1
- package/dist/level1/Agent.js +12 -6
- package/dist/level1/Agent.js.map +1 -1
- package/dist/level1/pricing/PriceCalculator.d.ts.map +1 -1
- package/dist/level1/pricing/PriceCalculator.js +4 -12
- package/dist/level1/pricing/PriceCalculator.js.map +1 -1
- package/dist/protocol/ACTPKernel.d.ts +4 -1
- package/dist/protocol/ACTPKernel.d.ts.map +1 -1
- package/dist/protocol/ACTPKernel.js +2 -1
- package/dist/protocol/ACTPKernel.js.map +1 -1
- package/dist/protocol/EventMonitor.d.ts +27 -1
- package/dist/protocol/EventMonitor.d.ts.map +1 -1
- package/dist/protocol/EventMonitor.js +11 -9
- package/dist/protocol/EventMonitor.js.map +1 -1
- package/dist/protocol/ProofGenerator.d.ts.map +1 -1
- package/dist/protocol/ProofGenerator.js +3 -2
- package/dist/protocol/ProofGenerator.js.map +1 -1
- package/dist/runtime/BlockchainRuntime.d.ts +2 -0
- package/dist/runtime/BlockchainRuntime.d.ts.map +1 -1
- package/dist/runtime/BlockchainRuntime.js +19 -6
- package/dist/runtime/BlockchainRuntime.js.map +1 -1
- package/dist/runtime/MockRuntime.d.ts +3 -2
- package/dist/runtime/MockRuntime.d.ts.map +1 -1
- package/dist/runtime/MockRuntime.js +16 -22
- package/dist/runtime/MockRuntime.js.map +1 -1
- package/dist/runtime/types/MockState.d.ts +4 -0
- package/dist/runtime/types/MockState.d.ts.map +1 -1
- package/dist/runtime/types/MockState.js.map +1 -1
- package/dist/server/buildX402Server.d.ts +131 -0
- package/dist/server/buildX402Server.d.ts.map +1 -0
- package/dist/server/buildX402Server.js +151 -0
- package/dist/server/buildX402Server.js.map +1 -0
- package/dist/server/index.d.ts +33 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +36 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types/adapter.d.ts +42 -12
- package/dist/types/adapter.d.ts.map +1 -1
- package/dist/types/adapter.js +6 -1
- package/dist/types/adapter.js.map +1 -1
- package/dist/types/eip712.d.ts +20 -0
- package/dist/types/eip712.d.ts.map +1 -1
- package/dist/types/x402.d.ts +8 -8
- package/dist/utils/security.d.ts.map +1 -1
- package/dist/utils/security.js +4 -6
- package/dist/utils/security.js.map +1 -1
- package/dist/wallet/AutoWalletProvider.d.ts +45 -1
- package/dist/wallet/AutoWalletProvider.d.ts.map +1 -1
- package/dist/wallet/AutoWalletProvider.js +154 -1
- package/dist/wallet/AutoWalletProvider.js.map +1 -1
- package/dist/wallet/EOAWalletProvider.d.ts +13 -1
- package/dist/wallet/EOAWalletProvider.d.ts.map +1 -1
- package/dist/wallet/EOAWalletProvider.js +24 -0
- package/dist/wallet/EOAWalletProvider.js.map +1 -1
- package/dist/wallet/IWalletProvider.d.ts +34 -0
- package/dist/wallet/IWalletProvider.d.ts.map +1 -1
- package/dist/wallet/SmartWalletRouter.d.ts.map +1 -1
- package/dist/wallet/SmartWalletRouter.js +3 -1
- package/dist/wallet/SmartWalletRouter.js.map +1 -1
- package/dist/wallet/aa/BundlerClient.js +8 -4
- package/dist/wallet/aa/BundlerClient.js.map +1 -1
- package/dist/wallet/aa/DualNonceManager.d.ts +4 -1
- package/dist/wallet/aa/DualNonceManager.d.ts.map +1 -1
- package/dist/wallet/aa/DualNonceManager.js +3 -0
- package/dist/wallet/aa/DualNonceManager.js.map +1 -1
- package/dist/wallet/keystore.d.ts.map +1 -1
- package/dist/wallet/keystore.js +6 -4
- package/dist/wallet/keystore.js.map +1 -1
- package/package.json +16 -3
- package/dist/adapters/BeginnerAdapter.d.ts +0 -152
- package/dist/adapters/BeginnerAdapter.d.ts.map +0 -1
- package/dist/adapters/BeginnerAdapter.js +0 -168
- package/dist/adapters/BeginnerAdapter.js.map +0 -1
- package/dist/adapters/IntermediateAdapter.d.ts +0 -211
- package/dist/adapters/IntermediateAdapter.d.ts.map +0 -1
- package/dist/adapters/IntermediateAdapter.js +0 -260
- package/dist/adapters/IntermediateAdapter.js.map +0 -1
|
@@ -1,511 +1,700 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* X402Adapter
|
|
3
|
+
* X402Adapter — real x402 v2 protocol support.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Thin wrapper around official @x402/fetch + @x402/evm + @x402/core packages.
|
|
6
|
+
* Replaces the legacy custom `x-payment-*` HTTP flow (which was never real x402)
|
|
7
|
+
* with proper EIP-3009 / Permit2 wire format for full interoperability with
|
|
8
|
+
* any x402 v2 seller (Coinbase demo, third-party servers, AGIRAILS sellers).
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
10
|
+
* Architecture:
|
|
11
|
+
* - Buyer signs EIP-3009 authorization or Permit2 witness OFF-CHAIN
|
|
12
|
+
* - Facilitator (server-configured) submits on-chain tx and pays gas
|
|
13
|
+
* - Buyer is always gassless for x402 by protocol design
|
|
14
|
+
* - Smart Wallet buyers use Permit2 path (ERC-1271 + ERC-6492 via viem)
|
|
15
|
+
* - EOA buyers use either path
|
|
11
16
|
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
* - Instant delivery (response IS the delivery)
|
|
15
|
-
* - Low-value, high-frequency transactions
|
|
17
|
+
* Zero fee layer: payTo goes directly to seller. X402Relay is never used.
|
|
18
|
+
* Zero reputation hooks: ERC-8004 registry never touched on x402 payments.
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* - High-value transactions needing dispute protection
|
|
20
|
-
* - Multi-step deliveries
|
|
20
|
+
* Architecture rationale and Smart Wallet signing flow are documented in
|
|
21
|
+
* the project's internal x402 v2 implementation notes.
|
|
21
22
|
*
|
|
22
23
|
* @module adapters/X402Adapter
|
|
23
24
|
*/
|
|
24
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
26
|
exports.X402Adapter = void 0;
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// ============================================================================
|
|
27
|
+
const fetch_1 = require("@x402/fetch");
|
|
28
|
+
const evm_1 = require("@x402/evm");
|
|
29
|
+
const X402Errors_1 = require("../errors/X402Errors");
|
|
30
|
+
const Logger_1 = require("../utils/Logger");
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* Key characteristics:
|
|
35
|
-
* - usesEscrow: false (direct payment)
|
|
36
|
-
* - supportsDisputes: false (atomic = final)
|
|
37
|
-
* - settlementMode: 'atomic' (instant)
|
|
38
|
-
* - releaseRequired: false (no escrow to release)
|
|
32
|
+
* Default networks if allowedNetworks is undefined.
|
|
39
33
|
*
|
|
40
|
-
* @
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
* Hand-maintained because @x402/core exposes `Network` as a structural string
|
|
35
|
+
* pattern (`${string}:${string}`), not a canonical enum. Review on each
|
|
36
|
+
* @x402/evm upgrade to keep in sync with upstream scheme support.
|
|
37
|
+
*/
|
|
38
|
+
const DEFAULT_EVM_NETWORKS = [
|
|
39
|
+
'eip155:1', // Ethereum mainnet
|
|
40
|
+
'eip155:8453', // Base mainnet
|
|
41
|
+
'eip155:84532', // Base Sepolia
|
|
42
|
+
'eip155:10', // Optimism
|
|
43
|
+
'eip155:42161', // Arbitrum One
|
|
44
|
+
'eip155:137', // Polygon
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Canonical USDC contract address per supported EVM network (CAIP-2 keys).
|
|
50
48
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* });
|
|
49
|
+
* Source: Circle's official deployment list as of 2026-04. Keep in sync with
|
|
50
|
+
* `DEFAULT_EVM_NETWORKS`. Used as the default `allowedAssets` allowlist so
|
|
51
|
+
* the adapter refuses to sign payments in any other token.
|
|
55
52
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* console.log(result.releaseRequired); // false
|
|
59
|
-
* ```
|
|
53
|
+
* Addresses stored lowercase so comparisons can use `.toLowerCase()` on
|
|
54
|
+
* server-provided asset addresses directly.
|
|
60
55
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
56
|
+
const DEFAULT_USDC_BY_NETWORK = {
|
|
57
|
+
'eip155:1': '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // Ethereum USDC
|
|
58
|
+
'eip155:8453': '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', // Base USDC
|
|
59
|
+
'eip155:84532': '0x036cbd53842c5426634e7929541ec2318f3dcf7e', // Base Sepolia USDC
|
|
60
|
+
'eip155:10': '0x0b2c639c533813f4aa9d7837caf62653d097ff85', // Optimism USDC
|
|
61
|
+
'eip155:42161': '0xaf88d065e77c8cc2239327c5edb3a432268e5831', // Arbitrum USDC
|
|
62
|
+
'eip155:137': '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359', // Polygon USDC
|
|
63
|
+
};
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// X402Adapter
|
|
66
|
+
// ============================================================================
|
|
67
|
+
class X402Adapter {
|
|
68
|
+
constructor(config) {
|
|
70
69
|
this.config = config;
|
|
71
|
-
/**
|
|
72
|
-
* Adapter metadata - atomic, no escrow.
|
|
73
|
-
*/
|
|
74
70
|
this.metadata = {
|
|
75
71
|
id: 'x402',
|
|
76
|
-
name: '
|
|
72
|
+
name: 'x402 v2',
|
|
77
73
|
usesEscrow: false,
|
|
78
74
|
supportsDisputes: false,
|
|
79
75
|
requiresIdentity: false,
|
|
80
|
-
settlementMode: 'atomic',
|
|
76
|
+
settlementMode: 'atomic', // x402 is stateless HTTP; settlement is atomic via facilitator
|
|
81
77
|
priority: 70,
|
|
82
78
|
};
|
|
83
|
-
/**
|
|
79
|
+
/** network:tokenAddress → cached approved state */
|
|
80
|
+
this.permit2ApprovedCache = new Set();
|
|
81
|
+
/** network:tokenAddress → in-flight approve promise (coalesces concurrent callers) */
|
|
82
|
+
this.permit2InflightApprovals = new Map();
|
|
83
|
+
/** Completed payment records for getStatus() lookups. Capped to prevent unbounded growth. */
|
|
84
84
|
this.payments = new Map();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
if (typeof config.walletProvider.signTypedData !== 'function') {
|
|
86
|
+
throw new X402Errors_1.X402ConfigError('X402Adapter requires a walletProvider with signTypedData() support. ' +
|
|
87
|
+
'Both EOAWalletProvider and AutoWalletProvider implement this in @agirails/sdk@3.3.0+.');
|
|
88
|
+
}
|
|
89
|
+
const signer = this.walletProviderToClientEvmSigner(config.walletProvider);
|
|
90
|
+
const scheme = new evm_1.ExactEvmScheme(signer);
|
|
91
|
+
// I1: compute and cache allowed network list once — selectRequirements
|
|
92
|
+
// runs on every payment, so we must avoid re-resolving per-call.
|
|
93
|
+
this.allowedNetworks = resolveAllowedNetworks(config.allowedNetworks);
|
|
94
|
+
// P1-1: resolve allowed assets. Undefined → canonical USDC per network.
|
|
95
|
+
// Empty array → sentinel for "allow any asset" (explicit opt-out).
|
|
96
|
+
// Non-empty array → user-provided override.
|
|
97
|
+
if (config.allowedAssets === undefined) {
|
|
98
|
+
const defaults = this.allowedNetworks
|
|
99
|
+
.map((n) => DEFAULT_USDC_BY_NETWORK[n])
|
|
100
|
+
.filter((a) => typeof a === 'string');
|
|
101
|
+
this.allowedAssetsLc = new Set(defaults.map((a) => a.toLowerCase()));
|
|
102
|
+
}
|
|
103
|
+
else if (config.allowedAssets.length === 0) {
|
|
104
|
+
this.allowedAssetsLc = undefined; // sentinel: any asset
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.allowedAssetsLc = new Set(config.allowedAssets.map((a) => a.toLowerCase()));
|
|
108
|
+
}
|
|
109
|
+
// P1-3: resolve allowed hosts. Default empty = always require opt-in.
|
|
110
|
+
this.allowedHostsLc = new Set((config.allowedHosts ?? []).map((h) => h.toLowerCase()));
|
|
111
|
+
// Build x402 client via fromConfig with all scheme registrations up front,
|
|
112
|
+
// plus our paymentRequirementsSelector. Hook is registered on the returned
|
|
113
|
+
// client instance (fromConfig does not take hooks).
|
|
114
|
+
this.x402 = fetch_1.x402Client.fromConfig({
|
|
115
|
+
schemes: this.allowedNetworks.map((network) => ({ network, client: scheme })),
|
|
116
|
+
paymentRequirementsSelector: this.selectRequirements.bind(this),
|
|
117
|
+
});
|
|
118
|
+
// onBeforePaymentCreation hook runs AFTER selectRequirements picks a
|
|
119
|
+
// requirement and BEFORE signing. We await Permit2 approve for Smart
|
|
120
|
+
// Wallet buyers inside the hook — single roundtrip, no race.
|
|
121
|
+
this.x402.onBeforePaymentCreation(this.beforePaymentCreationHook.bind(this));
|
|
122
|
+
// Wrap global fetch with the configured x402Client. This yields a fetch
|
|
123
|
+
// function that transparently retries on 402 with a signed payment payload.
|
|
124
|
+
this.fetchWithPayment = (0, fetch_1.wrapFetchWithPayment)(config.fetchImpl ?? fetch, this.x402);
|
|
125
|
+
// P1-3: hardened default cap lowered from $10 → $1. Reduces blast radius
|
|
126
|
+
// of accidental `client.pay({to: 'https://...'})` calls. Users who need
|
|
127
|
+
// higher caps must opt in explicitly via config.
|
|
128
|
+
this.maxAmountPerTx = parseUsdcAmount(config.maxAmountPerTx ?? '1');
|
|
129
|
+
this.maxAuthorizationValidSec = config.maxAuthorizationValidSec ?? 300;
|
|
89
130
|
}
|
|
90
131
|
// ==========================================================================
|
|
91
|
-
// IAdapter
|
|
132
|
+
// IAdapter implementation
|
|
92
133
|
// ==========================================================================
|
|
93
134
|
/**
|
|
94
|
-
*
|
|
135
|
+
* STRICT HTTPS ONLY. `http://` is rejected at the canHandle level to prevent
|
|
136
|
+
* MITM interception of signed payment payloads. Integration tests that need
|
|
137
|
+
* localhost use a dedicated test flag (not exposed to end users).
|
|
95
138
|
*
|
|
96
|
-
*
|
|
139
|
+
* P1-3: canHandle returns true for any HTTPS URL so the router can select
|
|
140
|
+
* this adapter, but `validate()` enforces explicit opt-in before payment
|
|
141
|
+
* actually executes. This keeps the declarative "any HTTPS is x402-capable"
|
|
142
|
+
* shape while protecting against accidental auto-pay.
|
|
97
143
|
*/
|
|
98
144
|
canHandle(params) {
|
|
99
|
-
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
const url = new URL(params.to);
|
|
104
|
-
return url.protocol === 'https:';
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
145
|
+
return /^https:\/\//i.test(params.to);
|
|
109
146
|
}
|
|
110
|
-
/**
|
|
111
|
-
* Validate parameters before execution.
|
|
112
|
-
*/
|
|
113
147
|
validate(params) {
|
|
148
|
+
if (!params.to || typeof params.to !== 'string') {
|
|
149
|
+
throw new X402Errors_1.X402ConfigError('x402: params.to must be a non-empty string URL');
|
|
150
|
+
}
|
|
114
151
|
if (!this.canHandle(params)) {
|
|
115
|
-
throw new
|
|
116
|
-
`
|
|
152
|
+
throw new X402Errors_1.X402ConfigError(`x402: refusing non-HTTPS target ${params.to}. Only https:// URLs are supported ` +
|
|
153
|
+
`to prevent MITM interception of signed payment payloads.`);
|
|
154
|
+
}
|
|
155
|
+
// P1-3: Explicit opt-in gate. A URL is allowed to trigger a payment only if:
|
|
156
|
+
// (a) caller set `metadata.paymentMethod === 'x402'`, OR
|
|
157
|
+
// (b) the target host is in the allowedHosts allowlist.
|
|
158
|
+
// Without one of these, throw so an accidental `client.pay({to: 'https://...'})`
|
|
159
|
+
// call doesn't silently cost money.
|
|
160
|
+
const explicitOptIn = params.metadata?.paymentMethod === 'x402';
|
|
161
|
+
let hostAllowed = false;
|
|
162
|
+
if (this.allowedHostsLc.size > 0) {
|
|
163
|
+
try {
|
|
164
|
+
const host = new URL(params.to).hostname.toLowerCase();
|
|
165
|
+
hostAllowed = this.allowedHostsLc.has(host);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// fall through — invalid URL will surface below
|
|
169
|
+
}
|
|
117
170
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
171
|
+
if (!explicitOptIn && !hostAllowed) {
|
|
172
|
+
throw new X402Errors_1.X402ConfigError(`x402: refusing to auto-pay ${params.to}. HTTPS URLs trigger x402 payments ` +
|
|
173
|
+
`only when the caller explicitly opts in. Either:\n` +
|
|
174
|
+
` (a) pass metadata: { paymentMethod: 'x402' } to client.pay(), or\n` +
|
|
175
|
+
` (b) add the host to X402AdapterConfig.allowedHosts.\n` +
|
|
176
|
+
`This safeguard prevents accidental charges from unrelated HTTPS calls.`);
|
|
121
177
|
}
|
|
122
178
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Execute atomic x402 payment flow with full HTTP support.
|
|
125
|
-
*
|
|
126
|
-
* 1. Request endpoint → get 402
|
|
127
|
-
* 2. Parse payment headers
|
|
128
|
-
* 3. Execute atomic USDC transfer
|
|
129
|
-
* 4. Retry with tx hash as proof (same method/headers/body)
|
|
130
|
-
* 5. Return response (settlement complete!)
|
|
131
|
-
*
|
|
132
|
-
* @param params - Payment parameters with optional HTTP method, headers, body
|
|
133
|
-
*/
|
|
134
179
|
async pay(params) {
|
|
135
180
|
this.validate(params);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
(x402Params.body && method !== 'GET' ? 'application/json' : undefined);
|
|
144
|
-
// Step 1: Initial request
|
|
145
|
-
const initialResponse = await this.makeRequest(endpoint, method, requestHeaders, requestBody, contentType);
|
|
146
|
-
// Step 2: Check response status
|
|
147
|
-
if (initialResponse.status !== 402) {
|
|
148
|
-
if (initialResponse.ok) {
|
|
149
|
-
return this.createFreeServiceResult(params, initialResponse);
|
|
181
|
+
Logger_1.sdkLogger.debug('x402 payment attempt', { to: params.to });
|
|
182
|
+
let res;
|
|
183
|
+
try {
|
|
184
|
+
const method = params.httpMethod ?? 'GET';
|
|
185
|
+
const headers = { accept: 'application/json' };
|
|
186
|
+
if (params.httpHeaders) {
|
|
187
|
+
Object.assign(headers, params.httpHeaders);
|
|
150
188
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
requester: this.requesterAddress.toLowerCase(),
|
|
173
|
-
amount: paymentHeaders.amount,
|
|
174
|
-
timestamp: now,
|
|
175
|
-
endpoint,
|
|
176
|
-
feeBreakdown,
|
|
177
|
-
});
|
|
178
|
-
// Step 9: Return result - DONE! No release needed.
|
|
179
|
-
return {
|
|
180
|
-
txId: txHash,
|
|
181
|
-
escrowId: null, // No escrow!
|
|
182
|
-
adapter: this.metadata.id,
|
|
183
|
-
state: 'COMMITTED', // Atomic = immediately settled
|
|
184
|
-
success: true,
|
|
185
|
-
amount: this.formatAmount(paymentHeaders.amount),
|
|
186
|
-
response: serviceResponse,
|
|
187
|
-
releaseRequired: false, // KEY DIFFERENCE from ACTP
|
|
188
|
-
provider: paymentHeaders.paymentAddress.toLowerCase(),
|
|
189
|
-
requester: this.requesterAddress.toLowerCase(),
|
|
190
|
-
deadline: new Date(paymentHeaders.deadline * 1000).toISOString(),
|
|
191
|
-
feeBreakdown,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Serialize request body to string.
|
|
196
|
-
*/
|
|
197
|
-
serializeBody(body, _contentType) {
|
|
198
|
-
if (body === undefined)
|
|
199
|
-
return undefined;
|
|
200
|
-
if (typeof body === 'string')
|
|
201
|
-
return body;
|
|
202
|
-
return JSON.stringify(body);
|
|
189
|
+
if (params.httpBody && typeof params.httpBody === 'string' && !headers['content-type'] && !headers['Content-Type']) {
|
|
190
|
+
headers['content-type'] = 'application/json';
|
|
191
|
+
}
|
|
192
|
+
const fetchInit = { method, headers };
|
|
193
|
+
if (params.httpBody && method !== 'GET' && method !== 'DELETE') {
|
|
194
|
+
fetchInit.body = params.httpBody;
|
|
195
|
+
}
|
|
196
|
+
res = await this.fetchWithPayment(params.to, fetchInit);
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
// Hook aborts bubble up here as "Payment creation aborted: {reason}"
|
|
200
|
+
if (e instanceof Error && /aborted/i.test(e.message)) {
|
|
201
|
+
throw new X402Errors_1.X402PaymentFailedError(`x402 payment aborted before signing: ${e.message}`);
|
|
202
|
+
}
|
|
203
|
+
throw new X402Errors_1.X402PaymentFailedError(`x402 payment failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
204
|
+
}
|
|
205
|
+
if (!res.ok) {
|
|
206
|
+
throw new X402Errors_1.X402PaymentFailedError(`x402 payment returned HTTP ${res.status} ${res.statusText}`);
|
|
207
|
+
}
|
|
208
|
+
const responseHeader = res.headers.get('payment-response');
|
|
209
|
+
return await this.mapToPayResult(res, responseHeader, params);
|
|
203
210
|
}
|
|
204
|
-
/**
|
|
205
|
-
* Get payment status by transaction hash.
|
|
206
|
-
*
|
|
207
|
-
* For atomic payments, status is simple:
|
|
208
|
-
* - If tx exists → SETTLED (atomic = instant settlement)
|
|
209
|
-
*/
|
|
210
211
|
async getStatus(txId) {
|
|
211
212
|
const record = this.payments.get(txId);
|
|
212
213
|
if (!record) {
|
|
213
|
-
throw new Error(`
|
|
214
|
+
throw new Error(`x402 payment ${txId} not found. x402 payments are atomic and stateless; ` +
|
|
215
|
+
`only payments made through this adapter instance are tracked.`);
|
|
214
216
|
}
|
|
217
|
+
// B4: state consistency — pay() returns `UnifiedPayResult.state = 'COMMITTED'`
|
|
218
|
+
// (the type only allows 'COMMITTED' | 'IN_PROGRESS'), so getStatus() must
|
|
219
|
+
// also return 'COMMITTED' for the same tx. Despite the name, for x402 this
|
|
220
|
+
// COMMITTED is effectively SETTLED — facilitator has confirmed on-chain
|
|
221
|
+
// settlement by the time mapToPayResult writes the record here, so there's
|
|
222
|
+
// no work-in-progress phase to worry about. Callers should NOT interpret
|
|
223
|
+
// 'COMMITTED' on an x402 record as "waiting for provider" — release is
|
|
224
|
+
// not required and lifecycle methods throw.
|
|
215
225
|
return {
|
|
216
|
-
state: '
|
|
226
|
+
state: 'COMMITTED',
|
|
217
227
|
canStartWork: false,
|
|
218
228
|
canDeliver: false,
|
|
219
229
|
canRelease: false,
|
|
220
230
|
canDispute: false,
|
|
221
|
-
amount:
|
|
222
|
-
provider: record.
|
|
223
|
-
requester: record.
|
|
231
|
+
amount: formatUsdcAmount(record.amount),
|
|
232
|
+
provider: record.payTo,
|
|
233
|
+
requester: record.payer,
|
|
224
234
|
};
|
|
225
235
|
}
|
|
226
|
-
/**
|
|
227
|
-
* Not applicable for atomic payments.
|
|
228
|
-
* @throws {Error} Always - x402 has no lifecycle
|
|
229
|
-
*/
|
|
230
236
|
async startWork(_txId) {
|
|
231
|
-
throw new Error('
|
|
232
|
-
'
|
|
237
|
+
throw new Error('x402 is stateless — no lifecycle methods. ' +
|
|
238
|
+
'The HTTP response IS the delivery. Use ACTP adapters for stateful transactions.');
|
|
233
239
|
}
|
|
234
|
-
/**
|
|
235
|
-
* Not applicable for atomic payments.
|
|
236
|
-
* @throws {Error} Always - x402 has no lifecycle
|
|
237
|
-
*/
|
|
238
240
|
async deliver(_txId, _proof) {
|
|
239
|
-
throw new Error('
|
|
240
|
-
'The HTTP response IS the delivery. Use ACTP for stateful transactions.');
|
|
241
|
+
throw new Error('x402 is stateless — no lifecycle methods. ' +
|
|
242
|
+
'The HTTP response IS the delivery. Use ACTP adapters for stateful transactions.');
|
|
241
243
|
}
|
|
242
|
-
/**
|
|
243
|
-
* Not applicable for atomic payments.
|
|
244
|
-
* @throws {Error} Always - x402 has no escrow
|
|
245
|
-
*/
|
|
246
244
|
async release(_escrowId, _attestationUID) {
|
|
247
|
-
throw new Error('
|
|
248
|
-
'
|
|
245
|
+
throw new Error('x402 has no escrow to release — payment settles instantly via the facilitator. ' +
|
|
246
|
+
'Use ACTP adapters for escrow-based transactions.');
|
|
249
247
|
}
|
|
250
248
|
// ==========================================================================
|
|
251
|
-
//
|
|
249
|
+
// @x402 hook + selector
|
|
252
250
|
// ==========================================================================
|
|
253
251
|
/**
|
|
254
|
-
*
|
|
252
|
+
* Registered as `onBeforePaymentCreation` hook on the x402Client in the
|
|
253
|
+
* constructor. Runs AFTER selectRequirements has chosen a target and BEFORE
|
|
254
|
+
* the scheme client signs the payload.
|
|
255
|
+
*
|
|
256
|
+
* For Smart Wallet + Permit2 flow, this is where we ensure the one-time
|
|
257
|
+
* Permit2 approve has been submitted and confirmed. The hook is awaited by
|
|
258
|
+
* @x402/core's createPaymentPayload, so we can block signing until the
|
|
259
|
+
* approve lands — no double roundtrip, no race.
|
|
255
260
|
*
|
|
256
|
-
*
|
|
257
|
-
* @param method - HTTP method
|
|
258
|
-
* @param customHeaders - Custom headers from request params
|
|
259
|
-
* @param body - Request body (optional)
|
|
260
|
-
* @param contentType - Content-Type header (optional)
|
|
261
|
-
* @param proofHeaders - Payment proof headers for retry (optional)
|
|
261
|
+
* Return `{ abort: true, reason }` to cancel the payment cleanly.
|
|
262
262
|
*/
|
|
263
|
-
async
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
};
|
|
288
|
-
// Add body for non-GET requests
|
|
289
|
-
if (body && method !== 'GET') {
|
|
290
|
-
init.body = body;
|
|
263
|
+
async beforePaymentCreationHook(ctx) {
|
|
264
|
+
const walletInfo = this.config.walletProvider.getWalletInfo();
|
|
265
|
+
if (walletInfo.tier !== 'auto')
|
|
266
|
+
return; // EOA doesn't need Permit2 approve
|
|
267
|
+
if (this.config.autoApprovePermit2 === false)
|
|
268
|
+
return;
|
|
269
|
+
const reqs = ctx.selectedRequirements;
|
|
270
|
+
const method = reqs.extra
|
|
271
|
+
?.assetTransferMethod;
|
|
272
|
+
if (method !== 'permit2')
|
|
273
|
+
return; // EIP-3009 path doesn't need approve
|
|
274
|
+
// Undeployed Smart Wallets (counterfactual): skip Permit2 approve.
|
|
275
|
+
// The facilitator with deployERC4337WithEIP6492 handles deployment +
|
|
276
|
+
// Permit2 settlement atomically. Attempting approve on an undeployed
|
|
277
|
+
// contract wallet would fail (no code → paymaster rejects UserOp).
|
|
278
|
+
// The on-chain allowance check returns false for undeployed wallets
|
|
279
|
+
// (no code to call), so we detect this via getCode-like heuristic.
|
|
280
|
+
const alreadyApproved = await this.readPermit2AllowanceIsSet(reqs.asset);
|
|
281
|
+
if (!alreadyApproved) {
|
|
282
|
+
// Check if wallet is deployed — if not, skip approve entirely
|
|
283
|
+
const isDeployed = await this.isWalletDeployed();
|
|
284
|
+
if (!isDeployed) {
|
|
285
|
+
Logger_1.sdkLogger.debug('x402: Permit2 approve skipped — Smart Wallet not yet deployed (ERC-6492 facilitator handles atomically)');
|
|
286
|
+
return;
|
|
291
287
|
}
|
|
292
|
-
return await this.fetchFn(url, init);
|
|
293
|
-
}
|
|
294
|
-
finally {
|
|
295
|
-
clearTimeout(timeoutId);
|
|
296
288
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
*/
|
|
301
|
-
parsePaymentHeaders(response) {
|
|
302
|
-
const h = response.headers;
|
|
303
|
-
const requiredHeader = h.get(x402_1.X402_HEADERS.REQUIRED);
|
|
304
|
-
if (requiredHeader?.toLowerCase() !== 'true') {
|
|
305
|
-
throw new x402_1.X402Error(`Missing or invalid ${x402_1.X402_HEADERS.REQUIRED} header`, x402_1.X402ErrorCode.MISSING_HEADERS, response);
|
|
306
|
-
}
|
|
307
|
-
const address = h.get(x402_1.X402_HEADERS.ADDRESS);
|
|
308
|
-
const amount = h.get(x402_1.X402_HEADERS.AMOUNT);
|
|
309
|
-
const network = h.get(x402_1.X402_HEADERS.NETWORK);
|
|
310
|
-
const token = h.get(x402_1.X402_HEADERS.TOKEN);
|
|
311
|
-
const deadline = h.get(x402_1.X402_HEADERS.DEADLINE);
|
|
312
|
-
if (!address) {
|
|
313
|
-
throw new x402_1.X402Error(`Missing ${x402_1.X402_HEADERS.ADDRESS}`, x402_1.X402ErrorCode.MISSING_HEADERS, response);
|
|
314
|
-
}
|
|
315
|
-
if (!amount) {
|
|
316
|
-
throw new x402_1.X402Error(`Missing ${x402_1.X402_HEADERS.AMOUNT}`, x402_1.X402ErrorCode.MISSING_HEADERS, response);
|
|
317
|
-
}
|
|
318
|
-
if (!network) {
|
|
319
|
-
throw new x402_1.X402Error(`Missing ${x402_1.X402_HEADERS.NETWORK}`, x402_1.X402ErrorCode.MISSING_HEADERS, response);
|
|
320
|
-
}
|
|
321
|
-
if (!token) {
|
|
322
|
-
throw new x402_1.X402Error(`Missing ${x402_1.X402_HEADERS.TOKEN}`, x402_1.X402ErrorCode.MISSING_HEADERS, response);
|
|
323
|
-
}
|
|
324
|
-
if (!deadline) {
|
|
325
|
-
throw new x402_1.X402Error(`Missing ${x402_1.X402_HEADERS.DEADLINE}`, x402_1.X402ErrorCode.MISSING_HEADERS, response);
|
|
326
|
-
}
|
|
327
|
-
// Validate address
|
|
328
|
-
const validatedAddress = this.validatePaymentAddress(address, response);
|
|
329
|
-
// Validate amount
|
|
330
|
-
if (!/^\d+$/.test(amount)) {
|
|
331
|
-
throw new x402_1.X402Error(`Invalid ${x402_1.X402_HEADERS.AMOUNT}: "${amount}"`, x402_1.X402ErrorCode.INVALID_AMOUNT, response);
|
|
332
|
-
}
|
|
333
|
-
// Validate network
|
|
334
|
-
if (!(0, x402_1.isValidX402Network)(network)) {
|
|
335
|
-
throw new x402_1.X402Error(`Invalid ${x402_1.X402_HEADERS.NETWORK}: "${network}"`, x402_1.X402ErrorCode.INVALID_NETWORK, response);
|
|
336
|
-
}
|
|
337
|
-
// Validate token
|
|
338
|
-
if (token.toUpperCase() !== 'USDC') {
|
|
339
|
-
throw new x402_1.X402Error(`Unsupported token: "${token}". Only USDC supported.`, x402_1.X402ErrorCode.MISSING_HEADERS, response);
|
|
340
|
-
}
|
|
341
|
-
const deadlineNum = parseInt(deadline, 10);
|
|
342
|
-
if (isNaN(deadlineNum) || deadlineNum <= 0) {
|
|
343
|
-
throw new x402_1.X402Error(`Invalid ${x402_1.X402_HEADERS.DEADLINE}: "${deadline}"`, x402_1.X402ErrorCode.MISSING_HEADERS, response);
|
|
289
|
+
else {
|
|
290
|
+
// Already approved — no action needed
|
|
291
|
+
return;
|
|
344
292
|
}
|
|
345
|
-
return {
|
|
346
|
-
required: true,
|
|
347
|
-
paymentAddress: validatedAddress,
|
|
348
|
-
amount,
|
|
349
|
-
network,
|
|
350
|
-
token: 'USDC',
|
|
351
|
-
deadline: deadlineNum,
|
|
352
|
-
serviceId: h.get(x402_1.X402_HEADERS.SERVICE_ID) ?? undefined,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Validate payment address from header.
|
|
357
|
-
*/
|
|
358
|
-
validatePaymentAddress(address, response) {
|
|
359
293
|
try {
|
|
360
|
-
|
|
294
|
+
await this.ensurePermit2Approved(reqs.network, reqs.asset);
|
|
361
295
|
}
|
|
362
|
-
catch {
|
|
363
|
-
|
|
296
|
+
catch (e) {
|
|
297
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
298
|
+
Logger_1.sdkLogger.warn('x402 Permit2 approve failed, aborting payment', { error: msg });
|
|
299
|
+
return {
|
|
300
|
+
abort: true,
|
|
301
|
+
reason: msg,
|
|
302
|
+
};
|
|
364
303
|
}
|
|
365
304
|
}
|
|
366
305
|
/**
|
|
367
|
-
*
|
|
306
|
+
* Registered as `paymentRequirementsSelector` on the x402Client.
|
|
307
|
+
* Called after server's payment-required is parsed, before signing.
|
|
368
308
|
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
309
|
+
* Picks the best requirement from `accepts[]` based on:
|
|
310
|
+
* 1. scheme === "exact" AND network in our allowlist
|
|
311
|
+
* 2. Wallet tier: Smart Wallet prefers Permit2, EOA prefers EIP-3009
|
|
312
|
+
* 3. Amount within maxAmountPerTx safety cap
|
|
313
|
+
* 4. Clamps maxTimeoutSeconds to maxAuthorizationValidSec (MEV cap)
|
|
372
314
|
*/
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const serviceId = headers.serviceId ?? '0x' + '0'.repeat(64);
|
|
389
|
-
const txHash = await this.config.relayPayFn(headers.paymentAddress, grossAmount, serviceId);
|
|
390
|
-
return {
|
|
391
|
-
txHash,
|
|
392
|
-
feeBreakdown: {
|
|
393
|
-
grossAmount,
|
|
394
|
-
providerNet: providerNet.toString(),
|
|
395
|
-
platformFee: fee.toString(),
|
|
396
|
-
feeBps,
|
|
397
|
-
estimated: true,
|
|
398
|
-
},
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
// feeCollector path: client-side fee splitting (non-atomic fallback)
|
|
402
|
-
if (!this.config.feeCollector) {
|
|
403
|
-
throw new x402_1.X402Error('x402 payment requires fee collection: configure either relayAddress (preferred) or feeCollector. ' +
|
|
404
|
-
'Since v2.6.0, transferFn-only configs are no longer allowed to prevent zero-fee payments.', x402_1.X402ErrorCode.PAYMENT_FAILED);
|
|
405
|
-
}
|
|
406
|
-
const grossAmount = headers.amount;
|
|
407
|
-
const feeBps = this.config.platformFeeBps ?? 100;
|
|
408
|
-
const MIN_FEE = 50000n; // $0.05 USDC
|
|
409
|
-
const grossBig = BigInt(grossAmount);
|
|
410
|
-
// Guard: grossAmount must cover at least the minimum fee
|
|
411
|
-
if (grossBig <= MIN_FEE) {
|
|
412
|
-
throw new x402_1.X402Error(`Payment amount ${grossAmount} too small: must exceed minimum fee of ${MIN_FEE} ($0.05 USDC)`, x402_1.X402ErrorCode.PAYMENT_FAILED);
|
|
315
|
+
selectRequirements(_version, requirements) {
|
|
316
|
+
const allowed = this.allowedNetworks; // I1: cached
|
|
317
|
+
// P1-1: filter by scheme + network + asset. Asset check rejects any
|
|
318
|
+
// token that's not in our allowlist (canonical USDC per chain by default).
|
|
319
|
+
// Without this, a malicious or misconfigured server could advertise
|
|
320
|
+
// USDT/DAI/scam-token and we'd sign a payment with wrong decimals,
|
|
321
|
+
// bypassing the maxAmountPerTx cap silently.
|
|
322
|
+
const candidates = requirements.filter((r) => {
|
|
323
|
+
if (r.scheme !== 'exact')
|
|
324
|
+
return false;
|
|
325
|
+
if (!allowed.includes(r.network))
|
|
326
|
+
return false;
|
|
327
|
+
if (this.allowedAssetsLc) {
|
|
328
|
+
if (typeof r.asset !== 'string' || !this.allowedAssetsLc.has(r.asset.toLowerCase()))
|
|
329
|
+
return false;
|
|
413
330
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
:
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
331
|
+
return true;
|
|
332
|
+
});
|
|
333
|
+
if (candidates.length === 0) {
|
|
334
|
+
const seen = requirements
|
|
335
|
+
.map((r) => `${r.scheme}@${r.network}(${(r.asset ?? '').slice(0, 10)}...)`)
|
|
336
|
+
.join(', ');
|
|
337
|
+
const assetInfo = this.allowedAssetsLc
|
|
338
|
+
? `, allowed assets: [${[...this.allowedAssetsLc].map((a) => a.slice(0, 10) + '...').join(', ')}]`
|
|
339
|
+
: '';
|
|
340
|
+
throw new X402Errors_1.X402NetworkNotAllowedError(`x402: no accepted requirement. Server offered [${seen}], ` +
|
|
341
|
+
`allowed networks: [${allowed.join(', ')}]${assetInfo}.`);
|
|
342
|
+
}
|
|
343
|
+
// Wallet-tier-aware ordering
|
|
344
|
+
const walletInfo = this.config.walletProvider.getWalletInfo();
|
|
345
|
+
const isPermit2 = (r) => r.extra?.assetTransferMethod === 'permit2';
|
|
346
|
+
// Smart Wallet prefers Permit2 (EIP-3009 won't validate on contract addresses).
|
|
347
|
+
// EOA prefers EIP-3009 (simpler, no one-time approve needed).
|
|
348
|
+
const prioritized = walletInfo.tier === 'auto'
|
|
349
|
+
? [...candidates].sort((a, b) => Number(isPermit2(b)) - Number(isPermit2(a)))
|
|
350
|
+
: [...candidates].sort((a, b) => Number(isPermit2(a)) - Number(isPermit2(b)));
|
|
351
|
+
// Note: we do NOT inject assetTransferMethod into requirements here.
|
|
352
|
+
// @x402/evm ExactEvmScheme client auto-selects EIP-3009 vs Permit2
|
|
353
|
+
// based on the signer type (EOA vs contract wallet). Injecting it
|
|
354
|
+
// would cause a requirements mismatch between buyer and server,
|
|
355
|
+
// breaking the facilitator's verify check.
|
|
356
|
+
const chosen = prioritized[0];
|
|
357
|
+
const amountBig = BigInt(chosen.amount);
|
|
358
|
+
if (amountBig > this.maxAmountPerTx) {
|
|
359
|
+
throw new X402Errors_1.X402AmountExceededError(`x402: required amount ${chosen.amount} (${formatUsdcAmount(amountBig)} USD) ` +
|
|
360
|
+
`exceeds maxAmountPerTx ${this.maxAmountPerTx.toString()} ` +
|
|
361
|
+
`(${this.config.maxAmountPerTx ?? '1'} USD).`);
|
|
362
|
+
}
|
|
363
|
+
// MEV hard cap on authorization validity
|
|
364
|
+
const serverTimeout = chosen.maxTimeoutSeconds ?? this.maxAuthorizationValidSec;
|
|
365
|
+
return {
|
|
366
|
+
...chosen,
|
|
367
|
+
maxTimeoutSeconds: Math.min(serverTimeout, this.maxAuthorizationValidSec),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
// ==========================================================================
|
|
371
|
+
// Permit2 approve (lazy, one-time, coalesced)
|
|
372
|
+
// ==========================================================================
|
|
373
|
+
async ensurePermit2Approved(network, token) {
|
|
374
|
+
const key = `${network}:${token.toLowerCase()}`;
|
|
375
|
+
if (this.permit2ApprovedCache.has(key))
|
|
376
|
+
return;
|
|
377
|
+
// Coalesce concurrent calls for the same (network, token).
|
|
378
|
+
// B1 fix: register the inflight promise BEFORE the IIFE's first await
|
|
379
|
+
// so concurrent callers that arrive mid-construction see it and wait
|
|
380
|
+
// instead of launching a duplicate approve.
|
|
381
|
+
const inflight = this.permit2InflightApprovals.get(key);
|
|
382
|
+
if (inflight)
|
|
383
|
+
return await inflight;
|
|
384
|
+
const doApprove = async () => {
|
|
435
385
|
try {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
386
|
+
// P1-2: check on-chain allowance BEFORE sending approve. The in-memory
|
|
387
|
+
// `permit2ApprovedCache` is only a fast path — after a process restart
|
|
388
|
+
// or horizontal scale, the cache is empty but the on-chain allowance
|
|
389
|
+
// may already be set from a prior run. Without this check we'd pay
|
|
390
|
+
// for the approve again.
|
|
391
|
+
const alreadyApproved = await this.readPermit2AllowanceIsSet(token);
|
|
392
|
+
if (alreadyApproved) {
|
|
393
|
+
this.permit2ApprovedCache.add(key);
|
|
394
|
+
Logger_1.sdkLogger.debug('x402 Permit2 approve: allowance already set on-chain, skipping', {
|
|
395
|
+
network,
|
|
396
|
+
token,
|
|
397
|
+
});
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
Logger_1.sdkLogger.info('x402 Permit2 approve: submitting one-time USDC approve', {
|
|
401
|
+
network,
|
|
402
|
+
token,
|
|
403
|
+
});
|
|
404
|
+
// Verified v4.2 spike: createPermit2ApprovalTx takes positional
|
|
405
|
+
// tokenAddress (0x${string}), returns { to, data, value? }.
|
|
406
|
+
const approvalTx = (0, evm_1.createPermit2ApprovalTx)(token);
|
|
407
|
+
const receipt = await this.config.walletProvider.sendTransaction({
|
|
408
|
+
to: approvalTx.to,
|
|
409
|
+
data: approvalTx.data,
|
|
410
|
+
value: '0',
|
|
411
|
+
});
|
|
412
|
+
if (receipt && typeof receipt === 'object' && 'success' in receipt && !receipt.success) {
|
|
413
|
+
throw new X402Errors_1.X402ApprovalFailedError(`Permit2 approve transaction reverted on-chain for ${network}:${token}`);
|
|
442
414
|
}
|
|
415
|
+
this.permit2ApprovedCache.add(key);
|
|
416
|
+
Logger_1.sdkLogger.info('x402 Permit2 approve confirmed', { network, token });
|
|
443
417
|
}
|
|
444
|
-
catch (
|
|
445
|
-
if (
|
|
446
|
-
throw
|
|
447
|
-
|
|
418
|
+
catch (e) {
|
|
419
|
+
if (e instanceof X402Errors_1.X402ApprovalFailedError)
|
|
420
|
+
throw e; // don't double-wrap
|
|
421
|
+
if ((0, X402Errors_1.isPaymasterGateError)(e)) {
|
|
422
|
+
throw new X402Errors_1.X402PublishRequiredError();
|
|
423
|
+
}
|
|
424
|
+
throw new X402Errors_1.X402ApprovalFailedError(`Permit2 approve failed for ${network}:${token}: ${e instanceof Error ? e.message : String(e)}`);
|
|
448
425
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
estimated: false,
|
|
459
|
-
},
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
catch (error) {
|
|
463
|
-
if (error instanceof x402_1.X402Error)
|
|
464
|
-
throw error;
|
|
465
|
-
throw new x402_1.X402Error(`Atomic payment failed: ${error instanceof Error ? error.message : 'Unknown error'}`, x402_1.X402ErrorCode.PAYMENT_FAILED);
|
|
466
|
-
}
|
|
426
|
+
finally {
|
|
427
|
+
this.permit2InflightApprovals.delete(key);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
// Register BEFORE invoking — Promise constructor is synchronous so any
|
|
431
|
+
// concurrent caller that polls `.get(key)` after this line sees it.
|
|
432
|
+
const approvalPromise = doApprove();
|
|
433
|
+
this.permit2InflightApprovals.set(key, approvalPromise);
|
|
434
|
+
return await approvalPromise;
|
|
467
435
|
}
|
|
468
436
|
/**
|
|
469
|
-
*
|
|
470
|
-
*
|
|
437
|
+
* P1-2: Read `USDC.allowance(smartWallet, PERMIT2_ADDRESS)` via the wallet
|
|
438
|
+
* provider's read provider and return true if already >= a sensible threshold
|
|
439
|
+
* (half of max uint256 — Permit2 approves are typically MAX_UINT256).
|
|
440
|
+
*
|
|
441
|
+
* Returns false if the wallet provider doesn't expose a read provider
|
|
442
|
+
* (older wallet implementations) — callers then fall back to submitting
|
|
443
|
+
* the approve unconditionally, which is the safe (but slightly wasteful)
|
|
444
|
+
* default.
|
|
471
445
|
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
* @param method - Original HTTP method
|
|
475
|
-
* @param customHeaders - Original custom headers
|
|
476
|
-
* @param body - Original request body
|
|
477
|
-
* @param contentType - Original content-type
|
|
446
|
+
* Uses eth_call with the standard ERC-20 allowance ABI selector to avoid
|
|
447
|
+
* pulling in additional contract ABIs.
|
|
478
448
|
*/
|
|
479
|
-
async
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
449
|
+
async readPermit2AllowanceIsSet(token) {
|
|
450
|
+
if (typeof this.config.walletProvider.getReadProvider !== 'function') {
|
|
451
|
+
return false; // no read capability — submit approve unconditionally
|
|
452
|
+
}
|
|
453
|
+
const rawProvider = this.config.walletProvider.getReadProvider();
|
|
454
|
+
// Duck-type: ethers v6 provider has `.call({to, data}): Promise<string>`.
|
|
455
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
456
|
+
const providerAny = rawProvider;
|
|
457
|
+
if (!providerAny || typeof providerAny.call !== 'function')
|
|
458
|
+
return false;
|
|
459
|
+
// ERC-20 allowance(address owner, address spender) — selector 0xdd62ed3e
|
|
460
|
+
const owner = this.config.walletProvider.getAddress().toLowerCase().replace(/^0x/, '');
|
|
461
|
+
const spender = evm_1.PERMIT2_ADDRESS.toLowerCase().replace(/^0x/, '');
|
|
462
|
+
const data = '0xdd62ed3e' +
|
|
463
|
+
owner.padStart(64, '0') +
|
|
464
|
+
spender.padStart(64, '0');
|
|
465
|
+
try {
|
|
466
|
+
const result = await providerAny.call({ to: token, data });
|
|
467
|
+
if (!result || result === '0x')
|
|
468
|
+
return false;
|
|
469
|
+
const allowance = BigInt(result);
|
|
470
|
+
// Permit2 approve is typically MAX_UINT256. Treat any value above
|
|
471
|
+
// half-max as "already approved" to tolerate partial-spend scenarios.
|
|
472
|
+
const THRESHOLD = (1n << 255n);
|
|
473
|
+
return allowance >= THRESHOLD;
|
|
474
|
+
}
|
|
475
|
+
catch (e) {
|
|
476
|
+
Logger_1.sdkLogger.debug('x402 allowance read failed, falling back to submit', {
|
|
477
|
+
error: e instanceof Error ? e.message : String(e),
|
|
478
|
+
});
|
|
479
|
+
return false;
|
|
488
480
|
}
|
|
489
|
-
return response;
|
|
490
481
|
}
|
|
491
482
|
/**
|
|
492
|
-
*
|
|
483
|
+
* Check if the Smart Wallet is deployed on-chain (has code).
|
|
484
|
+
* Returns true for EOA wallets (they're always "deployed" in a loose sense)
|
|
485
|
+
* and for deployed Smart Wallets. Returns false for counterfactual wallets.
|
|
493
486
|
*/
|
|
494
|
-
|
|
487
|
+
async isWalletDeployed() {
|
|
488
|
+
if (typeof this.config.walletProvider.getReadProvider !== 'function') {
|
|
489
|
+
return true; // can't check — assume deployed (safe default: attempt approve)
|
|
490
|
+
}
|
|
491
|
+
const rawProvider = this.config.walletProvider.getReadProvider();
|
|
492
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
493
|
+
const providerAny = rawProvider;
|
|
494
|
+
if (!providerAny || typeof providerAny.getCode !== 'function')
|
|
495
|
+
return true;
|
|
496
|
+
try {
|
|
497
|
+
const code = await providerAny.getCode(this.config.walletProvider.getAddress());
|
|
498
|
+
return code !== '0x' && code !== null && code !== undefined && code.length > 2;
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return true; // can't check — assume deployed
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// ==========================================================================
|
|
505
|
+
// Response mapping
|
|
506
|
+
// ==========================================================================
|
|
507
|
+
async mapToPayResult(res, paymentResponseHeader, params) {
|
|
508
|
+
// FIX v4.1: missing payment-response header is NOT silent success.
|
|
509
|
+
// x402 spec: facilitator sets this header ONLY after on-chain settlement
|
|
510
|
+
// is confirmed. Without it we have no settlement proof — reorg, pending
|
|
511
|
+
// mempool, facilitator bug, or malicious server could all be causes.
|
|
512
|
+
if (!paymentResponseHeader) {
|
|
513
|
+
throw new X402Errors_1.X402SettlementProofMissingError();
|
|
514
|
+
}
|
|
515
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
516
|
+
let decoded;
|
|
517
|
+
try {
|
|
518
|
+
decoded = (0, fetch_1.decodePaymentResponseHeader)(paymentResponseHeader);
|
|
519
|
+
}
|
|
520
|
+
catch (e) {
|
|
521
|
+
throw new X402Errors_1.X402SettlementProofMissingError(`Failed to decode payment-response header: ${e instanceof Error ? e.message : String(e)}`);
|
|
522
|
+
}
|
|
523
|
+
const rawTxHash = decoded?.transaction;
|
|
524
|
+
const rawNetwork = decoded?.network;
|
|
525
|
+
const rawPayer = decoded?.payer;
|
|
526
|
+
const payTo = decoded?.payTo;
|
|
527
|
+
const amount = decoded?.amount;
|
|
528
|
+
// P1: Validate critical settlement proof fields instead of falling back
|
|
529
|
+
// to empty values. Without a valid tx hash we have no on-chain proof.
|
|
530
|
+
const missing = [];
|
|
531
|
+
if (!rawTxHash || !/^0x[0-9a-f]{64}$/i.test(rawTxHash))
|
|
532
|
+
missing.push('transaction');
|
|
533
|
+
if (!rawNetwork)
|
|
534
|
+
missing.push('network');
|
|
535
|
+
if (!rawPayer || !/^0x[0-9a-f]{40}$/i.test(rawPayer))
|
|
536
|
+
missing.push('payer');
|
|
537
|
+
// payTo is optional in x402 spec — Coinbase facilitator omits it
|
|
538
|
+
if (missing.length > 0) {
|
|
539
|
+
throw new X402Errors_1.X402SettlementProofMissingError(`payment-response header decoded but missing/invalid fields: ${missing.join(', ')}. ` +
|
|
540
|
+
`Decoded values: transaction=${rawTxHash ?? 'undefined'}, network=${rawNetwork ?? 'undefined'}, ` +
|
|
541
|
+
`payer=${rawPayer ?? 'undefined'}. Do not treat as settled.`);
|
|
542
|
+
}
|
|
543
|
+
// After validation gate, these are guaranteed non-undefined.
|
|
544
|
+
const txHash = rawTxHash;
|
|
545
|
+
const network = rawNetwork;
|
|
546
|
+
const payer = rawPayer;
|
|
547
|
+
// Replay detection: payer must match our wallet address
|
|
548
|
+
const ourAddress = this.config.walletProvider.getAddress().toLowerCase();
|
|
549
|
+
if (payer.toLowerCase() !== ourAddress) {
|
|
550
|
+
throw new X402Errors_1.X402SettlementProofMissingError(`payment-response payer ${payer} does not match our wallet ${ourAddress}. ` +
|
|
551
|
+
`Possible replay of another client's settlement.`);
|
|
552
|
+
}
|
|
553
|
+
const amountBig = safeBigInt(amount ?? '0');
|
|
554
|
+
this.payments.set(txHash, {
|
|
555
|
+
txId: txHash,
|
|
556
|
+
amount: amountBig,
|
|
557
|
+
network,
|
|
558
|
+
payer,
|
|
559
|
+
payTo: payTo ?? '',
|
|
560
|
+
settledAt: Date.now(),
|
|
561
|
+
});
|
|
562
|
+
// Evict oldest entry if over cap to prevent unbounded memory growth
|
|
563
|
+
if (this.payments.size > X402Adapter.MAX_PAYMENT_RECORDS) {
|
|
564
|
+
const oldest = this.payments.keys().next().value;
|
|
565
|
+
if (oldest)
|
|
566
|
+
this.payments.delete(oldest);
|
|
567
|
+
}
|
|
568
|
+
Logger_1.sdkLogger.info('x402 settlement confirmed', {
|
|
569
|
+
txHash,
|
|
570
|
+
network,
|
|
571
|
+
amount: String(amount ?? '0'),
|
|
572
|
+
});
|
|
495
573
|
return {
|
|
496
|
-
txId:
|
|
574
|
+
txId: txHash,
|
|
497
575
|
escrowId: null,
|
|
498
|
-
adapter:
|
|
576
|
+
adapter: 'x402',
|
|
499
577
|
state: 'COMMITTED',
|
|
500
578
|
success: true,
|
|
501
|
-
amount:
|
|
502
|
-
response,
|
|
579
|
+
amount: formatUsdcAmount(amountBig),
|
|
580
|
+
response: res,
|
|
503
581
|
releaseRequired: false,
|
|
504
|
-
provider:
|
|
505
|
-
requester:
|
|
506
|
-
deadline: new Date(
|
|
582
|
+
provider: payTo || params.to,
|
|
583
|
+
requester: payer,
|
|
584
|
+
deadline: new Date().toISOString(), // x402 is atomic — settled at return time, no future deadline
|
|
585
|
+
erc8004AgentId: params.erc8004AgentId,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
// ==========================================================================
|
|
589
|
+
// Helpers
|
|
590
|
+
// ==========================================================================
|
|
591
|
+
/**
|
|
592
|
+
* Bridge our IWalletProvider.signTypedData to @x402/evm's ClientEvmSigner
|
|
593
|
+
* structural type. Only `address` + `signTypedData` are required for the
|
|
594
|
+
* `exact` scheme flow we use.
|
|
595
|
+
*
|
|
596
|
+
* B3: no outer try/catch here — each IWalletProvider implementation is the
|
|
597
|
+
* error-converting boundary and already throws X402SignatureFailedError on
|
|
598
|
+
* failure. Wrapping here would produce double-nested error messages like
|
|
599
|
+
* "walletProvider.signTypedData failed: EOA signTypedData failed: ...".
|
|
600
|
+
*/
|
|
601
|
+
walletProviderToClientEvmSigner(wp) {
|
|
602
|
+
return {
|
|
603
|
+
address: wp.getAddress(),
|
|
604
|
+
async signTypedData(params) {
|
|
605
|
+
const typedData = {
|
|
606
|
+
domain: params.domain,
|
|
607
|
+
types: params.types,
|
|
608
|
+
primaryType: params.primaryType,
|
|
609
|
+
message: params.message,
|
|
610
|
+
};
|
|
611
|
+
const sig = await wp.signTypedData(typedData);
|
|
612
|
+
return sig;
|
|
613
|
+
},
|
|
507
614
|
};
|
|
508
615
|
}
|
|
509
616
|
}
|
|
510
617
|
exports.X402Adapter = X402Adapter;
|
|
618
|
+
X402Adapter.MAX_PAYMENT_RECORDS = 10000;
|
|
619
|
+
// ============================================================================
|
|
620
|
+
// Local helpers
|
|
621
|
+
// ============================================================================
|
|
622
|
+
/**
|
|
623
|
+
* Parse human-readable USD amount like "10" or "0.50" to USDC 6-decimal bigint.
|
|
624
|
+
*/
|
|
625
|
+
function parseUsdcAmount(usd) {
|
|
626
|
+
const trimmed = usd.trim().replace(/^\$/, '');
|
|
627
|
+
if (!/^\d+(\.\d{1,6})?$/.test(trimmed)) {
|
|
628
|
+
throw new X402Errors_1.X402ConfigError(`Invalid maxAmountPerTx "${usd}" — must be a non-negative decimal with at most 6 digits after the point.`);
|
|
629
|
+
}
|
|
630
|
+
const [whole, frac = ''] = trimmed.split('.');
|
|
631
|
+
const fracPadded = (frac + '000000').slice(0, 6);
|
|
632
|
+
return BigInt(whole + fracPadded);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Format USDC 6-decimal bigint back to human-readable USD string.
|
|
636
|
+
*/
|
|
637
|
+
function formatUsdcAmount(amount) {
|
|
638
|
+
const whole = amount / 1000000n;
|
|
639
|
+
const frac = amount % 1000000n;
|
|
640
|
+
if (frac === 0n)
|
|
641
|
+
return whole.toString();
|
|
642
|
+
const fracStr = frac.toString().padStart(6, '0').replace(/0+$/, '');
|
|
643
|
+
return `${whole}.${fracStr}`;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Resolve the effective allowed-network list. Undefined / empty user config
|
|
647
|
+
* means "allow all EVM networks @x402/evm supports" — maximal interop default.
|
|
648
|
+
*/
|
|
649
|
+
function resolveAllowedNetworks(allowed) {
|
|
650
|
+
if (allowed && allowed.length > 0)
|
|
651
|
+
return allowed;
|
|
652
|
+
return DEFAULT_EVM_NETWORKS;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Parse any reasonable amount representation (raw int string, decimal string,
|
|
656
|
+
* bigint, or number) into a USDC 6-decimal bigint.
|
|
657
|
+
*
|
|
658
|
+
* B2 fix: the previous version rejected decimal strings (regex `^\d+$`) and
|
|
659
|
+
* silently returned 0n, which caused zero-amount records when a facilitator
|
|
660
|
+
* reported settlement amounts like `"0.01"` instead of `"10000"`.
|
|
661
|
+
*
|
|
662
|
+
* Behavior:
|
|
663
|
+
* - "10000" → 10000n (raw 6-decimal)
|
|
664
|
+
* - "0.01" → 10000n (parsed as USD, normalized to 6 decimals)
|
|
665
|
+
* - "10" → 10000000n (treated as whole-USD if no decimal)
|
|
666
|
+
* - number 5 → 5000000n
|
|
667
|
+
* - 5n → 5n (bigint passed through)
|
|
668
|
+
*
|
|
669
|
+
* Ambiguity warning: a bare integer string like "10" could be either raw
|
|
670
|
+
* 6-decimal (0.00001 USDC) or whole USD (10 USDC). We use a heuristic: if
|
|
671
|
+
* the string contains a decimal point, parse as USD. Otherwise parse as raw.
|
|
672
|
+
* This matches the x402 spec which uses raw 6-decimal strings consistently.
|
|
673
|
+
*/
|
|
674
|
+
function safeBigInt(v) {
|
|
675
|
+
try {
|
|
676
|
+
if (typeof v === 'bigint')
|
|
677
|
+
return v;
|
|
678
|
+
if (typeof v === 'number') {
|
|
679
|
+
if (!Number.isFinite(v) || v < 0)
|
|
680
|
+
return 0n;
|
|
681
|
+
// Whole number: treat as raw 6-decimal (spec-compliant shape)
|
|
682
|
+
if (Number.isInteger(v))
|
|
683
|
+
return BigInt(v);
|
|
684
|
+
// Decimal number: treat as USD, convert to 6-decimal
|
|
685
|
+
return parseUsdcAmount(v.toString());
|
|
686
|
+
}
|
|
687
|
+
if (typeof v === 'string') {
|
|
688
|
+
const trimmed = v.trim().replace(/^\$/, '');
|
|
689
|
+
if (/^\d+$/.test(trimmed))
|
|
690
|
+
return BigInt(trimmed);
|
|
691
|
+
if (/^\d+\.\d{1,6}$/.test(trimmed))
|
|
692
|
+
return parseUsdcAmount(trimmed);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch {
|
|
696
|
+
// fall through
|
|
697
|
+
}
|
|
698
|
+
return 0n;
|
|
699
|
+
}
|
|
511
700
|
//# sourceMappingURL=X402Adapter.js.map
|