@routedock/routedock 0.3.1 → 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-w4_hkI_1.d.ts → ProviderRegistry-D9KqyuH-.d.cts} +16 -36
- package/dist/{ProviderRegistry-CyZ5eryO.d.cts → ProviderRegistry-DVi2uzgk.d.ts} +16 -36
- package/dist/{RouteDockClient-CjM2VyOd.d.ts → RouteDockClient-6rmgkbYb.d.cts} +124 -144
- package/dist/{RouteDockClient-CXvWXix5.d.cts → RouteDockClient-CnAkxwrW.d.ts} +124 -144
- 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 -482
- package/dist/client.d.cts +80 -10
- package/dist/client.d.ts +80 -10
- package/dist/client.js +733 -463
- package/dist/index.cjs +1295 -585
- package/dist/index.d.cts +13 -10
- package/dist/index.d.ts +13 -10
- package/dist/index.js +1247 -554
- 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 -431
- package/dist/react.d.cts +7 -5
- package/dist/react.d.ts +7 -5
- package/dist/react.js +725 -420
- 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/react.cjs
CHANGED
|
@@ -3,20 +3,17 @@
|
|
|
3
3
|
var react = require('react');
|
|
4
4
|
var jsxRuntime = require('react/jsx-runtime');
|
|
5
5
|
var stellarSdk = require('@stellar/stellar-sdk');
|
|
6
|
-
var jsonSchema = require('@cfworker/json-schema');
|
|
7
6
|
var nulthSdk = require('@routedock/nulth-sdk');
|
|
8
|
-
var
|
|
7
|
+
var sha2_js = require('@noble/hashes/sha2.js');
|
|
8
|
+
var jsonSchema = require('@cfworker/json-schema');
|
|
9
9
|
var stellar = require('@x402/stellar');
|
|
10
|
-
var client = require('@x402/
|
|
11
|
-
var client$1 = require('@x402/core/client');
|
|
10
|
+
var client = require('@x402/core/client');
|
|
12
11
|
var http = require('@x402/core/http');
|
|
13
|
-
var client$
|
|
14
|
-
var client$
|
|
15
|
-
var mpp = require('@stellar/mpp');
|
|
16
|
-
var client$4 = require('@stellar/mpp/channel/client');
|
|
12
|
+
var client$2 = require('@stellar/mpp/charge/client');
|
|
13
|
+
var client$1 = require('mppx/client');
|
|
17
14
|
var mppx = require('mppx');
|
|
18
|
-
require('
|
|
19
|
-
require('
|
|
15
|
+
var mpp = require('@stellar/mpp');
|
|
16
|
+
var client$3 = require('@stellar/mpp/channel/client');
|
|
20
17
|
|
|
21
18
|
// src/react/context.tsx
|
|
22
19
|
var RouteDockContext = react.createContext(null);
|
|
@@ -123,6 +120,20 @@ var RouteDockPolicyRejectError = class extends RouteDockError {
|
|
|
123
120
|
this.reason = reason;
|
|
124
121
|
}
|
|
125
122
|
};
|
|
123
|
+
var RouteDockPaymentRejectedError = class extends RouteDockError {
|
|
124
|
+
status;
|
|
125
|
+
/** The provider's `reason` field, if its response body was JSON and had one. */
|
|
126
|
+
reason;
|
|
127
|
+
/** The provider's `error` field (short summary), if present. */
|
|
128
|
+
providerError;
|
|
129
|
+
constructor(message, status, details = {}, options = {}) {
|
|
130
|
+
super(message, "PAYMENT_REJECTED", false, options);
|
|
131
|
+
this.name = "RouteDockPaymentRejectedError";
|
|
132
|
+
this.status = status;
|
|
133
|
+
if (details.reason !== void 0) this.reason = details.reason;
|
|
134
|
+
if (details.providerError !== void 0) this.providerError = details.providerError;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
126
137
|
var RouteDockChannelStateError = class extends RouteDockError {
|
|
127
138
|
constructor(message, options = {}) {
|
|
128
139
|
super(message, "CHANNEL_STATE", false, options);
|
|
@@ -159,6 +170,59 @@ function httpStatusToError(message, status, response) {
|
|
|
159
170
|
}
|
|
160
171
|
return new RouteDockNetworkError(message, opts);
|
|
161
172
|
}
|
|
173
|
+
var MAX_REJECTION_BODY_BYTES = 64 * 1024;
|
|
174
|
+
var MAX_REJECTION_FIELD_CHARS = 500;
|
|
175
|
+
async function readBoundedText(response, limit) {
|
|
176
|
+
const body = response.body;
|
|
177
|
+
if (!body) return null;
|
|
178
|
+
const reader = body.getReader();
|
|
179
|
+
const chunks = [];
|
|
180
|
+
let total = 0;
|
|
181
|
+
try {
|
|
182
|
+
for (; ; ) {
|
|
183
|
+
const { done, value } = await reader.read();
|
|
184
|
+
if (done) break;
|
|
185
|
+
total += value.byteLength;
|
|
186
|
+
if (total > limit) {
|
|
187
|
+
await reader.cancel().catch(() => void 0);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
chunks.push(value);
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const bytes = new Uint8Array(total);
|
|
196
|
+
let offset = 0;
|
|
197
|
+
for (const chunk of chunks) {
|
|
198
|
+
bytes.set(chunk, offset);
|
|
199
|
+
offset += chunk.byteLength;
|
|
200
|
+
}
|
|
201
|
+
return new TextDecoder().decode(bytes);
|
|
202
|
+
}
|
|
203
|
+
async function paymentRejectedError(label, status, response) {
|
|
204
|
+
let reason;
|
|
205
|
+
let providerError;
|
|
206
|
+
try {
|
|
207
|
+
const text = await readBoundedText(response, MAX_REJECTION_BODY_BYTES);
|
|
208
|
+
const body = text === null ? null : JSON.parse(text);
|
|
209
|
+
if (body !== null && typeof body === "object") {
|
|
210
|
+
const { reason: r, error: e } = body;
|
|
211
|
+
if (typeof r === "string" && r.length > 0) reason = r.slice(0, MAX_REJECTION_FIELD_CHARS);
|
|
212
|
+
if (typeof e === "string" && e.length > 0) providerError = e.slice(0, MAX_REJECTION_FIELD_CHARS);
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
const detail = reason ?? providerError;
|
|
217
|
+
return new RouteDockPaymentRejectedError(
|
|
218
|
+
`${label}: HTTP ${status}${detail ? ` \u2014 ${detail}` : ""}`,
|
|
219
|
+
status,
|
|
220
|
+
{
|
|
221
|
+
...reason !== void 0 ? { reason } : {},
|
|
222
|
+
...providerError !== void 0 ? { providerError } : {}
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
}
|
|
162
226
|
function wrapFetchError(err, context) {
|
|
163
227
|
if (err instanceof RouteDockError) return err;
|
|
164
228
|
return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
|
|
@@ -220,7 +284,10 @@ async function withRetry(fn, policy = {}) {
|
|
|
220
284
|
if (!err.retryable || attempt >= maxAttempts - 1) {
|
|
221
285
|
throw err;
|
|
222
286
|
}
|
|
223
|
-
const delay =
|
|
287
|
+
const delay = Math.min(
|
|
288
|
+
err.retryAfterMs ?? backoffDelayMs(attempt, baseDelayMs, maxDelayMs),
|
|
289
|
+
maxDelayMs
|
|
290
|
+
);
|
|
224
291
|
if (onRetry) {
|
|
225
292
|
try {
|
|
226
293
|
await onRetry(attempt, err, delay);
|
|
@@ -233,10 +300,116 @@ async function withRetry(fn, policy = {}) {
|
|
|
233
300
|
}
|
|
234
301
|
throw lastError ?? new RouteDockError("Retry exhausted", "NETWORK", false);
|
|
235
302
|
}
|
|
303
|
+
|
|
304
|
+
// src/network.ts
|
|
236
305
|
var USDC_ISSUERS = {
|
|
237
306
|
testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
|
|
238
307
|
mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
|
|
239
308
|
};
|
|
309
|
+
var HORIZON_URLS = {
|
|
310
|
+
testnet: "https://horizon-testnet.stellar.org",
|
|
311
|
+
mainnet: "https://horizon.stellar.org"
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// package.json
|
|
315
|
+
var package_default = {
|
|
316
|
+
version: "0.4.0"};
|
|
317
|
+
|
|
318
|
+
// src/internal/encoding.ts
|
|
319
|
+
var encoder = new TextEncoder();
|
|
320
|
+
var decoder = new TextDecoder();
|
|
321
|
+
function utf8ToBytes(text) {
|
|
322
|
+
return encoder.encode(text);
|
|
323
|
+
}
|
|
324
|
+
function bytesToUtf8(bytes) {
|
|
325
|
+
return decoder.decode(bytes);
|
|
326
|
+
}
|
|
327
|
+
function base64ToBytes(b64) {
|
|
328
|
+
const end = b64.indexOf("=");
|
|
329
|
+
let clean = (end === -1 ? b64 : b64.slice(0, end)).replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
|
|
330
|
+
if (clean.length % 4 === 1) clean = clean.slice(0, -1);
|
|
331
|
+
const binary = atob(clean);
|
|
332
|
+
const bytes = new Uint8Array(binary.length);
|
|
333
|
+
for (let i = 0; i < binary.length; i++) {
|
|
334
|
+
bytes[i] = binary.charCodeAt(i);
|
|
335
|
+
}
|
|
336
|
+
return bytes;
|
|
337
|
+
}
|
|
338
|
+
function bytesToHex(bytes) {
|
|
339
|
+
let out = "";
|
|
340
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
341
|
+
out += bytes[i].toString(16).padStart(2, "0");
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
function sha256(data) {
|
|
346
|
+
return sha2_js.sha256(typeof data === "string" ? utf8ToBytes(data) : data);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/manifest/sign.ts
|
|
350
|
+
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
351
|
+
function canonicalize(value) {
|
|
352
|
+
if (Array.isArray(value)) {
|
|
353
|
+
return value.map(canonicalize);
|
|
354
|
+
}
|
|
355
|
+
if (value !== null && typeof value === "object") {
|
|
356
|
+
return Object.fromEntries(
|
|
357
|
+
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
function manifestDigest(manifest) {
|
|
363
|
+
const { signature: _omit, ...rest } = manifest;
|
|
364
|
+
const canonical = JSON.stringify(canonicalize(rest));
|
|
365
|
+
return sha256(utf8ToBytes(canonical));
|
|
366
|
+
}
|
|
367
|
+
function verifyManifestSignature(manifest, expectedPayee) {
|
|
368
|
+
if (!manifest.signature) {
|
|
369
|
+
throw new RouteDockSignatureError(
|
|
370
|
+
"Manifest is missing a signature field \u2014 cannot verify payee authenticity"
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
if (manifest.signature_version !== MANIFEST_SIGNATURE_VERSION) {
|
|
374
|
+
throw new RouteDockSignatureError(
|
|
375
|
+
`Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (expectedPayee !== void 0 && manifest.payee !== expectedPayee) {
|
|
379
|
+
throw new RouteDockSignatureError(
|
|
380
|
+
`Manifest payee ${manifest.payee} does not match expected payee ${expectedPayee}`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
let keypair;
|
|
384
|
+
try {
|
|
385
|
+
keypair = stellarSdk.Keypair.fromPublicKey(manifest.payee);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
throw new RouteDockSignatureError(
|
|
388
|
+
`Manifest payee ${manifest.payee} is not a valid Stellar public key`,
|
|
389
|
+
{ cause: err }
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
const digest = manifestDigest(manifest);
|
|
393
|
+
let sigBytes;
|
|
394
|
+
try {
|
|
395
|
+
sigBytes = base64ToBytes(manifest.signature);
|
|
396
|
+
} catch {
|
|
397
|
+
throw new RouteDockSignatureError("Manifest signature is not valid base64");
|
|
398
|
+
}
|
|
399
|
+
let valid = false;
|
|
400
|
+
try {
|
|
401
|
+
valid = keypair.verify(digest, sigBytes);
|
|
402
|
+
} catch {
|
|
403
|
+
throw new RouteDockSignatureError(
|
|
404
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (!valid) {
|
|
408
|
+
throw new RouteDockSignatureError(
|
|
409
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
240
413
|
|
|
241
414
|
// src/schemas/routedock.schema.json
|
|
242
415
|
var routedock_schema_default = {
|
|
@@ -315,6 +488,7 @@ var routedock_schema_default = {
|
|
|
315
488
|
},
|
|
316
489
|
asset_contract: {
|
|
317
490
|
type: "string",
|
|
491
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
318
492
|
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
319
493
|
},
|
|
320
494
|
assets: {
|
|
@@ -327,6 +501,7 @@ var routedock_schema_default = {
|
|
|
327
501
|
},
|
|
328
502
|
payee: {
|
|
329
503
|
type: "string",
|
|
504
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
330
505
|
description: "Stellar address (G...) that receives payments"
|
|
331
506
|
},
|
|
332
507
|
pricing: {
|
|
@@ -497,6 +672,7 @@ var routedock_schema_default = {
|
|
|
497
672
|
properties: {
|
|
498
673
|
amount: {
|
|
499
674
|
type: "string",
|
|
675
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
500
676
|
description: "Cost per request in the payment asset (decimal string, e.g. '0.001')"
|
|
501
677
|
},
|
|
502
678
|
per: {
|
|
@@ -510,6 +686,7 @@ var routedock_schema_default = {
|
|
|
510
686
|
},
|
|
511
687
|
payee: {
|
|
512
688
|
type: "string",
|
|
689
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
513
690
|
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."
|
|
514
691
|
}
|
|
515
692
|
}
|
|
@@ -522,6 +699,7 @@ var routedock_schema_default = {
|
|
|
522
699
|
properties: {
|
|
523
700
|
rate: {
|
|
524
701
|
type: "string",
|
|
702
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
525
703
|
description: "Cost per voucher in the payment asset (decimal string, e.g. '0.0001')"
|
|
526
704
|
},
|
|
527
705
|
per: {
|
|
@@ -531,10 +709,12 @@ var routedock_schema_default = {
|
|
|
531
709
|
},
|
|
532
710
|
channel_factory: {
|
|
533
711
|
type: "string",
|
|
712
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
534
713
|
description: "Stable Soroban factory address (C...) used to deploy a per-session channel"
|
|
535
714
|
},
|
|
536
715
|
min_deposit: {
|
|
537
716
|
type: "string",
|
|
717
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
538
718
|
description: "Minimum initial deposit to open a channel (decimal string)"
|
|
539
719
|
},
|
|
540
720
|
refund_waiting_period_ledgers: {
|
|
@@ -632,68 +812,110 @@ var routedock_schema_default = {
|
|
|
632
812
|
}
|
|
633
813
|
};
|
|
634
814
|
|
|
635
|
-
//
|
|
636
|
-
var
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
if (value !== null && typeof value === "object") {
|
|
644
|
-
return Object.fromEntries(
|
|
645
|
-
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
|
|
646
|
-
);
|
|
815
|
+
// src/manifest/validate.ts
|
|
816
|
+
var validator = new jsonSchema.Validator(routedock_schema_default, "7");
|
|
817
|
+
function validateManifestSchema(manifest, source) {
|
|
818
|
+
const result = validator.validate(manifest);
|
|
819
|
+
if (!result.valid) {
|
|
820
|
+
const msgs = result.errors.map((err) => err.error).join("; ");
|
|
821
|
+
throw new RouteDockManifestError(`Invalid manifest${source ? ` at ${source}` : ""}: ${msgs}`);
|
|
647
822
|
}
|
|
648
|
-
return value;
|
|
649
823
|
}
|
|
650
|
-
function
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
)
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
824
|
+
function assertManifestValid(manifest, baseUrl) {
|
|
825
|
+
const context = baseUrl ? ` at ${baseUrl}` : "";
|
|
826
|
+
if (manifest.latency_hints !== void 0) {
|
|
827
|
+
if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
|
|
828
|
+
throw new RouteDockManifestError(
|
|
829
|
+
`Invalid manifest${context}: latency_hints defined without declaring regions`
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
const regionSet = new Set(manifest.regions);
|
|
833
|
+
for (const region of Object.keys(manifest.latency_hints)) {
|
|
834
|
+
if (!regionSet.has(region)) {
|
|
835
|
+
throw new RouteDockManifestError(
|
|
836
|
+
`Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
665
840
|
}
|
|
666
|
-
if (
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
841
|
+
if (manifest.assets !== void 0) {
|
|
842
|
+
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
843
|
+
throw new RouteDockManifestError(
|
|
844
|
+
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
for (let i = 0; i < manifest.assets.length; i++) {
|
|
848
|
+
const a = manifest.assets[i];
|
|
849
|
+
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
850
|
+
throw new RouteDockManifestError(
|
|
851
|
+
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
const first = manifest.assets[0];
|
|
856
|
+
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
857
|
+
throw new RouteDockManifestError(
|
|
858
|
+
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
859
|
+
);
|
|
860
|
+
}
|
|
670
861
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
862
|
+
assertPricingMatchesModes(manifest, context);
|
|
863
|
+
assertDeprecatedModesListed(manifest, context);
|
|
864
|
+
assertPricesParse(manifest, context);
|
|
865
|
+
}
|
|
866
|
+
function assertPricingMatchesModes(manifest, context) {
|
|
867
|
+
const pricing = manifest.pricing ?? {};
|
|
868
|
+
for (const mode of manifest.modes ?? []) {
|
|
869
|
+
if (pricing[mode] === void 0 || pricing[mode] === null) {
|
|
870
|
+
throw new RouteDockManifestError(
|
|
871
|
+
`Invalid manifest${context}: mode '${mode}' is listed in modes but pricing.${mode} is missing`
|
|
872
|
+
);
|
|
873
|
+
}
|
|
678
874
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
875
|
+
}
|
|
876
|
+
function assertDeprecatedModesListed(manifest, context) {
|
|
877
|
+
if (manifest.deprecated_modes === void 0) return;
|
|
878
|
+
const modes = new Set(manifest.modes ?? []);
|
|
879
|
+
for (const mode of manifest.deprecated_modes) {
|
|
880
|
+
if (!modes.has(mode)) {
|
|
881
|
+
throw new RouteDockManifestError(
|
|
882
|
+
`Invalid manifest${context}: deprecated_modes entry '${mode}' is not listed in modes`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
686
885
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
886
|
+
}
|
|
887
|
+
var PRICE_FIELDS = {
|
|
888
|
+
x402: ["amount"],
|
|
889
|
+
"mpp-charge": ["amount"],
|
|
890
|
+
"mpp-session": ["rate", "min_deposit"],
|
|
891
|
+
"mpp-session-ws": ["rate", "min_deposit"]
|
|
892
|
+
};
|
|
893
|
+
function assertPricesParse(manifest, context) {
|
|
894
|
+
const pricing = manifest.pricing ?? {};
|
|
895
|
+
for (const [mode, fields] of Object.entries(PRICE_FIELDS)) {
|
|
896
|
+
const entry = pricing[mode];
|
|
897
|
+
if (entry === void 0 || entry === null) continue;
|
|
898
|
+
for (const field of fields) {
|
|
899
|
+
const value = entry[field];
|
|
900
|
+
try {
|
|
901
|
+
if (typeof value !== "string") {
|
|
902
|
+
throw new RangeError(`expected a decimal string, got ${typeof value}`);
|
|
903
|
+
}
|
|
904
|
+
nulthSdk.usdcToStroops(value);
|
|
905
|
+
} catch (err) {
|
|
906
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
907
|
+
throw new RouteDockManifestError(
|
|
908
|
+
`Invalid manifest${context}: pricing.${mode}.${field} ${JSON.stringify(value)} is not a valid amount (${detail})`,
|
|
909
|
+
{ cause: err }
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
691
913
|
}
|
|
692
914
|
}
|
|
693
915
|
|
|
694
916
|
// src/client/ModeRouter.ts
|
|
695
|
-
var validator = new jsonSchema.Validator(routedock_schema_default, "7");
|
|
696
917
|
var SDK_VERSION = package_default.version;
|
|
918
|
+
var SUPPORTED_MANIFEST_MAJOR = 1;
|
|
697
919
|
function parseMajorMinor(version) {
|
|
698
920
|
const parts = version.split(".");
|
|
699
921
|
return [Number.parseInt(parts[0] ?? "0", 10), Number.parseInt(parts[1] ?? "0", 10)];
|
|
@@ -711,6 +933,14 @@ function assertClientVersionSupported(manifest, baseUrl) {
|
|
|
711
933
|
);
|
|
712
934
|
}
|
|
713
935
|
}
|
|
936
|
+
function assertManifestVersionSupported(manifest, baseUrl) {
|
|
937
|
+
const [major] = parseMajorMinor(manifest.routedock);
|
|
938
|
+
if (major > SUPPORTED_MANIFEST_MAJOR) {
|
|
939
|
+
throw new RouteDockClientVersionError(
|
|
940
|
+
`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.`
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
714
944
|
function assertManifestActive(manifest, baseUrl) {
|
|
715
945
|
const sunsetAt = manifest.sunset_at;
|
|
716
946
|
if (!sunsetAt) return;
|
|
@@ -726,11 +956,16 @@ function assertManifestActive(manifest, baseUrl) {
|
|
|
726
956
|
);
|
|
727
957
|
}
|
|
728
958
|
}
|
|
729
|
-
function assertEndpointActive(manifest, url, logger) {
|
|
959
|
+
function assertEndpointActive(manifest, url, logger, method) {
|
|
730
960
|
const pathname = new URL(url).pathname;
|
|
731
|
-
const
|
|
961
|
+
const pathMatches = Object.entries(manifest.endpoints).filter(
|
|
732
962
|
([, descriptor]) => descriptor.path === pathname
|
|
733
963
|
);
|
|
964
|
+
if (pathMatches.length === 0) return;
|
|
965
|
+
const requestedMethod = method?.toUpperCase();
|
|
966
|
+
const matches = requestedMethod ? pathMatches.filter(
|
|
967
|
+
([, descriptor]) => descriptor.method.toUpperCase() === requestedMethod
|
|
968
|
+
) : pathMatches;
|
|
734
969
|
if (matches.length === 0) return;
|
|
735
970
|
const now = Date.now();
|
|
736
971
|
const allSunset = matches.every(([, descriptor]) => {
|
|
@@ -753,45 +988,6 @@ function assertEndpointActive(manifest, url, logger) {
|
|
|
753
988
|
);
|
|
754
989
|
}
|
|
755
990
|
}
|
|
756
|
-
function assertManifestValid(manifest, baseUrl) {
|
|
757
|
-
const context = baseUrl ? ` at ${baseUrl}` : "";
|
|
758
|
-
if (manifest.latency_hints !== void 0) {
|
|
759
|
-
if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
|
|
760
|
-
throw new RouteDockManifestError(
|
|
761
|
-
`Invalid manifest${context}: latency_hints defined without declaring regions`
|
|
762
|
-
);
|
|
763
|
-
}
|
|
764
|
-
const regionSet = new Set(manifest.regions);
|
|
765
|
-
for (const region of Object.keys(manifest.latency_hints)) {
|
|
766
|
-
if (!regionSet.has(region)) {
|
|
767
|
-
throw new RouteDockManifestError(
|
|
768
|
-
`Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
if (manifest.assets !== void 0) {
|
|
774
|
-
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
775
|
-
throw new RouteDockManifestError(
|
|
776
|
-
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
|
-
for (let i = 0; i < manifest.assets.length; i++) {
|
|
780
|
-
const a = manifest.assets[i];
|
|
781
|
-
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
782
|
-
throw new RouteDockManifestError(
|
|
783
|
-
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
784
|
-
);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
const first = manifest.assets[0];
|
|
788
|
-
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
789
|
-
throw new RouteDockManifestError(
|
|
790
|
-
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
991
|
var CACHE_TTL_MS = 6e4;
|
|
796
992
|
var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
|
|
797
993
|
var DEFAULT_MANIFEST_TIMEOUT_MS = 5e3;
|
|
@@ -832,6 +1028,7 @@ var LruCache = class {
|
|
|
832
1028
|
}
|
|
833
1029
|
};
|
|
834
1030
|
var manifestCache = new LruCache(DEFAULT_MANIFEST_CACHE_MAX_SIZE);
|
|
1031
|
+
var inflightManifestFetches = /* @__PURE__ */ new Map();
|
|
835
1032
|
function invalidateManifest(baseUrl) {
|
|
836
1033
|
manifestCache.delete(baseUrl);
|
|
837
1034
|
}
|
|
@@ -861,10 +1058,27 @@ async function fetchManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_M
|
|
|
861
1058
|
if (cached && Date.now() < cached.expiresAt) {
|
|
862
1059
|
verifyManifestSignature(cached.manifest, expectedPayee);
|
|
863
1060
|
assertClientVersionSupported(cached.manifest, baseUrl);
|
|
1061
|
+
assertManifestVersionSupported(cached.manifest, baseUrl);
|
|
864
1062
|
assertManifestValid(cached.manifest, baseUrl);
|
|
865
1063
|
assertManifestActive(cached.manifest, baseUrl);
|
|
866
1064
|
return cached.manifest;
|
|
867
1065
|
}
|
|
1066
|
+
let inflight = inflightManifestFetches.get(baseUrl);
|
|
1067
|
+
if (!inflight) {
|
|
1068
|
+
inflight = fetchAndCacheManifest(baseUrl, retryPolicy, manifestTimeoutMs).finally(() => {
|
|
1069
|
+
inflightManifestFetches.delete(baseUrl);
|
|
1070
|
+
});
|
|
1071
|
+
inflightManifestFetches.set(baseUrl, inflight);
|
|
1072
|
+
}
|
|
1073
|
+
const manifest = await inflight;
|
|
1074
|
+
verifyManifestSignature(manifest, expectedPayee);
|
|
1075
|
+
assertClientVersionSupported(manifest, baseUrl);
|
|
1076
|
+
assertManifestVersionSupported(manifest, baseUrl);
|
|
1077
|
+
assertManifestValid(manifest, baseUrl);
|
|
1078
|
+
assertManifestActive(manifest, baseUrl);
|
|
1079
|
+
return manifest;
|
|
1080
|
+
}
|
|
1081
|
+
async function fetchAndCacheManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_MANIFEST_TIMEOUT_MS) {
|
|
868
1082
|
const url = baseUrl.replace(/\/$/, "") + "/.well-known/routedock.json";
|
|
869
1083
|
return withRetry(async () => {
|
|
870
1084
|
let raw;
|
|
@@ -901,15 +1115,12 @@ async function fetchManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_M
|
|
|
901
1115
|
} catch (err) {
|
|
902
1116
|
throw new RouteDockManifestError(`Manifest at ${url} is not valid JSON`, { cause: err });
|
|
903
1117
|
}
|
|
904
|
-
|
|
905
|
-
if (!result.valid) {
|
|
906
|
-
const msgs = result.errors.map((err) => err.error).join("; ");
|
|
907
|
-
throw new RouteDockManifestError(`Invalid manifest at ${url}: ${msgs}`);
|
|
908
|
-
}
|
|
1118
|
+
validateManifestSchema(raw, url);
|
|
909
1119
|
const manifest = raw;
|
|
910
1120
|
assertManifestValid(manifest, baseUrl);
|
|
911
|
-
verifyManifestSignature(manifest
|
|
1121
|
+
verifyManifestSignature(manifest);
|
|
912
1122
|
assertClientVersionSupported(manifest, baseUrl);
|
|
1123
|
+
assertManifestVersionSupported(manifest, baseUrl);
|
|
913
1124
|
assertManifestActive(manifest, baseUrl);
|
|
914
1125
|
manifestCache.set(baseUrl, { manifest, fetchedAt: Date.now(), expiresAt });
|
|
915
1126
|
return manifest;
|
|
@@ -1008,6 +1219,48 @@ function selectMode(manifest, options = {}) {
|
|
|
1008
1219
|
);
|
|
1009
1220
|
}
|
|
1010
1221
|
|
|
1222
|
+
// src/internal/settleResult.ts
|
|
1223
|
+
function readSettleResult(result) {
|
|
1224
|
+
const settle = result;
|
|
1225
|
+
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
1226
|
+
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
1227
|
+
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
1228
|
+
return failed(settle.errorReason);
|
|
1229
|
+
}
|
|
1230
|
+
return { ok: true, txHash: settle.transaction };
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/internal/request.ts
|
|
1234
|
+
var DEFAULT_METHOD = "GET";
|
|
1235
|
+
var BODYLESS_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
|
|
1236
|
+
function isReplayable(body) {
|
|
1237
|
+
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;
|
|
1238
|
+
}
|
|
1239
|
+
async function bufferBody(body) {
|
|
1240
|
+
if (isReplayable(body)) return body;
|
|
1241
|
+
return new Response(body).arrayBuffer();
|
|
1242
|
+
}
|
|
1243
|
+
async function prepareRequest(request) {
|
|
1244
|
+
const method = (request?.method ?? DEFAULT_METHOD).toUpperCase();
|
|
1245
|
+
const headers = new Headers(request?.headers);
|
|
1246
|
+
const body = request?.body;
|
|
1247
|
+
if (body === void 0 || BODYLESS_METHODS.has(method)) {
|
|
1248
|
+
return { method, headers };
|
|
1249
|
+
}
|
|
1250
|
+
return { method, headers, body: await bufferBody(body) };
|
|
1251
|
+
}
|
|
1252
|
+
function toRequestInit(prepared, overrides) {
|
|
1253
|
+
const headers = new Headers(prepared.headers);
|
|
1254
|
+
if (overrides) {
|
|
1255
|
+
new Headers(overrides).forEach((value, name) => headers.set(name, value));
|
|
1256
|
+
}
|
|
1257
|
+
const init = { method: prepared.method, headers };
|
|
1258
|
+
if (prepared.body !== void 0) {
|
|
1259
|
+
init.body = prepared.body;
|
|
1260
|
+
}
|
|
1261
|
+
return init;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1011
1264
|
// src/internal/payee.ts
|
|
1012
1265
|
function resolvePayee(manifest, mode) {
|
|
1013
1266
|
const override = manifest.pricing[mode]?.payee;
|
|
@@ -1068,6 +1321,102 @@ function checkChargeChallenge(request, manifest, network, maxAmount) {
|
|
|
1068
1321
|
}
|
|
1069
1322
|
return checkAmount(request["amount"], maxAmount);
|
|
1070
1323
|
}
|
|
1324
|
+
var defaultIo = {
|
|
1325
|
+
async expirationLedger(network, maxTimeoutSeconds) {
|
|
1326
|
+
const { sequence } = await stellar.getRpcClient(network).getLatestLedger();
|
|
1327
|
+
const secondsPerLedger = await stellar.getEstimatedLedgerCloseTimeSeconds(network);
|
|
1328
|
+
return sequence + Math.ceil(maxTimeoutSeconds / secondsPerLedger);
|
|
1329
|
+
},
|
|
1330
|
+
async assemble({ asset, from, payTo, amount, networkPassphrase, rpcUrl }) {
|
|
1331
|
+
return stellarSdk.contract.AssembledTransaction.build({
|
|
1332
|
+
contractId: asset,
|
|
1333
|
+
method: "transfer",
|
|
1334
|
+
// SEP-41: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md#interface
|
|
1335
|
+
args: [
|
|
1336
|
+
stellarSdk.nativeToScVal(from, { type: "address" }),
|
|
1337
|
+
stellarSdk.nativeToScVal(payTo, { type: "address" }),
|
|
1338
|
+
stellarSdk.nativeToScVal(amount, { type: "i128" })
|
|
1339
|
+
],
|
|
1340
|
+
networkPassphrase,
|
|
1341
|
+
rpcUrl,
|
|
1342
|
+
parseResultXdr: (result) => result
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
var NulthStellarScheme = class {
|
|
1347
|
+
constructor(signer, io = defaultIo) {
|
|
1348
|
+
this.signer = signer;
|
|
1349
|
+
this.io = io;
|
|
1350
|
+
}
|
|
1351
|
+
signer;
|
|
1352
|
+
io;
|
|
1353
|
+
scheme = "exact";
|
|
1354
|
+
async createPaymentPayload(x402Version, paymentRequirements) {
|
|
1355
|
+
try {
|
|
1356
|
+
this.validateRequirements(paymentRequirements);
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
throw new Error(
|
|
1359
|
+
`Invalid input parameters for creating Nulth Stellar payment, cause: ${String(err)}`
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const from = this.signer.address;
|
|
1363
|
+
const { network, payTo, asset, amount, maxTimeoutSeconds } = paymentRequirements;
|
|
1364
|
+
if (!paymentRequirements.extra.areFeesSponsored) {
|
|
1365
|
+
throw new Error("Nulth exact scheme requires areFeesSponsored to be true");
|
|
1366
|
+
}
|
|
1367
|
+
const maxLedger = await this.io.expirationLedger(network, maxTimeoutSeconds);
|
|
1368
|
+
const tx = await this.io.assemble({
|
|
1369
|
+
asset,
|
|
1370
|
+
from,
|
|
1371
|
+
payTo,
|
|
1372
|
+
amount,
|
|
1373
|
+
networkPassphrase: stellar.getNetworkPassphrase(network),
|
|
1374
|
+
rpcUrl: stellar.getRpcUrl(network)
|
|
1375
|
+
});
|
|
1376
|
+
stellar.handleSimulationResult(tx.simulation);
|
|
1377
|
+
const signers = tx.needsNonInvokerSigningBy();
|
|
1378
|
+
if (!signers.includes(from) || signers.length > 1) {
|
|
1379
|
+
throw new Error(`Expected to sign with [${from}], but got [${signers.join(", ")}]`);
|
|
1380
|
+
}
|
|
1381
|
+
await tx.signAuthEntries({
|
|
1382
|
+
address: from,
|
|
1383
|
+
signAuthEntry: this.signer.signAuthEntry,
|
|
1384
|
+
expiration: maxLedger,
|
|
1385
|
+
authorizeEntry: nulthSdk.nulthAuthorizeEntry
|
|
1386
|
+
});
|
|
1387
|
+
await tx.simulate();
|
|
1388
|
+
stellar.handleSimulationResult(tx.simulation);
|
|
1389
|
+
const remaining = tx.needsNonInvokerSigningBy();
|
|
1390
|
+
if (remaining.length > 0) {
|
|
1391
|
+
throw new Error(`unexpected signer(s) required: [${remaining.join(", ")}]`);
|
|
1392
|
+
}
|
|
1393
|
+
if (!tx.built) {
|
|
1394
|
+
throw new Error("Nulth payment transaction has no assembled envelope to send");
|
|
1395
|
+
}
|
|
1396
|
+
return { x402Version, payload: { transaction: tx.built.toXDR() } };
|
|
1397
|
+
}
|
|
1398
|
+
validateRequirements(requirements) {
|
|
1399
|
+
const { scheme, network, payTo, asset, amount } = requirements;
|
|
1400
|
+
if (typeof amount !== "string" || !Number.isInteger(Number(amount)) || Number(amount) <= 0) {
|
|
1401
|
+
throw new Error(`Invalid amount: ${amount}. Amount must be a positive integer.`);
|
|
1402
|
+
}
|
|
1403
|
+
if (scheme !== this.scheme) {
|
|
1404
|
+
throw new Error(`Unsupported scheme: ${scheme}`);
|
|
1405
|
+
}
|
|
1406
|
+
if (!stellar.isStellarNetwork(network)) {
|
|
1407
|
+
throw new Error(`Unsupported Stellar network: ${network}`);
|
|
1408
|
+
}
|
|
1409
|
+
if (!stellar.validateStellarDestinationAddress(payTo)) {
|
|
1410
|
+
throw new Error(`Invalid Stellar destination address: ${payTo}`);
|
|
1411
|
+
}
|
|
1412
|
+
if (!stellar.validateStellarAssetAddress(asset)) {
|
|
1413
|
+
throw new Error(`Invalid Stellar asset address: ${asset}`);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
function selectStellarScheme(signer) {
|
|
1418
|
+
return nulthSdk.isNulthStellarSigner(signer) ? new NulthStellarScheme(signer) : new stellar.ExactStellarScheme(signer);
|
|
1419
|
+
}
|
|
1071
1420
|
|
|
1072
1421
|
// src/client/x402Client.ts
|
|
1073
1422
|
var CAIP2 = {
|
|
@@ -1075,25 +1424,29 @@ var CAIP2 = {
|
|
|
1075
1424
|
mainnet: "stellar:pubnet"
|
|
1076
1425
|
};
|
|
1077
1426
|
var X402Client = class _X402Client {
|
|
1078
|
-
constructor(secretKeyOrSigner, network, retryPolicy) {
|
|
1427
|
+
constructor(secretKeyOrSigner, network, retryPolicy, preSignHook, logger) {
|
|
1079
1428
|
this.network = network;
|
|
1080
1429
|
this.retryPolicy = retryPolicy;
|
|
1430
|
+
this.preSignHook = preSignHook;
|
|
1081
1431
|
const caip2 = CAIP2[network];
|
|
1082
1432
|
this.signer = typeof secretKeyOrSigner === "string" ? stellar.createEd25519Signer(secretKeyOrSigner, caip2) : secretKeyOrSigner;
|
|
1083
|
-
const scheme =
|
|
1084
|
-
const core = new client
|
|
1433
|
+
const scheme = selectStellarScheme(this.signer);
|
|
1434
|
+
const core = new client.x402Client();
|
|
1085
1435
|
core.register(caip2, scheme);
|
|
1086
|
-
this.httpClient = new client
|
|
1436
|
+
this.httpClient = new client.x402HTTPClient(core);
|
|
1437
|
+
this.logger = logger;
|
|
1087
1438
|
}
|
|
1088
1439
|
network;
|
|
1089
1440
|
retryPolicy;
|
|
1441
|
+
preSignHook;
|
|
1090
1442
|
httpClient;
|
|
1091
1443
|
signer;
|
|
1444
|
+
logger;
|
|
1092
1445
|
/** Replace signer (e.g. swap to Nulth ZK account payer before pay) */
|
|
1093
1446
|
withSigner(signer) {
|
|
1094
|
-
return new _X402Client(signer, this.network, this.retryPolicy);
|
|
1447
|
+
return new _X402Client(signer, this.network, this.retryPolicy, this.preSignHook, this.logger);
|
|
1095
1448
|
}
|
|
1096
|
-
async pay(url, manifest) {
|
|
1449
|
+
async pay(url, manifest, request) {
|
|
1097
1450
|
const pricing = manifest.pricing.x402;
|
|
1098
1451
|
if (!pricing) {
|
|
1099
1452
|
throw new RouteDockManifestError("manifest.pricing.x402 missing");
|
|
@@ -1102,13 +1455,15 @@ var X402Client = class _X402Client {
|
|
|
1102
1455
|
throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
|
|
1103
1456
|
}
|
|
1104
1457
|
const caip2 = CAIP2[this.network];
|
|
1458
|
+
const prepared = await prepareRequest(request);
|
|
1105
1459
|
const probe = await withRetry(async () => {
|
|
1106
1460
|
let init2;
|
|
1107
1461
|
try {
|
|
1108
|
-
init2 = await fetch(url,
|
|
1462
|
+
init2 = await fetch(url, toRequestInit(prepared, { "X-Preferred-Mode": "x402" }));
|
|
1109
1463
|
} catch (err) {
|
|
1110
1464
|
throw wrapFetchError(err, "x402 initial request");
|
|
1111
1465
|
}
|
|
1466
|
+
this.logger?.("debug", "[x402] unpaid probe response", { status: init2.status });
|
|
1112
1467
|
if (init2.status !== 402) {
|
|
1113
1468
|
if (!init2.ok) {
|
|
1114
1469
|
throw httpStatusToError(
|
|
@@ -1125,6 +1480,11 @@ var X402Client = class _X402Client {
|
|
|
1125
1480
|
`Failed to parse JSON from non-402 response (HTTP ${init2.status})`
|
|
1126
1481
|
);
|
|
1127
1482
|
}
|
|
1483
|
+
this.logger?.(
|
|
1484
|
+
"debug",
|
|
1485
|
+
"[x402] unpaid probe returned non-402 success \u2014 serving as free response",
|
|
1486
|
+
{ status: init2.status }
|
|
1487
|
+
);
|
|
1128
1488
|
return { kind: "free", data: data2 };
|
|
1129
1489
|
}
|
|
1130
1490
|
return { kind: "payment-required", init: init2 };
|
|
@@ -1153,6 +1513,18 @@ var X402Client = class _X402Client {
|
|
|
1153
1513
|
if (allowed.length === 0) {
|
|
1154
1514
|
throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
|
|
1155
1515
|
}
|
|
1516
|
+
if (this.preSignHook) {
|
|
1517
|
+
let decision;
|
|
1518
|
+
try {
|
|
1519
|
+
decision = await this.preSignHook({ url, manifest, accepts: allowed });
|
|
1520
|
+
} catch (err) {
|
|
1521
|
+
throw new RouteDockPolicyRejectError("pre_sign_hook_error", { cause: err });
|
|
1522
|
+
}
|
|
1523
|
+
if (!decision || decision.approve !== true) {
|
|
1524
|
+
const reason = decision && decision.approve === false ? decision.reason : void 0;
|
|
1525
|
+
throw new RouteDockPolicyRejectError(reason ?? "pre_sign_rejected");
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1156
1528
|
let paymentPayload;
|
|
1157
1529
|
try {
|
|
1158
1530
|
paymentPayload = await this.httpClient.createPaymentPayload({
|
|
@@ -1166,13 +1538,15 @@ var X402Client = class _X402Client {
|
|
|
1166
1538
|
}
|
|
1167
1539
|
const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
1168
1540
|
const signedAmount = nulthSdk.stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
|
|
1541
|
+
this.logger?.("debug", "[x402] sending paid request with payment header");
|
|
1169
1542
|
const settled = await withRetry(async () => {
|
|
1170
1543
|
let response;
|
|
1171
1544
|
try {
|
|
1172
|
-
response = await fetch(url,
|
|
1545
|
+
response = await fetch(url, toRequestInit(prepared, paymentHeaders));
|
|
1173
1546
|
} catch (err) {
|
|
1174
1547
|
throw wrapFetchError(err, "x402 settlement request");
|
|
1175
1548
|
}
|
|
1549
|
+
this.logger?.("debug", "[x402] paid request response", { status: response.status });
|
|
1176
1550
|
if (!response.ok) {
|
|
1177
1551
|
if (response.status >= 500 || response.status === 429 || response.status === 503) {
|
|
1178
1552
|
throw httpStatusToError(
|
|
@@ -1181,17 +1555,28 @@ var X402Client = class _X402Client {
|
|
|
1181
1555
|
response
|
|
1182
1556
|
);
|
|
1183
1557
|
}
|
|
1184
|
-
throw
|
|
1558
|
+
throw await paymentRejectedError("x402 payment failed", response.status, response);
|
|
1185
1559
|
}
|
|
1186
1560
|
return response;
|
|
1187
1561
|
}, this.retryPolicy);
|
|
1188
1562
|
let txHash = null;
|
|
1189
1563
|
const responseHeader = settled.headers.get("X-Payment-Response");
|
|
1190
|
-
if (responseHeader) {
|
|
1564
|
+
if (!responseHeader) {
|
|
1565
|
+
this.logger?.("debug", "[x402] settled 2xx carried no X-Payment-Response header");
|
|
1566
|
+
} else {
|
|
1191
1567
|
try {
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1568
|
+
const outcome = readSettleResult(http.decodePaymentResponseHeader(responseHeader));
|
|
1569
|
+
if (outcome.ok) {
|
|
1570
|
+
txHash = outcome.txHash;
|
|
1571
|
+
} else {
|
|
1572
|
+
this.logger?.("debug", "[x402] X-Payment-Response reports an unsettled payment", {
|
|
1573
|
+
...outcome.reason !== void 0 ? { reason: outcome.reason } : {}
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
} catch (err) {
|
|
1577
|
+
this.logger?.("debug", "[x402] X-Payment-Response header present but undecodable", {
|
|
1578
|
+
error: err
|
|
1579
|
+
});
|
|
1195
1580
|
}
|
|
1196
1581
|
}
|
|
1197
1582
|
let data;
|
|
@@ -1212,33 +1597,28 @@ var MppChargeClient = class {
|
|
|
1212
1597
|
keypair;
|
|
1213
1598
|
network;
|
|
1214
1599
|
retryPolicy;
|
|
1215
|
-
async pay(url, manifest) {
|
|
1600
|
+
async pay(url, manifest, request) {
|
|
1216
1601
|
const pricing = manifest.pricing["mpp-charge"];
|
|
1217
1602
|
if (!pricing) {
|
|
1218
1603
|
throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
|
|
1219
1604
|
}
|
|
1220
1605
|
const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
|
|
1221
|
-
|
|
1606
|
+
const prepared = await prepareRequest(request);
|
|
1222
1607
|
let signedAmount;
|
|
1223
1608
|
let credential;
|
|
1224
|
-
const mppx = client$
|
|
1609
|
+
const mppx = client$1.Mppx.create({
|
|
1225
1610
|
polyfill: false,
|
|
1226
1611
|
methods: [
|
|
1227
|
-
client$
|
|
1612
|
+
client$2.stellar.charge({
|
|
1228
1613
|
keypair: this.keypair,
|
|
1229
|
-
mode: "pull"
|
|
1230
|
-
onProgress(event) {
|
|
1231
|
-
if (event.type === "paid") {
|
|
1232
|
-
txHash = event.hash;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1614
|
+
mode: "pull"
|
|
1235
1615
|
})
|
|
1236
1616
|
],
|
|
1237
1617
|
onChallenge: async (challenge, { createCredential }) => {
|
|
1238
|
-
const
|
|
1239
|
-
const rejection = checkChargeChallenge(
|
|
1618
|
+
const request2 = challenge.request;
|
|
1619
|
+
const rejection = checkChargeChallenge(request2, manifest, caip2, pricing.amount);
|
|
1240
1620
|
if (rejection) throw rejection;
|
|
1241
|
-
const amount =
|
|
1621
|
+
const amount = request2["amount"];
|
|
1242
1622
|
if (typeof amount !== "string") {
|
|
1243
1623
|
throw new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1244
1624
|
}
|
|
@@ -1250,7 +1630,7 @@ var MppChargeClient = class {
|
|
|
1250
1630
|
return withRetry(async () => {
|
|
1251
1631
|
let response;
|
|
1252
1632
|
try {
|
|
1253
|
-
response = credential ? await mppx.rawFetch(url,
|
|
1633
|
+
response = credential ? await mppx.rawFetch(url, toRequestInit(prepared, { Authorization: credential })) : await mppx.fetch(url, toRequestInit(prepared));
|
|
1254
1634
|
} catch (err) {
|
|
1255
1635
|
throw wrapMppError(err, "MPP charge request");
|
|
1256
1636
|
}
|
|
@@ -1262,8 +1642,9 @@ var MppChargeClient = class {
|
|
|
1262
1642
|
response
|
|
1263
1643
|
);
|
|
1264
1644
|
}
|
|
1265
|
-
throw
|
|
1645
|
+
throw await paymentRejectedError("MPP charge failed", response.status, response);
|
|
1266
1646
|
}
|
|
1647
|
+
const txHash = readSettlementHash(response);
|
|
1267
1648
|
let data;
|
|
1268
1649
|
try {
|
|
1269
1650
|
data = await response.json();
|
|
@@ -1283,6 +1664,13 @@ var MppChargeClient = class {
|
|
|
1283
1664
|
}, this.retryPolicy);
|
|
1284
1665
|
}
|
|
1285
1666
|
};
|
|
1667
|
+
function readSettlementHash(response) {
|
|
1668
|
+
try {
|
|
1669
|
+
return mppx.Receipt.fromResponse(response).reference;
|
|
1670
|
+
} catch {
|
|
1671
|
+
return null;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1286
1674
|
|
|
1287
1675
|
// src/types.ts
|
|
1288
1676
|
var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
|
|
@@ -1290,6 +1678,31 @@ var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
|
|
|
1290
1678
|
// src/client/MppSessionClient.ts
|
|
1291
1679
|
var MIN_REFUND_WAITING_PERIOD = 17280;
|
|
1292
1680
|
var STREAM_CLEANUP_TIMEOUT_MS = 3e3;
|
|
1681
|
+
var TX_CONFIRM_INTERVAL_MS = 1e3;
|
|
1682
|
+
var TX_CONFIRM_TIMEOUT_MS = 3e4;
|
|
1683
|
+
async function confirmTransaction(server, hash, label, timeoutMs = TX_CONFIRM_TIMEOUT_MS, intervalMs = TX_CONFIRM_INTERVAL_MS) {
|
|
1684
|
+
const deadline = Date.now() + timeoutMs;
|
|
1685
|
+
while (true) {
|
|
1686
|
+
const txResponse = await server.getTransaction(hash);
|
|
1687
|
+
const status = txResponse?.status;
|
|
1688
|
+
if (status === "SUCCESS") {
|
|
1689
|
+
return hash;
|
|
1690
|
+
}
|
|
1691
|
+
if (status === "FAILED") {
|
|
1692
|
+
throw new RouteDockDisputeError(
|
|
1693
|
+
`${label} transaction failed on-chain: status FAILED (resultXdr: ${txResponse.resultXdr.toXDR("base64")})`
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
if (Date.now() >= deadline) {
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
const remaining = deadline - Date.now();
|
|
1700
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, Math.max(1, remaining))));
|
|
1701
|
+
}
|
|
1702
|
+
throw new RouteDockDisputeError(
|
|
1703
|
+
`${label} transaction confirmation timed out after ${timeoutMs}ms (hash: ${hash})`
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1293
1706
|
var WS_CONNECTING = 0;
|
|
1294
1707
|
var WS_OPEN = 1;
|
|
1295
1708
|
function defaultWebSocketFactory(url, init, handlers) {
|
|
@@ -1303,9 +1716,9 @@ function defaultWebSocketFactory(url, init, handlers) {
|
|
|
1303
1716
|
if (typeof data === "string") {
|
|
1304
1717
|
handlers.onMessage(data);
|
|
1305
1718
|
} else if (data instanceof ArrayBuffer) {
|
|
1306
|
-
handlers.onMessage(
|
|
1719
|
+
handlers.onMessage(bytesToUtf8(new Uint8Array(data)));
|
|
1307
1720
|
} else {
|
|
1308
|
-
void data.arrayBuffer().then((buffer) => handlers.onMessage(
|
|
1721
|
+
void data.arrayBuffer().then((buffer) => handlers.onMessage(bytesToUtf8(new Uint8Array(buffer)))).catch(() => {
|
|
1309
1722
|
});
|
|
1310
1723
|
}
|
|
1311
1724
|
};
|
|
@@ -1458,11 +1871,11 @@ var MppSessionClient = class {
|
|
|
1458
1871
|
throw err;
|
|
1459
1872
|
}
|
|
1460
1873
|
};
|
|
1461
|
-
const streamWs = (u, m, createCredential, onSigned) => this.streamWebSocket(u, m, createCredential, onSigned);
|
|
1462
|
-
const mppx = client$
|
|
1874
|
+
const streamWs = (u, m, createCredential, onSigned, request) => this.streamWebSocket(u, m, createCredential, onSigned, request);
|
|
1875
|
+
const mppx = client$1.Mppx.create({
|
|
1463
1876
|
polyfill: false,
|
|
1464
1877
|
methods: [
|
|
1465
|
-
client$
|
|
1878
|
+
client$3.stellar.channel({
|
|
1466
1879
|
commitmentKey,
|
|
1467
1880
|
sourceAccount: this.keypair.publicKey(),
|
|
1468
1881
|
...store ? { store } : {},
|
|
@@ -1530,6 +1943,7 @@ var MppSessionClient = class {
|
|
|
1530
1943
|
};
|
|
1531
1944
|
},
|
|
1532
1945
|
async *stream(options2) {
|
|
1946
|
+
const prepared = await prepareRequest(options2?.request);
|
|
1533
1947
|
const checkSpend = () => {
|
|
1534
1948
|
if (!onSpend) return Promise.resolve();
|
|
1535
1949
|
return onSpend(pricing.rate);
|
|
@@ -1539,7 +1953,13 @@ var MppSessionClient = class {
|
|
|
1539
1953
|
const onSigned = () => {
|
|
1540
1954
|
vouchersIssued++;
|
|
1541
1955
|
};
|
|
1542
|
-
for await (const item of streamWs(
|
|
1956
|
+
for await (const item of streamWs(
|
|
1957
|
+
url,
|
|
1958
|
+
mppx,
|
|
1959
|
+
createGuardedCredential,
|
|
1960
|
+
onSigned,
|
|
1961
|
+
prepared
|
|
1962
|
+
)) {
|
|
1543
1963
|
yield item;
|
|
1544
1964
|
}
|
|
1545
1965
|
return;
|
|
@@ -1554,7 +1974,10 @@ var MppSessionClient = class {
|
|
|
1554
1974
|
const doFetch = () => withRetry(async () => {
|
|
1555
1975
|
let resp;
|
|
1556
1976
|
try {
|
|
1557
|
-
resp = await mppx.fetch(url, {
|
|
1977
|
+
resp = await mppx.fetch(url, {
|
|
1978
|
+
...toRequestInit(prepared),
|
|
1979
|
+
signal: abortController.signal
|
|
1980
|
+
});
|
|
1558
1981
|
} catch (err) {
|
|
1559
1982
|
if (abortController.signal.aborted) {
|
|
1560
1983
|
throw err;
|
|
@@ -1639,10 +2062,10 @@ var MppSessionClient = class {
|
|
|
1639
2062
|
async close() {
|
|
1640
2063
|
clearSessionTimer();
|
|
1641
2064
|
closed = true;
|
|
1642
|
-
const { rpc: rpcMod, Contract, nativeToScVal, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
|
|
2065
|
+
const { rpc: rpcMod, Contract, nativeToScVal: nativeToScVal2, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
|
|
1643
2066
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1644
2067
|
const server = new rpcMod.Server(rpcUrl);
|
|
1645
|
-
const
|
|
2068
|
+
const contract2 = new Contract(channelFactory);
|
|
1646
2069
|
const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
|
|
1647
2070
|
const account = await withRetry(async () => {
|
|
1648
2071
|
try {
|
|
@@ -1657,9 +2080,9 @@ var MppSessionClient = class {
|
|
|
1657
2080
|
fee: BASE_FEE,
|
|
1658
2081
|
networkPassphrase: passphrase
|
|
1659
2082
|
}).addOperation(
|
|
1660
|
-
|
|
2083
|
+
contract2.call(
|
|
1661
2084
|
"prepare_commitment",
|
|
1662
|
-
|
|
2085
|
+
nativeToScVal2(currentCumulative, { type: "i128" })
|
|
1663
2086
|
)
|
|
1664
2087
|
).setTimeout(30).build();
|
|
1665
2088
|
return await server.simulateTransaction(simTx);
|
|
@@ -1678,7 +2101,7 @@ var MppSessionClient = class {
|
|
|
1678
2101
|
}
|
|
1679
2102
|
let signature;
|
|
1680
2103
|
try {
|
|
1681
|
-
signature = commitmentKey.sign(
|
|
2104
|
+
signature = commitmentKey.sign(commitmentBytes);
|
|
1682
2105
|
} catch (err) {
|
|
1683
2106
|
throw new RouteDockSignatureError("Channel close commitment signing failed", {
|
|
1684
2107
|
cause: err
|
|
@@ -1692,7 +2115,7 @@ var MppSessionClient = class {
|
|
|
1692
2115
|
headers: { "Content-Type": "application/json" },
|
|
1693
2116
|
body: JSON.stringify({
|
|
1694
2117
|
amount: currentCumulative.toString(),
|
|
1695
|
-
signature:
|
|
2118
|
+
signature: bytesToHex(signature)
|
|
1696
2119
|
})
|
|
1697
2120
|
});
|
|
1698
2121
|
} catch (err) {
|
|
@@ -1734,15 +2157,19 @@ var MppSessionClient = class {
|
|
|
1734
2157
|
vouchersIssued
|
|
1735
2158
|
};
|
|
1736
2159
|
},
|
|
1737
|
-
|
|
2160
|
+
/**
|
|
2161
|
+
* Request refund from the channel contract (initiates dispute).
|
|
2162
|
+
* Returns the transaction hash only after on-chain confirmation (SUCCESS).
|
|
2163
|
+
*/
|
|
2164
|
+
async requestRefund(options2) {
|
|
1738
2165
|
const { rpc: rpcMod, Contract, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
|
|
1739
2166
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1740
2167
|
const server = new rpcMod.Server(rpcUrl);
|
|
1741
|
-
const
|
|
2168
|
+
const contract2 = new Contract(channelFactory);
|
|
1742
2169
|
const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
|
|
1743
2170
|
try {
|
|
1744
2171
|
const account = await server.getAccount(agentPublicKey);
|
|
1745
|
-
const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(
|
|
2172
|
+
const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(contract2.call("request_refund")).setTimeout(30).build();
|
|
1746
2173
|
const simResult = await server.simulateTransaction(tx);
|
|
1747
2174
|
if (rpcMod.Api.isSimulationError(simResult)) {
|
|
1748
2175
|
throw new RouteDockDisputeError(`request_refund simulation failed: ${simResult.error}`);
|
|
@@ -1754,37 +2181,50 @@ var MppSessionClient = class {
|
|
|
1754
2181
|
const errDetail = result.errorResult ? JSON.stringify(result.errorResult) : "status ERROR";
|
|
1755
2182
|
throw new RouteDockDisputeError(`Refund request transaction failed: ${errDetail}`);
|
|
1756
2183
|
}
|
|
2184
|
+
if (result.status === "TRY_AGAIN_LATER") {
|
|
2185
|
+
throw new RouteDockDisputeError("Refund request transaction rejected with TRY_AGAIN_LATER: transaction not queued");
|
|
2186
|
+
}
|
|
1757
2187
|
if (!result.hash) {
|
|
1758
2188
|
throw new RouteDockDisputeError("Refund request transaction not sent");
|
|
1759
2189
|
}
|
|
1760
|
-
return
|
|
2190
|
+
return await confirmTransaction(
|
|
2191
|
+
server,
|
|
2192
|
+
result.hash,
|
|
2193
|
+
"Refund request",
|
|
2194
|
+
options2?.timeoutMs,
|
|
2195
|
+
options2?.intervalMs
|
|
2196
|
+
);
|
|
1761
2197
|
} catch (err) {
|
|
1762
2198
|
if (err instanceof RouteDockDisputeError) throw err;
|
|
1763
2199
|
throw new RouteDockDisputeError(`Failed to request refund: ${err instanceof Error ? err.message : String(err)}`);
|
|
1764
2200
|
}
|
|
1765
2201
|
},
|
|
1766
|
-
|
|
1767
|
-
|
|
2202
|
+
/**
|
|
2203
|
+
* Server-side counter-mechanism to settle with latest voucher before refund window expires.
|
|
2204
|
+
* Returns the transaction hash only after on-chain confirmation (SUCCESS).
|
|
2205
|
+
*/
|
|
2206
|
+
async settleWithLatestVoucher(options2) {
|
|
2207
|
+
const { rpc: rpcMod, Contract, nativeToScVal: nativeToScVal2, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
|
|
1768
2208
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1769
2209
|
const server = new rpcMod.Server(rpcUrl);
|
|
1770
|
-
const
|
|
2210
|
+
const contract2 = new Contract(channelFactory);
|
|
1771
2211
|
const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
|
|
1772
2212
|
try {
|
|
1773
2213
|
const account = await server.getAccount(agentPublicKey);
|
|
1774
|
-
const simTx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(
|
|
2214
|
+
const simTx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(contract2.call("prepare_commitment", nativeToScVal2(currentCumulative, { type: "i128" }))).setTimeout(30).build();
|
|
1775
2215
|
const simResult = await server.simulateTransaction(simTx);
|
|
1776
2216
|
if (rpcMod.Api.isSimulationError(simResult)) {
|
|
1777
2217
|
throw new RouteDockDisputeError(`prepare_commitment simulation failed: ${simResult.error}`);
|
|
1778
2218
|
}
|
|
1779
2219
|
const commitmentBytes = simResult.result?.retval?.bytes();
|
|
1780
2220
|
if (!commitmentBytes) throw new RouteDockDisputeError("prepare_commitment returned no bytes");
|
|
1781
|
-
const signature = commitmentKey.sign(
|
|
2221
|
+
const signature = commitmentKey.sign(commitmentBytes);
|
|
1782
2222
|
const settleAccount = await server.getAccount(agentPublicKey);
|
|
1783
2223
|
const settleTx = new TransactionBuilder(settleAccount, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(
|
|
1784
|
-
|
|
2224
|
+
contract2.call(
|
|
1785
2225
|
"settle_with_signature",
|
|
1786
|
-
|
|
1787
|
-
|
|
2226
|
+
nativeToScVal2(currentCumulative, { type: "i128" }),
|
|
2227
|
+
nativeToScVal2(signature)
|
|
1788
2228
|
)
|
|
1789
2229
|
).setTimeout(30).build();
|
|
1790
2230
|
const preparedSettleTx = await server.prepareTransaction(settleTx);
|
|
@@ -1794,10 +2234,19 @@ var MppSessionClient = class {
|
|
|
1794
2234
|
const errDetail = settleResult.errorResult ? JSON.stringify(settleResult.errorResult) : "status ERROR";
|
|
1795
2235
|
throw new RouteDockDisputeError(`Settlement transaction failed: ${errDetail}`);
|
|
1796
2236
|
}
|
|
2237
|
+
if (settleResult.status === "TRY_AGAIN_LATER") {
|
|
2238
|
+
throw new RouteDockDisputeError("Settlement transaction rejected with TRY_AGAIN_LATER: transaction not queued");
|
|
2239
|
+
}
|
|
1797
2240
|
if (!settleResult.hash) {
|
|
1798
2241
|
throw new RouteDockDisputeError("Settlement transaction not sent");
|
|
1799
2242
|
}
|
|
1800
|
-
return
|
|
2243
|
+
return await confirmTransaction(
|
|
2244
|
+
server,
|
|
2245
|
+
settleResult.hash,
|
|
2246
|
+
"Settlement",
|
|
2247
|
+
options2?.timeoutMs,
|
|
2248
|
+
options2?.intervalMs
|
|
2249
|
+
);
|
|
1801
2250
|
} catch (err) {
|
|
1802
2251
|
if (err instanceof RouteDockDisputeError) throw err;
|
|
1803
2252
|
throw new RouteDockDisputeError(`Failed to settle with latest voucher: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1807,11 +2256,11 @@ var MppSessionClient = class {
|
|
|
1807
2256
|
const { rpc: rpcMod, Contract, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
|
|
1808
2257
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1809
2258
|
const server = new rpcMod.Server(rpcUrl);
|
|
1810
|
-
const
|
|
2259
|
+
const contract2 = new Contract(channelFactory);
|
|
1811
2260
|
const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
|
|
1812
2261
|
try {
|
|
1813
2262
|
const account = await server.getAccount(agentPublicKey);
|
|
1814
|
-
const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(
|
|
2263
|
+
const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(contract2.call("get_channel_state")).setTimeout(30).build();
|
|
1815
2264
|
const simResult = await server.simulateTransaction(tx);
|
|
1816
2265
|
if (rpcMod.Api.isSimulationError(simResult)) {
|
|
1817
2266
|
throw new RouteDockChannelStateError(`Failed to query channel state: ${simResult.error}`);
|
|
@@ -1877,11 +2326,11 @@ var MppSessionClient = class {
|
|
|
1877
2326
|
* close code (1000/1001); abnormal closes and connection failures surface as
|
|
1878
2327
|
* RouteDockChannelStateError.
|
|
1879
2328
|
*/
|
|
1880
|
-
async *streamWebSocket(url, mppx$1, createCredential, onSigned) {
|
|
1881
|
-
const
|
|
2329
|
+
async *streamWebSocket(url, mppx$1, createCredential, onSigned, request) {
|
|
2330
|
+
const probeInit = toRequestInit(request);
|
|
1882
2331
|
let probe;
|
|
1883
2332
|
try {
|
|
1884
|
-
probe = await mppx$1.rawFetch(url,
|
|
2333
|
+
probe = await mppx$1.rawFetch(url, probeInit);
|
|
1885
2334
|
} catch (err) {
|
|
1886
2335
|
throw wrapFetchError(err, "Voucher request");
|
|
1887
2336
|
}
|
|
@@ -1985,230 +2434,19 @@ var MppSessionClient = class {
|
|
|
1985
2434
|
}
|
|
1986
2435
|
}
|
|
1987
2436
|
};
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
this.code = code;
|
|
1994
|
-
}
|
|
1995
|
-
};
|
|
1996
|
-
function sha256Hex(data) {
|
|
1997
|
-
const buf = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
1998
|
-
return crypto.createHash("sha256").update(buf).digest("hex");
|
|
1999
|
-
}
|
|
2000
|
-
function commitDailyCap(dailyCapStroops, witnessSecret) {
|
|
2001
|
-
return sha256Hex(`${witnessSecret}:cap:${dailyCapStroops.toString()}`);
|
|
2002
|
-
}
|
|
2003
|
-
function commitAllowlist(payees, witnessSecret) {
|
|
2004
|
-
const sorted = [...payees].sort().join(",");
|
|
2005
|
-
return sha256Hex(`${witnessSecret}:allowlist:${sorted}`);
|
|
2006
|
-
}
|
|
2007
|
-
function hashPayee(payee) {
|
|
2008
|
-
return sha256Hex(`payee:${payee}`);
|
|
2009
|
-
}
|
|
2010
|
-
function dayBucketFromLedger(ledgerSequence) {
|
|
2011
|
-
return Math.floor(ledgerSequence / 17280);
|
|
2012
|
-
}
|
|
2013
|
-
function authDigestFromEntry(authEntryBase64) {
|
|
2014
|
-
const entryHash = stellarSdk.hash(Buffer.from(authEntryBase64, "base64"));
|
|
2015
|
-
return sha256Hex(entryHash);
|
|
2016
|
-
}
|
|
2017
|
-
function assertAuthEntryMatchesContext(authEntry, from, ctx) {
|
|
2018
|
-
let preimage;
|
|
2437
|
+
function createNulthSigner(config) {
|
|
2438
|
+
const sharedConfig = { ...config, logger: resolveLogger(config.logger) };
|
|
2439
|
+
Object.defineProperty(sharedConfig, "paymentContext", {
|
|
2440
|
+
get: () => config.paymentContext
|
|
2441
|
+
});
|
|
2019
2442
|
try {
|
|
2020
|
-
|
|
2021
|
-
} catch {
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
if (preimage.switch() !== stellarSdk.xdr.EnvelopeType.envelopeTypeSorobanAuthorization()) {
|
|
2025
|
-
throw new NulthPolicyError(
|
|
2026
|
-
"auth_entry_mismatch",
|
|
2027
|
-
"authEntry is not a Soroban authorization preimage"
|
|
2028
|
-
);
|
|
2029
|
-
}
|
|
2030
|
-
const inv = preimage.sorobanAuthorization().invocation();
|
|
2031
|
-
if (inv.subInvocations().length !== 0) {
|
|
2032
|
-
throw new NulthPolicyError("auth_entry_mismatch", "authEntry has unexpected sub-invocations");
|
|
2033
|
-
}
|
|
2034
|
-
if (inv.function().switch() !== stellarSdk.xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn()) {
|
|
2035
|
-
throw new NulthPolicyError("auth_entry_mismatch", "authEntry does not invoke a contract function");
|
|
2036
|
-
}
|
|
2037
|
-
const fn = inv.function().contractFn();
|
|
2038
|
-
const contractAddress = stellarSdk.Address.fromScAddress(fn.contractAddress()).toString();
|
|
2039
|
-
if (contractAddress !== ctx.assetContract) {
|
|
2040
|
-
throw new NulthPolicyError(
|
|
2041
|
-
"auth_entry_mismatch",
|
|
2042
|
-
`authEntry contract ${contractAddress} does not match assetContract ${ctx.assetContract}`
|
|
2043
|
-
);
|
|
2044
|
-
}
|
|
2045
|
-
const functionName = fn.functionName().toString();
|
|
2046
|
-
if (functionName !== "transfer") {
|
|
2047
|
-
throw new NulthPolicyError(
|
|
2048
|
-
"auth_entry_mismatch",
|
|
2049
|
-
`authEntry function ${functionName} does not match expected transfer`
|
|
2050
|
-
);
|
|
2051
|
-
}
|
|
2052
|
-
const args = fn.args();
|
|
2053
|
-
if (args.length !== 3) {
|
|
2054
|
-
throw new NulthPolicyError("auth_entry_mismatch", `authEntry transfer has ${args.length} args, expected 3`);
|
|
2055
|
-
}
|
|
2056
|
-
const entryFrom = stellarSdk.scValToNative(args[0]);
|
|
2057
|
-
if (entryFrom !== from) {
|
|
2058
|
-
throw new NulthPolicyError(
|
|
2059
|
-
"auth_entry_mismatch",
|
|
2060
|
-
`authEntry from ${entryFrom} does not match nulthAccount ${from}`
|
|
2061
|
-
);
|
|
2062
|
-
}
|
|
2063
|
-
const entryTo = stellarSdk.scValToNative(args[1]);
|
|
2064
|
-
if (entryTo !== ctx.payee) {
|
|
2065
|
-
throw new NulthPolicyError(
|
|
2066
|
-
"auth_entry_mismatch",
|
|
2067
|
-
`authEntry to ${entryTo} does not match paymentContext.payee ${ctx.payee}`
|
|
2068
|
-
);
|
|
2069
|
-
}
|
|
2070
|
-
const entryAmount = stellarSdk.scValToNative(args[2]);
|
|
2071
|
-
if (entryAmount !== ctx.amountStroops) {
|
|
2072
|
-
throw new NulthPolicyError(
|
|
2073
|
-
"auth_entry_mismatch",
|
|
2074
|
-
`authEntry amount ${entryAmount} does not match paymentContext.amountStroops ${ctx.amountStroops}`
|
|
2075
|
-
);
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
function insecureMockProof(preimage) {
|
|
2079
|
-
return crypto.createHash("sha512").update(`nulth:${preimage}`).digest("base64");
|
|
2080
|
-
}
|
|
2081
|
-
function encodeAuthSignature(proof) {
|
|
2082
|
-
const payload = { nulth: "zk-v1", proof };
|
|
2083
|
-
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
2084
|
-
}
|
|
2085
|
-
var NulthClient = class {
|
|
2086
|
-
constructor(config) {
|
|
2087
|
-
this.config = config;
|
|
2088
|
-
this.policy = { ...config.policy };
|
|
2089
|
-
this.prover = config.prover ?? "mock";
|
|
2090
|
-
if (this.prover !== "mock") {
|
|
2091
|
-
throw new RouteDockManifestError(
|
|
2092
|
-
`NulthClient: unknown prover backend ${String(config.prover)}; only 'mock' is implemented`
|
|
2093
|
-
);
|
|
2094
|
-
}
|
|
2095
|
-
if (config.network !== "testnet") {
|
|
2096
|
-
throw new RouteDockManifestError(
|
|
2097
|
-
`NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
|
|
2098
|
-
);
|
|
2443
|
+
return nulthSdk.createNulthSigner(sharedConfig);
|
|
2444
|
+
} catch (error) {
|
|
2445
|
+
if (error instanceof nulthSdk.NulthConfigError) {
|
|
2446
|
+
throw new RouteDockManifestError(error.message, { cause: error });
|
|
2099
2447
|
}
|
|
2100
|
-
|
|
2101
|
-
"warn",
|
|
2102
|
-
"NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
|
|
2103
|
-
);
|
|
2448
|
+
throw error;
|
|
2104
2449
|
}
|
|
2105
|
-
config;
|
|
2106
|
-
policy;
|
|
2107
|
-
prover;
|
|
2108
|
-
get proverBackend() {
|
|
2109
|
-
return this.prover;
|
|
2110
|
-
}
|
|
2111
|
-
get nulthAccount() {
|
|
2112
|
-
return this.config.nulthAccount;
|
|
2113
|
-
}
|
|
2114
|
-
get allowlistCommitment() {
|
|
2115
|
-
return this.policy.allowlistCommitment;
|
|
2116
|
-
}
|
|
2117
|
-
get capCommitment() {
|
|
2118
|
-
return commitDailyCap(this.policy.dailyCapStroops, this.policy.witnessSecret);
|
|
2119
|
-
}
|
|
2120
|
-
buildPaymentAuthProof(context) {
|
|
2121
|
-
this.enforcePolicy(context);
|
|
2122
|
-
const bucket = dayBucketFromLedger(context.ledgerSequence);
|
|
2123
|
-
if (bucket !== this.policy.dayBucket) {
|
|
2124
|
-
this.policy.dayBucket = bucket;
|
|
2125
|
-
this.policy.dailySpendStroops = 0n;
|
|
2126
|
-
}
|
|
2127
|
-
const authDigest = authDigestFromEntry(context.authEntry);
|
|
2128
|
-
const capCommitment = this.capCommitment;
|
|
2129
|
-
const payeeHash = hashPayee(context.payee);
|
|
2130
|
-
const preimage = [
|
|
2131
|
-
authDigest,
|
|
2132
|
-
this.policy.allowlistCommitment,
|
|
2133
|
-
capCommitment,
|
|
2134
|
-
payeeHash,
|
|
2135
|
-
context.amountStroops.toString(),
|
|
2136
|
-
bucket.toString(),
|
|
2137
|
-
this.config.nulthAccount
|
|
2138
|
-
].join("|");
|
|
2139
|
-
const proof = {
|
|
2140
|
-
version: 1,
|
|
2141
|
-
proof: insecureMockProof(preimage),
|
|
2142
|
-
publicInputs: {
|
|
2143
|
-
authDigest,
|
|
2144
|
-
allowlistCommitment: this.policy.allowlistCommitment,
|
|
2145
|
-
capCommitment,
|
|
2146
|
-
payeeHash,
|
|
2147
|
-
amountStroops: context.amountStroops.toString(),
|
|
2148
|
-
dayBucket: bucket.toString()
|
|
2149
|
-
}
|
|
2150
|
-
};
|
|
2151
|
-
this.policy.dailySpendStroops += context.amountStroops;
|
|
2152
|
-
return proof;
|
|
2153
|
-
}
|
|
2154
|
-
enforcePolicy(context) {
|
|
2155
|
-
if (context.amountStroops < 0n) {
|
|
2156
|
-
throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
|
|
2157
|
-
}
|
|
2158
|
-
if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
|
|
2159
|
-
throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
|
|
2160
|
-
}
|
|
2161
|
-
if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
|
|
2162
|
-
throw new NulthPolicyError("session_expired", "Nulth session expired");
|
|
2163
|
-
}
|
|
2164
|
-
if (!this.policy.allowedPayees.includes(context.payee)) {
|
|
2165
|
-
throw new NulthPolicyError("payee_not_allowed", `Payee ${context.payee} not in off-chain allowlist`);
|
|
2166
|
-
}
|
|
2167
|
-
const bucket = dayBucketFromLedger(context.ledgerSequence);
|
|
2168
|
-
const spend = bucket === this.policy.dayBucket ? this.policy.dailySpendStroops : 0n;
|
|
2169
|
-
if (spend + context.amountStroops > this.policy.dailyCapStroops) {
|
|
2170
|
-
throw new NulthPolicyError("daily_cap_exceeded", "Nulth daily cap exceeded");
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
};
|
|
2174
|
-
function createNulthSigner(config) {
|
|
2175
|
-
const client = new NulthClient(config);
|
|
2176
|
-
const nulthAccount = config.nulthAccount;
|
|
2177
|
-
return {
|
|
2178
|
-
address: nulthAccount,
|
|
2179
|
-
signAuthEntry: async (authEntry) => {
|
|
2180
|
-
const ctx = config.paymentContext;
|
|
2181
|
-
if (!ctx) throw new Error("Nulth paymentContext must be set before signing");
|
|
2182
|
-
assertAuthEntryMatchesContext(authEntry, nulthAccount, ctx);
|
|
2183
|
-
const proof = client.buildPaymentAuthProof({ authEntry, ...ctx });
|
|
2184
|
-
return { signedAuthEntry: encodeAuthSignature(proof), signerAddress: nulthAccount };
|
|
2185
|
-
}
|
|
2186
|
-
};
|
|
2187
|
-
}
|
|
2188
|
-
function setNulthPaymentContext(config, context) {
|
|
2189
|
-
config.paymentContext = context;
|
|
2190
|
-
}
|
|
2191
|
-
function paymentContextFromManifest(manifest, mode, ledgerSequence) {
|
|
2192
|
-
const pricing = manifest.pricing[mode];
|
|
2193
|
-
if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
|
|
2194
|
-
return {
|
|
2195
|
-
payee: pricing.payee ?? manifest.payee,
|
|
2196
|
-
amountStroops: nulthSdk.usdcToStroops(pricing.amount),
|
|
2197
|
-
assetContract: manifest.asset_contract,
|
|
2198
|
-
ledgerSequence
|
|
2199
|
-
};
|
|
2200
|
-
}
|
|
2201
|
-
function createPolicyState(input) {
|
|
2202
|
-
const dailyCapStroops = nulthSdk.usdcToStroops(input.dailyCapUsdc);
|
|
2203
|
-
return {
|
|
2204
|
-
dailyCapStroops,
|
|
2205
|
-
allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
|
|
2206
|
-
allowedPayees: input.allowedPayees,
|
|
2207
|
-
dailySpendStroops: 0n,
|
|
2208
|
-
dayBucket: dayBucketFromLedger(input.ledgerSequence ?? 0),
|
|
2209
|
-
witnessSecret: input.witnessSecret,
|
|
2210
|
-
...input.expiryLedger !== void 0 ? { expiryLedger: input.expiryLedger } : {}
|
|
2211
|
-
};
|
|
2212
2450
|
}
|
|
2213
2451
|
function assertNulthVaultManifest(manifest, nulthAccount) {
|
|
2214
2452
|
if (manifest.vault !== void 0 && manifest.vault !== "nulth") {
|
|
@@ -2233,7 +2471,7 @@ async function prepareNulthSigner(vault, manifest, mode, network, ledgerSequence
|
|
|
2233
2471
|
);
|
|
2234
2472
|
}
|
|
2235
2473
|
const ledgerSequence = await fetchLedgerSequence(network);
|
|
2236
|
-
const policy = createPolicyState({
|
|
2474
|
+
const policy = nulthSdk.createPolicyState({
|
|
2237
2475
|
dailyCapUsdc: vault.dailyCapUsdc,
|
|
2238
2476
|
allowedPayees: vault.allowedPayees,
|
|
2239
2477
|
witnessSecret: vault.witnessSecret,
|
|
@@ -2248,7 +2486,7 @@ async function prepareNulthSigner(vault, manifest, mode, network, ledgerSequence
|
|
|
2248
2486
|
...vault.verifierContract !== void 0 ? { verifierContract: vault.verifierContract } : {}
|
|
2249
2487
|
};
|
|
2250
2488
|
if (mode === "x402" || mode === "mpp-charge") {
|
|
2251
|
-
setNulthPaymentContext(config, paymentContextFromManifest(manifest, mode, ledgerSequence));
|
|
2489
|
+
nulthSdk.setNulthPaymentContext(config, nulthSdk.paymentContextFromManifest(manifest, mode, ledgerSequence));
|
|
2252
2490
|
} else {
|
|
2253
2491
|
throw new RouteDockManifestError(`Nulth ZK vault does not support payment mode: ${mode}`);
|
|
2254
2492
|
}
|
|
@@ -2433,7 +2671,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2433
2671
|
}
|
|
2434
2672
|
const secretKey = this.keypair.secret();
|
|
2435
2673
|
const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
|
|
2436
|
-
this.x402 = new X402Client(secretKey, this.network, retryPolicy);
|
|
2674
|
+
this.x402 = new X402Client(secretKey, this.network, retryPolicy, config.preSignHook, this.logger);
|
|
2437
2675
|
this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
|
|
2438
2676
|
this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
|
|
2439
2677
|
}
|
|
@@ -2441,7 +2679,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2441
2679
|
async _resolveManifest(url, options) {
|
|
2442
2680
|
const baseUrl = new URL(url).origin;
|
|
2443
2681
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2444
|
-
assertEndpointActive(manifest, url, this.logger);
|
|
2682
|
+
assertEndpointActive(manifest, url, this.logger, options?.request?.method);
|
|
2445
2683
|
const mode = selectMode(manifest, options);
|
|
2446
2684
|
return { manifest, mode };
|
|
2447
2685
|
}
|
|
@@ -2458,13 +2696,9 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2458
2696
|
});
|
|
2459
2697
|
return next;
|
|
2460
2698
|
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
* call before committing to a payment — for approval gates and manual
|
|
2465
|
-
* trustline remediation.
|
|
2466
|
-
*/
|
|
2467
|
-
async preflight(manifest, mode, endpoint) {
|
|
2699
|
+
async preflight(target, mode, endpoint) {
|
|
2700
|
+
if (typeof target === "string") return this._preflightUrl(target);
|
|
2701
|
+
const manifest = target;
|
|
2468
2702
|
assertManifestValid(manifest);
|
|
2469
2703
|
const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
|
|
2470
2704
|
await this._checkTrustline(manifest, selectedAsset);
|
|
@@ -2477,6 +2711,42 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2477
2711
|
...manifest.capabilities && { capabilities: manifest.capabilities }
|
|
2478
2712
|
};
|
|
2479
2713
|
}
|
|
2714
|
+
/** Manifest-only preflight for a URL — no trustline check, no payment. */
|
|
2715
|
+
async _preflightUrl(url) {
|
|
2716
|
+
const baseUrl = new URL(url).origin;
|
|
2717
|
+
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2718
|
+
assertEndpointActive(manifest, url);
|
|
2719
|
+
const costs = manifest.modes.map((mode) => {
|
|
2720
|
+
const asset = selectAsset(manifest, mode, url).asset;
|
|
2721
|
+
switch (mode) {
|
|
2722
|
+
case "x402":
|
|
2723
|
+
case "mpp-charge": {
|
|
2724
|
+
const pricing = manifest.pricing[mode];
|
|
2725
|
+
return { mode, amount: pricing.amount, per: "request", asset };
|
|
2726
|
+
}
|
|
2727
|
+
case "mpp-session":
|
|
2728
|
+
case "mpp-session-ws": {
|
|
2729
|
+
const pricing = manifest.pricing[mode];
|
|
2730
|
+
return { mode, amount: pricing.rate, per: "voucher", asset, min_deposit: pricing.min_deposit };
|
|
2731
|
+
}
|
|
2732
|
+
default:
|
|
2733
|
+
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2734
|
+
}
|
|
2735
|
+
});
|
|
2736
|
+
const pathname = new URL(url).pathname;
|
|
2737
|
+
const matches = Object.values(manifest.endpoints).filter((descriptor) => descriptor.path === pathname);
|
|
2738
|
+
const endpointSunsets = matches.map((descriptor) => descriptor.sunset_at).filter((value) => typeof value === "string").sort((a, b) => Date.parse(a) - Date.parse(b));
|
|
2739
|
+
return {
|
|
2740
|
+
modes: manifest.modes,
|
|
2741
|
+
costs,
|
|
2742
|
+
compliance: {
|
|
2743
|
+
deprecated_modes: manifest.deprecated_modes ?? [],
|
|
2744
|
+
...manifest.sunset_at && { sunset_at: manifest.sunset_at },
|
|
2745
|
+
endpoint_deprecated: matches.some((descriptor) => descriptor.deprecated === true),
|
|
2746
|
+
...endpointSunsets[0] && { endpoint_sunset_at: endpointSunsets[0] }
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2480
2750
|
/**
|
|
2481
2751
|
* Verify via Horizon that the payer account has a trustline for the
|
|
2482
2752
|
* payment asset declared in the manifest. Throws RouteDockTrustlineError
|
|
@@ -2488,8 +2758,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2488
2758
|
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
|
|
2489
2759
|
const cached = _RouteDockClient._trustlineCache.get(cacheKey);
|
|
2490
2760
|
if (cached && Date.now() < cached.expiresAt) return;
|
|
2491
|
-
const
|
|
2492
|
-
const server = new stellarSdk.Horizon.Server(horizonUrl);
|
|
2761
|
+
const server = new stellarSdk.Horizon.Server(HORIZON_URLS[this.network]);
|
|
2493
2762
|
try {
|
|
2494
2763
|
const account = await server.loadAccount(this.keypair.publicKey());
|
|
2495
2764
|
const balances = account.balances;
|
|
@@ -2525,9 +2794,11 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2525
2794
|
async pay(url, options) {
|
|
2526
2795
|
const baseUrl = new URL(url).origin;
|
|
2527
2796
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2528
|
-
|
|
2797
|
+
const request = options?.request;
|
|
2798
|
+
assertEndpointActive(manifest, url, this.logger, request?.method);
|
|
2529
2799
|
const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
|
|
2530
2800
|
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2801
|
+
this._assertCapAsset(selectedAsset);
|
|
2531
2802
|
await this._checkTrustline(manifest, selectedAsset);
|
|
2532
2803
|
let amount;
|
|
2533
2804
|
switch (mode) {
|
|
@@ -2545,18 +2816,18 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2545
2816
|
default:
|
|
2546
2817
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2547
2818
|
}
|
|
2548
|
-
const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
|
|
2819
|
+
const reserveId = await this._checkAndReserveSpend(amount, baseUrl, selectedAsset);
|
|
2549
2820
|
let result;
|
|
2550
2821
|
try {
|
|
2551
2822
|
if (this.vault?.mode === "nulth") {
|
|
2552
|
-
result = await this._payWithNulthVault(url, manifest, mode);
|
|
2823
|
+
result = await this._payWithNulthVault(url, manifest, mode, request);
|
|
2553
2824
|
} else {
|
|
2554
2825
|
switch (mode) {
|
|
2555
2826
|
case "x402":
|
|
2556
|
-
result = await this.x402.pay(url, manifest);
|
|
2827
|
+
result = await this.x402.pay(url, manifest, request);
|
|
2557
2828
|
break;
|
|
2558
2829
|
case "mpp-charge":
|
|
2559
|
-
result = await this.charge.pay(url, manifest);
|
|
2830
|
+
result = await this.charge.pay(url, manifest, request);
|
|
2560
2831
|
break;
|
|
2561
2832
|
default:
|
|
2562
2833
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
@@ -2575,7 +2846,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2575
2846
|
return result;
|
|
2576
2847
|
}
|
|
2577
2848
|
/** Nulth ZK vault path — proof built off-chain, attached as auth signature */
|
|
2578
|
-
async _payWithNulthVault(url, manifest, mode) {
|
|
2849
|
+
async _payWithNulthVault(url, manifest, mode, request) {
|
|
2579
2850
|
const prover = this.vault?.prover ?? "mock";
|
|
2580
2851
|
if (this.network === "mainnet" && prover === "mock") {
|
|
2581
2852
|
throw new RouteDockManifestError("nulth vault uses a MOCK Groth16 prover and cannot be used on mainnet");
|
|
@@ -2588,10 +2859,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2588
2859
|
try {
|
|
2589
2860
|
const { signer } = await prepareNulthSigner(this.vault, manifest, mode, this.network);
|
|
2590
2861
|
const x402 = this.x402.withSigner(signer);
|
|
2591
|
-
const result = await x402.pay(url, manifest);
|
|
2862
|
+
const result = await x402.pay(url, manifest, request);
|
|
2592
2863
|
return result;
|
|
2593
2864
|
} catch (err) {
|
|
2594
|
-
if (err instanceof NulthPolicyError) {
|
|
2865
|
+
if (err instanceof nulthSdk.NulthPolicyError) {
|
|
2595
2866
|
throw new RouteDockPolicyRejectError(err.code);
|
|
2596
2867
|
}
|
|
2597
2868
|
throw err;
|
|
@@ -2657,17 +2928,35 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2657
2928
|
);
|
|
2658
2929
|
}
|
|
2659
2930
|
const endpointKey = new URL(url).origin;
|
|
2660
|
-
|
|
2931
|
+
let onSpend;
|
|
2932
|
+
if (this.spendCap) {
|
|
2933
|
+
const sessionAsset = selectAsset(manifest, mode, url).asset;
|
|
2934
|
+
this._assertCapAsset(sessionAsset);
|
|
2935
|
+
onSpend = (amount) => this._recordSessionSpend(amount, endpointKey, sessionAsset);
|
|
2936
|
+
}
|
|
2661
2937
|
return this.session.openSession(url, manifest, secret, options, onSpend);
|
|
2662
2938
|
}
|
|
2939
|
+
/**
|
|
2940
|
+
* The local cap accumulates one USDC total. Refuse to count any other asset
|
|
2941
|
+
* against it (or let it through uncounted) when a cap is configured.
|
|
2942
|
+
*/
|
|
2943
|
+
_assertCapAsset(asset) {
|
|
2944
|
+
if (this.spendCap && asset !== this.spendCap.asset) {
|
|
2945
|
+
throw new RouteDockPolicyRejectError("local_cap_asset_mismatch", {
|
|
2946
|
+
cause: new Error(
|
|
2947
|
+
`spendCap is denominated in ${this.spendCap.asset} but the selected payment asset is ${asset}`
|
|
2948
|
+
)
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2663
2952
|
/**
|
|
2664
2953
|
* Record spend for an MPP session voucher. Called before each voucher
|
|
2665
2954
|
* fetch in stream(). Enforces the same local daily and per-endpoint caps
|
|
2666
2955
|
* as discrete pay() calls. Returns a commit function to clean up the
|
|
2667
2956
|
* reservation after the voucher fetch succeeds.
|
|
2668
2957
|
*/
|
|
2669
|
-
async _recordSessionSpend(amount, endpointKey) {
|
|
2670
|
-
const reserveId = await this._checkAndReserveSpend(amount, endpointKey);
|
|
2958
|
+
async _recordSessionSpend(amount, endpointKey, asset) {
|
|
2959
|
+
const reserveId = await this._checkAndReserveSpend(amount, endpointKey, asset);
|
|
2671
2960
|
if (reserveId) {
|
|
2672
2961
|
await this._commitSpend(reserveId);
|
|
2673
2962
|
}
|
|
@@ -2706,15 +2995,16 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2706
2995
|
* Both limits are independent: a payment is only reserved after **both** pass.
|
|
2707
2996
|
* All spend (regardless of endpoint) counts toward the global accumulator.
|
|
2708
2997
|
*/
|
|
2709
|
-
async _checkAndReserveSpend(amount, endpointKey) {
|
|
2998
|
+
async _checkAndReserveSpend(amount, endpointKey, asset) {
|
|
2710
2999
|
if (!this.spendCap) return "";
|
|
2711
|
-
|
|
3000
|
+
this._assertCapAsset(asset);
|
|
3001
|
+
const spendCap = this.spendCap;
|
|
3002
|
+
const amountMicros = nulthSdk.usdcToStroops(amount);
|
|
3003
|
+
const reserve = (prev) => {
|
|
2712
3004
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2713
|
-
const
|
|
2714
|
-
const current = persisted && persisted.date === today ? persisted : { date: today, totalMicros: "0", endpoints: {} };
|
|
2715
|
-
const amountMicros = nulthSdk.usdcToStroops(amount);
|
|
3005
|
+
const current = prev && prev.date === today ? { ...prev, endpoints: { ...prev.endpoints } } : { date: today, totalMicros: "0", endpoints: {} };
|
|
2716
3006
|
const total = BigInt(current.totalMicros);
|
|
2717
|
-
const endpointCapStr =
|
|
3007
|
+
const endpointCapStr = spendCap.endpointCaps?.[endpointKey];
|
|
2718
3008
|
if (endpointCapStr !== void 0) {
|
|
2719
3009
|
const endpointCapMicros = nulthSdk.usdcToStroops(endpointCapStr);
|
|
2720
3010
|
const endpointTotal = BigInt(current.endpoints[endpointKey] ?? "0");
|
|
@@ -2722,17 +3012,25 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2722
3012
|
throw new RouteDockPolicyRejectError("local_endpoint_cap_exceeded");
|
|
2723
3013
|
}
|
|
2724
3014
|
}
|
|
2725
|
-
const globalCapMicros = nulthSdk.usdcToStroops(
|
|
3015
|
+
const globalCapMicros = nulthSdk.usdcToStroops(spendCap.daily);
|
|
2726
3016
|
if (total + amountMicros > globalCapMicros) {
|
|
2727
3017
|
throw new RouteDockPolicyRejectError("local_daily_cap_exceeded");
|
|
2728
3018
|
}
|
|
2729
|
-
const reserveId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2730
|
-
this._pendingReserves.set(reserveId, { endpointKey, amountMicros });
|
|
2731
3019
|
current.totalMicros = (total + amountMicros).toString();
|
|
2732
3020
|
if (endpointCapStr !== void 0) {
|
|
2733
3021
|
current.endpoints[endpointKey] = (BigInt(current.endpoints[endpointKey] ?? "0") + amountMicros).toString();
|
|
2734
3022
|
}
|
|
2735
|
-
|
|
3023
|
+
return current;
|
|
3024
|
+
};
|
|
3025
|
+
return this._withSpendMutex(async () => {
|
|
3026
|
+
const store = this.spendStore;
|
|
3027
|
+
if (store.update) {
|
|
3028
|
+
await store.update(reserve);
|
|
3029
|
+
} else {
|
|
3030
|
+
await store.write(reserve(await store.read()));
|
|
3031
|
+
}
|
|
3032
|
+
const reserveId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3033
|
+
this._pendingReserves.set(reserveId, { endpointKey, amountMicros });
|
|
2736
3034
|
return reserveId;
|
|
2737
3035
|
});
|
|
2738
3036
|
}
|
|
@@ -2754,22 +3052,29 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2754
3052
|
if (!reserve) return;
|
|
2755
3053
|
this._pendingReserves.delete(reserveId);
|
|
2756
3054
|
const { endpointKey, amountMicros } = reserve;
|
|
2757
|
-
|
|
3055
|
+
const release = (prev) => {
|
|
2758
3056
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
persisted.totalMicros = (currentTotal - amountMicros).toString();
|
|
3057
|
+
if (!prev || prev.date !== today) return void 0;
|
|
3058
|
+
const persisted = { ...prev, endpoints: { ...prev.endpoints } };
|
|
3059
|
+
persisted.totalMicros = (BigInt(persisted.totalMicros) - amountMicros).toString();
|
|
2763
3060
|
if (persisted.endpoints[endpointKey]) {
|
|
2764
|
-
const
|
|
2765
|
-
const newEndpointTotal = endpointTotal - amountMicros;
|
|
3061
|
+
const newEndpointTotal = BigInt(persisted.endpoints[endpointKey]) - amountMicros;
|
|
2766
3062
|
if (newEndpointTotal <= 0n) {
|
|
2767
3063
|
delete persisted.endpoints[endpointKey];
|
|
2768
3064
|
} else {
|
|
2769
3065
|
persisted.endpoints[endpointKey] = newEndpointTotal.toString();
|
|
2770
3066
|
}
|
|
2771
3067
|
}
|
|
2772
|
-
|
|
3068
|
+
return persisted;
|
|
3069
|
+
};
|
|
3070
|
+
await this._withSpendMutex(async () => {
|
|
3071
|
+
const store = this.spendStore;
|
|
3072
|
+
if (store.update) {
|
|
3073
|
+
await store.update(release);
|
|
3074
|
+
} else {
|
|
3075
|
+
const next = release(await store.read());
|
|
3076
|
+
if (next) await store.write(next);
|
|
3077
|
+
}
|
|
2773
3078
|
});
|
|
2774
3079
|
}
|
|
2775
3080
|
};
|