@dvmkit/sdk 0.0.0 → 0.1.0-rc.1
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/NOTICE +2 -0
- package/README.md +38 -2
- package/dist/chunk-27V2ILSR.js +291 -0
- package/dist/chunk-5GFED3GJ.js +955 -0
- package/dist/chunk-6JZIX5WW.js +1155 -0
- package/dist/chunk-7IH5SG2A.js +1038 -0
- package/dist/chunk-AT6V3SY7.js +102 -0
- package/dist/chunk-DCNT4PJS.js +733 -0
- package/dist/chunk-DMNLFNTW.js +135 -0
- package/dist/chunk-FROTD5XQ.js +70 -0
- package/dist/chunk-H25M54MI.js +149 -0
- package/dist/chunk-KQAJVVZT.js +712 -0
- package/dist/chunk-KXWROQGK.js +74 -0
- package/dist/chunk-L4OYF4DQ.js +67 -0
- package/dist/chunk-NTK5DJ6R.js +1256 -0
- package/dist/chunk-RPXHKMYE.js +3808 -0
- package/dist/chunk-S3XAHZQY.js +63 -0
- package/dist/chunk-YG7G4DPZ.js +25 -0
- package/dist/credit-ledger-EDMEZSA2.js +28 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +303 -0
- package/dist/job-store-C5n6bhap.d.ts +5090 -0
- package/dist/memory-credit-ledger-7TTZDSRS.js +9 -0
- package/dist/mpp-secret-state-WNAQQ6K4.js +127 -0
- package/dist/mpp-setup-MOBWGTWJ.js +30 -0
- package/dist/postgres-consumed-credential-store-VHBT4KEA.js +72 -0
- package/dist/postgres-job-store-J5F4GUWU.js +7 -0
- package/dist/postgres-kv-store-JFBDP5IP.js +7 -0
- package/dist/postgres-replay-store-UJXRT6VO.js +7 -0
- package/dist/pricing-4CEB34RM.js +48 -0
- package/dist/processed-payment-store-HAA4SFNK.js +11 -0
- package/dist/revenue-reporter-M35KP6V7.js +435 -0
- package/dist/server/index.d.ts +4108 -0
- package/dist/server/index.js +22538 -0
- package/dist/ssrf-BdHsrrIb.d.ts +325 -0
- package/dist/tempo-charge-store-6GJEMNUU.js +130 -0
- package/dist/tempo-session-store-FTEEGZXA.js +467 -0
- package/dist/testing/index.d.ts +135 -0
- package/dist/testing/index.js +151 -0
- package/dist/x402-35VLYFKZ.js +1272 -0
- package/package.json +89 -6
|
@@ -0,0 +1,1272 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONFIG_AUDIT_FILE,
|
|
3
|
+
CONFIG_BACKUP_FILE,
|
|
4
|
+
CONFIG_FILE,
|
|
5
|
+
CONFIG_PINNED_FILE,
|
|
6
|
+
DvmError,
|
|
7
|
+
X402_CHANNELS_FILE,
|
|
8
|
+
X402_DEFAULT_FACILITATOR,
|
|
9
|
+
X402_DEFAULT_NETWORK,
|
|
10
|
+
X402_NETWORKS,
|
|
11
|
+
X402_V1_VERSION,
|
|
12
|
+
X402_VERSION,
|
|
13
|
+
assertSupportedX402Network,
|
|
14
|
+
buildPaymentRequiredV2,
|
|
15
|
+
buildPaymentRequirements,
|
|
16
|
+
caip2ToX402Network,
|
|
17
|
+
chainIdFromCaip2,
|
|
18
|
+
costAnchor,
|
|
19
|
+
createX402FacilitatorAuthHeaders,
|
|
20
|
+
decodePayment,
|
|
21
|
+
decodePaymentRequiredHeader,
|
|
22
|
+
encodePayment,
|
|
23
|
+
encodePaymentRequiredHeader,
|
|
24
|
+
encodeSettleResponseHeader,
|
|
25
|
+
ensureConfigDir,
|
|
26
|
+
exactEvmAuthorization,
|
|
27
|
+
fetchBtcUsdRate,
|
|
28
|
+
formatStaleness,
|
|
29
|
+
formatUsd,
|
|
30
|
+
getLastKnownBtcRate,
|
|
31
|
+
msatsToUsdc,
|
|
32
|
+
paymentRequiredV2FromV1,
|
|
33
|
+
resolveRpcHttpTransportOptions,
|
|
34
|
+
resolveRpcOverride,
|
|
35
|
+
settleWithFacilitator,
|
|
36
|
+
usdcContractByCaip2,
|
|
37
|
+
usdcContractFor,
|
|
38
|
+
usdcDomainNameFor,
|
|
39
|
+
usdcDomainVersionFor,
|
|
40
|
+
usdcToMsats,
|
|
41
|
+
verifyWithFacilitator,
|
|
42
|
+
x402NetworkByCaip2,
|
|
43
|
+
x402NetworkLabel,
|
|
44
|
+
x402NetworkListForCopy,
|
|
45
|
+
x402NetworkToCaip2,
|
|
46
|
+
x402SupportedNetworksHint
|
|
47
|
+
} from "./chunk-DCNT4PJS.js";
|
|
48
|
+
import {
|
|
49
|
+
callerLoggers,
|
|
50
|
+
redactUrl,
|
|
51
|
+
redactUrlsInText
|
|
52
|
+
} from "./chunk-KXWROQGK.js";
|
|
53
|
+
import "./chunk-AT6V3SY7.js";
|
|
54
|
+
|
|
55
|
+
// src/lib/x402/asset.ts
|
|
56
|
+
function resolveX402Asset(network = X402_DEFAULT_NETWORK) {
|
|
57
|
+
const spec = x402NetworkByCaip2(network);
|
|
58
|
+
if (!spec) return null;
|
|
59
|
+
return {
|
|
60
|
+
address: spec.usdc,
|
|
61
|
+
symbol: "USDC",
|
|
62
|
+
// "native" is the load-bearing word: every contract in the table is
|
|
63
|
+
// Circle's own USDC, and the dollars that read as zero here are the
|
|
64
|
+
// bridged ones.
|
|
65
|
+
label: "native USDC",
|
|
66
|
+
unit: "USDC",
|
|
67
|
+
networkLabel: spec.label,
|
|
68
|
+
confusable: spec.confusable
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function x402WrongAssetHint(asset) {
|
|
72
|
+
const wrongToken = asset.confusable ? `Another dollar on ${asset.networkLabel} \u2014 ${asset.confusable}, the legacy bridged dollar, or any other stablecoin \u2014 is a different contract and will never show up here, however much of it your wallet says you hold.` : `Another dollar on ${asset.networkLabel} is a different contract and will never show up here, however much of it your wallet says you hold.`;
|
|
73
|
+
return `This rail reads only ${asset.label} (${asset.address}) on ${asset.networkLabel}. ${wrongToken} USDC on a different chain is invisible here too \u2014 this wallet reads ${asset.networkLabel} alone. If you funded the wrong one, swap it to ${asset.label} on ${asset.networkLabel}, or bridge the funds across to ${asset.networkLabel}; either way they land where this rail can spend them.`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/lib/x402/client.ts
|
|
77
|
+
import { randomBytes } from "crypto";
|
|
78
|
+
|
|
79
|
+
// src/lib/output.ts
|
|
80
|
+
import { writeSync } from "fs";
|
|
81
|
+
function info(msg) {
|
|
82
|
+
writeAllSync(2, msg + "\n");
|
|
83
|
+
}
|
|
84
|
+
function writeAllSync(fd, text) {
|
|
85
|
+
const buf = Buffer.from(text, "utf8");
|
|
86
|
+
let offset = 0;
|
|
87
|
+
while (offset < buf.length) {
|
|
88
|
+
try {
|
|
89
|
+
offset += writeSync(fd, buf, offset, buf.length - offset);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
const code = err.code;
|
|
92
|
+
if (code === "EAGAIN" || code === "EINTR") continue;
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/lib/btc-rate.ts
|
|
99
|
+
async function resolveBtcUsdRate(purpose) {
|
|
100
|
+
try {
|
|
101
|
+
return { rate: await fetchBtcUsdRate(), stale: false };
|
|
102
|
+
} catch {
|
|
103
|
+
const cached = getLastKnownBtcRate();
|
|
104
|
+
if (cached) {
|
|
105
|
+
warnStaleRate(cached.cachedAt);
|
|
106
|
+
return { rate: cached.rate, stale: true, cachedAt: cached.cachedAt };
|
|
107
|
+
}
|
|
108
|
+
throw rateUnavailable(purpose);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
var warnedStale = false;
|
|
112
|
+
function warnStaleRate(cachedAt) {
|
|
113
|
+
if (warnedStale) return;
|
|
114
|
+
warnedStale = true;
|
|
115
|
+
info(
|
|
116
|
+
`Warning: the BTC/USD rate provider is unreachable \u2014 using the last rate this machine saw (cached ${formatStaleness(cachedAt)}). Dollar figures may have drifted.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
function rateUnavailable(purpose) {
|
|
120
|
+
const outcome = purpose === "payment" ? "nothing was paid" : "no conversion was performed";
|
|
121
|
+
const noun = purpose === "payment" ? "payment" : "conversion";
|
|
122
|
+
const capped = outcome.charAt(0).toUpperCase() + outcome.slice(1);
|
|
123
|
+
return new DvmError(
|
|
124
|
+
"rate_unavailable",
|
|
125
|
+
`Could not price this ${noun}: the BTC/USD rate provider is unreachable and no cached rate is available.`,
|
|
126
|
+
`${capped}. The rate provider (CoinGecko) is rate-limiting or down, and this machine has never cached a rate. Retry in a minute; if it keeps failing, check network access to api.coingecko.com.${purpose === "payment" ? " Budgets are denominated in dollars, so a payment cannot be authorised without a rate." : ""}`,
|
|
127
|
+
{
|
|
128
|
+
display: `BTC/USD rate unavailable \u2014 ${outcome}.`,
|
|
129
|
+
anchor: "rate_unavailable",
|
|
130
|
+
purpose,
|
|
131
|
+
provider: "coingecko",
|
|
132
|
+
retryable: true
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/lib/payment-authorization-bounds.ts
|
|
138
|
+
var X402_EXACT_AUTHORIZATION_TTL_SECONDS = 600;
|
|
139
|
+
var X402_REFUSAL_TRANSACTION_DISCOVERY_WINDOW_MS = 15 * 6e4;
|
|
140
|
+
|
|
141
|
+
// src/lib/payment-rails/types.ts
|
|
142
|
+
var ADVERTISED_AMOUNT_TOLERANCE_BPS = 1000n;
|
|
143
|
+
function maxAdvertisedMicro(localMicro) {
|
|
144
|
+
return localMicro * (10000n + ADVERTISED_AMOUNT_TOLERANCE_BPS) / 10000n;
|
|
145
|
+
}
|
|
146
|
+
function withinAdvertisedTolerance(advertisedMicro, localMicro) {
|
|
147
|
+
return advertisedMicro * 10000n <= localMicro * (10000n + ADVERTISED_AMOUNT_TOLERANCE_BPS);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/lib/evm-private-key.ts
|
|
151
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
152
|
+
import { hexToBytes } from "@noble/hashes/utils.js";
|
|
153
|
+
var EVM_PRIVATE_KEY_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
154
|
+
function isValidEvmPrivateKey(value) {
|
|
155
|
+
return EVM_PRIVATE_KEY_RE.test(value) && secp256k1.utils.isValidSecretKey(hexToBytes(value.slice(2)));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/lib/identity.ts
|
|
159
|
+
import lockfile from "proper-lockfile";
|
|
160
|
+
|
|
161
|
+
// src/lib/config-mutation.ts
|
|
162
|
+
import {
|
|
163
|
+
appendFileSync,
|
|
164
|
+
chmodSync,
|
|
165
|
+
existsSync,
|
|
166
|
+
readFileSync,
|
|
167
|
+
renameSync,
|
|
168
|
+
statSync,
|
|
169
|
+
unlinkSync,
|
|
170
|
+
writeFileSync
|
|
171
|
+
} from "fs";
|
|
172
|
+
var PROTECTED_CONFIG_FIELDS = ["nwcUri", "defaultIdentity", "tempo", "x402"];
|
|
173
|
+
var ProtectedFieldAuthorization = class {
|
|
174
|
+
/** Targets this authorization covers. A field also covers its own leaves. */
|
|
175
|
+
targets;
|
|
176
|
+
/** Prefer {@link authorizeProtectedRemoval}; this exists for it to call. */
|
|
177
|
+
constructor(targets) {
|
|
178
|
+
this.targets = [...targets];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* True when `target` is one this authorization names. Naming a field covers
|
|
182
|
+
* the leaves under it; naming a leaf covers only that leaf, so a command
|
|
183
|
+
* entitled to clear `tempo.apiKey` still cannot take the whole `tempo` block.
|
|
184
|
+
*/
|
|
185
|
+
covers(target) {
|
|
186
|
+
if (this.targets.includes(target)) return true;
|
|
187
|
+
const field = target.split(".")[0];
|
|
188
|
+
return target !== field && this.targets.includes(field);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
function authorizeProtectedRemoval(...targets) {
|
|
192
|
+
return new ProtectedFieldAuthorization(targets);
|
|
193
|
+
}
|
|
194
|
+
function applyConfigPatch(base, patch) {
|
|
195
|
+
if (!isPlainObject(patch))
|
|
196
|
+
throw patchShapeError("A change must be an object with `set` and/or `unset`.");
|
|
197
|
+
if (patch.set !== void 0 && !isPlainObject(patch.set)) {
|
|
198
|
+
throw patchShapeError("A change's `set` must be an object of fields to write.");
|
|
199
|
+
}
|
|
200
|
+
if (patch.unset !== void 0 && !Array.isArray(patch.unset)) {
|
|
201
|
+
throw patchShapeError("A change's `unset` must be an array of field names.");
|
|
202
|
+
}
|
|
203
|
+
const set = patch.set ?? {};
|
|
204
|
+
const unset = patch.unset ?? [];
|
|
205
|
+
for (const [field, value] of Object.entries(set)) {
|
|
206
|
+
if (value !== void 0) continue;
|
|
207
|
+
throw new DvmError(
|
|
208
|
+
"config_patch_invalid",
|
|
209
|
+
`Config field '${field}' was declared with no value.`,
|
|
210
|
+
"Give the field a value, or name it in the change's `unset` list to remove it. An undefined value is how a field gets dropped without anyone deciding to drop it."
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
for (const field of unset) {
|
|
214
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
215
|
+
throw patchShapeError("Every entry in a change's `unset` must be a field name.");
|
|
216
|
+
}
|
|
217
|
+
if (!Object.hasOwn(set, field)) continue;
|
|
218
|
+
throw new DvmError(
|
|
219
|
+
"config_patch_invalid",
|
|
220
|
+
`Config field '${field}' is both written and removed by the same change.`,
|
|
221
|
+
"Declare each field once: in `set` with its new value, or in `unset` to remove it."
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const merged = { ...base, ...set };
|
|
225
|
+
const removing = new Set(unset);
|
|
226
|
+
return Object.fromEntries(
|
|
227
|
+
Object.entries(merged).filter(([field]) => !removing.has(field))
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
function deepFreezeConfig(config) {
|
|
231
|
+
freezeDeep(config);
|
|
232
|
+
return config;
|
|
233
|
+
}
|
|
234
|
+
function protectedRemovals(before, after) {
|
|
235
|
+
const previous = before;
|
|
236
|
+
const next = after;
|
|
237
|
+
const removed = [];
|
|
238
|
+
for (const field of PROTECTED_CONFIG_FIELDS) {
|
|
239
|
+
const held = previous[field];
|
|
240
|
+
if (!isPresentValue(held)) continue;
|
|
241
|
+
const kept = next[field];
|
|
242
|
+
if (!isPresentValue(kept)) {
|
|
243
|
+
removed.push(field);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const spec = PROTECTED_FIELDS[field];
|
|
247
|
+
if (spec.credentialLeaves.length === 0) continue;
|
|
248
|
+
if (!isPlainObject(held)) continue;
|
|
249
|
+
if (!isPlainObject(kept)) {
|
|
250
|
+
removed.push(field);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
for (const leaf of spec.credentialLeaves) {
|
|
254
|
+
if (!isPresentValue(held[leaf]) || isPresentValue(kept[leaf])) continue;
|
|
255
|
+
removed.push(`${field}.${leaf}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return removed;
|
|
259
|
+
}
|
|
260
|
+
function supersededProtectedSecrets(before, after) {
|
|
261
|
+
const previous = before;
|
|
262
|
+
const next = after;
|
|
263
|
+
const superseded = [];
|
|
264
|
+
for (const field of PROTECTED_CONFIG_FIELDS) {
|
|
265
|
+
const spec = PROTECTED_FIELDS[field];
|
|
266
|
+
if (!spec.secret) continue;
|
|
267
|
+
const held = previous[field];
|
|
268
|
+
const kept = next[field];
|
|
269
|
+
if (spec.credentialLeaves.length === 0) {
|
|
270
|
+
if (isPresentValue(held) && isPresentValue(kept) && held !== kept) superseded.push(field);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (!isPlainObject(held) || !isPlainObject(kept)) continue;
|
|
274
|
+
for (const leaf of spec.credentialLeaves) {
|
|
275
|
+
const was = held[leaf];
|
|
276
|
+
const now = kept[leaf];
|
|
277
|
+
if (!isPresentValue(was) || !isPresentValue(now) || was === now) continue;
|
|
278
|
+
superseded.push(`${field}.${leaf}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return superseded;
|
|
282
|
+
}
|
|
283
|
+
function changedConfigPaths(before, after) {
|
|
284
|
+
const found = /* @__PURE__ */ new Set();
|
|
285
|
+
collectChangedPaths(
|
|
286
|
+
before,
|
|
287
|
+
after,
|
|
288
|
+
CONFIG_PATH_SHAPE,
|
|
289
|
+
"",
|
|
290
|
+
found
|
|
291
|
+
);
|
|
292
|
+
return [...found].sort();
|
|
293
|
+
}
|
|
294
|
+
function assertValidConfig(config, fields) {
|
|
295
|
+
if (!isPlainObject(config)) {
|
|
296
|
+
throw new DvmError(
|
|
297
|
+
"config_invalid",
|
|
298
|
+
"The caller configuration is not a JSON object.",
|
|
299
|
+
`Expected an object at ${CONFIG_FILE}.${rollbackSuffix()}`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const record = config;
|
|
303
|
+
const scope = fields ? new Set(fields) : null;
|
|
304
|
+
for (const [field, kind] of Object.entries(CONFIG_FIELD_TYPES)) {
|
|
305
|
+
if (scope && !scope.has(field)) continue;
|
|
306
|
+
const value = record[field];
|
|
307
|
+
if (value === void 0) continue;
|
|
308
|
+
if (!matchesKind(value, kind)) throw invalidFieldError(field, kind);
|
|
309
|
+
}
|
|
310
|
+
for (const [path, kind] of Object.entries(CONFIG_LEAF_TYPES)) {
|
|
311
|
+
const [field, leaf] = path.split(".");
|
|
312
|
+
if (scope && !scope.has(field)) continue;
|
|
313
|
+
const owner = record[field];
|
|
314
|
+
if (!isPlainObject(owner)) continue;
|
|
315
|
+
const value = owner[leaf];
|
|
316
|
+
if (value === void 0 || !matchesKind(value, kind)) throw invalidFieldError(path, kind);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function readRawConfig() {
|
|
320
|
+
if (!existsSync(CONFIG_FILE)) return null;
|
|
321
|
+
try {
|
|
322
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
323
|
+
} catch {
|
|
324
|
+
throw new DvmError(
|
|
325
|
+
"config_corrupt",
|
|
326
|
+
"Config file is corrupt.",
|
|
327
|
+
// Not "delete it": those bytes are the only copy on this machine of the
|
|
328
|
+
// NWC connection string and both private keys, however badly they parse.
|
|
329
|
+
`Run 'dvm config rollback' to restore a stored copy, or move ${CONFIG_FILE} aside yourself and run 'dvm init'.${rollbackSuffix()}`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function writeConfigFile(config) {
|
|
334
|
+
writeJsonAtomic(CONFIG_FILE, config);
|
|
335
|
+
}
|
|
336
|
+
var CONFIG_SNAPSHOT_SLOTS = ["backup", "pinned"];
|
|
337
|
+
function configSnapshotPath(slot) {
|
|
338
|
+
return slot === "pinned" ? CONFIG_PINNED_FILE : CONFIG_BACKUP_FILE;
|
|
339
|
+
}
|
|
340
|
+
function describeConfigBackup(slot = "backup") {
|
|
341
|
+
const path = configSnapshotPath(slot);
|
|
342
|
+
try {
|
|
343
|
+
if (!existsSync(path)) return null;
|
|
344
|
+
const stat = statSync(path);
|
|
345
|
+
const posix = process.platform !== "win32";
|
|
346
|
+
const mode = stat.mode & 511;
|
|
347
|
+
return {
|
|
348
|
+
path,
|
|
349
|
+
slot,
|
|
350
|
+
bytes: stat.size,
|
|
351
|
+
modifiedAt: stat.mtime.toISOString(),
|
|
352
|
+
modeOctal: posix ? toOctal(mode) : null,
|
|
353
|
+
ownerOnly: !posix || (mode & 63) === 0
|
|
354
|
+
};
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function readConfigBackup(slot = "backup") {
|
|
360
|
+
const path = configSnapshotPath(slot);
|
|
361
|
+
if (!existsSync(path)) return null;
|
|
362
|
+
try {
|
|
363
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
364
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function retainConfigBackup(slot = "backup") {
|
|
370
|
+
if (!existsSync(CONFIG_FILE)) return false;
|
|
371
|
+
ensureConfigDir();
|
|
372
|
+
const path = configSnapshotPath(slot);
|
|
373
|
+
const tmp = `${path}.tmp`;
|
|
374
|
+
writeFileSync(tmp, readFileSync(CONFIG_FILE), { mode: 384 });
|
|
375
|
+
chmodSync(tmp, 384);
|
|
376
|
+
commitTmp(tmp, path);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
function writeConfigBackup(config, slot = "backup") {
|
|
380
|
+
writeJsonAtomic(configSnapshotPath(slot), config);
|
|
381
|
+
}
|
|
382
|
+
function removeConfigBackup(slot = "backup") {
|
|
383
|
+
const path = configSnapshotPath(slot);
|
|
384
|
+
if (!existsSync(path)) return false;
|
|
385
|
+
unlinkSync(path);
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
function protectedTargetValue(config, target) {
|
|
389
|
+
const [field, named] = target.split(".");
|
|
390
|
+
const held = config[field];
|
|
391
|
+
const leaves = PROTECTED_FIELDS[field].credentialLeaves;
|
|
392
|
+
const leaf = named ?? (leaves.length > 0 ? leaves[0] : void 0);
|
|
393
|
+
if (leaf === void 0) return held;
|
|
394
|
+
return isPlainObject(held) ? held[leaf] : void 0;
|
|
395
|
+
}
|
|
396
|
+
function protectedValuesMatch(target, left, right) {
|
|
397
|
+
const normalized = comparableProtectedValue(target, left);
|
|
398
|
+
return normalized !== void 0 && normalized === comparableProtectedValue(target, right);
|
|
399
|
+
}
|
|
400
|
+
function isSecretProtectedTarget(target) {
|
|
401
|
+
return PROTECTED_FIELDS[target.split(".")[0]].secret;
|
|
402
|
+
}
|
|
403
|
+
function withoutProtectedTargets(config, targets) {
|
|
404
|
+
const dropping = /* @__PURE__ */ new Set();
|
|
405
|
+
for (const target of targets) {
|
|
406
|
+
const field = target.split(".")[0];
|
|
407
|
+
dropping.add(field);
|
|
408
|
+
for (const alias of PROTECTED_FIELDS[field].aliases) dropping.add(alias);
|
|
409
|
+
}
|
|
410
|
+
return Object.fromEntries(
|
|
411
|
+
Object.entries(config).filter(([field]) => !dropping.has(field))
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
function appendConfigAudit(record) {
|
|
415
|
+
try {
|
|
416
|
+
ensureConfigDir();
|
|
417
|
+
appendFileSync(CONFIG_AUDIT_FILE, JSON.stringify(record) + "\n", { mode: 384 });
|
|
418
|
+
chmodSync(CONFIG_AUDIT_FILE, 384);
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function protectedRemovalError(targets, reason) {
|
|
423
|
+
const named = targets.join(", ");
|
|
424
|
+
const one = targets.length === 1;
|
|
425
|
+
return new DvmError(
|
|
426
|
+
"config_protected_field",
|
|
427
|
+
`The '${reason}' change would remove ${named} from the local configuration.`,
|
|
428
|
+
`Nothing was written. ${named} ${one ? "is a credential or identity pointer" : "are credentials or identity pointers"} nothing else on this machine records, so only a command that names ${one ? "it" : "them"} explicitly may remove ${one ? "it" : "them"} \u2014 'dvm wallet disconnect', 'dvm wallet tempo-disconnect' and 'dvm wallet x402-disconnect' are those commands.`,
|
|
429
|
+
{ fields: targets }
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
var PROTECTED_FIELDS = {
|
|
433
|
+
nwcUri: { aliases: [], credentialLeaves: [], secret: true, hexCredential: false },
|
|
434
|
+
// A pointer at a key, not a key: worth protecting from removal, and never
|
|
435
|
+
// worth stripping out of a stored copy — that is recovery lost for nothing.
|
|
436
|
+
defaultIdentity: { aliases: [], credentialLeaves: [], secret: false, hexCredential: false },
|
|
437
|
+
// `mpp` is the pre-internal-review name; a caller who has not run a mutating command
|
|
438
|
+
// since the rename still has their private key under it.
|
|
439
|
+
tempo: { aliases: ["mpp"], credentialLeaves: ["apiKey"], secret: true, hexCredential: true },
|
|
440
|
+
x402: { aliases: [], credentialLeaves: ["privateKey"], secret: true, hexCredential: true }
|
|
441
|
+
};
|
|
442
|
+
var CONFIG_PATH_SHAPE = {
|
|
443
|
+
float: { statedBudget: {}, connectedAt: {}, alias: {}, methods: {} },
|
|
444
|
+
x402: { privateKey: {}, network: {} },
|
|
445
|
+
tempo: { method: {}, accountId: {}, apiKey: {} },
|
|
446
|
+
credit: {
|
|
447
|
+
targetJobs: {},
|
|
448
|
+
posture: {},
|
|
449
|
+
tier0Cap: {},
|
|
450
|
+
tier1Cap: {},
|
|
451
|
+
pocketTarget: {},
|
|
452
|
+
fundingRail: {},
|
|
453
|
+
perDvm: { "*": { targetJobs: {}, posture: {}, fundingRail: {}, dvmId: {}, builderPubkey: {} } },
|
|
454
|
+
trustOverrides: { "*": {} }
|
|
455
|
+
},
|
|
456
|
+
feedback: { posture: {} }
|
|
457
|
+
};
|
|
458
|
+
var CONFIG_FIELD_TYPES = {
|
|
459
|
+
nwcUri: "string",
|
|
460
|
+
float: "object",
|
|
461
|
+
defaultBudget: "string",
|
|
462
|
+
autoPayThreshold: "string",
|
|
463
|
+
defaultMaxPayments: "number",
|
|
464
|
+
cashuMints: "string[]",
|
|
465
|
+
x402: "object",
|
|
466
|
+
tempo: "object",
|
|
467
|
+
defaultRail: "string",
|
|
468
|
+
defaultIdentity: "string",
|
|
469
|
+
credit: "object",
|
|
470
|
+
feedback: "object"
|
|
471
|
+
};
|
|
472
|
+
var CONFIG_LEAF_TYPES = {
|
|
473
|
+
"float.connectedAt": "string",
|
|
474
|
+
"x402.privateKey": "string",
|
|
475
|
+
"tempo.method": "string"
|
|
476
|
+
};
|
|
477
|
+
function collectChangedPaths(before, after, shape, prefix, found) {
|
|
478
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
479
|
+
const held = before[key];
|
|
480
|
+
const kept = after[key];
|
|
481
|
+
if (deepEqual(held, kept)) continue;
|
|
482
|
+
const known = shape !== void 0 && Object.hasOwn(shape, key);
|
|
483
|
+
const wildcard = !known && shape !== void 0 && Object.hasOwn(shape, "*");
|
|
484
|
+
const child = known ? shape[key] : wildcard ? shape["*"] : void 0;
|
|
485
|
+
const path = prefix ? `${prefix}.${wildcard ? "*" : key}` : key;
|
|
486
|
+
const descendable = child !== void 0 && Object.keys(child).length > 0 && (held === void 0 || isPlainObject(held)) && (kept === void 0 || isPlainObject(kept));
|
|
487
|
+
if (descendable) {
|
|
488
|
+
collectChangedPaths(
|
|
489
|
+
isPlainObject(held) ? held : {},
|
|
490
|
+
isPlainObject(kept) ? kept : {},
|
|
491
|
+
child,
|
|
492
|
+
path,
|
|
493
|
+
found
|
|
494
|
+
);
|
|
495
|
+
} else {
|
|
496
|
+
found.add(path);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function invalidFieldError(path, kind) {
|
|
501
|
+
return new DvmError(
|
|
502
|
+
"config_invalid",
|
|
503
|
+
`Config field '${path}' must be ${describeKind(kind)}.`,
|
|
504
|
+
`Nothing was written. Correct the field in ${CONFIG_FILE}, or run 'dvm doctor' for the full picture.${rollbackSuffix()}`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
function describeKind(kind) {
|
|
508
|
+
switch (kind) {
|
|
509
|
+
case "string":
|
|
510
|
+
return "a string";
|
|
511
|
+
case "number":
|
|
512
|
+
return "a number";
|
|
513
|
+
case "object":
|
|
514
|
+
return "an object";
|
|
515
|
+
case "string[]":
|
|
516
|
+
return "an array of strings";
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function matchesKind(value, kind) {
|
|
520
|
+
switch (kind) {
|
|
521
|
+
case "string":
|
|
522
|
+
return typeof value === "string";
|
|
523
|
+
case "number":
|
|
524
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
525
|
+
case "object":
|
|
526
|
+
return isPlainObject(value);
|
|
527
|
+
case "string[]":
|
|
528
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function rollbackSuffix() {
|
|
532
|
+
const stored = describeConfigBackup() ?? describeConfigBackup("pinned");
|
|
533
|
+
return stored === null ? "" : ` A stored copy of the previous configuration is at ${stored.path}.`;
|
|
534
|
+
}
|
|
535
|
+
function writeJsonAtomic(path, value) {
|
|
536
|
+
ensureConfigDir();
|
|
537
|
+
const tmp = `${path}.tmp`;
|
|
538
|
+
writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
539
|
+
chmodSync(tmp, 384);
|
|
540
|
+
commitTmp(tmp, path);
|
|
541
|
+
}
|
|
542
|
+
function commitTmp(tmp, path) {
|
|
543
|
+
try {
|
|
544
|
+
renameSync(tmp, path);
|
|
545
|
+
} catch (err) {
|
|
546
|
+
try {
|
|
547
|
+
unlinkSync(tmp);
|
|
548
|
+
} catch {
|
|
549
|
+
}
|
|
550
|
+
throw err;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function deepEqual(a, b) {
|
|
554
|
+
if (a === b) return true;
|
|
555
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
556
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
557
|
+
return a.every((item, index) => deepEqual(item, b[index]));
|
|
558
|
+
}
|
|
559
|
+
if (!isPlainObject(a) || !isPlainObject(b)) return false;
|
|
560
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
561
|
+
if (!deepEqual(a[key], b[key])) return false;
|
|
562
|
+
}
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
function isPresentValue(value) {
|
|
566
|
+
if (value === void 0 || value === null) return false;
|
|
567
|
+
return typeof value !== "string" || value.trim().length > 0;
|
|
568
|
+
}
|
|
569
|
+
function comparableProtectedValue(target, value) {
|
|
570
|
+
if (typeof value !== "string") return void 0;
|
|
571
|
+
const trimmed = value.trim();
|
|
572
|
+
if (trimmed.length === 0) return void 0;
|
|
573
|
+
const field = target.split(".")[0];
|
|
574
|
+
return PROTECTED_FIELDS[field].hexCredential ? trimmed.toLowerCase() : trimmed;
|
|
575
|
+
}
|
|
576
|
+
function patchShapeError(message) {
|
|
577
|
+
return new DvmError(
|
|
578
|
+
"config_patch_invalid",
|
|
579
|
+
message,
|
|
580
|
+
"A change declares `set` (fields to write) and `unset` (field names to remove). Anything else cannot be applied, and nothing was written."
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
function freezeDeep(value) {
|
|
584
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return;
|
|
585
|
+
Object.freeze(value);
|
|
586
|
+
for (const nested of Object.values(value)) freezeDeep(nested);
|
|
587
|
+
}
|
|
588
|
+
function isPlainObject(value) {
|
|
589
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
590
|
+
}
|
|
591
|
+
function toOctal(mode) {
|
|
592
|
+
return "0" + mode.toString(8).padStart(3, "0");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/lib/identity.ts
|
|
596
|
+
function migrateCreditConfig(config) {
|
|
597
|
+
const credit = config.credit;
|
|
598
|
+
if (!credit) return config;
|
|
599
|
+
const migrateLayer = (layer) => {
|
|
600
|
+
const railed = migrateFundingRail(layer);
|
|
601
|
+
if (!("disabled" in railed)) return railed;
|
|
602
|
+
const { disabled, ...rest } = railed;
|
|
603
|
+
const migrated = { ...rest };
|
|
604
|
+
if (migrated.posture === void 0 && disabled === true) migrated.posture = "off";
|
|
605
|
+
return migrated;
|
|
606
|
+
};
|
|
607
|
+
const perDvm = credit.perDvm;
|
|
608
|
+
return {
|
|
609
|
+
...config,
|
|
610
|
+
credit: {
|
|
611
|
+
...migrateLayer(credit),
|
|
612
|
+
...perDvm ? {
|
|
613
|
+
perDvm: Object.fromEntries(
|
|
614
|
+
Object.entries(perDvm).map(([endpoint, per]) => [endpoint, migrateLayer(per)])
|
|
615
|
+
)
|
|
616
|
+
} : {}
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function migrateFundingRail(layer) {
|
|
621
|
+
const loose = layer;
|
|
622
|
+
if (loose.fundingRail !== "mpp") return layer;
|
|
623
|
+
return { ...layer, fundingRail: "tempo" };
|
|
624
|
+
}
|
|
625
|
+
function migrateTempoWalletKey(config) {
|
|
626
|
+
const loose = config;
|
|
627
|
+
if (!loose.mpp || loose.tempo) return config;
|
|
628
|
+
const { mpp, ...rest } = loose;
|
|
629
|
+
return { ...rest, tempo: mpp };
|
|
630
|
+
}
|
|
631
|
+
function migrateConfig(config) {
|
|
632
|
+
return migrateCreditConfig(migrateTempoWalletKey(config));
|
|
633
|
+
}
|
|
634
|
+
function loadConfig() {
|
|
635
|
+
const raw = readRawConfig();
|
|
636
|
+
return raw === null ? null : migrateConfig(raw);
|
|
637
|
+
}
|
|
638
|
+
async function updateConfig(update) {
|
|
639
|
+
return withConfigLock(async () => {
|
|
640
|
+
const raw = readRawConfig();
|
|
641
|
+
const snapshot = raw === null ? {} : structuredClone(raw);
|
|
642
|
+
const base = raw === null ? null : migrateConfig(structuredClone(raw));
|
|
643
|
+
const handed = raw === null ? null : deepFreezeConfig(migrateConfig(structuredClone(raw)));
|
|
644
|
+
const next = applyConfigPatch(base ?? {}, await runPatch(update, handed));
|
|
645
|
+
const changed = changedConfigPaths(snapshot, next);
|
|
646
|
+
if (changed.length === 0) {
|
|
647
|
+
return {
|
|
648
|
+
config: base ?? {},
|
|
649
|
+
previous: base,
|
|
650
|
+
changed: [],
|
|
651
|
+
written: false,
|
|
652
|
+
backup: describeConfigBackup()
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
const removals = protectedRemovals(base ?? {}, next);
|
|
656
|
+
const unauthorized = removals.filter((target) => update.authorize?.covers(target) !== true);
|
|
657
|
+
if (unauthorized.length > 0) throw protectedRemovalError(unauthorized, update.reason);
|
|
658
|
+
assertValidConfig(
|
|
659
|
+
next,
|
|
660
|
+
changed.map((path) => path.split(".")[0])
|
|
661
|
+
);
|
|
662
|
+
const superseded = supersededProtectedSecrets(base ?? {}, next);
|
|
663
|
+
const stripped = [
|
|
664
|
+
.../* @__PURE__ */ new Set([...removals, ...superseded, ...update.sanitizeBackup ?? []])
|
|
665
|
+
].filter(isSecretProtectedTarget);
|
|
666
|
+
const sweeping = update.sweepSlots ?? CONFIG_SNAPSHOT_SLOTS;
|
|
667
|
+
const swept = [];
|
|
668
|
+
if (raw !== null) {
|
|
669
|
+
if (stripped.length > 0) {
|
|
670
|
+
writeConfigBackup(withoutProtectedTargets(raw, stripped));
|
|
671
|
+
swept.push("backup");
|
|
672
|
+
} else {
|
|
673
|
+
retainConfigBackup();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (stripped.length > 0 && sweeping.includes("pinned")) {
|
|
677
|
+
if (sweepPinnedSnapshot(stripped, base ?? {}, update.reason)) swept.push("pinned");
|
|
678
|
+
}
|
|
679
|
+
const backup = describeConfigBackup();
|
|
680
|
+
writeConfigFile(next);
|
|
681
|
+
appendConfigAudit({
|
|
682
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
683
|
+
event: "update",
|
|
684
|
+
reason: update.reason,
|
|
685
|
+
fields: changed,
|
|
686
|
+
...removals.length > 0 ? { removed: removals } : {},
|
|
687
|
+
...superseded.length > 0 ? { superseded } : {},
|
|
688
|
+
...swept.length > 0 ? { sweptSlots: swept } : {},
|
|
689
|
+
backup: backup !== null,
|
|
690
|
+
pid: process.pid
|
|
691
|
+
});
|
|
692
|
+
return { config: next, previous: base, changed, written: true, backup };
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
async function withConfigLock(fn) {
|
|
696
|
+
ensureConfigDir();
|
|
697
|
+
let release;
|
|
698
|
+
try {
|
|
699
|
+
release = await lockfile.lock(CONFIG_FILE, {
|
|
700
|
+
lockfilePath: `${CONFIG_FILE}.lock`,
|
|
701
|
+
realpath: false,
|
|
702
|
+
stale: 3e4,
|
|
703
|
+
retries: { retries: 10, factor: 1, minTimeout: 100, maxTimeout: 100 }
|
|
704
|
+
});
|
|
705
|
+
} catch {
|
|
706
|
+
throw new DvmError(
|
|
707
|
+
"config_locked",
|
|
708
|
+
"Another dvm process is updating the local configuration.",
|
|
709
|
+
"Wait a moment and retry."
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
return await fn();
|
|
714
|
+
} finally {
|
|
715
|
+
await release().catch(() => void 0);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async function runPatch(update, current) {
|
|
719
|
+
try {
|
|
720
|
+
return await update.patch(current);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
if (err instanceof TypeError && /read only|not extensible|frozen|cannot delete property|cannot add property/i.test(
|
|
723
|
+
err.message
|
|
724
|
+
)) {
|
|
725
|
+
throw new DvmError(
|
|
726
|
+
"config_patch_invalid",
|
|
727
|
+
`The '${update.reason}' change tried to modify the configuration it was handed.`,
|
|
728
|
+
"Nothing was written. A change declares what it wants through its `set` and `unset` lists; editing the current config in place bypasses the protected-field gate, so the object is frozen."
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
throw err;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function sweepPinnedSnapshot(targets, departing, reason) {
|
|
735
|
+
if (describeConfigBackup("pinned") === null) return false;
|
|
736
|
+
const pinned = readConfigBackup("pinned");
|
|
737
|
+
if (pinned === null) {
|
|
738
|
+
return retireSlot(`${reason}:unreadable_pin`, "pinned");
|
|
739
|
+
}
|
|
740
|
+
const migrated = migrateConfig(structuredClone(pinned));
|
|
741
|
+
const matching = targets.filter(
|
|
742
|
+
(target) => protectedValuesMatch(
|
|
743
|
+
target,
|
|
744
|
+
protectedTargetValue(departing, target),
|
|
745
|
+
protectedTargetValue(migrated, target)
|
|
746
|
+
)
|
|
747
|
+
);
|
|
748
|
+
if (matching.length === 0) return false;
|
|
749
|
+
const sanitized = withoutProtectedTargets(pinned, matching);
|
|
750
|
+
const changed = changedConfigPaths(pinned, sanitized);
|
|
751
|
+
if (changed.length === 0) return false;
|
|
752
|
+
writeConfigBackup(sanitized, "pinned");
|
|
753
|
+
appendConfigAudit({
|
|
754
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
755
|
+
event: "backup_sanitized",
|
|
756
|
+
reason,
|
|
757
|
+
fields: changed,
|
|
758
|
+
removed: matching,
|
|
759
|
+
slot: "pinned",
|
|
760
|
+
backup: describeConfigBackup() !== null,
|
|
761
|
+
pid: process.pid
|
|
762
|
+
});
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
function retireSlot(reason, slot) {
|
|
766
|
+
if (!removeConfigBackup(slot)) return false;
|
|
767
|
+
appendConfigAudit({
|
|
768
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
769
|
+
event: "backup_retired",
|
|
770
|
+
reason,
|
|
771
|
+
fields: [],
|
|
772
|
+
slot,
|
|
773
|
+
backup: describeConfigBackup() !== null,
|
|
774
|
+
pid: process.pid
|
|
775
|
+
});
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/lib/x402/channel-store.ts
|
|
780
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
781
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
782
|
+
import lockfile3 from "proper-lockfile";
|
|
783
|
+
|
|
784
|
+
// src/lib/credit-store.ts
|
|
785
|
+
import lockfile2 from "proper-lockfile";
|
|
786
|
+
|
|
787
|
+
// src/lib/x402/channel-store.ts
|
|
788
|
+
var X402_PENDING_STALE_MS = 24 * 60 * 60 * 1e3;
|
|
789
|
+
var EMPTY = { version: 1, channels: {}, pending: [], exact: [] };
|
|
790
|
+
var x402LockOwner = new AsyncLocalStorage();
|
|
791
|
+
async function withX402ChannelLock(operation) {
|
|
792
|
+
if (x402LockOwner.getStore()?.active) return operation();
|
|
793
|
+
ensureX402ChannelFile();
|
|
794
|
+
const release = await lockfile3.lock(X402_CHANNELS_FILE, {
|
|
795
|
+
realpath: false,
|
|
796
|
+
retries: { retries: 20, minTimeout: 25, maxTimeout: 250 }
|
|
797
|
+
});
|
|
798
|
+
const owner = { active: true };
|
|
799
|
+
try {
|
|
800
|
+
return await x402LockOwner.run(owner, operation);
|
|
801
|
+
} finally {
|
|
802
|
+
owner.active = false;
|
|
803
|
+
await release();
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function ensureX402ChannelFile() {
|
|
807
|
+
ensureConfigDir();
|
|
808
|
+
if (existsSync2(X402_CHANNELS_FILE)) return;
|
|
809
|
+
try {
|
|
810
|
+
writeFileSync2(X402_CHANNELS_FILE, `${JSON.stringify(EMPTY, null, 2)}
|
|
811
|
+
`, {
|
|
812
|
+
mode: 384,
|
|
813
|
+
flag: "wx"
|
|
814
|
+
});
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/lib/x402/wallet.ts
|
|
820
|
+
function loadX402Wallet() {
|
|
821
|
+
const config = loadConfig();
|
|
822
|
+
const x402 = config?.x402;
|
|
823
|
+
const privateKey = x402?.privateKey;
|
|
824
|
+
if (!privateKey || !isValidEvmPrivateKey(privateKey)) return null;
|
|
825
|
+
return {
|
|
826
|
+
privateKey,
|
|
827
|
+
address: "",
|
|
828
|
+
network: x402.network ?? X402_DEFAULT_NETWORK
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
async function ensureAddress(wallet) {
|
|
832
|
+
if (!isValidEvmPrivateKey(wallet.privateKey)) throw invalidX402PrivateKey();
|
|
833
|
+
if (wallet.address) return wallet;
|
|
834
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
835
|
+
const account = privateKeyToAccount(wallet.privateKey);
|
|
836
|
+
return { ...wallet, address: account.address };
|
|
837
|
+
}
|
|
838
|
+
async function connectedX402Payer() {
|
|
839
|
+
const wallet = loadX402Wallet();
|
|
840
|
+
if (!wallet) return null;
|
|
841
|
+
return (await ensureAddress(wallet)).address;
|
|
842
|
+
}
|
|
843
|
+
async function generateX402PrivateKey() {
|
|
844
|
+
const { generatePrivateKey } = await import("viem/accounts");
|
|
845
|
+
return generatePrivateKey();
|
|
846
|
+
}
|
|
847
|
+
async function repinX402WalletNetwork(network) {
|
|
848
|
+
assertSupportedX402Network(network);
|
|
849
|
+
return withX402ChannelLock(async () => {
|
|
850
|
+
const { previous } = await updateConfig({
|
|
851
|
+
reason: "x402_wallet_repin",
|
|
852
|
+
patch: (current) => {
|
|
853
|
+
if (!current) throw new DvmError("no_config", "No config found.", "Run: dvm init");
|
|
854
|
+
const privateKey = current.x402?.privateKey;
|
|
855
|
+
if (privateKey === void 0) throw x402WalletMissingError();
|
|
856
|
+
if (!isValidEvmPrivateKey(privateKey)) throw invalidX402PrivateKey();
|
|
857
|
+
return { set: { x402: { privateKey, network } } };
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
const stored = previous?.x402;
|
|
861
|
+
if (stored === void 0) throw x402WalletMissingError();
|
|
862
|
+
if (!isValidEvmPrivateKey(stored.privateKey)) throw invalidX402PrivateKey();
|
|
863
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
864
|
+
return {
|
|
865
|
+
address: privateKeyToAccount(stored.privateKey).address,
|
|
866
|
+
previousNetwork: stored.network ?? X402_DEFAULT_NETWORK,
|
|
867
|
+
network
|
|
868
|
+
};
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
async function saveX402Wallet(privateKey, network) {
|
|
872
|
+
if (network !== void 0) assertSupportedX402Network(network);
|
|
873
|
+
if (!isValidEvmPrivateKey(privateKey)) throw invalidX402PrivateKey();
|
|
874
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
875
|
+
const account = privateKeyToAccount(privateKey);
|
|
876
|
+
let resolved;
|
|
877
|
+
await updateConfig({
|
|
878
|
+
reason: "x402_wallet_connect",
|
|
879
|
+
patch: (current) => {
|
|
880
|
+
resolved = network ?? current?.x402?.network;
|
|
881
|
+
return {
|
|
882
|
+
set: { x402: resolved === void 0 ? { privateKey } : { privateKey, network: resolved } }
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
return { address: account.address, network: resolved ?? X402_DEFAULT_NETWORK };
|
|
887
|
+
}
|
|
888
|
+
async function removeX402Wallet() {
|
|
889
|
+
await updateConfig({
|
|
890
|
+
reason: "x402_wallet_disconnect",
|
|
891
|
+
authorize: authorizeProtectedRemoval("x402"),
|
|
892
|
+
patch: () => ({ unset: ["x402"] })
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
function invalidX402PrivateKey() {
|
|
896
|
+
return new DvmError(
|
|
897
|
+
"invalid_key",
|
|
898
|
+
"Invalid secp256k1 private key.",
|
|
899
|
+
"Reconnect with `dvm wallet x402-connect --key-file <path>`, set DVM_X402_KEY, or pipe a valid 0x-prefixed 32-byte secp256k1 private key through stdin."
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
function x402WalletMissingError() {
|
|
903
|
+
return new DvmError(
|
|
904
|
+
"x402_wallet_missing",
|
|
905
|
+
"No x402 wallet is connected.",
|
|
906
|
+
"Run 'dvm wallet x402-connect' to create one, or add --key-file <path> to import a key you already hold."
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
async function getX402Balance(wallet) {
|
|
910
|
+
const w = await ensureAddress(wallet);
|
|
911
|
+
const spec = x402NetworkByCaip2(w.network);
|
|
912
|
+
if (!spec) {
|
|
913
|
+
throw new DvmError(
|
|
914
|
+
"x402_unsupported_network",
|
|
915
|
+
`The x402 wallet is configured for ${w.network}, a chain this rail can't settle on.`,
|
|
916
|
+
`${x402SupportedNetworksHint()} Re-pin the connected wallet without handling its key: 'dvm wallet x402-network ${X402_DEFAULT_NETWORK}'.`
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
const chains = await import("viem/chains");
|
|
920
|
+
const { createPublicClient, http, erc20Abi } = await import("viem");
|
|
921
|
+
const chain = chains[spec.viemChain];
|
|
922
|
+
const { url: rpcUrl, source: rpcSource } = resolveRpcOverride(
|
|
923
|
+
process.env.DVM_X402_RPC_URL,
|
|
924
|
+
chain?.rpcUrls.default.http[0]
|
|
925
|
+
);
|
|
926
|
+
try {
|
|
927
|
+
const transportOptions = resolveRpcHttpTransportOptions(
|
|
928
|
+
rpcSource,
|
|
929
|
+
process.env.DVM_X402_RPC_HEADERS_JSON
|
|
930
|
+
);
|
|
931
|
+
const client = createPublicClient({
|
|
932
|
+
chain,
|
|
933
|
+
transport: transportOptions ? http(rpcUrl, transportOptions) : http(rpcUrl)
|
|
934
|
+
});
|
|
935
|
+
const balance = await client.readContract({
|
|
936
|
+
address: spec.usdc,
|
|
937
|
+
abi: erc20Abi,
|
|
938
|
+
functionName: "balanceOf",
|
|
939
|
+
args: [w.address]
|
|
940
|
+
});
|
|
941
|
+
return {
|
|
942
|
+
address: w.address,
|
|
943
|
+
usdc: Number(balance) / 1e6,
|
|
944
|
+
network: w.network,
|
|
945
|
+
tokenAddress: spec.usdc,
|
|
946
|
+
// internal-review: redacted here rather than at a display surface, so the clear
|
|
947
|
+
// endpoint never leaves this function on the success path either.
|
|
948
|
+
rpcUrlRedacted: rpcUrl ? redactUrl(rpcUrl) : void 0,
|
|
949
|
+
rpcSource
|
|
950
|
+
};
|
|
951
|
+
} catch (err) {
|
|
952
|
+
const cause = redactUrlsInText(err instanceof Error ? err.message : String(err));
|
|
953
|
+
throw new DvmError(
|
|
954
|
+
"x402_rpc_error",
|
|
955
|
+
`x402 USDC balance lookup on ${spec.label} failed: ${cause}`,
|
|
956
|
+
// A caller who set the override doesn't need to be told to set it. Which
|
|
957
|
+
// knob is theirs to turn depends on whose endpoint this was.
|
|
958
|
+
rpcSource === "override" ? `DVM_X402_RPC_URL sent this read to ${redactUrl(rpcUrl)}. Correct it, or unset it to fall back to ${spec.label}'s bundled endpoint.` : `Verify connectivity to ${spec.label}'s bundled endpoint${rpcUrl ? ` (${redactUrl(rpcUrl)})` : ""}, or send the read somewhere else with DVM_X402_RPC_URL.`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/lib/x402/client.ts
|
|
964
|
+
async function tryX402Payment(request, wallet) {
|
|
965
|
+
const selection = selectPaymentRequirement(request);
|
|
966
|
+
let networkSlug;
|
|
967
|
+
let networkCaip2;
|
|
968
|
+
let chainId;
|
|
969
|
+
let usdc;
|
|
970
|
+
try {
|
|
971
|
+
networkCaip2 = x402NetworkToCaip2(selection.network);
|
|
972
|
+
const mapped = x402NetworkByCaip2(networkCaip2);
|
|
973
|
+
networkSlug = mapped?.slug ?? selection.network;
|
|
974
|
+
chainId = chainIdFromCaip2(networkCaip2);
|
|
975
|
+
usdc = usdcContractFor(networkSlug);
|
|
976
|
+
} catch (err) {
|
|
977
|
+
throw new DvmError(
|
|
978
|
+
"x402_build_failed",
|
|
979
|
+
`Could not resolve x402 network from '${selection.network}': ${err instanceof Error ? err.message : String(err)}`,
|
|
980
|
+
"Check that the DVM's advertised x402.network is a CAIP-2 chain id (e.g. 'eip155:8453' for Base)."
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
if (!usdc) return null;
|
|
984
|
+
const rate = await priceableRate(request.btcRate);
|
|
985
|
+
if (rate === null) return null;
|
|
986
|
+
const localUsdcMicro = BigInt(msatsToUsdc(request.amountMsats, rate));
|
|
987
|
+
const requiredUsdcMicro = resolveRequiredUsdcMicro(selection.amount, localUsdcMicro);
|
|
988
|
+
const signedAmountMsats = usdcToMsats(Number(requiredUsdcMicro) / 1e6, rate);
|
|
989
|
+
try {
|
|
990
|
+
const w = await ensureAddress(wallet);
|
|
991
|
+
const validAfter = 0n;
|
|
992
|
+
const validBefore = BigInt(
|
|
993
|
+
Math.floor(Date.now() / 1e3) + X402_EXACT_AUTHORIZATION_TTL_SECONDS
|
|
994
|
+
);
|
|
995
|
+
const nonce = request.nonce ?? `0x${randomBytes(32).toString("hex")}`;
|
|
996
|
+
const domainName = selectedDomainField(selection.accepted?.extra?.name, "name") ?? usdcDomainNameFor(networkSlug) ?? "USD Coin";
|
|
997
|
+
const domainVersion = selectedDomainField(selection.accepted?.extra?.version, "version") ?? usdcDomainVersionFor(networkSlug) ?? "2";
|
|
998
|
+
const { signTypedData } = await import("viem/accounts");
|
|
999
|
+
const signature = await signTypedData({
|
|
1000
|
+
privateKey: w.privateKey,
|
|
1001
|
+
domain: {
|
|
1002
|
+
name: domainName,
|
|
1003
|
+
version: domainVersion,
|
|
1004
|
+
chainId,
|
|
1005
|
+
verifyingContract: usdc
|
|
1006
|
+
},
|
|
1007
|
+
types: {
|
|
1008
|
+
TransferWithAuthorization: [
|
|
1009
|
+
{ name: "from", type: "address" },
|
|
1010
|
+
{ name: "to", type: "address" },
|
|
1011
|
+
{ name: "value", type: "uint256" },
|
|
1012
|
+
{ name: "validAfter", type: "uint256" },
|
|
1013
|
+
{ name: "validBefore", type: "uint256" },
|
|
1014
|
+
{ name: "nonce", type: "bytes32" }
|
|
1015
|
+
]
|
|
1016
|
+
},
|
|
1017
|
+
primaryType: "TransferWithAuthorization",
|
|
1018
|
+
message: {
|
|
1019
|
+
from: w.address,
|
|
1020
|
+
to: selection.payTo,
|
|
1021
|
+
value: requiredUsdcMicro,
|
|
1022
|
+
validAfter,
|
|
1023
|
+
validBefore,
|
|
1024
|
+
nonce
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
const exactPayload = {
|
|
1028
|
+
signature,
|
|
1029
|
+
payload: {
|
|
1030
|
+
authorization: {
|
|
1031
|
+
from: w.address,
|
|
1032
|
+
to: selection.payTo,
|
|
1033
|
+
value: requiredUsdcMicro.toString(),
|
|
1034
|
+
validAfter: validAfter.toString(),
|
|
1035
|
+
validBefore: validBefore.toString(),
|
|
1036
|
+
nonce
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
const payload = selection.version === X402_V1_VERSION ? {
|
|
1041
|
+
x402Version: X402_V1_VERSION,
|
|
1042
|
+
scheme: "exact",
|
|
1043
|
+
network: networkSlug,
|
|
1044
|
+
payload: { signature: exactPayload.signature, ...exactPayload.payload }
|
|
1045
|
+
} : {
|
|
1046
|
+
x402Version: X402_VERSION,
|
|
1047
|
+
...selection.resource ? { resource: selection.resource } : {},
|
|
1048
|
+
accepted: selection.accepted ?? buildPaymentRequirements({
|
|
1049
|
+
x402Config: {
|
|
1050
|
+
payTo: selection.payTo,
|
|
1051
|
+
network: networkCaip2,
|
|
1052
|
+
...selection.asset ? { asset: selection.asset } : {}
|
|
1053
|
+
},
|
|
1054
|
+
requiredUsdcMicro,
|
|
1055
|
+
resource: selection.resource?.url ?? "",
|
|
1056
|
+
version: X402_VERSION
|
|
1057
|
+
}),
|
|
1058
|
+
payload: { signature: exactPayload.signature, ...exactPayload.payload },
|
|
1059
|
+
extensions: selection.extensions ?? {}
|
|
1060
|
+
};
|
|
1061
|
+
return {
|
|
1062
|
+
paymentHeader: encodePayment(payload),
|
|
1063
|
+
x402Version: selection.version,
|
|
1064
|
+
amountUsdcMicro: requiredUsdcMicro.toString(),
|
|
1065
|
+
amountMsats: signedAmountMsats,
|
|
1066
|
+
btcRate: rate
|
|
1067
|
+
};
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
throw new DvmError(
|
|
1070
|
+
"x402_build_failed",
|
|
1071
|
+
`Failed to build x402 payment: ${err instanceof Error ? err.message : String(err)}`,
|
|
1072
|
+
"Check the configured x402 private key and RPC reachability for the target chain."
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
function isSignableExactRequirement(requirement) {
|
|
1077
|
+
const transferMethod = requirement.extra?.assetTransferMethod;
|
|
1078
|
+
return requirement.scheme === "exact" && (transferMethod === void 0 || transferMethod === "eip3009");
|
|
1079
|
+
}
|
|
1080
|
+
function selectPaymentRequirement(request) {
|
|
1081
|
+
const challenge = request.paymentRequired;
|
|
1082
|
+
if (challenge?.x402Version === X402_VERSION) {
|
|
1083
|
+
const accepted = challenge.accepts.find(isSignableExactRequirement);
|
|
1084
|
+
if (!accepted) {
|
|
1085
|
+
throw invalidChallenge("The DVM's v2 x402 challenge has no exact EIP-3009 requirement.");
|
|
1086
|
+
}
|
|
1087
|
+
return {
|
|
1088
|
+
version: X402_VERSION,
|
|
1089
|
+
network: accepted.network,
|
|
1090
|
+
amount: accepted.amount,
|
|
1091
|
+
payTo: accepted.payTo,
|
|
1092
|
+
asset: accepted.asset,
|
|
1093
|
+
accepted,
|
|
1094
|
+
resource: challenge.resource,
|
|
1095
|
+
extensions: challenge.extensions
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
if (challenge?.x402Version === X402_V1_VERSION) {
|
|
1099
|
+
const accepted = challenge.accepts.find((candidate) => candidate.scheme === "exact");
|
|
1100
|
+
if (!accepted) throw invalidChallenge("The DVM's v1 x402 challenge has no exact requirement.");
|
|
1101
|
+
return {
|
|
1102
|
+
version: X402_V1_VERSION,
|
|
1103
|
+
network: accepted.network,
|
|
1104
|
+
amount: accepted.maxAmountRequired,
|
|
1105
|
+
payTo: accepted.payTo,
|
|
1106
|
+
asset: accepted.asset
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
return {
|
|
1110
|
+
version: request.x402Version ?? X402_V1_VERSION,
|
|
1111
|
+
network: request.network,
|
|
1112
|
+
amount: request.requiredUsdcMicro,
|
|
1113
|
+
payTo: request.payTo,
|
|
1114
|
+
asset: request.asset,
|
|
1115
|
+
resource: request.resource
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function invalidChallenge(message) {
|
|
1119
|
+
return new DvmError(
|
|
1120
|
+
"x402_build_failed",
|
|
1121
|
+
message,
|
|
1122
|
+
"Ask the DVM operator to advertise an exact x402 payment requirement."
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
function selectedDomainField(value, field) {
|
|
1126
|
+
if (value === void 0 || value === null) return void 0;
|
|
1127
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
1128
|
+
throw invalidChallenge(`The DVM advertised an invalid x402 ${field}.`);
|
|
1129
|
+
}
|
|
1130
|
+
function resolveRequiredUsdcMicro(advertised, localUsdcMicro) {
|
|
1131
|
+
if (advertised === void 0) return localUsdcMicro;
|
|
1132
|
+
if (!/^[1-9]\d*$/.test(advertised)) {
|
|
1133
|
+
throw new DvmError(
|
|
1134
|
+
"x402_amount_invalid",
|
|
1135
|
+
`The DVM advertised an invalid x402 maxAmountRequired: '${advertised}'.`,
|
|
1136
|
+
"Do not sign this payment. Ask the DVM operator to return a positive integer number of USDC micro-units."
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
const required = BigInt(advertised);
|
|
1140
|
+
if (withinAdvertisedTolerance(required, localUsdcMicro)) return required;
|
|
1141
|
+
const advertisedUsd = Number(required) / 1e6;
|
|
1142
|
+
const localUsd = Number(localUsdcMicro) / 1e6;
|
|
1143
|
+
throw new DvmError(
|
|
1144
|
+
"x402_amount_exceeds_limit",
|
|
1145
|
+
`The DVM's x402 charge ($${formatUsd(advertisedUsd)} USDC) is more than 10% above the caller's local price ($${formatUsd(localUsd)} USDC).`,
|
|
1146
|
+
"The payment was not signed. Refresh the quote or ask the DVM operator to check its BTC/USD rate.",
|
|
1147
|
+
{
|
|
1148
|
+
advertised_usdc_microunits: required.toString(),
|
|
1149
|
+
local_usdc_microunits: localUsdcMicro.toString(),
|
|
1150
|
+
max_usdc_microunits: maxAdvertisedMicro(localUsdcMicro).toString(),
|
|
1151
|
+
anchor: costAnchor(advertisedUsd),
|
|
1152
|
+
display: `Payment refused: $${formatUsd(advertisedUsd)} USDC exceeds the 10% safety margin over the caller's $${formatUsd(localUsd)} USDC estimate.`
|
|
1153
|
+
}
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
async function priceableRate(btcRate) {
|
|
1157
|
+
if (btcRate && btcRate > 0) return btcRate;
|
|
1158
|
+
try {
|
|
1159
|
+
return (await resolveBtcUsdRate("payment")).rate;
|
|
1160
|
+
} catch (err) {
|
|
1161
|
+
if (err instanceof DvmError && err.code === "rate_unavailable") {
|
|
1162
|
+
info(
|
|
1163
|
+
"Skipping the x402 rail: it prices in USDC and the BTC/USD rate is unavailable (no cached rate on this machine). Trying the remaining rails."
|
|
1164
|
+
);
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
throw err;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/lib/x402/verify.ts
|
|
1172
|
+
async function verifyX402Payment(paymentHeader, config, requiredUsdcMicro, resource, btcUsdRate, exactVersions = { v1: true, v2: true }) {
|
|
1173
|
+
return callerLoggers.x402.span(
|
|
1174
|
+
"x402.verify_payment",
|
|
1175
|
+
{
|
|
1176
|
+
resource,
|
|
1177
|
+
required_usdc_micro: Number(requiredUsdcMicro),
|
|
1178
|
+
facilitator_url: config.facilitator
|
|
1179
|
+
},
|
|
1180
|
+
async () => {
|
|
1181
|
+
let payload;
|
|
1182
|
+
try {
|
|
1183
|
+
payload = decodePayment(paymentHeader);
|
|
1184
|
+
} catch {
|
|
1185
|
+
return { verified: false, amountMsats: 0, invalidReason: "decode_failed" };
|
|
1186
|
+
}
|
|
1187
|
+
if (payload.x402Version === 1 && !exactVersions.v1 || payload.x402Version === 2 && !exactVersions.v2) {
|
|
1188
|
+
return { verified: false, amountMsats: 0, invalidReason: "unsupported_version" };
|
|
1189
|
+
}
|
|
1190
|
+
const requirementOpts = { x402Config: config, requiredUsdcMicro, resource };
|
|
1191
|
+
const requirements = payload.x402Version === 1 ? buildPaymentRequirements({ ...requirementOpts, version: 1 }) : buildPaymentRequirements({ ...requirementOpts, version: 2 });
|
|
1192
|
+
const createAuthHeaders = createX402FacilitatorAuthHeaders(config);
|
|
1193
|
+
const verifyRes = await verifyWithFacilitator(
|
|
1194
|
+
payload,
|
|
1195
|
+
requirements,
|
|
1196
|
+
config,
|
|
1197
|
+
createAuthHeaders
|
|
1198
|
+
);
|
|
1199
|
+
if (!verifyRes.isValid) {
|
|
1200
|
+
return {
|
|
1201
|
+
verified: false,
|
|
1202
|
+
amountMsats: 0,
|
|
1203
|
+
invalidReason: verifyRes.invalidReason ?? "verify_failed"
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
const settleRes = await settleWithFacilitator(
|
|
1207
|
+
payload,
|
|
1208
|
+
requirements,
|
|
1209
|
+
config,
|
|
1210
|
+
createAuthHeaders
|
|
1211
|
+
);
|
|
1212
|
+
if (!settleRes.success) {
|
|
1213
|
+
return {
|
|
1214
|
+
verified: false,
|
|
1215
|
+
amountMsats: 0,
|
|
1216
|
+
invalidReason: settleRes.errorReason ?? "settle_failed"
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
const amountUsdcMicro = Number.parseInt(exactEvmAuthorization(payload).value, 10);
|
|
1220
|
+
const amountMsats = usdcToMsats(amountUsdcMicro / 1e6, btcUsdRate);
|
|
1221
|
+
callerLoggers.x402.info("x402.verify_payment.result", {
|
|
1222
|
+
verified: true,
|
|
1223
|
+
tx_hash: settleRes.transaction,
|
|
1224
|
+
amount_msats: amountMsats
|
|
1225
|
+
});
|
|
1226
|
+
return {
|
|
1227
|
+
verified: true,
|
|
1228
|
+
amountMsats,
|
|
1229
|
+
amountUsdcMicro,
|
|
1230
|
+
txHash: settleRes.transaction,
|
|
1231
|
+
settleResponseHeader: encodeSettleResponseHeader(settleRes)
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
export {
|
|
1237
|
+
X402_DEFAULT_FACILITATOR,
|
|
1238
|
+
X402_DEFAULT_NETWORK,
|
|
1239
|
+
X402_NETWORKS,
|
|
1240
|
+
X402_V1_VERSION,
|
|
1241
|
+
X402_VERSION,
|
|
1242
|
+
assertSupportedX402Network,
|
|
1243
|
+
buildPaymentRequiredV2,
|
|
1244
|
+
buildPaymentRequirements,
|
|
1245
|
+
caip2ToX402Network,
|
|
1246
|
+
chainIdFromCaip2,
|
|
1247
|
+
connectedX402Payer,
|
|
1248
|
+
decodePayment,
|
|
1249
|
+
decodePaymentRequiredHeader,
|
|
1250
|
+
encodePayment,
|
|
1251
|
+
encodePaymentRequiredHeader,
|
|
1252
|
+
encodeSettleResponseHeader,
|
|
1253
|
+
ensureAddress,
|
|
1254
|
+
exactEvmAuthorization,
|
|
1255
|
+
generateX402PrivateKey,
|
|
1256
|
+
getX402Balance,
|
|
1257
|
+
loadX402Wallet,
|
|
1258
|
+
paymentRequiredV2FromV1,
|
|
1259
|
+
removeX402Wallet,
|
|
1260
|
+
repinX402WalletNetwork,
|
|
1261
|
+
resolveX402Asset,
|
|
1262
|
+
saveX402Wallet,
|
|
1263
|
+
tryX402Payment,
|
|
1264
|
+
usdcContractByCaip2,
|
|
1265
|
+
verifyX402Payment,
|
|
1266
|
+
x402NetworkByCaip2,
|
|
1267
|
+
x402NetworkLabel,
|
|
1268
|
+
x402NetworkListForCopy,
|
|
1269
|
+
x402NetworkToCaip2,
|
|
1270
|
+
x402SupportedNetworksHint,
|
|
1271
|
+
x402WrongAssetHint
|
|
1272
|
+
};
|