@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/client.js
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import { usdcToStroops, stroopsToUsdc } from '@routedock/nulth-sdk';
|
|
2
|
-
export { USDC_DECIMALS, usdcToStroops } from '@routedock/nulth-sdk';
|
|
3
|
-
import { Keypair, rpc, Horizon, Networks,
|
|
1
|
+
import { usdcToStroops, nulthAuthorizeEntry, isNulthStellarSigner, stroopsToUsdc, createNulthSigner as createNulthSigner$1, NulthConfigError, createPolicyState, setNulthPaymentContext, paymentContextFromManifest, NulthPolicyError } from '@routedock/nulth-sdk';
|
|
2
|
+
export { NulthPolicyError, USDC_DECIMALS, createPolicyState, decodeAuthSignature, paymentContextFromManifest, setNulthPaymentContext, usdcToStroops } from '@routedock/nulth-sdk';
|
|
3
|
+
import { Keypair, rpc, Horizon, Networks, contract, nativeToScVal } from '@stellar/stellar-sdk';
|
|
4
|
+
import { sha256 as sha256$1 } from '@noble/hashes/sha2.js';
|
|
4
5
|
import { Validator } from '@cfworker/json-schema';
|
|
5
|
-
import {
|
|
6
|
-
import { createEd25519Signer } from '@x402/stellar';
|
|
7
|
-
import { ExactStellarScheme } from '@x402/stellar/exact/client';
|
|
6
|
+
import { getRpcUrl, getNetworkPassphrase, handleSimulationResult, isStellarNetwork, validateStellarDestinationAddress, validateStellarAssetAddress, ExactStellarScheme, createEd25519Signer, getRpcClient, getEstimatedLedgerCloseTimeSeconds } from '@x402/stellar';
|
|
8
7
|
import { x402Client, x402HTTPClient } from '@x402/core/client';
|
|
9
8
|
import { decodePaymentRequiredHeader, decodePaymentResponseHeader } from '@x402/core/http';
|
|
10
9
|
import { stellar } from '@stellar/mpp/charge/client';
|
|
11
10
|
import { Mppx } from 'mppx/client';
|
|
11
|
+
import { Receipt, Challenge } from 'mppx';
|
|
12
12
|
import { STELLAR_PUBNET, STELLAR_TESTNET } from '@stellar/mpp';
|
|
13
13
|
import { stellar as stellar$1 } from '@stellar/mpp/channel/client';
|
|
14
|
-
import { Challenge } from 'mppx';
|
|
15
|
-
import fs from 'fs/promises';
|
|
16
|
-
import path from 'path';
|
|
17
14
|
|
|
18
15
|
// src/errors.ts
|
|
19
16
|
var RouteDockError = class extends Error {
|
|
@@ -111,6 +108,20 @@ var RouteDockPolicyRejectError = class extends RouteDockError {
|
|
|
111
108
|
this.reason = reason;
|
|
112
109
|
}
|
|
113
110
|
};
|
|
111
|
+
var RouteDockPaymentRejectedError = class extends RouteDockError {
|
|
112
|
+
status;
|
|
113
|
+
/** The provider's `reason` field, if its response body was JSON and had one. */
|
|
114
|
+
reason;
|
|
115
|
+
/** The provider's `error` field (short summary), if present. */
|
|
116
|
+
providerError;
|
|
117
|
+
constructor(message, status, details = {}, options = {}) {
|
|
118
|
+
super(message, "PAYMENT_REJECTED", false, options);
|
|
119
|
+
this.name = "RouteDockPaymentRejectedError";
|
|
120
|
+
this.status = status;
|
|
121
|
+
if (details.reason !== void 0) this.reason = details.reason;
|
|
122
|
+
if (details.providerError !== void 0) this.providerError = details.providerError;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
114
125
|
var RouteDockChannelStateError = class extends RouteDockError {
|
|
115
126
|
constructor(message, options = {}) {
|
|
116
127
|
super(message, "CHANNEL_STATE", false, options);
|
|
@@ -155,6 +166,59 @@ function httpStatusToError(message, status, response) {
|
|
|
155
166
|
}
|
|
156
167
|
return new RouteDockNetworkError(message, opts);
|
|
157
168
|
}
|
|
169
|
+
var MAX_REJECTION_BODY_BYTES = 64 * 1024;
|
|
170
|
+
var MAX_REJECTION_FIELD_CHARS = 500;
|
|
171
|
+
async function readBoundedText(response, limit) {
|
|
172
|
+
const body = response.body;
|
|
173
|
+
if (!body) return null;
|
|
174
|
+
const reader = body.getReader();
|
|
175
|
+
const chunks = [];
|
|
176
|
+
let total = 0;
|
|
177
|
+
try {
|
|
178
|
+
for (; ; ) {
|
|
179
|
+
const { done, value } = await reader.read();
|
|
180
|
+
if (done) break;
|
|
181
|
+
total += value.byteLength;
|
|
182
|
+
if (total > limit) {
|
|
183
|
+
await reader.cancel().catch(() => void 0);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
chunks.push(value);
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const bytes = new Uint8Array(total);
|
|
192
|
+
let offset = 0;
|
|
193
|
+
for (const chunk of chunks) {
|
|
194
|
+
bytes.set(chunk, offset);
|
|
195
|
+
offset += chunk.byteLength;
|
|
196
|
+
}
|
|
197
|
+
return new TextDecoder().decode(bytes);
|
|
198
|
+
}
|
|
199
|
+
async function paymentRejectedError(label, status, response) {
|
|
200
|
+
let reason;
|
|
201
|
+
let providerError;
|
|
202
|
+
try {
|
|
203
|
+
const text = await readBoundedText(response, MAX_REJECTION_BODY_BYTES);
|
|
204
|
+
const body = text === null ? null : JSON.parse(text);
|
|
205
|
+
if (body !== null && typeof body === "object") {
|
|
206
|
+
const { reason: r, error: e } = body;
|
|
207
|
+
if (typeof r === "string" && r.length > 0) reason = r.slice(0, MAX_REJECTION_FIELD_CHARS);
|
|
208
|
+
if (typeof e === "string" && e.length > 0) providerError = e.slice(0, MAX_REJECTION_FIELD_CHARS);
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
const detail = reason ?? providerError;
|
|
213
|
+
return new RouteDockPaymentRejectedError(
|
|
214
|
+
`${label}: HTTP ${status}${detail ? ` \u2014 ${detail}` : ""}`,
|
|
215
|
+
status,
|
|
216
|
+
{
|
|
217
|
+
...reason !== void 0 ? { reason } : {},
|
|
218
|
+
...providerError !== void 0 ? { providerError } : {}
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
}
|
|
158
222
|
function wrapFetchError(err, context) {
|
|
159
223
|
if (err instanceof RouteDockError) return err;
|
|
160
224
|
return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
|
|
@@ -219,7 +283,10 @@ async function withRetry(fn, policy = {}) {
|
|
|
219
283
|
if (!err.retryable || attempt >= maxAttempts - 1) {
|
|
220
284
|
throw err;
|
|
221
285
|
}
|
|
222
|
-
const delay =
|
|
286
|
+
const delay = Math.min(
|
|
287
|
+
err.retryAfterMs ?? backoffDelayMs(attempt, baseDelayMs, maxDelayMs),
|
|
288
|
+
maxDelayMs
|
|
289
|
+
);
|
|
223
290
|
if (onRetry) {
|
|
224
291
|
try {
|
|
225
292
|
await onRetry(attempt, err, delay);
|
|
@@ -232,10 +299,18 @@ async function withRetry(fn, policy = {}) {
|
|
|
232
299
|
}
|
|
233
300
|
throw lastError ?? new RouteDockError("Retry exhausted", "NETWORK", false);
|
|
234
301
|
}
|
|
302
|
+
|
|
303
|
+
// src/network.ts
|
|
235
304
|
var USDC_ISSUERS = {
|
|
236
305
|
testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
|
|
237
306
|
mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
|
|
238
307
|
};
|
|
308
|
+
var HORIZON_URLS = {
|
|
309
|
+
testnet: "https://horizon-testnet.stellar.org",
|
|
310
|
+
mainnet: "https://horizon.stellar.org"
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// src/internal/usdc.ts
|
|
239
314
|
var usdcToUnits = usdcToStroops;
|
|
240
315
|
|
|
241
316
|
// src/internal/assetUtils.ts
|
|
@@ -316,6 +391,109 @@ function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
|
|
|
316
391
|
);
|
|
317
392
|
}
|
|
318
393
|
|
|
394
|
+
// package.json
|
|
395
|
+
var package_default = {
|
|
396
|
+
version: "0.4.0"};
|
|
397
|
+
|
|
398
|
+
// src/internal/encoding.ts
|
|
399
|
+
var encoder = new TextEncoder();
|
|
400
|
+
var decoder = new TextDecoder();
|
|
401
|
+
function utf8ToBytes(text) {
|
|
402
|
+
return encoder.encode(text);
|
|
403
|
+
}
|
|
404
|
+
function bytesToUtf8(bytes) {
|
|
405
|
+
return decoder.decode(bytes);
|
|
406
|
+
}
|
|
407
|
+
function base64ToBytes(b64) {
|
|
408
|
+
const end = b64.indexOf("=");
|
|
409
|
+
let clean = (end === -1 ? b64 : b64.slice(0, end)).replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
|
|
410
|
+
if (clean.length % 4 === 1) clean = clean.slice(0, -1);
|
|
411
|
+
const binary = atob(clean);
|
|
412
|
+
const bytes = new Uint8Array(binary.length);
|
|
413
|
+
for (let i = 0; i < binary.length; i++) {
|
|
414
|
+
bytes[i] = binary.charCodeAt(i);
|
|
415
|
+
}
|
|
416
|
+
return bytes;
|
|
417
|
+
}
|
|
418
|
+
function base64ToUtf8(b64) {
|
|
419
|
+
return bytesToUtf8(base64ToBytes(b64));
|
|
420
|
+
}
|
|
421
|
+
function bytesToHex(bytes) {
|
|
422
|
+
let out = "";
|
|
423
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
424
|
+
out += bytes[i].toString(16).padStart(2, "0");
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
function sha256(data) {
|
|
429
|
+
return sha256$1(typeof data === "string" ? utf8ToBytes(data) : data);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/manifest/sign.ts
|
|
433
|
+
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
434
|
+
function canonicalize(value) {
|
|
435
|
+
if (Array.isArray(value)) {
|
|
436
|
+
return value.map(canonicalize);
|
|
437
|
+
}
|
|
438
|
+
if (value !== null && typeof value === "object") {
|
|
439
|
+
return Object.fromEntries(
|
|
440
|
+
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
445
|
+
function manifestDigest(manifest) {
|
|
446
|
+
const { signature: _omit, ...rest } = manifest;
|
|
447
|
+
const canonical = JSON.stringify(canonicalize(rest));
|
|
448
|
+
return sha256(utf8ToBytes(canonical));
|
|
449
|
+
}
|
|
450
|
+
function verifyManifestSignature(manifest, expectedPayee) {
|
|
451
|
+
if (!manifest.signature) {
|
|
452
|
+
throw new RouteDockSignatureError(
|
|
453
|
+
"Manifest is missing a signature field \u2014 cannot verify payee authenticity"
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
if (manifest.signature_version !== MANIFEST_SIGNATURE_VERSION) {
|
|
457
|
+
throw new RouteDockSignatureError(
|
|
458
|
+
`Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
if (expectedPayee !== void 0 && manifest.payee !== expectedPayee) {
|
|
462
|
+
throw new RouteDockSignatureError(
|
|
463
|
+
`Manifest payee ${manifest.payee} does not match expected payee ${expectedPayee}`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
let keypair;
|
|
467
|
+
try {
|
|
468
|
+
keypair = Keypair.fromPublicKey(manifest.payee);
|
|
469
|
+
} catch (err) {
|
|
470
|
+
throw new RouteDockSignatureError(
|
|
471
|
+
`Manifest payee ${manifest.payee} is not a valid Stellar public key`,
|
|
472
|
+
{ cause: err }
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
const digest = manifestDigest(manifest);
|
|
476
|
+
let sigBytes;
|
|
477
|
+
try {
|
|
478
|
+
sigBytes = base64ToBytes(manifest.signature);
|
|
479
|
+
} catch {
|
|
480
|
+
throw new RouteDockSignatureError("Manifest signature is not valid base64");
|
|
481
|
+
}
|
|
482
|
+
let valid = false;
|
|
483
|
+
try {
|
|
484
|
+
valid = keypair.verify(digest, sigBytes);
|
|
485
|
+
} catch {
|
|
486
|
+
throw new RouteDockSignatureError(
|
|
487
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
if (!valid) {
|
|
491
|
+
throw new RouteDockSignatureError(
|
|
492
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
319
497
|
// src/schemas/routedock.schema.json
|
|
320
498
|
var routedock_schema_default = {
|
|
321
499
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
@@ -393,6 +571,7 @@ var routedock_schema_default = {
|
|
|
393
571
|
},
|
|
394
572
|
asset_contract: {
|
|
395
573
|
type: "string",
|
|
574
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
396
575
|
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
397
576
|
},
|
|
398
577
|
assets: {
|
|
@@ -405,6 +584,7 @@ var routedock_schema_default = {
|
|
|
405
584
|
},
|
|
406
585
|
payee: {
|
|
407
586
|
type: "string",
|
|
587
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
408
588
|
description: "Stellar address (G...) that receives payments"
|
|
409
589
|
},
|
|
410
590
|
pricing: {
|
|
@@ -575,6 +755,7 @@ var routedock_schema_default = {
|
|
|
575
755
|
properties: {
|
|
576
756
|
amount: {
|
|
577
757
|
type: "string",
|
|
758
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
578
759
|
description: "Cost per request in the payment asset (decimal string, e.g. '0.001')"
|
|
579
760
|
},
|
|
580
761
|
per: {
|
|
@@ -588,6 +769,7 @@ var routedock_schema_default = {
|
|
|
588
769
|
},
|
|
589
770
|
payee: {
|
|
590
771
|
type: "string",
|
|
772
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
591
773
|
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."
|
|
592
774
|
}
|
|
593
775
|
}
|
|
@@ -600,6 +782,7 @@ var routedock_schema_default = {
|
|
|
600
782
|
properties: {
|
|
601
783
|
rate: {
|
|
602
784
|
type: "string",
|
|
785
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
603
786
|
description: "Cost per voucher in the payment asset (decimal string, e.g. '0.0001')"
|
|
604
787
|
},
|
|
605
788
|
per: {
|
|
@@ -609,10 +792,12 @@ var routedock_schema_default = {
|
|
|
609
792
|
},
|
|
610
793
|
channel_factory: {
|
|
611
794
|
type: "string",
|
|
795
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
612
796
|
description: "Stable Soroban factory address (C...) used to deploy a per-session channel"
|
|
613
797
|
},
|
|
614
798
|
min_deposit: {
|
|
615
799
|
type: "string",
|
|
800
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
616
801
|
description: "Minimum initial deposit to open a channel (decimal string)"
|
|
617
802
|
},
|
|
618
803
|
refund_waiting_period_ledgers: {
|
|
@@ -710,68 +895,110 @@ var routedock_schema_default = {
|
|
|
710
895
|
}
|
|
711
896
|
};
|
|
712
897
|
|
|
713
|
-
//
|
|
714
|
-
var
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
721
|
-
if (value !== null && typeof value === "object") {
|
|
722
|
-
return Object.fromEntries(
|
|
723
|
-
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
|
|
724
|
-
);
|
|
898
|
+
// src/manifest/validate.ts
|
|
899
|
+
var validator = new Validator(routedock_schema_default, "7");
|
|
900
|
+
function validateManifestSchema(manifest, source) {
|
|
901
|
+
const result = validator.validate(manifest);
|
|
902
|
+
if (!result.valid) {
|
|
903
|
+
const msgs = result.errors.map((err) => err.error).join("; ");
|
|
904
|
+
throw new RouteDockManifestError(`Invalid manifest${source ? ` at ${source}` : ""}: ${msgs}`);
|
|
725
905
|
}
|
|
726
|
-
return value;
|
|
727
|
-
}
|
|
728
|
-
function manifestDigest(manifest) {
|
|
729
|
-
const { signature: _omit, ...rest } = manifest;
|
|
730
|
-
const canonical = JSON.stringify(canonicalize(rest));
|
|
731
|
-
return createHash("sha256").update(canonical, "utf8").digest();
|
|
732
906
|
}
|
|
733
|
-
function
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
)
|
|
907
|
+
function assertManifestValid(manifest, baseUrl) {
|
|
908
|
+
const context = baseUrl ? ` at ${baseUrl}` : "";
|
|
909
|
+
if (manifest.latency_hints !== void 0) {
|
|
910
|
+
if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
|
|
911
|
+
throw new RouteDockManifestError(
|
|
912
|
+
`Invalid manifest${context}: latency_hints defined without declaring regions`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
const regionSet = new Set(manifest.regions);
|
|
916
|
+
for (const region of Object.keys(manifest.latency_hints)) {
|
|
917
|
+
if (!regionSet.has(region)) {
|
|
918
|
+
throw new RouteDockManifestError(
|
|
919
|
+
`Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
743
923
|
}
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
924
|
+
if (manifest.assets !== void 0) {
|
|
925
|
+
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
926
|
+
throw new RouteDockManifestError(
|
|
927
|
+
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
for (let i = 0; i < manifest.assets.length; i++) {
|
|
931
|
+
const a = manifest.assets[i];
|
|
932
|
+
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
933
|
+
throw new RouteDockManifestError(
|
|
934
|
+
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const first = manifest.assets[0];
|
|
939
|
+
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
940
|
+
throw new RouteDockManifestError(
|
|
941
|
+
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
748
944
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
945
|
+
assertPricingMatchesModes(manifest, context);
|
|
946
|
+
assertDeprecatedModesListed(manifest, context);
|
|
947
|
+
assertPricesParse(manifest, context);
|
|
948
|
+
}
|
|
949
|
+
function assertPricingMatchesModes(manifest, context) {
|
|
950
|
+
const pricing = manifest.pricing ?? {};
|
|
951
|
+
for (const mode of manifest.modes ?? []) {
|
|
952
|
+
if (pricing[mode] === void 0 || pricing[mode] === null) {
|
|
953
|
+
throw new RouteDockManifestError(
|
|
954
|
+
`Invalid manifest${context}: mode '${mode}' is listed in modes but pricing.${mode} is missing`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
756
957
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
958
|
+
}
|
|
959
|
+
function assertDeprecatedModesListed(manifest, context) {
|
|
960
|
+
if (manifest.deprecated_modes === void 0) return;
|
|
961
|
+
const modes = new Set(manifest.modes ?? []);
|
|
962
|
+
for (const mode of manifest.deprecated_modes) {
|
|
963
|
+
if (!modes.has(mode)) {
|
|
964
|
+
throw new RouteDockManifestError(
|
|
965
|
+
`Invalid manifest${context}: deprecated_modes entry '${mode}' is not listed in modes`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
764
968
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
969
|
+
}
|
|
970
|
+
var PRICE_FIELDS = {
|
|
971
|
+
x402: ["amount"],
|
|
972
|
+
"mpp-charge": ["amount"],
|
|
973
|
+
"mpp-session": ["rate", "min_deposit"],
|
|
974
|
+
"mpp-session-ws": ["rate", "min_deposit"]
|
|
975
|
+
};
|
|
976
|
+
function assertPricesParse(manifest, context) {
|
|
977
|
+
const pricing = manifest.pricing ?? {};
|
|
978
|
+
for (const [mode, fields] of Object.entries(PRICE_FIELDS)) {
|
|
979
|
+
const entry = pricing[mode];
|
|
980
|
+
if (entry === void 0 || entry === null) continue;
|
|
981
|
+
for (const field of fields) {
|
|
982
|
+
const value = entry[field];
|
|
983
|
+
try {
|
|
984
|
+
if (typeof value !== "string") {
|
|
985
|
+
throw new RangeError(`expected a decimal string, got ${typeof value}`);
|
|
986
|
+
}
|
|
987
|
+
usdcToStroops(value);
|
|
988
|
+
} catch (err) {
|
|
989
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
990
|
+
throw new RouteDockManifestError(
|
|
991
|
+
`Invalid manifest${context}: pricing.${mode}.${field} ${JSON.stringify(value)} is not a valid amount (${detail})`,
|
|
992
|
+
{ cause: err }
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
769
996
|
}
|
|
770
997
|
}
|
|
771
998
|
|
|
772
999
|
// src/client/ModeRouter.ts
|
|
773
|
-
var validator = new Validator(routedock_schema_default, "7");
|
|
774
1000
|
var SDK_VERSION = package_default.version;
|
|
1001
|
+
var SUPPORTED_MANIFEST_MAJOR = 1;
|
|
775
1002
|
function parseMajorMinor(version) {
|
|
776
1003
|
const parts = version.split(".");
|
|
777
1004
|
return [Number.parseInt(parts[0] ?? "0", 10), Number.parseInt(parts[1] ?? "0", 10)];
|
|
@@ -789,6 +1016,14 @@ function assertClientVersionSupported(manifest, baseUrl) {
|
|
|
789
1016
|
);
|
|
790
1017
|
}
|
|
791
1018
|
}
|
|
1019
|
+
function assertManifestVersionSupported(manifest, baseUrl) {
|
|
1020
|
+
const [major] = parseMajorMinor(manifest.routedock);
|
|
1021
|
+
if (major > SUPPORTED_MANIFEST_MAJOR) {
|
|
1022
|
+
throw new RouteDockClientVersionError(
|
|
1023
|
+
`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.`
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
792
1027
|
function assertManifestActive(manifest, baseUrl) {
|
|
793
1028
|
const sunsetAt = manifest.sunset_at;
|
|
794
1029
|
if (!sunsetAt) return;
|
|
@@ -804,11 +1039,16 @@ function assertManifestActive(manifest, baseUrl) {
|
|
|
804
1039
|
);
|
|
805
1040
|
}
|
|
806
1041
|
}
|
|
807
|
-
function assertEndpointActive(manifest, url, logger) {
|
|
1042
|
+
function assertEndpointActive(manifest, url, logger, method) {
|
|
808
1043
|
const pathname = new URL(url).pathname;
|
|
809
|
-
const
|
|
1044
|
+
const pathMatches = Object.entries(manifest.endpoints).filter(
|
|
810
1045
|
([, descriptor]) => descriptor.path === pathname
|
|
811
1046
|
);
|
|
1047
|
+
if (pathMatches.length === 0) return;
|
|
1048
|
+
const requestedMethod = method?.toUpperCase();
|
|
1049
|
+
const matches = requestedMethod ? pathMatches.filter(
|
|
1050
|
+
([, descriptor]) => descriptor.method.toUpperCase() === requestedMethod
|
|
1051
|
+
) : pathMatches;
|
|
812
1052
|
if (matches.length === 0) return;
|
|
813
1053
|
const now = Date.now();
|
|
814
1054
|
const allSunset = matches.every(([, descriptor]) => {
|
|
@@ -831,45 +1071,6 @@ function assertEndpointActive(manifest, url, logger) {
|
|
|
831
1071
|
);
|
|
832
1072
|
}
|
|
833
1073
|
}
|
|
834
|
-
function assertManifestValid(manifest, baseUrl) {
|
|
835
|
-
const context = baseUrl ? ` at ${baseUrl}` : "";
|
|
836
|
-
if (manifest.latency_hints !== void 0) {
|
|
837
|
-
if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
|
|
838
|
-
throw new RouteDockManifestError(
|
|
839
|
-
`Invalid manifest${context}: latency_hints defined without declaring regions`
|
|
840
|
-
);
|
|
841
|
-
}
|
|
842
|
-
const regionSet = new Set(manifest.regions);
|
|
843
|
-
for (const region of Object.keys(manifest.latency_hints)) {
|
|
844
|
-
if (!regionSet.has(region)) {
|
|
845
|
-
throw new RouteDockManifestError(
|
|
846
|
-
`Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
|
|
847
|
-
);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
if (manifest.assets !== void 0) {
|
|
852
|
-
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
853
|
-
throw new RouteDockManifestError(
|
|
854
|
-
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
855
|
-
);
|
|
856
|
-
}
|
|
857
|
-
for (let i = 0; i < manifest.assets.length; i++) {
|
|
858
|
-
const a = manifest.assets[i];
|
|
859
|
-
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
860
|
-
throw new RouteDockManifestError(
|
|
861
|
-
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
862
|
-
);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
const first = manifest.assets[0];
|
|
866
|
-
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
867
|
-
throw new RouteDockManifestError(
|
|
868
|
-
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
869
|
-
);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
1074
|
var CACHE_TTL_MS = 6e4;
|
|
874
1075
|
var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
|
|
875
1076
|
var DEFAULT_MANIFEST_TIMEOUT_MS = 5e3;
|
|
@@ -910,6 +1111,7 @@ var LruCache = class {
|
|
|
910
1111
|
}
|
|
911
1112
|
};
|
|
912
1113
|
var manifestCache = new LruCache(DEFAULT_MANIFEST_CACHE_MAX_SIZE);
|
|
1114
|
+
var inflightManifestFetches = /* @__PURE__ */ new Map();
|
|
913
1115
|
function configureManifestCache(maxSize) {
|
|
914
1116
|
if (!Number.isInteger(maxSize) || maxSize <= 0) {
|
|
915
1117
|
throw new RouteDockManifestError("Invalid manifest cache size: " + maxSize);
|
|
@@ -945,10 +1147,27 @@ async function fetchManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_M
|
|
|
945
1147
|
if (cached && Date.now() < cached.expiresAt) {
|
|
946
1148
|
verifyManifestSignature(cached.manifest, expectedPayee);
|
|
947
1149
|
assertClientVersionSupported(cached.manifest, baseUrl);
|
|
1150
|
+
assertManifestVersionSupported(cached.manifest, baseUrl);
|
|
948
1151
|
assertManifestValid(cached.manifest, baseUrl);
|
|
949
1152
|
assertManifestActive(cached.manifest, baseUrl);
|
|
950
1153
|
return cached.manifest;
|
|
951
1154
|
}
|
|
1155
|
+
let inflight = inflightManifestFetches.get(baseUrl);
|
|
1156
|
+
if (!inflight) {
|
|
1157
|
+
inflight = fetchAndCacheManifest(baseUrl, retryPolicy, manifestTimeoutMs).finally(() => {
|
|
1158
|
+
inflightManifestFetches.delete(baseUrl);
|
|
1159
|
+
});
|
|
1160
|
+
inflightManifestFetches.set(baseUrl, inflight);
|
|
1161
|
+
}
|
|
1162
|
+
const manifest = await inflight;
|
|
1163
|
+
verifyManifestSignature(manifest, expectedPayee);
|
|
1164
|
+
assertClientVersionSupported(manifest, baseUrl);
|
|
1165
|
+
assertManifestVersionSupported(manifest, baseUrl);
|
|
1166
|
+
assertManifestValid(manifest, baseUrl);
|
|
1167
|
+
assertManifestActive(manifest, baseUrl);
|
|
1168
|
+
return manifest;
|
|
1169
|
+
}
|
|
1170
|
+
async function fetchAndCacheManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_MANIFEST_TIMEOUT_MS) {
|
|
952
1171
|
const url = baseUrl.replace(/\/$/, "") + "/.well-known/routedock.json";
|
|
953
1172
|
return withRetry(async () => {
|
|
954
1173
|
let raw;
|
|
@@ -985,15 +1204,12 @@ async function fetchManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_M
|
|
|
985
1204
|
} catch (err) {
|
|
986
1205
|
throw new RouteDockManifestError(`Manifest at ${url} is not valid JSON`, { cause: err });
|
|
987
1206
|
}
|
|
988
|
-
|
|
989
|
-
if (!result.valid) {
|
|
990
|
-
const msgs = result.errors.map((err) => err.error).join("; ");
|
|
991
|
-
throw new RouteDockManifestError(`Invalid manifest at ${url}: ${msgs}`);
|
|
992
|
-
}
|
|
1207
|
+
validateManifestSchema(raw, url);
|
|
993
1208
|
const manifest = raw;
|
|
994
1209
|
assertManifestValid(manifest, baseUrl);
|
|
995
|
-
verifyManifestSignature(manifest
|
|
1210
|
+
verifyManifestSignature(manifest);
|
|
996
1211
|
assertClientVersionSupported(manifest, baseUrl);
|
|
1212
|
+
assertManifestVersionSupported(manifest, baseUrl);
|
|
997
1213
|
assertManifestActive(manifest, baseUrl);
|
|
998
1214
|
manifestCache.set(baseUrl, { manifest, fetchedAt: Date.now(), expiresAt });
|
|
999
1215
|
return manifest;
|
|
@@ -1108,6 +1324,48 @@ function rankProvidersByLatency(manifests, targetRegion) {
|
|
|
1108
1324
|
});
|
|
1109
1325
|
}
|
|
1110
1326
|
|
|
1327
|
+
// src/internal/settleResult.ts
|
|
1328
|
+
function readSettleResult(result) {
|
|
1329
|
+
const settle = result;
|
|
1330
|
+
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
1331
|
+
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
1332
|
+
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
1333
|
+
return failed(settle.errorReason);
|
|
1334
|
+
}
|
|
1335
|
+
return { ok: true, txHash: settle.transaction };
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// src/internal/request.ts
|
|
1339
|
+
var DEFAULT_METHOD = "GET";
|
|
1340
|
+
var BODYLESS_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
|
|
1341
|
+
function isReplayable(body) {
|
|
1342
|
+
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;
|
|
1343
|
+
}
|
|
1344
|
+
async function bufferBody(body) {
|
|
1345
|
+
if (isReplayable(body)) return body;
|
|
1346
|
+
return new Response(body).arrayBuffer();
|
|
1347
|
+
}
|
|
1348
|
+
async function prepareRequest(request) {
|
|
1349
|
+
const method = (request?.method ?? DEFAULT_METHOD).toUpperCase();
|
|
1350
|
+
const headers = new Headers(request?.headers);
|
|
1351
|
+
const body = request?.body;
|
|
1352
|
+
if (body === void 0 || BODYLESS_METHODS.has(method)) {
|
|
1353
|
+
return { method, headers };
|
|
1354
|
+
}
|
|
1355
|
+
return { method, headers, body: await bufferBody(body) };
|
|
1356
|
+
}
|
|
1357
|
+
function toRequestInit(prepared, overrides) {
|
|
1358
|
+
const headers = new Headers(prepared.headers);
|
|
1359
|
+
if (overrides) {
|
|
1360
|
+
new Headers(overrides).forEach((value, name) => headers.set(name, value));
|
|
1361
|
+
}
|
|
1362
|
+
const init = { method: prepared.method, headers };
|
|
1363
|
+
if (prepared.body !== void 0) {
|
|
1364
|
+
init.body = prepared.body;
|
|
1365
|
+
}
|
|
1366
|
+
return init;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1111
1369
|
// src/internal/payee.ts
|
|
1112
1370
|
function resolvePayee(manifest, mode) {
|
|
1113
1371
|
const override = manifest.pricing[mode]?.payee;
|
|
@@ -1168,6 +1426,102 @@ function checkChargeChallenge(request, manifest, network, maxAmount) {
|
|
|
1168
1426
|
}
|
|
1169
1427
|
return checkAmount(request["amount"], maxAmount);
|
|
1170
1428
|
}
|
|
1429
|
+
var defaultIo = {
|
|
1430
|
+
async expirationLedger(network, maxTimeoutSeconds) {
|
|
1431
|
+
const { sequence } = await getRpcClient(network).getLatestLedger();
|
|
1432
|
+
const secondsPerLedger = await getEstimatedLedgerCloseTimeSeconds(network);
|
|
1433
|
+
return sequence + Math.ceil(maxTimeoutSeconds / secondsPerLedger);
|
|
1434
|
+
},
|
|
1435
|
+
async assemble({ asset, from, payTo, amount, networkPassphrase, rpcUrl }) {
|
|
1436
|
+
return contract.AssembledTransaction.build({
|
|
1437
|
+
contractId: asset,
|
|
1438
|
+
method: "transfer",
|
|
1439
|
+
// SEP-41: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md#interface
|
|
1440
|
+
args: [
|
|
1441
|
+
nativeToScVal(from, { type: "address" }),
|
|
1442
|
+
nativeToScVal(payTo, { type: "address" }),
|
|
1443
|
+
nativeToScVal(amount, { type: "i128" })
|
|
1444
|
+
],
|
|
1445
|
+
networkPassphrase,
|
|
1446
|
+
rpcUrl,
|
|
1447
|
+
parseResultXdr: (result) => result
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
var NulthStellarScheme = class {
|
|
1452
|
+
constructor(signer, io = defaultIo) {
|
|
1453
|
+
this.signer = signer;
|
|
1454
|
+
this.io = io;
|
|
1455
|
+
}
|
|
1456
|
+
signer;
|
|
1457
|
+
io;
|
|
1458
|
+
scheme = "exact";
|
|
1459
|
+
async createPaymentPayload(x402Version, paymentRequirements) {
|
|
1460
|
+
try {
|
|
1461
|
+
this.validateRequirements(paymentRequirements);
|
|
1462
|
+
} catch (err) {
|
|
1463
|
+
throw new Error(
|
|
1464
|
+
`Invalid input parameters for creating Nulth Stellar payment, cause: ${String(err)}`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
const from = this.signer.address;
|
|
1468
|
+
const { network, payTo, asset, amount, maxTimeoutSeconds } = paymentRequirements;
|
|
1469
|
+
if (!paymentRequirements.extra.areFeesSponsored) {
|
|
1470
|
+
throw new Error("Nulth exact scheme requires areFeesSponsored to be true");
|
|
1471
|
+
}
|
|
1472
|
+
const maxLedger = await this.io.expirationLedger(network, maxTimeoutSeconds);
|
|
1473
|
+
const tx = await this.io.assemble({
|
|
1474
|
+
asset,
|
|
1475
|
+
from,
|
|
1476
|
+
payTo,
|
|
1477
|
+
amount,
|
|
1478
|
+
networkPassphrase: getNetworkPassphrase(network),
|
|
1479
|
+
rpcUrl: getRpcUrl(network)
|
|
1480
|
+
});
|
|
1481
|
+
handleSimulationResult(tx.simulation);
|
|
1482
|
+
const signers = tx.needsNonInvokerSigningBy();
|
|
1483
|
+
if (!signers.includes(from) || signers.length > 1) {
|
|
1484
|
+
throw new Error(`Expected to sign with [${from}], but got [${signers.join(", ")}]`);
|
|
1485
|
+
}
|
|
1486
|
+
await tx.signAuthEntries({
|
|
1487
|
+
address: from,
|
|
1488
|
+
signAuthEntry: this.signer.signAuthEntry,
|
|
1489
|
+
expiration: maxLedger,
|
|
1490
|
+
authorizeEntry: nulthAuthorizeEntry
|
|
1491
|
+
});
|
|
1492
|
+
await tx.simulate();
|
|
1493
|
+
handleSimulationResult(tx.simulation);
|
|
1494
|
+
const remaining = tx.needsNonInvokerSigningBy();
|
|
1495
|
+
if (remaining.length > 0) {
|
|
1496
|
+
throw new Error(`unexpected signer(s) required: [${remaining.join(", ")}]`);
|
|
1497
|
+
}
|
|
1498
|
+
if (!tx.built) {
|
|
1499
|
+
throw new Error("Nulth payment transaction has no assembled envelope to send");
|
|
1500
|
+
}
|
|
1501
|
+
return { x402Version, payload: { transaction: tx.built.toXDR() } };
|
|
1502
|
+
}
|
|
1503
|
+
validateRequirements(requirements) {
|
|
1504
|
+
const { scheme, network, payTo, asset, amount } = requirements;
|
|
1505
|
+
if (typeof amount !== "string" || !Number.isInteger(Number(amount)) || Number(amount) <= 0) {
|
|
1506
|
+
throw new Error(`Invalid amount: ${amount}. Amount must be a positive integer.`);
|
|
1507
|
+
}
|
|
1508
|
+
if (scheme !== this.scheme) {
|
|
1509
|
+
throw new Error(`Unsupported scheme: ${scheme}`);
|
|
1510
|
+
}
|
|
1511
|
+
if (!isStellarNetwork(network)) {
|
|
1512
|
+
throw new Error(`Unsupported Stellar network: ${network}`);
|
|
1513
|
+
}
|
|
1514
|
+
if (!validateStellarDestinationAddress(payTo)) {
|
|
1515
|
+
throw new Error(`Invalid Stellar destination address: ${payTo}`);
|
|
1516
|
+
}
|
|
1517
|
+
if (!validateStellarAssetAddress(asset)) {
|
|
1518
|
+
throw new Error(`Invalid Stellar asset address: ${asset}`);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
function selectStellarScheme(signer) {
|
|
1523
|
+
return isNulthStellarSigner(signer) ? new NulthStellarScheme(signer) : new ExactStellarScheme(signer);
|
|
1524
|
+
}
|
|
1171
1525
|
|
|
1172
1526
|
// src/client/x402Client.ts
|
|
1173
1527
|
var CAIP2 = {
|
|
@@ -1175,25 +1529,29 @@ var CAIP2 = {
|
|
|
1175
1529
|
mainnet: "stellar:pubnet"
|
|
1176
1530
|
};
|
|
1177
1531
|
var X402Client = class _X402Client {
|
|
1178
|
-
constructor(secretKeyOrSigner, network, retryPolicy) {
|
|
1532
|
+
constructor(secretKeyOrSigner, network, retryPolicy, preSignHook, logger) {
|
|
1179
1533
|
this.network = network;
|
|
1180
1534
|
this.retryPolicy = retryPolicy;
|
|
1535
|
+
this.preSignHook = preSignHook;
|
|
1181
1536
|
const caip2 = CAIP2[network];
|
|
1182
1537
|
this.signer = typeof secretKeyOrSigner === "string" ? createEd25519Signer(secretKeyOrSigner, caip2) : secretKeyOrSigner;
|
|
1183
|
-
const scheme =
|
|
1538
|
+
const scheme = selectStellarScheme(this.signer);
|
|
1184
1539
|
const core = new x402Client();
|
|
1185
1540
|
core.register(caip2, scheme);
|
|
1186
1541
|
this.httpClient = new x402HTTPClient(core);
|
|
1542
|
+
this.logger = logger;
|
|
1187
1543
|
}
|
|
1188
1544
|
network;
|
|
1189
1545
|
retryPolicy;
|
|
1546
|
+
preSignHook;
|
|
1190
1547
|
httpClient;
|
|
1191
1548
|
signer;
|
|
1549
|
+
logger;
|
|
1192
1550
|
/** Replace signer (e.g. swap to Nulth ZK account payer before pay) */
|
|
1193
1551
|
withSigner(signer) {
|
|
1194
|
-
return new _X402Client(signer, this.network, this.retryPolicy);
|
|
1552
|
+
return new _X402Client(signer, this.network, this.retryPolicy, this.preSignHook, this.logger);
|
|
1195
1553
|
}
|
|
1196
|
-
async pay(url, manifest) {
|
|
1554
|
+
async pay(url, manifest, request) {
|
|
1197
1555
|
const pricing = manifest.pricing.x402;
|
|
1198
1556
|
if (!pricing) {
|
|
1199
1557
|
throw new RouteDockManifestError("manifest.pricing.x402 missing");
|
|
@@ -1202,13 +1560,15 @@ var X402Client = class _X402Client {
|
|
|
1202
1560
|
throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
|
|
1203
1561
|
}
|
|
1204
1562
|
const caip2 = CAIP2[this.network];
|
|
1563
|
+
const prepared = await prepareRequest(request);
|
|
1205
1564
|
const probe = await withRetry(async () => {
|
|
1206
1565
|
let init2;
|
|
1207
1566
|
try {
|
|
1208
|
-
init2 = await fetch(url,
|
|
1567
|
+
init2 = await fetch(url, toRequestInit(prepared, { "X-Preferred-Mode": "x402" }));
|
|
1209
1568
|
} catch (err) {
|
|
1210
1569
|
throw wrapFetchError(err, "x402 initial request");
|
|
1211
1570
|
}
|
|
1571
|
+
this.logger?.("debug", "[x402] unpaid probe response", { status: init2.status });
|
|
1212
1572
|
if (init2.status !== 402) {
|
|
1213
1573
|
if (!init2.ok) {
|
|
1214
1574
|
throw httpStatusToError(
|
|
@@ -1225,6 +1585,11 @@ var X402Client = class _X402Client {
|
|
|
1225
1585
|
`Failed to parse JSON from non-402 response (HTTP ${init2.status})`
|
|
1226
1586
|
);
|
|
1227
1587
|
}
|
|
1588
|
+
this.logger?.(
|
|
1589
|
+
"debug",
|
|
1590
|
+
"[x402] unpaid probe returned non-402 success \u2014 serving as free response",
|
|
1591
|
+
{ status: init2.status }
|
|
1592
|
+
);
|
|
1228
1593
|
return { kind: "free", data: data2 };
|
|
1229
1594
|
}
|
|
1230
1595
|
return { kind: "payment-required", init: init2 };
|
|
@@ -1253,6 +1618,18 @@ var X402Client = class _X402Client {
|
|
|
1253
1618
|
if (allowed.length === 0) {
|
|
1254
1619
|
throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
|
|
1255
1620
|
}
|
|
1621
|
+
if (this.preSignHook) {
|
|
1622
|
+
let decision;
|
|
1623
|
+
try {
|
|
1624
|
+
decision = await this.preSignHook({ url, manifest, accepts: allowed });
|
|
1625
|
+
} catch (err) {
|
|
1626
|
+
throw new RouteDockPolicyRejectError("pre_sign_hook_error", { cause: err });
|
|
1627
|
+
}
|
|
1628
|
+
if (!decision || decision.approve !== true) {
|
|
1629
|
+
const reason = decision && decision.approve === false ? decision.reason : void 0;
|
|
1630
|
+
throw new RouteDockPolicyRejectError(reason ?? "pre_sign_rejected");
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1256
1633
|
let paymentPayload;
|
|
1257
1634
|
try {
|
|
1258
1635
|
paymentPayload = await this.httpClient.createPaymentPayload({
|
|
@@ -1266,13 +1643,15 @@ var X402Client = class _X402Client {
|
|
|
1266
1643
|
}
|
|
1267
1644
|
const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
1268
1645
|
const signedAmount = stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
|
|
1646
|
+
this.logger?.("debug", "[x402] sending paid request with payment header");
|
|
1269
1647
|
const settled = await withRetry(async () => {
|
|
1270
1648
|
let response;
|
|
1271
1649
|
try {
|
|
1272
|
-
response = await fetch(url,
|
|
1650
|
+
response = await fetch(url, toRequestInit(prepared, paymentHeaders));
|
|
1273
1651
|
} catch (err) {
|
|
1274
1652
|
throw wrapFetchError(err, "x402 settlement request");
|
|
1275
1653
|
}
|
|
1654
|
+
this.logger?.("debug", "[x402] paid request response", { status: response.status });
|
|
1276
1655
|
if (!response.ok) {
|
|
1277
1656
|
if (response.status >= 500 || response.status === 429 || response.status === 503) {
|
|
1278
1657
|
throw httpStatusToError(
|
|
@@ -1281,17 +1660,28 @@ var X402Client = class _X402Client {
|
|
|
1281
1660
|
response
|
|
1282
1661
|
);
|
|
1283
1662
|
}
|
|
1284
|
-
throw
|
|
1663
|
+
throw await paymentRejectedError("x402 payment failed", response.status, response);
|
|
1285
1664
|
}
|
|
1286
1665
|
return response;
|
|
1287
1666
|
}, this.retryPolicy);
|
|
1288
1667
|
let txHash = null;
|
|
1289
1668
|
const responseHeader = settled.headers.get("X-Payment-Response");
|
|
1290
|
-
if (responseHeader) {
|
|
1669
|
+
if (!responseHeader) {
|
|
1670
|
+
this.logger?.("debug", "[x402] settled 2xx carried no X-Payment-Response header");
|
|
1671
|
+
} else {
|
|
1291
1672
|
try {
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1673
|
+
const outcome = readSettleResult(decodePaymentResponseHeader(responseHeader));
|
|
1674
|
+
if (outcome.ok) {
|
|
1675
|
+
txHash = outcome.txHash;
|
|
1676
|
+
} else {
|
|
1677
|
+
this.logger?.("debug", "[x402] X-Payment-Response reports an unsettled payment", {
|
|
1678
|
+
...outcome.reason !== void 0 ? { reason: outcome.reason } : {}
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
} catch (err) {
|
|
1682
|
+
this.logger?.("debug", "[x402] X-Payment-Response header present but undecodable", {
|
|
1683
|
+
error: err
|
|
1684
|
+
});
|
|
1295
1685
|
}
|
|
1296
1686
|
}
|
|
1297
1687
|
let data;
|
|
@@ -1312,13 +1702,13 @@ var MppChargeClient = class {
|
|
|
1312
1702
|
keypair;
|
|
1313
1703
|
network;
|
|
1314
1704
|
retryPolicy;
|
|
1315
|
-
async pay(url, manifest) {
|
|
1705
|
+
async pay(url, manifest, request) {
|
|
1316
1706
|
const pricing = manifest.pricing["mpp-charge"];
|
|
1317
1707
|
if (!pricing) {
|
|
1318
1708
|
throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
|
|
1319
1709
|
}
|
|
1320
1710
|
const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
|
|
1321
|
-
|
|
1711
|
+
const prepared = await prepareRequest(request);
|
|
1322
1712
|
let signedAmount;
|
|
1323
1713
|
let credential;
|
|
1324
1714
|
const mppx = Mppx.create({
|
|
@@ -1326,19 +1716,14 @@ var MppChargeClient = class {
|
|
|
1326
1716
|
methods: [
|
|
1327
1717
|
stellar.charge({
|
|
1328
1718
|
keypair: this.keypair,
|
|
1329
|
-
mode: "pull"
|
|
1330
|
-
onProgress(event) {
|
|
1331
|
-
if (event.type === "paid") {
|
|
1332
|
-
txHash = event.hash;
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1719
|
+
mode: "pull"
|
|
1335
1720
|
})
|
|
1336
1721
|
],
|
|
1337
1722
|
onChallenge: async (challenge, { createCredential }) => {
|
|
1338
|
-
const
|
|
1339
|
-
const rejection = checkChargeChallenge(
|
|
1723
|
+
const request2 = challenge.request;
|
|
1724
|
+
const rejection = checkChargeChallenge(request2, manifest, caip2, pricing.amount);
|
|
1340
1725
|
if (rejection) throw rejection;
|
|
1341
|
-
const amount =
|
|
1726
|
+
const amount = request2["amount"];
|
|
1342
1727
|
if (typeof amount !== "string") {
|
|
1343
1728
|
throw new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1344
1729
|
}
|
|
@@ -1350,7 +1735,7 @@ var MppChargeClient = class {
|
|
|
1350
1735
|
return withRetry(async () => {
|
|
1351
1736
|
let response;
|
|
1352
1737
|
try {
|
|
1353
|
-
response = credential ? await mppx.rawFetch(url,
|
|
1738
|
+
response = credential ? await mppx.rawFetch(url, toRequestInit(prepared, { Authorization: credential })) : await mppx.fetch(url, toRequestInit(prepared));
|
|
1354
1739
|
} catch (err) {
|
|
1355
1740
|
throw wrapMppError(err, "MPP charge request");
|
|
1356
1741
|
}
|
|
@@ -1362,8 +1747,9 @@ var MppChargeClient = class {
|
|
|
1362
1747
|
response
|
|
1363
1748
|
);
|
|
1364
1749
|
}
|
|
1365
|
-
throw
|
|
1750
|
+
throw await paymentRejectedError("MPP charge failed", response.status, response);
|
|
1366
1751
|
}
|
|
1752
|
+
const txHash = readSettlementHash(response);
|
|
1367
1753
|
let data;
|
|
1368
1754
|
try {
|
|
1369
1755
|
data = await response.json();
|
|
@@ -1383,8 +1769,40 @@ var MppChargeClient = class {
|
|
|
1383
1769
|
}, this.retryPolicy);
|
|
1384
1770
|
}
|
|
1385
1771
|
};
|
|
1772
|
+
function readSettlementHash(response) {
|
|
1773
|
+
try {
|
|
1774
|
+
return Receipt.fromResponse(response).reference;
|
|
1775
|
+
} catch {
|
|
1776
|
+
return null;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1386
1779
|
var MIN_REFUND_WAITING_PERIOD = 17280;
|
|
1387
1780
|
var STREAM_CLEANUP_TIMEOUT_MS = 3e3;
|
|
1781
|
+
var TX_CONFIRM_INTERVAL_MS = 1e3;
|
|
1782
|
+
var TX_CONFIRM_TIMEOUT_MS = 3e4;
|
|
1783
|
+
async function confirmTransaction(server, hash, label, timeoutMs = TX_CONFIRM_TIMEOUT_MS, intervalMs = TX_CONFIRM_INTERVAL_MS) {
|
|
1784
|
+
const deadline = Date.now() + timeoutMs;
|
|
1785
|
+
while (true) {
|
|
1786
|
+
const txResponse = await server.getTransaction(hash);
|
|
1787
|
+
const status = txResponse?.status;
|
|
1788
|
+
if (status === "SUCCESS") {
|
|
1789
|
+
return hash;
|
|
1790
|
+
}
|
|
1791
|
+
if (status === "FAILED") {
|
|
1792
|
+
throw new RouteDockDisputeError(
|
|
1793
|
+
`${label} transaction failed on-chain: status FAILED (resultXdr: ${txResponse.resultXdr.toXDR("base64")})`
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
if (Date.now() >= deadline) {
|
|
1797
|
+
break;
|
|
1798
|
+
}
|
|
1799
|
+
const remaining = deadline - Date.now();
|
|
1800
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, Math.max(1, remaining))));
|
|
1801
|
+
}
|
|
1802
|
+
throw new RouteDockDisputeError(
|
|
1803
|
+
`${label} transaction confirmation timed out after ${timeoutMs}ms (hash: ${hash})`
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1388
1806
|
var WS_CONNECTING = 0;
|
|
1389
1807
|
var WS_OPEN = 1;
|
|
1390
1808
|
function defaultWebSocketFactory(url, init, handlers) {
|
|
@@ -1398,9 +1816,9 @@ function defaultWebSocketFactory(url, init, handlers) {
|
|
|
1398
1816
|
if (typeof data === "string") {
|
|
1399
1817
|
handlers.onMessage(data);
|
|
1400
1818
|
} else if (data instanceof ArrayBuffer) {
|
|
1401
|
-
handlers.onMessage(
|
|
1819
|
+
handlers.onMessage(bytesToUtf8(new Uint8Array(data)));
|
|
1402
1820
|
} else {
|
|
1403
|
-
void data.arrayBuffer().then((buffer) => handlers.onMessage(
|
|
1821
|
+
void data.arrayBuffer().then((buffer) => handlers.onMessage(bytesToUtf8(new Uint8Array(buffer)))).catch(() => {
|
|
1404
1822
|
});
|
|
1405
1823
|
}
|
|
1406
1824
|
};
|
|
@@ -1553,7 +1971,7 @@ var MppSessionClient = class {
|
|
|
1553
1971
|
throw err;
|
|
1554
1972
|
}
|
|
1555
1973
|
};
|
|
1556
|
-
const streamWs = (u, m, createCredential, onSigned) => this.streamWebSocket(u, m, createCredential, onSigned);
|
|
1974
|
+
const streamWs = (u, m, createCredential, onSigned, request) => this.streamWebSocket(u, m, createCredential, onSigned, request);
|
|
1557
1975
|
const mppx = Mppx.create({
|
|
1558
1976
|
polyfill: false,
|
|
1559
1977
|
methods: [
|
|
@@ -1625,6 +2043,7 @@ var MppSessionClient = class {
|
|
|
1625
2043
|
};
|
|
1626
2044
|
},
|
|
1627
2045
|
async *stream(options2) {
|
|
2046
|
+
const prepared = await prepareRequest(options2?.request);
|
|
1628
2047
|
const checkSpend = () => {
|
|
1629
2048
|
if (!onSpend) return Promise.resolve();
|
|
1630
2049
|
return onSpend(pricing.rate);
|
|
@@ -1634,7 +2053,13 @@ var MppSessionClient = class {
|
|
|
1634
2053
|
const onSigned = () => {
|
|
1635
2054
|
vouchersIssued++;
|
|
1636
2055
|
};
|
|
1637
|
-
for await (const item of streamWs(
|
|
2056
|
+
for await (const item of streamWs(
|
|
2057
|
+
url,
|
|
2058
|
+
mppx,
|
|
2059
|
+
createGuardedCredential,
|
|
2060
|
+
onSigned,
|
|
2061
|
+
prepared
|
|
2062
|
+
)) {
|
|
1638
2063
|
yield item;
|
|
1639
2064
|
}
|
|
1640
2065
|
return;
|
|
@@ -1649,7 +2074,10 @@ var MppSessionClient = class {
|
|
|
1649
2074
|
const doFetch = () => withRetry(async () => {
|
|
1650
2075
|
let resp;
|
|
1651
2076
|
try {
|
|
1652
|
-
resp = await mppx.fetch(url, {
|
|
2077
|
+
resp = await mppx.fetch(url, {
|
|
2078
|
+
...toRequestInit(prepared),
|
|
2079
|
+
signal: abortController.signal
|
|
2080
|
+
});
|
|
1653
2081
|
} catch (err) {
|
|
1654
2082
|
if (abortController.signal.aborted) {
|
|
1655
2083
|
throw err;
|
|
@@ -1734,10 +2162,10 @@ var MppSessionClient = class {
|
|
|
1734
2162
|
async close() {
|
|
1735
2163
|
clearSessionTimer();
|
|
1736
2164
|
closed = true;
|
|
1737
|
-
const { rpc: rpcMod, Contract, nativeToScVal, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
|
|
2165
|
+
const { rpc: rpcMod, Contract, nativeToScVal: nativeToScVal2, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
|
|
1738
2166
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1739
2167
|
const server = new rpcMod.Server(rpcUrl);
|
|
1740
|
-
const
|
|
2168
|
+
const contract2 = new Contract(channelFactory);
|
|
1741
2169
|
const passphrase = network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
1742
2170
|
const account = await withRetry(async () => {
|
|
1743
2171
|
try {
|
|
@@ -1752,9 +2180,9 @@ var MppSessionClient = class {
|
|
|
1752
2180
|
fee: BASE_FEE,
|
|
1753
2181
|
networkPassphrase: passphrase
|
|
1754
2182
|
}).addOperation(
|
|
1755
|
-
|
|
2183
|
+
contract2.call(
|
|
1756
2184
|
"prepare_commitment",
|
|
1757
|
-
|
|
2185
|
+
nativeToScVal2(currentCumulative, { type: "i128" })
|
|
1758
2186
|
)
|
|
1759
2187
|
).setTimeout(30).build();
|
|
1760
2188
|
return await server.simulateTransaction(simTx);
|
|
@@ -1773,7 +2201,7 @@ var MppSessionClient = class {
|
|
|
1773
2201
|
}
|
|
1774
2202
|
let signature;
|
|
1775
2203
|
try {
|
|
1776
|
-
signature = commitmentKey.sign(
|
|
2204
|
+
signature = commitmentKey.sign(commitmentBytes);
|
|
1777
2205
|
} catch (err) {
|
|
1778
2206
|
throw new RouteDockSignatureError("Channel close commitment signing failed", {
|
|
1779
2207
|
cause: err
|
|
@@ -1787,7 +2215,7 @@ var MppSessionClient = class {
|
|
|
1787
2215
|
headers: { "Content-Type": "application/json" },
|
|
1788
2216
|
body: JSON.stringify({
|
|
1789
2217
|
amount: currentCumulative.toString(),
|
|
1790
|
-
signature:
|
|
2218
|
+
signature: bytesToHex(signature)
|
|
1791
2219
|
})
|
|
1792
2220
|
});
|
|
1793
2221
|
} catch (err) {
|
|
@@ -1829,15 +2257,19 @@ var MppSessionClient = class {
|
|
|
1829
2257
|
vouchersIssued
|
|
1830
2258
|
};
|
|
1831
2259
|
},
|
|
1832
|
-
|
|
2260
|
+
/**
|
|
2261
|
+
* Request refund from the channel contract (initiates dispute).
|
|
2262
|
+
* Returns the transaction hash only after on-chain confirmation (SUCCESS).
|
|
2263
|
+
*/
|
|
2264
|
+
async requestRefund(options2) {
|
|
1833
2265
|
const { rpc: rpcMod, Contract, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
|
|
1834
2266
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1835
2267
|
const server = new rpcMod.Server(rpcUrl);
|
|
1836
|
-
const
|
|
2268
|
+
const contract2 = new Contract(channelFactory);
|
|
1837
2269
|
const passphrase = network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
1838
2270
|
try {
|
|
1839
2271
|
const account = await server.getAccount(agentPublicKey);
|
|
1840
|
-
const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(
|
|
2272
|
+
const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(contract2.call("request_refund")).setTimeout(30).build();
|
|
1841
2273
|
const simResult = await server.simulateTransaction(tx);
|
|
1842
2274
|
if (rpcMod.Api.isSimulationError(simResult)) {
|
|
1843
2275
|
throw new RouteDockDisputeError(`request_refund simulation failed: ${simResult.error}`);
|
|
@@ -1849,37 +2281,50 @@ var MppSessionClient = class {
|
|
|
1849
2281
|
const errDetail = result.errorResult ? JSON.stringify(result.errorResult) : "status ERROR";
|
|
1850
2282
|
throw new RouteDockDisputeError(`Refund request transaction failed: ${errDetail}`);
|
|
1851
2283
|
}
|
|
2284
|
+
if (result.status === "TRY_AGAIN_LATER") {
|
|
2285
|
+
throw new RouteDockDisputeError("Refund request transaction rejected with TRY_AGAIN_LATER: transaction not queued");
|
|
2286
|
+
}
|
|
1852
2287
|
if (!result.hash) {
|
|
1853
2288
|
throw new RouteDockDisputeError("Refund request transaction not sent");
|
|
1854
2289
|
}
|
|
1855
|
-
return
|
|
2290
|
+
return await confirmTransaction(
|
|
2291
|
+
server,
|
|
2292
|
+
result.hash,
|
|
2293
|
+
"Refund request",
|
|
2294
|
+
options2?.timeoutMs,
|
|
2295
|
+
options2?.intervalMs
|
|
2296
|
+
);
|
|
1856
2297
|
} catch (err) {
|
|
1857
2298
|
if (err instanceof RouteDockDisputeError) throw err;
|
|
1858
2299
|
throw new RouteDockDisputeError(`Failed to request refund: ${err instanceof Error ? err.message : String(err)}`);
|
|
1859
2300
|
}
|
|
1860
2301
|
},
|
|
1861
|
-
|
|
1862
|
-
|
|
2302
|
+
/**
|
|
2303
|
+
* Server-side counter-mechanism to settle with latest voucher before refund window expires.
|
|
2304
|
+
* Returns the transaction hash only after on-chain confirmation (SUCCESS).
|
|
2305
|
+
*/
|
|
2306
|
+
async settleWithLatestVoucher(options2) {
|
|
2307
|
+
const { rpc: rpcMod, Contract, nativeToScVal: nativeToScVal2, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
|
|
1863
2308
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1864
2309
|
const server = new rpcMod.Server(rpcUrl);
|
|
1865
|
-
const
|
|
2310
|
+
const contract2 = new Contract(channelFactory);
|
|
1866
2311
|
const passphrase = network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
1867
2312
|
try {
|
|
1868
2313
|
const account = await server.getAccount(agentPublicKey);
|
|
1869
|
-
const simTx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(
|
|
2314
|
+
const simTx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(contract2.call("prepare_commitment", nativeToScVal2(currentCumulative, { type: "i128" }))).setTimeout(30).build();
|
|
1870
2315
|
const simResult = await server.simulateTransaction(simTx);
|
|
1871
2316
|
if (rpcMod.Api.isSimulationError(simResult)) {
|
|
1872
2317
|
throw new RouteDockDisputeError(`prepare_commitment simulation failed: ${simResult.error}`);
|
|
1873
2318
|
}
|
|
1874
2319
|
const commitmentBytes = simResult.result?.retval?.bytes();
|
|
1875
2320
|
if (!commitmentBytes) throw new RouteDockDisputeError("prepare_commitment returned no bytes");
|
|
1876
|
-
const signature = commitmentKey.sign(
|
|
2321
|
+
const signature = commitmentKey.sign(commitmentBytes);
|
|
1877
2322
|
const settleAccount = await server.getAccount(agentPublicKey);
|
|
1878
2323
|
const settleTx = new TransactionBuilder(settleAccount, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(
|
|
1879
|
-
|
|
2324
|
+
contract2.call(
|
|
1880
2325
|
"settle_with_signature",
|
|
1881
|
-
|
|
1882
|
-
|
|
2326
|
+
nativeToScVal2(currentCumulative, { type: "i128" }),
|
|
2327
|
+
nativeToScVal2(signature)
|
|
1883
2328
|
)
|
|
1884
2329
|
).setTimeout(30).build();
|
|
1885
2330
|
const preparedSettleTx = await server.prepareTransaction(settleTx);
|
|
@@ -1889,10 +2334,19 @@ var MppSessionClient = class {
|
|
|
1889
2334
|
const errDetail = settleResult.errorResult ? JSON.stringify(settleResult.errorResult) : "status ERROR";
|
|
1890
2335
|
throw new RouteDockDisputeError(`Settlement transaction failed: ${errDetail}`);
|
|
1891
2336
|
}
|
|
2337
|
+
if (settleResult.status === "TRY_AGAIN_LATER") {
|
|
2338
|
+
throw new RouteDockDisputeError("Settlement transaction rejected with TRY_AGAIN_LATER: transaction not queued");
|
|
2339
|
+
}
|
|
1892
2340
|
if (!settleResult.hash) {
|
|
1893
2341
|
throw new RouteDockDisputeError("Settlement transaction not sent");
|
|
1894
2342
|
}
|
|
1895
|
-
return
|
|
2343
|
+
return await confirmTransaction(
|
|
2344
|
+
server,
|
|
2345
|
+
settleResult.hash,
|
|
2346
|
+
"Settlement",
|
|
2347
|
+
options2?.timeoutMs,
|
|
2348
|
+
options2?.intervalMs
|
|
2349
|
+
);
|
|
1896
2350
|
} catch (err) {
|
|
1897
2351
|
if (err instanceof RouteDockDisputeError) throw err;
|
|
1898
2352
|
throw new RouteDockDisputeError(`Failed to settle with latest voucher: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1902,11 +2356,11 @@ var MppSessionClient = class {
|
|
|
1902
2356
|
const { rpc: rpcMod, Contract, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
|
|
1903
2357
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1904
2358
|
const server = new rpcMod.Server(rpcUrl);
|
|
1905
|
-
const
|
|
2359
|
+
const contract2 = new Contract(channelFactory);
|
|
1906
2360
|
const passphrase = network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
1907
2361
|
try {
|
|
1908
2362
|
const account = await server.getAccount(agentPublicKey);
|
|
1909
|
-
const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(
|
|
2363
|
+
const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(contract2.call("get_channel_state")).setTimeout(30).build();
|
|
1910
2364
|
const simResult = await server.simulateTransaction(tx);
|
|
1911
2365
|
if (rpcMod.Api.isSimulationError(simResult)) {
|
|
1912
2366
|
throw new RouteDockChannelStateError(`Failed to query channel state: ${simResult.error}`);
|
|
@@ -1972,11 +2426,11 @@ var MppSessionClient = class {
|
|
|
1972
2426
|
* close code (1000/1001); abnormal closes and connection failures surface as
|
|
1973
2427
|
* RouteDockChannelStateError.
|
|
1974
2428
|
*/
|
|
1975
|
-
async *streamWebSocket(url, mppx, createCredential, onSigned) {
|
|
1976
|
-
const
|
|
2429
|
+
async *streamWebSocket(url, mppx, createCredential, onSigned, request) {
|
|
2430
|
+
const probeInit = toRequestInit(request);
|
|
1977
2431
|
let probe;
|
|
1978
2432
|
try {
|
|
1979
|
-
probe = await mppx.rawFetch(url,
|
|
2433
|
+
probe = await mppx.rawFetch(url, probeInit);
|
|
1980
2434
|
} catch (err) {
|
|
1981
2435
|
throw wrapFetchError(err, "Voucher request");
|
|
1982
2436
|
}
|
|
@@ -2080,230 +2534,19 @@ var MppSessionClient = class {
|
|
|
2080
2534
|
}
|
|
2081
2535
|
}
|
|
2082
2536
|
};
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
this.code = code;
|
|
2089
|
-
}
|
|
2090
|
-
};
|
|
2091
|
-
function sha256Hex(data) {
|
|
2092
|
-
const buf = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
2093
|
-
return createHash("sha256").update(buf).digest("hex");
|
|
2094
|
-
}
|
|
2095
|
-
function commitDailyCap(dailyCapStroops, witnessSecret) {
|
|
2096
|
-
return sha256Hex(`${witnessSecret}:cap:${dailyCapStroops.toString()}`);
|
|
2097
|
-
}
|
|
2098
|
-
function commitAllowlist(payees, witnessSecret) {
|
|
2099
|
-
const sorted = [...payees].sort().join(",");
|
|
2100
|
-
return sha256Hex(`${witnessSecret}:allowlist:${sorted}`);
|
|
2101
|
-
}
|
|
2102
|
-
function hashPayee(payee) {
|
|
2103
|
-
return sha256Hex(`payee:${payee}`);
|
|
2104
|
-
}
|
|
2105
|
-
function dayBucketFromLedger(ledgerSequence) {
|
|
2106
|
-
return Math.floor(ledgerSequence / 17280);
|
|
2107
|
-
}
|
|
2108
|
-
function authDigestFromEntry(authEntryBase64) {
|
|
2109
|
-
const entryHash = hash(Buffer.from(authEntryBase64, "base64"));
|
|
2110
|
-
return sha256Hex(entryHash);
|
|
2111
|
-
}
|
|
2112
|
-
function assertAuthEntryMatchesContext(authEntry, from, ctx) {
|
|
2113
|
-
let preimage;
|
|
2537
|
+
function createNulthSigner(config) {
|
|
2538
|
+
const sharedConfig = { ...config, logger: resolveLogger(config.logger) };
|
|
2539
|
+
Object.defineProperty(sharedConfig, "paymentContext", {
|
|
2540
|
+
get: () => config.paymentContext
|
|
2541
|
+
});
|
|
2114
2542
|
try {
|
|
2115
|
-
|
|
2116
|
-
} catch {
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
if (preimage.switch() !== xdr.EnvelopeType.envelopeTypeSorobanAuthorization()) {
|
|
2120
|
-
throw new NulthPolicyError(
|
|
2121
|
-
"auth_entry_mismatch",
|
|
2122
|
-
"authEntry is not a Soroban authorization preimage"
|
|
2123
|
-
);
|
|
2124
|
-
}
|
|
2125
|
-
const inv = preimage.sorobanAuthorization().invocation();
|
|
2126
|
-
if (inv.subInvocations().length !== 0) {
|
|
2127
|
-
throw new NulthPolicyError("auth_entry_mismatch", "authEntry has unexpected sub-invocations");
|
|
2128
|
-
}
|
|
2129
|
-
if (inv.function().switch() !== xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn()) {
|
|
2130
|
-
throw new NulthPolicyError("auth_entry_mismatch", "authEntry does not invoke a contract function");
|
|
2131
|
-
}
|
|
2132
|
-
const fn = inv.function().contractFn();
|
|
2133
|
-
const contractAddress = Address.fromScAddress(fn.contractAddress()).toString();
|
|
2134
|
-
if (contractAddress !== ctx.assetContract) {
|
|
2135
|
-
throw new NulthPolicyError(
|
|
2136
|
-
"auth_entry_mismatch",
|
|
2137
|
-
`authEntry contract ${contractAddress} does not match assetContract ${ctx.assetContract}`
|
|
2138
|
-
);
|
|
2139
|
-
}
|
|
2140
|
-
const functionName = fn.functionName().toString();
|
|
2141
|
-
if (functionName !== "transfer") {
|
|
2142
|
-
throw new NulthPolicyError(
|
|
2143
|
-
"auth_entry_mismatch",
|
|
2144
|
-
`authEntry function ${functionName} does not match expected transfer`
|
|
2145
|
-
);
|
|
2146
|
-
}
|
|
2147
|
-
const args = fn.args();
|
|
2148
|
-
if (args.length !== 3) {
|
|
2149
|
-
throw new NulthPolicyError("auth_entry_mismatch", `authEntry transfer has ${args.length} args, expected 3`);
|
|
2150
|
-
}
|
|
2151
|
-
const entryFrom = scValToNative(args[0]);
|
|
2152
|
-
if (entryFrom !== from) {
|
|
2153
|
-
throw new NulthPolicyError(
|
|
2154
|
-
"auth_entry_mismatch",
|
|
2155
|
-
`authEntry from ${entryFrom} does not match nulthAccount ${from}`
|
|
2156
|
-
);
|
|
2157
|
-
}
|
|
2158
|
-
const entryTo = scValToNative(args[1]);
|
|
2159
|
-
if (entryTo !== ctx.payee) {
|
|
2160
|
-
throw new NulthPolicyError(
|
|
2161
|
-
"auth_entry_mismatch",
|
|
2162
|
-
`authEntry to ${entryTo} does not match paymentContext.payee ${ctx.payee}`
|
|
2163
|
-
);
|
|
2164
|
-
}
|
|
2165
|
-
const entryAmount = scValToNative(args[2]);
|
|
2166
|
-
if (entryAmount !== ctx.amountStroops) {
|
|
2167
|
-
throw new NulthPolicyError(
|
|
2168
|
-
"auth_entry_mismatch",
|
|
2169
|
-
`authEntry amount ${entryAmount} does not match paymentContext.amountStroops ${ctx.amountStroops}`
|
|
2170
|
-
);
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
function insecureMockProof(preimage) {
|
|
2174
|
-
return createHash("sha512").update(`nulth:${preimage}`).digest("base64");
|
|
2175
|
-
}
|
|
2176
|
-
function encodeAuthSignature(proof) {
|
|
2177
|
-
const payload = { nulth: "zk-v1", proof };
|
|
2178
|
-
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
2179
|
-
}
|
|
2180
|
-
var NulthClient = class {
|
|
2181
|
-
constructor(config) {
|
|
2182
|
-
this.config = config;
|
|
2183
|
-
this.policy = { ...config.policy };
|
|
2184
|
-
this.prover = config.prover ?? "mock";
|
|
2185
|
-
if (this.prover !== "mock") {
|
|
2186
|
-
throw new RouteDockManifestError(
|
|
2187
|
-
`NulthClient: unknown prover backend ${String(config.prover)}; only 'mock' is implemented`
|
|
2188
|
-
);
|
|
2543
|
+
return createNulthSigner$1(sharedConfig);
|
|
2544
|
+
} catch (error) {
|
|
2545
|
+
if (error instanceof NulthConfigError) {
|
|
2546
|
+
throw new RouteDockManifestError(error.message, { cause: error });
|
|
2189
2547
|
}
|
|
2190
|
-
|
|
2191
|
-
throw new RouteDockManifestError(
|
|
2192
|
-
`NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
|
|
2193
|
-
);
|
|
2194
|
-
}
|
|
2195
|
-
resolveLogger(config.logger)(
|
|
2196
|
-
"warn",
|
|
2197
|
-
"NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
|
|
2198
|
-
);
|
|
2548
|
+
throw error;
|
|
2199
2549
|
}
|
|
2200
|
-
config;
|
|
2201
|
-
policy;
|
|
2202
|
-
prover;
|
|
2203
|
-
get proverBackend() {
|
|
2204
|
-
return this.prover;
|
|
2205
|
-
}
|
|
2206
|
-
get nulthAccount() {
|
|
2207
|
-
return this.config.nulthAccount;
|
|
2208
|
-
}
|
|
2209
|
-
get allowlistCommitment() {
|
|
2210
|
-
return this.policy.allowlistCommitment;
|
|
2211
|
-
}
|
|
2212
|
-
get capCommitment() {
|
|
2213
|
-
return commitDailyCap(this.policy.dailyCapStroops, this.policy.witnessSecret);
|
|
2214
|
-
}
|
|
2215
|
-
buildPaymentAuthProof(context) {
|
|
2216
|
-
this.enforcePolicy(context);
|
|
2217
|
-
const bucket = dayBucketFromLedger(context.ledgerSequence);
|
|
2218
|
-
if (bucket !== this.policy.dayBucket) {
|
|
2219
|
-
this.policy.dayBucket = bucket;
|
|
2220
|
-
this.policy.dailySpendStroops = 0n;
|
|
2221
|
-
}
|
|
2222
|
-
const authDigest = authDigestFromEntry(context.authEntry);
|
|
2223
|
-
const capCommitment = this.capCommitment;
|
|
2224
|
-
const payeeHash = hashPayee(context.payee);
|
|
2225
|
-
const preimage = [
|
|
2226
|
-
authDigest,
|
|
2227
|
-
this.policy.allowlistCommitment,
|
|
2228
|
-
capCommitment,
|
|
2229
|
-
payeeHash,
|
|
2230
|
-
context.amountStroops.toString(),
|
|
2231
|
-
bucket.toString(),
|
|
2232
|
-
this.config.nulthAccount
|
|
2233
|
-
].join("|");
|
|
2234
|
-
const proof = {
|
|
2235
|
-
version: 1,
|
|
2236
|
-
proof: insecureMockProof(preimage),
|
|
2237
|
-
publicInputs: {
|
|
2238
|
-
authDigest,
|
|
2239
|
-
allowlistCommitment: this.policy.allowlistCommitment,
|
|
2240
|
-
capCommitment,
|
|
2241
|
-
payeeHash,
|
|
2242
|
-
amountStroops: context.amountStroops.toString(),
|
|
2243
|
-
dayBucket: bucket.toString()
|
|
2244
|
-
}
|
|
2245
|
-
};
|
|
2246
|
-
this.policy.dailySpendStroops += context.amountStroops;
|
|
2247
|
-
return proof;
|
|
2248
|
-
}
|
|
2249
|
-
enforcePolicy(context) {
|
|
2250
|
-
if (context.amountStroops < 0n) {
|
|
2251
|
-
throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
|
|
2252
|
-
}
|
|
2253
|
-
if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
|
|
2254
|
-
throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
|
|
2255
|
-
}
|
|
2256
|
-
if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
|
|
2257
|
-
throw new NulthPolicyError("session_expired", "Nulth session expired");
|
|
2258
|
-
}
|
|
2259
|
-
if (!this.policy.allowedPayees.includes(context.payee)) {
|
|
2260
|
-
throw new NulthPolicyError("payee_not_allowed", `Payee ${context.payee} not in off-chain allowlist`);
|
|
2261
|
-
}
|
|
2262
|
-
const bucket = dayBucketFromLedger(context.ledgerSequence);
|
|
2263
|
-
const spend = bucket === this.policy.dayBucket ? this.policy.dailySpendStroops : 0n;
|
|
2264
|
-
if (spend + context.amountStroops > this.policy.dailyCapStroops) {
|
|
2265
|
-
throw new NulthPolicyError("daily_cap_exceeded", "Nulth daily cap exceeded");
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
};
|
|
2269
|
-
function createNulthSigner(config) {
|
|
2270
|
-
const client = new NulthClient(config);
|
|
2271
|
-
const nulthAccount = config.nulthAccount;
|
|
2272
|
-
return {
|
|
2273
|
-
address: nulthAccount,
|
|
2274
|
-
signAuthEntry: async (authEntry) => {
|
|
2275
|
-
const ctx = config.paymentContext;
|
|
2276
|
-
if (!ctx) throw new Error("Nulth paymentContext must be set before signing");
|
|
2277
|
-
assertAuthEntryMatchesContext(authEntry, nulthAccount, ctx);
|
|
2278
|
-
const proof = client.buildPaymentAuthProof({ authEntry, ...ctx });
|
|
2279
|
-
return { signedAuthEntry: encodeAuthSignature(proof), signerAddress: nulthAccount };
|
|
2280
|
-
}
|
|
2281
|
-
};
|
|
2282
|
-
}
|
|
2283
|
-
function setNulthPaymentContext(config, context) {
|
|
2284
|
-
config.paymentContext = context;
|
|
2285
|
-
}
|
|
2286
|
-
function paymentContextFromManifest(manifest, mode, ledgerSequence) {
|
|
2287
|
-
const pricing = manifest.pricing[mode];
|
|
2288
|
-
if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
|
|
2289
|
-
return {
|
|
2290
|
-
payee: pricing.payee ?? manifest.payee,
|
|
2291
|
-
amountStroops: usdcToStroops(pricing.amount),
|
|
2292
|
-
assetContract: manifest.asset_contract,
|
|
2293
|
-
ledgerSequence
|
|
2294
|
-
};
|
|
2295
|
-
}
|
|
2296
|
-
function createPolicyState(input) {
|
|
2297
|
-
const dailyCapStroops = usdcToStroops(input.dailyCapUsdc);
|
|
2298
|
-
return {
|
|
2299
|
-
dailyCapStroops,
|
|
2300
|
-
allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
|
|
2301
|
-
allowedPayees: input.allowedPayees,
|
|
2302
|
-
dailySpendStroops: 0n,
|
|
2303
|
-
dayBucket: dayBucketFromLedger(input.ledgerSequence ?? 0),
|
|
2304
|
-
witnessSecret: input.witnessSecret,
|
|
2305
|
-
...input.expiryLedger !== void 0 ? { expiryLedger: input.expiryLedger } : {}
|
|
2306
|
-
};
|
|
2307
2550
|
}
|
|
2308
2551
|
function assertNulthVaultManifest(manifest, nulthAccount) {
|
|
2309
2552
|
if (manifest.vault !== void 0 && manifest.vault !== "nulth") {
|
|
@@ -2350,46 +2593,6 @@ async function prepareNulthSigner(vault, manifest, mode, network, ledgerSequence
|
|
|
2350
2593
|
const signer = createNulthSigner(config);
|
|
2351
2594
|
return { signer, config };
|
|
2352
2595
|
}
|
|
2353
|
-
function decodeAuthSignature(encoded) {
|
|
2354
|
-
const raw = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
2355
|
-
if (typeof raw !== "object" || raw === null || raw.nulth !== "zk-v1") {
|
|
2356
|
-
throw new Error("Invalid Nulth ZK auth signature");
|
|
2357
|
-
}
|
|
2358
|
-
return raw;
|
|
2359
|
-
}
|
|
2360
|
-
var FileSpendStore = class {
|
|
2361
|
-
filePath;
|
|
2362
|
-
constructor(filePath) {
|
|
2363
|
-
this.filePath = filePath;
|
|
2364
|
-
}
|
|
2365
|
-
async read() {
|
|
2366
|
-
let data;
|
|
2367
|
-
try {
|
|
2368
|
-
data = await fs.readFile(this.filePath, "utf-8");
|
|
2369
|
-
} catch (err) {
|
|
2370
|
-
if (err.code === "ENOENT") {
|
|
2371
|
-
return null;
|
|
2372
|
-
}
|
|
2373
|
-
throw new Error(
|
|
2374
|
-
`[FileSpendStore] Failed to read spend file at ${this.filePath}: ${err.message}`
|
|
2375
|
-
);
|
|
2376
|
-
}
|
|
2377
|
-
try {
|
|
2378
|
-
return JSON.parse(data);
|
|
2379
|
-
} catch {
|
|
2380
|
-
throw new Error(
|
|
2381
|
-
`[FileSpendStore] Spend file at ${this.filePath} contains invalid JSON. Delete or repair it before resuming. Raw content: ${data.slice(0, 120)}`
|
|
2382
|
-
);
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
|
-
async write(state) {
|
|
2386
|
-
const dir = path.dirname(this.filePath);
|
|
2387
|
-
await fs.mkdir(dir, { recursive: true });
|
|
2388
|
-
const tmpPath = `${this.filePath}.tmp`;
|
|
2389
|
-
await fs.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
2390
|
-
await fs.rename(tmpPath, this.filePath);
|
|
2391
|
-
}
|
|
2392
|
-
};
|
|
2393
2596
|
|
|
2394
2597
|
// src/store/SpendStore.ts
|
|
2395
2598
|
var InMemorySpendStore = class {
|
|
@@ -2503,7 +2706,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2503
2706
|
}
|
|
2504
2707
|
const secretKey = this.keypair.secret();
|
|
2505
2708
|
const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
|
|
2506
|
-
this.x402 = new X402Client(secretKey, this.network, retryPolicy);
|
|
2709
|
+
this.x402 = new X402Client(secretKey, this.network, retryPolicy, config.preSignHook, this.logger);
|
|
2507
2710
|
this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
|
|
2508
2711
|
this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
|
|
2509
2712
|
}
|
|
@@ -2511,7 +2714,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2511
2714
|
async _resolveManifest(url, options) {
|
|
2512
2715
|
const baseUrl = new URL(url).origin;
|
|
2513
2716
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2514
|
-
assertEndpointActive(manifest, url, this.logger);
|
|
2717
|
+
assertEndpointActive(manifest, url, this.logger, options?.request?.method);
|
|
2515
2718
|
const mode = selectMode(manifest, options);
|
|
2516
2719
|
return { manifest, mode };
|
|
2517
2720
|
}
|
|
@@ -2528,13 +2731,9 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2528
2731
|
});
|
|
2529
2732
|
return next;
|
|
2530
2733
|
}
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
* call before committing to a payment — for approval gates and manual
|
|
2535
|
-
* trustline remediation.
|
|
2536
|
-
*/
|
|
2537
|
-
async preflight(manifest, mode, endpoint) {
|
|
2734
|
+
async preflight(target, mode, endpoint) {
|
|
2735
|
+
if (typeof target === "string") return this._preflightUrl(target);
|
|
2736
|
+
const manifest = target;
|
|
2538
2737
|
assertManifestValid(manifest);
|
|
2539
2738
|
const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
|
|
2540
2739
|
await this._checkTrustline(manifest, selectedAsset);
|
|
@@ -2547,6 +2746,42 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2547
2746
|
...manifest.capabilities && { capabilities: manifest.capabilities }
|
|
2548
2747
|
};
|
|
2549
2748
|
}
|
|
2749
|
+
/** Manifest-only preflight for a URL — no trustline check, no payment. */
|
|
2750
|
+
async _preflightUrl(url) {
|
|
2751
|
+
const baseUrl = new URL(url).origin;
|
|
2752
|
+
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2753
|
+
assertEndpointActive(manifest, url);
|
|
2754
|
+
const costs = manifest.modes.map((mode) => {
|
|
2755
|
+
const asset = selectAsset(manifest, mode, url).asset;
|
|
2756
|
+
switch (mode) {
|
|
2757
|
+
case "x402":
|
|
2758
|
+
case "mpp-charge": {
|
|
2759
|
+
const pricing = manifest.pricing[mode];
|
|
2760
|
+
return { mode, amount: pricing.amount, per: "request", asset };
|
|
2761
|
+
}
|
|
2762
|
+
case "mpp-session":
|
|
2763
|
+
case "mpp-session-ws": {
|
|
2764
|
+
const pricing = manifest.pricing[mode];
|
|
2765
|
+
return { mode, amount: pricing.rate, per: "voucher", asset, min_deposit: pricing.min_deposit };
|
|
2766
|
+
}
|
|
2767
|
+
default:
|
|
2768
|
+
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2769
|
+
}
|
|
2770
|
+
});
|
|
2771
|
+
const pathname = new URL(url).pathname;
|
|
2772
|
+
const matches = Object.values(manifest.endpoints).filter((descriptor) => descriptor.path === pathname);
|
|
2773
|
+
const endpointSunsets = matches.map((descriptor) => descriptor.sunset_at).filter((value) => typeof value === "string").sort((a, b) => Date.parse(a) - Date.parse(b));
|
|
2774
|
+
return {
|
|
2775
|
+
modes: manifest.modes,
|
|
2776
|
+
costs,
|
|
2777
|
+
compliance: {
|
|
2778
|
+
deprecated_modes: manifest.deprecated_modes ?? [],
|
|
2779
|
+
...manifest.sunset_at && { sunset_at: manifest.sunset_at },
|
|
2780
|
+
endpoint_deprecated: matches.some((descriptor) => descriptor.deprecated === true),
|
|
2781
|
+
...endpointSunsets[0] && { endpoint_sunset_at: endpointSunsets[0] }
|
|
2782
|
+
}
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2550
2785
|
/**
|
|
2551
2786
|
* Verify via Horizon that the payer account has a trustline for the
|
|
2552
2787
|
* payment asset declared in the manifest. Throws RouteDockTrustlineError
|
|
@@ -2558,8 +2793,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2558
2793
|
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
|
|
2559
2794
|
const cached = _RouteDockClient._trustlineCache.get(cacheKey);
|
|
2560
2795
|
if (cached && Date.now() < cached.expiresAt) return;
|
|
2561
|
-
const
|
|
2562
|
-
const server = new Horizon.Server(horizonUrl);
|
|
2796
|
+
const server = new Horizon.Server(HORIZON_URLS[this.network]);
|
|
2563
2797
|
try {
|
|
2564
2798
|
const account = await server.loadAccount(this.keypair.publicKey());
|
|
2565
2799
|
const balances = account.balances;
|
|
@@ -2595,9 +2829,11 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2595
2829
|
async pay(url, options) {
|
|
2596
2830
|
const baseUrl = new URL(url).origin;
|
|
2597
2831
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2598
|
-
|
|
2832
|
+
const request = options?.request;
|
|
2833
|
+
assertEndpointActive(manifest, url, this.logger, request?.method);
|
|
2599
2834
|
const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
|
|
2600
2835
|
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2836
|
+
this._assertCapAsset(selectedAsset);
|
|
2601
2837
|
await this._checkTrustline(manifest, selectedAsset);
|
|
2602
2838
|
let amount;
|
|
2603
2839
|
switch (mode) {
|
|
@@ -2615,18 +2851,18 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2615
2851
|
default:
|
|
2616
2852
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2617
2853
|
}
|
|
2618
|
-
const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
|
|
2854
|
+
const reserveId = await this._checkAndReserveSpend(amount, baseUrl, selectedAsset);
|
|
2619
2855
|
let result;
|
|
2620
2856
|
try {
|
|
2621
2857
|
if (this.vault?.mode === "nulth") {
|
|
2622
|
-
result = await this._payWithNulthVault(url, manifest, mode);
|
|
2858
|
+
result = await this._payWithNulthVault(url, manifest, mode, request);
|
|
2623
2859
|
} else {
|
|
2624
2860
|
switch (mode) {
|
|
2625
2861
|
case "x402":
|
|
2626
|
-
result = await this.x402.pay(url, manifest);
|
|
2862
|
+
result = await this.x402.pay(url, manifest, request);
|
|
2627
2863
|
break;
|
|
2628
2864
|
case "mpp-charge":
|
|
2629
|
-
result = await this.charge.pay(url, manifest);
|
|
2865
|
+
result = await this.charge.pay(url, manifest, request);
|
|
2630
2866
|
break;
|
|
2631
2867
|
default:
|
|
2632
2868
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
@@ -2645,7 +2881,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2645
2881
|
return result;
|
|
2646
2882
|
}
|
|
2647
2883
|
/** Nulth ZK vault path — proof built off-chain, attached as auth signature */
|
|
2648
|
-
async _payWithNulthVault(url, manifest, mode) {
|
|
2884
|
+
async _payWithNulthVault(url, manifest, mode, request) {
|
|
2649
2885
|
const prover = this.vault?.prover ?? "mock";
|
|
2650
2886
|
if (this.network === "mainnet" && prover === "mock") {
|
|
2651
2887
|
throw new RouteDockManifestError("nulth vault uses a MOCK Groth16 prover and cannot be used on mainnet");
|
|
@@ -2658,7 +2894,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2658
2894
|
try {
|
|
2659
2895
|
const { signer } = await prepareNulthSigner(this.vault, manifest, mode, this.network);
|
|
2660
2896
|
const x402 = this.x402.withSigner(signer);
|
|
2661
|
-
const result = await x402.pay(url, manifest);
|
|
2897
|
+
const result = await x402.pay(url, manifest, request);
|
|
2662
2898
|
return result;
|
|
2663
2899
|
} catch (err) {
|
|
2664
2900
|
if (err instanceof NulthPolicyError) {
|
|
@@ -2727,17 +2963,35 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2727
2963
|
);
|
|
2728
2964
|
}
|
|
2729
2965
|
const endpointKey = new URL(url).origin;
|
|
2730
|
-
|
|
2966
|
+
let onSpend;
|
|
2967
|
+
if (this.spendCap) {
|
|
2968
|
+
const sessionAsset = selectAsset(manifest, mode, url).asset;
|
|
2969
|
+
this._assertCapAsset(sessionAsset);
|
|
2970
|
+
onSpend = (amount) => this._recordSessionSpend(amount, endpointKey, sessionAsset);
|
|
2971
|
+
}
|
|
2731
2972
|
return this.session.openSession(url, manifest, secret, options, onSpend);
|
|
2732
2973
|
}
|
|
2974
|
+
/**
|
|
2975
|
+
* The local cap accumulates one USDC total. Refuse to count any other asset
|
|
2976
|
+
* against it (or let it through uncounted) when a cap is configured.
|
|
2977
|
+
*/
|
|
2978
|
+
_assertCapAsset(asset) {
|
|
2979
|
+
if (this.spendCap && asset !== this.spendCap.asset) {
|
|
2980
|
+
throw new RouteDockPolicyRejectError("local_cap_asset_mismatch", {
|
|
2981
|
+
cause: new Error(
|
|
2982
|
+
`spendCap is denominated in ${this.spendCap.asset} but the selected payment asset is ${asset}`
|
|
2983
|
+
)
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2733
2987
|
/**
|
|
2734
2988
|
* Record spend for an MPP session voucher. Called before each voucher
|
|
2735
2989
|
* fetch in stream(). Enforces the same local daily and per-endpoint caps
|
|
2736
2990
|
* as discrete pay() calls. Returns a commit function to clean up the
|
|
2737
2991
|
* reservation after the voucher fetch succeeds.
|
|
2738
2992
|
*/
|
|
2739
|
-
async _recordSessionSpend(amount, endpointKey) {
|
|
2740
|
-
const reserveId = await this._checkAndReserveSpend(amount, endpointKey);
|
|
2993
|
+
async _recordSessionSpend(amount, endpointKey, asset) {
|
|
2994
|
+
const reserveId = await this._checkAndReserveSpend(amount, endpointKey, asset);
|
|
2741
2995
|
if (reserveId) {
|
|
2742
2996
|
await this._commitSpend(reserveId);
|
|
2743
2997
|
}
|
|
@@ -2776,15 +3030,16 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2776
3030
|
* Both limits are independent: a payment is only reserved after **both** pass.
|
|
2777
3031
|
* All spend (regardless of endpoint) counts toward the global accumulator.
|
|
2778
3032
|
*/
|
|
2779
|
-
async _checkAndReserveSpend(amount, endpointKey) {
|
|
3033
|
+
async _checkAndReserveSpend(amount, endpointKey, asset) {
|
|
2780
3034
|
if (!this.spendCap) return "";
|
|
2781
|
-
|
|
3035
|
+
this._assertCapAsset(asset);
|
|
3036
|
+
const spendCap = this.spendCap;
|
|
3037
|
+
const amountMicros = usdcToStroops(amount);
|
|
3038
|
+
const reserve = (prev) => {
|
|
2782
3039
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2783
|
-
const
|
|
2784
|
-
const current = persisted && persisted.date === today ? persisted : { date: today, totalMicros: "0", endpoints: {} };
|
|
2785
|
-
const amountMicros = usdcToStroops(amount);
|
|
3040
|
+
const current = prev && prev.date === today ? { ...prev, endpoints: { ...prev.endpoints } } : { date: today, totalMicros: "0", endpoints: {} };
|
|
2786
3041
|
const total = BigInt(current.totalMicros);
|
|
2787
|
-
const endpointCapStr =
|
|
3042
|
+
const endpointCapStr = spendCap.endpointCaps?.[endpointKey];
|
|
2788
3043
|
if (endpointCapStr !== void 0) {
|
|
2789
3044
|
const endpointCapMicros = usdcToStroops(endpointCapStr);
|
|
2790
3045
|
const endpointTotal = BigInt(current.endpoints[endpointKey] ?? "0");
|
|
@@ -2792,17 +3047,25 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2792
3047
|
throw new RouteDockPolicyRejectError("local_endpoint_cap_exceeded");
|
|
2793
3048
|
}
|
|
2794
3049
|
}
|
|
2795
|
-
const globalCapMicros = usdcToStroops(
|
|
3050
|
+
const globalCapMicros = usdcToStroops(spendCap.daily);
|
|
2796
3051
|
if (total + amountMicros > globalCapMicros) {
|
|
2797
3052
|
throw new RouteDockPolicyRejectError("local_daily_cap_exceeded");
|
|
2798
3053
|
}
|
|
2799
|
-
const reserveId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2800
|
-
this._pendingReserves.set(reserveId, { endpointKey, amountMicros });
|
|
2801
3054
|
current.totalMicros = (total + amountMicros).toString();
|
|
2802
3055
|
if (endpointCapStr !== void 0) {
|
|
2803
3056
|
current.endpoints[endpointKey] = (BigInt(current.endpoints[endpointKey] ?? "0") + amountMicros).toString();
|
|
2804
3057
|
}
|
|
2805
|
-
|
|
3058
|
+
return current;
|
|
3059
|
+
};
|
|
3060
|
+
return this._withSpendMutex(async () => {
|
|
3061
|
+
const store = this.spendStore;
|
|
3062
|
+
if (store.update) {
|
|
3063
|
+
await store.update(reserve);
|
|
3064
|
+
} else {
|
|
3065
|
+
await store.write(reserve(await store.read()));
|
|
3066
|
+
}
|
|
3067
|
+
const reserveId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3068
|
+
this._pendingReserves.set(reserveId, { endpointKey, amountMicros });
|
|
2806
3069
|
return reserveId;
|
|
2807
3070
|
});
|
|
2808
3071
|
}
|
|
@@ -2824,22 +3087,29 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2824
3087
|
if (!reserve) return;
|
|
2825
3088
|
this._pendingReserves.delete(reserveId);
|
|
2826
3089
|
const { endpointKey, amountMicros } = reserve;
|
|
2827
|
-
|
|
3090
|
+
const release = (prev) => {
|
|
2828
3091
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
persisted.totalMicros = (currentTotal - amountMicros).toString();
|
|
3092
|
+
if (!prev || prev.date !== today) return void 0;
|
|
3093
|
+
const persisted = { ...prev, endpoints: { ...prev.endpoints } };
|
|
3094
|
+
persisted.totalMicros = (BigInt(persisted.totalMicros) - amountMicros).toString();
|
|
2833
3095
|
if (persisted.endpoints[endpointKey]) {
|
|
2834
|
-
const
|
|
2835
|
-
const newEndpointTotal = endpointTotal - amountMicros;
|
|
3096
|
+
const newEndpointTotal = BigInt(persisted.endpoints[endpointKey]) - amountMicros;
|
|
2836
3097
|
if (newEndpointTotal <= 0n) {
|
|
2837
3098
|
delete persisted.endpoints[endpointKey];
|
|
2838
3099
|
} else {
|
|
2839
3100
|
persisted.endpoints[endpointKey] = newEndpointTotal.toString();
|
|
2840
3101
|
}
|
|
2841
3102
|
}
|
|
2842
|
-
|
|
3103
|
+
return persisted;
|
|
3104
|
+
};
|
|
3105
|
+
await this._withSpendMutex(async () => {
|
|
3106
|
+
const store = this.spendStore;
|
|
3107
|
+
if (store.update) {
|
|
3108
|
+
await store.update(release);
|
|
3109
|
+
} else {
|
|
3110
|
+
const next = release(await store.read());
|
|
3111
|
+
if (next) await store.write(next);
|
|
3112
|
+
}
|
|
2843
3113
|
});
|
|
2844
3114
|
}
|
|
2845
3115
|
};
|
|
@@ -2928,7 +3198,7 @@ function tryDecodeBase64(value) {
|
|
|
2928
3198
|
return value;
|
|
2929
3199
|
}
|
|
2930
3200
|
try {
|
|
2931
|
-
const decoded =
|
|
3201
|
+
const decoded = base64ToUtf8(value);
|
|
2932
3202
|
if (decoded.length > 0 && decoded !== value) return decoded;
|
|
2933
3203
|
} catch {
|
|
2934
3204
|
}
|
|
@@ -3065,4 +3335,4 @@ var ProviderRegistry = class {
|
|
|
3065
3335
|
}
|
|
3066
3336
|
};
|
|
3067
3337
|
|
|
3068
|
-
export { DEFAULT_MAX_SESSION_DURATION_MS, DEFAULT_RETRY_POLICY,
|
|
3338
|
+
export { DEFAULT_MAX_SESSION_DURATION_MS, DEFAULT_RETRY_POLICY, InMemorySpendStore, MppChargeClient, MppSessionClient, NulthStellarScheme, OnChainRegistry, ProviderRegistry, RouteDockChannelStateError, RouteDockClient, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockManifestSunsetError, RouteDockManifestTimeoutError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPaymentRejectedError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SupabaseSessionStore, TX_CONFIRM_INTERVAL_MS, TX_CONFIRM_TIMEOUT_MS, X402Client, assertEndpointActive, assertManifestValid, assertNulthVaultManifest, backoffDelayMs, configureManifestCache, consoleLogger, createConsoleLogger, createNulthSigner, fetchLedgerSequence, fetchManifest, getEligibleAssets, httpStatusToError, invalidateManifest, isAssetEligible, noopLogger, normalizeManifestAssets, parseRetryAfterMs, paymentRejectedError, prepareNulthSigner, rankProvidersByLatency, resolveAssetContract, resolveLogger, selectAsset, selectMode, selectStellarScheme, usdcToMicros, usdcToUnits, withRetry, wrapFetchError, wrapMppError };
|