@devlab.group/agent-commerce 2.0.0 → 2.2.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/README.md +137 -56
- package/dist/ap2.d.ts +2 -2
- package/dist/ap2.js +4 -3
- package/dist/ap2.js.map +1 -1
- package/dist/{authorization-BpbWGGrf.d.ts → authorization-CMkIuAkO.d.ts} +1 -1
- package/dist/chunk-2CJNA7MS.js +8 -0
- package/dist/chunk-2CJNA7MS.js.map +1 -0
- package/dist/{chunk-CKJRITWY.js → chunk-5ZYXBCDX.js} +3 -3
- package/dist/{chunk-CKJRITWY.js.map → chunk-5ZYXBCDX.js.map} +1 -1
- package/dist/{chunk-3LUJHVKX.js → chunk-6Z2H53CG.js} +5 -4
- package/dist/chunk-6Z2H53CG.js.map +1 -0
- package/dist/{chunk-53WXXIPR.js → chunk-7ZMMXZ4G.js} +6 -10
- package/dist/chunk-7ZMMXZ4G.js.map +1 -0
- package/dist/{chunk-HOZA3IWP.js → chunk-ES3V6ECS.js} +16 -6
- package/dist/chunk-ES3V6ECS.js.map +1 -0
- package/dist/chunk-HNRDZLSN.js +1398 -0
- package/dist/chunk-HNRDZLSN.js.map +1 -0
- package/dist/{chunk-SYEGCNJZ.js → chunk-IEW7NSP2.js} +23 -5
- package/dist/chunk-IEW7NSP2.js.map +1 -0
- package/dist/chunk-KA2AX5FH.js +44 -0
- package/dist/chunk-KA2AX5FH.js.map +1 -0
- package/dist/chunk-Y4RE3XYF.js +581 -0
- package/dist/chunk-Y4RE3XYF.js.map +1 -0
- package/dist/cli/ap2-MBMQHFX4.js +678 -0
- package/dist/cli/ap2-MBMQHFX4.js.map +1 -0
- package/dist/cli/chunk-2ZZOGI5I.js +1352 -0
- package/dist/cli/chunk-2ZZOGI5I.js.map +1 -0
- package/dist/cli/chunk-3EQBCFZY.js +53 -0
- package/dist/cli/chunk-3EQBCFZY.js.map +1 -0
- package/dist/cli/chunk-3M6U7ILB.js +8 -0
- package/dist/cli/chunk-3M6U7ILB.js.map +1 -0
- package/dist/cli/chunk-3MGAJ5FX.js +70 -0
- package/dist/cli/chunk-3MGAJ5FX.js.map +1 -0
- package/dist/cli/chunk-5K4XEAZV.js +113 -0
- package/dist/cli/chunk-5K4XEAZV.js.map +1 -0
- package/dist/cli/chunk-7JNYFUT3.js +256 -0
- package/dist/cli/chunk-7JNYFUT3.js.map +1 -0
- package/dist/cli/chunk-AA3FIEAR.js +27 -0
- package/dist/cli/chunk-AA3FIEAR.js.map +1 -0
- package/dist/cli/chunk-FHCFR2GS.js +17 -0
- package/dist/cli/chunk-FHCFR2GS.js.map +1 -0
- package/dist/cli/chunk-FRMLWGSS.js +8 -0
- package/dist/cli/chunk-FRMLWGSS.js.map +1 -0
- package/dist/cli/chunk-KLX2YZDG.js +1400 -0
- package/dist/cli/chunk-KLX2YZDG.js.map +1 -0
- package/dist/cli/chunk-NSPPEF5M.js +334 -0
- package/dist/cli/chunk-NSPPEF5M.js.map +1 -0
- package/dist/cli/chunk-OSTCU6ZK.js +44 -0
- package/dist/cli/chunk-OSTCU6ZK.js.map +1 -0
- package/dist/cli/chunk-QQ3WY5L3.js +271 -0
- package/dist/cli/chunk-QQ3WY5L3.js.map +1 -0
- package/dist/cli/chunk-RWVPYAKC.js +1742 -0
- package/dist/cli/chunk-RWVPYAKC.js.map +1 -0
- package/dist/cli/chunk-SPAYKVUP.js +118 -0
- package/dist/cli/chunk-SPAYKVUP.js.map +1 -0
- package/dist/cli/chunk-SS3QFF5N.js +614 -0
- package/dist/cli/chunk-SS3QFF5N.js.map +1 -0
- package/dist/cli/chunk-TPC4EJLT.js +11 -0
- package/dist/cli/chunk-TPC4EJLT.js.map +1 -0
- package/dist/cli/config-4IBGVFSP.js +11 -0
- package/dist/cli/config-4IBGVFSP.js.map +1 -0
- package/dist/cli/index.js +822 -3392
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-CNWQ2MOE.js +559 -0
- package/dist/cli/mcp-CNWQ2MOE.js.map +1 -0
- package/dist/cli/near-5AY3HWYF.js +58 -0
- package/dist/cli/near-5AY3HWYF.js.map +1 -0
- package/dist/cli/provider-4BQDH56Y.js +396 -0
- package/dist/cli/provider-4BQDH56Y.js.map +1 -0
- package/dist/cli/run-5MB2D45X.js +7286 -0
- package/dist/cli/run-5MB2D45X.js.map +1 -0
- package/dist/cli/svm-3MMYFPC7.js +33 -0
- package/dist/cli/svm-3MMYFPC7.js.map +1 -0
- package/dist/cli/x402-NKR3WQDQ.js +96 -0
- package/dist/cli/x402-NKR3WQDQ.js.map +1 -0
- package/dist/{constants-BSBRlKRR.d.ts → constants-nackLItD.d.ts} +2 -1
- package/dist/index.d.ts +61 -40
- package/dist/index.js +2642 -2205
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +3 -3
- package/dist/mcp.js +15 -20
- package/dist/mcp.js.map +1 -1
- package/dist/mpp.d.ts +12 -5
- package/dist/mpp.js +69 -65
- package/dist/mpp.js.map +1 -1
- package/dist/near-D6C42VPE.js +58 -0
- package/dist/near-D6C42VPE.js.map +1 -0
- package/dist/networks-olfflCI_.d.ts +38 -0
- package/dist/{protocol-adapter-DonxfAwa.d.ts → protocol-adapter-CfxU8Z2T.d.ts} +2 -2
- package/dist/{runtime-CiYbgGpi.d.ts → runtime-7vLNYvmU.d.ts} +25 -3
- package/dist/svm-QEUAUNPQ.js +33 -0
- package/dist/svm-QEUAUNPQ.js.map +1 -0
- package/dist/x402.d.ts +76 -64
- package/dist/x402.js +13 -4
- package/dist/x402.js.map +1 -1
- package/package.json +22 -2
- package/dist/chunk-3LUJHVKX.js.map +0 -1
- package/dist/chunk-53WXXIPR.js.map +0 -1
- package/dist/chunk-HOZA3IWP.js.map +0 -1
- package/dist/chunk-RKBGCPCK.js +0 -733
- package/dist/chunk-RKBGCPCK.js.map +0 -1
- package/dist/chunk-SYEGCNJZ.js.map +0 -1
- package/dist/chunk-TWRUNWDR.js +0 -265
- package/dist/chunk-TWRUNWDR.js.map +0 -1
- package/dist/{payment-flow-DCS882tR.d.ts → guardrails-CX_jYXvZ.d.ts} +8 -8
|
@@ -0,0 +1,1400 @@
|
|
|
1
|
+
import { assertPayToIsNotDevAddress, resolveX402Deployment, assertDevKeyIsLocalOnly, describeRpc, x402OfferName } from './chunk-NSPPEF5M.js';
|
|
2
|
+
import { findX402Network, normalizeAddress, X402_DEFAULT_PAYMENT_FLOW, REUSE_REJECTED_AUTHORIZATION_METADATA_KEY, SETTLE_AFTER_BACKEND_METADATA_KEY, sameAddress, LOCAL_CHAIN_ID, isTransactionReference, isEvmAddress, describeDeploymentMode } from './chunk-7JNYFUT3.js';
|
|
3
|
+
import { computeReplayKey, computePermit2ReplayKey } from './chunk-OSTCU6ZK.js';
|
|
4
|
+
import { PACKAGE_VERSION } from './chunk-FRMLWGSS.js';
|
|
5
|
+
import { CommerceError, systemClock, NOOP_LOGGER, redactedErrorText } from './chunk-QQ3WY5L3.js';
|
|
6
|
+
import { isRecord } from './chunk-3M6U7ILB.js';
|
|
7
|
+
import { x402ExactPermit2ProxyAddress, toFacilitatorEvmSigner, PERMIT2_ADDRESS } from '@x402/evm';
|
|
8
|
+
import { isAddress, getAddress, formatUnits, createWalletClient, http, publicActions, createPublicClient, HttpRequestError, TimeoutError, WaitForTransactionReceiptTimeoutError, BaseError, ContractFunctionRevertedError } from 'viem';
|
|
9
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
10
|
+
import { HTTPFacilitatorClient } from '@x402/core/http';
|
|
11
|
+
import { SettleError, VerifyError } from '@x402/core/types';
|
|
12
|
+
import { x402Facilitator } from '@x402/core/facilitator';
|
|
13
|
+
import { registerExactEvmScheme } from '@x402/evm/exact/facilitator';
|
|
14
|
+
import { PaymentPayloadV2Schema } from '@x402/core/schemas';
|
|
15
|
+
|
|
16
|
+
// src/payments/x402/provider/constants.ts
|
|
17
|
+
var X402_VERSION = 2;
|
|
18
|
+
var X402_PROTOCOL_VERSION = String(X402_VERSION);
|
|
19
|
+
var DEFAULT_MAX_TIMEOUT_SECONDS = 60;
|
|
20
|
+
var DEFAULT_MIME_TYPE = "application/json";
|
|
21
|
+
var HEALTH_TIMEOUT_MS = 4e3;
|
|
22
|
+
var DEFAULT_IDS = {
|
|
23
|
+
next: (prefix) => `${prefix ? `${prefix}_` : ""}${crypto.randomUUID()}`
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/payments/x402/amount.ts
|
|
27
|
+
var DECIMAL_STRING_PATTERN = /^\d+(?:\.\d+)?$/;
|
|
28
|
+
function parseCanonicalAmount(amount, decimals) {
|
|
29
|
+
if (!Number.isInteger(decimals) || decimals < 0) {
|
|
30
|
+
throw new CommerceError("INTERNAL_ERROR", `Invalid asset decimals: ${decimals}`);
|
|
31
|
+
}
|
|
32
|
+
if (!DECIMAL_STRING_PATTERN.test(amount)) {
|
|
33
|
+
throw new CommerceError(
|
|
34
|
+
"PAYMENT_INVALID",
|
|
35
|
+
`Amount "${amount}" is not a valid non-negative decimal string`,
|
|
36
|
+
{
|
|
37
|
+
details: { amount }
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const [integerPart = "0", fractionPart = ""] = amount.split(".");
|
|
42
|
+
if (fractionPart.length > decimals) {
|
|
43
|
+
throw new CommerceError(
|
|
44
|
+
"PAYMENT_INVALID",
|
|
45
|
+
`Amount "${amount}" has more precision than the asset supports (${decimals} decimals)`,
|
|
46
|
+
{ details: { amount, assetDecimals: decimals } }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const paddedFraction = fractionPart.padEnd(decimals, "0");
|
|
50
|
+
const combined = `${integerPart}${paddedFraction}`;
|
|
51
|
+
return BigInt(combined);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/payments/x402/provider/fee-payer.ts
|
|
55
|
+
var SUPPORTED_TTL_MS = 6e4;
|
|
56
|
+
function createSvmFeePayerSource(ctx) {
|
|
57
|
+
const { clock, remoteBinding } = ctx;
|
|
58
|
+
let supportedSnapshot;
|
|
59
|
+
const feePayers = /* @__PURE__ */ new Map();
|
|
60
|
+
function currentSupported() {
|
|
61
|
+
const now = clock.monotonicMs();
|
|
62
|
+
if (supportedSnapshot && now - supportedSnapshot.at < SUPPORTED_TTL_MS) {
|
|
63
|
+
return supportedSnapshot.response;
|
|
64
|
+
}
|
|
65
|
+
const remote = remoteBinding;
|
|
66
|
+
const response = remote.supported();
|
|
67
|
+
const snapshot = { at: now, response };
|
|
68
|
+
supportedSnapshot = snapshot;
|
|
69
|
+
response.catch(() => {
|
|
70
|
+
if (supportedSnapshot === snapshot) supportedSnapshot = void 0;
|
|
71
|
+
});
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
74
|
+
async function svmFeePayer(offer) {
|
|
75
|
+
let supported;
|
|
76
|
+
try {
|
|
77
|
+
supported = await currentSupported();
|
|
78
|
+
} catch (cause) {
|
|
79
|
+
throw new CommerceError(
|
|
80
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
81
|
+
`x402 provider: facilitator ${offer.binding.describe} did not answer /supported, which names the Solana fee payer`,
|
|
82
|
+
{ cause }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const listed = supported.kinds.find(
|
|
86
|
+
(kind) => kind.x402Version === X402_VERSION && kind.scheme === "exact" && kind.network === offer.network
|
|
87
|
+
)?.extra?.["feePayer"];
|
|
88
|
+
const previous = feePayers.get(offer.network);
|
|
89
|
+
const stillSigns = previous !== void 0 && (previous === listed || Object.values(supported.signers).some((s) => s.includes(previous)));
|
|
90
|
+
const feePayer = stillSigns ? previous : listed;
|
|
91
|
+
if (typeof feePayer !== "string" || normalizeAddress(offer.profile, feePayer) === void 0) {
|
|
92
|
+
throw new CommerceError(
|
|
93
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
94
|
+
`x402 provider: facilitator ${offer.binding.describe} names no Solana fee payer for ${offer.network}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
feePayers.set(offer.network, feePayer);
|
|
98
|
+
return feePayer;
|
|
99
|
+
}
|
|
100
|
+
return svmFeePayer;
|
|
101
|
+
}
|
|
102
|
+
function createHealthCheck(ctx) {
|
|
103
|
+
const { offers, clock, deploymentMode, remoteBinding, svm, near } = ctx;
|
|
104
|
+
async function offerHealth(offer, kinds) {
|
|
105
|
+
if (offer.family === "svm") return svmOfferHealth(offer, kinds);
|
|
106
|
+
if (offer.family === "near") return nearOfferHealth(offer, kinds);
|
|
107
|
+
const { profile, rpcOrigin, healthClient, binding } = offer;
|
|
108
|
+
const chainId = await healthClient.getChainId();
|
|
109
|
+
if (chainId !== profile.chainId) {
|
|
110
|
+
return {
|
|
111
|
+
status: "fail",
|
|
112
|
+
detail: `RPC at ${rpcOrigin} reports chain id ${chainId}, expected ${profile.chainId} (${profile.displayName})`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const code = await healthClient.getCode({ address: offer.asset });
|
|
116
|
+
if (!code || code === "0x") {
|
|
117
|
+
return {
|
|
118
|
+
status: "fail",
|
|
119
|
+
detail: `No contract code found at configured asset address ${offer.asset}`
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (offer.method === "permit2") {
|
|
123
|
+
for (const [name, address] of PERMIT2_CONTRACTS) {
|
|
124
|
+
const deployed = await healthClient.getCode({ address });
|
|
125
|
+
if (!deployed || deployed === "0x") {
|
|
126
|
+
return {
|
|
127
|
+
status: "fail",
|
|
128
|
+
detail: `No ${name} contract code found at ${address} on ${profile.displayName}`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const contracts = `asset ${offer.asset}${offer.method === "permit2" ? ", Permit2 and its x402 proxy" : ""}`;
|
|
134
|
+
if (binding.kind === "remote") {
|
|
135
|
+
const supportsUs = (await kinds()).some(
|
|
136
|
+
(kind) => kind.x402Version === X402_VERSION && kind.scheme === "exact" && kind.network === offer.network
|
|
137
|
+
);
|
|
138
|
+
if (!supportsUs) {
|
|
139
|
+
return {
|
|
140
|
+
status: "fail",
|
|
141
|
+
detail: `Facilitator ${binding.describe} does not advertise x402 v${X402_VERSION} scheme=exact on ${offer.network} (${profile.displayName}); no payment on this deployment can settle`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
status: "pass",
|
|
146
|
+
detail: `${describeDeploymentMode(deploymentMode)}: RPC ${rpcOrigin} reachable, chain id ${profile.chainId} (${profile.displayName}), ${contracts} have code, facilitator ${binding.describe} supports exact/${offer.network}`
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const isAnvil = await probeIsAnvilNode(healthClient);
|
|
150
|
+
if (!isAnvil) {
|
|
151
|
+
return {
|
|
152
|
+
status: "fail",
|
|
153
|
+
detail: `RPC at ${rpcOrigin} reports chain id ${profile.chainId} but does not answer "anvil_nodeInfo", so it does not look like a local Anvil dev node. Refusing to treat it as safe for a local-facilitator dev key.`
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
status: "pass",
|
|
158
|
+
detail: `${describeDeploymentMode(deploymentMode)}: RPC ${rpcOrigin} reachable, chain id ${profile.chainId}, ${contracts} have code, confirmed Anvil dev node`
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async function svmOfferHealth(offer, kinds) {
|
|
162
|
+
const { profile, rpcOrigin, binding } = offer;
|
|
163
|
+
const svmModule = await svm();
|
|
164
|
+
const genesis = await jsonRpc(offer.rpcUrl, "getGenesisHash", []);
|
|
165
|
+
const cluster = typeof genesis === "string" ? `solana:${genesis.slice(0, 32)}` : void 0;
|
|
166
|
+
if (cluster !== offer.network) {
|
|
167
|
+
return {
|
|
168
|
+
status: "fail",
|
|
169
|
+
detail: `RPC at ${rpcOrigin} serves ${cluster ?? "an unknown cluster"}, expected ${offer.network} (${profile.displayName})`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const mint = await jsonRpc(offer.rpcUrl, "getAccountInfo", [
|
|
173
|
+
offer.asset,
|
|
174
|
+
{ encoding: "base64" }
|
|
175
|
+
]);
|
|
176
|
+
const mintInfo = readMint(mint);
|
|
177
|
+
if (mintInfo === void 0) {
|
|
178
|
+
return {
|
|
179
|
+
status: "fail",
|
|
180
|
+
detail: `No initialized token mint found at configured asset address ${offer.asset}`
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const { decimals, tokenProgram } = mintInfo;
|
|
184
|
+
if (decimals !== offer.assetDecimals) {
|
|
185
|
+
return {
|
|
186
|
+
status: "fail",
|
|
187
|
+
detail: `Mint ${offer.asset} has ${decimals} decimals, configured ${offer.assetDecimals}`
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const payeeAccount = await svmModule.associatedTokenAccount(
|
|
191
|
+
offer.payTo,
|
|
192
|
+
offer.asset,
|
|
193
|
+
tokenProgram
|
|
194
|
+
);
|
|
195
|
+
const account = await jsonRpc(offer.rpcUrl, "getAccountInfo", [
|
|
196
|
+
payeeAccount,
|
|
197
|
+
{ encoding: "base64" }
|
|
198
|
+
]);
|
|
199
|
+
const accountValue = isRecord(account) ? account["value"] : void 0;
|
|
200
|
+
if (!isRecord(accountValue) || accountValue["owner"] !== tokenProgram) {
|
|
201
|
+
return {
|
|
202
|
+
status: "fail",
|
|
203
|
+
detail: `payTo ${offer.payTo} has no token account for mint ${offer.asset}; create its associated token account (${payeeAccount}) before taking payments`
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const feePayer = (await kinds()).find(
|
|
207
|
+
(kind) => kind.x402Version === X402_VERSION && kind.scheme === "exact" && kind.network === offer.network
|
|
208
|
+
)?.extra?.["feePayer"];
|
|
209
|
+
if (typeof feePayer !== "string" || normalizeAddress(profile, feePayer) === void 0) {
|
|
210
|
+
return {
|
|
211
|
+
status: "fail",
|
|
212
|
+
detail: `Facilitator ${binding.describe} does not advertise x402 v${X402_VERSION} scheme=exact on ${offer.network} (${profile.displayName}) with a fee payer; no payment on this deployment can settle`
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
status: "pass",
|
|
217
|
+
detail: `${describeDeploymentMode(deploymentMode)}: RPC ${rpcOrigin} serves ${profile.displayName}, mint ${offer.asset} has ${decimals} decimals and a token account for ${offer.payTo}, facilitator ${binding.describe} supports exact/${offer.network} with fee payer ${feePayer}`
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async function nearOfferHealth(offer, kinds) {
|
|
221
|
+
const { profile, rpcOrigin, binding, rpcUrl } = offer;
|
|
222
|
+
await near();
|
|
223
|
+
const status = await jsonRpc(rpcUrl, "status", []);
|
|
224
|
+
const chainId = isRecord(status) ? status["chain_id"] : void 0;
|
|
225
|
+
const expected = offer.network.slice("near:".length);
|
|
226
|
+
if (chainId !== expected) {
|
|
227
|
+
return {
|
|
228
|
+
status: "fail",
|
|
229
|
+
detail: `RPC at ${rpcOrigin} serves chain ${String(chainId ?? "unknown")}, expected ${expected} (${profile.displayName})`
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const token = await jsonRpc(rpcUrl, "query", {
|
|
233
|
+
request_type: "view_account",
|
|
234
|
+
finality: "final",
|
|
235
|
+
account_id: offer.asset
|
|
236
|
+
});
|
|
237
|
+
if (!isRecord(token) || token["code_hash"] === NEAR_EMPTY_CODE_HASH) {
|
|
238
|
+
return {
|
|
239
|
+
status: "fail",
|
|
240
|
+
detail: `No contract code found at configured asset account ${offer.asset}`
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const metadata = await nearView(rpcUrl, offer.asset, "ft_metadata", {});
|
|
244
|
+
const decimals = isRecord(metadata) ? metadata["decimals"] : void 0;
|
|
245
|
+
if (decimals !== offer.assetDecimals) {
|
|
246
|
+
return {
|
|
247
|
+
status: "fail",
|
|
248
|
+
detail: `Token ${offer.asset} has ${String(decimals)} decimals, configured ${offer.assetDecimals}`
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
let registered = true;
|
|
252
|
+
try {
|
|
253
|
+
registered = await nearView(rpcUrl, offer.asset, "storage_balance_of", {
|
|
254
|
+
account_id: offer.payTo
|
|
255
|
+
}) !== null;
|
|
256
|
+
} catch (err) {
|
|
257
|
+
if (!/MethodNotFound|MethodResolveError/i.test(redactedErrorText(err))) throw err;
|
|
258
|
+
}
|
|
259
|
+
if (!registered) {
|
|
260
|
+
return {
|
|
261
|
+
status: "fail",
|
|
262
|
+
detail: `payTo ${offer.payTo} has no storage registered with token ${offer.asset}; register it (NEP-145 storage_deposit) before taking payments`
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const listed = (await kinds()).some(
|
|
266
|
+
(kind) => kind.x402Version === X402_VERSION && kind.scheme === "exact" && kind.network === offer.network
|
|
267
|
+
);
|
|
268
|
+
if (!listed) {
|
|
269
|
+
return {
|
|
270
|
+
status: "fail",
|
|
271
|
+
detail: `Facilitator ${binding.describe} does not advertise x402 v${X402_VERSION} scheme=exact on ${offer.network} (${profile.displayName}); no payment on this deployment can settle`
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
status: "pass",
|
|
276
|
+
detail: `${describeDeploymentMode(deploymentMode)}: RPC ${rpcOrigin} serves ${expected}, token ${offer.asset} has ${decimals} decimals and storage for ${offer.payTo}, facilitator ${binding.describe} supports exact/${offer.network}`
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async function computeHealth() {
|
|
280
|
+
const startedAt = clock.monotonicMs();
|
|
281
|
+
const checkedAt = clock.nowIso();
|
|
282
|
+
let supported;
|
|
283
|
+
const kinds = () => {
|
|
284
|
+
supported ??= remoteBinding.supported().then((response) => response.kinds);
|
|
285
|
+
return supported;
|
|
286
|
+
};
|
|
287
|
+
const results = await Promise.all(
|
|
288
|
+
offers.map(
|
|
289
|
+
(offer) => offerHealth(offer, kinds).catch((err) => ({
|
|
290
|
+
status: "fail",
|
|
291
|
+
detail: `x402 health check failed: ${redactedErrorText(err)}`
|
|
292
|
+
}))
|
|
293
|
+
)
|
|
294
|
+
);
|
|
295
|
+
const failed = results.some((result) => result.status === "fail");
|
|
296
|
+
const details = offers.flatMap((offer, index) => {
|
|
297
|
+
const result = results[index];
|
|
298
|
+
if (failed && result.status !== "fail") return [];
|
|
299
|
+
return [offers.length === 1 ? result.detail : `${x402OfferName(offer)}: ${result.detail}`];
|
|
300
|
+
});
|
|
301
|
+
return {
|
|
302
|
+
status: failed ? "fail" : "pass",
|
|
303
|
+
detail: details.join("; "),
|
|
304
|
+
components: offers.map((offer, index) => ({
|
|
305
|
+
name: x402OfferName(offer),
|
|
306
|
+
status: results[index].status
|
|
307
|
+
})),
|
|
308
|
+
checkedAt,
|
|
309
|
+
durationMs: clock.monotonicMs() - startedAt
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const HEALTH_CACHE_MS = 5e3;
|
|
313
|
+
let cachedHealth;
|
|
314
|
+
let inFlightHealth;
|
|
315
|
+
async function health() {
|
|
316
|
+
const now = clock.monotonicMs();
|
|
317
|
+
if (cachedHealth && now - cachedHealth.at < HEALTH_CACHE_MS) {
|
|
318
|
+
return cachedHealth.value;
|
|
319
|
+
}
|
|
320
|
+
if (inFlightHealth) return inFlightHealth;
|
|
321
|
+
inFlightHealth = computeHealth().then((value) => {
|
|
322
|
+
cachedHealth = { at: clock.monotonicMs(), value };
|
|
323
|
+
return value;
|
|
324
|
+
}).finally(() => {
|
|
325
|
+
inFlightHealth = void 0;
|
|
326
|
+
});
|
|
327
|
+
return inFlightHealth;
|
|
328
|
+
}
|
|
329
|
+
return health;
|
|
330
|
+
}
|
|
331
|
+
var PERMIT2_CONTRACTS = [
|
|
332
|
+
["Permit2", PERMIT2_ADDRESS],
|
|
333
|
+
["x402 exact Permit2 proxy", x402ExactPermit2ProxyAddress]
|
|
334
|
+
];
|
|
335
|
+
var TOKEN_PROGRAMS = /* @__PURE__ */ new Set([
|
|
336
|
+
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
|
337
|
+
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
|
338
|
+
]);
|
|
339
|
+
function readMint(result) {
|
|
340
|
+
const value = isRecord(result) ? result["value"] : void 0;
|
|
341
|
+
if (!isRecord(value)) return void 0;
|
|
342
|
+
const owner = value["owner"];
|
|
343
|
+
if (typeof owner !== "string" || !TOKEN_PROGRAMS.has(owner)) return void 0;
|
|
344
|
+
const data = value["data"];
|
|
345
|
+
if (!Array.isArray(data) || typeof data[0] !== "string") return void 0;
|
|
346
|
+
const bytes = Buffer.from(data[0], "base64");
|
|
347
|
+
const decimals = bytes[44];
|
|
348
|
+
return bytes.length >= 82 && bytes[45] === 1 && decimals !== void 0 ? { decimals, tokenProgram: owner } : void 0;
|
|
349
|
+
}
|
|
350
|
+
async function jsonRpc(rpcUrl, method, params) {
|
|
351
|
+
const response = await fetch(rpcUrl, {
|
|
352
|
+
method: "POST",
|
|
353
|
+
headers: { "content-type": "application/json" },
|
|
354
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
355
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
|
|
356
|
+
});
|
|
357
|
+
const body = await response.json();
|
|
358
|
+
if (body.error !== void 0) {
|
|
359
|
+
throw new CommerceError(
|
|
360
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
361
|
+
`RPC ${method} failed: ${JSON.stringify(body.error)}`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
return body.result;
|
|
365
|
+
}
|
|
366
|
+
async function nearView(rpcUrl, accountId, method, args) {
|
|
367
|
+
const result = await jsonRpc(rpcUrl, "query", {
|
|
368
|
+
request_type: "call_function",
|
|
369
|
+
finality: "final",
|
|
370
|
+
account_id: accountId,
|
|
371
|
+
method_name: method,
|
|
372
|
+
args_base64: Buffer.from(JSON.stringify(args)).toString("base64")
|
|
373
|
+
});
|
|
374
|
+
const bytes = isRecord(result) ? result["result"] : void 0;
|
|
375
|
+
if (!Array.isArray(bytes)) {
|
|
376
|
+
throw new CommerceError(
|
|
377
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
378
|
+
`NEAR view ${method} returned no result`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
382
|
+
}
|
|
383
|
+
var NEAR_EMPTY_CODE_HASH = "11111111111111111111111111111111";
|
|
384
|
+
async function probeIsAnvilNode(client) {
|
|
385
|
+
const request = client.request;
|
|
386
|
+
try {
|
|
387
|
+
await request({ method: "anvil_nodeInfo", params: [] });
|
|
388
|
+
return true;
|
|
389
|
+
} catch {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function chainIdFromCaip2(network) {
|
|
394
|
+
const match = /^eip155:(\d+)$/.exec(network);
|
|
395
|
+
if (!match?.[1]) return void 0;
|
|
396
|
+
const chainId = Number(match[1]);
|
|
397
|
+
return Number.isSafeInteger(chainId) && chainId > 0 ? chainId : void 0;
|
|
398
|
+
}
|
|
399
|
+
function buildLocalChain(rpcUrl, chainId = LOCAL_CHAIN_ID) {
|
|
400
|
+
return {
|
|
401
|
+
id: chainId,
|
|
402
|
+
name: "agent-commerce-local",
|
|
403
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
404
|
+
rpcUrls: {
|
|
405
|
+
default: { http: [rpcUrl] }
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function createLocalPublicClient(rpcUrl, timeoutMs, chainId) {
|
|
410
|
+
return createPublicClient({
|
|
411
|
+
chain: buildLocalChain(rpcUrl, chainId),
|
|
412
|
+
transport: http(rpcUrl, { timeout: timeoutMs } )
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
function createLocalFacilitatorClient(rpcUrl, signerPrivateKey, chainId) {
|
|
416
|
+
const chain = buildLocalChain(rpcUrl, chainId);
|
|
417
|
+
const account = privateKeyToAccount(signerPrivateKey);
|
|
418
|
+
return createWalletClient({
|
|
419
|
+
account,
|
|
420
|
+
chain,
|
|
421
|
+
transport: http(rpcUrl)
|
|
422
|
+
}).extend(publicActions);
|
|
423
|
+
}
|
|
424
|
+
var SETTLEMENT_CONFIRMATION_TIMEOUT_MS = 6e4;
|
|
425
|
+
var SUPPORTED_TIMEOUT_MS = 1e4;
|
|
426
|
+
function cdpAuthHeaders(apiKeyId, apiKeySecret) {
|
|
427
|
+
const loading = import('@coinbase/x402').then(
|
|
428
|
+
(mod) => mod.createCdpAuthHeaders(apiKeyId, apiKeySecret),
|
|
429
|
+
(cause) => {
|
|
430
|
+
throw new CommerceError(
|
|
431
|
+
"CONFIG_INVALID",
|
|
432
|
+
'x402 provider: facilitator.auth.type is "cdp", which needs the optional peer "@coinbase/x402". Install it (npm install @coinbase/x402), or use a facilitator that accepts a static token with auth.type "bearer".',
|
|
433
|
+
{ cause }
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
loading.catch(() => {
|
|
438
|
+
});
|
|
439
|
+
return async () => {
|
|
440
|
+
const create = await loading;
|
|
441
|
+
if (!create) {
|
|
442
|
+
throw new CommerceError(
|
|
443
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
444
|
+
"x402 provider: @coinbase/x402 returned no auth-header factory"
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
return await create();
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function verdictFromVerifyError(err) {
|
|
451
|
+
if (err instanceof VerifyError && err.statusCode === 400 && typeof err.invalidReason === "string") {
|
|
452
|
+
return {
|
|
453
|
+
isValid: false,
|
|
454
|
+
invalidReason: err.invalidReason,
|
|
455
|
+
...err.payer !== void 0 ? { payer: err.payer } : {}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
throw err;
|
|
459
|
+
}
|
|
460
|
+
function verdictFromSettleError(err, network) {
|
|
461
|
+
if (err instanceof SettleError && err.statusCode === 400 && typeof err.errorReason === "string" && err.errorReason !== "" && !err.transaction) {
|
|
462
|
+
return {
|
|
463
|
+
success: false,
|
|
464
|
+
errorReason: err.errorReason,
|
|
465
|
+
transaction: "",
|
|
466
|
+
network: err.network ?? network,
|
|
467
|
+
...err.payer !== void 0 ? { payer: err.payer } : {}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
throw err;
|
|
471
|
+
}
|
|
472
|
+
function createRemoteFacilitatorBinding(options) {
|
|
473
|
+
const auth = options.auth;
|
|
474
|
+
let createAuthHeaders;
|
|
475
|
+
if (auth.type === "bearer") {
|
|
476
|
+
createAuthHeaders = async () => {
|
|
477
|
+
const headers = { Authorization: `Bearer ${auth.token}` };
|
|
478
|
+
return { verify: headers, settle: headers, supported: headers };
|
|
479
|
+
};
|
|
480
|
+
} else if (auth.type === "cdp") {
|
|
481
|
+
createAuthHeaders = cdpAuthHeaders(auth.apiKeyId, auth.apiKeySecret);
|
|
482
|
+
}
|
|
483
|
+
const client = new HTTPFacilitatorClient({
|
|
484
|
+
url: options.url,
|
|
485
|
+
timeoutMs: SETTLEMENT_CONFIRMATION_TIMEOUT_MS,
|
|
486
|
+
...createAuthHeaders ? { createAuthHeaders } : {}
|
|
487
|
+
});
|
|
488
|
+
const supportedClient = new HTTPFacilitatorClient({
|
|
489
|
+
url: options.url,
|
|
490
|
+
timeoutMs: SUPPORTED_TIMEOUT_MS,
|
|
491
|
+
...createAuthHeaders ? { createAuthHeaders } : {}
|
|
492
|
+
});
|
|
493
|
+
return {
|
|
494
|
+
kind: "remote",
|
|
495
|
+
describe: `remote ${new URL(options.url).origin} (auth=${auth.type})`,
|
|
496
|
+
open() {
|
|
497
|
+
let failed = false;
|
|
498
|
+
const watch = async (call) => {
|
|
499
|
+
try {
|
|
500
|
+
return await call();
|
|
501
|
+
} catch (err) {
|
|
502
|
+
failed = true;
|
|
503
|
+
throw err;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
return {
|
|
507
|
+
verify: (payload, requirements) => watch(() => client.verify(payload, requirements).catch(verdictFromVerifyError)),
|
|
508
|
+
settle: (payload, requirements) => watch(
|
|
509
|
+
() => client.settle(payload, requirements).catch((err) => verdictFromSettleError(err, requirements.network))
|
|
510
|
+
),
|
|
511
|
+
transportFailed: () => failed
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
supported: () => withDeadline(supportedClient.getSupported(), SUPPORTED_TIMEOUT_MS)
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function withDeadline(call, ms) {
|
|
518
|
+
let timer;
|
|
519
|
+
const deadline = new Promise((_, reject) => {
|
|
520
|
+
timer = setTimeout(
|
|
521
|
+
() => reject(
|
|
522
|
+
new CommerceError(
|
|
523
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
524
|
+
`x402 provider: facilitator /supported did not answer within ${ms / 1e3} s`
|
|
525
|
+
)
|
|
526
|
+
),
|
|
527
|
+
ms
|
|
528
|
+
);
|
|
529
|
+
timer.unref();
|
|
530
|
+
});
|
|
531
|
+
return Promise.race([call, deadline]).finally(() => clearTimeout(timer));
|
|
532
|
+
}
|
|
533
|
+
function createLocalFacilitatorBinding(client, network, isTransportError) {
|
|
534
|
+
const build = () => {
|
|
535
|
+
let failed = false;
|
|
536
|
+
const watch = (promise) => promise.catch((err) => {
|
|
537
|
+
if (isTransportError(err)) failed = true;
|
|
538
|
+
throw err;
|
|
539
|
+
});
|
|
540
|
+
const signer = {
|
|
541
|
+
address: client.account.address,
|
|
542
|
+
readContract: (args) => watch(client.readContract(args)),
|
|
543
|
+
verifyTypedData: (args) => watch(client.verifyTypedData(args)),
|
|
544
|
+
writeContract: (args) => watch(client.writeContract(args)),
|
|
545
|
+
sendTransaction: (args) => watch(client.sendTransaction(args)),
|
|
546
|
+
waitForTransactionReceipt: (args) => watch(client.waitForTransactionReceipt(args)),
|
|
547
|
+
getCode: (args) => watch(client.getCode(args))
|
|
548
|
+
};
|
|
549
|
+
const facilitator = registerExactEvmScheme(new x402Facilitator(), {
|
|
550
|
+
signer: toFacilitatorEvmSigner(signer, {
|
|
551
|
+
confirmationTimeoutMs: SETTLEMENT_CONFIRMATION_TIMEOUT_MS
|
|
552
|
+
}),
|
|
553
|
+
networks: network
|
|
554
|
+
});
|
|
555
|
+
return { facilitator, failed: () => failed };
|
|
556
|
+
};
|
|
557
|
+
return {
|
|
558
|
+
kind: "local",
|
|
559
|
+
describe: "local (in-process)",
|
|
560
|
+
open() {
|
|
561
|
+
const { facilitator, failed } = build();
|
|
562
|
+
return {
|
|
563
|
+
verify: (payload, requirements) => facilitator.verify(payload, requirements),
|
|
564
|
+
settle: (payload, requirements) => facilitator.settle(payload, requirements),
|
|
565
|
+
transportFailed: failed
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
var SETTLEMENT_PENDING_REASON = "settlement_pending";
|
|
571
|
+
var TRANSACTION_FAILED_REASONS = [
|
|
572
|
+
"invalid_exact_evm_transaction_failed",
|
|
573
|
+
"invalid_exact_svm_transaction_failed",
|
|
574
|
+
"settlement_failed"
|
|
575
|
+
];
|
|
576
|
+
var isTransactionFailedReason = (reason) => reason !== void 0 && TRANSACTION_FAILED_REASONS.some((known) => reason === known || reason.startsWith(`${known}:`));
|
|
577
|
+
var ON_CHAIN_FAILURE_REASONS = {
|
|
578
|
+
evm: "invalid_exact_evm_transaction_failed",
|
|
579
|
+
svm: "invalid_exact_svm_transaction_failed",
|
|
580
|
+
near: "settlement_failed"
|
|
581
|
+
};
|
|
582
|
+
var UNKNOWN_OUTCOME_REASONS = /* @__PURE__ */ new Set([
|
|
583
|
+
"duplicate_settlement",
|
|
584
|
+
"post_settlement_transfer_not_confirmed"
|
|
585
|
+
]);
|
|
586
|
+
var NO_VERDICT_REASONS = /* @__PURE__ */ new Set([
|
|
587
|
+
"unexpected_verify_error",
|
|
588
|
+
"invalid_exact_near_no_relayer_configured",
|
|
589
|
+
"invalid_exact_near_current_block_height_unavailable",
|
|
590
|
+
"invalid_exact_near_access_key_lookup_failed",
|
|
591
|
+
"invalid_exact_near_account_lookup_failed",
|
|
592
|
+
"invalid_exact_near_token_account_lookup_failed",
|
|
593
|
+
"invalid_exact_near_balance_check_failed",
|
|
594
|
+
"invalid_exact_near_storage_check_failed",
|
|
595
|
+
"invalid_exact_svm_payload_missing_fee_payer",
|
|
596
|
+
"invalid_exact_svm_fee_payer_not_managed_by_facilitator",
|
|
597
|
+
"invalid_exact_svm_unsupported_scheme",
|
|
598
|
+
"invalid_exact_svm_network_mismatch"
|
|
599
|
+
]);
|
|
600
|
+
var NEAR_PRE_SUBMISSION_REASONS = /* @__PURE__ */ new Set([
|
|
601
|
+
"invalid_x402_version",
|
|
602
|
+
"unsupported_scheme",
|
|
603
|
+
"invalid_network",
|
|
604
|
+
"insufficient_funds",
|
|
605
|
+
"verification_failed"
|
|
606
|
+
]);
|
|
607
|
+
var isNearVerdict = (reason) => reason !== void 0 && (reason === "settlement_failed" || reason.startsWith("invalid_exact_near_") || NEAR_PRE_SUBMISSION_REASONS.has(reason));
|
|
608
|
+
var MAX_REASON_LENGTH = 128;
|
|
609
|
+
var REASON_SHAPE = /^[a-z0-9_.-]+$/i;
|
|
610
|
+
function sanitizeReason(reason, fallback) {
|
|
611
|
+
if (reason === void 0) return fallback;
|
|
612
|
+
const trimmed = reason.trim();
|
|
613
|
+
if (trimmed.length === 0 || trimmed.length > MAX_REASON_LENGTH) return fallback;
|
|
614
|
+
return REASON_SHAPE.test(trimmed) ? trimmed : fallback;
|
|
615
|
+
}
|
|
616
|
+
function isOnChainRevertError(err) {
|
|
617
|
+
return err instanceof BaseError && err.walk((e) => e instanceof ContractFunctionRevertedError) !== null;
|
|
618
|
+
}
|
|
619
|
+
function isProviderUnavailableError(err) {
|
|
620
|
+
if (err instanceof HttpRequestError || err instanceof TimeoutError || err instanceof WaitForTransactionReceiptTimeoutError) {
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
const message = redactedErrorText(err).toLowerCase();
|
|
624
|
+
return message.includes("econnrefused") || message.includes("fetch failed") || message.includes("enotfound") || message.includes("etimedout") || message.includes("timed out") || message.includes("network error");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/payments/x402/provider/offers.ts
|
|
628
|
+
function resolveOffers(inputs, options) {
|
|
629
|
+
let remoteBinding;
|
|
630
|
+
let mode;
|
|
631
|
+
const built = /* @__PURE__ */ new Map();
|
|
632
|
+
const resolveList = (list, field) => {
|
|
633
|
+
const seen = /* @__PURE__ */ new Set();
|
|
634
|
+
return list.map((input, index) => {
|
|
635
|
+
const at = (name) => field(index, name);
|
|
636
|
+
const known = findX402Network(input.network, options.customNetworks);
|
|
637
|
+
if (known !== void 0 && known.family !== "evm") {
|
|
638
|
+
const shape = known.family === "svm" ? "a Solana address (base58, 32 bytes)" : "a NEAR account ID";
|
|
639
|
+
for (const name of ["asset", "payTo"]) {
|
|
640
|
+
if (normalizeAddress(known, input[name]) === void 0) {
|
|
641
|
+
throw new CommerceError("CONFIG_INVALID", `x402 provider: ${at(name)} is not ${shape}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (input.assetTransferMethod !== void 0) {
|
|
645
|
+
throw new CommerceError(
|
|
646
|
+
"CONFIG_INVALID",
|
|
647
|
+
`x402 provider: ${at("assetTransferMethod")} applies to EVM networks only`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
} else {
|
|
651
|
+
if (!isAddress(input.asset)) {
|
|
652
|
+
throw new CommerceError(
|
|
653
|
+
"CONFIG_INVALID",
|
|
654
|
+
`x402 provider: ${at("asset")} is not a valid EVM address (0x and 40 hex characters; a mixed-case address needs a valid EIP-55 checksum)`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
if (!isAddress(input.payTo)) {
|
|
658
|
+
throw new CommerceError(
|
|
659
|
+
"CONFIG_INVALID",
|
|
660
|
+
`x402 provider: ${at("payTo")} is not a valid EVM address (0x and 40 hex characters; a mixed-case address needs a valid EIP-55 checksum)`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
assertPayToIsNotDevAddress(input.rpcUrl, input.payTo);
|
|
664
|
+
if ((input.assetTransferMethod ?? "eip3009") === "eip3009" && (!input.assetName || !input.assetVersion)) {
|
|
665
|
+
throw new CommerceError(
|
|
666
|
+
"CONFIG_INVALID",
|
|
667
|
+
`x402 provider: an EIP-3009 offer needs ${at("assetName")} and ${at("assetVersion")}, the token's EIP-712 domain`
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (!Number.isInteger(input.assetDecimals) || input.assetDecimals < 0) {
|
|
672
|
+
throw new CommerceError(
|
|
673
|
+
"CONFIG_INVALID",
|
|
674
|
+
`x402 provider: ${at("assetDecimals")} must be a non-negative integer`
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
const deployment = resolveX402Deployment({
|
|
678
|
+
network: input.network,
|
|
679
|
+
...options.customNetworks !== void 0 ? { customNetworks: options.customNetworks } : {},
|
|
680
|
+
payTo: input.payTo,
|
|
681
|
+
asset: input.asset,
|
|
682
|
+
...input.assetName !== void 0 ? { assetName: input.assetName } : {},
|
|
683
|
+
...input.assetVersion !== void 0 ? { assetVersion: input.assetVersion } : {},
|
|
684
|
+
facilitator: options.facilitator,
|
|
685
|
+
...options.allowMainnet !== void 0 ? { allowMainnet: options.allowMainnet } : {},
|
|
686
|
+
...options.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: options.allowUnauthenticatedFacilitator } : {}
|
|
687
|
+
});
|
|
688
|
+
const profile = deployment.profile;
|
|
689
|
+
if (mode !== void 0 && deployment.mode !== mode) {
|
|
690
|
+
throw new CommerceError(
|
|
691
|
+
"CONFIG_INVALID",
|
|
692
|
+
`x402 provider: offers mix ${mode} and ${deployment.mode} networks; every offer must be on a ${mode} network`
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
mode = deployment.mode;
|
|
696
|
+
const comparable = (value) => profile.family === "evm" ? value.toLowerCase() : value;
|
|
697
|
+
const key = `${input.network}|${comparable(input.asset)}|${comparable(input.payTo)}`;
|
|
698
|
+
if (seen.has(key)) {
|
|
699
|
+
throw new CommerceError(
|
|
700
|
+
"CONFIG_INVALID",
|
|
701
|
+
`x402 provider: ${at("network")} repeats an earlier offer of the same asset to the same payTo on ${input.network}`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
seen.add(key);
|
|
705
|
+
const identity = [
|
|
706
|
+
key,
|
|
707
|
+
input.assetTransferMethod ?? "eip3009",
|
|
708
|
+
input.assetName,
|
|
709
|
+
input.assetVersion,
|
|
710
|
+
input.assetDecimals,
|
|
711
|
+
input.rpcUrl
|
|
712
|
+
].join("|");
|
|
713
|
+
const existing = built.get(identity);
|
|
714
|
+
if (existing !== void 0) return existing;
|
|
715
|
+
let binding;
|
|
716
|
+
if (options.facilitator.mode === "local") {
|
|
717
|
+
assertDevKeyIsLocalOnly(input.rpcUrl, options.facilitator.signerPrivateKey);
|
|
718
|
+
let client;
|
|
719
|
+
try {
|
|
720
|
+
client = createLocalFacilitatorClient(
|
|
721
|
+
input.rpcUrl,
|
|
722
|
+
options.facilitator.signerPrivateKey
|
|
723
|
+
);
|
|
724
|
+
} catch (cause) {
|
|
725
|
+
throw new CommerceError(
|
|
726
|
+
"CONFIG_INVALID",
|
|
727
|
+
"x402 provider: could not construct a local facilitator signer from facilitator.signerPrivateKey. It must be a valid 32-byte hex private key.",
|
|
728
|
+
{ cause }
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
binding = createLocalFacilitatorBinding(client, input.network, isProviderUnavailableError);
|
|
732
|
+
} else {
|
|
733
|
+
remoteBinding ??= createRemoteFacilitatorBinding({
|
|
734
|
+
url: options.facilitator.url,
|
|
735
|
+
auth: options.facilitator.auth
|
|
736
|
+
});
|
|
737
|
+
binding = remoteBinding;
|
|
738
|
+
}
|
|
739
|
+
const base = {
|
|
740
|
+
network: input.network,
|
|
741
|
+
assetDecimals: input.assetDecimals,
|
|
742
|
+
rpcUrl: input.rpcUrl,
|
|
743
|
+
rpcOrigin: describeRpc(input.rpcUrl),
|
|
744
|
+
binding
|
|
745
|
+
};
|
|
746
|
+
const offer = profile.family === "svm" ? { ...base, family: "svm", profile, asset: input.asset, payTo: input.payTo } : profile.family === "near" ? { ...base, family: "near", profile, asset: input.asset, payTo: input.payTo } : {
|
|
747
|
+
...base,
|
|
748
|
+
family: "evm",
|
|
749
|
+
profile,
|
|
750
|
+
method: input.assetTransferMethod ?? "eip3009",
|
|
751
|
+
asset: input.asset,
|
|
752
|
+
assetName: input.assetName,
|
|
753
|
+
assetVersion: input.assetVersion,
|
|
754
|
+
payTo: input.payTo,
|
|
755
|
+
healthClient: createLocalPublicClient(
|
|
756
|
+
input.rpcUrl,
|
|
757
|
+
HEALTH_TIMEOUT_MS,
|
|
758
|
+
profile.chainId
|
|
759
|
+
)
|
|
760
|
+
};
|
|
761
|
+
built.set(identity, offer);
|
|
762
|
+
return offer;
|
|
763
|
+
});
|
|
764
|
+
};
|
|
765
|
+
const offers = resolveList(
|
|
766
|
+
inputs,
|
|
767
|
+
(index, name) => inputs.length === 1 ? `"${name}"` : `"offers[${index}].${name}"`
|
|
768
|
+
);
|
|
769
|
+
const resourceOffers = new Map(
|
|
770
|
+
Object.entries(options.resourceOffers ?? {}).map(([id, list]) => {
|
|
771
|
+
if (list.length === 0) {
|
|
772
|
+
throw new CommerceError("CONFIG_INVALID", `x402 provider: "resourceOffers.${id}" is empty`);
|
|
773
|
+
}
|
|
774
|
+
return [id, resolveList(list, (index, name) => `"resourceOffers.${id}[${index}].${name}"`)];
|
|
775
|
+
})
|
|
776
|
+
);
|
|
777
|
+
return {
|
|
778
|
+
offers,
|
|
779
|
+
resourceOffers,
|
|
780
|
+
pool: [...built.values()],
|
|
781
|
+
deploymentMode: mode,
|
|
782
|
+
remoteBinding
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function offerCurrency(offer, quoted) {
|
|
786
|
+
const canonical = offer.profile.canonicalAsset;
|
|
787
|
+
return canonical !== void 0 && sameAddress(offer.profile, offer.asset, canonical.address) ? canonical.symbol : quoted;
|
|
788
|
+
}
|
|
789
|
+
function decodePaymentSubmission(payloadBase64) {
|
|
790
|
+
const json = decodeJson(payloadBase64);
|
|
791
|
+
if (json === void 0) return void 0;
|
|
792
|
+
const result = PaymentPayloadV2Schema.safeParse(json);
|
|
793
|
+
return result.success ? result.data : void 0;
|
|
794
|
+
}
|
|
795
|
+
function declaredX402Version(payloadBase64) {
|
|
796
|
+
const json = decodeJson(payloadBase64);
|
|
797
|
+
return typeof json === "object" && json !== null ? json["x402Version"] : void 0;
|
|
798
|
+
}
|
|
799
|
+
function forFacilitator(payload) {
|
|
800
|
+
const { resource: _resource, extensions: _extensions, ...rest } = payload;
|
|
801
|
+
return rest;
|
|
802
|
+
}
|
|
803
|
+
function decodeJson(payloadBase64) {
|
|
804
|
+
try {
|
|
805
|
+
return JSON.parse(Buffer.from(payloadBase64, "base64").toString("utf8"));
|
|
806
|
+
} catch {
|
|
807
|
+
return void 0;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function isExactEvmPayload(payload) {
|
|
811
|
+
const inner = payload.payload;
|
|
812
|
+
return payload.accepted.scheme === "exact" && isRecord(inner) && isRecord(inner["authorization"]) && !("permit2Authorization" in inner) && typeof inner["signature"] === "string";
|
|
813
|
+
}
|
|
814
|
+
var UINT256 = /^(0|[1-9]\d{0,77})$/;
|
|
815
|
+
var UINT256_MAX = 2n ** 256n - 1n;
|
|
816
|
+
var BYTES32 = /^0x[0-9a-fA-F]{64}$/;
|
|
817
|
+
var isUint256 = (value) => typeof value === "string" && UINT256.test(value) && BigInt(value) <= UINT256_MAX;
|
|
818
|
+
function hasEip3009Fields(authorization) {
|
|
819
|
+
return isEvmAddress(authorization["from"]) && isEvmAddress(authorization["to"]) && isUint256(authorization["value"]) && isUint256(authorization["validAfter"]) && isUint256(authorization["validBefore"]) && typeof authorization["nonce"] === "string" && BYTES32.test(authorization["nonce"]);
|
|
820
|
+
}
|
|
821
|
+
function isExactPermit2Payload(payload) {
|
|
822
|
+
const inner = payload.payload;
|
|
823
|
+
if (payload.accepted.scheme !== "exact" || !isRecord(inner)) return false;
|
|
824
|
+
const auth = inner["permit2Authorization"];
|
|
825
|
+
if (!isRecord(auth) || "authorization" in inner || typeof inner["signature"] !== "string") {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
const { permitted, witness } = auth;
|
|
829
|
+
return isRecord(permitted) && isRecord(witness) && isEvmAddress(auth["from"]) && isEvmAddress(auth["spender"]) && isEvmAddress(permitted["token"]) && isEvmAddress(witness["to"]) && isUint256(permitted["amount"]) && isUint256(auth["nonce"]) && isUint256(auth["deadline"]) && isUint256(witness["validAfter"]);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/payments/x402/provider/requirement.ts
|
|
833
|
+
var SELECTED_OFFER_KEY = "x402.offer";
|
|
834
|
+
function offerIndexOf(requirement) {
|
|
835
|
+
const selected = requirement.metadata?.[SELECTED_OFFER_KEY];
|
|
836
|
+
if (typeof selected === "number") return selected;
|
|
837
|
+
return requirement.challenge.accepts.length <= 1 ? 0 : void 0;
|
|
838
|
+
}
|
|
839
|
+
function acceptedMatchesOffer(offered, accepted, network) {
|
|
840
|
+
if (!network) return false;
|
|
841
|
+
const same = (a, b) => sameAddress(network, a, b);
|
|
842
|
+
const offeredExtra = offered.extra ?? {};
|
|
843
|
+
const acceptedExtra = accepted.extra ?? {};
|
|
844
|
+
const flow = (extra) => extra["paymentFlow"] ?? X402_DEFAULT_PAYMENT_FLOW;
|
|
845
|
+
return accepted.scheme === offered.scheme && accepted.network === offered.network && accepted.amount === offered.amount && same(accepted.asset, offered.asset) && same(accepted.payTo, offered.payTo) && Object.entries(offeredExtra).every(([key, value]) => acceptedExtra[key] === value) && flow(acceptedExtra) === flow(offeredExtra);
|
|
846
|
+
}
|
|
847
|
+
function createRequirementBuilders(ctx, svmFeePayer) {
|
|
848
|
+
const { options, offersFor, clock, ids, maxTimeoutSeconds } = ctx;
|
|
849
|
+
async function createRequirement(context) {
|
|
850
|
+
const offers = offersFor(context.resource.id);
|
|
851
|
+
const paymentFlow = options.resourcePaymentFlows?.[context.resource.id] ?? options.paymentFlow ?? X402_DEFAULT_PAYMENT_FLOW;
|
|
852
|
+
const flow = paymentFlow === "upfront" ? { paymentFlow } : {};
|
|
853
|
+
const accepts = await Promise.all(
|
|
854
|
+
offers.map(async (offer) => ({
|
|
855
|
+
scheme: "exact",
|
|
856
|
+
network: offer.network,
|
|
857
|
+
asset: offer.asset,
|
|
858
|
+
amount: parseCanonicalAmount(context.amount, offer.assetDecimals).toString(),
|
|
859
|
+
payTo: offer.payTo,
|
|
860
|
+
maxTimeoutSeconds,
|
|
861
|
+
// EIP-3009 needs `name`/`version` for a shared EIP-712 domain without
|
|
862
|
+
// a token `version()` call; Permit2 uses its own domain. EVM clients
|
|
863
|
+
// sign the named `assetTransferMethod`. Solana needs the facilitator's
|
|
864
|
+
// fee payer; NEAR needs no scheme-specific extra because the
|
|
865
|
+
// facilitator chooses its relayer.
|
|
866
|
+
extra: offer.family === "svm" ? { feePayer: await svmFeePayer(offer), ...flow } : offer.family === "near" ? { ...flow } : {
|
|
867
|
+
...offer.assetName !== void 0 ? { name: offer.assetName } : {},
|
|
868
|
+
...offer.assetVersion !== void 0 ? { version: offer.assetVersion } : {},
|
|
869
|
+
assetTransferMethod: offer.method,
|
|
870
|
+
...flow
|
|
871
|
+
}
|
|
872
|
+
}))
|
|
873
|
+
);
|
|
874
|
+
const single = offers.length === 1 ? offers[0] : void 0;
|
|
875
|
+
const resourceId = encodeURIComponent(context.resource.id);
|
|
876
|
+
const envelope = {
|
|
877
|
+
x402Version: X402_VERSION,
|
|
878
|
+
resource: {
|
|
879
|
+
url: single ? `resource://agent-commerce/${encodeURIComponent(single.network)}/${single.payTo}/resources/${resourceId}` : `resource://agent-commerce/resources/${resourceId}`,
|
|
880
|
+
description: context.resource.description ?? context.resource.name,
|
|
881
|
+
// The media type the merchant declares, else `application/json`; an HTTP
|
|
882
|
+
// delivery carries the backend's own type when it is text and fits the body
|
|
883
|
+
mimeType: options.resourceMimeTypes?.[context.resource.id] ?? DEFAULT_MIME_TYPE
|
|
884
|
+
},
|
|
885
|
+
accepts
|
|
886
|
+
};
|
|
887
|
+
const expiresAt = new Date(clock.now().getTime() + maxTimeoutSeconds * 1e3).toISOString();
|
|
888
|
+
return {
|
|
889
|
+
id: ids.next("payreq"),
|
|
890
|
+
requestId: context.requestId,
|
|
891
|
+
resourceId: context.resource.id,
|
|
892
|
+
provider: "x402",
|
|
893
|
+
amount: context.amount,
|
|
894
|
+
currency: context.currency,
|
|
895
|
+
// Several offers name several destinations; selection fills these in
|
|
896
|
+
...single ? { destination: single.payTo, network: single.network, asset: single.asset } : {},
|
|
897
|
+
expiresAt,
|
|
898
|
+
challenge: {
|
|
899
|
+
provider: "x402",
|
|
900
|
+
version: X402_PROTOCOL_VERSION,
|
|
901
|
+
accepts,
|
|
902
|
+
envelope
|
|
903
|
+
},
|
|
904
|
+
metadata: {
|
|
905
|
+
[SETTLE_AFTER_BACKEND_METADATA_KEY]: paymentFlow === "authorization",
|
|
906
|
+
// The SDK server keeps no record of a payload whose handler or
|
|
907
|
+
// settlement failed and verifies it again; the chain spends it once
|
|
908
|
+
[REUSE_REJECTED_AUTHORIZATION_METADATA_KEY]: true
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
async function selectRequirement(context) {
|
|
913
|
+
const { requirement, submission } = context;
|
|
914
|
+
const offered = requirement.challenge.accepts;
|
|
915
|
+
if (offered.length < 2) return requirement;
|
|
916
|
+
const offers = offersFor(requirement.resourceId);
|
|
917
|
+
const payload = decodePaymentSubmission(submission.payload);
|
|
918
|
+
if (!payload) return requirement;
|
|
919
|
+
const matches = offered.flatMap(
|
|
920
|
+
(offer2, index2) => acceptedMatchesOffer(offer2, payload.accepted, offers[index2]?.profile) ? [index2] : []
|
|
921
|
+
);
|
|
922
|
+
const index = matches.length === 1 ? matches[0] : void 0;
|
|
923
|
+
const offer = index === void 0 ? void 0 : offers[index];
|
|
924
|
+
if (index === void 0 || !offer) return requirement;
|
|
925
|
+
return {
|
|
926
|
+
...requirement,
|
|
927
|
+
destination: offer.payTo,
|
|
928
|
+
network: offer.network,
|
|
929
|
+
asset: offer.asset,
|
|
930
|
+
metadata: { ...requirement.metadata, [SELECTED_OFFER_KEY]: index }
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
return { createRequirement, selectRequirement };
|
|
934
|
+
}
|
|
935
|
+
function readTransfer(payload, offer) {
|
|
936
|
+
const { chainId } = offer.profile;
|
|
937
|
+
if (offer.method === "eip3009") {
|
|
938
|
+
if (!isExactEvmPayload(payload)) return "unsupported_scheme";
|
|
939
|
+
const { authorization } = payload.payload;
|
|
940
|
+
const { from: from2, to, value, nonce: nonce2 } = authorization;
|
|
941
|
+
if (!hasEip3009Fields(authorization) || !isAddress(from2) || !isAddress(to)) {
|
|
942
|
+
return "invalid_payload";
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
from: getAddress(from2),
|
|
946
|
+
to: getAddress(to),
|
|
947
|
+
value,
|
|
948
|
+
replayKey: computeReplayKey({
|
|
949
|
+
chainId,
|
|
950
|
+
asset: getAddress(offer.asset),
|
|
951
|
+
from: getAddress(from2),
|
|
952
|
+
nonce: nonce2
|
|
953
|
+
}),
|
|
954
|
+
recipientMismatch: "invalid_exact_evm_payload_recipient_mismatch",
|
|
955
|
+
amountMismatch: "invalid_exact_evm_payload_authorization_value_mismatch"
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
if (!isExactPermit2Payload(payload)) return "unsupported_scheme";
|
|
959
|
+
const { from, spender, permitted, witness, nonce } = payload.payload.permit2Authorization;
|
|
960
|
+
if (getAddress(spender) !== getAddress(x402ExactPermit2ProxyAddress)) {
|
|
961
|
+
return "invalid_permit2_spender";
|
|
962
|
+
}
|
|
963
|
+
if (getAddress(permitted.token) !== getAddress(offer.asset)) return "permit2_token_mismatch";
|
|
964
|
+
return {
|
|
965
|
+
from: getAddress(from),
|
|
966
|
+
to: getAddress(witness.to),
|
|
967
|
+
value: permitted.amount,
|
|
968
|
+
replayKey: computePermit2ReplayKey({ chainId, owner: getAddress(from), nonce }),
|
|
969
|
+
recipientMismatch: "invalid_permit2_recipient_mismatch",
|
|
970
|
+
amountMismatch: "permit2_amount_mismatch"
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/payments/x402/provider/settle.ts
|
|
975
|
+
function createSettle(ctx) {
|
|
976
|
+
const { offersFor, clock, logger } = ctx;
|
|
977
|
+
async function settle(context) {
|
|
978
|
+
const { requirement, submission, verification } = context;
|
|
979
|
+
if (verification.status !== "verified") {
|
|
980
|
+
throw new CommerceError(
|
|
981
|
+
"PAYMENT_INVALID",
|
|
982
|
+
"x402 provider: settle() called without a successful verify()"
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
const index = offerIndexOf(requirement);
|
|
986
|
+
const offer = index === void 0 ? void 0 : offersFor(requirement.resourceId)[index];
|
|
987
|
+
const requirements = index === void 0 ? void 0 : requirement.challenge.accepts[index];
|
|
988
|
+
if (!offer || !requirements) {
|
|
989
|
+
throw new CommerceError(
|
|
990
|
+
"PAYMENT_INVALID",
|
|
991
|
+
"x402 provider: settle() found no payment requirements to settle against"
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
const payload = decodePaymentSubmission(submission.payload);
|
|
995
|
+
const transfer = payload && offer.family === "evm" ? readTransfer(payload, offer) : void 0;
|
|
996
|
+
if (!payload || typeof transfer === "string") {
|
|
997
|
+
throw new CommerceError(
|
|
998
|
+
"PAYMENT_INVALID",
|
|
999
|
+
"x402 provider: settle() received an undecodable payment payload"
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
const reference = (value) => isTransactionReference(offer.profile, value);
|
|
1003
|
+
const rejectedSettlement = (rejectionReason) => ({
|
|
1004
|
+
status: "rejected",
|
|
1005
|
+
provider: "x402",
|
|
1006
|
+
amount: verification.amount,
|
|
1007
|
+
currency: verification.currency,
|
|
1008
|
+
...verification.network !== void 0 ? { network: verification.network } : {},
|
|
1009
|
+
...verification.asset !== void 0 ? { asset: verification.asset } : {},
|
|
1010
|
+
...verification.replayKey !== void 0 ? { replayKey: verification.replayKey } : {},
|
|
1011
|
+
rejectionReason
|
|
1012
|
+
});
|
|
1013
|
+
const scope = offer.binding.open();
|
|
1014
|
+
let sdkResult;
|
|
1015
|
+
try {
|
|
1016
|
+
sdkResult = await scope.settle(forFacilitator(payload), requirements);
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
if (isProviderUnavailableError(err) || scope.transportFailed()) {
|
|
1019
|
+
throw new CommerceError(
|
|
1020
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
1021
|
+
`x402 provider: ${offer.binding.kind} facilitator unreachable during settle()`,
|
|
1022
|
+
{ cause: err }
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
if (!isOnChainRevertError(err)) {
|
|
1026
|
+
logger.warn(
|
|
1027
|
+
{ err: redactedErrorText(err) },
|
|
1028
|
+
"x402 settle(): settlement failed with an unclassified error; outcome unknown"
|
|
1029
|
+
);
|
|
1030
|
+
throw new CommerceError(
|
|
1031
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
1032
|
+
"x402 provider: settlement failed with an unclassified error; outcome unknown",
|
|
1033
|
+
{ cause: err }
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
logger.warn(
|
|
1037
|
+
{ err: redactedErrorText(err), rejectionReason: "transaction_reverted" },
|
|
1038
|
+
"x402 settle(): settlement transaction reverted on chain"
|
|
1039
|
+
);
|
|
1040
|
+
return rejectedSettlement("transaction_reverted");
|
|
1041
|
+
}
|
|
1042
|
+
const payer = verification.payer ?? sdkResult.payer;
|
|
1043
|
+
if (!sdkResult.success) {
|
|
1044
|
+
if (isTransactionFailedReason(sdkResult.errorReason) && !reference(sdkResult.transaction)) {
|
|
1045
|
+
logger.warn(
|
|
1046
|
+
{ reportedReason: redactedErrorText(sdkResult.errorReason) },
|
|
1047
|
+
"x402 settle(): the broadcast failed without a transaction hash; outcome unknown"
|
|
1048
|
+
);
|
|
1049
|
+
throw new CommerceError(
|
|
1050
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
1051
|
+
"x402 provider: settlement failed with an unclassified error; outcome unknown"
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
if (offer.family === "near" && NO_VERDICT_REASONS.has(sdkResult.errorReason ?? "") && !sdkResult.transaction) {
|
|
1055
|
+
throw new CommerceError(
|
|
1056
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
1057
|
+
"x402 provider: the facilitator could not check the payment before submitting it",
|
|
1058
|
+
{
|
|
1059
|
+
details: {
|
|
1060
|
+
settlementNotSubmitted: true,
|
|
1061
|
+
reason: sanitizeReason(sdkResult.errorReason, "facilitator_unavailable")
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
if (sdkResult.errorReason === SETTLEMENT_PENDING_REASON || sdkResult.errorMessage?.startsWith(SETTLEMENT_PENDING_REASON) === true || UNKNOWN_OUTCOME_REASONS.has(sdkResult.errorReason ?? "") || NO_VERDICT_REASONS.has(sdkResult.errorReason ?? "") || scope.transportFailed()) {
|
|
1067
|
+
throw new CommerceError(
|
|
1068
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
1069
|
+
"x402 provider: settlement was broadcast but could not be confirmed",
|
|
1070
|
+
{
|
|
1071
|
+
...sdkResult.transaction ? { details: { transactionHash: sdkResult.transaction } } : {}
|
|
1072
|
+
}
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
if (sdkResult.transaction && sdkResult.errorReason !== ON_CHAIN_FAILURE_REASONS[offer.family]) {
|
|
1076
|
+
logger.warn(
|
|
1077
|
+
{ reportedReason: redactedErrorText(sdkResult.errorReason ?? "") },
|
|
1078
|
+
"x402 settle(): the facilitator reported a failure with a transaction; outcome unknown"
|
|
1079
|
+
);
|
|
1080
|
+
throw new CommerceError(
|
|
1081
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
1082
|
+
"x402 provider: the facilitator reported a failed settlement with a transaction; outcome unknown",
|
|
1083
|
+
{ details: { transactionHash: sdkResult.transaction } }
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
if (offer.family === "near" && !isNearVerdict(sdkResult.errorReason)) {
|
|
1087
|
+
logger.warn(
|
|
1088
|
+
{ reportedReason: redactedErrorText(sdkResult.errorReason ?? "") },
|
|
1089
|
+
"x402 settle(): NEAR facilitator failure reason is not a verdict; outcome unknown"
|
|
1090
|
+
);
|
|
1091
|
+
throw new CommerceError(
|
|
1092
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
1093
|
+
"x402 provider: settlement failed with an unclassified error; outcome unknown"
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
if (sdkResult.errorReason !== void 0) {
|
|
1097
|
+
logger.debug(
|
|
1098
|
+
{ reportedReason: redactedErrorText(sdkResult.errorReason) },
|
|
1099
|
+
"x402 settle(): facilitator failure reason"
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
return {
|
|
1103
|
+
...rejectedSettlement(sanitizeReason(sdkResult.errorReason, "settlement_failed")),
|
|
1104
|
+
network: sdkResult.network,
|
|
1105
|
+
...payer !== void 0 ? { payer } : {},
|
|
1106
|
+
// The failed transaction, which the facilitator paid for
|
|
1107
|
+
...sdkResult.transaction ? { externalReference: sdkResult.transaction } : {}
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
if (!reference(sdkResult.transaction) || sdkResult.network !== offer.network) {
|
|
1111
|
+
logger.warn(
|
|
1112
|
+
{ network: sdkResult.network },
|
|
1113
|
+
"x402 settle(): the facilitator reported a success it did not identify; outcome unknown"
|
|
1114
|
+
);
|
|
1115
|
+
throw new CommerceError(
|
|
1116
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
1117
|
+
"x402 provider: the facilitator reported a settlement without a valid transaction; outcome unknown"
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
status: "settled",
|
|
1122
|
+
provider: "x402",
|
|
1123
|
+
externalReference: sdkResult.transaction,
|
|
1124
|
+
...payer !== void 0 ? { payer } : {},
|
|
1125
|
+
payee: requirements.payTo,
|
|
1126
|
+
amount: verification.amount,
|
|
1127
|
+
currency: verification.currency,
|
|
1128
|
+
network: sdkResult.network,
|
|
1129
|
+
...verification.asset !== void 0 ? { asset: verification.asset } : {},
|
|
1130
|
+
...verification.replayKey !== void 0 ? { replayKey: verification.replayKey } : {},
|
|
1131
|
+
settledAt: clock.nowIso(),
|
|
1132
|
+
// `amount` is the display value; x402's settlement response reports base
|
|
1133
|
+
// units. verify() matched an EVM authorization's value to the offer.
|
|
1134
|
+
metadata: {
|
|
1135
|
+
amountBaseUnits: transfer?.value ?? requirements.amount,
|
|
1136
|
+
// The resource's own price, when it names a currency other than the token
|
|
1137
|
+
...verification.currency !== requirement.currency ? { quote: { amount: requirement.amount, currency: requirement.currency } } : {}
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
return settle;
|
|
1142
|
+
}
|
|
1143
|
+
function createVerify(ctx) {
|
|
1144
|
+
const { offersFor, clock, logger, svm, near } = ctx;
|
|
1145
|
+
async function verify(context) {
|
|
1146
|
+
const { requirement, submission } = context;
|
|
1147
|
+
const rejected = (reason) => ({
|
|
1148
|
+
status: "rejected",
|
|
1149
|
+
provider: "x402",
|
|
1150
|
+
amount: requirement.amount,
|
|
1151
|
+
currency: requirement.currency,
|
|
1152
|
+
...requirement.network !== void 0 ? { network: requirement.network } : {},
|
|
1153
|
+
...requirement.asset !== void 0 ? { asset: requirement.asset } : {},
|
|
1154
|
+
rejectionReason: reason
|
|
1155
|
+
});
|
|
1156
|
+
if (requirement.provider !== "x402" || requirement.challenge.provider !== "x402") {
|
|
1157
|
+
return rejected("wrong_provider");
|
|
1158
|
+
}
|
|
1159
|
+
const index = offerIndexOf(requirement);
|
|
1160
|
+
if (index === void 0) {
|
|
1161
|
+
return rejected("invalid_payment_requirements");
|
|
1162
|
+
}
|
|
1163
|
+
const offer = offersFor(requirement.resourceId)[index];
|
|
1164
|
+
const requirements = requirement.challenge.accepts[index];
|
|
1165
|
+
if (!offer || !requirements) {
|
|
1166
|
+
return rejected("missing_payment_requirements");
|
|
1167
|
+
}
|
|
1168
|
+
if (requirement.expiresAt !== void 0 && Date.parse(requirement.expiresAt) < clock.now().getTime()) {
|
|
1169
|
+
return rejected("requirement_expired");
|
|
1170
|
+
}
|
|
1171
|
+
if (!sameAddress(offer.profile, requirements.asset, offer.asset)) {
|
|
1172
|
+
return rejected("wrong_asset");
|
|
1173
|
+
}
|
|
1174
|
+
const payload = decodePaymentSubmission(submission.payload);
|
|
1175
|
+
if (!payload) {
|
|
1176
|
+
const version = declaredX402Version(submission.payload);
|
|
1177
|
+
return rejected(
|
|
1178
|
+
typeof version === "number" && version !== X402_VERSION ? "invalid_x402_version" : "invalid_payload"
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
if (payload.x402Version !== X402_VERSION) {
|
|
1182
|
+
return rejected("invalid_x402_version");
|
|
1183
|
+
}
|
|
1184
|
+
let transfer;
|
|
1185
|
+
let replayKey;
|
|
1186
|
+
let derivedPayer;
|
|
1187
|
+
if (offer.family === "svm") {
|
|
1188
|
+
const identity = (await svm()).svmReplayKey(payload.payload, offer.network);
|
|
1189
|
+
if (typeof identity === "string") return rejected(identity);
|
|
1190
|
+
replayKey = identity.replayKey;
|
|
1191
|
+
} else if (offer.family === "near") {
|
|
1192
|
+
const inspected = (await near()).inspectNearPayload(payload.payload, {
|
|
1193
|
+
network: offer.network,
|
|
1194
|
+
asset: offer.asset,
|
|
1195
|
+
payTo: offer.payTo,
|
|
1196
|
+
amount: requirements.amount
|
|
1197
|
+
});
|
|
1198
|
+
if (typeof inspected === "string") return rejected(inspected);
|
|
1199
|
+
replayKey = inspected.replayKey;
|
|
1200
|
+
derivedPayer = inspected.payer;
|
|
1201
|
+
} else {
|
|
1202
|
+
const read = readTransfer(payload, offer);
|
|
1203
|
+
if (typeof read === "string") return rejected(read);
|
|
1204
|
+
transfer = read;
|
|
1205
|
+
replayKey = read.replayKey;
|
|
1206
|
+
derivedPayer = read.from;
|
|
1207
|
+
}
|
|
1208
|
+
if (payload.accepted.network !== offer.network) {
|
|
1209
|
+
return rejected("invalid_network");
|
|
1210
|
+
}
|
|
1211
|
+
const offeredUrl = requirement.challenge.envelope?.resource?.url;
|
|
1212
|
+
if (!acceptedMatchesOffer(requirements, payload.accepted, offer.profile) || payload.resource !== void 0 && payload.resource.url !== offeredUrl) {
|
|
1213
|
+
return rejected("invalid_payment_requirements");
|
|
1214
|
+
}
|
|
1215
|
+
let required;
|
|
1216
|
+
try {
|
|
1217
|
+
required = BigInt(requirements.amount);
|
|
1218
|
+
} catch {
|
|
1219
|
+
return rejected("invalid_payload");
|
|
1220
|
+
}
|
|
1221
|
+
if (transfer) {
|
|
1222
|
+
if (transfer.to !== getAddress(offer.payTo)) {
|
|
1223
|
+
return rejected(transfer.recipientMismatch);
|
|
1224
|
+
}
|
|
1225
|
+
let authorizedValue;
|
|
1226
|
+
try {
|
|
1227
|
+
authorizedValue = BigInt(transfer.value);
|
|
1228
|
+
} catch {
|
|
1229
|
+
return rejected("invalid_payload");
|
|
1230
|
+
}
|
|
1231
|
+
if (authorizedValue !== required) {
|
|
1232
|
+
return rejected(transfer.amountMismatch);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
const scope = offer.binding.open();
|
|
1236
|
+
let sdkResult;
|
|
1237
|
+
try {
|
|
1238
|
+
sdkResult = await scope.verify(forFacilitator(payload), requirements);
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
if (!isProviderUnavailableError(err) && !scope.transportFailed()) {
|
|
1241
|
+
logger.warn({ err: redactedErrorText(err) }, "x402 verify(): unexpected SDK error");
|
|
1242
|
+
}
|
|
1243
|
+
throw new CommerceError(
|
|
1244
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
1245
|
+
`x402 provider: ${offer.binding.kind} facilitator returned no verdict during verify()`,
|
|
1246
|
+
{ cause: err }
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
if (!sdkResult.isValid) {
|
|
1250
|
+
if (scope.transportFailed() || NO_VERDICT_REASONS.has(sdkResult.invalidReason ?? "")) {
|
|
1251
|
+
if (sdkResult.invalidReason !== void 0) {
|
|
1252
|
+
logger.debug(
|
|
1253
|
+
{ reportedReason: redactedErrorText(sdkResult.invalidReason) },
|
|
1254
|
+
"x402 verify(): local facilitator transport failure reason"
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
throw new CommerceError(
|
|
1258
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
1259
|
+
`x402 provider: ${offer.binding.kind} facilitator unreachable during verify()`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
if (sdkResult.invalidReason !== void 0) {
|
|
1263
|
+
logger.debug(
|
|
1264
|
+
{ reportedReason: redactedErrorText(sdkResult.invalidReason) },
|
|
1265
|
+
"x402 verify(): facilitator rejection reason"
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
return rejected(sanitizeReason(sdkResult.invalidReason, "invalid_payment"));
|
|
1269
|
+
}
|
|
1270
|
+
const payer = derivedPayer ?? (sdkResult.payer !== void 0 && normalizeAddress(offer.profile, sdkResult.payer) ? sdkResult.payer : void 0);
|
|
1271
|
+
return {
|
|
1272
|
+
status: "verified",
|
|
1273
|
+
provider: "x402",
|
|
1274
|
+
...payer !== void 0 ? { payer } : {},
|
|
1275
|
+
payee: transfer?.to ?? offer.payTo,
|
|
1276
|
+
amount: formatUnits(required, offer.assetDecimals),
|
|
1277
|
+
currency: offerCurrency(offer, requirement.currency),
|
|
1278
|
+
network: payload.accepted.network,
|
|
1279
|
+
asset: offer.family === "evm" ? getAddress(requirements.asset) : offer.asset,
|
|
1280
|
+
replayKey
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
return verify;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// src/payments/x402/provider/index.ts
|
|
1287
|
+
function createX402PaymentProvider(options) {
|
|
1288
|
+
const inputs = "offers" in options ? options.offers : [options];
|
|
1289
|
+
if (inputs.length === 0) {
|
|
1290
|
+
throw new CommerceError("CONFIG_INVALID", 'x402 provider: "offers" is empty');
|
|
1291
|
+
}
|
|
1292
|
+
if (options.maxTimeoutSeconds !== void 0 && (!Number.isInteger(options.maxTimeoutSeconds) || options.maxTimeoutSeconds <= 0)) {
|
|
1293
|
+
throw new CommerceError(
|
|
1294
|
+
"CONFIG_INVALID",
|
|
1295
|
+
`x402 provider: maxTimeoutSeconds must be a positive integer (got ${options.maxTimeoutSeconds})`
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
const {
|
|
1299
|
+
offers: defaultOffers,
|
|
1300
|
+
resourceOffers,
|
|
1301
|
+
pool: offers,
|
|
1302
|
+
deploymentMode,
|
|
1303
|
+
remoteBinding
|
|
1304
|
+
} = resolveOffers(inputs, options);
|
|
1305
|
+
const offersFor = (resourceId) => resourceOffers.get(resourceId) ?? defaultOffers;
|
|
1306
|
+
const severalOffers = [defaultOffers, ...resourceOffers.values()].some((list) => list.length > 1);
|
|
1307
|
+
const clock = options.clock ?? systemClock;
|
|
1308
|
+
const ids = options.ids ?? DEFAULT_IDS;
|
|
1309
|
+
const logger = options.logger ?? NOOP_LOGGER;
|
|
1310
|
+
const maxTimeoutSeconds = options.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
|
|
1311
|
+
const evmOffers = offers.filter((offer) => offer.family === "evm");
|
|
1312
|
+
const hasSvm = offers.some((offer) => offer.family === "svm");
|
|
1313
|
+
const hasNear = offers.some((offer) => offer.family === "near");
|
|
1314
|
+
const descriptor = {
|
|
1315
|
+
name: "x402",
|
|
1316
|
+
kind: "payment",
|
|
1317
|
+
implementationVersion: PACKAGE_VERSION,
|
|
1318
|
+
supportedSpec: `x402/v${X402_VERSION} scheme=exact family=${[
|
|
1319
|
+
...evmOffers.length > 0 ? ["eip155"] : [],
|
|
1320
|
+
...hasSvm ? ["solana"] : [],
|
|
1321
|
+
...hasNear ? ["near"] : []
|
|
1322
|
+
].join(",")}`,
|
|
1323
|
+
capabilities: [
|
|
1324
|
+
...evmOffers.length > 0 ? ["exact-evm"] : [],
|
|
1325
|
+
...hasSvm ? ["exact-svm"] : [],
|
|
1326
|
+
...hasNear ? ["exact-near"] : [],
|
|
1327
|
+
`${options.facilitator.mode}-facilitator`,
|
|
1328
|
+
...evmOffers.some((offer) => offer.method === "eip3009") ? ["eip-3009"] : [],
|
|
1329
|
+
...evmOffers.some((offer) => offer.method === "permit2") ? ["permit2"] : [],
|
|
1330
|
+
"caip-2",
|
|
1331
|
+
`mode=${deploymentMode}`,
|
|
1332
|
+
"authorization-flow",
|
|
1333
|
+
"upfront-flow",
|
|
1334
|
+
...severalOffers ? ["multiple-offers"] : []
|
|
1335
|
+
],
|
|
1336
|
+
status: "stable",
|
|
1337
|
+
unsupported: [
|
|
1338
|
+
"permit2 gas sponsoring extensions",
|
|
1339
|
+
"upto scheme",
|
|
1340
|
+
"batch-settlement scheme",
|
|
1341
|
+
"auth-capture scheme"
|
|
1342
|
+
]
|
|
1343
|
+
};
|
|
1344
|
+
const svmModule = hasSvm ? loadOptionalModule(() => import('./svm-3MMYFPC7.js'), "Solana offers need @x402/svm and @solana/kit") : void 0;
|
|
1345
|
+
const nearModule = hasNear ? loadOptionalModule(() => import('./near-5AY3HWYF.js'), "NEAR offers need @x402/near") : void 0;
|
|
1346
|
+
const svm = async () => {
|
|
1347
|
+
if (!svmModule)
|
|
1348
|
+
throw new CommerceError("INTERNAL_ERROR", "x402 provider: no Solana offer is configured");
|
|
1349
|
+
return svmModule;
|
|
1350
|
+
};
|
|
1351
|
+
const near = async () => {
|
|
1352
|
+
if (!nearModule)
|
|
1353
|
+
throw new CommerceError("INTERNAL_ERROR", "x402 provider: no NEAR offer is configured");
|
|
1354
|
+
return nearModule;
|
|
1355
|
+
};
|
|
1356
|
+
const ctx = {
|
|
1357
|
+
options,
|
|
1358
|
+
offers,
|
|
1359
|
+
offersFor,
|
|
1360
|
+
deploymentMode,
|
|
1361
|
+
remoteBinding,
|
|
1362
|
+
clock,
|
|
1363
|
+
ids,
|
|
1364
|
+
logger,
|
|
1365
|
+
maxTimeoutSeconds,
|
|
1366
|
+
svm,
|
|
1367
|
+
near
|
|
1368
|
+
};
|
|
1369
|
+
const { createRequirement, selectRequirement } = createRequirementBuilders(
|
|
1370
|
+
ctx,
|
|
1371
|
+
createSvmFeePayerSource(ctx)
|
|
1372
|
+
);
|
|
1373
|
+
const verify = createVerify(ctx);
|
|
1374
|
+
const settle = createSettle(ctx);
|
|
1375
|
+
const health = createHealthCheck(ctx);
|
|
1376
|
+
return {
|
|
1377
|
+
name: "x402",
|
|
1378
|
+
descriptor,
|
|
1379
|
+
createRequirement,
|
|
1380
|
+
// A single offer needs no selection
|
|
1381
|
+
...severalOffers ? { selectRequirement } : {},
|
|
1382
|
+
verify,
|
|
1383
|
+
settle,
|
|
1384
|
+
health
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
function loadOptionalModule(load, missing) {
|
|
1388
|
+
const loading = load().catch((cause) => {
|
|
1389
|
+
throw new CommerceError("PAYMENT_PROVIDER_UNAVAILABLE", `x402 provider: ${missing}`, {
|
|
1390
|
+
cause
|
|
1391
|
+
});
|
|
1392
|
+
});
|
|
1393
|
+
loading.catch(() => {
|
|
1394
|
+
});
|
|
1395
|
+
return loading;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
export { DEFAULT_IDS, chainIdFromCaip2, createX402PaymentProvider, parseCanonicalAmount };
|
|
1399
|
+
//# sourceMappingURL=chunk-KLX2YZDG.js.map
|
|
1400
|
+
//# sourceMappingURL=chunk-KLX2YZDG.js.map
|