@routedock/routedock 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -12
- package/dist/{MppSessionHandler-1EL8QBAn.d.cts → MppSessionHandler-3QfCHI5x.d.ts} +40 -2
- package/dist/{MppSessionHandler-Bq4S-JTE.d.ts → MppSessionHandler-xu4Kxu6N.d.cts} +40 -2
- package/dist/{ProviderRegistry-CA_TdREG.d.ts → ProviderRegistry-D9KqyuH-.d.cts} +16 -36
- package/dist/{ProviderRegistry-D-0rqK0N.d.cts → ProviderRegistry-DVi2uzgk.d.ts} +16 -36
- package/dist/{RouteDockClient-go9aopfF.d.cts → RouteDockClient-6rmgkbYb.d.cts} +124 -142
- package/dist/{RouteDockClient-Bh86bVly.d.ts → RouteDockClient-CnAkxwrW.d.ts} +124 -142
- package/dist/{SessionReconciler-Bw-PoBVl.d.ts → SessionReconciler-Cce9MXO1.d.ts} +10 -3
- package/dist/{SessionReconciler-Cz2nuOlJ.d.cts → SessionReconciler-Dpe9qXdD.d.cts} +10 -3
- package/dist/{SessionStore-BaYiegP0.d.ts → SessionStore-DkLBxFQu.d.cts} +28 -3
- package/dist/{SessionStore-B4-Hi4H8.d.cts → SessionStore-_ZkoBzFY.d.ts} +28 -3
- package/dist/SpendStore-BUclV02i.d.ts +73 -0
- package/dist/SpendStore-s_dfcemj.d.cts +73 -0
- package/dist/client.cjs +767 -487
- package/dist/client.d.cts +80 -10
- package/dist/client.d.ts +80 -10
- package/dist/client.js +733 -468
- package/dist/index.cjs +1295 -590
- package/dist/index.d.cts +13 -10
- package/dist/index.d.ts +13 -10
- package/dist/index.js +1247 -559
- package/dist/logger-DVa7VT4c.d.cts +41 -0
- package/dist/logger-DVa7VT4c.d.ts +41 -0
- package/dist/network.cjs +57 -0
- package/dist/network.d.cts +72 -0
- package/dist/network.d.ts +72 -0
- package/dist/network.js +49 -0
- package/dist/node.cjs +166 -0
- package/dist/node.d.cts +77 -0
- package/dist/node.d.ts +77 -0
- package/dist/node.js +159 -0
- package/dist/provider/fastify.cjs +847 -62
- package/dist/provider/fastify.d.cts +10 -3
- package/dist/provider/fastify.d.ts +10 -3
- package/dist/provider/fastify.js +848 -63
- package/dist/provider/hono.cjs +936 -60
- package/dist/provider/hono.d.cts +23 -8
- package/dist/provider/hono.d.ts +23 -8
- package/dist/provider/hono.js +937 -61
- package/dist/provider.cjs +894 -70
- package/dist/provider.d.cts +9 -8
- package/dist/provider.d.ts +9 -8
- package/dist/provider.js +895 -72
- package/dist/react.cjs +736 -436
- package/dist/react.d.cts +7 -5
- package/dist/react.d.ts +7 -5
- package/dist/react.js +725 -425
- package/dist/{routedockMiddleware-57gUaEMf.d.ts → routedockMiddleware-Cq9HOJEa.d.ts} +9 -2
- package/dist/{routedockMiddleware-DlyLrxp7.d.cts → routedockMiddleware-CxsWkSKa.d.cts} +9 -2
- package/dist/schema.cjs +7 -0
- package/dist/schema.js +7 -0
- package/dist/{sign-AKH67sft.d.cts → sign-Bq08SeFq.d.ts} +2 -2
- package/dist/{sign-BEq-OsSq.d.ts → sign-DqGE0Plv.d.cts} +2 -2
- package/dist/testing.d.cts +4 -3
- package/dist/testing.d.ts +4 -3
- package/dist/{logger-D8_Q2ytp.d.cts → types-DXaS5Wip.d.cts} +85 -47
- package/dist/{logger-D8_Q2ytp.d.ts → types-DXaS5Wip.d.ts} +85 -47
- package/package.json +50 -4
package/dist/index.cjs
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
var nulthSdk = require('@routedock/nulth-sdk');
|
|
4
4
|
var stellarSdk = require('@stellar/stellar-sdk');
|
|
5
|
+
var sha2_js = require('@noble/hashes/sha2.js');
|
|
5
6
|
var jsonSchema = require('@cfworker/json-schema');
|
|
6
|
-
var crypto$1 = require('crypto');
|
|
7
7
|
var stellar = require('@x402/stellar');
|
|
8
|
-
var client = require('@x402/
|
|
9
|
-
var client$1 = require('@x402/core/client');
|
|
8
|
+
var client = require('@x402/core/client');
|
|
10
9
|
var http = require('@x402/core/http');
|
|
11
|
-
var client$
|
|
12
|
-
var client$
|
|
13
|
-
var mpp = require('@stellar/mpp');
|
|
14
|
-
var client$4 = require('@stellar/mpp/channel/client');
|
|
10
|
+
var client$2 = require('@stellar/mpp/charge/client');
|
|
11
|
+
var client$1 = require('mppx/client');
|
|
15
12
|
var mppx = require('mppx');
|
|
13
|
+
var mpp = require('@stellar/mpp');
|
|
14
|
+
var client$3 = require('@stellar/mpp/channel/client');
|
|
15
|
+
var crypto$1 = require('crypto');
|
|
16
16
|
var fs = require('fs/promises');
|
|
17
17
|
var path = require('path');
|
|
18
18
|
var facilitator = require('@x402/stellar/exact/facilitator');
|
|
@@ -123,6 +123,20 @@ var RouteDockPolicyRejectError = class extends RouteDockError {
|
|
|
123
123
|
this.reason = reason;
|
|
124
124
|
}
|
|
125
125
|
};
|
|
126
|
+
var RouteDockPaymentRejectedError = class extends RouteDockError {
|
|
127
|
+
status;
|
|
128
|
+
/** The provider's `reason` field, if its response body was JSON and had one. */
|
|
129
|
+
reason;
|
|
130
|
+
/** The provider's `error` field (short summary), if present. */
|
|
131
|
+
providerError;
|
|
132
|
+
constructor(message, status, details = {}, options = {}) {
|
|
133
|
+
super(message, "PAYMENT_REJECTED", false, options);
|
|
134
|
+
this.name = "RouteDockPaymentRejectedError";
|
|
135
|
+
this.status = status;
|
|
136
|
+
if (details.reason !== void 0) this.reason = details.reason;
|
|
137
|
+
if (details.providerError !== void 0) this.providerError = details.providerError;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
126
140
|
var RouteDockChannelStateError = class extends RouteDockError {
|
|
127
141
|
constructor(message, options = {}) {
|
|
128
142
|
super(message, "CHANNEL_STATE", false, options);
|
|
@@ -167,6 +181,59 @@ function httpStatusToError(message, status, response) {
|
|
|
167
181
|
}
|
|
168
182
|
return new RouteDockNetworkError(message, opts);
|
|
169
183
|
}
|
|
184
|
+
var MAX_REJECTION_BODY_BYTES = 64 * 1024;
|
|
185
|
+
var MAX_REJECTION_FIELD_CHARS = 500;
|
|
186
|
+
async function readBoundedText(response, limit) {
|
|
187
|
+
const body = response.body;
|
|
188
|
+
if (!body) return null;
|
|
189
|
+
const reader = body.getReader();
|
|
190
|
+
const chunks = [];
|
|
191
|
+
let total = 0;
|
|
192
|
+
try {
|
|
193
|
+
for (; ; ) {
|
|
194
|
+
const { done, value } = await reader.read();
|
|
195
|
+
if (done) break;
|
|
196
|
+
total += value.byteLength;
|
|
197
|
+
if (total > limit) {
|
|
198
|
+
await reader.cancel().catch(() => void 0);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
chunks.push(value);
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const bytes = new Uint8Array(total);
|
|
207
|
+
let offset = 0;
|
|
208
|
+
for (const chunk of chunks) {
|
|
209
|
+
bytes.set(chunk, offset);
|
|
210
|
+
offset += chunk.byteLength;
|
|
211
|
+
}
|
|
212
|
+
return new TextDecoder().decode(bytes);
|
|
213
|
+
}
|
|
214
|
+
async function paymentRejectedError(label, status, response) {
|
|
215
|
+
let reason;
|
|
216
|
+
let providerError;
|
|
217
|
+
try {
|
|
218
|
+
const text = await readBoundedText(response, MAX_REJECTION_BODY_BYTES);
|
|
219
|
+
const body = text === null ? null : JSON.parse(text);
|
|
220
|
+
if (body !== null && typeof body === "object") {
|
|
221
|
+
const { reason: r, error: e } = body;
|
|
222
|
+
if (typeof r === "string" && r.length > 0) reason = r.slice(0, MAX_REJECTION_FIELD_CHARS);
|
|
223
|
+
if (typeof e === "string" && e.length > 0) providerError = e.slice(0, MAX_REJECTION_FIELD_CHARS);
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
const detail = reason ?? providerError;
|
|
228
|
+
return new RouteDockPaymentRejectedError(
|
|
229
|
+
`${label}: HTTP ${status}${detail ? ` \u2014 ${detail}` : ""}`,
|
|
230
|
+
status,
|
|
231
|
+
{
|
|
232
|
+
...reason !== void 0 ? { reason } : {},
|
|
233
|
+
...providerError !== void 0 ? { providerError } : {}
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
}
|
|
170
237
|
function wrapFetchError(err, context) {
|
|
171
238
|
if (err instanceof RouteDockError) return err;
|
|
172
239
|
return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
|
|
@@ -231,23 +298,34 @@ async function withRetry(fn, policy = {}) {
|
|
|
231
298
|
if (!err.retryable || attempt >= maxAttempts - 1) {
|
|
232
299
|
throw err;
|
|
233
300
|
}
|
|
234
|
-
const
|
|
301
|
+
const delay2 = Math.min(
|
|
302
|
+
err.retryAfterMs ?? backoffDelayMs(attempt, baseDelayMs, maxDelayMs),
|
|
303
|
+
maxDelayMs
|
|
304
|
+
);
|
|
235
305
|
if (onRetry) {
|
|
236
306
|
try {
|
|
237
|
-
await onRetry(attempt, err,
|
|
307
|
+
await onRetry(attempt, err, delay2);
|
|
238
308
|
} catch (cbErr) {
|
|
239
309
|
log("error", "[withRetry] onRetry callback threw:", { error: cbErr });
|
|
240
310
|
}
|
|
241
311
|
}
|
|
242
|
-
await sleep(
|
|
312
|
+
await sleep(delay2);
|
|
243
313
|
}
|
|
244
314
|
}
|
|
245
315
|
throw lastError ?? new RouteDockError("Retry exhausted", "NETWORK", false);
|
|
246
316
|
}
|
|
317
|
+
|
|
318
|
+
// src/network.ts
|
|
247
319
|
var USDC_ISSUERS = {
|
|
248
320
|
testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
|
|
249
321
|
mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
|
|
250
322
|
};
|
|
323
|
+
var HORIZON_URLS = {
|
|
324
|
+
testnet: "https://horizon-testnet.stellar.org",
|
|
325
|
+
mainnet: "https://horizon.stellar.org"
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// src/internal/usdc.ts
|
|
251
329
|
var usdcToUnits = nulthSdk.usdcToStroops;
|
|
252
330
|
|
|
253
331
|
// src/internal/assetUtils.ts
|
|
@@ -328,6 +406,138 @@ function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
|
|
|
328
406
|
);
|
|
329
407
|
}
|
|
330
408
|
|
|
409
|
+
// package.json
|
|
410
|
+
var package_default = {
|
|
411
|
+
version: "0.4.0"};
|
|
412
|
+
|
|
413
|
+
// src/internal/encoding.ts
|
|
414
|
+
var encoder = new TextEncoder();
|
|
415
|
+
var decoder = new TextDecoder();
|
|
416
|
+
function utf8ToBytes(text) {
|
|
417
|
+
return encoder.encode(text);
|
|
418
|
+
}
|
|
419
|
+
function bytesToUtf8(bytes) {
|
|
420
|
+
return decoder.decode(bytes);
|
|
421
|
+
}
|
|
422
|
+
function bytesToBase64(bytes) {
|
|
423
|
+
const chunk = 32768;
|
|
424
|
+
let binary = "";
|
|
425
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
426
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
427
|
+
}
|
|
428
|
+
return btoa(binary);
|
|
429
|
+
}
|
|
430
|
+
function base64ToBytes(b64) {
|
|
431
|
+
const end = b64.indexOf("=");
|
|
432
|
+
let clean = (end === -1 ? b64 : b64.slice(0, end)).replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
|
|
433
|
+
if (clean.length % 4 === 1) clean = clean.slice(0, -1);
|
|
434
|
+
const binary = atob(clean);
|
|
435
|
+
const bytes = new Uint8Array(binary.length);
|
|
436
|
+
for (let i = 0; i < binary.length; i++) {
|
|
437
|
+
bytes[i] = binary.charCodeAt(i);
|
|
438
|
+
}
|
|
439
|
+
return bytes;
|
|
440
|
+
}
|
|
441
|
+
function base64ToUtf8(b64) {
|
|
442
|
+
return bytesToUtf8(base64ToBytes(b64));
|
|
443
|
+
}
|
|
444
|
+
function hexToBytes(hex) {
|
|
445
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
446
|
+
if (clean.length % 2 !== 0) {
|
|
447
|
+
throw new Error("hexToBytes: hex string must have an even length");
|
|
448
|
+
}
|
|
449
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
450
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
451
|
+
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
452
|
+
}
|
|
453
|
+
return bytes;
|
|
454
|
+
}
|
|
455
|
+
function bytesToHex(bytes) {
|
|
456
|
+
let out = "";
|
|
457
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
458
|
+
out += bytes[i].toString(16).padStart(2, "0");
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
function sha256(data) {
|
|
463
|
+
return sha2_js.sha256(typeof data === "string" ? utf8ToBytes(data) : data);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/manifest/sign.ts
|
|
467
|
+
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
468
|
+
function canonicalize(value) {
|
|
469
|
+
if (Array.isArray(value)) {
|
|
470
|
+
return value.map(canonicalize);
|
|
471
|
+
}
|
|
472
|
+
if (value !== null && typeof value === "object") {
|
|
473
|
+
return Object.fromEntries(
|
|
474
|
+
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return value;
|
|
478
|
+
}
|
|
479
|
+
function manifestDigest(manifest) {
|
|
480
|
+
const { signature: _omit, ...rest } = manifest;
|
|
481
|
+
const canonical = JSON.stringify(canonicalize(rest));
|
|
482
|
+
return sha256(utf8ToBytes(canonical));
|
|
483
|
+
}
|
|
484
|
+
function signManifest(manifest, payeeSecretKey) {
|
|
485
|
+
const keypair = stellarSdk.Keypair.fromSecret(payeeSecretKey);
|
|
486
|
+
const versionedManifest = {
|
|
487
|
+
...manifest,
|
|
488
|
+
signature_version: MANIFEST_SIGNATURE_VERSION
|
|
489
|
+
};
|
|
490
|
+
const digest = manifestDigest(versionedManifest);
|
|
491
|
+
const sig = keypair.sign(digest);
|
|
492
|
+
return { ...versionedManifest, signature: bytesToBase64(sig) };
|
|
493
|
+
}
|
|
494
|
+
function verifyManifestSignature(manifest, expectedPayee) {
|
|
495
|
+
if (!manifest.signature) {
|
|
496
|
+
throw new RouteDockSignatureError(
|
|
497
|
+
"Manifest is missing a signature field \u2014 cannot verify payee authenticity"
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
if (manifest.signature_version !== MANIFEST_SIGNATURE_VERSION) {
|
|
501
|
+
throw new RouteDockSignatureError(
|
|
502
|
+
`Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
if (expectedPayee !== void 0 && manifest.payee !== expectedPayee) {
|
|
506
|
+
throw new RouteDockSignatureError(
|
|
507
|
+
`Manifest payee ${manifest.payee} does not match expected payee ${expectedPayee}`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
let keypair;
|
|
511
|
+
try {
|
|
512
|
+
keypair = stellarSdk.Keypair.fromPublicKey(manifest.payee);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
throw new RouteDockSignatureError(
|
|
515
|
+
`Manifest payee ${manifest.payee} is not a valid Stellar public key`,
|
|
516
|
+
{ cause: err }
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
const digest = manifestDigest(manifest);
|
|
520
|
+
let sigBytes;
|
|
521
|
+
try {
|
|
522
|
+
sigBytes = base64ToBytes(manifest.signature);
|
|
523
|
+
} catch {
|
|
524
|
+
throw new RouteDockSignatureError("Manifest signature is not valid base64");
|
|
525
|
+
}
|
|
526
|
+
let valid = false;
|
|
527
|
+
try {
|
|
528
|
+
valid = keypair.verify(digest, sigBytes);
|
|
529
|
+
} catch {
|
|
530
|
+
throw new RouteDockSignatureError(
|
|
531
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
if (!valid) {
|
|
535
|
+
throw new RouteDockSignatureError(
|
|
536
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
331
541
|
// src/schemas/routedock.schema.json
|
|
332
542
|
var routedock_schema_default = {
|
|
333
543
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
@@ -405,6 +615,7 @@ var routedock_schema_default = {
|
|
|
405
615
|
},
|
|
406
616
|
asset_contract: {
|
|
407
617
|
type: "string",
|
|
618
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
408
619
|
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
409
620
|
},
|
|
410
621
|
assets: {
|
|
@@ -417,6 +628,7 @@ var routedock_schema_default = {
|
|
|
417
628
|
},
|
|
418
629
|
payee: {
|
|
419
630
|
type: "string",
|
|
631
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
420
632
|
description: "Stellar address (G...) that receives payments"
|
|
421
633
|
},
|
|
422
634
|
pricing: {
|
|
@@ -587,6 +799,7 @@ var routedock_schema_default = {
|
|
|
587
799
|
properties: {
|
|
588
800
|
amount: {
|
|
589
801
|
type: "string",
|
|
802
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
590
803
|
description: "Cost per request in the payment asset (decimal string, e.g. '0.001')"
|
|
591
804
|
},
|
|
592
805
|
per: {
|
|
@@ -600,6 +813,7 @@ var routedock_schema_default = {
|
|
|
600
813
|
},
|
|
601
814
|
payee: {
|
|
602
815
|
type: "string",
|
|
816
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
603
817
|
description: "Optional per-mode payee override (Stellar G... address). Directs settlements for this mode to a separate account for treasury separation. Falls back to the top-level 'payee' when omitted."
|
|
604
818
|
}
|
|
605
819
|
}
|
|
@@ -612,6 +826,7 @@ var routedock_schema_default = {
|
|
|
612
826
|
properties: {
|
|
613
827
|
rate: {
|
|
614
828
|
type: "string",
|
|
829
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
615
830
|
description: "Cost per voucher in the payment asset (decimal string, e.g. '0.0001')"
|
|
616
831
|
},
|
|
617
832
|
per: {
|
|
@@ -621,10 +836,12 @@ var routedock_schema_default = {
|
|
|
621
836
|
},
|
|
622
837
|
channel_factory: {
|
|
623
838
|
type: "string",
|
|
839
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
624
840
|
description: "Stable Soroban factory address (C...) used to deploy a per-session channel"
|
|
625
841
|
},
|
|
626
842
|
min_deposit: {
|
|
627
843
|
type: "string",
|
|
844
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
628
845
|
description: "Minimum initial deposit to open a channel (decimal string)"
|
|
629
846
|
},
|
|
630
847
|
refund_waiting_period_ledgers: {
|
|
@@ -722,78 +939,110 @@ var routedock_schema_default = {
|
|
|
722
939
|
}
|
|
723
940
|
};
|
|
724
941
|
|
|
725
|
-
//
|
|
726
|
-
var
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
}
|
|
733
|
-
if (value !== null && typeof value === "object") {
|
|
734
|
-
return Object.fromEntries(
|
|
735
|
-
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
|
|
736
|
-
);
|
|
942
|
+
// src/manifest/validate.ts
|
|
943
|
+
var validator = new jsonSchema.Validator(routedock_schema_default, "7");
|
|
944
|
+
function validateManifestSchema(manifest, source) {
|
|
945
|
+
const result = validator.validate(manifest);
|
|
946
|
+
if (!result.valid) {
|
|
947
|
+
const msgs = result.errors.map((err) => err.error).join("; ");
|
|
948
|
+
throw new RouteDockManifestError(`Invalid manifest${source ? ` at ${source}` : ""}: ${msgs}`);
|
|
737
949
|
}
|
|
738
|
-
return value;
|
|
739
950
|
}
|
|
740
|
-
function
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
if (!manifest.signature) {
|
|
757
|
-
throw new RouteDockSignatureError(
|
|
758
|
-
"Manifest is missing a signature field \u2014 cannot verify payee authenticity"
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
if (manifest.signature_version !== MANIFEST_SIGNATURE_VERSION) {
|
|
762
|
-
throw new RouteDockSignatureError(
|
|
763
|
-
`Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
|
|
764
|
-
);
|
|
951
|
+
function assertManifestValid(manifest, baseUrl) {
|
|
952
|
+
const context = baseUrl ? ` at ${baseUrl}` : "";
|
|
953
|
+
if (manifest.latency_hints !== void 0) {
|
|
954
|
+
if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
|
|
955
|
+
throw new RouteDockManifestError(
|
|
956
|
+
`Invalid manifest${context}: latency_hints defined without declaring regions`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
const regionSet = new Set(manifest.regions);
|
|
960
|
+
for (const region of Object.keys(manifest.latency_hints)) {
|
|
961
|
+
if (!regionSet.has(region)) {
|
|
962
|
+
throw new RouteDockManifestError(
|
|
963
|
+
`Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
765
967
|
}
|
|
766
|
-
if (
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
968
|
+
if (manifest.assets !== void 0) {
|
|
969
|
+
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
970
|
+
throw new RouteDockManifestError(
|
|
971
|
+
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
for (let i = 0; i < manifest.assets.length; i++) {
|
|
975
|
+
const a = manifest.assets[i];
|
|
976
|
+
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
977
|
+
throw new RouteDockManifestError(
|
|
978
|
+
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const first = manifest.assets[0];
|
|
983
|
+
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
984
|
+
throw new RouteDockManifestError(
|
|
985
|
+
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
986
|
+
);
|
|
987
|
+
}
|
|
770
988
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
989
|
+
assertPricingMatchesModes(manifest, context);
|
|
990
|
+
assertDeprecatedModesListed(manifest, context);
|
|
991
|
+
assertPricesParse(manifest, context);
|
|
992
|
+
}
|
|
993
|
+
function assertPricingMatchesModes(manifest, context) {
|
|
994
|
+
const pricing = manifest.pricing ?? {};
|
|
995
|
+
for (const mode of manifest.modes ?? []) {
|
|
996
|
+
if (pricing[mode] === void 0 || pricing[mode] === null) {
|
|
997
|
+
throw new RouteDockManifestError(
|
|
998
|
+
`Invalid manifest${context}: mode '${mode}' is listed in modes but pricing.${mode} is missing`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
778
1001
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1002
|
+
}
|
|
1003
|
+
function assertDeprecatedModesListed(manifest, context) {
|
|
1004
|
+
if (manifest.deprecated_modes === void 0) return;
|
|
1005
|
+
const modes = new Set(manifest.modes ?? []);
|
|
1006
|
+
for (const mode of manifest.deprecated_modes) {
|
|
1007
|
+
if (!modes.has(mode)) {
|
|
1008
|
+
throw new RouteDockManifestError(
|
|
1009
|
+
`Invalid manifest${context}: deprecated_modes entry '${mode}' is not listed in modes`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
786
1012
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1013
|
+
}
|
|
1014
|
+
var PRICE_FIELDS = {
|
|
1015
|
+
x402: ["amount"],
|
|
1016
|
+
"mpp-charge": ["amount"],
|
|
1017
|
+
"mpp-session": ["rate", "min_deposit"],
|
|
1018
|
+
"mpp-session-ws": ["rate", "min_deposit"]
|
|
1019
|
+
};
|
|
1020
|
+
function assertPricesParse(manifest, context) {
|
|
1021
|
+
const pricing = manifest.pricing ?? {};
|
|
1022
|
+
for (const [mode, fields] of Object.entries(PRICE_FIELDS)) {
|
|
1023
|
+
const entry = pricing[mode];
|
|
1024
|
+
if (entry === void 0 || entry === null) continue;
|
|
1025
|
+
for (const field of fields) {
|
|
1026
|
+
const value = entry[field];
|
|
1027
|
+
try {
|
|
1028
|
+
if (typeof value !== "string") {
|
|
1029
|
+
throw new RangeError(`expected a decimal string, got ${typeof value}`);
|
|
1030
|
+
}
|
|
1031
|
+
nulthSdk.usdcToStroops(value);
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1034
|
+
throw new RouteDockManifestError(
|
|
1035
|
+
`Invalid manifest${context}: pricing.${mode}.${field} ${JSON.stringify(value)} is not a valid amount (${detail})`,
|
|
1036
|
+
{ cause: err }
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
791
1040
|
}
|
|
792
1041
|
}
|
|
793
1042
|
|
|
794
1043
|
// src/client/ModeRouter.ts
|
|
795
|
-
var validator = new jsonSchema.Validator(routedock_schema_default, "7");
|
|
796
1044
|
var SDK_VERSION = package_default.version;
|
|
1045
|
+
var SUPPORTED_MANIFEST_MAJOR = 1;
|
|
797
1046
|
function parseMajorMinor(version) {
|
|
798
1047
|
const parts = version.split(".");
|
|
799
1048
|
return [Number.parseInt(parts[0] ?? "0", 10), Number.parseInt(parts[1] ?? "0", 10)];
|
|
@@ -811,6 +1060,14 @@ function assertClientVersionSupported(manifest, baseUrl) {
|
|
|
811
1060
|
);
|
|
812
1061
|
}
|
|
813
1062
|
}
|
|
1063
|
+
function assertManifestVersionSupported(manifest, baseUrl) {
|
|
1064
|
+
const [major] = parseMajorMinor(manifest.routedock);
|
|
1065
|
+
if (major > SUPPORTED_MANIFEST_MAJOR) {
|
|
1066
|
+
throw new RouteDockClientVersionError(
|
|
1067
|
+
`Manifest version ${manifest.routedock} from provider at ${baseUrl} has schema major version ${major}, which is newer than the highest version this SDK supports (${SUPPORTED_MANIFEST_MAJOR}.x). Please upgrade the SDK.`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
814
1071
|
function assertManifestActive(manifest, baseUrl) {
|
|
815
1072
|
const sunsetAt = manifest.sunset_at;
|
|
816
1073
|
if (!sunsetAt) return;
|
|
@@ -826,11 +1083,16 @@ function assertManifestActive(manifest, baseUrl) {
|
|
|
826
1083
|
);
|
|
827
1084
|
}
|
|
828
1085
|
}
|
|
829
|
-
function assertEndpointActive(manifest, url, logger) {
|
|
1086
|
+
function assertEndpointActive(manifest, url, logger, method) {
|
|
830
1087
|
const pathname = new URL(url).pathname;
|
|
831
|
-
const
|
|
1088
|
+
const pathMatches = Object.entries(manifest.endpoints).filter(
|
|
832
1089
|
([, descriptor]) => descriptor.path === pathname
|
|
833
1090
|
);
|
|
1091
|
+
if (pathMatches.length === 0) return;
|
|
1092
|
+
const requestedMethod = method?.toUpperCase();
|
|
1093
|
+
const matches = requestedMethod ? pathMatches.filter(
|
|
1094
|
+
([, descriptor]) => descriptor.method.toUpperCase() === requestedMethod
|
|
1095
|
+
) : pathMatches;
|
|
834
1096
|
if (matches.length === 0) return;
|
|
835
1097
|
const now = Date.now();
|
|
836
1098
|
const allSunset = matches.every(([, descriptor]) => {
|
|
@@ -853,45 +1115,6 @@ function assertEndpointActive(manifest, url, logger) {
|
|
|
853
1115
|
);
|
|
854
1116
|
}
|
|
855
1117
|
}
|
|
856
|
-
function assertManifestValid(manifest, baseUrl) {
|
|
857
|
-
const context = baseUrl ? ` at ${baseUrl}` : "";
|
|
858
|
-
if (manifest.latency_hints !== void 0) {
|
|
859
|
-
if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
|
|
860
|
-
throw new RouteDockManifestError(
|
|
861
|
-
`Invalid manifest${context}: latency_hints defined without declaring regions`
|
|
862
|
-
);
|
|
863
|
-
}
|
|
864
|
-
const regionSet = new Set(manifest.regions);
|
|
865
|
-
for (const region of Object.keys(manifest.latency_hints)) {
|
|
866
|
-
if (!regionSet.has(region)) {
|
|
867
|
-
throw new RouteDockManifestError(
|
|
868
|
-
`Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
|
|
869
|
-
);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
if (manifest.assets !== void 0) {
|
|
874
|
-
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
875
|
-
throw new RouteDockManifestError(
|
|
876
|
-
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
877
|
-
);
|
|
878
|
-
}
|
|
879
|
-
for (let i = 0; i < manifest.assets.length; i++) {
|
|
880
|
-
const a = manifest.assets[i];
|
|
881
|
-
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
882
|
-
throw new RouteDockManifestError(
|
|
883
|
-
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
884
|
-
);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
const first = manifest.assets[0];
|
|
888
|
-
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
889
|
-
throw new RouteDockManifestError(
|
|
890
|
-
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
891
|
-
);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
1118
|
var CACHE_TTL_MS = 6e4;
|
|
896
1119
|
var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
|
|
897
1120
|
var DEFAULT_MANIFEST_TIMEOUT_MS = 5e3;
|
|
@@ -932,6 +1155,7 @@ var LruCache = class {
|
|
|
932
1155
|
}
|
|
933
1156
|
};
|
|
934
1157
|
var manifestCache = new LruCache(DEFAULT_MANIFEST_CACHE_MAX_SIZE);
|
|
1158
|
+
var inflightManifestFetches = /* @__PURE__ */ new Map();
|
|
935
1159
|
function configureManifestCache(maxSize) {
|
|
936
1160
|
if (!Number.isInteger(maxSize) || maxSize <= 0) {
|
|
937
1161
|
throw new RouteDockManifestError("Invalid manifest cache size: " + maxSize);
|
|
@@ -967,10 +1191,27 @@ async function fetchManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_M
|
|
|
967
1191
|
if (cached && Date.now() < cached.expiresAt) {
|
|
968
1192
|
verifyManifestSignature(cached.manifest, expectedPayee);
|
|
969
1193
|
assertClientVersionSupported(cached.manifest, baseUrl);
|
|
1194
|
+
assertManifestVersionSupported(cached.manifest, baseUrl);
|
|
970
1195
|
assertManifestValid(cached.manifest, baseUrl);
|
|
971
1196
|
assertManifestActive(cached.manifest, baseUrl);
|
|
972
1197
|
return cached.manifest;
|
|
973
1198
|
}
|
|
1199
|
+
let inflight = inflightManifestFetches.get(baseUrl);
|
|
1200
|
+
if (!inflight) {
|
|
1201
|
+
inflight = fetchAndCacheManifest(baseUrl, retryPolicy, manifestTimeoutMs).finally(() => {
|
|
1202
|
+
inflightManifestFetches.delete(baseUrl);
|
|
1203
|
+
});
|
|
1204
|
+
inflightManifestFetches.set(baseUrl, inflight);
|
|
1205
|
+
}
|
|
1206
|
+
const manifest = await inflight;
|
|
1207
|
+
verifyManifestSignature(manifest, expectedPayee);
|
|
1208
|
+
assertClientVersionSupported(manifest, baseUrl);
|
|
1209
|
+
assertManifestVersionSupported(manifest, baseUrl);
|
|
1210
|
+
assertManifestValid(manifest, baseUrl);
|
|
1211
|
+
assertManifestActive(manifest, baseUrl);
|
|
1212
|
+
return manifest;
|
|
1213
|
+
}
|
|
1214
|
+
async function fetchAndCacheManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_MANIFEST_TIMEOUT_MS) {
|
|
974
1215
|
const url = baseUrl.replace(/\/$/, "") + "/.well-known/routedock.json";
|
|
975
1216
|
return withRetry(async () => {
|
|
976
1217
|
let raw;
|
|
@@ -1007,15 +1248,12 @@ async function fetchManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_M
|
|
|
1007
1248
|
} catch (err) {
|
|
1008
1249
|
throw new RouteDockManifestError(`Manifest at ${url} is not valid JSON`, { cause: err });
|
|
1009
1250
|
}
|
|
1010
|
-
|
|
1011
|
-
if (!result.valid) {
|
|
1012
|
-
const msgs = result.errors.map((err) => err.error).join("; ");
|
|
1013
|
-
throw new RouteDockManifestError(`Invalid manifest at ${url}: ${msgs}`);
|
|
1014
|
-
}
|
|
1251
|
+
validateManifestSchema(raw, url);
|
|
1015
1252
|
const manifest = raw;
|
|
1016
1253
|
assertManifestValid(manifest, baseUrl);
|
|
1017
|
-
verifyManifestSignature(manifest
|
|
1254
|
+
verifyManifestSignature(manifest);
|
|
1018
1255
|
assertClientVersionSupported(manifest, baseUrl);
|
|
1256
|
+
assertManifestVersionSupported(manifest, baseUrl);
|
|
1019
1257
|
assertManifestActive(manifest, baseUrl);
|
|
1020
1258
|
manifestCache.set(baseUrl, { manifest, fetchedAt: Date.now(), expiresAt });
|
|
1021
1259
|
return manifest;
|
|
@@ -1130,6 +1368,48 @@ function rankProvidersByLatency(manifests, targetRegion) {
|
|
|
1130
1368
|
});
|
|
1131
1369
|
}
|
|
1132
1370
|
|
|
1371
|
+
// src/internal/settleResult.ts
|
|
1372
|
+
function readSettleResult(result) {
|
|
1373
|
+
const settle = result;
|
|
1374
|
+
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
1375
|
+
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
1376
|
+
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
1377
|
+
return failed(settle.errorReason);
|
|
1378
|
+
}
|
|
1379
|
+
return { ok: true, txHash: settle.transaction };
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// src/internal/request.ts
|
|
1383
|
+
var DEFAULT_METHOD = "GET";
|
|
1384
|
+
var BODYLESS_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
|
|
1385
|
+
function isReplayable(body) {
|
|
1386
|
+
return typeof body === "string" || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || typeof Blob !== "undefined" && body instanceof Blob || typeof FormData !== "undefined" && body instanceof FormData || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams;
|
|
1387
|
+
}
|
|
1388
|
+
async function bufferBody(body) {
|
|
1389
|
+
if (isReplayable(body)) return body;
|
|
1390
|
+
return new Response(body).arrayBuffer();
|
|
1391
|
+
}
|
|
1392
|
+
async function prepareRequest(request) {
|
|
1393
|
+
const method = (request?.method ?? DEFAULT_METHOD).toUpperCase();
|
|
1394
|
+
const headers = new Headers(request?.headers);
|
|
1395
|
+
const body = request?.body;
|
|
1396
|
+
if (body === void 0 || BODYLESS_METHODS.has(method)) {
|
|
1397
|
+
return { method, headers };
|
|
1398
|
+
}
|
|
1399
|
+
return { method, headers, body: await bufferBody(body) };
|
|
1400
|
+
}
|
|
1401
|
+
function toRequestInit(prepared, overrides) {
|
|
1402
|
+
const headers = new Headers(prepared.headers);
|
|
1403
|
+
if (overrides) {
|
|
1404
|
+
new Headers(overrides).forEach((value, name) => headers.set(name, value));
|
|
1405
|
+
}
|
|
1406
|
+
const init = { method: prepared.method, headers };
|
|
1407
|
+
if (prepared.body !== void 0) {
|
|
1408
|
+
init.body = prepared.body;
|
|
1409
|
+
}
|
|
1410
|
+
return init;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1133
1413
|
// src/internal/payee.ts
|
|
1134
1414
|
function resolvePayee(manifest, mode) {
|
|
1135
1415
|
const override = manifest.pricing[mode]?.payee;
|
|
@@ -1190,6 +1470,102 @@ function checkChargeChallenge(request, manifest, network, maxAmount) {
|
|
|
1190
1470
|
}
|
|
1191
1471
|
return checkAmount(request["amount"], maxAmount);
|
|
1192
1472
|
}
|
|
1473
|
+
var defaultIo = {
|
|
1474
|
+
async expirationLedger(network, maxTimeoutSeconds) {
|
|
1475
|
+
const { sequence } = await stellar.getRpcClient(network).getLatestLedger();
|
|
1476
|
+
const secondsPerLedger = await stellar.getEstimatedLedgerCloseTimeSeconds(network);
|
|
1477
|
+
return sequence + Math.ceil(maxTimeoutSeconds / secondsPerLedger);
|
|
1478
|
+
},
|
|
1479
|
+
async assemble({ asset, from, payTo, amount, networkPassphrase, rpcUrl }) {
|
|
1480
|
+
return stellarSdk.contract.AssembledTransaction.build({
|
|
1481
|
+
contractId: asset,
|
|
1482
|
+
method: "transfer",
|
|
1483
|
+
// SEP-41: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md#interface
|
|
1484
|
+
args: [
|
|
1485
|
+
stellarSdk.nativeToScVal(from, { type: "address" }),
|
|
1486
|
+
stellarSdk.nativeToScVal(payTo, { type: "address" }),
|
|
1487
|
+
stellarSdk.nativeToScVal(amount, { type: "i128" })
|
|
1488
|
+
],
|
|
1489
|
+
networkPassphrase,
|
|
1490
|
+
rpcUrl,
|
|
1491
|
+
parseResultXdr: (result) => result
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
var NulthStellarScheme = class {
|
|
1496
|
+
constructor(signer, io = defaultIo) {
|
|
1497
|
+
this.signer = signer;
|
|
1498
|
+
this.io = io;
|
|
1499
|
+
}
|
|
1500
|
+
signer;
|
|
1501
|
+
io;
|
|
1502
|
+
scheme = "exact";
|
|
1503
|
+
async createPaymentPayload(x402Version, paymentRequirements) {
|
|
1504
|
+
try {
|
|
1505
|
+
this.validateRequirements(paymentRequirements);
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
throw new Error(
|
|
1508
|
+
`Invalid input parameters for creating Nulth Stellar payment, cause: ${String(err)}`
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
const from = this.signer.address;
|
|
1512
|
+
const { network, payTo, asset, amount, maxTimeoutSeconds } = paymentRequirements;
|
|
1513
|
+
if (!paymentRequirements.extra.areFeesSponsored) {
|
|
1514
|
+
throw new Error("Nulth exact scheme requires areFeesSponsored to be true");
|
|
1515
|
+
}
|
|
1516
|
+
const maxLedger = await this.io.expirationLedger(network, maxTimeoutSeconds);
|
|
1517
|
+
const tx = await this.io.assemble({
|
|
1518
|
+
asset,
|
|
1519
|
+
from,
|
|
1520
|
+
payTo,
|
|
1521
|
+
amount,
|
|
1522
|
+
networkPassphrase: stellar.getNetworkPassphrase(network),
|
|
1523
|
+
rpcUrl: stellar.getRpcUrl(network)
|
|
1524
|
+
});
|
|
1525
|
+
stellar.handleSimulationResult(tx.simulation);
|
|
1526
|
+
const signers = tx.needsNonInvokerSigningBy();
|
|
1527
|
+
if (!signers.includes(from) || signers.length > 1) {
|
|
1528
|
+
throw new Error(`Expected to sign with [${from}], but got [${signers.join(", ")}]`);
|
|
1529
|
+
}
|
|
1530
|
+
await tx.signAuthEntries({
|
|
1531
|
+
address: from,
|
|
1532
|
+
signAuthEntry: this.signer.signAuthEntry,
|
|
1533
|
+
expiration: maxLedger,
|
|
1534
|
+
authorizeEntry: nulthSdk.nulthAuthorizeEntry
|
|
1535
|
+
});
|
|
1536
|
+
await tx.simulate();
|
|
1537
|
+
stellar.handleSimulationResult(tx.simulation);
|
|
1538
|
+
const remaining = tx.needsNonInvokerSigningBy();
|
|
1539
|
+
if (remaining.length > 0) {
|
|
1540
|
+
throw new Error(`unexpected signer(s) required: [${remaining.join(", ")}]`);
|
|
1541
|
+
}
|
|
1542
|
+
if (!tx.built) {
|
|
1543
|
+
throw new Error("Nulth payment transaction has no assembled envelope to send");
|
|
1544
|
+
}
|
|
1545
|
+
return { x402Version, payload: { transaction: tx.built.toXDR() } };
|
|
1546
|
+
}
|
|
1547
|
+
validateRequirements(requirements) {
|
|
1548
|
+
const { scheme, network, payTo, asset, amount } = requirements;
|
|
1549
|
+
if (typeof amount !== "string" || !Number.isInteger(Number(amount)) || Number(amount) <= 0) {
|
|
1550
|
+
throw new Error(`Invalid amount: ${amount}. Amount must be a positive integer.`);
|
|
1551
|
+
}
|
|
1552
|
+
if (scheme !== this.scheme) {
|
|
1553
|
+
throw new Error(`Unsupported scheme: ${scheme}`);
|
|
1554
|
+
}
|
|
1555
|
+
if (!stellar.isStellarNetwork(network)) {
|
|
1556
|
+
throw new Error(`Unsupported Stellar network: ${network}`);
|
|
1557
|
+
}
|
|
1558
|
+
if (!stellar.validateStellarDestinationAddress(payTo)) {
|
|
1559
|
+
throw new Error(`Invalid Stellar destination address: ${payTo}`);
|
|
1560
|
+
}
|
|
1561
|
+
if (!stellar.validateStellarAssetAddress(asset)) {
|
|
1562
|
+
throw new Error(`Invalid Stellar asset address: ${asset}`);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
function selectStellarScheme(signer) {
|
|
1567
|
+
return nulthSdk.isNulthStellarSigner(signer) ? new NulthStellarScheme(signer) : new stellar.ExactStellarScheme(signer);
|
|
1568
|
+
}
|
|
1193
1569
|
|
|
1194
1570
|
// src/client/x402Client.ts
|
|
1195
1571
|
var CAIP2 = {
|
|
@@ -1197,25 +1573,29 @@ var CAIP2 = {
|
|
|
1197
1573
|
mainnet: "stellar:pubnet"
|
|
1198
1574
|
};
|
|
1199
1575
|
var X402Client = class _X402Client {
|
|
1200
|
-
constructor(secretKeyOrSigner, network, retryPolicy) {
|
|
1576
|
+
constructor(secretKeyOrSigner, network, retryPolicy, preSignHook, logger) {
|
|
1201
1577
|
this.network = network;
|
|
1202
1578
|
this.retryPolicy = retryPolicy;
|
|
1579
|
+
this.preSignHook = preSignHook;
|
|
1203
1580
|
const caip2 = CAIP2[network];
|
|
1204
1581
|
this.signer = typeof secretKeyOrSigner === "string" ? stellar.createEd25519Signer(secretKeyOrSigner, caip2) : secretKeyOrSigner;
|
|
1205
|
-
const scheme =
|
|
1206
|
-
const core = new client
|
|
1582
|
+
const scheme = selectStellarScheme(this.signer);
|
|
1583
|
+
const core = new client.x402Client();
|
|
1207
1584
|
core.register(caip2, scheme);
|
|
1208
|
-
this.httpClient = new client
|
|
1585
|
+
this.httpClient = new client.x402HTTPClient(core);
|
|
1586
|
+
this.logger = logger;
|
|
1209
1587
|
}
|
|
1210
1588
|
network;
|
|
1211
1589
|
retryPolicy;
|
|
1590
|
+
preSignHook;
|
|
1212
1591
|
httpClient;
|
|
1213
1592
|
signer;
|
|
1593
|
+
logger;
|
|
1214
1594
|
/** Replace signer (e.g. swap to Nulth ZK account payer before pay) */
|
|
1215
1595
|
withSigner(signer) {
|
|
1216
|
-
return new _X402Client(signer, this.network, this.retryPolicy);
|
|
1596
|
+
return new _X402Client(signer, this.network, this.retryPolicy, this.preSignHook, this.logger);
|
|
1217
1597
|
}
|
|
1218
|
-
async pay(url, manifest) {
|
|
1598
|
+
async pay(url, manifest, request) {
|
|
1219
1599
|
const pricing = manifest.pricing.x402;
|
|
1220
1600
|
if (!pricing) {
|
|
1221
1601
|
throw new RouteDockManifestError("manifest.pricing.x402 missing");
|
|
@@ -1224,13 +1604,15 @@ var X402Client = class _X402Client {
|
|
|
1224
1604
|
throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
|
|
1225
1605
|
}
|
|
1226
1606
|
const caip2 = CAIP2[this.network];
|
|
1607
|
+
const prepared = await prepareRequest(request);
|
|
1227
1608
|
const probe = await withRetry(async () => {
|
|
1228
1609
|
let init2;
|
|
1229
1610
|
try {
|
|
1230
|
-
init2 = await fetch(url,
|
|
1611
|
+
init2 = await fetch(url, toRequestInit(prepared, { "X-Preferred-Mode": "x402" }));
|
|
1231
1612
|
} catch (err) {
|
|
1232
1613
|
throw wrapFetchError(err, "x402 initial request");
|
|
1233
1614
|
}
|
|
1615
|
+
this.logger?.("debug", "[x402] unpaid probe response", { status: init2.status });
|
|
1234
1616
|
if (init2.status !== 402) {
|
|
1235
1617
|
if (!init2.ok) {
|
|
1236
1618
|
throw httpStatusToError(
|
|
@@ -1247,6 +1629,11 @@ var X402Client = class _X402Client {
|
|
|
1247
1629
|
`Failed to parse JSON from non-402 response (HTTP ${init2.status})`
|
|
1248
1630
|
);
|
|
1249
1631
|
}
|
|
1632
|
+
this.logger?.(
|
|
1633
|
+
"debug",
|
|
1634
|
+
"[x402] unpaid probe returned non-402 success \u2014 serving as free response",
|
|
1635
|
+
{ status: init2.status }
|
|
1636
|
+
);
|
|
1250
1637
|
return { kind: "free", data: data2 };
|
|
1251
1638
|
}
|
|
1252
1639
|
return { kind: "payment-required", init: init2 };
|
|
@@ -1275,6 +1662,18 @@ var X402Client = class _X402Client {
|
|
|
1275
1662
|
if (allowed.length === 0) {
|
|
1276
1663
|
throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
|
|
1277
1664
|
}
|
|
1665
|
+
if (this.preSignHook) {
|
|
1666
|
+
let decision;
|
|
1667
|
+
try {
|
|
1668
|
+
decision = await this.preSignHook({ url, manifest, accepts: allowed });
|
|
1669
|
+
} catch (err) {
|
|
1670
|
+
throw new RouteDockPolicyRejectError("pre_sign_hook_error", { cause: err });
|
|
1671
|
+
}
|
|
1672
|
+
if (!decision || decision.approve !== true) {
|
|
1673
|
+
const reason = decision && decision.approve === false ? decision.reason : void 0;
|
|
1674
|
+
throw new RouteDockPolicyRejectError(reason ?? "pre_sign_rejected");
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1278
1677
|
let paymentPayload;
|
|
1279
1678
|
try {
|
|
1280
1679
|
paymentPayload = await this.httpClient.createPaymentPayload({
|
|
@@ -1288,13 +1687,15 @@ var X402Client = class _X402Client {
|
|
|
1288
1687
|
}
|
|
1289
1688
|
const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
1290
1689
|
const signedAmount = nulthSdk.stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
|
|
1690
|
+
this.logger?.("debug", "[x402] sending paid request with payment header");
|
|
1291
1691
|
const settled = await withRetry(async () => {
|
|
1292
1692
|
let response;
|
|
1293
1693
|
try {
|
|
1294
|
-
response = await fetch(url,
|
|
1694
|
+
response = await fetch(url, toRequestInit(prepared, paymentHeaders));
|
|
1295
1695
|
} catch (err) {
|
|
1296
1696
|
throw wrapFetchError(err, "x402 settlement request");
|
|
1297
1697
|
}
|
|
1698
|
+
this.logger?.("debug", "[x402] paid request response", { status: response.status });
|
|
1298
1699
|
if (!response.ok) {
|
|
1299
1700
|
if (response.status >= 500 || response.status === 429 || response.status === 503) {
|
|
1300
1701
|
throw httpStatusToError(
|
|
@@ -1303,17 +1704,28 @@ var X402Client = class _X402Client {
|
|
|
1303
1704
|
response
|
|
1304
1705
|
);
|
|
1305
1706
|
}
|
|
1306
|
-
throw
|
|
1707
|
+
throw await paymentRejectedError("x402 payment failed", response.status, response);
|
|
1307
1708
|
}
|
|
1308
1709
|
return response;
|
|
1309
1710
|
}, this.retryPolicy);
|
|
1310
1711
|
let txHash = null;
|
|
1311
1712
|
const responseHeader = settled.headers.get("X-Payment-Response");
|
|
1312
|
-
if (responseHeader) {
|
|
1713
|
+
if (!responseHeader) {
|
|
1714
|
+
this.logger?.("debug", "[x402] settled 2xx carried no X-Payment-Response header");
|
|
1715
|
+
} else {
|
|
1313
1716
|
try {
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1717
|
+
const outcome = readSettleResult(http.decodePaymentResponseHeader(responseHeader));
|
|
1718
|
+
if (outcome.ok) {
|
|
1719
|
+
txHash = outcome.txHash;
|
|
1720
|
+
} else {
|
|
1721
|
+
this.logger?.("debug", "[x402] X-Payment-Response reports an unsettled payment", {
|
|
1722
|
+
...outcome.reason !== void 0 ? { reason: outcome.reason } : {}
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
} catch (err) {
|
|
1726
|
+
this.logger?.("debug", "[x402] X-Payment-Response header present but undecodable", {
|
|
1727
|
+
error: err
|
|
1728
|
+
});
|
|
1317
1729
|
}
|
|
1318
1730
|
}
|
|
1319
1731
|
let data;
|
|
@@ -1334,33 +1746,28 @@ var MppChargeClient = class {
|
|
|
1334
1746
|
keypair;
|
|
1335
1747
|
network;
|
|
1336
1748
|
retryPolicy;
|
|
1337
|
-
async pay(url, manifest) {
|
|
1749
|
+
async pay(url, manifest, request) {
|
|
1338
1750
|
const pricing = manifest.pricing["mpp-charge"];
|
|
1339
1751
|
if (!pricing) {
|
|
1340
1752
|
throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
|
|
1341
1753
|
}
|
|
1342
1754
|
const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
|
|
1343
|
-
|
|
1755
|
+
const prepared = await prepareRequest(request);
|
|
1344
1756
|
let signedAmount;
|
|
1345
1757
|
let credential;
|
|
1346
|
-
const mppx = client$
|
|
1758
|
+
const mppx = client$1.Mppx.create({
|
|
1347
1759
|
polyfill: false,
|
|
1348
1760
|
methods: [
|
|
1349
|
-
client$
|
|
1761
|
+
client$2.stellar.charge({
|
|
1350
1762
|
keypair: this.keypair,
|
|
1351
|
-
mode: "pull"
|
|
1352
|
-
onProgress(event) {
|
|
1353
|
-
if (event.type === "paid") {
|
|
1354
|
-
txHash = event.hash;
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1763
|
+
mode: "pull"
|
|
1357
1764
|
})
|
|
1358
1765
|
],
|
|
1359
1766
|
onChallenge: async (challenge, { createCredential }) => {
|
|
1360
|
-
const
|
|
1361
|
-
const rejection = checkChargeChallenge(
|
|
1767
|
+
const request2 = challenge.request;
|
|
1768
|
+
const rejection = checkChargeChallenge(request2, manifest, caip2, pricing.amount);
|
|
1362
1769
|
if (rejection) throw rejection;
|
|
1363
|
-
const amount =
|
|
1770
|
+
const amount = request2["amount"];
|
|
1364
1771
|
if (typeof amount !== "string") {
|
|
1365
1772
|
throw new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1366
1773
|
}
|
|
@@ -1372,7 +1779,7 @@ var MppChargeClient = class {
|
|
|
1372
1779
|
return withRetry(async () => {
|
|
1373
1780
|
let response;
|
|
1374
1781
|
try {
|
|
1375
|
-
response = credential ? await mppx.rawFetch(url,
|
|
1782
|
+
response = credential ? await mppx.rawFetch(url, toRequestInit(prepared, { Authorization: credential })) : await mppx.fetch(url, toRequestInit(prepared));
|
|
1376
1783
|
} catch (err) {
|
|
1377
1784
|
throw wrapMppError(err, "MPP charge request");
|
|
1378
1785
|
}
|
|
@@ -1384,8 +1791,9 @@ var MppChargeClient = class {
|
|
|
1384
1791
|
response
|
|
1385
1792
|
);
|
|
1386
1793
|
}
|
|
1387
|
-
throw
|
|
1794
|
+
throw await paymentRejectedError("MPP charge failed", response.status, response);
|
|
1388
1795
|
}
|
|
1796
|
+
const txHash = readSettlementHash(response);
|
|
1389
1797
|
let data;
|
|
1390
1798
|
try {
|
|
1391
1799
|
data = await response.json();
|
|
@@ -1405,8 +1813,40 @@ var MppChargeClient = class {
|
|
|
1405
1813
|
}, this.retryPolicy);
|
|
1406
1814
|
}
|
|
1407
1815
|
};
|
|
1816
|
+
function readSettlementHash(response) {
|
|
1817
|
+
try {
|
|
1818
|
+
return mppx.Receipt.fromResponse(response).reference;
|
|
1819
|
+
} catch {
|
|
1820
|
+
return null;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1408
1823
|
var MIN_REFUND_WAITING_PERIOD = 17280;
|
|
1409
1824
|
var STREAM_CLEANUP_TIMEOUT_MS = 3e3;
|
|
1825
|
+
var TX_CONFIRM_INTERVAL_MS = 1e3;
|
|
1826
|
+
var TX_CONFIRM_TIMEOUT_MS = 3e4;
|
|
1827
|
+
async function confirmTransaction(server, hash, label, timeoutMs = TX_CONFIRM_TIMEOUT_MS, intervalMs = TX_CONFIRM_INTERVAL_MS) {
|
|
1828
|
+
const deadline = Date.now() + timeoutMs;
|
|
1829
|
+
while (true) {
|
|
1830
|
+
const txResponse = await server.getTransaction(hash);
|
|
1831
|
+
const status = txResponse?.status;
|
|
1832
|
+
if (status === "SUCCESS") {
|
|
1833
|
+
return hash;
|
|
1834
|
+
}
|
|
1835
|
+
if (status === "FAILED") {
|
|
1836
|
+
throw new RouteDockDisputeError(
|
|
1837
|
+
`${label} transaction failed on-chain: status FAILED (resultXdr: ${txResponse.resultXdr.toXDR("base64")})`
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
if (Date.now() >= deadline) {
|
|
1841
|
+
break;
|
|
1842
|
+
}
|
|
1843
|
+
const remaining = deadline - Date.now();
|
|
1844
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, Math.max(1, remaining))));
|
|
1845
|
+
}
|
|
1846
|
+
throw new RouteDockDisputeError(
|
|
1847
|
+
`${label} transaction confirmation timed out after ${timeoutMs}ms (hash: ${hash})`
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1410
1850
|
var WS_CONNECTING = 0;
|
|
1411
1851
|
var WS_OPEN = 1;
|
|
1412
1852
|
function defaultWebSocketFactory(url, init, handlers) {
|
|
@@ -1420,9 +1860,9 @@ function defaultWebSocketFactory(url, init, handlers) {
|
|
|
1420
1860
|
if (typeof data === "string") {
|
|
1421
1861
|
handlers.onMessage(data);
|
|
1422
1862
|
} else if (data instanceof ArrayBuffer) {
|
|
1423
|
-
handlers.onMessage(
|
|
1863
|
+
handlers.onMessage(bytesToUtf8(new Uint8Array(data)));
|
|
1424
1864
|
} else {
|
|
1425
|
-
void data.arrayBuffer().then((buffer) => handlers.onMessage(
|
|
1865
|
+
void data.arrayBuffer().then((buffer) => handlers.onMessage(bytesToUtf8(new Uint8Array(buffer)))).catch(() => {
|
|
1426
1866
|
});
|
|
1427
1867
|
}
|
|
1428
1868
|
};
|
|
@@ -1575,11 +2015,11 @@ var MppSessionClient = class {
|
|
|
1575
2015
|
throw err;
|
|
1576
2016
|
}
|
|
1577
2017
|
};
|
|
1578
|
-
const streamWs = (u, m, createCredential, onSigned) => this.streamWebSocket(u, m, createCredential, onSigned);
|
|
1579
|
-
const mppx = client$
|
|
2018
|
+
const streamWs = (u, m, createCredential, onSigned, request) => this.streamWebSocket(u, m, createCredential, onSigned, request);
|
|
2019
|
+
const mppx = client$1.Mppx.create({
|
|
1580
2020
|
polyfill: false,
|
|
1581
2021
|
methods: [
|
|
1582
|
-
client$
|
|
2022
|
+
client$3.stellar.channel({
|
|
1583
2023
|
commitmentKey,
|
|
1584
2024
|
sourceAccount: this.keypair.publicKey(),
|
|
1585
2025
|
...store ? { store } : {},
|
|
@@ -1647,6 +2087,7 @@ var MppSessionClient = class {
|
|
|
1647
2087
|
};
|
|
1648
2088
|
},
|
|
1649
2089
|
async *stream(options2) {
|
|
2090
|
+
const prepared = await prepareRequest(options2?.request);
|
|
1650
2091
|
const checkSpend = () => {
|
|
1651
2092
|
if (!onSpend) return Promise.resolve();
|
|
1652
2093
|
return onSpend(pricing.rate);
|
|
@@ -1656,7 +2097,13 @@ var MppSessionClient = class {
|
|
|
1656
2097
|
const onSigned = () => {
|
|
1657
2098
|
vouchersIssued++;
|
|
1658
2099
|
};
|
|
1659
|
-
for await (const item of streamWs(
|
|
2100
|
+
for await (const item of streamWs(
|
|
2101
|
+
url,
|
|
2102
|
+
mppx,
|
|
2103
|
+
createGuardedCredential,
|
|
2104
|
+
onSigned,
|
|
2105
|
+
prepared
|
|
2106
|
+
)) {
|
|
1660
2107
|
yield item;
|
|
1661
2108
|
}
|
|
1662
2109
|
return;
|
|
@@ -1671,7 +2118,10 @@ var MppSessionClient = class {
|
|
|
1671
2118
|
const doFetch = () => withRetry(async () => {
|
|
1672
2119
|
let resp;
|
|
1673
2120
|
try {
|
|
1674
|
-
resp = await mppx.fetch(url, {
|
|
2121
|
+
resp = await mppx.fetch(url, {
|
|
2122
|
+
...toRequestInit(prepared),
|
|
2123
|
+
signal: abortController.signal
|
|
2124
|
+
});
|
|
1675
2125
|
} catch (err) {
|
|
1676
2126
|
if (abortController.signal.aborted) {
|
|
1677
2127
|
throw err;
|
|
@@ -1756,10 +2206,10 @@ var MppSessionClient = class {
|
|
|
1756
2206
|
async close() {
|
|
1757
2207
|
clearSessionTimer();
|
|
1758
2208
|
closed = true;
|
|
1759
|
-
const { rpc: rpcMod, Contract: Contract2, nativeToScVal:
|
|
2209
|
+
const { rpc: rpcMod, Contract: Contract2, nativeToScVal: nativeToScVal3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1760
2210
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1761
2211
|
const server = new rpcMod.Server(rpcUrl);
|
|
1762
|
-
const
|
|
2212
|
+
const contract2 = new Contract2(channelFactory);
|
|
1763
2213
|
const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
|
|
1764
2214
|
const account = await withRetry(async () => {
|
|
1765
2215
|
try {
|
|
@@ -1774,9 +2224,9 @@ var MppSessionClient = class {
|
|
|
1774
2224
|
fee: BASE_FEE2,
|
|
1775
2225
|
networkPassphrase: passphrase
|
|
1776
2226
|
}).addOperation(
|
|
1777
|
-
|
|
2227
|
+
contract2.call(
|
|
1778
2228
|
"prepare_commitment",
|
|
1779
|
-
|
|
2229
|
+
nativeToScVal3(currentCumulative, { type: "i128" })
|
|
1780
2230
|
)
|
|
1781
2231
|
).setTimeout(30).build();
|
|
1782
2232
|
return await server.simulateTransaction(simTx);
|
|
@@ -1795,7 +2245,7 @@ var MppSessionClient = class {
|
|
|
1795
2245
|
}
|
|
1796
2246
|
let signature;
|
|
1797
2247
|
try {
|
|
1798
|
-
signature = commitmentKey.sign(
|
|
2248
|
+
signature = commitmentKey.sign(commitmentBytes);
|
|
1799
2249
|
} catch (err) {
|
|
1800
2250
|
throw new RouteDockSignatureError("Channel close commitment signing failed", {
|
|
1801
2251
|
cause: err
|
|
@@ -1809,7 +2259,7 @@ var MppSessionClient = class {
|
|
|
1809
2259
|
headers: { "Content-Type": "application/json" },
|
|
1810
2260
|
body: JSON.stringify({
|
|
1811
2261
|
amount: currentCumulative.toString(),
|
|
1812
|
-
signature:
|
|
2262
|
+
signature: bytesToHex(signature)
|
|
1813
2263
|
})
|
|
1814
2264
|
});
|
|
1815
2265
|
} catch (err) {
|
|
@@ -1851,15 +2301,19 @@ var MppSessionClient = class {
|
|
|
1851
2301
|
vouchersIssued
|
|
1852
2302
|
};
|
|
1853
2303
|
},
|
|
1854
|
-
|
|
2304
|
+
/**
|
|
2305
|
+
* Request refund from the channel contract (initiates dispute).
|
|
2306
|
+
* Returns the transaction hash only after on-chain confirmation (SUCCESS).
|
|
2307
|
+
*/
|
|
2308
|
+
async requestRefund(options2) {
|
|
1855
2309
|
const { rpc: rpcMod, Contract: Contract2, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1856
2310
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1857
2311
|
const server = new rpcMod.Server(rpcUrl);
|
|
1858
|
-
const
|
|
2312
|
+
const contract2 = new Contract2(channelFactory);
|
|
1859
2313
|
const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
|
|
1860
2314
|
try {
|
|
1861
2315
|
const account = await server.getAccount(agentPublicKey);
|
|
1862
|
-
const tx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(
|
|
2316
|
+
const tx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(contract2.call("request_refund")).setTimeout(30).build();
|
|
1863
2317
|
const simResult = await server.simulateTransaction(tx);
|
|
1864
2318
|
if (rpcMod.Api.isSimulationError(simResult)) {
|
|
1865
2319
|
throw new RouteDockDisputeError(`request_refund simulation failed: ${simResult.error}`);
|
|
@@ -1871,37 +2325,50 @@ var MppSessionClient = class {
|
|
|
1871
2325
|
const errDetail = result.errorResult ? JSON.stringify(result.errorResult) : "status ERROR";
|
|
1872
2326
|
throw new RouteDockDisputeError(`Refund request transaction failed: ${errDetail}`);
|
|
1873
2327
|
}
|
|
2328
|
+
if (result.status === "TRY_AGAIN_LATER") {
|
|
2329
|
+
throw new RouteDockDisputeError("Refund request transaction rejected with TRY_AGAIN_LATER: transaction not queued");
|
|
2330
|
+
}
|
|
1874
2331
|
if (!result.hash) {
|
|
1875
2332
|
throw new RouteDockDisputeError("Refund request transaction not sent");
|
|
1876
2333
|
}
|
|
1877
|
-
return
|
|
2334
|
+
return await confirmTransaction(
|
|
2335
|
+
server,
|
|
2336
|
+
result.hash,
|
|
2337
|
+
"Refund request",
|
|
2338
|
+
options2?.timeoutMs,
|
|
2339
|
+
options2?.intervalMs
|
|
2340
|
+
);
|
|
1878
2341
|
} catch (err) {
|
|
1879
2342
|
if (err instanceof RouteDockDisputeError) throw err;
|
|
1880
2343
|
throw new RouteDockDisputeError(`Failed to request refund: ${err instanceof Error ? err.message : String(err)}`);
|
|
1881
2344
|
}
|
|
1882
2345
|
},
|
|
1883
|
-
|
|
1884
|
-
|
|
2346
|
+
/**
|
|
2347
|
+
* Server-side counter-mechanism to settle with latest voucher before refund window expires.
|
|
2348
|
+
* Returns the transaction hash only after on-chain confirmation (SUCCESS).
|
|
2349
|
+
*/
|
|
2350
|
+
async settleWithLatestVoucher(options2) {
|
|
2351
|
+
const { rpc: rpcMod, Contract: Contract2, nativeToScVal: nativeToScVal3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1885
2352
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1886
2353
|
const server = new rpcMod.Server(rpcUrl);
|
|
1887
|
-
const
|
|
2354
|
+
const contract2 = new Contract2(channelFactory);
|
|
1888
2355
|
const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
|
|
1889
2356
|
try {
|
|
1890
2357
|
const account = await server.getAccount(agentPublicKey);
|
|
1891
|
-
const simTx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(
|
|
2358
|
+
const simTx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(contract2.call("prepare_commitment", nativeToScVal3(currentCumulative, { type: "i128" }))).setTimeout(30).build();
|
|
1892
2359
|
const simResult = await server.simulateTransaction(simTx);
|
|
1893
2360
|
if (rpcMod.Api.isSimulationError(simResult)) {
|
|
1894
2361
|
throw new RouteDockDisputeError(`prepare_commitment simulation failed: ${simResult.error}`);
|
|
1895
2362
|
}
|
|
1896
2363
|
const commitmentBytes = simResult.result?.retval?.bytes();
|
|
1897
2364
|
if (!commitmentBytes) throw new RouteDockDisputeError("prepare_commitment returned no bytes");
|
|
1898
|
-
const signature = commitmentKey.sign(
|
|
2365
|
+
const signature = commitmentKey.sign(commitmentBytes);
|
|
1899
2366
|
const settleAccount = await server.getAccount(agentPublicKey);
|
|
1900
2367
|
const settleTx = new TransactionBuilder2(settleAccount, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(
|
|
1901
|
-
|
|
2368
|
+
contract2.call(
|
|
1902
2369
|
"settle_with_signature",
|
|
1903
|
-
|
|
1904
|
-
|
|
2370
|
+
nativeToScVal3(currentCumulative, { type: "i128" }),
|
|
2371
|
+
nativeToScVal3(signature)
|
|
1905
2372
|
)
|
|
1906
2373
|
).setTimeout(30).build();
|
|
1907
2374
|
const preparedSettleTx = await server.prepareTransaction(settleTx);
|
|
@@ -1911,10 +2378,19 @@ var MppSessionClient = class {
|
|
|
1911
2378
|
const errDetail = settleResult.errorResult ? JSON.stringify(settleResult.errorResult) : "status ERROR";
|
|
1912
2379
|
throw new RouteDockDisputeError(`Settlement transaction failed: ${errDetail}`);
|
|
1913
2380
|
}
|
|
2381
|
+
if (settleResult.status === "TRY_AGAIN_LATER") {
|
|
2382
|
+
throw new RouteDockDisputeError("Settlement transaction rejected with TRY_AGAIN_LATER: transaction not queued");
|
|
2383
|
+
}
|
|
1914
2384
|
if (!settleResult.hash) {
|
|
1915
2385
|
throw new RouteDockDisputeError("Settlement transaction not sent");
|
|
1916
2386
|
}
|
|
1917
|
-
return
|
|
2387
|
+
return await confirmTransaction(
|
|
2388
|
+
server,
|
|
2389
|
+
settleResult.hash,
|
|
2390
|
+
"Settlement",
|
|
2391
|
+
options2?.timeoutMs,
|
|
2392
|
+
options2?.intervalMs
|
|
2393
|
+
);
|
|
1918
2394
|
} catch (err) {
|
|
1919
2395
|
if (err instanceof RouteDockDisputeError) throw err;
|
|
1920
2396
|
throw new RouteDockDisputeError(`Failed to settle with latest voucher: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1924,11 +2400,11 @@ var MppSessionClient = class {
|
|
|
1924
2400
|
const { rpc: rpcMod, Contract: Contract2, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1925
2401
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1926
2402
|
const server = new rpcMod.Server(rpcUrl);
|
|
1927
|
-
const
|
|
2403
|
+
const contract2 = new Contract2(channelFactory);
|
|
1928
2404
|
const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
|
|
1929
2405
|
try {
|
|
1930
2406
|
const account = await server.getAccount(agentPublicKey);
|
|
1931
|
-
const tx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(
|
|
2407
|
+
const tx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(contract2.call("get_channel_state")).setTimeout(30).build();
|
|
1932
2408
|
const simResult = await server.simulateTransaction(tx);
|
|
1933
2409
|
if (rpcMod.Api.isSimulationError(simResult)) {
|
|
1934
2410
|
throw new RouteDockChannelStateError(`Failed to query channel state: ${simResult.error}`);
|
|
@@ -1994,11 +2470,11 @@ var MppSessionClient = class {
|
|
|
1994
2470
|
* close code (1000/1001); abnormal closes and connection failures surface as
|
|
1995
2471
|
* RouteDockChannelStateError.
|
|
1996
2472
|
*/
|
|
1997
|
-
async *streamWebSocket(url, mppx$1, createCredential, onSigned) {
|
|
1998
|
-
const
|
|
2473
|
+
async *streamWebSocket(url, mppx$1, createCredential, onSigned, request) {
|
|
2474
|
+
const probeInit = toRequestInit(request);
|
|
1999
2475
|
let probe;
|
|
2000
2476
|
try {
|
|
2001
|
-
probe = await mppx$1.rawFetch(url,
|
|
2477
|
+
probe = await mppx$1.rawFetch(url, probeInit);
|
|
2002
2478
|
} catch (err) {
|
|
2003
2479
|
throw wrapFetchError(err, "Voucher request");
|
|
2004
2480
|
}
|
|
@@ -2102,235 +2578,19 @@ var MppSessionClient = class {
|
|
|
2102
2578
|
}
|
|
2103
2579
|
}
|
|
2104
2580
|
};
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
this.code = code;
|
|
2111
|
-
}
|
|
2112
|
-
};
|
|
2113
|
-
function sha256Hex(data) {
|
|
2114
|
-
const buf = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
2115
|
-
return crypto$1.createHash("sha256").update(buf).digest("hex");
|
|
2116
|
-
}
|
|
2117
|
-
function commitDailyCap(dailyCapStroops, witnessSecret) {
|
|
2118
|
-
return sha256Hex(`${witnessSecret}:cap:${dailyCapStroops.toString()}`);
|
|
2119
|
-
}
|
|
2120
|
-
function commitAllowlist(payees, witnessSecret) {
|
|
2121
|
-
const sorted = [...payees].sort().join(",");
|
|
2122
|
-
return sha256Hex(`${witnessSecret}:allowlist:${sorted}`);
|
|
2123
|
-
}
|
|
2124
|
-
function hashPayee(payee) {
|
|
2125
|
-
return sha256Hex(`payee:${payee}`);
|
|
2126
|
-
}
|
|
2127
|
-
function dayBucketFromLedger(ledgerSequence) {
|
|
2128
|
-
return Math.floor(ledgerSequence / 17280);
|
|
2129
|
-
}
|
|
2130
|
-
function authDigestFromEntry(authEntryBase64) {
|
|
2131
|
-
const entryHash = stellarSdk.hash(Buffer.from(authEntryBase64, "base64"));
|
|
2132
|
-
return sha256Hex(entryHash);
|
|
2133
|
-
}
|
|
2134
|
-
function assertAuthEntryMatchesContext(authEntry, from, ctx) {
|
|
2135
|
-
let preimage;
|
|
2581
|
+
function createNulthSigner(config) {
|
|
2582
|
+
const sharedConfig = { ...config, logger: resolveLogger(config.logger) };
|
|
2583
|
+
Object.defineProperty(sharedConfig, "paymentContext", {
|
|
2584
|
+
get: () => config.paymentContext
|
|
2585
|
+
});
|
|
2136
2586
|
try {
|
|
2137
|
-
|
|
2138
|
-
} catch {
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
if (preimage.switch() !== stellarSdk.xdr.EnvelopeType.envelopeTypeSorobanAuthorization()) {
|
|
2142
|
-
throw new NulthPolicyError(
|
|
2143
|
-
"auth_entry_mismatch",
|
|
2144
|
-
"authEntry is not a Soroban authorization preimage"
|
|
2145
|
-
);
|
|
2146
|
-
}
|
|
2147
|
-
const inv = preimage.sorobanAuthorization().invocation();
|
|
2148
|
-
if (inv.subInvocations().length !== 0) {
|
|
2149
|
-
throw new NulthPolicyError("auth_entry_mismatch", "authEntry has unexpected sub-invocations");
|
|
2150
|
-
}
|
|
2151
|
-
if (inv.function().switch() !== stellarSdk.xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn()) {
|
|
2152
|
-
throw new NulthPolicyError("auth_entry_mismatch", "authEntry does not invoke a contract function");
|
|
2153
|
-
}
|
|
2154
|
-
const fn = inv.function().contractFn();
|
|
2155
|
-
const contractAddress = stellarSdk.Address.fromScAddress(fn.contractAddress()).toString();
|
|
2156
|
-
if (contractAddress !== ctx.assetContract) {
|
|
2157
|
-
throw new NulthPolicyError(
|
|
2158
|
-
"auth_entry_mismatch",
|
|
2159
|
-
`authEntry contract ${contractAddress} does not match assetContract ${ctx.assetContract}`
|
|
2160
|
-
);
|
|
2161
|
-
}
|
|
2162
|
-
const functionName = fn.functionName().toString();
|
|
2163
|
-
if (functionName !== "transfer") {
|
|
2164
|
-
throw new NulthPolicyError(
|
|
2165
|
-
"auth_entry_mismatch",
|
|
2166
|
-
`authEntry function ${functionName} does not match expected transfer`
|
|
2167
|
-
);
|
|
2168
|
-
}
|
|
2169
|
-
const args = fn.args();
|
|
2170
|
-
if (args.length !== 3) {
|
|
2171
|
-
throw new NulthPolicyError("auth_entry_mismatch", `authEntry transfer has ${args.length} args, expected 3`);
|
|
2172
|
-
}
|
|
2173
|
-
const entryFrom = stellarSdk.scValToNative(args[0]);
|
|
2174
|
-
if (entryFrom !== from) {
|
|
2175
|
-
throw new NulthPolicyError(
|
|
2176
|
-
"auth_entry_mismatch",
|
|
2177
|
-
`authEntry from ${entryFrom} does not match nulthAccount ${from}`
|
|
2178
|
-
);
|
|
2179
|
-
}
|
|
2180
|
-
const entryTo = stellarSdk.scValToNative(args[1]);
|
|
2181
|
-
if (entryTo !== ctx.payee) {
|
|
2182
|
-
throw new NulthPolicyError(
|
|
2183
|
-
"auth_entry_mismatch",
|
|
2184
|
-
`authEntry to ${entryTo} does not match paymentContext.payee ${ctx.payee}`
|
|
2185
|
-
);
|
|
2186
|
-
}
|
|
2187
|
-
const entryAmount = stellarSdk.scValToNative(args[2]);
|
|
2188
|
-
if (entryAmount !== ctx.amountStroops) {
|
|
2189
|
-
throw new NulthPolicyError(
|
|
2190
|
-
"auth_entry_mismatch",
|
|
2191
|
-
`authEntry amount ${entryAmount} does not match paymentContext.amountStroops ${ctx.amountStroops}`
|
|
2192
|
-
);
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
function insecureMockProof(preimage) {
|
|
2196
|
-
return crypto$1.createHash("sha512").update(`nulth:${preimage}`).digest("base64");
|
|
2197
|
-
}
|
|
2198
|
-
function encodeAuthSignature(proof) {
|
|
2199
|
-
const payload = { nulth: "zk-v1", proof };
|
|
2200
|
-
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
2201
|
-
}
|
|
2202
|
-
function usdcToStroops2(amount) {
|
|
2203
|
-
const [whole = "0", frac = ""] = amount.split(".");
|
|
2204
|
-
const padded = (frac + "0000000").slice(0, 7);
|
|
2205
|
-
return BigInt(whole) * 10000000n + BigInt(padded);
|
|
2206
|
-
}
|
|
2207
|
-
var NulthClient = class {
|
|
2208
|
-
constructor(config) {
|
|
2209
|
-
this.config = config;
|
|
2210
|
-
this.policy = { ...config.policy };
|
|
2211
|
-
this.prover = config.prover ?? "mock";
|
|
2212
|
-
if (this.prover !== "mock") {
|
|
2213
|
-
throw new RouteDockManifestError(
|
|
2214
|
-
`NulthClient: unknown prover backend ${String(config.prover)}; only 'mock' is implemented`
|
|
2215
|
-
);
|
|
2216
|
-
}
|
|
2217
|
-
if (config.network !== "testnet") {
|
|
2218
|
-
throw new RouteDockManifestError(
|
|
2219
|
-
`NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
|
|
2220
|
-
);
|
|
2221
|
-
}
|
|
2222
|
-
resolveLogger(config.logger)(
|
|
2223
|
-
"warn",
|
|
2224
|
-
"NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
|
|
2225
|
-
);
|
|
2226
|
-
}
|
|
2227
|
-
config;
|
|
2228
|
-
policy;
|
|
2229
|
-
prover;
|
|
2230
|
-
get proverBackend() {
|
|
2231
|
-
return this.prover;
|
|
2232
|
-
}
|
|
2233
|
-
get nulthAccount() {
|
|
2234
|
-
return this.config.nulthAccount;
|
|
2235
|
-
}
|
|
2236
|
-
get allowlistCommitment() {
|
|
2237
|
-
return this.policy.allowlistCommitment;
|
|
2238
|
-
}
|
|
2239
|
-
get capCommitment() {
|
|
2240
|
-
return commitDailyCap(this.policy.dailyCapStroops, this.policy.witnessSecret);
|
|
2241
|
-
}
|
|
2242
|
-
buildPaymentAuthProof(context) {
|
|
2243
|
-
this.enforcePolicy(context);
|
|
2244
|
-
const bucket = dayBucketFromLedger(context.ledgerSequence);
|
|
2245
|
-
if (bucket !== this.policy.dayBucket) {
|
|
2246
|
-
this.policy.dayBucket = bucket;
|
|
2247
|
-
this.policy.dailySpendStroops = 0n;
|
|
2248
|
-
}
|
|
2249
|
-
const authDigest = authDigestFromEntry(context.authEntry);
|
|
2250
|
-
const capCommitment = this.capCommitment;
|
|
2251
|
-
const payeeHash = hashPayee(context.payee);
|
|
2252
|
-
const preimage = [
|
|
2253
|
-
authDigest,
|
|
2254
|
-
this.policy.allowlistCommitment,
|
|
2255
|
-
capCommitment,
|
|
2256
|
-
payeeHash,
|
|
2257
|
-
context.amountStroops.toString(),
|
|
2258
|
-
bucket.toString(),
|
|
2259
|
-
this.config.nulthAccount
|
|
2260
|
-
].join("|");
|
|
2261
|
-
const proof = {
|
|
2262
|
-
version: 1,
|
|
2263
|
-
proof: insecureMockProof(preimage),
|
|
2264
|
-
publicInputs: {
|
|
2265
|
-
authDigest,
|
|
2266
|
-
allowlistCommitment: this.policy.allowlistCommitment,
|
|
2267
|
-
capCommitment,
|
|
2268
|
-
payeeHash,
|
|
2269
|
-
amountStroops: context.amountStroops.toString(),
|
|
2270
|
-
dayBucket: bucket.toString()
|
|
2271
|
-
}
|
|
2272
|
-
};
|
|
2273
|
-
this.policy.dailySpendStroops += context.amountStroops;
|
|
2274
|
-
return proof;
|
|
2275
|
-
}
|
|
2276
|
-
enforcePolicy(context) {
|
|
2277
|
-
if (context.amountStroops < 0n) {
|
|
2278
|
-
throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
|
|
2279
|
-
}
|
|
2280
|
-
if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
|
|
2281
|
-
throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
|
|
2282
|
-
}
|
|
2283
|
-
if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
|
|
2284
|
-
throw new NulthPolicyError("session_expired", "Nulth session expired");
|
|
2285
|
-
}
|
|
2286
|
-
if (!this.policy.allowedPayees.includes(context.payee)) {
|
|
2287
|
-
throw new NulthPolicyError("payee_not_allowed", `Payee ${context.payee} not in off-chain allowlist`);
|
|
2288
|
-
}
|
|
2289
|
-
const bucket = dayBucketFromLedger(context.ledgerSequence);
|
|
2290
|
-
const spend = bucket === this.policy.dayBucket ? this.policy.dailySpendStroops : 0n;
|
|
2291
|
-
if (spend + context.amountStroops > this.policy.dailyCapStroops) {
|
|
2292
|
-
throw new NulthPolicyError("daily_cap_exceeded", "Nulth daily cap exceeded");
|
|
2587
|
+
return nulthSdk.createNulthSigner(sharedConfig);
|
|
2588
|
+
} catch (error) {
|
|
2589
|
+
if (error instanceof nulthSdk.NulthConfigError) {
|
|
2590
|
+
throw new RouteDockManifestError(error.message, { cause: error });
|
|
2293
2591
|
}
|
|
2592
|
+
throw error;
|
|
2294
2593
|
}
|
|
2295
|
-
};
|
|
2296
|
-
function createNulthSigner(config) {
|
|
2297
|
-
const client = new NulthClient(config);
|
|
2298
|
-
const nulthAccount = config.nulthAccount;
|
|
2299
|
-
return {
|
|
2300
|
-
address: nulthAccount,
|
|
2301
|
-
signAuthEntry: async (authEntry) => {
|
|
2302
|
-
const ctx = config.paymentContext;
|
|
2303
|
-
if (!ctx) throw new Error("Nulth paymentContext must be set before signing");
|
|
2304
|
-
assertAuthEntryMatchesContext(authEntry, nulthAccount, ctx);
|
|
2305
|
-
const proof = client.buildPaymentAuthProof({ authEntry, ...ctx });
|
|
2306
|
-
return { signedAuthEntry: encodeAuthSignature(proof), signerAddress: nulthAccount };
|
|
2307
|
-
}
|
|
2308
|
-
};
|
|
2309
|
-
}
|
|
2310
|
-
function setNulthPaymentContext(config, context) {
|
|
2311
|
-
config.paymentContext = context;
|
|
2312
|
-
}
|
|
2313
|
-
function paymentContextFromManifest(manifest, mode, ledgerSequence) {
|
|
2314
|
-
const pricing = manifest.pricing[mode];
|
|
2315
|
-
if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
|
|
2316
|
-
return {
|
|
2317
|
-
payee: manifest.payee,
|
|
2318
|
-
amountStroops: usdcToStroops2(pricing.amount),
|
|
2319
|
-
assetContract: manifest.asset_contract,
|
|
2320
|
-
ledgerSequence
|
|
2321
|
-
};
|
|
2322
|
-
}
|
|
2323
|
-
function createPolicyState(input) {
|
|
2324
|
-
const dailyCapStroops = usdcToStroops2(input.dailyCapUsdc);
|
|
2325
|
-
return {
|
|
2326
|
-
dailyCapStroops,
|
|
2327
|
-
allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
|
|
2328
|
-
allowedPayees: input.allowedPayees,
|
|
2329
|
-
dailySpendStroops: 0n,
|
|
2330
|
-
dayBucket: dayBucketFromLedger(input.ledgerSequence ?? 0),
|
|
2331
|
-
witnessSecret: input.witnessSecret,
|
|
2332
|
-
...input.expiryLedger !== void 0 ? { expiryLedger: input.expiryLedger } : {}
|
|
2333
|
-
};
|
|
2334
2594
|
}
|
|
2335
2595
|
function assertNulthVaultManifest(manifest, nulthAccount) {
|
|
2336
2596
|
if (manifest.vault !== void 0 && manifest.vault !== "nulth") {
|
|
@@ -2354,69 +2614,29 @@ async function prepareNulthSigner(vault, manifest, mode, network, ledgerSequence
|
|
|
2354
2614
|
"nulth vault uses the insecure mock prover and cannot be used on mainnet"
|
|
2355
2615
|
);
|
|
2356
2616
|
}
|
|
2357
|
-
const ledgerSequence = ledgerSequenceOverride ?? await fetchLedgerSequence(network);
|
|
2358
|
-
const policy = createPolicyState({
|
|
2359
|
-
dailyCapUsdc: vault.dailyCapUsdc,
|
|
2360
|
-
allowedPayees: vault.allowedPayees,
|
|
2361
|
-
witnessSecret: vault.witnessSecret,
|
|
2362
|
-
ledgerSequence,
|
|
2363
|
-
...vault.expiryLedger !== void 0 ? { expiryLedger: vault.expiryLedger } : {}
|
|
2364
|
-
});
|
|
2365
|
-
const config = {
|
|
2366
|
-
nulthAccount: vault.nulthAccount,
|
|
2367
|
-
network,
|
|
2368
|
-
prover,
|
|
2369
|
-
policy,
|
|
2370
|
-
...vault.verifierContract !== void 0 ? { verifierContract: vault.verifierContract } : {}
|
|
2371
|
-
};
|
|
2372
|
-
if (mode === "x402" || mode === "mpp-charge") {
|
|
2373
|
-
setNulthPaymentContext(config, paymentContextFromManifest(manifest, mode, ledgerSequence));
|
|
2374
|
-
} else {
|
|
2375
|
-
throw new RouteDockManifestError(`Nulth ZK vault does not support payment mode: ${mode}`);
|
|
2376
|
-
}
|
|
2377
|
-
const signer = createNulthSigner(config);
|
|
2378
|
-
return { signer, config };
|
|
2379
|
-
}
|
|
2380
|
-
function decodeAuthSignature(encoded) {
|
|
2381
|
-
const raw = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
2382
|
-
if (typeof raw !== "object" || raw === null || raw.nulth !== "zk-v1") {
|
|
2383
|
-
throw new Error("Invalid Nulth ZK auth signature");
|
|
2384
|
-
}
|
|
2385
|
-
return raw;
|
|
2386
|
-
}
|
|
2387
|
-
var FileSpendStore = class {
|
|
2388
|
-
filePath;
|
|
2389
|
-
constructor(filePath) {
|
|
2390
|
-
this.filePath = filePath;
|
|
2391
|
-
}
|
|
2392
|
-
async read() {
|
|
2393
|
-
let data;
|
|
2394
|
-
try {
|
|
2395
|
-
data = await fs__default.default.readFile(this.filePath, "utf-8");
|
|
2396
|
-
} catch (err) {
|
|
2397
|
-
if (err.code === "ENOENT") {
|
|
2398
|
-
return null;
|
|
2399
|
-
}
|
|
2400
|
-
throw new Error(
|
|
2401
|
-
`[FileSpendStore] Failed to read spend file at ${this.filePath}: ${err.message}`
|
|
2402
|
-
);
|
|
2403
|
-
}
|
|
2404
|
-
try {
|
|
2405
|
-
return JSON.parse(data);
|
|
2406
|
-
} catch {
|
|
2407
|
-
throw new Error(
|
|
2408
|
-
`[FileSpendStore] Spend file at ${this.filePath} contains invalid JSON. Delete or repair it before resuming. Raw content: ${data.slice(0, 120)}`
|
|
2409
|
-
);
|
|
2410
|
-
}
|
|
2411
|
-
}
|
|
2412
|
-
async write(state) {
|
|
2413
|
-
const dir = path__default.default.dirname(this.filePath);
|
|
2414
|
-
await fs__default.default.mkdir(dir, { recursive: true });
|
|
2415
|
-
const tmpPath = `${this.filePath}.tmp`;
|
|
2416
|
-
await fs__default.default.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
2417
|
-
await fs__default.default.rename(tmpPath, this.filePath);
|
|
2617
|
+
const ledgerSequence = ledgerSequenceOverride ?? await fetchLedgerSequence(network);
|
|
2618
|
+
const policy = nulthSdk.createPolicyState({
|
|
2619
|
+
dailyCapUsdc: vault.dailyCapUsdc,
|
|
2620
|
+
allowedPayees: vault.allowedPayees,
|
|
2621
|
+
witnessSecret: vault.witnessSecret,
|
|
2622
|
+
ledgerSequence,
|
|
2623
|
+
...vault.expiryLedger !== void 0 ? { expiryLedger: vault.expiryLedger } : {}
|
|
2624
|
+
});
|
|
2625
|
+
const config = {
|
|
2626
|
+
nulthAccount: vault.nulthAccount,
|
|
2627
|
+
network,
|
|
2628
|
+
prover,
|
|
2629
|
+
policy,
|
|
2630
|
+
...vault.verifierContract !== void 0 ? { verifierContract: vault.verifierContract } : {}
|
|
2631
|
+
};
|
|
2632
|
+
if (mode === "x402" || mode === "mpp-charge") {
|
|
2633
|
+
nulthSdk.setNulthPaymentContext(config, nulthSdk.paymentContextFromManifest(manifest, mode, ledgerSequence));
|
|
2634
|
+
} else {
|
|
2635
|
+
throw new RouteDockManifestError(`Nulth ZK vault does not support payment mode: ${mode}`);
|
|
2418
2636
|
}
|
|
2419
|
-
|
|
2637
|
+
const signer = createNulthSigner(config);
|
|
2638
|
+
return { signer, config };
|
|
2639
|
+
}
|
|
2420
2640
|
|
|
2421
2641
|
// src/store/SpendStore.ts
|
|
2422
2642
|
var InMemorySpendStore = class {
|
|
@@ -2530,7 +2750,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2530
2750
|
}
|
|
2531
2751
|
const secretKey = this.keypair.secret();
|
|
2532
2752
|
const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
|
|
2533
|
-
this.x402 = new X402Client(secretKey, this.network, retryPolicy);
|
|
2753
|
+
this.x402 = new X402Client(secretKey, this.network, retryPolicy, config.preSignHook, this.logger);
|
|
2534
2754
|
this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
|
|
2535
2755
|
this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
|
|
2536
2756
|
}
|
|
@@ -2538,7 +2758,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2538
2758
|
async _resolveManifest(url, options) {
|
|
2539
2759
|
const baseUrl = new URL(url).origin;
|
|
2540
2760
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2541
|
-
assertEndpointActive(manifest, url, this.logger);
|
|
2761
|
+
assertEndpointActive(manifest, url, this.logger, options?.request?.method);
|
|
2542
2762
|
const mode = selectMode(manifest, options);
|
|
2543
2763
|
return { manifest, mode };
|
|
2544
2764
|
}
|
|
@@ -2555,13 +2775,9 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2555
2775
|
});
|
|
2556
2776
|
return next;
|
|
2557
2777
|
}
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
* call before committing to a payment — for approval gates and manual
|
|
2562
|
-
* trustline remediation.
|
|
2563
|
-
*/
|
|
2564
|
-
async preflight(manifest, mode, endpoint) {
|
|
2778
|
+
async preflight(target, mode, endpoint) {
|
|
2779
|
+
if (typeof target === "string") return this._preflightUrl(target);
|
|
2780
|
+
const manifest = target;
|
|
2565
2781
|
assertManifestValid(manifest);
|
|
2566
2782
|
const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
|
|
2567
2783
|
await this._checkTrustline(manifest, selectedAsset);
|
|
@@ -2574,6 +2790,42 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2574
2790
|
...manifest.capabilities && { capabilities: manifest.capabilities }
|
|
2575
2791
|
};
|
|
2576
2792
|
}
|
|
2793
|
+
/** Manifest-only preflight for a URL — no trustline check, no payment. */
|
|
2794
|
+
async _preflightUrl(url) {
|
|
2795
|
+
const baseUrl = new URL(url).origin;
|
|
2796
|
+
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2797
|
+
assertEndpointActive(manifest, url);
|
|
2798
|
+
const costs = manifest.modes.map((mode) => {
|
|
2799
|
+
const asset = selectAsset(manifest, mode, url).asset;
|
|
2800
|
+
switch (mode) {
|
|
2801
|
+
case "x402":
|
|
2802
|
+
case "mpp-charge": {
|
|
2803
|
+
const pricing = manifest.pricing[mode];
|
|
2804
|
+
return { mode, amount: pricing.amount, per: "request", asset };
|
|
2805
|
+
}
|
|
2806
|
+
case "mpp-session":
|
|
2807
|
+
case "mpp-session-ws": {
|
|
2808
|
+
const pricing = manifest.pricing[mode];
|
|
2809
|
+
return { mode, amount: pricing.rate, per: "voucher", asset, min_deposit: pricing.min_deposit };
|
|
2810
|
+
}
|
|
2811
|
+
default:
|
|
2812
|
+
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2813
|
+
}
|
|
2814
|
+
});
|
|
2815
|
+
const pathname = new URL(url).pathname;
|
|
2816
|
+
const matches = Object.values(manifest.endpoints).filter((descriptor) => descriptor.path === pathname);
|
|
2817
|
+
const endpointSunsets = matches.map((descriptor) => descriptor.sunset_at).filter((value) => typeof value === "string").sort((a, b) => Date.parse(a) - Date.parse(b));
|
|
2818
|
+
return {
|
|
2819
|
+
modes: manifest.modes,
|
|
2820
|
+
costs,
|
|
2821
|
+
compliance: {
|
|
2822
|
+
deprecated_modes: manifest.deprecated_modes ?? [],
|
|
2823
|
+
...manifest.sunset_at && { sunset_at: manifest.sunset_at },
|
|
2824
|
+
endpoint_deprecated: matches.some((descriptor) => descriptor.deprecated === true),
|
|
2825
|
+
...endpointSunsets[0] && { endpoint_sunset_at: endpointSunsets[0] }
|
|
2826
|
+
}
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2577
2829
|
/**
|
|
2578
2830
|
* Verify via Horizon that the payer account has a trustline for the
|
|
2579
2831
|
* payment asset declared in the manifest. Throws RouteDockTrustlineError
|
|
@@ -2585,8 +2837,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2585
2837
|
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
|
|
2586
2838
|
const cached = _RouteDockClient._trustlineCache.get(cacheKey);
|
|
2587
2839
|
if (cached && Date.now() < cached.expiresAt) return;
|
|
2588
|
-
const
|
|
2589
|
-
const server = new stellarSdk.Horizon.Server(horizonUrl);
|
|
2840
|
+
const server = new stellarSdk.Horizon.Server(HORIZON_URLS[this.network]);
|
|
2590
2841
|
try {
|
|
2591
2842
|
const account = await server.loadAccount(this.keypair.publicKey());
|
|
2592
2843
|
const balances = account.balances;
|
|
@@ -2622,9 +2873,11 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2622
2873
|
async pay(url, options) {
|
|
2623
2874
|
const baseUrl = new URL(url).origin;
|
|
2624
2875
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2625
|
-
|
|
2876
|
+
const request = options?.request;
|
|
2877
|
+
assertEndpointActive(manifest, url, this.logger, request?.method);
|
|
2626
2878
|
const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
|
|
2627
2879
|
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2880
|
+
this._assertCapAsset(selectedAsset);
|
|
2628
2881
|
await this._checkTrustline(manifest, selectedAsset);
|
|
2629
2882
|
let amount;
|
|
2630
2883
|
switch (mode) {
|
|
@@ -2642,18 +2895,18 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2642
2895
|
default:
|
|
2643
2896
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2644
2897
|
}
|
|
2645
|
-
const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
|
|
2898
|
+
const reserveId = await this._checkAndReserveSpend(amount, baseUrl, selectedAsset);
|
|
2646
2899
|
let result;
|
|
2647
2900
|
try {
|
|
2648
2901
|
if (this.vault?.mode === "nulth") {
|
|
2649
|
-
result = await this._payWithNulthVault(url, manifest, mode);
|
|
2902
|
+
result = await this._payWithNulthVault(url, manifest, mode, request);
|
|
2650
2903
|
} else {
|
|
2651
2904
|
switch (mode) {
|
|
2652
2905
|
case "x402":
|
|
2653
|
-
result = await this.x402.pay(url, manifest);
|
|
2906
|
+
result = await this.x402.pay(url, manifest, request);
|
|
2654
2907
|
break;
|
|
2655
2908
|
case "mpp-charge":
|
|
2656
|
-
result = await this.charge.pay(url, manifest);
|
|
2909
|
+
result = await this.charge.pay(url, manifest, request);
|
|
2657
2910
|
break;
|
|
2658
2911
|
default:
|
|
2659
2912
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
@@ -2672,7 +2925,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2672
2925
|
return result;
|
|
2673
2926
|
}
|
|
2674
2927
|
/** Nulth ZK vault path — proof built off-chain, attached as auth signature */
|
|
2675
|
-
async _payWithNulthVault(url, manifest, mode) {
|
|
2928
|
+
async _payWithNulthVault(url, manifest, mode, request) {
|
|
2676
2929
|
const prover = this.vault?.prover ?? "mock";
|
|
2677
2930
|
if (this.network === "mainnet" && prover === "mock") {
|
|
2678
2931
|
throw new RouteDockManifestError("nulth vault uses a MOCK Groth16 prover and cannot be used on mainnet");
|
|
@@ -2685,10 +2938,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2685
2938
|
try {
|
|
2686
2939
|
const { signer } = await prepareNulthSigner(this.vault, manifest, mode, this.network);
|
|
2687
2940
|
const x402 = this.x402.withSigner(signer);
|
|
2688
|
-
const result = await x402.pay(url, manifest);
|
|
2941
|
+
const result = await x402.pay(url, manifest, request);
|
|
2689
2942
|
return result;
|
|
2690
2943
|
} catch (err) {
|
|
2691
|
-
if (err instanceof NulthPolicyError) {
|
|
2944
|
+
if (err instanceof nulthSdk.NulthPolicyError) {
|
|
2692
2945
|
throw new RouteDockPolicyRejectError(err.code);
|
|
2693
2946
|
}
|
|
2694
2947
|
throw err;
|
|
@@ -2754,17 +3007,35 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2754
3007
|
);
|
|
2755
3008
|
}
|
|
2756
3009
|
const endpointKey = new URL(url).origin;
|
|
2757
|
-
|
|
3010
|
+
let onSpend;
|
|
3011
|
+
if (this.spendCap) {
|
|
3012
|
+
const sessionAsset = selectAsset(manifest, mode, url).asset;
|
|
3013
|
+
this._assertCapAsset(sessionAsset);
|
|
3014
|
+
onSpend = (amount) => this._recordSessionSpend(amount, endpointKey, sessionAsset);
|
|
3015
|
+
}
|
|
2758
3016
|
return this.session.openSession(url, manifest, secret, options, onSpend);
|
|
2759
3017
|
}
|
|
3018
|
+
/**
|
|
3019
|
+
* The local cap accumulates one USDC total. Refuse to count any other asset
|
|
3020
|
+
* against it (or let it through uncounted) when a cap is configured.
|
|
3021
|
+
*/
|
|
3022
|
+
_assertCapAsset(asset) {
|
|
3023
|
+
if (this.spendCap && asset !== this.spendCap.asset) {
|
|
3024
|
+
throw new RouteDockPolicyRejectError("local_cap_asset_mismatch", {
|
|
3025
|
+
cause: new Error(
|
|
3026
|
+
`spendCap is denominated in ${this.spendCap.asset} but the selected payment asset is ${asset}`
|
|
3027
|
+
)
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
2760
3031
|
/**
|
|
2761
3032
|
* Record spend for an MPP session voucher. Called before each voucher
|
|
2762
3033
|
* fetch in stream(). Enforces the same local daily and per-endpoint caps
|
|
2763
3034
|
* as discrete pay() calls. Returns a commit function to clean up the
|
|
2764
3035
|
* reservation after the voucher fetch succeeds.
|
|
2765
3036
|
*/
|
|
2766
|
-
async _recordSessionSpend(amount, endpointKey) {
|
|
2767
|
-
const reserveId = await this._checkAndReserveSpend(amount, endpointKey);
|
|
3037
|
+
async _recordSessionSpend(amount, endpointKey, asset) {
|
|
3038
|
+
const reserveId = await this._checkAndReserveSpend(amount, endpointKey, asset);
|
|
2768
3039
|
if (reserveId) {
|
|
2769
3040
|
await this._commitSpend(reserveId);
|
|
2770
3041
|
}
|
|
@@ -2803,15 +3074,16 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2803
3074
|
* Both limits are independent: a payment is only reserved after **both** pass.
|
|
2804
3075
|
* All spend (regardless of endpoint) counts toward the global accumulator.
|
|
2805
3076
|
*/
|
|
2806
|
-
async _checkAndReserveSpend(amount, endpointKey) {
|
|
3077
|
+
async _checkAndReserveSpend(amount, endpointKey, asset) {
|
|
2807
3078
|
if (!this.spendCap) return "";
|
|
2808
|
-
|
|
3079
|
+
this._assertCapAsset(asset);
|
|
3080
|
+
const spendCap = this.spendCap;
|
|
3081
|
+
const amountMicros = nulthSdk.usdcToStroops(amount);
|
|
3082
|
+
const reserve = (prev) => {
|
|
2809
3083
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2810
|
-
const
|
|
2811
|
-
const current = persisted && persisted.date === today ? persisted : { date: today, totalMicros: "0", endpoints: {} };
|
|
2812
|
-
const amountMicros = nulthSdk.usdcToStroops(amount);
|
|
3084
|
+
const current = prev && prev.date === today ? { ...prev, endpoints: { ...prev.endpoints } } : { date: today, totalMicros: "0", endpoints: {} };
|
|
2813
3085
|
const total = BigInt(current.totalMicros);
|
|
2814
|
-
const endpointCapStr =
|
|
3086
|
+
const endpointCapStr = spendCap.endpointCaps?.[endpointKey];
|
|
2815
3087
|
if (endpointCapStr !== void 0) {
|
|
2816
3088
|
const endpointCapMicros = nulthSdk.usdcToStroops(endpointCapStr);
|
|
2817
3089
|
const endpointTotal = BigInt(current.endpoints[endpointKey] ?? "0");
|
|
@@ -2819,17 +3091,25 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2819
3091
|
throw new RouteDockPolicyRejectError("local_endpoint_cap_exceeded");
|
|
2820
3092
|
}
|
|
2821
3093
|
}
|
|
2822
|
-
const globalCapMicros = nulthSdk.usdcToStroops(
|
|
3094
|
+
const globalCapMicros = nulthSdk.usdcToStroops(spendCap.daily);
|
|
2823
3095
|
if (total + amountMicros > globalCapMicros) {
|
|
2824
3096
|
throw new RouteDockPolicyRejectError("local_daily_cap_exceeded");
|
|
2825
3097
|
}
|
|
2826
|
-
const reserveId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2827
|
-
this._pendingReserves.set(reserveId, { endpointKey, amountMicros });
|
|
2828
3098
|
current.totalMicros = (total + amountMicros).toString();
|
|
2829
3099
|
if (endpointCapStr !== void 0) {
|
|
2830
3100
|
current.endpoints[endpointKey] = (BigInt(current.endpoints[endpointKey] ?? "0") + amountMicros).toString();
|
|
2831
3101
|
}
|
|
2832
|
-
|
|
3102
|
+
return current;
|
|
3103
|
+
};
|
|
3104
|
+
return this._withSpendMutex(async () => {
|
|
3105
|
+
const store = this.spendStore;
|
|
3106
|
+
if (store.update) {
|
|
3107
|
+
await store.update(reserve);
|
|
3108
|
+
} else {
|
|
3109
|
+
await store.write(reserve(await store.read()));
|
|
3110
|
+
}
|
|
3111
|
+
const reserveId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3112
|
+
this._pendingReserves.set(reserveId, { endpointKey, amountMicros });
|
|
2833
3113
|
return reserveId;
|
|
2834
3114
|
});
|
|
2835
3115
|
}
|
|
@@ -2851,22 +3131,29 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2851
3131
|
if (!reserve) return;
|
|
2852
3132
|
this._pendingReserves.delete(reserveId);
|
|
2853
3133
|
const { endpointKey, amountMicros } = reserve;
|
|
2854
|
-
|
|
3134
|
+
const release = (prev) => {
|
|
2855
3135
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
persisted.totalMicros = (currentTotal - amountMicros).toString();
|
|
3136
|
+
if (!prev || prev.date !== today) return void 0;
|
|
3137
|
+
const persisted = { ...prev, endpoints: { ...prev.endpoints } };
|
|
3138
|
+
persisted.totalMicros = (BigInt(persisted.totalMicros) - amountMicros).toString();
|
|
2860
3139
|
if (persisted.endpoints[endpointKey]) {
|
|
2861
|
-
const
|
|
2862
|
-
const newEndpointTotal = endpointTotal - amountMicros;
|
|
3140
|
+
const newEndpointTotal = BigInt(persisted.endpoints[endpointKey]) - amountMicros;
|
|
2863
3141
|
if (newEndpointTotal <= 0n) {
|
|
2864
3142
|
delete persisted.endpoints[endpointKey];
|
|
2865
3143
|
} else {
|
|
2866
3144
|
persisted.endpoints[endpointKey] = newEndpointTotal.toString();
|
|
2867
3145
|
}
|
|
2868
3146
|
}
|
|
2869
|
-
|
|
3147
|
+
return persisted;
|
|
3148
|
+
};
|
|
3149
|
+
await this._withSpendMutex(async () => {
|
|
3150
|
+
const store = this.spendStore;
|
|
3151
|
+
if (store.update) {
|
|
3152
|
+
await store.update(release);
|
|
3153
|
+
} else {
|
|
3154
|
+
const next = release(await store.read());
|
|
3155
|
+
if (next) await store.write(next);
|
|
3156
|
+
}
|
|
2870
3157
|
});
|
|
2871
3158
|
}
|
|
2872
3159
|
};
|
|
@@ -2950,6 +3237,158 @@ var SupabaseSessionStore = class {
|
|
|
2950
3237
|
await this.setStatus(channelId, "closed");
|
|
2951
3238
|
}
|
|
2952
3239
|
};
|
|
3240
|
+
var DEFAULT_LOCK_STALE_MS = 1e4;
|
|
3241
|
+
var DEFAULT_LOCK_TIMEOUT_MS = 15e3;
|
|
3242
|
+
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3243
|
+
var FileSpendStore = class {
|
|
3244
|
+
filePath;
|
|
3245
|
+
lockPath;
|
|
3246
|
+
lockStaleMs;
|
|
3247
|
+
lockTimeoutMs;
|
|
3248
|
+
constructor(filePath, options = {}) {
|
|
3249
|
+
this.filePath = filePath;
|
|
3250
|
+
this.lockPath = `${filePath}.lock`;
|
|
3251
|
+
this.lockStaleMs = options.lockStaleMs ?? DEFAULT_LOCK_STALE_MS;
|
|
3252
|
+
this.lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
3253
|
+
}
|
|
3254
|
+
async read() {
|
|
3255
|
+
let data;
|
|
3256
|
+
try {
|
|
3257
|
+
data = await fs__default.default.readFile(this.filePath, "utf-8");
|
|
3258
|
+
} catch (err) {
|
|
3259
|
+
if (err.code === "ENOENT") {
|
|
3260
|
+
return null;
|
|
3261
|
+
}
|
|
3262
|
+
throw new Error(
|
|
3263
|
+
`[FileSpendStore] Failed to read spend file at ${this.filePath}: ${err.message}`
|
|
3264
|
+
);
|
|
3265
|
+
}
|
|
3266
|
+
try {
|
|
3267
|
+
return JSON.parse(data);
|
|
3268
|
+
} catch {
|
|
3269
|
+
throw new Error(
|
|
3270
|
+
`[FileSpendStore] Spend file at ${this.filePath} contains invalid JSON. Delete or repair it before resuming. Raw content: ${data.slice(0, 120)}`
|
|
3271
|
+
);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
async write(state) {
|
|
3275
|
+
await this.withLock(async (assertHeld) => {
|
|
3276
|
+
await this.writeFile(state, assertHeld);
|
|
3277
|
+
});
|
|
3278
|
+
}
|
|
3279
|
+
/**
|
|
3280
|
+
* Atomically read-modify-write the accumulator under the cross-process lock.
|
|
3281
|
+
* `fn` receives the current state (or null) and returns the state to persist,
|
|
3282
|
+
* or `undefined` to leave the file untouched. If `fn` throws, nothing is
|
|
3283
|
+
* written and the lock is released.
|
|
3284
|
+
*/
|
|
3285
|
+
async update(fn) {
|
|
3286
|
+
return this.withLock(async (assertHeld) => {
|
|
3287
|
+
const prev = await this.read();
|
|
3288
|
+
const next = fn(prev);
|
|
3289
|
+
if (next === void 0) return prev;
|
|
3290
|
+
await this.writeFile(next, assertHeld);
|
|
3291
|
+
return next;
|
|
3292
|
+
});
|
|
3293
|
+
}
|
|
3294
|
+
// ── internals ──────────────────────────────────────────────────────────────
|
|
3295
|
+
async writeFile(state, assertHeld) {
|
|
3296
|
+
const tmpPath = `${this.filePath}.${process.pid}.${crypto$1.randomBytes(6).toString("hex")}.tmp`;
|
|
3297
|
+
try {
|
|
3298
|
+
await fs__default.default.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
3299
|
+
await assertHeld();
|
|
3300
|
+
await fs__default.default.rename(tmpPath, this.filePath);
|
|
3301
|
+
} catch (err) {
|
|
3302
|
+
await fs__default.default.rm(tmpPath, { force: true }).catch(() => {
|
|
3303
|
+
});
|
|
3304
|
+
throw err;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
/** Run `fn` while holding the lock file; always releases it. */
|
|
3308
|
+
async withLock(fn) {
|
|
3309
|
+
await fs__default.default.mkdir(path__default.default.dirname(this.filePath), { recursive: true });
|
|
3310
|
+
const token = `${process.pid}:${crypto$1.randomBytes(8).toString("hex")}`;
|
|
3311
|
+
await this.acquireLock(token);
|
|
3312
|
+
const heartbeat = setInterval(() => {
|
|
3313
|
+
const now = /* @__PURE__ */ new Date();
|
|
3314
|
+
fs__default.default.utimes(this.lockPath, now, now).catch(() => {
|
|
3315
|
+
});
|
|
3316
|
+
}, Math.max(10, Math.floor(this.lockStaleMs / 3)));
|
|
3317
|
+
heartbeat.unref();
|
|
3318
|
+
const assertHeld = async () => {
|
|
3319
|
+
let current;
|
|
3320
|
+
try {
|
|
3321
|
+
current = await fs__default.default.readFile(this.lockPath, "utf-8");
|
|
3322
|
+
} catch {
|
|
3323
|
+
current = "";
|
|
3324
|
+
}
|
|
3325
|
+
if (current !== token) {
|
|
3326
|
+
throw new Error(
|
|
3327
|
+
`[FileSpendStore] Lost the lock on ${this.lockPath} before committing; refusing to overwrite the spend file. Retry the operation.`
|
|
3328
|
+
);
|
|
3329
|
+
}
|
|
3330
|
+
};
|
|
3331
|
+
try {
|
|
3332
|
+
return await fn(assertHeld);
|
|
3333
|
+
} finally {
|
|
3334
|
+
clearInterval(heartbeat);
|
|
3335
|
+
await this.releaseLock(token);
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
async acquireLock(token) {
|
|
3339
|
+
const deadline = Date.now() + this.lockTimeoutMs;
|
|
3340
|
+
let delay2 = 5;
|
|
3341
|
+
for (; ; ) {
|
|
3342
|
+
try {
|
|
3343
|
+
const handle = await fs__default.default.open(this.lockPath, "wx");
|
|
3344
|
+
try {
|
|
3345
|
+
await handle.writeFile(token, "utf-8");
|
|
3346
|
+
} finally {
|
|
3347
|
+
await handle.close();
|
|
3348
|
+
}
|
|
3349
|
+
return;
|
|
3350
|
+
} catch (err) {
|
|
3351
|
+
if (err.code !== "EEXIST") {
|
|
3352
|
+
throw new Error(
|
|
3353
|
+
`[FileSpendStore] Failed to create lock file ${this.lockPath}: ${err.message}`
|
|
3354
|
+
);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
await this.breakIfStale();
|
|
3358
|
+
if (Date.now() >= deadline) {
|
|
3359
|
+
throw new Error(
|
|
3360
|
+
`[FileSpendStore] Timed out after ${this.lockTimeoutMs}ms waiting for ${this.lockPath}. Another process is holding the spend file lock; if none is running, delete the lock file.`
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
await sleep2(delay2 + Math.floor(Math.random() * delay2));
|
|
3364
|
+
delay2 = Math.min(delay2 * 2, 50);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
/** Remove the lock if its holder stopped refreshing it (crashed process). */
|
|
3368
|
+
async breakIfStale() {
|
|
3369
|
+
let before;
|
|
3370
|
+
try {
|
|
3371
|
+
before = await fs__default.default.stat(this.lockPath);
|
|
3372
|
+
} catch {
|
|
3373
|
+
return;
|
|
3374
|
+
}
|
|
3375
|
+
if (Date.now() - before.mtimeMs <= this.lockStaleMs) return;
|
|
3376
|
+
try {
|
|
3377
|
+
const after = await fs__default.default.stat(this.lockPath);
|
|
3378
|
+
if (after.ino !== before.ino || after.mtimeMs !== before.mtimeMs) return;
|
|
3379
|
+
await fs__default.default.unlink(this.lockPath);
|
|
3380
|
+
} catch {
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
async releaseLock(token) {
|
|
3384
|
+
try {
|
|
3385
|
+
if (await fs__default.default.readFile(this.lockPath, "utf-8") === token) {
|
|
3386
|
+
await fs__default.default.unlink(this.lockPath);
|
|
3387
|
+
}
|
|
3388
|
+
} catch {
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
};
|
|
2953
3392
|
|
|
2954
3393
|
// src/store/SupabaseSeenTxStore.ts
|
|
2955
3394
|
var SupabaseSeenTxStore = class {
|
|
@@ -3001,6 +3440,107 @@ var SupabaseSeenTxStore = class {
|
|
|
3001
3440
|
return data;
|
|
3002
3441
|
}
|
|
3003
3442
|
};
|
|
3443
|
+
var FLAT_MODES = ["x402", "mpp-charge"];
|
|
3444
|
+
var SESSION_MODES = ["mpp-session", "mpp-session-ws"];
|
|
3445
|
+
function sameAmount(adapter, label, optionValue, manifestValue) {
|
|
3446
|
+
const parse = (where, value) => {
|
|
3447
|
+
try {
|
|
3448
|
+
return nulthSdk.usdcToStroops(value);
|
|
3449
|
+
} catch {
|
|
3450
|
+
throw new RouteDockManifestError(
|
|
3451
|
+
`${adapter}: ${label} has an unparseable ${where} amount "${value}"`
|
|
3452
|
+
);
|
|
3453
|
+
}
|
|
3454
|
+
};
|
|
3455
|
+
return parse("option", optionValue) === parse("manifest", manifestValue);
|
|
3456
|
+
}
|
|
3457
|
+
function assertProviderConfig(opts, adapterName) {
|
|
3458
|
+
const { manifest } = opts;
|
|
3459
|
+
if (manifest === null || typeof manifest !== "object") {
|
|
3460
|
+
throw new RouteDockManifestError(`${adapterName}: opts.manifest is required and must be an object`);
|
|
3461
|
+
}
|
|
3462
|
+
try {
|
|
3463
|
+
validateManifestSchema(manifest);
|
|
3464
|
+
assertManifestValid(manifest);
|
|
3465
|
+
} catch (err) {
|
|
3466
|
+
if (err instanceof RouteDockManifestError) {
|
|
3467
|
+
throw new RouteDockManifestError(`${adapterName}: ${err.message}`, { cause: err });
|
|
3468
|
+
}
|
|
3469
|
+
throw err;
|
|
3470
|
+
}
|
|
3471
|
+
const served = new Set(opts.modes.filter((mode) => Boolean(opts.pricing[mode])));
|
|
3472
|
+
const advertised = new Set(manifest.modes);
|
|
3473
|
+
const notAdvertised = [...served].filter((mode) => !advertised.has(mode));
|
|
3474
|
+
const notServed = [...advertised].filter((mode) => !served.has(mode));
|
|
3475
|
+
if (notAdvertised.length > 0 || notServed.length > 0) {
|
|
3476
|
+
const parts = [];
|
|
3477
|
+
if (notAdvertised.length > 0) parts.push(`served but not in manifest.modes: ${notAdvertised.join(", ")}`);
|
|
3478
|
+
if (notServed.length > 0) parts.push(`in manifest.modes but not served (missing from modes or pricing): ${notServed.join(", ")}`);
|
|
3479
|
+
throw new RouteDockManifestError(`${adapterName}: modes do not match manifest.modes (${parts.join("; ")})`);
|
|
3480
|
+
}
|
|
3481
|
+
for (const mode of FLAT_MODES) {
|
|
3482
|
+
if (!served.has(mode)) continue;
|
|
3483
|
+
const optionValue = opts.pricing[mode];
|
|
3484
|
+
const manifestValue = manifest.pricing[mode]?.amount;
|
|
3485
|
+
if (manifestValue === void 0) {
|
|
3486
|
+
throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode}.amount is missing but ${mode} is served`);
|
|
3487
|
+
}
|
|
3488
|
+
if (!sameAmount(adapterName, `pricing.${mode}`, optionValue, manifestValue)) {
|
|
3489
|
+
throw new RouteDockManifestError(
|
|
3490
|
+
`${adapterName}: pricing.${mode} "${optionValue}" does not match manifest.pricing.${mode}.amount "${manifestValue}"`
|
|
3491
|
+
);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
for (const mode of SESSION_MODES) {
|
|
3495
|
+
if (!served.has(mode)) continue;
|
|
3496
|
+
const optionValue = opts.pricing[mode];
|
|
3497
|
+
const manifestValue = manifest.pricing[mode];
|
|
3498
|
+
if (!manifestValue) {
|
|
3499
|
+
throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode} is missing but ${mode} is served`);
|
|
3500
|
+
}
|
|
3501
|
+
if (!sameAmount(adapterName, `pricing.${mode}.rate`, optionValue.rate, manifestValue.rate)) {
|
|
3502
|
+
throw new RouteDockManifestError(
|
|
3503
|
+
`${adapterName}: pricing.${mode}.rate "${optionValue.rate}" does not match manifest.pricing.${mode}.rate "${manifestValue.rate}"`
|
|
3504
|
+
);
|
|
3505
|
+
}
|
|
3506
|
+
if (optionValue.channelFactory !== manifestValue.channel_factory) {
|
|
3507
|
+
throw new RouteDockManifestError(
|
|
3508
|
+
`${adapterName}: pricing.${mode}.channelFactory "${optionValue.channelFactory}" does not match manifest.pricing.${mode}.channel_factory "${manifestValue.channel_factory}"`
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
if (opts.assetContract !== void 0 && opts.assetContract !== manifest.asset_contract) {
|
|
3513
|
+
throw new RouteDockManifestError(
|
|
3514
|
+
`${adapterName}: assetContract "${opts.assetContract}" does not match manifest.asset_contract "${manifest.asset_contract}"`
|
|
3515
|
+
);
|
|
3516
|
+
}
|
|
3517
|
+
if (opts.asset !== void 0 && opts.asset !== manifest.asset) {
|
|
3518
|
+
throw new RouteDockManifestError(
|
|
3519
|
+
`${adapterName}: asset "${opts.asset}" does not match manifest.asset "${manifest.asset}"`
|
|
3520
|
+
);
|
|
3521
|
+
}
|
|
3522
|
+
if (opts.network !== manifest.network) {
|
|
3523
|
+
throw new RouteDockManifestError(
|
|
3524
|
+
`${adapterName}: network "${opts.network}" does not match manifest.network "${manifest.network}"`
|
|
3525
|
+
);
|
|
3526
|
+
}
|
|
3527
|
+
let signerPublicKey;
|
|
3528
|
+
try {
|
|
3529
|
+
signerPublicKey = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey).publicKey();
|
|
3530
|
+
} catch (err) {
|
|
3531
|
+
throw new RouteDockManifestError(`${adapterName}: payeeSecretKey is not a valid Stellar secret key`, { cause: err });
|
|
3532
|
+
}
|
|
3533
|
+
if (signerPublicKey !== manifest.payee) {
|
|
3534
|
+
throw new RouteDockManifestError(
|
|
3535
|
+
`${adapterName}: payeeSecretKey belongs to ${signerPublicKey}, not manifest.payee ${manifest.payee}; clients would reject the signed manifest`
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3538
|
+
if (opts.payee !== manifest.payee) {
|
|
3539
|
+
throw new RouteDockManifestError(
|
|
3540
|
+
`${adapterName}: payee "${opts.payee}" does not match manifest.payee "${manifest.payee}"`
|
|
3541
|
+
);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3004
3544
|
function extractPayerAddress(key) {
|
|
3005
3545
|
if (typeof key !== "string") return null;
|
|
3006
3546
|
return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
@@ -3009,6 +3549,18 @@ function extractPayerAddress(key) {
|
|
|
3009
3549
|
// src/provider/SeenTxStore.ts
|
|
3010
3550
|
var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
|
|
3011
3551
|
var MAX_SETTLEMENT_REPLAYS = 1;
|
|
3552
|
+
async function recordSettlement(store, key, record, ctx) {
|
|
3553
|
+
try {
|
|
3554
|
+
await store.set(key, record);
|
|
3555
|
+
} catch (err) {
|
|
3556
|
+
ctx.logger("error", `${ctx.label} seenTxStore.set failed after settlement`, { error: err });
|
|
3557
|
+
try {
|
|
3558
|
+
ctx.onCallbackError?.(err, "seenTxStore.set");
|
|
3559
|
+
} catch (cbErr) {
|
|
3560
|
+
ctx.logger("error", `${ctx.label} onCallbackError threw`, { error: cbErr });
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3012
3564
|
async function checkSettlementReplay(store, key, now = Date.now()) {
|
|
3013
3565
|
const record = await store.get(key);
|
|
3014
3566
|
if (!record) return { kind: "miss" };
|
|
@@ -3063,7 +3615,7 @@ var InMemorySeenTxStore = class {
|
|
|
3063
3615
|
return next;
|
|
3064
3616
|
}
|
|
3065
3617
|
};
|
|
3066
|
-
async function
|
|
3618
|
+
async function sha256Hex(input) {
|
|
3067
3619
|
const buf = new TextEncoder().encode(input);
|
|
3068
3620
|
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
3069
3621
|
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
@@ -3071,22 +3623,11 @@ async function sha256Hex2(input) {
|
|
|
3071
3623
|
async function paymentIdempotencyKey(getHeader, scope) {
|
|
3072
3624
|
const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
|
|
3073
3625
|
if (!material) return null;
|
|
3074
|
-
return
|
|
3626
|
+
return sha256Hex(
|
|
3075
3627
|
[scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
|
|
3076
3628
|
);
|
|
3077
3629
|
}
|
|
3078
3630
|
|
|
3079
|
-
// src/provider/settleResult.ts
|
|
3080
|
-
function readSettleResult(result) {
|
|
3081
|
-
const settle = result;
|
|
3082
|
-
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
3083
|
-
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
3084
|
-
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
3085
|
-
return failed(settle.errorReason);
|
|
3086
|
-
}
|
|
3087
|
-
return { ok: true, txHash: settle.transaction };
|
|
3088
|
-
}
|
|
3089
|
-
|
|
3090
3631
|
// src/provider/x402Handler.ts
|
|
3091
3632
|
var CAIP22 = {
|
|
3092
3633
|
testnet: "stellar:testnet",
|
|
@@ -3226,10 +3767,10 @@ function createX402Handler(opts) {
|
|
|
3226
3767
|
requirements
|
|
3227
3768
|
);
|
|
3228
3769
|
if (!verifyResult.isValid) {
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3770
|
+
await respondPaymentRequired(
|
|
3771
|
+
"Payment verification failed",
|
|
3772
|
+
verifyResult.invalidReason
|
|
3773
|
+
);
|
|
3233
3774
|
return;
|
|
3234
3775
|
}
|
|
3235
3776
|
const settleResult = await localFacilitator.settle(
|
|
@@ -3255,7 +3796,12 @@ function createX402Handler(opts) {
|
|
|
3255
3796
|
if (typeof paymentResponse === "string") {
|
|
3256
3797
|
headers["X-Payment-Response"] = paymentResponse;
|
|
3257
3798
|
}
|
|
3258
|
-
await
|
|
3799
|
+
await recordSettlement(
|
|
3800
|
+
seenTxStore,
|
|
3801
|
+
idempotencyKey,
|
|
3802
|
+
{ txHash, headers, createdAt: Date.now() },
|
|
3803
|
+
{ logger, label: "[x402]", onCallbackError: opts.onCallbackError }
|
|
3804
|
+
);
|
|
3259
3805
|
}
|
|
3260
3806
|
if (txHash && opts.onSettled) {
|
|
3261
3807
|
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
@@ -3285,21 +3831,21 @@ function createMppChargeHandler(opts) {
|
|
|
3285
3831
|
const logger = resolveLogger(opts.logger);
|
|
3286
3832
|
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
3287
3833
|
const mppxInstances = /* @__PURE__ */ new Map();
|
|
3288
|
-
function getMppx(
|
|
3289
|
-
let instance = mppxInstances.get(
|
|
3834
|
+
function getMppx(contract2) {
|
|
3835
|
+
let instance = mppxInstances.get(contract2);
|
|
3290
3836
|
if (!instance) {
|
|
3291
3837
|
instance = server$2.Mppx.create({
|
|
3292
3838
|
secretKey: opts.payeeSecretKey,
|
|
3293
3839
|
methods: [
|
|
3294
3840
|
server$4.stellar.charge({
|
|
3295
3841
|
recipient,
|
|
3296
|
-
currency:
|
|
3842
|
+
currency: contract2,
|
|
3297
3843
|
network: networkId,
|
|
3298
3844
|
feePayer: { envelopeSigner: opts.payeeSecretKey }
|
|
3299
3845
|
})
|
|
3300
3846
|
]
|
|
3301
3847
|
});
|
|
3302
|
-
mppxInstances.set(
|
|
3848
|
+
mppxInstances.set(contract2, instance);
|
|
3303
3849
|
}
|
|
3304
3850
|
return instance;
|
|
3305
3851
|
}
|
|
@@ -3320,7 +3866,7 @@ function createMppChargeHandler(opts) {
|
|
|
3320
3866
|
const credPart = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
|
|
3321
3867
|
if (credPart) {
|
|
3322
3868
|
const b64 = credPart.split("=").slice(1).join("=").replace(/^"|"$/g, "");
|
|
3323
|
-
const credJson =
|
|
3869
|
+
const credJson = base64ToUtf8(b64);
|
|
3324
3870
|
const cred = JSON.parse(credJson);
|
|
3325
3871
|
const key = cred.sender ?? cred.payload?.sender ?? cred.payload?.from;
|
|
3326
3872
|
payerAddress = extractPayerAddress(key);
|
|
@@ -3382,18 +3928,19 @@ function createMppChargeHandler(opts) {
|
|
|
3382
3928
|
if (receiptHeader) {
|
|
3383
3929
|
try {
|
|
3384
3930
|
const parsed = JSON.parse(
|
|
3385
|
-
|
|
3931
|
+
base64ToUtf8(receiptHeader)
|
|
3386
3932
|
);
|
|
3387
3933
|
reference = parsed.reference;
|
|
3388
3934
|
} catch {
|
|
3389
3935
|
}
|
|
3390
3936
|
}
|
|
3391
3937
|
if (idempotencyKey) {
|
|
3392
|
-
await
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
createdAt: Date.now()
|
|
3396
|
-
|
|
3938
|
+
await recordSettlement(
|
|
3939
|
+
seenTxStore,
|
|
3940
|
+
idempotencyKey,
|
|
3941
|
+
{ txHash: reference ?? null, headers: receiptHeaders, createdAt: Date.now() },
|
|
3942
|
+
{ logger, label: "[mpp-charge]", onCallbackError: opts.onCallbackError }
|
|
3943
|
+
);
|
|
3397
3944
|
}
|
|
3398
3945
|
if (reference && opts.onSettled) {
|
|
3399
3946
|
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
@@ -3407,6 +3954,14 @@ function createMppChargeHandler(opts) {
|
|
|
3407
3954
|
}
|
|
3408
3955
|
};
|
|
3409
3956
|
}
|
|
3957
|
+
|
|
3958
|
+
// src/provider/internal/vaultRpc.ts
|
|
3959
|
+
async function createSorobanRpcServer(rpcUrl) {
|
|
3960
|
+
const { Server } = await import('@stellar/stellar-sdk/rpc');
|
|
3961
|
+
return new Server(rpcUrl);
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
// src/provider/internal/vaultSettlement.ts
|
|
3410
3965
|
function resolveVaultSettlementAddresses(payer, payee) {
|
|
3411
3966
|
if (!payer) return null;
|
|
3412
3967
|
return { payer, payee };
|
|
@@ -3426,6 +3981,111 @@ function buildSessionSettlementTransaction(source, vaultContractId, channelId, a
|
|
|
3426
3981
|
networkPassphrase
|
|
3427
3982
|
}).addOperation(op).setTimeout(30).build();
|
|
3428
3983
|
}
|
|
3984
|
+
var DEFAULT_RPC_URLS = {
|
|
3985
|
+
testnet: "https://soroban-testnet.stellar.org",
|
|
3986
|
+
mainnet: "https://mainnet.sorobanrpc.com"
|
|
3987
|
+
};
|
|
3988
|
+
var CONFIRM_TIMEOUT_MS = 12e4;
|
|
3989
|
+
var CONFIRM_POLL_MS = 1e3;
|
|
3990
|
+
function delay(ms) {
|
|
3991
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3992
|
+
}
|
|
3993
|
+
function describeSendError(sent) {
|
|
3994
|
+
const errorResult = sent.errorResult;
|
|
3995
|
+
try {
|
|
3996
|
+
const name = errorResult?.result?.().switch?.().name;
|
|
3997
|
+
if (name) return `sendTransaction returned ERROR: ${name}`;
|
|
3998
|
+
} catch {
|
|
3999
|
+
}
|
|
4000
|
+
return `sendTransaction returned status ${sent.status}`;
|
|
4001
|
+
}
|
|
4002
|
+
async function waitForConfirmation(server, txHash) {
|
|
4003
|
+
const deadline = Date.now() + CONFIRM_TIMEOUT_MS;
|
|
4004
|
+
for (; ; ) {
|
|
4005
|
+
const tx = await server.getTransaction(txHash);
|
|
4006
|
+
if (tx.status === "SUCCESS") return "success";
|
|
4007
|
+
if (tx.status === "FAILED") return "failed";
|
|
4008
|
+
if (Date.now() >= deadline) return "timeout";
|
|
4009
|
+
await delay(CONFIRM_POLL_MS);
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
async function recordSessionSettlement(request, { logger, onCallbackError }) {
|
|
4013
|
+
const reportFailure = (fields) => {
|
|
4014
|
+
logger("error", "[vault] failed to record session_settled", {
|
|
4015
|
+
channelId: request.channelId,
|
|
4016
|
+
...fields.txHash ? { txHash: fields.txHash } : {},
|
|
4017
|
+
reason: fields.reason,
|
|
4018
|
+
...fields.error !== void 0 ? { error: fields.error } : {}
|
|
4019
|
+
});
|
|
4020
|
+
if (onCallbackError) {
|
|
4021
|
+
const err = fields.error ?? new Error(`session_settled vault record failed: ${fields.reason}`);
|
|
4022
|
+
onCallbackError(err, "vaultSettlement");
|
|
4023
|
+
}
|
|
4024
|
+
return { ok: false, reason: fields.reason, ...fields.txHash ? { txHash: fields.txHash } : {} };
|
|
4025
|
+
};
|
|
4026
|
+
try {
|
|
4027
|
+
const payeeKeypair = stellarSdk.Keypair.fromSecret(request.payeeSecretKey);
|
|
4028
|
+
const addresses = resolveVaultSettlementAddresses(request.payer, payeeKeypair.publicKey());
|
|
4029
|
+
if (!addresses) {
|
|
4030
|
+
logger("warn", "[vault] skipped session_settled record: payer address unavailable", {
|
|
4031
|
+
channelId: request.channelId
|
|
4032
|
+
});
|
|
4033
|
+
return { ok: false, reason: "payer address unavailable" };
|
|
4034
|
+
}
|
|
4035
|
+
const networkPassphrase = request.network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
|
|
4036
|
+
const rpcUrl = request.config.rpcUrl ?? DEFAULT_RPC_URLS[request.network];
|
|
4037
|
+
const server = await createSorobanRpcServer(rpcUrl);
|
|
4038
|
+
const sourceAccount = await server.getAccount(payeeKeypair.publicKey());
|
|
4039
|
+
const tx = buildSessionSettlementTransaction(
|
|
4040
|
+
sourceAccount,
|
|
4041
|
+
request.config.contract,
|
|
4042
|
+
request.channelId,
|
|
4043
|
+
addresses,
|
|
4044
|
+
request.amount,
|
|
4045
|
+
request.voucherCount,
|
|
4046
|
+
networkPassphrase
|
|
4047
|
+
);
|
|
4048
|
+
const preparedTx = await server.prepareTransaction(tx);
|
|
4049
|
+
preparedTx.sign(payeeKeypair);
|
|
4050
|
+
const sent = await server.sendTransaction(preparedTx);
|
|
4051
|
+
if (sent.status === "ERROR") {
|
|
4052
|
+
return reportFailure({
|
|
4053
|
+
txHash: sent.hash || preparedTx.hash().toString("hex"),
|
|
4054
|
+
reason: describeSendError(sent)
|
|
4055
|
+
});
|
|
4056
|
+
}
|
|
4057
|
+
const txHash = sent.hash || preparedTx.hash().toString("hex");
|
|
4058
|
+
const confirmation = await waitForConfirmation(server, txHash);
|
|
4059
|
+
if (confirmation === "success") {
|
|
4060
|
+
logger("debug", "[vault] recorded session_settled", {
|
|
4061
|
+
channelId: request.channelId,
|
|
4062
|
+
txHash
|
|
4063
|
+
});
|
|
4064
|
+
return { ok: true, txHash };
|
|
4065
|
+
}
|
|
4066
|
+
return reportFailure({
|
|
4067
|
+
txHash,
|
|
4068
|
+
reason: confirmation === "failed" ? "transaction failed on-chain" : `confirmation timed out after ${CONFIRM_TIMEOUT_MS}ms`
|
|
4069
|
+
});
|
|
4070
|
+
} catch (err) {
|
|
4071
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
4072
|
+
return reportFailure({ reason, error: err });
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
// src/internal/vaultConfig.ts
|
|
4077
|
+
function resolveVaultSettlementConfig(option, logger) {
|
|
4078
|
+
if (option) return option;
|
|
4079
|
+
if (typeof process === "undefined") return void 0;
|
|
4080
|
+
const fromEnv = process.env.AGENT_VAULT_CONTRACT ?? process.env.AGENT_VAULT_CONTRACT_ID;
|
|
4081
|
+
if (!fromEnv) return void 0;
|
|
4082
|
+
logger(
|
|
4083
|
+
"warn",
|
|
4084
|
+
"[vault] configuring the vault contract via the AGENT_VAULT_CONTRACT/AGENT_VAULT_CONTRACT_ID environment variables is deprecated and will be removed in the next minor release \u2014 pass the vaultSettlement option instead",
|
|
4085
|
+
{}
|
|
4086
|
+
);
|
|
4087
|
+
return { contract: fromEnv };
|
|
4088
|
+
}
|
|
3429
4089
|
function channelAuthorizer(envelopeSigner) {
|
|
3430
4090
|
return { envelopeSigner };
|
|
3431
4091
|
}
|
|
@@ -3504,6 +4164,7 @@ function createMppSessionHandler(opts) {
|
|
|
3504
4164
|
let record = null;
|
|
3505
4165
|
let voucherCount = 0;
|
|
3506
4166
|
let sessionOpened = false;
|
|
4167
|
+
let orphanFlagged = false;
|
|
3507
4168
|
let idleTimer = null;
|
|
3508
4169
|
let settledCleanly = false;
|
|
3509
4170
|
let closeListenerArmed = false;
|
|
@@ -3529,6 +4190,14 @@ function createMppSessionHandler(opts) {
|
|
|
3529
4190
|
return null;
|
|
3530
4191
|
}
|
|
3531
4192
|
record = { amount: BigInt(stored.amount), signature: stored.signature, payer: stored.payer ?? null };
|
|
4193
|
+
if (typeof stored.voucherCount === "number") {
|
|
4194
|
+
voucherCount = stored.voucherCount;
|
|
4195
|
+
}
|
|
4196
|
+
if (typeof stored.sessionOpened === "boolean") {
|
|
4197
|
+
sessionOpened = stored.sessionOpened;
|
|
4198
|
+
} else if (record !== null) {
|
|
4199
|
+
sessionOpened = true;
|
|
4200
|
+
}
|
|
3532
4201
|
} catch {
|
|
3533
4202
|
return null;
|
|
3534
4203
|
}
|
|
@@ -3536,8 +4205,8 @@ function createMppSessionHandler(opts) {
|
|
|
3536
4205
|
}
|
|
3537
4206
|
async function flagOrphan(reason) {
|
|
3538
4207
|
await loadPersistedRecord();
|
|
3539
|
-
if (!sessionOpened || settledCleanly) return;
|
|
3540
|
-
|
|
4208
|
+
if (!sessionOpened || settledCleanly || orphanFlagged) return;
|
|
4209
|
+
orphanFlagged = true;
|
|
3541
4210
|
closeListenerArmed = false;
|
|
3542
4211
|
clearIdleTimer();
|
|
3543
4212
|
const cumulativeAmount = record ? (Number(record.amount) / 1e7).toFixed(7) : "0.0000000";
|
|
@@ -3585,16 +4254,20 @@ function createMppSessionHandler(opts) {
|
|
|
3585
4254
|
if (record && amount < record.amount) return;
|
|
3586
4255
|
const payer = record?.payer ?? extractPayerAddress(credential.source);
|
|
3587
4256
|
record = { amount, signature: payload.signature, payer };
|
|
4257
|
+
voucherCount++;
|
|
4258
|
+
settledCleanly = false;
|
|
4259
|
+
orphanFlagged = false;
|
|
4260
|
+
armIdleTimer();
|
|
4261
|
+
const wasOpened = sessionOpened;
|
|
4262
|
+
sessionOpened = true;
|
|
3588
4263
|
await innerStore.put(voucherRecordKey, {
|
|
3589
4264
|
amount: record.amount.toString(),
|
|
3590
4265
|
signature: record.signature,
|
|
3591
|
-
payer: record.payer
|
|
4266
|
+
payer: record.payer,
|
|
4267
|
+
voucherCount,
|
|
4268
|
+
sessionOpened: true
|
|
3592
4269
|
});
|
|
3593
|
-
|
|
3594
|
-
settledCleanly = false;
|
|
3595
|
-
armIdleTimer();
|
|
3596
|
-
if (!sessionOpened) {
|
|
3597
|
-
sessionOpened = true;
|
|
4270
|
+
if (!wasOpened) {
|
|
3598
4271
|
if (opts.onSessionOpen) {
|
|
3599
4272
|
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
3600
4273
|
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
@@ -3646,10 +4319,11 @@ function createMppSessionHandler(opts) {
|
|
|
3646
4319
|
}
|
|
3647
4320
|
if (closeAmount > 0n && closeSig) {
|
|
3648
4321
|
const closePayer = record?.payer ?? null;
|
|
4322
|
+
const vouchersClosed = voucherCount;
|
|
3649
4323
|
const closeTxHash = await server$3.close({
|
|
3650
4324
|
channel: opts.channelFactory,
|
|
3651
4325
|
amount: closeAmount,
|
|
3652
|
-
signature:
|
|
4326
|
+
signature: hexToBytes(closeSig),
|
|
3653
4327
|
feePayer: channelAuthorizer(payeeKeypair),
|
|
3654
4328
|
network: networkId
|
|
3655
4329
|
});
|
|
@@ -3662,46 +4336,28 @@ function createMppSessionHandler(opts) {
|
|
|
3662
4336
|
opts.onCallbackError?.(err, "onSettled");
|
|
3663
4337
|
});
|
|
3664
4338
|
}
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
4339
|
+
res.json({ closeTxHash });
|
|
4340
|
+
const vaultConfig = resolveVaultSettlementConfig(opts.vaultSettlement, logger);
|
|
4341
|
+
if (vaultConfig) {
|
|
4342
|
+
await recordSessionSettlement(
|
|
4343
|
+
{
|
|
4344
|
+
config: vaultConfig,
|
|
4345
|
+
network: opts.network,
|
|
4346
|
+
payeeSecretKey: opts.payeeSecretKey,
|
|
4347
|
+
channelId: opts.channelFactory,
|
|
4348
|
+
payer: closePayer,
|
|
4349
|
+
amount: closeAmount,
|
|
4350
|
+
voucherCount: vouchersClosed
|
|
4351
|
+
},
|
|
4352
|
+
{ logger, onCallbackError: opts.onCallbackError }
|
|
3670
4353
|
);
|
|
3671
|
-
if (!settlementAddresses) {
|
|
3672
|
-
logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
3673
|
-
} else {
|
|
3674
|
-
try {
|
|
3675
|
-
const { Networks: Networks2 } = await import('@stellar/stellar-sdk');
|
|
3676
|
-
const { Server } = await import('@stellar/stellar-sdk/rpc');
|
|
3677
|
-
const networkPassphrase = opts.network === "mainnet" ? Networks2.PUBLIC : Networks2.TESTNET;
|
|
3678
|
-
const rpcUrl = opts.network === "mainnet" ? "https://mainnet.sorobanrpc.com" : "https://soroban-testnet.stellar.org";
|
|
3679
|
-
const server = new Server(rpcUrl);
|
|
3680
|
-
const payeePublicKey = payeeKeypair.publicKey();
|
|
3681
|
-
const sourceAccount = await server.getAccount(payeePublicKey);
|
|
3682
|
-
const tx = buildSessionSettlementTransaction(
|
|
3683
|
-
sourceAccount,
|
|
3684
|
-
vaultContract,
|
|
3685
|
-
opts.channelFactory,
|
|
3686
|
-
settlementAddresses,
|
|
3687
|
-
closeAmount,
|
|
3688
|
-
voucherCount,
|
|
3689
|
-
networkPassphrase
|
|
3690
|
-
);
|
|
3691
|
-
const preparedTx = await server.prepareTransaction(tx);
|
|
3692
|
-
preparedTx.sign(payeeKeypair);
|
|
3693
|
-
await server.sendTransaction(preparedTx);
|
|
3694
|
-
} catch (recordErr) {
|
|
3695
|
-
logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
|
|
3696
|
-
}
|
|
3697
|
-
}
|
|
3698
4354
|
}
|
|
3699
|
-
res.json({ closeTxHash });
|
|
3700
4355
|
} else {
|
|
3701
4356
|
res.json({ closeTxHash: null, message: "no vouchers received" });
|
|
3702
4357
|
}
|
|
3703
4358
|
sessionOpened = false;
|
|
3704
4359
|
voucherCount = 0;
|
|
4360
|
+
orphanFlagged = false;
|
|
3705
4361
|
record = null;
|
|
3706
4362
|
await innerStore.delete(voucherRecordKey);
|
|
3707
4363
|
closeListenerArmed = false;
|
|
@@ -3738,6 +4394,7 @@ function createMppSessionHandler(opts) {
|
|
|
3738
4394
|
var MPP_MODES = ["mpp-session", "mpp-charge"];
|
|
3739
4395
|
function routedock(opts) {
|
|
3740
4396
|
const startTime = Date.now();
|
|
4397
|
+
assertProviderConfig(opts, "routedock");
|
|
3741
4398
|
const signedManifest = signManifest(opts.manifest, opts.payeeSecretKey);
|
|
3742
4399
|
const handlerMap = /* @__PURE__ */ new Map();
|
|
3743
4400
|
if (opts.modes.includes("x402")) {
|
|
@@ -3801,7 +4458,8 @@ function routedock(opts) {
|
|
|
3801
4458
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3802
4459
|
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
3803
4460
|
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
3804
|
-
...opts.logger ? { logger: opts.logger } : {}
|
|
4461
|
+
...opts.logger ? { logger: opts.logger } : {},
|
|
4462
|
+
...opts.vaultSettlement ? { vaultSettlement: opts.vaultSettlement } : {}
|
|
3805
4463
|
})
|
|
3806
4464
|
);
|
|
3807
4465
|
}
|
|
@@ -3891,6 +4549,8 @@ var MPP_NETWORK3 = {
|
|
|
3891
4549
|
testnet: "stellar:testnet",
|
|
3892
4550
|
mainnet: "stellar:pubnet"
|
|
3893
4551
|
};
|
|
4552
|
+
var DEFAULT_MAX_RECONCILE_ATTEMPTS = 5;
|
|
4553
|
+
var ALREADY_CLOSED_PATTERN = /channel has been closed|channel is closed|already closed|finalized/i;
|
|
3894
4554
|
function decimalToStroops(amountStr) {
|
|
3895
4555
|
return nulthSdk.usdcToStroops(amountStr);
|
|
3896
4556
|
}
|
|
@@ -3900,12 +4560,15 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
3900
4560
|
recoveredCount: 0,
|
|
3901
4561
|
skippedCount: 0,
|
|
3902
4562
|
failedCount: 0,
|
|
4563
|
+
alreadyClosedCount: 0,
|
|
4564
|
+
terminalCount: 0,
|
|
3903
4565
|
errors: []
|
|
3904
4566
|
};
|
|
4567
|
+
const maxAttempts = opts.maxReconcileAttempts ?? DEFAULT_MAX_RECONCILE_ATTEMPTS;
|
|
3905
4568
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
3906
4569
|
const networkId = MPP_NETWORK3[opts.network];
|
|
3907
4570
|
try {
|
|
3908
|
-
const { data: closingSessions, error } = await opts.supabase.from("sessions").select("channel_id, channel_contract, cumulative_amount, last_signature, settlement_tx_hash").eq("status", "closing").eq("network", opts.network).eq("payee", payeeKeypair.publicKey()).is("settlement_tx_hash", null).not("last_signature", "is", null).order("updated_at", { ascending: true }).limit(100);
|
|
4571
|
+
const { data: closingSessions, error } = await opts.supabase.from("sessions").select("channel_id, channel_contract, cumulative_amount, last_signature, settlement_tx_hash, reconcile_attempts").eq("status", "closing").eq("network", opts.network).eq("payee", payeeKeypair.publicKey()).is("settlement_tx_hash", null).not("last_signature", "is", null).order("updated_at", { ascending: true }).limit(100);
|
|
3909
4572
|
if (error) {
|
|
3910
4573
|
throw new Error(`Failed to query sessions: ${error.message}`);
|
|
3911
4574
|
}
|
|
@@ -3927,7 +4590,7 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
3927
4590
|
continue;
|
|
3928
4591
|
}
|
|
3929
4592
|
const closeAmount = decimalToStroops(cumulative_amount);
|
|
3930
|
-
const closeSig =
|
|
4593
|
+
const closeSig = hexToBytes(last_signature);
|
|
3931
4594
|
const closeFn = opts.channelClose ?? (await import('@stellar/mpp/channel/server')).close;
|
|
3932
4595
|
const closeTxHash = await closeFn({
|
|
3933
4596
|
channel: channelId,
|
|
@@ -3950,11 +4613,36 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
3950
4613
|
await opts.onRecovered(channelId, closeTxHash, totalPaid);
|
|
3951
4614
|
}
|
|
3952
4615
|
} catch (err) {
|
|
4616
|
+
const reason = formatMppError(err);
|
|
4617
|
+
if (ALREADY_CLOSED_PATTERN.test(reason)) {
|
|
4618
|
+
const { error: closeError } = await opts.supabase.from("sessions").update({
|
|
4619
|
+
status: "closed",
|
|
4620
|
+
last_reconcile_error: `channel already closed on-chain: ${reason}`,
|
|
4621
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4622
|
+
}).eq("channel_id", session.channel_id);
|
|
4623
|
+
if (!closeError) {
|
|
4624
|
+
stats.alreadyClosedCount++;
|
|
4625
|
+
continue;
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
3953
4628
|
stats.failedCount++;
|
|
3954
|
-
stats.errors.push({
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
})
|
|
4629
|
+
stats.errors.push({ channelId: session.channel_id, reason });
|
|
4630
|
+
const attempts = (Number(session.reconcile_attempts) || 0) + 1;
|
|
4631
|
+
const terminal = attempts >= maxAttempts;
|
|
4632
|
+
const { error: bookkeepingError } = await opts.supabase.from("sessions").update({
|
|
4633
|
+
reconcile_attempts: attempts,
|
|
4634
|
+
last_reconcile_error: reason,
|
|
4635
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4636
|
+
...terminal ? { status: "failed" } : {}
|
|
4637
|
+
}).eq("channel_id", session.channel_id);
|
|
4638
|
+
if (bookkeepingError) {
|
|
4639
|
+
stats.errors.push({
|
|
4640
|
+
channelId: session.channel_id,
|
|
4641
|
+
reason: `Failed to record reconcile failure: ${bookkeepingError.message}`
|
|
4642
|
+
});
|
|
4643
|
+
} else if (terminal) {
|
|
4644
|
+
stats.terminalCount++;
|
|
4645
|
+
}
|
|
3958
4646
|
}
|
|
3959
4647
|
}
|
|
3960
4648
|
return stats;
|
|
@@ -3975,7 +4663,7 @@ async function runStartupReconciliation(opts, logger) {
|
|
|
3975
4663
|
}
|
|
3976
4664
|
log(
|
|
3977
4665
|
"info",
|
|
3978
|
-
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
|
|
4666
|
+
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}, Already closed: ${stats.alreadyClosedCount}, Moved to failed: ${stats.terminalCount}`
|
|
3979
4667
|
);
|
|
3980
4668
|
if (stats.errors.length > 0) {
|
|
3981
4669
|
log("warn", "[SessionReconciler] Reconciliation errors:");
|
|
@@ -3992,7 +4680,7 @@ function tryDecodeBase64(value) {
|
|
|
3992
4680
|
return value;
|
|
3993
4681
|
}
|
|
3994
4682
|
try {
|
|
3995
|
-
const decoded =
|
|
4683
|
+
const decoded = base64ToUtf8(value);
|
|
3996
4684
|
if (decoded.length > 0 && decoded !== value) return decoded;
|
|
3997
4685
|
} catch {
|
|
3998
4686
|
}
|
|
@@ -4129,10 +4817,30 @@ var ProviderRegistry = class {
|
|
|
4129
4817
|
}
|
|
4130
4818
|
};
|
|
4131
4819
|
|
|
4820
|
+
Object.defineProperty(exports, "NulthPolicyError", {
|
|
4821
|
+
enumerable: true,
|
|
4822
|
+
get: function () { return nulthSdk.NulthPolicyError; }
|
|
4823
|
+
});
|
|
4132
4824
|
Object.defineProperty(exports, "USDC_DECIMALS", {
|
|
4133
4825
|
enumerable: true,
|
|
4134
4826
|
get: function () { return nulthSdk.USDC_DECIMALS; }
|
|
4135
4827
|
});
|
|
4828
|
+
Object.defineProperty(exports, "createPolicyState", {
|
|
4829
|
+
enumerable: true,
|
|
4830
|
+
get: function () { return nulthSdk.createPolicyState; }
|
|
4831
|
+
});
|
|
4832
|
+
Object.defineProperty(exports, "decodeAuthSignature", {
|
|
4833
|
+
enumerable: true,
|
|
4834
|
+
get: function () { return nulthSdk.decodeAuthSignature; }
|
|
4835
|
+
});
|
|
4836
|
+
Object.defineProperty(exports, "paymentContextFromManifest", {
|
|
4837
|
+
enumerable: true,
|
|
4838
|
+
get: function () { return nulthSdk.paymentContextFromManifest; }
|
|
4839
|
+
});
|
|
4840
|
+
Object.defineProperty(exports, "setNulthPaymentContext", {
|
|
4841
|
+
enumerable: true,
|
|
4842
|
+
get: function () { return nulthSdk.setNulthPaymentContext; }
|
|
4843
|
+
});
|
|
4136
4844
|
Object.defineProperty(exports, "usdcToStroops", {
|
|
4137
4845
|
enumerable: true,
|
|
4138
4846
|
get: function () { return nulthSdk.usdcToStroops; }
|
|
@@ -4142,7 +4850,6 @@ exports.DEFAULT_RETRY_POLICY = DEFAULT_RETRY_POLICY;
|
|
|
4142
4850
|
exports.FileSpendStore = FileSpendStore;
|
|
4143
4851
|
exports.InMemorySpendStore = InMemorySpendStore;
|
|
4144
4852
|
exports.MppChargeClient = MppChargeClient;
|
|
4145
|
-
exports.NulthPolicyError = NulthPolicyError;
|
|
4146
4853
|
exports.OnChainRegistry = OnChainRegistry;
|
|
4147
4854
|
exports.ProviderRegistry = ProviderRegistry;
|
|
4148
4855
|
exports.RouteDockChannelStateError = RouteDockChannelStateError;
|
|
@@ -4156,6 +4863,7 @@ exports.RouteDockManifestSunsetError = RouteDockManifestSunsetError;
|
|
|
4156
4863
|
exports.RouteDockManifestTimeoutError = RouteDockManifestTimeoutError;
|
|
4157
4864
|
exports.RouteDockNetworkError = RouteDockNetworkError;
|
|
4158
4865
|
exports.RouteDockNoSupportedModeError = RouteDockNoSupportedModeError;
|
|
4866
|
+
exports.RouteDockPaymentRejectedError = RouteDockPaymentRejectedError;
|
|
4159
4867
|
exports.RouteDockPolicyRejectError = RouteDockPolicyRejectError;
|
|
4160
4868
|
exports.RouteDockPolicyRejectedError = RouteDockPolicyRejectedError;
|
|
4161
4869
|
exports.RouteDockRefundWindowError = RouteDockRefundWindowError;
|
|
@@ -4174,9 +4882,7 @@ exports.configureManifestCache = configureManifestCache;
|
|
|
4174
4882
|
exports.consoleLogger = consoleLogger;
|
|
4175
4883
|
exports.createConsoleLogger = createConsoleLogger;
|
|
4176
4884
|
exports.createNulthSigner = createNulthSigner;
|
|
4177
|
-
exports.createPolicyState = createPolicyState;
|
|
4178
4885
|
exports.decimalToStroops = decimalToStroops;
|
|
4179
|
-
exports.decodeAuthSignature = decodeAuthSignature;
|
|
4180
4886
|
exports.fetchLedgerSequence = fetchLedgerSequence;
|
|
4181
4887
|
exports.fetchManifest = fetchManifest;
|
|
4182
4888
|
exports.getEligibleAssets = getEligibleAssets;
|
|
@@ -4187,7 +4893,7 @@ exports.manifestDigest = manifestDigest;
|
|
|
4187
4893
|
exports.noopLogger = noopLogger;
|
|
4188
4894
|
exports.normalizeManifestAssets = normalizeManifestAssets;
|
|
4189
4895
|
exports.parseRetryAfterMs = parseRetryAfterMs;
|
|
4190
|
-
exports.
|
|
4896
|
+
exports.paymentRejectedError = paymentRejectedError;
|
|
4191
4897
|
exports.prepareNulthSigner = prepareNulthSigner;
|
|
4192
4898
|
exports.rankProvidersByLatency = rankProvidersByLatency;
|
|
4193
4899
|
exports.reconcileAbandonedSessions = reconcileAbandonedSessions;
|
|
@@ -4198,7 +4904,6 @@ exports.routedock = routedock;
|
|
|
4198
4904
|
exports.runStartupReconciliation = runStartupReconciliation;
|
|
4199
4905
|
exports.selectAsset = selectAsset;
|
|
4200
4906
|
exports.selectMode = selectMode;
|
|
4201
|
-
exports.setNulthPaymentContext = setNulthPaymentContext;
|
|
4202
4907
|
exports.signManifest = signManifest;
|
|
4203
4908
|
exports.usdcToMicros = usdcToMicros;
|
|
4204
4909
|
exports.usdcToUnits = usdcToUnits;
|