@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/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,235 +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
|
-
function usdcToStroops2(amount) {
|
|
2086
|
-
const [whole = "0", frac = ""] = amount.split(".");
|
|
2087
|
-
const padded = (frac + "0000000").slice(0, 7);
|
|
2088
|
-
return BigInt(whole) * 10000000n + BigInt(padded);
|
|
2089
|
-
}
|
|
2090
|
-
var NulthClient = class {
|
|
2091
|
-
constructor(config) {
|
|
2092
|
-
this.config = config;
|
|
2093
|
-
this.policy = { ...config.policy };
|
|
2094
|
-
this.prover = config.prover ?? "mock";
|
|
2095
|
-
if (this.prover !== "mock") {
|
|
2096
|
-
throw new RouteDockManifestError(
|
|
2097
|
-
`NulthClient: unknown prover backend ${String(config.prover)}; only 'mock' is implemented`
|
|
2098
|
-
);
|
|
2099
|
-
}
|
|
2100
|
-
if (config.network !== "testnet") {
|
|
2101
|
-
throw new RouteDockManifestError(
|
|
2102
|
-
`NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
|
|
2103
|
-
);
|
|
2104
|
-
}
|
|
2105
|
-
resolveLogger(config.logger)(
|
|
2106
|
-
"warn",
|
|
2107
|
-
"NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
|
|
2108
|
-
);
|
|
2109
|
-
}
|
|
2110
|
-
config;
|
|
2111
|
-
policy;
|
|
2112
|
-
prover;
|
|
2113
|
-
get proverBackend() {
|
|
2114
|
-
return this.prover;
|
|
2115
|
-
}
|
|
2116
|
-
get nulthAccount() {
|
|
2117
|
-
return this.config.nulthAccount;
|
|
2118
|
-
}
|
|
2119
|
-
get allowlistCommitment() {
|
|
2120
|
-
return this.policy.allowlistCommitment;
|
|
2121
|
-
}
|
|
2122
|
-
get capCommitment() {
|
|
2123
|
-
return commitDailyCap(this.policy.dailyCapStroops, this.policy.witnessSecret);
|
|
2124
|
-
}
|
|
2125
|
-
buildPaymentAuthProof(context) {
|
|
2126
|
-
this.enforcePolicy(context);
|
|
2127
|
-
const bucket = dayBucketFromLedger(context.ledgerSequence);
|
|
2128
|
-
if (bucket !== this.policy.dayBucket) {
|
|
2129
|
-
this.policy.dayBucket = bucket;
|
|
2130
|
-
this.policy.dailySpendStroops = 0n;
|
|
2131
|
-
}
|
|
2132
|
-
const authDigest = authDigestFromEntry(context.authEntry);
|
|
2133
|
-
const capCommitment = this.capCommitment;
|
|
2134
|
-
const payeeHash = hashPayee(context.payee);
|
|
2135
|
-
const preimage = [
|
|
2136
|
-
authDigest,
|
|
2137
|
-
this.policy.allowlistCommitment,
|
|
2138
|
-
capCommitment,
|
|
2139
|
-
payeeHash,
|
|
2140
|
-
context.amountStroops.toString(),
|
|
2141
|
-
bucket.toString(),
|
|
2142
|
-
this.config.nulthAccount
|
|
2143
|
-
].join("|");
|
|
2144
|
-
const proof = {
|
|
2145
|
-
version: 1,
|
|
2146
|
-
proof: insecureMockProof(preimage),
|
|
2147
|
-
publicInputs: {
|
|
2148
|
-
authDigest,
|
|
2149
|
-
allowlistCommitment: this.policy.allowlistCommitment,
|
|
2150
|
-
capCommitment,
|
|
2151
|
-
payeeHash,
|
|
2152
|
-
amountStroops: context.amountStroops.toString(),
|
|
2153
|
-
dayBucket: bucket.toString()
|
|
2154
|
-
}
|
|
2155
|
-
};
|
|
2156
|
-
this.policy.dailySpendStroops += context.amountStroops;
|
|
2157
|
-
return proof;
|
|
2158
|
-
}
|
|
2159
|
-
enforcePolicy(context) {
|
|
2160
|
-
if (context.amountStroops < 0n) {
|
|
2161
|
-
throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
|
|
2162
|
-
}
|
|
2163
|
-
if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
|
|
2164
|
-
throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
|
|
2165
|
-
}
|
|
2166
|
-
if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
|
|
2167
|
-
throw new NulthPolicyError("session_expired", "Nulth session expired");
|
|
2168
|
-
}
|
|
2169
|
-
if (!this.policy.allowedPayees.includes(context.payee)) {
|
|
2170
|
-
throw new NulthPolicyError("payee_not_allowed", `Payee ${context.payee} not in off-chain allowlist`);
|
|
2171
|
-
}
|
|
2172
|
-
const bucket = dayBucketFromLedger(context.ledgerSequence);
|
|
2173
|
-
const spend = bucket === this.policy.dayBucket ? this.policy.dailySpendStroops : 0n;
|
|
2174
|
-
if (spend + context.amountStroops > this.policy.dailyCapStroops) {
|
|
2175
|
-
throw new NulthPolicyError("daily_cap_exceeded", "Nulth daily cap exceeded");
|
|
2443
|
+
return nulthSdk.createNulthSigner(sharedConfig);
|
|
2444
|
+
} catch (error) {
|
|
2445
|
+
if (error instanceof nulthSdk.NulthConfigError) {
|
|
2446
|
+
throw new RouteDockManifestError(error.message, { cause: error });
|
|
2176
2447
|
}
|
|
2448
|
+
throw error;
|
|
2177
2449
|
}
|
|
2178
|
-
};
|
|
2179
|
-
function createNulthSigner(config) {
|
|
2180
|
-
const client = new NulthClient(config);
|
|
2181
|
-
const nulthAccount = config.nulthAccount;
|
|
2182
|
-
return {
|
|
2183
|
-
address: nulthAccount,
|
|
2184
|
-
signAuthEntry: async (authEntry) => {
|
|
2185
|
-
const ctx = config.paymentContext;
|
|
2186
|
-
if (!ctx) throw new Error("Nulth paymentContext must be set before signing");
|
|
2187
|
-
assertAuthEntryMatchesContext(authEntry, nulthAccount, ctx);
|
|
2188
|
-
const proof = client.buildPaymentAuthProof({ authEntry, ...ctx });
|
|
2189
|
-
return { signedAuthEntry: encodeAuthSignature(proof), signerAddress: nulthAccount };
|
|
2190
|
-
}
|
|
2191
|
-
};
|
|
2192
|
-
}
|
|
2193
|
-
function setNulthPaymentContext(config, context) {
|
|
2194
|
-
config.paymentContext = context;
|
|
2195
|
-
}
|
|
2196
|
-
function paymentContextFromManifest(manifest, mode, ledgerSequence) {
|
|
2197
|
-
const pricing = manifest.pricing[mode];
|
|
2198
|
-
if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
|
|
2199
|
-
return {
|
|
2200
|
-
payee: manifest.payee,
|
|
2201
|
-
amountStroops: usdcToStroops2(pricing.amount),
|
|
2202
|
-
assetContract: manifest.asset_contract,
|
|
2203
|
-
ledgerSequence
|
|
2204
|
-
};
|
|
2205
|
-
}
|
|
2206
|
-
function createPolicyState(input) {
|
|
2207
|
-
const dailyCapStroops = usdcToStroops2(input.dailyCapUsdc);
|
|
2208
|
-
return {
|
|
2209
|
-
dailyCapStroops,
|
|
2210
|
-
allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
|
|
2211
|
-
allowedPayees: input.allowedPayees,
|
|
2212
|
-
dailySpendStroops: 0n,
|
|
2213
|
-
dayBucket: dayBucketFromLedger(input.ledgerSequence ?? 0),
|
|
2214
|
-
witnessSecret: input.witnessSecret,
|
|
2215
|
-
...input.expiryLedger !== void 0 ? { expiryLedger: input.expiryLedger } : {}
|
|
2216
|
-
};
|
|
2217
2450
|
}
|
|
2218
2451
|
function assertNulthVaultManifest(manifest, nulthAccount) {
|
|
2219
2452
|
if (manifest.vault !== void 0 && manifest.vault !== "nulth") {
|
|
@@ -2238,7 +2471,7 @@ async function prepareNulthSigner(vault, manifest, mode, network, ledgerSequence
|
|
|
2238
2471
|
);
|
|
2239
2472
|
}
|
|
2240
2473
|
const ledgerSequence = await fetchLedgerSequence(network);
|
|
2241
|
-
const policy = createPolicyState({
|
|
2474
|
+
const policy = nulthSdk.createPolicyState({
|
|
2242
2475
|
dailyCapUsdc: vault.dailyCapUsdc,
|
|
2243
2476
|
allowedPayees: vault.allowedPayees,
|
|
2244
2477
|
witnessSecret: vault.witnessSecret,
|
|
@@ -2253,7 +2486,7 @@ async function prepareNulthSigner(vault, manifest, mode, network, ledgerSequence
|
|
|
2253
2486
|
...vault.verifierContract !== void 0 ? { verifierContract: vault.verifierContract } : {}
|
|
2254
2487
|
};
|
|
2255
2488
|
if (mode === "x402" || mode === "mpp-charge") {
|
|
2256
|
-
setNulthPaymentContext(config, paymentContextFromManifest(manifest, mode, ledgerSequence));
|
|
2489
|
+
nulthSdk.setNulthPaymentContext(config, nulthSdk.paymentContextFromManifest(manifest, mode, ledgerSequence));
|
|
2257
2490
|
} else {
|
|
2258
2491
|
throw new RouteDockManifestError(`Nulth ZK vault does not support payment mode: ${mode}`);
|
|
2259
2492
|
}
|
|
@@ -2438,7 +2671,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2438
2671
|
}
|
|
2439
2672
|
const secretKey = this.keypair.secret();
|
|
2440
2673
|
const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
|
|
2441
|
-
this.x402 = new X402Client(secretKey, this.network, retryPolicy);
|
|
2674
|
+
this.x402 = new X402Client(secretKey, this.network, retryPolicy, config.preSignHook, this.logger);
|
|
2442
2675
|
this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
|
|
2443
2676
|
this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
|
|
2444
2677
|
}
|
|
@@ -2446,7 +2679,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2446
2679
|
async _resolveManifest(url, options) {
|
|
2447
2680
|
const baseUrl = new URL(url).origin;
|
|
2448
2681
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2449
|
-
assertEndpointActive(manifest, url, this.logger);
|
|
2682
|
+
assertEndpointActive(manifest, url, this.logger, options?.request?.method);
|
|
2450
2683
|
const mode = selectMode(manifest, options);
|
|
2451
2684
|
return { manifest, mode };
|
|
2452
2685
|
}
|
|
@@ -2463,13 +2696,9 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2463
2696
|
});
|
|
2464
2697
|
return next;
|
|
2465
2698
|
}
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
* call before committing to a payment — for approval gates and manual
|
|
2470
|
-
* trustline remediation.
|
|
2471
|
-
*/
|
|
2472
|
-
async preflight(manifest, mode, endpoint) {
|
|
2699
|
+
async preflight(target, mode, endpoint) {
|
|
2700
|
+
if (typeof target === "string") return this._preflightUrl(target);
|
|
2701
|
+
const manifest = target;
|
|
2473
2702
|
assertManifestValid(manifest);
|
|
2474
2703
|
const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
|
|
2475
2704
|
await this._checkTrustline(manifest, selectedAsset);
|
|
@@ -2482,6 +2711,42 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2482
2711
|
...manifest.capabilities && { capabilities: manifest.capabilities }
|
|
2483
2712
|
};
|
|
2484
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
|
+
}
|
|
2485
2750
|
/**
|
|
2486
2751
|
* Verify via Horizon that the payer account has a trustline for the
|
|
2487
2752
|
* payment asset declared in the manifest. Throws RouteDockTrustlineError
|
|
@@ -2493,8 +2758,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2493
2758
|
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
|
|
2494
2759
|
const cached = _RouteDockClient._trustlineCache.get(cacheKey);
|
|
2495
2760
|
if (cached && Date.now() < cached.expiresAt) return;
|
|
2496
|
-
const
|
|
2497
|
-
const server = new stellarSdk.Horizon.Server(horizonUrl);
|
|
2761
|
+
const server = new stellarSdk.Horizon.Server(HORIZON_URLS[this.network]);
|
|
2498
2762
|
try {
|
|
2499
2763
|
const account = await server.loadAccount(this.keypair.publicKey());
|
|
2500
2764
|
const balances = account.balances;
|
|
@@ -2530,9 +2794,11 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2530
2794
|
async pay(url, options) {
|
|
2531
2795
|
const baseUrl = new URL(url).origin;
|
|
2532
2796
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2533
|
-
|
|
2797
|
+
const request = options?.request;
|
|
2798
|
+
assertEndpointActive(manifest, url, this.logger, request?.method);
|
|
2534
2799
|
const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
|
|
2535
2800
|
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2801
|
+
this._assertCapAsset(selectedAsset);
|
|
2536
2802
|
await this._checkTrustline(manifest, selectedAsset);
|
|
2537
2803
|
let amount;
|
|
2538
2804
|
switch (mode) {
|
|
@@ -2550,18 +2816,18 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2550
2816
|
default:
|
|
2551
2817
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2552
2818
|
}
|
|
2553
|
-
const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
|
|
2819
|
+
const reserveId = await this._checkAndReserveSpend(amount, baseUrl, selectedAsset);
|
|
2554
2820
|
let result;
|
|
2555
2821
|
try {
|
|
2556
2822
|
if (this.vault?.mode === "nulth") {
|
|
2557
|
-
result = await this._payWithNulthVault(url, manifest, mode);
|
|
2823
|
+
result = await this._payWithNulthVault(url, manifest, mode, request);
|
|
2558
2824
|
} else {
|
|
2559
2825
|
switch (mode) {
|
|
2560
2826
|
case "x402":
|
|
2561
|
-
result = await this.x402.pay(url, manifest);
|
|
2827
|
+
result = await this.x402.pay(url, manifest, request);
|
|
2562
2828
|
break;
|
|
2563
2829
|
case "mpp-charge":
|
|
2564
|
-
result = await this.charge.pay(url, manifest);
|
|
2830
|
+
result = await this.charge.pay(url, manifest, request);
|
|
2565
2831
|
break;
|
|
2566
2832
|
default:
|
|
2567
2833
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
@@ -2580,7 +2846,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2580
2846
|
return result;
|
|
2581
2847
|
}
|
|
2582
2848
|
/** Nulth ZK vault path — proof built off-chain, attached as auth signature */
|
|
2583
|
-
async _payWithNulthVault(url, manifest, mode) {
|
|
2849
|
+
async _payWithNulthVault(url, manifest, mode, request) {
|
|
2584
2850
|
const prover = this.vault?.prover ?? "mock";
|
|
2585
2851
|
if (this.network === "mainnet" && prover === "mock") {
|
|
2586
2852
|
throw new RouteDockManifestError("nulth vault uses a MOCK Groth16 prover and cannot be used on mainnet");
|
|
@@ -2593,10 +2859,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2593
2859
|
try {
|
|
2594
2860
|
const { signer } = await prepareNulthSigner(this.vault, manifest, mode, this.network);
|
|
2595
2861
|
const x402 = this.x402.withSigner(signer);
|
|
2596
|
-
const result = await x402.pay(url, manifest);
|
|
2862
|
+
const result = await x402.pay(url, manifest, request);
|
|
2597
2863
|
return result;
|
|
2598
2864
|
} catch (err) {
|
|
2599
|
-
if (err instanceof NulthPolicyError) {
|
|
2865
|
+
if (err instanceof nulthSdk.NulthPolicyError) {
|
|
2600
2866
|
throw new RouteDockPolicyRejectError(err.code);
|
|
2601
2867
|
}
|
|
2602
2868
|
throw err;
|
|
@@ -2662,17 +2928,35 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2662
2928
|
);
|
|
2663
2929
|
}
|
|
2664
2930
|
const endpointKey = new URL(url).origin;
|
|
2665
|
-
|
|
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
|
+
}
|
|
2666
2937
|
return this.session.openSession(url, manifest, secret, options, onSpend);
|
|
2667
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
|
+
}
|
|
2668
2952
|
/**
|
|
2669
2953
|
* Record spend for an MPP session voucher. Called before each voucher
|
|
2670
2954
|
* fetch in stream(). Enforces the same local daily and per-endpoint caps
|
|
2671
2955
|
* as discrete pay() calls. Returns a commit function to clean up the
|
|
2672
2956
|
* reservation after the voucher fetch succeeds.
|
|
2673
2957
|
*/
|
|
2674
|
-
async _recordSessionSpend(amount, endpointKey) {
|
|
2675
|
-
const reserveId = await this._checkAndReserveSpend(amount, endpointKey);
|
|
2958
|
+
async _recordSessionSpend(amount, endpointKey, asset) {
|
|
2959
|
+
const reserveId = await this._checkAndReserveSpend(amount, endpointKey, asset);
|
|
2676
2960
|
if (reserveId) {
|
|
2677
2961
|
await this._commitSpend(reserveId);
|
|
2678
2962
|
}
|
|
@@ -2711,15 +2995,16 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2711
2995
|
* Both limits are independent: a payment is only reserved after **both** pass.
|
|
2712
2996
|
* All spend (regardless of endpoint) counts toward the global accumulator.
|
|
2713
2997
|
*/
|
|
2714
|
-
async _checkAndReserveSpend(amount, endpointKey) {
|
|
2998
|
+
async _checkAndReserveSpend(amount, endpointKey, asset) {
|
|
2715
2999
|
if (!this.spendCap) return "";
|
|
2716
|
-
|
|
3000
|
+
this._assertCapAsset(asset);
|
|
3001
|
+
const spendCap = this.spendCap;
|
|
3002
|
+
const amountMicros = nulthSdk.usdcToStroops(amount);
|
|
3003
|
+
const reserve = (prev) => {
|
|
2717
3004
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2718
|
-
const
|
|
2719
|
-
const current = persisted && persisted.date === today ? persisted : { date: today, totalMicros: "0", endpoints: {} };
|
|
2720
|
-
const amountMicros = nulthSdk.usdcToStroops(amount);
|
|
3005
|
+
const current = prev && prev.date === today ? { ...prev, endpoints: { ...prev.endpoints } } : { date: today, totalMicros: "0", endpoints: {} };
|
|
2721
3006
|
const total = BigInt(current.totalMicros);
|
|
2722
|
-
const endpointCapStr =
|
|
3007
|
+
const endpointCapStr = spendCap.endpointCaps?.[endpointKey];
|
|
2723
3008
|
if (endpointCapStr !== void 0) {
|
|
2724
3009
|
const endpointCapMicros = nulthSdk.usdcToStroops(endpointCapStr);
|
|
2725
3010
|
const endpointTotal = BigInt(current.endpoints[endpointKey] ?? "0");
|
|
@@ -2727,17 +3012,25 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2727
3012
|
throw new RouteDockPolicyRejectError("local_endpoint_cap_exceeded");
|
|
2728
3013
|
}
|
|
2729
3014
|
}
|
|
2730
|
-
const globalCapMicros = nulthSdk.usdcToStroops(
|
|
3015
|
+
const globalCapMicros = nulthSdk.usdcToStroops(spendCap.daily);
|
|
2731
3016
|
if (total + amountMicros > globalCapMicros) {
|
|
2732
3017
|
throw new RouteDockPolicyRejectError("local_daily_cap_exceeded");
|
|
2733
3018
|
}
|
|
2734
|
-
const reserveId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2735
|
-
this._pendingReserves.set(reserveId, { endpointKey, amountMicros });
|
|
2736
3019
|
current.totalMicros = (total + amountMicros).toString();
|
|
2737
3020
|
if (endpointCapStr !== void 0) {
|
|
2738
3021
|
current.endpoints[endpointKey] = (BigInt(current.endpoints[endpointKey] ?? "0") + amountMicros).toString();
|
|
2739
3022
|
}
|
|
2740
|
-
|
|
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 });
|
|
2741
3034
|
return reserveId;
|
|
2742
3035
|
});
|
|
2743
3036
|
}
|
|
@@ -2759,22 +3052,29 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2759
3052
|
if (!reserve) return;
|
|
2760
3053
|
this._pendingReserves.delete(reserveId);
|
|
2761
3054
|
const { endpointKey, amountMicros } = reserve;
|
|
2762
|
-
|
|
3055
|
+
const release = (prev) => {
|
|
2763
3056
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
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();
|
|
2768
3060
|
if (persisted.endpoints[endpointKey]) {
|
|
2769
|
-
const
|
|
2770
|
-
const newEndpointTotal = endpointTotal - amountMicros;
|
|
3061
|
+
const newEndpointTotal = BigInt(persisted.endpoints[endpointKey]) - amountMicros;
|
|
2771
3062
|
if (newEndpointTotal <= 0n) {
|
|
2772
3063
|
delete persisted.endpoints[endpointKey];
|
|
2773
3064
|
} else {
|
|
2774
3065
|
persisted.endpoints[endpointKey] = newEndpointTotal.toString();
|
|
2775
3066
|
}
|
|
2776
3067
|
}
|
|
2777
|
-
|
|
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
|
+
}
|
|
2778
3078
|
});
|
|
2779
3079
|
}
|
|
2780
3080
|
};
|