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