run402 4.6.0 → 4.7.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/package.json +1 -1
- package/sdk/dist/errors.d.ts +53 -2
- package/sdk/dist/errors.d.ts.map +1 -1
- package/sdk/dist/errors.js +91 -0
- package/sdk/dist/errors.js.map +1 -1
- package/sdk/dist/index.d.ts +2 -2
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +1 -1
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/node/_paid-stack.d.ts +1 -1
- package/sdk/dist/node/_paid-stack.d.ts.map +1 -1
- package/sdk/dist/node/index.d.ts +15 -2
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +23 -4
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/node/paid-fetch.d.ts +147 -9
- package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
- package/sdk/dist/node/paid-fetch.js +785 -75
- package/sdk/dist/node/paid-fetch.js.map +1 -1
- package/sdk/dist/node/payment-attempts.d.ts +54 -0
- package/sdk/dist/node/payment-attempts.d.ts.map +1 -0
- package/sdk/dist/node/payment-attempts.js +220 -0
- package/sdk/dist/node/payment-attempts.js.map +1 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Node-only x402-wrapped fetch. Reads the allowance file, checks on-chain
|
|
3
|
-
* USDC balances
|
|
4
|
-
* responses when the
|
|
3
|
+
* USDC balances through independent RPC providers, and returns a fetch wrapper
|
|
4
|
+
* that auto-signs 402 responses only when the requested chain has confirmed
|
|
5
|
+
* funds.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Balance reads are pre-payment, read-only operations. They may be retried and
|
|
8
|
+
* failed over safely; a payment payload has not been created or submitted yet.
|
|
9
|
+
* An exhausted RPC check is never represented as a zero balance.
|
|
9
10
|
*
|
|
10
11
|
* The viem / @x402/* / mppx imports live behind `./_paid-stack.ts` so the
|
|
11
12
|
* SDK's direct surface to those packages is auditable from one file and the
|
|
@@ -15,7 +16,57 @@
|
|
|
15
16
|
* CLI edge.
|
|
16
17
|
*/
|
|
17
18
|
import { readAllowance } from "../../core-dist/allowance.js";
|
|
19
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
20
|
+
import { LocalError, PaymentAttemptError, Run402Error, } from "../errors.js";
|
|
18
21
|
import { PaidStackUnavailable, loadMppStack, loadX402Stack } from "./_paid-stack.js";
|
|
22
|
+
import { attemptIdFromRequest, createFilePaymentAttemptStore, createPaymentAttemptId, hasPaymentAuthorization, requestSummary, withPaymentAttemptHeader, } from "./payment-attempts.js";
|
|
23
|
+
/**
|
|
24
|
+
* A machine-readable x402 balance-preflight failure.
|
|
25
|
+
*
|
|
26
|
+
* `safeToRetry` is true only for read-only RPC failures. A confirmed balance
|
|
27
|
+
* miss is not retryable without changing wallet funds or payment requirements.
|
|
28
|
+
* Both states are emitted before payment payload creation, so
|
|
29
|
+
* `mutationState` is always `not_started`.
|
|
30
|
+
*/
|
|
31
|
+
export class X402BalanceError extends Run402Error {
|
|
32
|
+
kind = "local_error";
|
|
33
|
+
code;
|
|
34
|
+
cause;
|
|
35
|
+
constructor(code, message, details, cause) {
|
|
36
|
+
const rpcFailure = code !== "X402_INSUFFICIENT_FUNDS";
|
|
37
|
+
super(message, null, {
|
|
38
|
+
error: code,
|
|
39
|
+
message,
|
|
40
|
+
code,
|
|
41
|
+
category: rpcFailure ? "network" : "payment_required",
|
|
42
|
+
source: "sdk",
|
|
43
|
+
retryable: rpcFailure,
|
|
44
|
+
safe_to_retry: rpcFailure,
|
|
45
|
+
mutation_state: "not_started",
|
|
46
|
+
details: {
|
|
47
|
+
phase: "balance_preflight",
|
|
48
|
+
payment_started: false,
|
|
49
|
+
...details,
|
|
50
|
+
},
|
|
51
|
+
next_actions: rpcFailure
|
|
52
|
+
? [
|
|
53
|
+
{
|
|
54
|
+
type: "retry",
|
|
55
|
+
why: "Retry the identical request; no payment payload was created.",
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
: [
|
|
59
|
+
{
|
|
60
|
+
type: "fund_wallet",
|
|
61
|
+
why: "Fund the configured allowance wallet on an accepted network, then retry.",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
}, "checking x402 USDC balance");
|
|
65
|
+
this.code = code;
|
|
66
|
+
if (cause !== undefined)
|
|
67
|
+
this.cause = cause;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
19
70
|
const USDC_ABI = [
|
|
20
71
|
{
|
|
21
72
|
name: "balanceOf",
|
|
@@ -27,108 +78,767 @@ const USDC_ABI = [
|
|
|
27
78
|
];
|
|
28
79
|
const USDC_MAINNET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
29
80
|
const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
81
|
+
// Independent public providers. Do not add credential-bearing URLs here.
|
|
82
|
+
const BASE_RPC_URLS = [
|
|
83
|
+
"https://mainnet.base.org",
|
|
84
|
+
"https://base-rpc.publicnode.com",
|
|
85
|
+
"https://1rpc.io/base",
|
|
86
|
+
];
|
|
87
|
+
const BASE_SEPOLIA_RPC_URLS = [
|
|
88
|
+
"https://sepolia.base.org",
|
|
89
|
+
"https://base-sepolia-rpc.publicnode.com",
|
|
90
|
+
"https://base-sepolia.drpc.org",
|
|
91
|
+
];
|
|
92
|
+
const DEFAULT_ATTEMPTS_PER_PROVIDER = 2;
|
|
93
|
+
const DEFAULT_BASE_DELAY_MS = 100;
|
|
94
|
+
const MAX_PENDING_POLICY_ERRORS = 32;
|
|
30
95
|
let warnedMissingDeps = false;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
96
|
+
let policyErrorSequence = 0;
|
|
97
|
+
const policyErrors = new Map();
|
|
98
|
+
let stackLoaders = {
|
|
99
|
+
x402: loadX402Stack,
|
|
100
|
+
mpp: loadMppStack,
|
|
101
|
+
};
|
|
102
|
+
/** @internal Test seam; not re-exported from `@run402/sdk/node`. */
|
|
103
|
+
export function _setPaidStackLoadersForTest(loaders) {
|
|
104
|
+
stackLoaders = {
|
|
105
|
+
x402: loaders?.x402 ?? loadX402Stack,
|
|
106
|
+
mpp: loaders?.mpp ?? loadMppStack,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
class AttemptJournalWriteError extends Error {
|
|
110
|
+
cause;
|
|
111
|
+
constructor(cause) {
|
|
112
|
+
super("Could not persist the x402 payment attempt journal");
|
|
113
|
+
this.cause = cause;
|
|
114
|
+
this.name = "AttemptJournalWriteError";
|
|
40
115
|
}
|
|
41
|
-
|
|
42
|
-
|
|
116
|
+
}
|
|
117
|
+
function classifyRpcFailure(err) {
|
|
118
|
+
const record = err && typeof err === "object" ? err : null;
|
|
119
|
+
const code = typeof record?.code === "string" ? record.code.toLowerCase() : "";
|
|
120
|
+
const status = typeof record?.status === "number" ? record.status : null;
|
|
121
|
+
const message = err instanceof Error ? err.message.toLowerCase() : "";
|
|
122
|
+
if (code.includes("timeout") ||
|
|
123
|
+
code === "etimedout" ||
|
|
124
|
+
code === "abort_err" ||
|
|
125
|
+
/timed? out|timeout|aborted/.test(message)) {
|
|
126
|
+
return "timeout";
|
|
127
|
+
}
|
|
128
|
+
if (status === 429 || code.includes("rate") || /rate.?limit|too many requests|\b429\b/.test(message)) {
|
|
129
|
+
return "rate_limited";
|
|
130
|
+
}
|
|
131
|
+
if (code === "econnreset" ||
|
|
132
|
+
code === "econnrefused" ||
|
|
133
|
+
code === "enotfound" ||
|
|
134
|
+
code === "eai_again" ||
|
|
135
|
+
/network|connection|socket|dns|fetch failed/.test(message)) {
|
|
136
|
+
return "network";
|
|
43
137
|
}
|
|
138
|
+
return "rpc_error";
|
|
44
139
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
140
|
+
function exhaustedRpcCode(failures) {
|
|
141
|
+
if (failures.length > 0 && failures.every((failure) => failure.reason === "timeout")) {
|
|
142
|
+
return "X402_RPC_TIMEOUT";
|
|
143
|
+
}
|
|
144
|
+
if (failures.length > 0 && failures.every((failure) => failure.reason === "rate_limited")) {
|
|
145
|
+
return "X402_RPC_RATE_LIMITED";
|
|
146
|
+
}
|
|
147
|
+
return "X402_RPC_UNAVAILABLE";
|
|
148
|
+
}
|
|
149
|
+
function delayForAttempt(baseDelayMs, retryIndex, random) {
|
|
150
|
+
const exponential = baseDelayMs * 2 ** retryIndex;
|
|
151
|
+
return Math.round(exponential + exponential * 0.25 * random());
|
|
152
|
+
}
|
|
153
|
+
/** @internal Exported only for deterministic source-level tests; not re-exported by the package. */
|
|
154
|
+
export async function checkBalanceAcrossProviders(clients, tokenAddress, walletAddress, network, options = {}) {
|
|
155
|
+
const attemptsPerProvider = options.attemptsPerProvider ?? DEFAULT_ATTEMPTS_PER_PROVIDER;
|
|
156
|
+
const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
157
|
+
const sleep = options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
|
|
158
|
+
const random = options.random ?? Math.random;
|
|
159
|
+
const failures = [];
|
|
160
|
+
let lastCause;
|
|
161
|
+
for (let providerIndex = 0; providerIndex < clients.length; providerIndex += 1) {
|
|
162
|
+
for (let attempt = 1; attempt <= attemptsPerProvider; attempt += 1) {
|
|
163
|
+
if (attempt > 1) {
|
|
164
|
+
await sleep(delayForAttempt(baseDelayMs, attempt - 2, random));
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
return await clients[providerIndex].readContract({
|
|
168
|
+
address: tokenAddress,
|
|
169
|
+
abi: USDC_ABI,
|
|
170
|
+
functionName: "balanceOf",
|
|
171
|
+
args: [walletAddress],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
lastCause = err;
|
|
176
|
+
failures.push({
|
|
177
|
+
provider_index: providerIndex,
|
|
178
|
+
attempt,
|
|
179
|
+
reason: classifyRpcFailure(err),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const code = exhaustedRpcCode(failures);
|
|
185
|
+
throw new X402BalanceError(code, `Unable to confirm the x402 USDC balance on ${network}; no zero balance was assumed.`, {
|
|
186
|
+
network,
|
|
187
|
+
balance_status: "unknown",
|
|
188
|
+
providers_exhausted: true,
|
|
189
|
+
providers_attempted: clients.length,
|
|
190
|
+
attempts: failures,
|
|
191
|
+
}, lastCause);
|
|
192
|
+
}
|
|
193
|
+
function bigintAmount(value) {
|
|
194
|
+
if (typeof value !== "string" || !/^\d+$/.test(value))
|
|
195
|
+
return null;
|
|
52
196
|
try {
|
|
53
|
-
|
|
197
|
+
return BigInt(value);
|
|
54
198
|
}
|
|
55
199
|
catch {
|
|
56
200
|
return null;
|
|
57
201
|
}
|
|
58
|
-
|
|
202
|
+
}
|
|
203
|
+
/** @internal Exported only for deterministic source-level tests; not re-exported by the package. */
|
|
204
|
+
export function filterAffordableRequirements(requirements, balances) {
|
|
205
|
+
const recognized = requirements.filter((requirement) => typeof requirement.network === "string" && requirement.network in balances);
|
|
206
|
+
const known = recognized.filter((requirement) => typeof requirement.network === "string" &&
|
|
207
|
+
balances[requirement.network]?.status === "known" &&
|
|
208
|
+
bigintAmount(requirement.amount) !== null);
|
|
209
|
+
const affordable = known.filter((requirement) => {
|
|
210
|
+
const state = balances[requirement.network];
|
|
211
|
+
return state.balance >= bigintAmount(requirement.amount);
|
|
212
|
+
});
|
|
213
|
+
if (affordable.length > 0)
|
|
214
|
+
return affordable;
|
|
215
|
+
const unknown = recognized
|
|
216
|
+
.map((requirement) => (typeof requirement.network === "string" ? balances[requirement.network] : undefined))
|
|
217
|
+
.find((state) => state?.status === "unknown");
|
|
218
|
+
if (unknown)
|
|
219
|
+
throw unknown.error;
|
|
220
|
+
// Only call this confirmed-insufficient when every recognized requirement
|
|
221
|
+
// had a valid amount and a successful authoritative balance read.
|
|
222
|
+
if (recognized.length > 0 && known.length === recognized.length) {
|
|
223
|
+
const balanceDetails = Object.fromEntries(Object.entries(balances)
|
|
224
|
+
.filter((entry) => entry[1].status === "known")
|
|
225
|
+
.map(([network, state]) => [network, state.balance.toString()]));
|
|
226
|
+
throw new X402BalanceError("X402_INSUFFICIENT_FUNDS", "The configured allowance wallet has insufficient confirmed USDC for the accepted x402 payment requirements.", {
|
|
227
|
+
balances: balanceDetails,
|
|
228
|
+
requirements: recognized.map((requirement) => ({
|
|
229
|
+
network: requirement.network,
|
|
230
|
+
amount: requirement.amount,
|
|
231
|
+
})),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
// Preserve the x402 library's invalid/unsupported-requirement behavior; it
|
|
235
|
+
// would be unfaithful to label malformed input as a confirmed balance miss.
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
function wrapPolicyError(error) {
|
|
239
|
+
policyErrorSequence += 1;
|
|
240
|
+
const token = `run402-x402-preflight-${policyErrorSequence}`;
|
|
241
|
+
// @x402/fetch currently preserves the thrown message, so the outer wrapper
|
|
242
|
+
// removes this entry immediately. Keep the relay bounded anyway: if a future
|
|
243
|
+
// release rewrites messages, no process-lifetime sentinel leak can grow.
|
|
244
|
+
if (policyErrors.size >= MAX_PENDING_POLICY_ERRORS) {
|
|
245
|
+
const oldest = policyErrors.keys().next().value;
|
|
246
|
+
if (oldest)
|
|
247
|
+
policyErrors.delete(oldest);
|
|
248
|
+
}
|
|
249
|
+
policyErrors.set(token, error);
|
|
250
|
+
return new Error(`${token}: ${error.message}`);
|
|
251
|
+
}
|
|
252
|
+
function unwrapPolicyError(error) {
|
|
253
|
+
const message = error instanceof Error ? error.message : "";
|
|
254
|
+
const token = /run402-x402-preflight-\d+/.exec(message)?.[0];
|
|
255
|
+
if (!token)
|
|
256
|
+
return null;
|
|
257
|
+
const structured = policyErrors.get(token) ?? null;
|
|
258
|
+
policyErrors.delete(token);
|
|
259
|
+
return structured;
|
|
260
|
+
}
|
|
261
|
+
function createRpcClients(stack, chain, urls) {
|
|
262
|
+
return urls.map((url) => stack.createPublicClient({ chain, transport: stack.http(url) }));
|
|
263
|
+
}
|
|
264
|
+
async function balanceState(clients, tokenAddress, walletAddress, network) {
|
|
265
|
+
try {
|
|
266
|
+
return {
|
|
267
|
+
status: "known",
|
|
268
|
+
balance: await checkBalanceAcrossProviders(clients, tokenAddress, walletAddress, network),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
if (err instanceof X402BalanceError)
|
|
273
|
+
return { status: "unknown", error: err };
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export async function setupPaidFetch(options = {}) {
|
|
278
|
+
validatePaymentSource(options);
|
|
279
|
+
// Malformed or missing selected local state degrades to an unwrapped 402,
|
|
280
|
+
// but it never falls back to a different wallet source.
|
|
281
|
+
const resolvedAllowance = options.paymentSigner ? null : await resolveAllowance(options);
|
|
282
|
+
const allowance = resolvedAllowance?.allowance ?? null;
|
|
283
|
+
if (!allowance && !options.paymentSigner)
|
|
59
284
|
return null;
|
|
60
285
|
try {
|
|
61
|
-
if (allowance
|
|
62
|
-
const stack = await
|
|
286
|
+
if (allowance?.rail === "mpp") {
|
|
287
|
+
const stack = await stackLoaders.mpp();
|
|
63
288
|
const account = stack.privateKeyToAccount(allowance.privateKey);
|
|
64
289
|
const mppx = stack.Mppx.create({
|
|
65
290
|
polyfill: false,
|
|
66
291
|
methods: [stack.tempo({ account })],
|
|
67
292
|
});
|
|
68
|
-
return mppx.fetch
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const account = stack.privateKeyToAccount(allowance.privateKey);
|
|
73
|
-
const mainnetClient = stack.createPublicClient({ chain: stack.base, transport: stack.http() });
|
|
74
|
-
const sepoliaClient = stack.createPublicClient({ chain: stack.baseSepolia, transport: stack.http() });
|
|
75
|
-
const [mainnetBalance, sepoliaBalance] = await Promise.all([
|
|
76
|
-
checkBalance(mainnetClient, USDC_MAINNET, allowance.address),
|
|
77
|
-
checkBalance(sepoliaClient, USDC_SEPOLIA, allowance.address),
|
|
78
|
-
]);
|
|
79
|
-
const client = new stack.x402Client();
|
|
80
|
-
client.register("eip155:8453", new stack.ExactEvmScheme(stack.toClientEvmSigner(account, mainnetClient)));
|
|
81
|
-
client.register("eip155:84532", new stack.ExactEvmScheme(stack.toClientEvmSigner(account, sepoliaClient)));
|
|
82
|
-
if (mainnetBalance > 0 || sepoliaBalance > 0) {
|
|
83
|
-
// Compare against the requested amount, not > 0. A chain with dust below
|
|
84
|
-
// the request amount would otherwise be picked by x402Client and fail
|
|
85
|
-
// at settlement (silent 402 leak when dust sits on one chain and funds
|
|
86
|
-
// sit on another).
|
|
87
|
-
client.registerPolicy((_version, reqs) => {
|
|
88
|
-
return reqs.filter((r) => {
|
|
89
|
-
const typed = r;
|
|
90
|
-
const required = Number(typed.amount ?? 0);
|
|
91
|
-
if (typed.network === "eip155:8453")
|
|
92
|
-
return mainnetBalance >= required;
|
|
93
|
-
if (typed.network === "eip155:84532")
|
|
94
|
-
return sepoliaBalance >= required;
|
|
95
|
-
return false;
|
|
96
|
-
});
|
|
293
|
+
return withPayer(mppx.fetch, {
|
|
294
|
+
source: resolvedAllowance.source,
|
|
295
|
+
rail: "mpp",
|
|
296
|
+
payers: [{ address: allowance.address }],
|
|
97
297
|
});
|
|
98
298
|
}
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
299
|
+
// Default: x402 on Base + Base Sepolia. Each chain has its own independent
|
|
300
|
+
// provider list; one degraded chain does not erase a confirmed balance on
|
|
301
|
+
// the other chain.
|
|
302
|
+
const stack = await stackLoaders.x402();
|
|
303
|
+
const mainnetClients = createRpcClients(stack, stack.base, BASE_RPC_URLS);
|
|
304
|
+
const sepoliaClients = createRpcClients(stack, stack.baseSepolia, BASE_SEPOLIA_RPC_URLS);
|
|
305
|
+
const [mainnetSigner, sepoliaSigner] = options.paymentSigner
|
|
306
|
+
? await Promise.all([
|
|
307
|
+
options.paymentSigner.getSigner({ network: "eip155:8453", publicClient: mainnetClients[0] }),
|
|
308
|
+
options.paymentSigner.getSigner({ network: "eip155:84532", publicClient: sepoliaClients[0] }),
|
|
309
|
+
])
|
|
310
|
+
: localAllowanceSigners(stack, allowance, mainnetClients[0], sepoliaClients[0]);
|
|
311
|
+
if (!mainnetSigner && !sepoliaSigner)
|
|
312
|
+
return null;
|
|
313
|
+
const balances = {};
|
|
314
|
+
const refreshBalances = async () => {
|
|
315
|
+
const [mainnet, sepolia] = await Promise.all([
|
|
316
|
+
mainnetSigner
|
|
317
|
+
? balanceState(mainnetClients, USDC_MAINNET, mainnetSigner.address, "eip155:8453")
|
|
318
|
+
: null,
|
|
319
|
+
sepoliaSigner
|
|
320
|
+
? balanceState(sepoliaClients, USDC_SEPOLIA, sepoliaSigner.address, "eip155:84532")
|
|
321
|
+
: null,
|
|
322
|
+
]);
|
|
323
|
+
if (mainnet)
|
|
324
|
+
balances["eip155:8453"] = mainnet;
|
|
325
|
+
else
|
|
326
|
+
delete balances["eip155:8453"];
|
|
327
|
+
if (sepolia)
|
|
328
|
+
balances["eip155:84532"] = sepolia;
|
|
329
|
+
else
|
|
330
|
+
delete balances["eip155:84532"];
|
|
331
|
+
};
|
|
332
|
+
await refreshBalances();
|
|
333
|
+
const client = new stack.x402Client();
|
|
334
|
+
if (mainnetSigner) {
|
|
335
|
+
client.register("eip155:8453", new stack.ExactEvmScheme(stack.toClientEvmSigner(mainnetSigner, mainnetClients[0])));
|
|
336
|
+
}
|
|
337
|
+
if (sepoliaSigner) {
|
|
338
|
+
client.register("eip155:84532", new stack.ExactEvmScheme(stack.toClientEvmSigner(sepoliaSigner, sepoliaClients[0])));
|
|
339
|
+
}
|
|
340
|
+
client.registerPolicy((_version, requirements) => {
|
|
341
|
+
try {
|
|
342
|
+
return filterAffordableRequirements(requirements, balances);
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
// @x402/fetch currently wraps payment-creation errors in a plain Error.
|
|
346
|
+
// A per-call token lets the outer wrapper restore the original typed
|
|
347
|
+
// error without putting wallet or RPC details into the sentinel.
|
|
348
|
+
if (err instanceof X402BalanceError)
|
|
349
|
+
throw wrapPolicyError(err);
|
|
350
|
+
throw err;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
const trackedFetch = createTrackedX402Fetch((baseFetch, rawClient) => {
|
|
354
|
+
const wrapped = stack.wrapFetchWithPayment(baseFetch, rawClient);
|
|
355
|
+
return async (input, init) => {
|
|
356
|
+
try {
|
|
357
|
+
return await wrapped(input, init);
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
throw unwrapPolicyError(err) ?? err;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}, client);
|
|
364
|
+
return withPayer(trackedFetch, {
|
|
365
|
+
source: options.paymentSigner ? "payment_signer" : resolvedAllowance.source,
|
|
366
|
+
rail: "x402",
|
|
367
|
+
payers: [
|
|
368
|
+
...(mainnetSigner ? [{ address: mainnetSigner.address, network: "eip155:8453" }] : []),
|
|
369
|
+
...(sepoliaSigner ? [{ address: sepoliaSigner.address, network: "eip155:84532" }] : []),
|
|
370
|
+
],
|
|
371
|
+
}, refreshBalances);
|
|
103
372
|
}
|
|
104
373
|
catch (err) {
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
374
|
+
// Missing optional peers are a stable local capability state. Other setup
|
|
375
|
+
// failures are thrown so lazy initialization can try again on a later call
|
|
376
|
+
// instead of permanently caching an unwrapped/degraded fetch.
|
|
377
|
+
if (err instanceof PaidStackUnavailable) {
|
|
378
|
+
if (!warnedMissingDeps) {
|
|
379
|
+
warnedMissingDeps = true;
|
|
380
|
+
console.warn(`[run402] ${err.message}`);
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
112
383
|
}
|
|
113
|
-
|
|
384
|
+
throw err;
|
|
114
385
|
}
|
|
115
386
|
}
|
|
387
|
+
function isRetryableBalanceError(error) {
|
|
388
|
+
if (!error || typeof error !== "object")
|
|
389
|
+
return false;
|
|
390
|
+
const candidate = error;
|
|
391
|
+
return (typeof candidate.code === "string" &&
|
|
392
|
+
candidate.code.startsWith("X402_RPC_") &&
|
|
393
|
+
candidate.safeToRetry === true);
|
|
394
|
+
}
|
|
395
|
+
function isBalancePreflightError(error) {
|
|
396
|
+
if (!error || typeof error !== "object")
|
|
397
|
+
return false;
|
|
398
|
+
const code = error.code;
|
|
399
|
+
return typeof code === "string" &&
|
|
400
|
+
(code.startsWith("X402_RPC_") || code === "X402_INSUFFICIENT_FUNDS");
|
|
401
|
+
}
|
|
402
|
+
function createLazyPaidFetchFrom(setup) {
|
|
403
|
+
let cached;
|
|
404
|
+
let initializing;
|
|
405
|
+
return async (input, init) => {
|
|
406
|
+
if (cached === undefined) {
|
|
407
|
+
initializing ??= setup()
|
|
408
|
+
.then((initialized) => {
|
|
409
|
+
cached = initialized;
|
|
410
|
+
return initialized;
|
|
411
|
+
})
|
|
412
|
+
.finally(() => {
|
|
413
|
+
initializing = undefined;
|
|
414
|
+
});
|
|
415
|
+
await initializing;
|
|
416
|
+
}
|
|
417
|
+
// Read `globalThis.fetch` fresh each call so test suites that override it
|
|
418
|
+
// after the SDK is constructed still see their mocks.
|
|
419
|
+
if (!cached)
|
|
420
|
+
return globalThis.fetch(input, init);
|
|
421
|
+
try {
|
|
422
|
+
return await cached(input, init);
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
// A retryable RPC preflight never created a payment payload. Drop only
|
|
426
|
+
// that failed initialization so the next call re-reads provider health.
|
|
427
|
+
if (isRetryableBalanceError(err))
|
|
428
|
+
cached = undefined;
|
|
429
|
+
throw err;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
116
433
|
/**
|
|
117
434
|
* Returns a fetch that lazily initializes the x402 wrapper on first call.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
435
|
+
* Failed initialization is not cached; concurrent first calls share one
|
|
436
|
+
* attempt, and a later request can recover after transient RPC/setup failure.
|
|
120
437
|
*/
|
|
121
|
-
export function createLazyPaidFetch() {
|
|
438
|
+
export function createLazyPaidFetch(options = {}) {
|
|
122
439
|
let cached;
|
|
123
|
-
|
|
440
|
+
let pending;
|
|
441
|
+
let refreshBeforeNextCall = false;
|
|
442
|
+
const initialize = async () => {
|
|
124
443
|
if (cached === undefined) {
|
|
125
|
-
|
|
444
|
+
pending ??= setupPaidFetch(options).finally(() => {
|
|
445
|
+
pending = undefined;
|
|
446
|
+
});
|
|
447
|
+
const initialized = await pending;
|
|
448
|
+
// Cache only a successful wrapper. Missing/recoverable local state is
|
|
449
|
+
// retried on the next request instead of pinning an unwrapped fetch for
|
|
450
|
+
// the lifetime of the client.
|
|
451
|
+
if (initialized)
|
|
452
|
+
cached = initialized;
|
|
126
453
|
}
|
|
454
|
+
return cached ?? null;
|
|
455
|
+
};
|
|
456
|
+
const lazy = async (input, init) => {
|
|
457
|
+
await initialize();
|
|
127
458
|
// Read `globalThis.fetch` fresh each call so test suites that override
|
|
128
459
|
// it after the SDK is constructed still see their mocks.
|
|
129
|
-
if (cached)
|
|
130
|
-
|
|
460
|
+
if (cached) {
|
|
461
|
+
if (refreshBeforeNextCall) {
|
|
462
|
+
await cached.refreshBalances();
|
|
463
|
+
refreshBeforeNextCall = false;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
return await cached(input, init);
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
// Preserve the selected signer/address. Only mutable RPC-derived
|
|
470
|
+
// balance state is refreshed before the caller's next retry.
|
|
471
|
+
if (isBalancePreflightError(err))
|
|
472
|
+
refreshBeforeNextCall = true;
|
|
473
|
+
throw err;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
131
476
|
return globalThis.fetch(input, init);
|
|
132
477
|
};
|
|
478
|
+
return Object.assign(lazy, {
|
|
479
|
+
async getPayer() {
|
|
480
|
+
return (await initialize())?.payer ?? null;
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
function validatePaymentSource(options) {
|
|
485
|
+
if (options.paymentSigner && options.allowancePath) {
|
|
486
|
+
throw new LocalError("Configure exactly one explicit payment source: paymentSigner or allowancePath", "configuring paid fetch", {
|
|
487
|
+
code: "PAYMENT_SOURCE_CONFLICT",
|
|
488
|
+
details: { fields: ["paymentSigner", "allowancePath"] },
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async function resolveAllowance(options) {
|
|
493
|
+
try {
|
|
494
|
+
// An explicit payment path is authoritative even when auth uses a custom
|
|
495
|
+
// credentials provider. Do not fall back if it is absent or malformed.
|
|
496
|
+
if (options.allowancePath !== undefined) {
|
|
497
|
+
const allowance = readAllowance(options.allowancePath);
|
|
498
|
+
return allowance ? { allowance, source: "allowance_path" } : null;
|
|
499
|
+
}
|
|
500
|
+
// Once a credentials provider is supplied it is the only implicit payment
|
|
501
|
+
// source. Providers without allowance capability fail closed; they never
|
|
502
|
+
// inherit the process-global wallet.
|
|
503
|
+
if (options.credentials !== undefined) {
|
|
504
|
+
if (typeof options.credentials.readAllowance !== "function")
|
|
505
|
+
return null;
|
|
506
|
+
const allowance = await options.credentials.readAllowance();
|
|
507
|
+
return allowance ? { allowance, source: "credentials" } : null;
|
|
508
|
+
}
|
|
509
|
+
// Direct setupPaidFetch()/createLazyPaidFetch() calls retain the Node
|
|
510
|
+
// default for backwards compatibility.
|
|
511
|
+
const allowance = readAllowance();
|
|
512
|
+
return allowance ? { allowance, source: "default_allowance" } : null;
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function withPayer(fetchFn, payer, refreshBalances = async () => { }) {
|
|
519
|
+
return Object.assign(fetchFn, { payer, refreshBalances });
|
|
520
|
+
}
|
|
521
|
+
function localAllowanceSigners(stack, allowance, mainnetClient, sepoliaClient) {
|
|
522
|
+
const account = stack.privateKeyToAccount(allowance.privateKey);
|
|
523
|
+
return [
|
|
524
|
+
stack.toClientEvmSigner(account, mainnetClient),
|
|
525
|
+
stack.toClientEvmSigner(account, sepoliaClient),
|
|
526
|
+
];
|
|
527
|
+
}
|
|
528
|
+
/** @internal Source-level unit-test seam; not re-exported by the package. */
|
|
529
|
+
export const __paidFetchInternals = {
|
|
530
|
+
createLazyPaidFetchFrom,
|
|
531
|
+
};
|
|
532
|
+
/**
|
|
533
|
+
* Request-scoped payment tracker around `@x402/fetch`.
|
|
534
|
+
*
|
|
535
|
+
* The wrapped package may call its base fetch twice: first without payment to
|
|
536
|
+
* obtain the 402 challenge, then with a signed payment authorization. The
|
|
537
|
+
* AsyncLocalStorage context lets the base-fetch boundary update the correct
|
|
538
|
+
* durable attempt even when multiple paid requests run concurrently.
|
|
539
|
+
*
|
|
540
|
+
* Exported from this module for deterministic boundary tests, but intentionally
|
|
541
|
+
* not re-exported from the package entry point.
|
|
542
|
+
*/
|
|
543
|
+
export function createTrackedX402Fetch(wrapFetchWithPayment, client, opts = {}) {
|
|
544
|
+
const storage = new AsyncLocalStorage();
|
|
545
|
+
const store = opts.store ?? createFilePaymentAttemptStore();
|
|
546
|
+
const makeId = opts.createAttemptId ?? createPaymentAttemptId;
|
|
547
|
+
const now = opts.now ?? (() => new Date().toISOString());
|
|
548
|
+
const baseFetch = async (input, init) => {
|
|
549
|
+
const context = storage.getStore();
|
|
550
|
+
if (!context)
|
|
551
|
+
return (opts.fetch ?? globalThis.fetch)(input, init);
|
|
552
|
+
const paymentBearing = hasPaymentAuthorization(input, init);
|
|
553
|
+
if (paymentBearing) {
|
|
554
|
+
ensureIntent(context, store, now);
|
|
555
|
+
context.phase = "payment_submission";
|
|
556
|
+
const startedAt = now();
|
|
557
|
+
writeRecord(context, store, {
|
|
558
|
+
state: "submitting",
|
|
559
|
+
mutation_state: "in_progress",
|
|
560
|
+
provider_started_at: startedAt,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
const [nextInput, nextInit] = withPaymentAttemptHeader(input, init, context.id, paymentBearing);
|
|
564
|
+
if (paymentBearing) {
|
|
565
|
+
// This assignment is deliberately immediately before the external call.
|
|
566
|
+
// From this line onward a thrown transport error has an unknown outcome.
|
|
567
|
+
context.providerStarted = true;
|
|
568
|
+
}
|
|
569
|
+
const response = await (opts.fetch ?? globalThis.fetch)(nextInput, nextInit);
|
|
570
|
+
if (!paymentBearing && response.status === 402) {
|
|
571
|
+
context.phase = "challenge_received";
|
|
572
|
+
ensureIntent(context, store, now);
|
|
573
|
+
}
|
|
574
|
+
else if (paymentBearing) {
|
|
575
|
+
context.phase = "payment_response";
|
|
576
|
+
context.responseStatus = response.status;
|
|
577
|
+
writeRecord(context, store, {
|
|
578
|
+
state: "response_received",
|
|
579
|
+
mutation_state: "in_progress",
|
|
580
|
+
response_status: response.status,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
return response;
|
|
584
|
+
};
|
|
585
|
+
const paidFetch = wrapFetchWithPayment(baseFetch, client);
|
|
586
|
+
return async (input, init) => {
|
|
587
|
+
const suppliedAttemptId = attemptIdFromRequest(input, init);
|
|
588
|
+
const request = requestSummary(input, init);
|
|
589
|
+
if (suppliedAttemptId) {
|
|
590
|
+
let existing;
|
|
591
|
+
try {
|
|
592
|
+
existing = store.read(suppliedAttemptId);
|
|
593
|
+
}
|
|
594
|
+
catch (cause) {
|
|
595
|
+
const code = cause instanceof Run402Error && cause.code
|
|
596
|
+
? cause.code
|
|
597
|
+
: "X402_ATTEMPT_JOURNAL_FAILED";
|
|
598
|
+
throw new PaymentAttemptError({
|
|
599
|
+
code,
|
|
600
|
+
message: "The existing x402 payment attempt could not be inspected; no payment was dispatched.",
|
|
601
|
+
phase: "initial_request",
|
|
602
|
+
paymentAttemptId: suppliedAttemptId,
|
|
603
|
+
providerStarted: false,
|
|
604
|
+
mutationState: "not_started",
|
|
605
|
+
safeToRetry: true,
|
|
606
|
+
retryable: false,
|
|
607
|
+
nextActions: [
|
|
608
|
+
{
|
|
609
|
+
type: "contact_support",
|
|
610
|
+
payment_attempt_id: suppliedAttemptId,
|
|
611
|
+
safe_to_auto_execute: false,
|
|
612
|
+
why: "Preserve and repair the unreadable local payment-attempt record before creating a fresh attempt.",
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
cause,
|
|
616
|
+
request,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
if (existing) {
|
|
620
|
+
throw new PaymentAttemptError({
|
|
621
|
+
code: "X402_ATTEMPT_ID_ALREADY_EXISTS",
|
|
622
|
+
message: "This x402 payment attempt id already exists; reconcile it before authorizing another payment.",
|
|
623
|
+
phase: "payment_response",
|
|
624
|
+
paymentAttemptId: suppliedAttemptId,
|
|
625
|
+
providerStarted: existing.mutation_state !== "not_started",
|
|
626
|
+
responseStatus: existing.response_status ?? null,
|
|
627
|
+
mutationState: existing.mutation_state,
|
|
628
|
+
safeToRetry: false,
|
|
629
|
+
cause: null,
|
|
630
|
+
request,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const context = {
|
|
635
|
+
id: suppliedAttemptId ?? makeId(),
|
|
636
|
+
request,
|
|
637
|
+
createdAt: now(),
|
|
638
|
+
phase: "initial_request",
|
|
639
|
+
providerStarted: false,
|
|
640
|
+
responseStatus: null,
|
|
641
|
+
record: null,
|
|
642
|
+
journalFailure: false,
|
|
643
|
+
now,
|
|
644
|
+
};
|
|
645
|
+
if (suppliedAttemptId) {
|
|
646
|
+
let claimed;
|
|
647
|
+
try {
|
|
648
|
+
claimed = claimIntent(context, store, now);
|
|
649
|
+
}
|
|
650
|
+
catch (cause) {
|
|
651
|
+
throw new PaymentAttemptError({
|
|
652
|
+
code: "X402_ATTEMPT_JOURNAL_FAILED",
|
|
653
|
+
message: "The x402 payment attempt id could not be reserved durably; no request was dispatched.",
|
|
654
|
+
phase: "initial_request",
|
|
655
|
+
paymentAttemptId: suppliedAttemptId,
|
|
656
|
+
providerStarted: false,
|
|
657
|
+
mutationState: "not_started",
|
|
658
|
+
safeToRetry: true,
|
|
659
|
+
cause: cause instanceof AttemptJournalWriteError ? cause.cause : cause,
|
|
660
|
+
request,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
if (!claimed) {
|
|
664
|
+
throw new PaymentAttemptError({
|
|
665
|
+
code: "X402_ATTEMPT_ID_ALREADY_EXISTS",
|
|
666
|
+
message: "This x402 payment attempt id was claimed concurrently; reconcile it before authorizing another payment.",
|
|
667
|
+
phase: "initial_request",
|
|
668
|
+
paymentAttemptId: suppliedAttemptId,
|
|
669
|
+
providerStarted: true,
|
|
670
|
+
mutationState: "ambiguous",
|
|
671
|
+
safeToRetry: false,
|
|
672
|
+
cause: null,
|
|
673
|
+
request,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return storage.run(context, async () => {
|
|
678
|
+
try {
|
|
679
|
+
const response = await paidFetch(input, init);
|
|
680
|
+
if (context.providerStarted) {
|
|
681
|
+
const completed = response.ok;
|
|
682
|
+
writeRecordBestEffort(context, store, {
|
|
683
|
+
state: completed ? "completed" : "ambiguous",
|
|
684
|
+
mutation_state: completed ? "completed" : "ambiguous",
|
|
685
|
+
response_status: response.status,
|
|
686
|
+
});
|
|
687
|
+
if (!completed) {
|
|
688
|
+
throw new PaymentAttemptError({
|
|
689
|
+
code: "X402_PAYMENT_OUTCOME_AMBIGUOUS",
|
|
690
|
+
message: "The x402 payment target returned a non-success response after provider dispatch; reconcile the attempt before paying again.",
|
|
691
|
+
phase: "payment_response",
|
|
692
|
+
paymentAttemptId: context.id,
|
|
693
|
+
providerStarted: true,
|
|
694
|
+
responseStatus: response.status,
|
|
695
|
+
mutationState: "ambiguous",
|
|
696
|
+
safeToRetry: false,
|
|
697
|
+
cause: null,
|
|
698
|
+
request: context.request,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
else if (context.record && context.phase === "initial_request") {
|
|
703
|
+
writeRecordBestEffort(context, store, {
|
|
704
|
+
state: "completed",
|
|
705
|
+
mutation_state: "not_started",
|
|
706
|
+
response_status: response.status,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
return response;
|
|
710
|
+
}
|
|
711
|
+
catch (cause) {
|
|
712
|
+
const balanceError = cause instanceof X402BalanceError ? cause : unwrapPolicyError(cause);
|
|
713
|
+
if (balanceError) {
|
|
714
|
+
writeRecordBestEffort(context, store, {
|
|
715
|
+
state: "failed",
|
|
716
|
+
mutation_state: "not_started",
|
|
717
|
+
last_error_code: balanceError.code,
|
|
718
|
+
});
|
|
719
|
+
throw balanceError;
|
|
720
|
+
}
|
|
721
|
+
if (cause instanceof PaymentAttemptError)
|
|
722
|
+
throw cause;
|
|
723
|
+
if (cause instanceof Run402Error && !context.providerStarted) {
|
|
724
|
+
writeRecordBestEffort(context, store, {
|
|
725
|
+
state: "failed",
|
|
726
|
+
mutation_state: "not_started",
|
|
727
|
+
last_error_code: cause.code,
|
|
728
|
+
});
|
|
729
|
+
throw cause;
|
|
730
|
+
}
|
|
731
|
+
const providerStarted = context.providerStarted;
|
|
732
|
+
const journalFailure = cause instanceof AttemptJournalWriteError || context.journalFailure;
|
|
733
|
+
const code = journalFailure
|
|
734
|
+
? "X402_ATTEMPT_JOURNAL_FAILED"
|
|
735
|
+
: providerStarted
|
|
736
|
+
? "X402_PAYMENT_OUTCOME_AMBIGUOUS"
|
|
737
|
+
: context.phase === "challenge_received"
|
|
738
|
+
? "X402_PAYMENT_SIGNING_FAILED"
|
|
739
|
+
: "X402_INITIAL_REQUEST_FAILED";
|
|
740
|
+
const phase = providerStarted
|
|
741
|
+
? context.responseStatus === null
|
|
742
|
+
? "payment_submission"
|
|
743
|
+
: "payment_response"
|
|
744
|
+
: context.phase === "challenge_received"
|
|
745
|
+
? "payment_signing"
|
|
746
|
+
: context.phase;
|
|
747
|
+
const mutationState = providerStarted ? "ambiguous" : "not_started";
|
|
748
|
+
writeRecordBestEffort(context, store, {
|
|
749
|
+
state: providerStarted ? "ambiguous" : "failed",
|
|
750
|
+
mutation_state: mutationState,
|
|
751
|
+
...(context.responseStatus !== null ? { response_status: context.responseStatus } : {}),
|
|
752
|
+
last_error_code: code,
|
|
753
|
+
});
|
|
754
|
+
throw new PaymentAttemptError({
|
|
755
|
+
code,
|
|
756
|
+
message: providerStarted
|
|
757
|
+
? "The x402 payment request failed after provider dispatch; its outcome is unknown."
|
|
758
|
+
: journalFailure
|
|
759
|
+
? "The x402 payment was not dispatched because its durable attempt could not be recorded."
|
|
760
|
+
: phase === "payment_signing"
|
|
761
|
+
? "The x402 payment authorization could not be created; no payment was dispatched."
|
|
762
|
+
: "The initial request failed before an x402 payment was dispatched.",
|
|
763
|
+
phase,
|
|
764
|
+
paymentAttemptId: context.id,
|
|
765
|
+
providerStarted,
|
|
766
|
+
responseStatus: context.responseStatus,
|
|
767
|
+
mutationState,
|
|
768
|
+
safeToRetry: !providerStarted,
|
|
769
|
+
cause: cause instanceof AttemptJournalWriteError ? cause.cause : cause,
|
|
770
|
+
request: context.request,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function ensureIntent(context, store, now) {
|
|
777
|
+
if (context.record)
|
|
778
|
+
return;
|
|
779
|
+
if (!claimIntent(context, store, now)) {
|
|
780
|
+
throw new AttemptJournalWriteError(new LocalError("The generated x402 payment attempt id already exists.", "reserving x402 payment attempt", { code: "X402_ATTEMPT_ID_COLLISION" }));
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function claimIntent(context, store, now) {
|
|
784
|
+
if (context.record)
|
|
785
|
+
return true;
|
|
786
|
+
const updatedAt = now();
|
|
787
|
+
const record = {
|
|
788
|
+
version: 1,
|
|
789
|
+
payment_attempt_id: context.id,
|
|
790
|
+
rail: "x402",
|
|
791
|
+
state: "intent",
|
|
792
|
+
mutation_state: "not_started",
|
|
793
|
+
method: context.request.method,
|
|
794
|
+
origin: context.request.origin,
|
|
795
|
+
path_sha256: context.request.path_sha256,
|
|
796
|
+
created_at: context.createdAt,
|
|
797
|
+
updated_at: updatedAt,
|
|
798
|
+
};
|
|
799
|
+
try {
|
|
800
|
+
const claimed = store.claim(record);
|
|
801
|
+
if (claimed)
|
|
802
|
+
context.record = record;
|
|
803
|
+
return claimed;
|
|
804
|
+
}
|
|
805
|
+
catch (cause) {
|
|
806
|
+
context.journalFailure = true;
|
|
807
|
+
throw new AttemptJournalWriteError(cause);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function writeRecord(context, store, patch) {
|
|
811
|
+
if (!context.record)
|
|
812
|
+
throw new AttemptJournalWriteError("payment intent missing");
|
|
813
|
+
const next = {
|
|
814
|
+
...context.record,
|
|
815
|
+
...patch,
|
|
816
|
+
updated_at: context.now(),
|
|
817
|
+
};
|
|
818
|
+
try {
|
|
819
|
+
store.write(next);
|
|
820
|
+
context.record = next;
|
|
821
|
+
}
|
|
822
|
+
catch (cause) {
|
|
823
|
+
context.journalFailure = true;
|
|
824
|
+
throw new AttemptJournalWriteError(cause);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
function writeRecordBestEffort(context, store, patch) {
|
|
828
|
+
if (!context.record)
|
|
829
|
+
return;
|
|
830
|
+
try {
|
|
831
|
+
const next = {
|
|
832
|
+
...context.record,
|
|
833
|
+
...patch,
|
|
834
|
+
updated_at: context.now(),
|
|
835
|
+
};
|
|
836
|
+
store.write(next);
|
|
837
|
+
context.record = next;
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
// The failure already carries the in-process structured outcome. Never
|
|
841
|
+
// replace a known successful response with a local journal write error.
|
|
842
|
+
}
|
|
133
843
|
}
|
|
134
844
|
//# sourceMappingURL=paid-fetch.js.map
|