@routedock/routedock 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -12
- package/dist/{MppSessionHandler-1EL8QBAn.d.cts → MppSessionHandler-3QfCHI5x.d.ts} +40 -2
- package/dist/{MppSessionHandler-Bq4S-JTE.d.ts → MppSessionHandler-xu4Kxu6N.d.cts} +40 -2
- package/dist/{ProviderRegistry-CA_TdREG.d.ts → ProviderRegistry-D9KqyuH-.d.cts} +16 -36
- package/dist/{ProviderRegistry-D-0rqK0N.d.cts → ProviderRegistry-DVi2uzgk.d.ts} +16 -36
- package/dist/{RouteDockClient-go9aopfF.d.cts → RouteDockClient-6rmgkbYb.d.cts} +124 -142
- package/dist/{RouteDockClient-Bh86bVly.d.ts → RouteDockClient-CnAkxwrW.d.ts} +124 -142
- package/dist/{SessionReconciler-Bw-PoBVl.d.ts → SessionReconciler-Cce9MXO1.d.ts} +10 -3
- package/dist/{SessionReconciler-Cz2nuOlJ.d.cts → SessionReconciler-Dpe9qXdD.d.cts} +10 -3
- package/dist/{SessionStore-BaYiegP0.d.ts → SessionStore-DkLBxFQu.d.cts} +28 -3
- package/dist/{SessionStore-B4-Hi4H8.d.cts → SessionStore-_ZkoBzFY.d.ts} +28 -3
- package/dist/SpendStore-BUclV02i.d.ts +73 -0
- package/dist/SpendStore-s_dfcemj.d.cts +73 -0
- package/dist/client.cjs +767 -487
- package/dist/client.d.cts +80 -10
- package/dist/client.d.ts +80 -10
- package/dist/client.js +733 -468
- package/dist/index.cjs +1295 -590
- package/dist/index.d.cts +13 -10
- package/dist/index.d.ts +13 -10
- package/dist/index.js +1247 -559
- package/dist/logger-DVa7VT4c.d.cts +41 -0
- package/dist/logger-DVa7VT4c.d.ts +41 -0
- package/dist/network.cjs +57 -0
- package/dist/network.d.cts +72 -0
- package/dist/network.d.ts +72 -0
- package/dist/network.js +49 -0
- package/dist/node.cjs +166 -0
- package/dist/node.d.cts +77 -0
- package/dist/node.d.ts +77 -0
- package/dist/node.js +159 -0
- package/dist/provider/fastify.cjs +847 -62
- package/dist/provider/fastify.d.cts +10 -3
- package/dist/provider/fastify.d.ts +10 -3
- package/dist/provider/fastify.js +848 -63
- package/dist/provider/hono.cjs +936 -60
- package/dist/provider/hono.d.cts +23 -8
- package/dist/provider/hono.d.ts +23 -8
- package/dist/provider/hono.js +937 -61
- package/dist/provider.cjs +894 -70
- package/dist/provider.d.cts +9 -8
- package/dist/provider.d.ts +9 -8
- package/dist/provider.js +895 -72
- package/dist/react.cjs +736 -436
- package/dist/react.d.cts +7 -5
- package/dist/react.d.ts +7 -5
- package/dist/react.js +725 -425
- package/dist/{routedockMiddleware-57gUaEMf.d.ts → routedockMiddleware-Cq9HOJEa.d.ts} +9 -2
- package/dist/{routedockMiddleware-DlyLrxp7.d.cts → routedockMiddleware-CxsWkSKa.d.cts} +9 -2
- package/dist/schema.cjs +7 -0
- package/dist/schema.js +7 -0
- package/dist/{sign-AKH67sft.d.cts → sign-Bq08SeFq.d.ts} +2 -2
- package/dist/{sign-BEq-OsSq.d.ts → sign-DqGE0Plv.d.cts} +2 -2
- package/dist/testing.d.cts +4 -3
- package/dist/testing.d.ts +4 -3
- package/dist/{logger-D8_Q2ytp.d.cts → types-DXaS5Wip.d.cts} +85 -47
- package/dist/{logger-D8_Q2ytp.d.ts → types-DXaS5Wip.d.ts} +85 -47
- package/package.json +50 -4
package/dist/index.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { usdcToStroops, stroopsToUsdc } from '@routedock/nulth-sdk';
|
|
2
|
-
export { USDC_DECIMALS, usdcToStroops } from '@routedock/nulth-sdk';
|
|
3
|
-
import { Keypair, rpc, Horizon,
|
|
1
|
+
import { usdcToStroops, stroopsToUsdc, createNulthSigner as createNulthSigner$1, NulthConfigError, createPolicyState, setNulthPaymentContext, paymentContextFromManifest, NulthPolicyError, isNulthStellarSigner, nulthAuthorizeEntry } from '@routedock/nulth-sdk';
|
|
2
|
+
export { NulthPolicyError, USDC_DECIMALS, createPolicyState, decodeAuthSignature, paymentContextFromManifest, setNulthPaymentContext, usdcToStroops } from '@routedock/nulth-sdk';
|
|
3
|
+
import { Keypair, rpc, Horizon, Networks, StrKey, contract, nativeToScVal, Contract, TransactionBuilder, BASE_FEE } 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 { createEd25519Signer, ExactStellarScheme, getRpcUrl, getNetworkPassphrase, handleSimulationResult, isStellarNetwork, validateStellarDestinationAddress, validateStellarAssetAddress, getRpcClient, getEstimatedLedgerCloseTimeSeconds } from '@x402/stellar';
|
|
8
7
|
import { x402Client, x402HTTPClient } from '@x402/core/client';
|
|
9
8
|
import { decodePaymentRequiredHeader, decodePaymentResponseHeader, encodePaymentRequiredHeader, decodePaymentSignatureHeader, encodePaymentResponseHeader } from '@x402/core/http';
|
|
10
9
|
import { stellar } from '@stellar/mpp/charge/client';
|
|
11
10
|
import { Mppx } from 'mppx/client';
|
|
11
|
+
import { Receipt, Challenge, Errors } 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 {
|
|
14
|
+
import { randomBytes } from 'crypto';
|
|
15
15
|
import fs from 'fs/promises';
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import { ExactStellarScheme as ExactStellarScheme$1 } from '@x402/stellar/exact/facilitator';
|
|
@@ -117,6 +117,20 @@ var RouteDockPolicyRejectError = class extends RouteDockError {
|
|
|
117
117
|
this.reason = reason;
|
|
118
118
|
}
|
|
119
119
|
};
|
|
120
|
+
var RouteDockPaymentRejectedError = class extends RouteDockError {
|
|
121
|
+
status;
|
|
122
|
+
/** The provider's `reason` field, if its response body was JSON and had one. */
|
|
123
|
+
reason;
|
|
124
|
+
/** The provider's `error` field (short summary), if present. */
|
|
125
|
+
providerError;
|
|
126
|
+
constructor(message, status, details = {}, options = {}) {
|
|
127
|
+
super(message, "PAYMENT_REJECTED", false, options);
|
|
128
|
+
this.name = "RouteDockPaymentRejectedError";
|
|
129
|
+
this.status = status;
|
|
130
|
+
if (details.reason !== void 0) this.reason = details.reason;
|
|
131
|
+
if (details.providerError !== void 0) this.providerError = details.providerError;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
120
134
|
var RouteDockChannelStateError = class extends RouteDockError {
|
|
121
135
|
constructor(message, options = {}) {
|
|
122
136
|
super(message, "CHANNEL_STATE", false, options);
|
|
@@ -161,6 +175,59 @@ function httpStatusToError(message, status, response) {
|
|
|
161
175
|
}
|
|
162
176
|
return new RouteDockNetworkError(message, opts);
|
|
163
177
|
}
|
|
178
|
+
var MAX_REJECTION_BODY_BYTES = 64 * 1024;
|
|
179
|
+
var MAX_REJECTION_FIELD_CHARS = 500;
|
|
180
|
+
async function readBoundedText(response, limit) {
|
|
181
|
+
const body = response.body;
|
|
182
|
+
if (!body) return null;
|
|
183
|
+
const reader = body.getReader();
|
|
184
|
+
const chunks = [];
|
|
185
|
+
let total = 0;
|
|
186
|
+
try {
|
|
187
|
+
for (; ; ) {
|
|
188
|
+
const { done, value } = await reader.read();
|
|
189
|
+
if (done) break;
|
|
190
|
+
total += value.byteLength;
|
|
191
|
+
if (total > limit) {
|
|
192
|
+
await reader.cancel().catch(() => void 0);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
chunks.push(value);
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const bytes = new Uint8Array(total);
|
|
201
|
+
let offset = 0;
|
|
202
|
+
for (const chunk of chunks) {
|
|
203
|
+
bytes.set(chunk, offset);
|
|
204
|
+
offset += chunk.byteLength;
|
|
205
|
+
}
|
|
206
|
+
return new TextDecoder().decode(bytes);
|
|
207
|
+
}
|
|
208
|
+
async function paymentRejectedError(label, status, response) {
|
|
209
|
+
let reason;
|
|
210
|
+
let providerError;
|
|
211
|
+
try {
|
|
212
|
+
const text = await readBoundedText(response, MAX_REJECTION_BODY_BYTES);
|
|
213
|
+
const body = text === null ? null : JSON.parse(text);
|
|
214
|
+
if (body !== null && typeof body === "object") {
|
|
215
|
+
const { reason: r, error: e } = body;
|
|
216
|
+
if (typeof r === "string" && r.length > 0) reason = r.slice(0, MAX_REJECTION_FIELD_CHARS);
|
|
217
|
+
if (typeof e === "string" && e.length > 0) providerError = e.slice(0, MAX_REJECTION_FIELD_CHARS);
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
const detail = reason ?? providerError;
|
|
222
|
+
return new RouteDockPaymentRejectedError(
|
|
223
|
+
`${label}: HTTP ${status}${detail ? ` \u2014 ${detail}` : ""}`,
|
|
224
|
+
status,
|
|
225
|
+
{
|
|
226
|
+
...reason !== void 0 ? { reason } : {},
|
|
227
|
+
...providerError !== void 0 ? { providerError } : {}
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
}
|
|
164
231
|
function wrapFetchError(err, context) {
|
|
165
232
|
if (err instanceof RouteDockError) return err;
|
|
166
233
|
return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
|
|
@@ -225,23 +292,34 @@ async function withRetry(fn, policy = {}) {
|
|
|
225
292
|
if (!err.retryable || attempt >= maxAttempts - 1) {
|
|
226
293
|
throw err;
|
|
227
294
|
}
|
|
228
|
-
const
|
|
295
|
+
const delay2 = Math.min(
|
|
296
|
+
err.retryAfterMs ?? backoffDelayMs(attempt, baseDelayMs, maxDelayMs),
|
|
297
|
+
maxDelayMs
|
|
298
|
+
);
|
|
229
299
|
if (onRetry) {
|
|
230
300
|
try {
|
|
231
|
-
await onRetry(attempt, err,
|
|
301
|
+
await onRetry(attempt, err, delay2);
|
|
232
302
|
} catch (cbErr) {
|
|
233
303
|
log("error", "[withRetry] onRetry callback threw:", { error: cbErr });
|
|
234
304
|
}
|
|
235
305
|
}
|
|
236
|
-
await sleep(
|
|
306
|
+
await sleep(delay2);
|
|
237
307
|
}
|
|
238
308
|
}
|
|
239
309
|
throw lastError ?? new RouteDockError("Retry exhausted", "NETWORK", false);
|
|
240
310
|
}
|
|
311
|
+
|
|
312
|
+
// src/network.ts
|
|
241
313
|
var USDC_ISSUERS = {
|
|
242
314
|
testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
|
|
243
315
|
mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
|
|
244
316
|
};
|
|
317
|
+
var HORIZON_URLS = {
|
|
318
|
+
testnet: "https://horizon-testnet.stellar.org",
|
|
319
|
+
mainnet: "https://horizon.stellar.org"
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// src/internal/usdc.ts
|
|
245
323
|
var usdcToUnits = usdcToStroops;
|
|
246
324
|
|
|
247
325
|
// src/internal/assetUtils.ts
|
|
@@ -322,6 +400,138 @@ function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
|
|
|
322
400
|
);
|
|
323
401
|
}
|
|
324
402
|
|
|
403
|
+
// package.json
|
|
404
|
+
var package_default = {
|
|
405
|
+
version: "0.4.0"};
|
|
406
|
+
|
|
407
|
+
// src/internal/encoding.ts
|
|
408
|
+
var encoder = new TextEncoder();
|
|
409
|
+
var decoder = new TextDecoder();
|
|
410
|
+
function utf8ToBytes(text) {
|
|
411
|
+
return encoder.encode(text);
|
|
412
|
+
}
|
|
413
|
+
function bytesToUtf8(bytes) {
|
|
414
|
+
return decoder.decode(bytes);
|
|
415
|
+
}
|
|
416
|
+
function bytesToBase64(bytes) {
|
|
417
|
+
const chunk = 32768;
|
|
418
|
+
let binary = "";
|
|
419
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
420
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
421
|
+
}
|
|
422
|
+
return btoa(binary);
|
|
423
|
+
}
|
|
424
|
+
function base64ToBytes(b64) {
|
|
425
|
+
const end = b64.indexOf("=");
|
|
426
|
+
let clean = (end === -1 ? b64 : b64.slice(0, end)).replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
|
|
427
|
+
if (clean.length % 4 === 1) clean = clean.slice(0, -1);
|
|
428
|
+
const binary = atob(clean);
|
|
429
|
+
const bytes = new Uint8Array(binary.length);
|
|
430
|
+
for (let i = 0; i < binary.length; i++) {
|
|
431
|
+
bytes[i] = binary.charCodeAt(i);
|
|
432
|
+
}
|
|
433
|
+
return bytes;
|
|
434
|
+
}
|
|
435
|
+
function base64ToUtf8(b64) {
|
|
436
|
+
return bytesToUtf8(base64ToBytes(b64));
|
|
437
|
+
}
|
|
438
|
+
function hexToBytes(hex) {
|
|
439
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
440
|
+
if (clean.length % 2 !== 0) {
|
|
441
|
+
throw new Error("hexToBytes: hex string must have an even length");
|
|
442
|
+
}
|
|
443
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
444
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
445
|
+
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
446
|
+
}
|
|
447
|
+
return bytes;
|
|
448
|
+
}
|
|
449
|
+
function bytesToHex(bytes) {
|
|
450
|
+
let out = "";
|
|
451
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
452
|
+
out += bytes[i].toString(16).padStart(2, "0");
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
function sha256(data) {
|
|
457
|
+
return sha256$1(typeof data === "string" ? utf8ToBytes(data) : data);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/manifest/sign.ts
|
|
461
|
+
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
462
|
+
function canonicalize(value) {
|
|
463
|
+
if (Array.isArray(value)) {
|
|
464
|
+
return value.map(canonicalize);
|
|
465
|
+
}
|
|
466
|
+
if (value !== null && typeof value === "object") {
|
|
467
|
+
return Object.fromEntries(
|
|
468
|
+
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return value;
|
|
472
|
+
}
|
|
473
|
+
function manifestDigest(manifest) {
|
|
474
|
+
const { signature: _omit, ...rest } = manifest;
|
|
475
|
+
const canonical = JSON.stringify(canonicalize(rest));
|
|
476
|
+
return sha256(utf8ToBytes(canonical));
|
|
477
|
+
}
|
|
478
|
+
function signManifest(manifest, payeeSecretKey) {
|
|
479
|
+
const keypair = Keypair.fromSecret(payeeSecretKey);
|
|
480
|
+
const versionedManifest = {
|
|
481
|
+
...manifest,
|
|
482
|
+
signature_version: MANIFEST_SIGNATURE_VERSION
|
|
483
|
+
};
|
|
484
|
+
const digest = manifestDigest(versionedManifest);
|
|
485
|
+
const sig = keypair.sign(digest);
|
|
486
|
+
return { ...versionedManifest, signature: bytesToBase64(sig) };
|
|
487
|
+
}
|
|
488
|
+
function verifyManifestSignature(manifest, expectedPayee) {
|
|
489
|
+
if (!manifest.signature) {
|
|
490
|
+
throw new RouteDockSignatureError(
|
|
491
|
+
"Manifest is missing a signature field \u2014 cannot verify payee authenticity"
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
if (manifest.signature_version !== MANIFEST_SIGNATURE_VERSION) {
|
|
495
|
+
throw new RouteDockSignatureError(
|
|
496
|
+
`Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
if (expectedPayee !== void 0 && manifest.payee !== expectedPayee) {
|
|
500
|
+
throw new RouteDockSignatureError(
|
|
501
|
+
`Manifest payee ${manifest.payee} does not match expected payee ${expectedPayee}`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
let keypair;
|
|
505
|
+
try {
|
|
506
|
+
keypair = Keypair.fromPublicKey(manifest.payee);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
throw new RouteDockSignatureError(
|
|
509
|
+
`Manifest payee ${manifest.payee} is not a valid Stellar public key`,
|
|
510
|
+
{ cause: err }
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
const digest = manifestDigest(manifest);
|
|
514
|
+
let sigBytes;
|
|
515
|
+
try {
|
|
516
|
+
sigBytes = base64ToBytes(manifest.signature);
|
|
517
|
+
} catch {
|
|
518
|
+
throw new RouteDockSignatureError("Manifest signature is not valid base64");
|
|
519
|
+
}
|
|
520
|
+
let valid = false;
|
|
521
|
+
try {
|
|
522
|
+
valid = keypair.verify(digest, sigBytes);
|
|
523
|
+
} catch {
|
|
524
|
+
throw new RouteDockSignatureError(
|
|
525
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (!valid) {
|
|
529
|
+
throw new RouteDockSignatureError(
|
|
530
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
325
535
|
// src/schemas/routedock.schema.json
|
|
326
536
|
var routedock_schema_default = {
|
|
327
537
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
@@ -399,6 +609,7 @@ var routedock_schema_default = {
|
|
|
399
609
|
},
|
|
400
610
|
asset_contract: {
|
|
401
611
|
type: "string",
|
|
612
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
402
613
|
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
403
614
|
},
|
|
404
615
|
assets: {
|
|
@@ -411,6 +622,7 @@ var routedock_schema_default = {
|
|
|
411
622
|
},
|
|
412
623
|
payee: {
|
|
413
624
|
type: "string",
|
|
625
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
414
626
|
description: "Stellar address (G...) that receives payments"
|
|
415
627
|
},
|
|
416
628
|
pricing: {
|
|
@@ -581,6 +793,7 @@ var routedock_schema_default = {
|
|
|
581
793
|
properties: {
|
|
582
794
|
amount: {
|
|
583
795
|
type: "string",
|
|
796
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
584
797
|
description: "Cost per request in the payment asset (decimal string, e.g. '0.001')"
|
|
585
798
|
},
|
|
586
799
|
per: {
|
|
@@ -594,6 +807,7 @@ var routedock_schema_default = {
|
|
|
594
807
|
},
|
|
595
808
|
payee: {
|
|
596
809
|
type: "string",
|
|
810
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
597
811
|
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."
|
|
598
812
|
}
|
|
599
813
|
}
|
|
@@ -606,6 +820,7 @@ var routedock_schema_default = {
|
|
|
606
820
|
properties: {
|
|
607
821
|
rate: {
|
|
608
822
|
type: "string",
|
|
823
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
609
824
|
description: "Cost per voucher in the payment asset (decimal string, e.g. '0.0001')"
|
|
610
825
|
},
|
|
611
826
|
per: {
|
|
@@ -615,10 +830,12 @@ var routedock_schema_default = {
|
|
|
615
830
|
},
|
|
616
831
|
channel_factory: {
|
|
617
832
|
type: "string",
|
|
833
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
618
834
|
description: "Stable Soroban factory address (C...) used to deploy a per-session channel"
|
|
619
835
|
},
|
|
620
836
|
min_deposit: {
|
|
621
837
|
type: "string",
|
|
838
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
622
839
|
description: "Minimum initial deposit to open a channel (decimal string)"
|
|
623
840
|
},
|
|
624
841
|
refund_waiting_period_ledgers: {
|
|
@@ -716,78 +933,110 @@ var routedock_schema_default = {
|
|
|
716
933
|
}
|
|
717
934
|
};
|
|
718
935
|
|
|
719
|
-
//
|
|
720
|
-
var
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
}
|
|
727
|
-
if (value !== null && typeof value === "object") {
|
|
728
|
-
return Object.fromEntries(
|
|
729
|
-
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
|
|
730
|
-
);
|
|
936
|
+
// src/manifest/validate.ts
|
|
937
|
+
var validator = new Validator(routedock_schema_default, "7");
|
|
938
|
+
function validateManifestSchema(manifest, source) {
|
|
939
|
+
const result = validator.validate(manifest);
|
|
940
|
+
if (!result.valid) {
|
|
941
|
+
const msgs = result.errors.map((err) => err.error).join("; ");
|
|
942
|
+
throw new RouteDockManifestError(`Invalid manifest${source ? ` at ${source}` : ""}: ${msgs}`);
|
|
731
943
|
}
|
|
732
|
-
return value;
|
|
733
|
-
}
|
|
734
|
-
function manifestDigest(manifest) {
|
|
735
|
-
const { signature: _omit, ...rest } = manifest;
|
|
736
|
-
const canonical = JSON.stringify(canonicalize(rest));
|
|
737
|
-
return createHash("sha256").update(canonical, "utf8").digest();
|
|
738
|
-
}
|
|
739
|
-
function signManifest(manifest, payeeSecretKey) {
|
|
740
|
-
const keypair = Keypair.fromSecret(payeeSecretKey);
|
|
741
|
-
const versionedManifest = {
|
|
742
|
-
...manifest,
|
|
743
|
-
signature_version: MANIFEST_SIGNATURE_VERSION
|
|
744
|
-
};
|
|
745
|
-
const digest = manifestDigest(versionedManifest);
|
|
746
|
-
const sig = keypair.sign(digest);
|
|
747
|
-
return { ...versionedManifest, signature: Buffer.from(sig).toString("base64") };
|
|
748
944
|
}
|
|
749
|
-
function
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
)
|
|
945
|
+
function assertManifestValid(manifest, baseUrl) {
|
|
946
|
+
const context = baseUrl ? ` at ${baseUrl}` : "";
|
|
947
|
+
if (manifest.latency_hints !== void 0) {
|
|
948
|
+
if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
|
|
949
|
+
throw new RouteDockManifestError(
|
|
950
|
+
`Invalid manifest${context}: latency_hints defined without declaring regions`
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
const regionSet = new Set(manifest.regions);
|
|
954
|
+
for (const region of Object.keys(manifest.latency_hints)) {
|
|
955
|
+
if (!regionSet.has(region)) {
|
|
956
|
+
throw new RouteDockManifestError(
|
|
957
|
+
`Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
759
961
|
}
|
|
760
|
-
if (
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
962
|
+
if (manifest.assets !== void 0) {
|
|
963
|
+
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
964
|
+
throw new RouteDockManifestError(
|
|
965
|
+
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
for (let i = 0; i < manifest.assets.length; i++) {
|
|
969
|
+
const a = manifest.assets[i];
|
|
970
|
+
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
971
|
+
throw new RouteDockManifestError(
|
|
972
|
+
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const first = manifest.assets[0];
|
|
977
|
+
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
978
|
+
throw new RouteDockManifestError(
|
|
979
|
+
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
764
982
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
983
|
+
assertPricingMatchesModes(manifest, context);
|
|
984
|
+
assertDeprecatedModesListed(manifest, context);
|
|
985
|
+
assertPricesParse(manifest, context);
|
|
986
|
+
}
|
|
987
|
+
function assertPricingMatchesModes(manifest, context) {
|
|
988
|
+
const pricing = manifest.pricing ?? {};
|
|
989
|
+
for (const mode of manifest.modes ?? []) {
|
|
990
|
+
if (pricing[mode] === void 0 || pricing[mode] === null) {
|
|
991
|
+
throw new RouteDockManifestError(
|
|
992
|
+
`Invalid manifest${context}: mode '${mode}' is listed in modes but pricing.${mode} is missing`
|
|
993
|
+
);
|
|
994
|
+
}
|
|
772
995
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
996
|
+
}
|
|
997
|
+
function assertDeprecatedModesListed(manifest, context) {
|
|
998
|
+
if (manifest.deprecated_modes === void 0) return;
|
|
999
|
+
const modes = new Set(manifest.modes ?? []);
|
|
1000
|
+
for (const mode of manifest.deprecated_modes) {
|
|
1001
|
+
if (!modes.has(mode)) {
|
|
1002
|
+
throw new RouteDockManifestError(
|
|
1003
|
+
`Invalid manifest${context}: deprecated_modes entry '${mode}' is not listed in modes`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
780
1006
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1007
|
+
}
|
|
1008
|
+
var PRICE_FIELDS = {
|
|
1009
|
+
x402: ["amount"],
|
|
1010
|
+
"mpp-charge": ["amount"],
|
|
1011
|
+
"mpp-session": ["rate", "min_deposit"],
|
|
1012
|
+
"mpp-session-ws": ["rate", "min_deposit"]
|
|
1013
|
+
};
|
|
1014
|
+
function assertPricesParse(manifest, context) {
|
|
1015
|
+
const pricing = manifest.pricing ?? {};
|
|
1016
|
+
for (const [mode, fields] of Object.entries(PRICE_FIELDS)) {
|
|
1017
|
+
const entry = pricing[mode];
|
|
1018
|
+
if (entry === void 0 || entry === null) continue;
|
|
1019
|
+
for (const field of fields) {
|
|
1020
|
+
const value = entry[field];
|
|
1021
|
+
try {
|
|
1022
|
+
if (typeof value !== "string") {
|
|
1023
|
+
throw new RangeError(`expected a decimal string, got ${typeof value}`);
|
|
1024
|
+
}
|
|
1025
|
+
usdcToStroops(value);
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1028
|
+
throw new RouteDockManifestError(
|
|
1029
|
+
`Invalid manifest${context}: pricing.${mode}.${field} ${JSON.stringify(value)} is not a valid amount (${detail})`,
|
|
1030
|
+
{ cause: err }
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
785
1034
|
}
|
|
786
1035
|
}
|
|
787
1036
|
|
|
788
1037
|
// src/client/ModeRouter.ts
|
|
789
|
-
var validator = new Validator(routedock_schema_default, "7");
|
|
790
1038
|
var SDK_VERSION = package_default.version;
|
|
1039
|
+
var SUPPORTED_MANIFEST_MAJOR = 1;
|
|
791
1040
|
function parseMajorMinor(version) {
|
|
792
1041
|
const parts = version.split(".");
|
|
793
1042
|
return [Number.parseInt(parts[0] ?? "0", 10), Number.parseInt(parts[1] ?? "0", 10)];
|
|
@@ -805,6 +1054,14 @@ function assertClientVersionSupported(manifest, baseUrl) {
|
|
|
805
1054
|
);
|
|
806
1055
|
}
|
|
807
1056
|
}
|
|
1057
|
+
function assertManifestVersionSupported(manifest, baseUrl) {
|
|
1058
|
+
const [major] = parseMajorMinor(manifest.routedock);
|
|
1059
|
+
if (major > SUPPORTED_MANIFEST_MAJOR) {
|
|
1060
|
+
throw new RouteDockClientVersionError(
|
|
1061
|
+
`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.`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
808
1065
|
function assertManifestActive(manifest, baseUrl) {
|
|
809
1066
|
const sunsetAt = manifest.sunset_at;
|
|
810
1067
|
if (!sunsetAt) return;
|
|
@@ -820,11 +1077,16 @@ function assertManifestActive(manifest, baseUrl) {
|
|
|
820
1077
|
);
|
|
821
1078
|
}
|
|
822
1079
|
}
|
|
823
|
-
function assertEndpointActive(manifest, url, logger) {
|
|
1080
|
+
function assertEndpointActive(manifest, url, logger, method) {
|
|
824
1081
|
const pathname = new URL(url).pathname;
|
|
825
|
-
const
|
|
1082
|
+
const pathMatches = Object.entries(manifest.endpoints).filter(
|
|
826
1083
|
([, descriptor]) => descriptor.path === pathname
|
|
827
1084
|
);
|
|
1085
|
+
if (pathMatches.length === 0) return;
|
|
1086
|
+
const requestedMethod = method?.toUpperCase();
|
|
1087
|
+
const matches = requestedMethod ? pathMatches.filter(
|
|
1088
|
+
([, descriptor]) => descriptor.method.toUpperCase() === requestedMethod
|
|
1089
|
+
) : pathMatches;
|
|
828
1090
|
if (matches.length === 0) return;
|
|
829
1091
|
const now = Date.now();
|
|
830
1092
|
const allSunset = matches.every(([, descriptor]) => {
|
|
@@ -847,45 +1109,6 @@ function assertEndpointActive(manifest, url, logger) {
|
|
|
847
1109
|
);
|
|
848
1110
|
}
|
|
849
1111
|
}
|
|
850
|
-
function assertManifestValid(manifest, baseUrl) {
|
|
851
|
-
const context = baseUrl ? ` at ${baseUrl}` : "";
|
|
852
|
-
if (manifest.latency_hints !== void 0) {
|
|
853
|
-
if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
|
|
854
|
-
throw new RouteDockManifestError(
|
|
855
|
-
`Invalid manifest${context}: latency_hints defined without declaring regions`
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
const regionSet = new Set(manifest.regions);
|
|
859
|
-
for (const region of Object.keys(manifest.latency_hints)) {
|
|
860
|
-
if (!regionSet.has(region)) {
|
|
861
|
-
throw new RouteDockManifestError(
|
|
862
|
-
`Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
if (manifest.assets !== void 0) {
|
|
868
|
-
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
869
|
-
throw new RouteDockManifestError(
|
|
870
|
-
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
871
|
-
);
|
|
872
|
-
}
|
|
873
|
-
for (let i = 0; i < manifest.assets.length; i++) {
|
|
874
|
-
const a = manifest.assets[i];
|
|
875
|
-
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
876
|
-
throw new RouteDockManifestError(
|
|
877
|
-
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
878
|
-
);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
const first = manifest.assets[0];
|
|
882
|
-
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
883
|
-
throw new RouteDockManifestError(
|
|
884
|
-
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
1112
|
var CACHE_TTL_MS = 6e4;
|
|
890
1113
|
var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
|
|
891
1114
|
var DEFAULT_MANIFEST_TIMEOUT_MS = 5e3;
|
|
@@ -926,6 +1149,7 @@ var LruCache = class {
|
|
|
926
1149
|
}
|
|
927
1150
|
};
|
|
928
1151
|
var manifestCache = new LruCache(DEFAULT_MANIFEST_CACHE_MAX_SIZE);
|
|
1152
|
+
var inflightManifestFetches = /* @__PURE__ */ new Map();
|
|
929
1153
|
function configureManifestCache(maxSize) {
|
|
930
1154
|
if (!Number.isInteger(maxSize) || maxSize <= 0) {
|
|
931
1155
|
throw new RouteDockManifestError("Invalid manifest cache size: " + maxSize);
|
|
@@ -961,10 +1185,27 @@ async function fetchManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_M
|
|
|
961
1185
|
if (cached && Date.now() < cached.expiresAt) {
|
|
962
1186
|
verifyManifestSignature(cached.manifest, expectedPayee);
|
|
963
1187
|
assertClientVersionSupported(cached.manifest, baseUrl);
|
|
1188
|
+
assertManifestVersionSupported(cached.manifest, baseUrl);
|
|
964
1189
|
assertManifestValid(cached.manifest, baseUrl);
|
|
965
1190
|
assertManifestActive(cached.manifest, baseUrl);
|
|
966
1191
|
return cached.manifest;
|
|
967
1192
|
}
|
|
1193
|
+
let inflight = inflightManifestFetches.get(baseUrl);
|
|
1194
|
+
if (!inflight) {
|
|
1195
|
+
inflight = fetchAndCacheManifest(baseUrl, retryPolicy, manifestTimeoutMs).finally(() => {
|
|
1196
|
+
inflightManifestFetches.delete(baseUrl);
|
|
1197
|
+
});
|
|
1198
|
+
inflightManifestFetches.set(baseUrl, inflight);
|
|
1199
|
+
}
|
|
1200
|
+
const manifest = await inflight;
|
|
1201
|
+
verifyManifestSignature(manifest, expectedPayee);
|
|
1202
|
+
assertClientVersionSupported(manifest, baseUrl);
|
|
1203
|
+
assertManifestVersionSupported(manifest, baseUrl);
|
|
1204
|
+
assertManifestValid(manifest, baseUrl);
|
|
1205
|
+
assertManifestActive(manifest, baseUrl);
|
|
1206
|
+
return manifest;
|
|
1207
|
+
}
|
|
1208
|
+
async function fetchAndCacheManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_MANIFEST_TIMEOUT_MS) {
|
|
968
1209
|
const url = baseUrl.replace(/\/$/, "") + "/.well-known/routedock.json";
|
|
969
1210
|
return withRetry(async () => {
|
|
970
1211
|
let raw;
|
|
@@ -1001,15 +1242,12 @@ async function fetchManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_M
|
|
|
1001
1242
|
} catch (err) {
|
|
1002
1243
|
throw new RouteDockManifestError(`Manifest at ${url} is not valid JSON`, { cause: err });
|
|
1003
1244
|
}
|
|
1004
|
-
|
|
1005
|
-
if (!result.valid) {
|
|
1006
|
-
const msgs = result.errors.map((err) => err.error).join("; ");
|
|
1007
|
-
throw new RouteDockManifestError(`Invalid manifest at ${url}: ${msgs}`);
|
|
1008
|
-
}
|
|
1245
|
+
validateManifestSchema(raw, url);
|
|
1009
1246
|
const manifest = raw;
|
|
1010
1247
|
assertManifestValid(manifest, baseUrl);
|
|
1011
|
-
verifyManifestSignature(manifest
|
|
1248
|
+
verifyManifestSignature(manifest);
|
|
1012
1249
|
assertClientVersionSupported(manifest, baseUrl);
|
|
1250
|
+
assertManifestVersionSupported(manifest, baseUrl);
|
|
1013
1251
|
assertManifestActive(manifest, baseUrl);
|
|
1014
1252
|
manifestCache.set(baseUrl, { manifest, fetchedAt: Date.now(), expiresAt });
|
|
1015
1253
|
return manifest;
|
|
@@ -1124,6 +1362,48 @@ function rankProvidersByLatency(manifests, targetRegion) {
|
|
|
1124
1362
|
});
|
|
1125
1363
|
}
|
|
1126
1364
|
|
|
1365
|
+
// src/internal/settleResult.ts
|
|
1366
|
+
function readSettleResult(result) {
|
|
1367
|
+
const settle = result;
|
|
1368
|
+
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
1369
|
+
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
1370
|
+
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
1371
|
+
return failed(settle.errorReason);
|
|
1372
|
+
}
|
|
1373
|
+
return { ok: true, txHash: settle.transaction };
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// src/internal/request.ts
|
|
1377
|
+
var DEFAULT_METHOD = "GET";
|
|
1378
|
+
var BODYLESS_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
|
|
1379
|
+
function isReplayable(body) {
|
|
1380
|
+
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;
|
|
1381
|
+
}
|
|
1382
|
+
async function bufferBody(body) {
|
|
1383
|
+
if (isReplayable(body)) return body;
|
|
1384
|
+
return new Response(body).arrayBuffer();
|
|
1385
|
+
}
|
|
1386
|
+
async function prepareRequest(request) {
|
|
1387
|
+
const method = (request?.method ?? DEFAULT_METHOD).toUpperCase();
|
|
1388
|
+
const headers = new Headers(request?.headers);
|
|
1389
|
+
const body = request?.body;
|
|
1390
|
+
if (body === void 0 || BODYLESS_METHODS.has(method)) {
|
|
1391
|
+
return { method, headers };
|
|
1392
|
+
}
|
|
1393
|
+
return { method, headers, body: await bufferBody(body) };
|
|
1394
|
+
}
|
|
1395
|
+
function toRequestInit(prepared, overrides) {
|
|
1396
|
+
const headers = new Headers(prepared.headers);
|
|
1397
|
+
if (overrides) {
|
|
1398
|
+
new Headers(overrides).forEach((value, name) => headers.set(name, value));
|
|
1399
|
+
}
|
|
1400
|
+
const init = { method: prepared.method, headers };
|
|
1401
|
+
if (prepared.body !== void 0) {
|
|
1402
|
+
init.body = prepared.body;
|
|
1403
|
+
}
|
|
1404
|
+
return init;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1127
1407
|
// src/internal/payee.ts
|
|
1128
1408
|
function resolvePayee(manifest, mode) {
|
|
1129
1409
|
const override = manifest.pricing[mode]?.payee;
|
|
@@ -1184,6 +1464,102 @@ function checkChargeChallenge(request, manifest, network, maxAmount) {
|
|
|
1184
1464
|
}
|
|
1185
1465
|
return checkAmount(request["amount"], maxAmount);
|
|
1186
1466
|
}
|
|
1467
|
+
var defaultIo = {
|
|
1468
|
+
async expirationLedger(network, maxTimeoutSeconds) {
|
|
1469
|
+
const { sequence } = await getRpcClient(network).getLatestLedger();
|
|
1470
|
+
const secondsPerLedger = await getEstimatedLedgerCloseTimeSeconds(network);
|
|
1471
|
+
return sequence + Math.ceil(maxTimeoutSeconds / secondsPerLedger);
|
|
1472
|
+
},
|
|
1473
|
+
async assemble({ asset, from, payTo, amount, networkPassphrase, rpcUrl }) {
|
|
1474
|
+
return contract.AssembledTransaction.build({
|
|
1475
|
+
contractId: asset,
|
|
1476
|
+
method: "transfer",
|
|
1477
|
+
// SEP-41: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md#interface
|
|
1478
|
+
args: [
|
|
1479
|
+
nativeToScVal(from, { type: "address" }),
|
|
1480
|
+
nativeToScVal(payTo, { type: "address" }),
|
|
1481
|
+
nativeToScVal(amount, { type: "i128" })
|
|
1482
|
+
],
|
|
1483
|
+
networkPassphrase,
|
|
1484
|
+
rpcUrl,
|
|
1485
|
+
parseResultXdr: (result) => result
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
var NulthStellarScheme = class {
|
|
1490
|
+
constructor(signer, io = defaultIo) {
|
|
1491
|
+
this.signer = signer;
|
|
1492
|
+
this.io = io;
|
|
1493
|
+
}
|
|
1494
|
+
signer;
|
|
1495
|
+
io;
|
|
1496
|
+
scheme = "exact";
|
|
1497
|
+
async createPaymentPayload(x402Version, paymentRequirements) {
|
|
1498
|
+
try {
|
|
1499
|
+
this.validateRequirements(paymentRequirements);
|
|
1500
|
+
} catch (err) {
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
`Invalid input parameters for creating Nulth Stellar payment, cause: ${String(err)}`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
const from = this.signer.address;
|
|
1506
|
+
const { network, payTo, asset, amount, maxTimeoutSeconds } = paymentRequirements;
|
|
1507
|
+
if (!paymentRequirements.extra.areFeesSponsored) {
|
|
1508
|
+
throw new Error("Nulth exact scheme requires areFeesSponsored to be true");
|
|
1509
|
+
}
|
|
1510
|
+
const maxLedger = await this.io.expirationLedger(network, maxTimeoutSeconds);
|
|
1511
|
+
const tx = await this.io.assemble({
|
|
1512
|
+
asset,
|
|
1513
|
+
from,
|
|
1514
|
+
payTo,
|
|
1515
|
+
amount,
|
|
1516
|
+
networkPassphrase: getNetworkPassphrase(network),
|
|
1517
|
+
rpcUrl: getRpcUrl(network)
|
|
1518
|
+
});
|
|
1519
|
+
handleSimulationResult(tx.simulation);
|
|
1520
|
+
const signers = tx.needsNonInvokerSigningBy();
|
|
1521
|
+
if (!signers.includes(from) || signers.length > 1) {
|
|
1522
|
+
throw new Error(`Expected to sign with [${from}], but got [${signers.join(", ")}]`);
|
|
1523
|
+
}
|
|
1524
|
+
await tx.signAuthEntries({
|
|
1525
|
+
address: from,
|
|
1526
|
+
signAuthEntry: this.signer.signAuthEntry,
|
|
1527
|
+
expiration: maxLedger,
|
|
1528
|
+
authorizeEntry: nulthAuthorizeEntry
|
|
1529
|
+
});
|
|
1530
|
+
await tx.simulate();
|
|
1531
|
+
handleSimulationResult(tx.simulation);
|
|
1532
|
+
const remaining = tx.needsNonInvokerSigningBy();
|
|
1533
|
+
if (remaining.length > 0) {
|
|
1534
|
+
throw new Error(`unexpected signer(s) required: [${remaining.join(", ")}]`);
|
|
1535
|
+
}
|
|
1536
|
+
if (!tx.built) {
|
|
1537
|
+
throw new Error("Nulth payment transaction has no assembled envelope to send");
|
|
1538
|
+
}
|
|
1539
|
+
return { x402Version, payload: { transaction: tx.built.toXDR() } };
|
|
1540
|
+
}
|
|
1541
|
+
validateRequirements(requirements) {
|
|
1542
|
+
const { scheme, network, payTo, asset, amount } = requirements;
|
|
1543
|
+
if (typeof amount !== "string" || !Number.isInteger(Number(amount)) || Number(amount) <= 0) {
|
|
1544
|
+
throw new Error(`Invalid amount: ${amount}. Amount must be a positive integer.`);
|
|
1545
|
+
}
|
|
1546
|
+
if (scheme !== this.scheme) {
|
|
1547
|
+
throw new Error(`Unsupported scheme: ${scheme}`);
|
|
1548
|
+
}
|
|
1549
|
+
if (!isStellarNetwork(network)) {
|
|
1550
|
+
throw new Error(`Unsupported Stellar network: ${network}`);
|
|
1551
|
+
}
|
|
1552
|
+
if (!validateStellarDestinationAddress(payTo)) {
|
|
1553
|
+
throw new Error(`Invalid Stellar destination address: ${payTo}`);
|
|
1554
|
+
}
|
|
1555
|
+
if (!validateStellarAssetAddress(asset)) {
|
|
1556
|
+
throw new Error(`Invalid Stellar asset address: ${asset}`);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
function selectStellarScheme(signer) {
|
|
1561
|
+
return isNulthStellarSigner(signer) ? new NulthStellarScheme(signer) : new ExactStellarScheme(signer);
|
|
1562
|
+
}
|
|
1187
1563
|
|
|
1188
1564
|
// src/client/x402Client.ts
|
|
1189
1565
|
var CAIP2 = {
|
|
@@ -1191,25 +1567,29 @@ var CAIP2 = {
|
|
|
1191
1567
|
mainnet: "stellar:pubnet"
|
|
1192
1568
|
};
|
|
1193
1569
|
var X402Client = class _X402Client {
|
|
1194
|
-
constructor(secretKeyOrSigner, network, retryPolicy) {
|
|
1570
|
+
constructor(secretKeyOrSigner, network, retryPolicy, preSignHook, logger) {
|
|
1195
1571
|
this.network = network;
|
|
1196
1572
|
this.retryPolicy = retryPolicy;
|
|
1573
|
+
this.preSignHook = preSignHook;
|
|
1197
1574
|
const caip2 = CAIP2[network];
|
|
1198
1575
|
this.signer = typeof secretKeyOrSigner === "string" ? createEd25519Signer(secretKeyOrSigner, caip2) : secretKeyOrSigner;
|
|
1199
|
-
const scheme =
|
|
1576
|
+
const scheme = selectStellarScheme(this.signer);
|
|
1200
1577
|
const core = new x402Client();
|
|
1201
1578
|
core.register(caip2, scheme);
|
|
1202
1579
|
this.httpClient = new x402HTTPClient(core);
|
|
1580
|
+
this.logger = logger;
|
|
1203
1581
|
}
|
|
1204
1582
|
network;
|
|
1205
1583
|
retryPolicy;
|
|
1584
|
+
preSignHook;
|
|
1206
1585
|
httpClient;
|
|
1207
1586
|
signer;
|
|
1587
|
+
logger;
|
|
1208
1588
|
/** Replace signer (e.g. swap to Nulth ZK account payer before pay) */
|
|
1209
1589
|
withSigner(signer) {
|
|
1210
|
-
return new _X402Client(signer, this.network, this.retryPolicy);
|
|
1590
|
+
return new _X402Client(signer, this.network, this.retryPolicy, this.preSignHook, this.logger);
|
|
1211
1591
|
}
|
|
1212
|
-
async pay(url, manifest) {
|
|
1592
|
+
async pay(url, manifest, request) {
|
|
1213
1593
|
const pricing = manifest.pricing.x402;
|
|
1214
1594
|
if (!pricing) {
|
|
1215
1595
|
throw new RouteDockManifestError("manifest.pricing.x402 missing");
|
|
@@ -1218,13 +1598,15 @@ var X402Client = class _X402Client {
|
|
|
1218
1598
|
throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
|
|
1219
1599
|
}
|
|
1220
1600
|
const caip2 = CAIP2[this.network];
|
|
1601
|
+
const prepared = await prepareRequest(request);
|
|
1221
1602
|
const probe = await withRetry(async () => {
|
|
1222
1603
|
let init2;
|
|
1223
1604
|
try {
|
|
1224
|
-
init2 = await fetch(url,
|
|
1605
|
+
init2 = await fetch(url, toRequestInit(prepared, { "X-Preferred-Mode": "x402" }));
|
|
1225
1606
|
} catch (err) {
|
|
1226
1607
|
throw wrapFetchError(err, "x402 initial request");
|
|
1227
1608
|
}
|
|
1609
|
+
this.logger?.("debug", "[x402] unpaid probe response", { status: init2.status });
|
|
1228
1610
|
if (init2.status !== 402) {
|
|
1229
1611
|
if (!init2.ok) {
|
|
1230
1612
|
throw httpStatusToError(
|
|
@@ -1241,6 +1623,11 @@ var X402Client = class _X402Client {
|
|
|
1241
1623
|
`Failed to parse JSON from non-402 response (HTTP ${init2.status})`
|
|
1242
1624
|
);
|
|
1243
1625
|
}
|
|
1626
|
+
this.logger?.(
|
|
1627
|
+
"debug",
|
|
1628
|
+
"[x402] unpaid probe returned non-402 success \u2014 serving as free response",
|
|
1629
|
+
{ status: init2.status }
|
|
1630
|
+
);
|
|
1244
1631
|
return { kind: "free", data: data2 };
|
|
1245
1632
|
}
|
|
1246
1633
|
return { kind: "payment-required", init: init2 };
|
|
@@ -1269,6 +1656,18 @@ var X402Client = class _X402Client {
|
|
|
1269
1656
|
if (allowed.length === 0) {
|
|
1270
1657
|
throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
|
|
1271
1658
|
}
|
|
1659
|
+
if (this.preSignHook) {
|
|
1660
|
+
let decision;
|
|
1661
|
+
try {
|
|
1662
|
+
decision = await this.preSignHook({ url, manifest, accepts: allowed });
|
|
1663
|
+
} catch (err) {
|
|
1664
|
+
throw new RouteDockPolicyRejectError("pre_sign_hook_error", { cause: err });
|
|
1665
|
+
}
|
|
1666
|
+
if (!decision || decision.approve !== true) {
|
|
1667
|
+
const reason = decision && decision.approve === false ? decision.reason : void 0;
|
|
1668
|
+
throw new RouteDockPolicyRejectError(reason ?? "pre_sign_rejected");
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1272
1671
|
let paymentPayload;
|
|
1273
1672
|
try {
|
|
1274
1673
|
paymentPayload = await this.httpClient.createPaymentPayload({
|
|
@@ -1282,13 +1681,15 @@ var X402Client = class _X402Client {
|
|
|
1282
1681
|
}
|
|
1283
1682
|
const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
1284
1683
|
const signedAmount = stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
|
|
1684
|
+
this.logger?.("debug", "[x402] sending paid request with payment header");
|
|
1285
1685
|
const settled = await withRetry(async () => {
|
|
1286
1686
|
let response;
|
|
1287
1687
|
try {
|
|
1288
|
-
response = await fetch(url,
|
|
1688
|
+
response = await fetch(url, toRequestInit(prepared, paymentHeaders));
|
|
1289
1689
|
} catch (err) {
|
|
1290
1690
|
throw wrapFetchError(err, "x402 settlement request");
|
|
1291
1691
|
}
|
|
1692
|
+
this.logger?.("debug", "[x402] paid request response", { status: response.status });
|
|
1292
1693
|
if (!response.ok) {
|
|
1293
1694
|
if (response.status >= 500 || response.status === 429 || response.status === 503) {
|
|
1294
1695
|
throw httpStatusToError(
|
|
@@ -1297,17 +1698,28 @@ var X402Client = class _X402Client {
|
|
|
1297
1698
|
response
|
|
1298
1699
|
);
|
|
1299
1700
|
}
|
|
1300
|
-
throw
|
|
1701
|
+
throw await paymentRejectedError("x402 payment failed", response.status, response);
|
|
1301
1702
|
}
|
|
1302
1703
|
return response;
|
|
1303
1704
|
}, this.retryPolicy);
|
|
1304
1705
|
let txHash = null;
|
|
1305
1706
|
const responseHeader = settled.headers.get("X-Payment-Response");
|
|
1306
|
-
if (responseHeader) {
|
|
1707
|
+
if (!responseHeader) {
|
|
1708
|
+
this.logger?.("debug", "[x402] settled 2xx carried no X-Payment-Response header");
|
|
1709
|
+
} else {
|
|
1307
1710
|
try {
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1711
|
+
const outcome = readSettleResult(decodePaymentResponseHeader(responseHeader));
|
|
1712
|
+
if (outcome.ok) {
|
|
1713
|
+
txHash = outcome.txHash;
|
|
1714
|
+
} else {
|
|
1715
|
+
this.logger?.("debug", "[x402] X-Payment-Response reports an unsettled payment", {
|
|
1716
|
+
...outcome.reason !== void 0 ? { reason: outcome.reason } : {}
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
this.logger?.("debug", "[x402] X-Payment-Response header present but undecodable", {
|
|
1721
|
+
error: err
|
|
1722
|
+
});
|
|
1311
1723
|
}
|
|
1312
1724
|
}
|
|
1313
1725
|
let data;
|
|
@@ -1328,13 +1740,13 @@ var MppChargeClient = class {
|
|
|
1328
1740
|
keypair;
|
|
1329
1741
|
network;
|
|
1330
1742
|
retryPolicy;
|
|
1331
|
-
async pay(url, manifest) {
|
|
1743
|
+
async pay(url, manifest, request) {
|
|
1332
1744
|
const pricing = manifest.pricing["mpp-charge"];
|
|
1333
1745
|
if (!pricing) {
|
|
1334
1746
|
throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
|
|
1335
1747
|
}
|
|
1336
1748
|
const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
|
|
1337
|
-
|
|
1749
|
+
const prepared = await prepareRequest(request);
|
|
1338
1750
|
let signedAmount;
|
|
1339
1751
|
let credential;
|
|
1340
1752
|
const mppx = Mppx.create({
|
|
@@ -1342,19 +1754,14 @@ var MppChargeClient = class {
|
|
|
1342
1754
|
methods: [
|
|
1343
1755
|
stellar.charge({
|
|
1344
1756
|
keypair: this.keypair,
|
|
1345
|
-
mode: "pull"
|
|
1346
|
-
onProgress(event) {
|
|
1347
|
-
if (event.type === "paid") {
|
|
1348
|
-
txHash = event.hash;
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1757
|
+
mode: "pull"
|
|
1351
1758
|
})
|
|
1352
1759
|
],
|
|
1353
1760
|
onChallenge: async (challenge, { createCredential }) => {
|
|
1354
|
-
const
|
|
1355
|
-
const rejection = checkChargeChallenge(
|
|
1761
|
+
const request2 = challenge.request;
|
|
1762
|
+
const rejection = checkChargeChallenge(request2, manifest, caip2, pricing.amount);
|
|
1356
1763
|
if (rejection) throw rejection;
|
|
1357
|
-
const amount =
|
|
1764
|
+
const amount = request2["amount"];
|
|
1358
1765
|
if (typeof amount !== "string") {
|
|
1359
1766
|
throw new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1360
1767
|
}
|
|
@@ -1366,7 +1773,7 @@ var MppChargeClient = class {
|
|
|
1366
1773
|
return withRetry(async () => {
|
|
1367
1774
|
let response;
|
|
1368
1775
|
try {
|
|
1369
|
-
response = credential ? await mppx.rawFetch(url,
|
|
1776
|
+
response = credential ? await mppx.rawFetch(url, toRequestInit(prepared, { Authorization: credential })) : await mppx.fetch(url, toRequestInit(prepared));
|
|
1370
1777
|
} catch (err) {
|
|
1371
1778
|
throw wrapMppError(err, "MPP charge request");
|
|
1372
1779
|
}
|
|
@@ -1378,8 +1785,9 @@ var MppChargeClient = class {
|
|
|
1378
1785
|
response
|
|
1379
1786
|
);
|
|
1380
1787
|
}
|
|
1381
|
-
throw
|
|
1788
|
+
throw await paymentRejectedError("MPP charge failed", response.status, response);
|
|
1382
1789
|
}
|
|
1790
|
+
const txHash = readSettlementHash(response);
|
|
1383
1791
|
let data;
|
|
1384
1792
|
try {
|
|
1385
1793
|
data = await response.json();
|
|
@@ -1399,8 +1807,40 @@ var MppChargeClient = class {
|
|
|
1399
1807
|
}, this.retryPolicy);
|
|
1400
1808
|
}
|
|
1401
1809
|
};
|
|
1810
|
+
function readSettlementHash(response) {
|
|
1811
|
+
try {
|
|
1812
|
+
return Receipt.fromResponse(response).reference;
|
|
1813
|
+
} catch {
|
|
1814
|
+
return null;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1402
1817
|
var MIN_REFUND_WAITING_PERIOD = 17280;
|
|
1403
1818
|
var STREAM_CLEANUP_TIMEOUT_MS = 3e3;
|
|
1819
|
+
var TX_CONFIRM_INTERVAL_MS = 1e3;
|
|
1820
|
+
var TX_CONFIRM_TIMEOUT_MS = 3e4;
|
|
1821
|
+
async function confirmTransaction(server, hash, label, timeoutMs = TX_CONFIRM_TIMEOUT_MS, intervalMs = TX_CONFIRM_INTERVAL_MS) {
|
|
1822
|
+
const deadline = Date.now() + timeoutMs;
|
|
1823
|
+
while (true) {
|
|
1824
|
+
const txResponse = await server.getTransaction(hash);
|
|
1825
|
+
const status = txResponse?.status;
|
|
1826
|
+
if (status === "SUCCESS") {
|
|
1827
|
+
return hash;
|
|
1828
|
+
}
|
|
1829
|
+
if (status === "FAILED") {
|
|
1830
|
+
throw new RouteDockDisputeError(
|
|
1831
|
+
`${label} transaction failed on-chain: status FAILED (resultXdr: ${txResponse.resultXdr.toXDR("base64")})`
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
if (Date.now() >= deadline) {
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
const remaining = deadline - Date.now();
|
|
1838
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, Math.max(1, remaining))));
|
|
1839
|
+
}
|
|
1840
|
+
throw new RouteDockDisputeError(
|
|
1841
|
+
`${label} transaction confirmation timed out after ${timeoutMs}ms (hash: ${hash})`
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1404
1844
|
var WS_CONNECTING = 0;
|
|
1405
1845
|
var WS_OPEN = 1;
|
|
1406
1846
|
function defaultWebSocketFactory(url, init, handlers) {
|
|
@@ -1414,9 +1854,9 @@ function defaultWebSocketFactory(url, init, handlers) {
|
|
|
1414
1854
|
if (typeof data === "string") {
|
|
1415
1855
|
handlers.onMessage(data);
|
|
1416
1856
|
} else if (data instanceof ArrayBuffer) {
|
|
1417
|
-
handlers.onMessage(
|
|
1857
|
+
handlers.onMessage(bytesToUtf8(new Uint8Array(data)));
|
|
1418
1858
|
} else {
|
|
1419
|
-
void data.arrayBuffer().then((buffer) => handlers.onMessage(
|
|
1859
|
+
void data.arrayBuffer().then((buffer) => handlers.onMessage(bytesToUtf8(new Uint8Array(buffer)))).catch(() => {
|
|
1420
1860
|
});
|
|
1421
1861
|
}
|
|
1422
1862
|
};
|
|
@@ -1569,7 +2009,7 @@ var MppSessionClient = class {
|
|
|
1569
2009
|
throw err;
|
|
1570
2010
|
}
|
|
1571
2011
|
};
|
|
1572
|
-
const streamWs = (u, m, createCredential, onSigned) => this.streamWebSocket(u, m, createCredential, onSigned);
|
|
2012
|
+
const streamWs = (u, m, createCredential, onSigned, request) => this.streamWebSocket(u, m, createCredential, onSigned, request);
|
|
1573
2013
|
const mppx = Mppx.create({
|
|
1574
2014
|
polyfill: false,
|
|
1575
2015
|
methods: [
|
|
@@ -1641,6 +2081,7 @@ var MppSessionClient = class {
|
|
|
1641
2081
|
};
|
|
1642
2082
|
},
|
|
1643
2083
|
async *stream(options2) {
|
|
2084
|
+
const prepared = await prepareRequest(options2?.request);
|
|
1644
2085
|
const checkSpend = () => {
|
|
1645
2086
|
if (!onSpend) return Promise.resolve();
|
|
1646
2087
|
return onSpend(pricing.rate);
|
|
@@ -1650,7 +2091,13 @@ var MppSessionClient = class {
|
|
|
1650
2091
|
const onSigned = () => {
|
|
1651
2092
|
vouchersIssued++;
|
|
1652
2093
|
};
|
|
1653
|
-
for await (const item of streamWs(
|
|
2094
|
+
for await (const item of streamWs(
|
|
2095
|
+
url,
|
|
2096
|
+
mppx,
|
|
2097
|
+
createGuardedCredential,
|
|
2098
|
+
onSigned,
|
|
2099
|
+
prepared
|
|
2100
|
+
)) {
|
|
1654
2101
|
yield item;
|
|
1655
2102
|
}
|
|
1656
2103
|
return;
|
|
@@ -1665,7 +2112,10 @@ var MppSessionClient = class {
|
|
|
1665
2112
|
const doFetch = () => withRetry(async () => {
|
|
1666
2113
|
let resp;
|
|
1667
2114
|
try {
|
|
1668
|
-
resp = await mppx.fetch(url, {
|
|
2115
|
+
resp = await mppx.fetch(url, {
|
|
2116
|
+
...toRequestInit(prepared),
|
|
2117
|
+
signal: abortController.signal
|
|
2118
|
+
});
|
|
1669
2119
|
} catch (err) {
|
|
1670
2120
|
if (abortController.signal.aborted) {
|
|
1671
2121
|
throw err;
|
|
@@ -1750,10 +2200,10 @@ var MppSessionClient = class {
|
|
|
1750
2200
|
async close() {
|
|
1751
2201
|
clearSessionTimer();
|
|
1752
2202
|
closed = true;
|
|
1753
|
-
const { rpc: rpcMod, Contract: Contract2, nativeToScVal:
|
|
2203
|
+
const { rpc: rpcMod, Contract: Contract2, nativeToScVal: nativeToScVal3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1754
2204
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1755
2205
|
const server = new rpcMod.Server(rpcUrl);
|
|
1756
|
-
const
|
|
2206
|
+
const contract2 = new Contract2(channelFactory);
|
|
1757
2207
|
const passphrase = network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
1758
2208
|
const account = await withRetry(async () => {
|
|
1759
2209
|
try {
|
|
@@ -1768,9 +2218,9 @@ var MppSessionClient = class {
|
|
|
1768
2218
|
fee: BASE_FEE2,
|
|
1769
2219
|
networkPassphrase: passphrase
|
|
1770
2220
|
}).addOperation(
|
|
1771
|
-
|
|
2221
|
+
contract2.call(
|
|
1772
2222
|
"prepare_commitment",
|
|
1773
|
-
|
|
2223
|
+
nativeToScVal3(currentCumulative, { type: "i128" })
|
|
1774
2224
|
)
|
|
1775
2225
|
).setTimeout(30).build();
|
|
1776
2226
|
return await server.simulateTransaction(simTx);
|
|
@@ -1789,7 +2239,7 @@ var MppSessionClient = class {
|
|
|
1789
2239
|
}
|
|
1790
2240
|
let signature;
|
|
1791
2241
|
try {
|
|
1792
|
-
signature = commitmentKey.sign(
|
|
2242
|
+
signature = commitmentKey.sign(commitmentBytes);
|
|
1793
2243
|
} catch (err) {
|
|
1794
2244
|
throw new RouteDockSignatureError("Channel close commitment signing failed", {
|
|
1795
2245
|
cause: err
|
|
@@ -1803,7 +2253,7 @@ var MppSessionClient = class {
|
|
|
1803
2253
|
headers: { "Content-Type": "application/json" },
|
|
1804
2254
|
body: JSON.stringify({
|
|
1805
2255
|
amount: currentCumulative.toString(),
|
|
1806
|
-
signature:
|
|
2256
|
+
signature: bytesToHex(signature)
|
|
1807
2257
|
})
|
|
1808
2258
|
});
|
|
1809
2259
|
} catch (err) {
|
|
@@ -1845,15 +2295,19 @@ var MppSessionClient = class {
|
|
|
1845
2295
|
vouchersIssued
|
|
1846
2296
|
};
|
|
1847
2297
|
},
|
|
1848
|
-
|
|
2298
|
+
/**
|
|
2299
|
+
* Request refund from the channel contract (initiates dispute).
|
|
2300
|
+
* Returns the transaction hash only after on-chain confirmation (SUCCESS).
|
|
2301
|
+
*/
|
|
2302
|
+
async requestRefund(options2) {
|
|
1849
2303
|
const { rpc: rpcMod, Contract: Contract2, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1850
2304
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1851
2305
|
const server = new rpcMod.Server(rpcUrl);
|
|
1852
|
-
const
|
|
2306
|
+
const contract2 = new Contract2(channelFactory);
|
|
1853
2307
|
const passphrase = network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
1854
2308
|
try {
|
|
1855
2309
|
const account = await server.getAccount(agentPublicKey);
|
|
1856
|
-
const tx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(
|
|
2310
|
+
const tx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(contract2.call("request_refund")).setTimeout(30).build();
|
|
1857
2311
|
const simResult = await server.simulateTransaction(tx);
|
|
1858
2312
|
if (rpcMod.Api.isSimulationError(simResult)) {
|
|
1859
2313
|
throw new RouteDockDisputeError(`request_refund simulation failed: ${simResult.error}`);
|
|
@@ -1865,37 +2319,50 @@ var MppSessionClient = class {
|
|
|
1865
2319
|
const errDetail = result.errorResult ? JSON.stringify(result.errorResult) : "status ERROR";
|
|
1866
2320
|
throw new RouteDockDisputeError(`Refund request transaction failed: ${errDetail}`);
|
|
1867
2321
|
}
|
|
2322
|
+
if (result.status === "TRY_AGAIN_LATER") {
|
|
2323
|
+
throw new RouteDockDisputeError("Refund request transaction rejected with TRY_AGAIN_LATER: transaction not queued");
|
|
2324
|
+
}
|
|
1868
2325
|
if (!result.hash) {
|
|
1869
2326
|
throw new RouteDockDisputeError("Refund request transaction not sent");
|
|
1870
2327
|
}
|
|
1871
|
-
return
|
|
2328
|
+
return await confirmTransaction(
|
|
2329
|
+
server,
|
|
2330
|
+
result.hash,
|
|
2331
|
+
"Refund request",
|
|
2332
|
+
options2?.timeoutMs,
|
|
2333
|
+
options2?.intervalMs
|
|
2334
|
+
);
|
|
1872
2335
|
} catch (err) {
|
|
1873
2336
|
if (err instanceof RouteDockDisputeError) throw err;
|
|
1874
2337
|
throw new RouteDockDisputeError(`Failed to request refund: ${err instanceof Error ? err.message : String(err)}`);
|
|
1875
2338
|
}
|
|
1876
2339
|
},
|
|
1877
|
-
|
|
1878
|
-
|
|
2340
|
+
/**
|
|
2341
|
+
* Server-side counter-mechanism to settle with latest voucher before refund window expires.
|
|
2342
|
+
* Returns the transaction hash only after on-chain confirmation (SUCCESS).
|
|
2343
|
+
*/
|
|
2344
|
+
async settleWithLatestVoucher(options2) {
|
|
2345
|
+
const { rpc: rpcMod, Contract: Contract2, nativeToScVal: nativeToScVal3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1879
2346
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1880
2347
|
const server = new rpcMod.Server(rpcUrl);
|
|
1881
|
-
const
|
|
2348
|
+
const contract2 = new Contract2(channelFactory);
|
|
1882
2349
|
const passphrase = network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
1883
2350
|
try {
|
|
1884
2351
|
const account = await server.getAccount(agentPublicKey);
|
|
1885
|
-
const simTx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(
|
|
2352
|
+
const simTx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(contract2.call("prepare_commitment", nativeToScVal3(currentCumulative, { type: "i128" }))).setTimeout(30).build();
|
|
1886
2353
|
const simResult = await server.simulateTransaction(simTx);
|
|
1887
2354
|
if (rpcMod.Api.isSimulationError(simResult)) {
|
|
1888
2355
|
throw new RouteDockDisputeError(`prepare_commitment simulation failed: ${simResult.error}`);
|
|
1889
2356
|
}
|
|
1890
2357
|
const commitmentBytes = simResult.result?.retval?.bytes();
|
|
1891
2358
|
if (!commitmentBytes) throw new RouteDockDisputeError("prepare_commitment returned no bytes");
|
|
1892
|
-
const signature = commitmentKey.sign(
|
|
2359
|
+
const signature = commitmentKey.sign(commitmentBytes);
|
|
1893
2360
|
const settleAccount = await server.getAccount(agentPublicKey);
|
|
1894
2361
|
const settleTx = new TransactionBuilder2(settleAccount, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(
|
|
1895
|
-
|
|
2362
|
+
contract2.call(
|
|
1896
2363
|
"settle_with_signature",
|
|
1897
|
-
|
|
1898
|
-
|
|
2364
|
+
nativeToScVal3(currentCumulative, { type: "i128" }),
|
|
2365
|
+
nativeToScVal3(signature)
|
|
1899
2366
|
)
|
|
1900
2367
|
).setTimeout(30).build();
|
|
1901
2368
|
const preparedSettleTx = await server.prepareTransaction(settleTx);
|
|
@@ -1905,10 +2372,19 @@ var MppSessionClient = class {
|
|
|
1905
2372
|
const errDetail = settleResult.errorResult ? JSON.stringify(settleResult.errorResult) : "status ERROR";
|
|
1906
2373
|
throw new RouteDockDisputeError(`Settlement transaction failed: ${errDetail}`);
|
|
1907
2374
|
}
|
|
2375
|
+
if (settleResult.status === "TRY_AGAIN_LATER") {
|
|
2376
|
+
throw new RouteDockDisputeError("Settlement transaction rejected with TRY_AGAIN_LATER: transaction not queued");
|
|
2377
|
+
}
|
|
1908
2378
|
if (!settleResult.hash) {
|
|
1909
2379
|
throw new RouteDockDisputeError("Settlement transaction not sent");
|
|
1910
2380
|
}
|
|
1911
|
-
return
|
|
2381
|
+
return await confirmTransaction(
|
|
2382
|
+
server,
|
|
2383
|
+
settleResult.hash,
|
|
2384
|
+
"Settlement",
|
|
2385
|
+
options2?.timeoutMs,
|
|
2386
|
+
options2?.intervalMs
|
|
2387
|
+
);
|
|
1912
2388
|
} catch (err) {
|
|
1913
2389
|
if (err instanceof RouteDockDisputeError) throw err;
|
|
1914
2390
|
throw new RouteDockDisputeError(`Failed to settle with latest voucher: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1918,11 +2394,11 @@ var MppSessionClient = class {
|
|
|
1918
2394
|
const { rpc: rpcMod, Contract: Contract2, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
|
|
1919
2395
|
const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
|
|
1920
2396
|
const server = new rpcMod.Server(rpcUrl);
|
|
1921
|
-
const
|
|
2397
|
+
const contract2 = new Contract2(channelFactory);
|
|
1922
2398
|
const passphrase = network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
1923
2399
|
try {
|
|
1924
2400
|
const account = await server.getAccount(agentPublicKey);
|
|
1925
|
-
const tx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(
|
|
2401
|
+
const tx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(contract2.call("get_channel_state")).setTimeout(30).build();
|
|
1926
2402
|
const simResult = await server.simulateTransaction(tx);
|
|
1927
2403
|
if (rpcMod.Api.isSimulationError(simResult)) {
|
|
1928
2404
|
throw new RouteDockChannelStateError(`Failed to query channel state: ${simResult.error}`);
|
|
@@ -1988,11 +2464,11 @@ var MppSessionClient = class {
|
|
|
1988
2464
|
* close code (1000/1001); abnormal closes and connection failures surface as
|
|
1989
2465
|
* RouteDockChannelStateError.
|
|
1990
2466
|
*/
|
|
1991
|
-
async *streamWebSocket(url, mppx, createCredential, onSigned) {
|
|
1992
|
-
const
|
|
2467
|
+
async *streamWebSocket(url, mppx, createCredential, onSigned, request) {
|
|
2468
|
+
const probeInit = toRequestInit(request);
|
|
1993
2469
|
let probe;
|
|
1994
2470
|
try {
|
|
1995
|
-
probe = await mppx.rawFetch(url,
|
|
2471
|
+
probe = await mppx.rawFetch(url, probeInit);
|
|
1996
2472
|
} catch (err) {
|
|
1997
2473
|
throw wrapFetchError(err, "Voucher request");
|
|
1998
2474
|
}
|
|
@@ -2096,235 +2572,19 @@ var MppSessionClient = class {
|
|
|
2096
2572
|
}
|
|
2097
2573
|
}
|
|
2098
2574
|
};
|
|
2099
|
-
var NulthPolicyError = class extends Error {
|
|
2100
|
-
code;
|
|
2101
|
-
constructor(code, message) {
|
|
2102
|
-
super(message);
|
|
2103
|
-
this.name = "NulthPolicyError";
|
|
2104
|
-
this.code = code;
|
|
2105
|
-
}
|
|
2106
|
-
};
|
|
2107
|
-
function sha256Hex(data) {
|
|
2108
|
-
const buf = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
2109
|
-
return createHash("sha256").update(buf).digest("hex");
|
|
2110
|
-
}
|
|
2111
|
-
function commitDailyCap(dailyCapStroops, witnessSecret) {
|
|
2112
|
-
return sha256Hex(`${witnessSecret}:cap:${dailyCapStroops.toString()}`);
|
|
2113
|
-
}
|
|
2114
|
-
function commitAllowlist(payees, witnessSecret) {
|
|
2115
|
-
const sorted = [...payees].sort().join(",");
|
|
2116
|
-
return sha256Hex(`${witnessSecret}:allowlist:${sorted}`);
|
|
2117
|
-
}
|
|
2118
|
-
function hashPayee(payee) {
|
|
2119
|
-
return sha256Hex(`payee:${payee}`);
|
|
2120
|
-
}
|
|
2121
|
-
function dayBucketFromLedger(ledgerSequence) {
|
|
2122
|
-
return Math.floor(ledgerSequence / 17280);
|
|
2123
|
-
}
|
|
2124
|
-
function authDigestFromEntry(authEntryBase64) {
|
|
2125
|
-
const entryHash = hash(Buffer.from(authEntryBase64, "base64"));
|
|
2126
|
-
return sha256Hex(entryHash);
|
|
2127
|
-
}
|
|
2128
|
-
function assertAuthEntryMatchesContext(authEntry, from, ctx) {
|
|
2129
|
-
let preimage;
|
|
2130
|
-
try {
|
|
2131
|
-
preimage = xdr.HashIdPreimage.fromXDR(authEntry, "base64");
|
|
2132
|
-
} catch {
|
|
2133
|
-
throw new NulthPolicyError("auth_entry_mismatch", "authEntry does not decode as a HashIdPreimage");
|
|
2134
|
-
}
|
|
2135
|
-
if (preimage.switch() !== xdr.EnvelopeType.envelopeTypeSorobanAuthorization()) {
|
|
2136
|
-
throw new NulthPolicyError(
|
|
2137
|
-
"auth_entry_mismatch",
|
|
2138
|
-
"authEntry is not a Soroban authorization preimage"
|
|
2139
|
-
);
|
|
2140
|
-
}
|
|
2141
|
-
const inv = preimage.sorobanAuthorization().invocation();
|
|
2142
|
-
if (inv.subInvocations().length !== 0) {
|
|
2143
|
-
throw new NulthPolicyError("auth_entry_mismatch", "authEntry has unexpected sub-invocations");
|
|
2144
|
-
}
|
|
2145
|
-
if (inv.function().switch() !== xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn()) {
|
|
2146
|
-
throw new NulthPolicyError("auth_entry_mismatch", "authEntry does not invoke a contract function");
|
|
2147
|
-
}
|
|
2148
|
-
const fn = inv.function().contractFn();
|
|
2149
|
-
const contractAddress = Address.fromScAddress(fn.contractAddress()).toString();
|
|
2150
|
-
if (contractAddress !== ctx.assetContract) {
|
|
2151
|
-
throw new NulthPolicyError(
|
|
2152
|
-
"auth_entry_mismatch",
|
|
2153
|
-
`authEntry contract ${contractAddress} does not match assetContract ${ctx.assetContract}`
|
|
2154
|
-
);
|
|
2155
|
-
}
|
|
2156
|
-
const functionName = fn.functionName().toString();
|
|
2157
|
-
if (functionName !== "transfer") {
|
|
2158
|
-
throw new NulthPolicyError(
|
|
2159
|
-
"auth_entry_mismatch",
|
|
2160
|
-
`authEntry function ${functionName} does not match expected transfer`
|
|
2161
|
-
);
|
|
2162
|
-
}
|
|
2163
|
-
const args = fn.args();
|
|
2164
|
-
if (args.length !== 3) {
|
|
2165
|
-
throw new NulthPolicyError("auth_entry_mismatch", `authEntry transfer has ${args.length} args, expected 3`);
|
|
2166
|
-
}
|
|
2167
|
-
const entryFrom = scValToNative(args[0]);
|
|
2168
|
-
if (entryFrom !== from) {
|
|
2169
|
-
throw new NulthPolicyError(
|
|
2170
|
-
"auth_entry_mismatch",
|
|
2171
|
-
`authEntry from ${entryFrom} does not match nulthAccount ${from}`
|
|
2172
|
-
);
|
|
2173
|
-
}
|
|
2174
|
-
const entryTo = scValToNative(args[1]);
|
|
2175
|
-
if (entryTo !== ctx.payee) {
|
|
2176
|
-
throw new NulthPolicyError(
|
|
2177
|
-
"auth_entry_mismatch",
|
|
2178
|
-
`authEntry to ${entryTo} does not match paymentContext.payee ${ctx.payee}`
|
|
2179
|
-
);
|
|
2180
|
-
}
|
|
2181
|
-
const entryAmount = scValToNative(args[2]);
|
|
2182
|
-
if (entryAmount !== ctx.amountStroops) {
|
|
2183
|
-
throw new NulthPolicyError(
|
|
2184
|
-
"auth_entry_mismatch",
|
|
2185
|
-
`authEntry amount ${entryAmount} does not match paymentContext.amountStroops ${ctx.amountStroops}`
|
|
2186
|
-
);
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
function insecureMockProof(preimage) {
|
|
2190
|
-
return createHash("sha512").update(`nulth:${preimage}`).digest("base64");
|
|
2191
|
-
}
|
|
2192
|
-
function encodeAuthSignature(proof) {
|
|
2193
|
-
const payload = { nulth: "zk-v1", proof };
|
|
2194
|
-
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
2195
|
-
}
|
|
2196
|
-
function usdcToStroops2(amount) {
|
|
2197
|
-
const [whole = "0", frac = ""] = amount.split(".");
|
|
2198
|
-
const padded = (frac + "0000000").slice(0, 7);
|
|
2199
|
-
return BigInt(whole) * 10000000n + BigInt(padded);
|
|
2200
|
-
}
|
|
2201
|
-
var NulthClient = class {
|
|
2202
|
-
constructor(config) {
|
|
2203
|
-
this.config = config;
|
|
2204
|
-
this.policy = { ...config.policy };
|
|
2205
|
-
this.prover = config.prover ?? "mock";
|
|
2206
|
-
if (this.prover !== "mock") {
|
|
2207
|
-
throw new RouteDockManifestError(
|
|
2208
|
-
`NulthClient: unknown prover backend ${String(config.prover)}; only 'mock' is implemented`
|
|
2209
|
-
);
|
|
2210
|
-
}
|
|
2211
|
-
if (config.network !== "testnet") {
|
|
2212
|
-
throw new RouteDockManifestError(
|
|
2213
|
-
`NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
|
|
2214
|
-
);
|
|
2215
|
-
}
|
|
2216
|
-
resolveLogger(config.logger)(
|
|
2217
|
-
"warn",
|
|
2218
|
-
"NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
|
|
2219
|
-
);
|
|
2220
|
-
}
|
|
2221
|
-
config;
|
|
2222
|
-
policy;
|
|
2223
|
-
prover;
|
|
2224
|
-
get proverBackend() {
|
|
2225
|
-
return this.prover;
|
|
2226
|
-
}
|
|
2227
|
-
get nulthAccount() {
|
|
2228
|
-
return this.config.nulthAccount;
|
|
2229
|
-
}
|
|
2230
|
-
get allowlistCommitment() {
|
|
2231
|
-
return this.policy.allowlistCommitment;
|
|
2232
|
-
}
|
|
2233
|
-
get capCommitment() {
|
|
2234
|
-
return commitDailyCap(this.policy.dailyCapStroops, this.policy.witnessSecret);
|
|
2235
|
-
}
|
|
2236
|
-
buildPaymentAuthProof(context) {
|
|
2237
|
-
this.enforcePolicy(context);
|
|
2238
|
-
const bucket = dayBucketFromLedger(context.ledgerSequence);
|
|
2239
|
-
if (bucket !== this.policy.dayBucket) {
|
|
2240
|
-
this.policy.dayBucket = bucket;
|
|
2241
|
-
this.policy.dailySpendStroops = 0n;
|
|
2242
|
-
}
|
|
2243
|
-
const authDigest = authDigestFromEntry(context.authEntry);
|
|
2244
|
-
const capCommitment = this.capCommitment;
|
|
2245
|
-
const payeeHash = hashPayee(context.payee);
|
|
2246
|
-
const preimage = [
|
|
2247
|
-
authDigest,
|
|
2248
|
-
this.policy.allowlistCommitment,
|
|
2249
|
-
capCommitment,
|
|
2250
|
-
payeeHash,
|
|
2251
|
-
context.amountStroops.toString(),
|
|
2252
|
-
bucket.toString(),
|
|
2253
|
-
this.config.nulthAccount
|
|
2254
|
-
].join("|");
|
|
2255
|
-
const proof = {
|
|
2256
|
-
version: 1,
|
|
2257
|
-
proof: insecureMockProof(preimage),
|
|
2258
|
-
publicInputs: {
|
|
2259
|
-
authDigest,
|
|
2260
|
-
allowlistCommitment: this.policy.allowlistCommitment,
|
|
2261
|
-
capCommitment,
|
|
2262
|
-
payeeHash,
|
|
2263
|
-
amountStroops: context.amountStroops.toString(),
|
|
2264
|
-
dayBucket: bucket.toString()
|
|
2265
|
-
}
|
|
2266
|
-
};
|
|
2267
|
-
this.policy.dailySpendStroops += context.amountStroops;
|
|
2268
|
-
return proof;
|
|
2269
|
-
}
|
|
2270
|
-
enforcePolicy(context) {
|
|
2271
|
-
if (context.amountStroops < 0n) {
|
|
2272
|
-
throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
|
|
2273
|
-
}
|
|
2274
|
-
if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
|
|
2275
|
-
throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
|
|
2276
|
-
}
|
|
2277
|
-
if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
|
|
2278
|
-
throw new NulthPolicyError("session_expired", "Nulth session expired");
|
|
2279
|
-
}
|
|
2280
|
-
if (!this.policy.allowedPayees.includes(context.payee)) {
|
|
2281
|
-
throw new NulthPolicyError("payee_not_allowed", `Payee ${context.payee} not in off-chain allowlist`);
|
|
2282
|
-
}
|
|
2283
|
-
const bucket = dayBucketFromLedger(context.ledgerSequence);
|
|
2284
|
-
const spend = bucket === this.policy.dayBucket ? this.policy.dailySpendStroops : 0n;
|
|
2285
|
-
if (spend + context.amountStroops > this.policy.dailyCapStroops) {
|
|
2286
|
-
throw new NulthPolicyError("daily_cap_exceeded", "Nulth daily cap exceeded");
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
};
|
|
2290
2575
|
function createNulthSigner(config) {
|
|
2291
|
-
const
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
function setNulthPaymentContext(config, context) {
|
|
2305
|
-
config.paymentContext = context;
|
|
2306
|
-
}
|
|
2307
|
-
function paymentContextFromManifest(manifest, mode, ledgerSequence) {
|
|
2308
|
-
const pricing = manifest.pricing[mode];
|
|
2309
|
-
if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
|
|
2310
|
-
return {
|
|
2311
|
-
payee: manifest.payee,
|
|
2312
|
-
amountStroops: usdcToStroops2(pricing.amount),
|
|
2313
|
-
assetContract: manifest.asset_contract,
|
|
2314
|
-
ledgerSequence
|
|
2315
|
-
};
|
|
2316
|
-
}
|
|
2317
|
-
function createPolicyState(input) {
|
|
2318
|
-
const dailyCapStroops = usdcToStroops2(input.dailyCapUsdc);
|
|
2319
|
-
return {
|
|
2320
|
-
dailyCapStroops,
|
|
2321
|
-
allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
|
|
2322
|
-
allowedPayees: input.allowedPayees,
|
|
2323
|
-
dailySpendStroops: 0n,
|
|
2324
|
-
dayBucket: dayBucketFromLedger(input.ledgerSequence ?? 0),
|
|
2325
|
-
witnessSecret: input.witnessSecret,
|
|
2326
|
-
...input.expiryLedger !== void 0 ? { expiryLedger: input.expiryLedger } : {}
|
|
2327
|
-
};
|
|
2576
|
+
const sharedConfig = { ...config, logger: resolveLogger(config.logger) };
|
|
2577
|
+
Object.defineProperty(sharedConfig, "paymentContext", {
|
|
2578
|
+
get: () => config.paymentContext
|
|
2579
|
+
});
|
|
2580
|
+
try {
|
|
2581
|
+
return createNulthSigner$1(sharedConfig);
|
|
2582
|
+
} catch (error) {
|
|
2583
|
+
if (error instanceof NulthConfigError) {
|
|
2584
|
+
throw new RouteDockManifestError(error.message, { cause: error });
|
|
2585
|
+
}
|
|
2586
|
+
throw error;
|
|
2587
|
+
}
|
|
2328
2588
|
}
|
|
2329
2589
|
function assertNulthVaultManifest(manifest, nulthAccount) {
|
|
2330
2590
|
if (manifest.vault !== void 0 && manifest.vault !== "nulth") {
|
|
@@ -2371,46 +2631,6 @@ async function prepareNulthSigner(vault, manifest, mode, network, ledgerSequence
|
|
|
2371
2631
|
const signer = createNulthSigner(config);
|
|
2372
2632
|
return { signer, config };
|
|
2373
2633
|
}
|
|
2374
|
-
function decodeAuthSignature(encoded) {
|
|
2375
|
-
const raw = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
2376
|
-
if (typeof raw !== "object" || raw === null || raw.nulth !== "zk-v1") {
|
|
2377
|
-
throw new Error("Invalid Nulth ZK auth signature");
|
|
2378
|
-
}
|
|
2379
|
-
return raw;
|
|
2380
|
-
}
|
|
2381
|
-
var FileSpendStore = class {
|
|
2382
|
-
filePath;
|
|
2383
|
-
constructor(filePath) {
|
|
2384
|
-
this.filePath = filePath;
|
|
2385
|
-
}
|
|
2386
|
-
async read() {
|
|
2387
|
-
let data;
|
|
2388
|
-
try {
|
|
2389
|
-
data = await fs.readFile(this.filePath, "utf-8");
|
|
2390
|
-
} catch (err) {
|
|
2391
|
-
if (err.code === "ENOENT") {
|
|
2392
|
-
return null;
|
|
2393
|
-
}
|
|
2394
|
-
throw new Error(
|
|
2395
|
-
`[FileSpendStore] Failed to read spend file at ${this.filePath}: ${err.message}`
|
|
2396
|
-
);
|
|
2397
|
-
}
|
|
2398
|
-
try {
|
|
2399
|
-
return JSON.parse(data);
|
|
2400
|
-
} catch {
|
|
2401
|
-
throw new Error(
|
|
2402
|
-
`[FileSpendStore] Spend file at ${this.filePath} contains invalid JSON. Delete or repair it before resuming. Raw content: ${data.slice(0, 120)}`
|
|
2403
|
-
);
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
async write(state) {
|
|
2407
|
-
const dir = path.dirname(this.filePath);
|
|
2408
|
-
await fs.mkdir(dir, { recursive: true });
|
|
2409
|
-
const tmpPath = `${this.filePath}.tmp`;
|
|
2410
|
-
await fs.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
2411
|
-
await fs.rename(tmpPath, this.filePath);
|
|
2412
|
-
}
|
|
2413
|
-
};
|
|
2414
2634
|
|
|
2415
2635
|
// src/store/SpendStore.ts
|
|
2416
2636
|
var InMemorySpendStore = class {
|
|
@@ -2524,7 +2744,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2524
2744
|
}
|
|
2525
2745
|
const secretKey = this.keypair.secret();
|
|
2526
2746
|
const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
|
|
2527
|
-
this.x402 = new X402Client(secretKey, this.network, retryPolicy);
|
|
2747
|
+
this.x402 = new X402Client(secretKey, this.network, retryPolicy, config.preSignHook, this.logger);
|
|
2528
2748
|
this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
|
|
2529
2749
|
this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
|
|
2530
2750
|
}
|
|
@@ -2532,7 +2752,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2532
2752
|
async _resolveManifest(url, options) {
|
|
2533
2753
|
const baseUrl = new URL(url).origin;
|
|
2534
2754
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2535
|
-
assertEndpointActive(manifest, url, this.logger);
|
|
2755
|
+
assertEndpointActive(manifest, url, this.logger, options?.request?.method);
|
|
2536
2756
|
const mode = selectMode(manifest, options);
|
|
2537
2757
|
return { manifest, mode };
|
|
2538
2758
|
}
|
|
@@ -2549,13 +2769,9 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2549
2769
|
});
|
|
2550
2770
|
return next;
|
|
2551
2771
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
* call before committing to a payment — for approval gates and manual
|
|
2556
|
-
* trustline remediation.
|
|
2557
|
-
*/
|
|
2558
|
-
async preflight(manifest, mode, endpoint) {
|
|
2772
|
+
async preflight(target, mode, endpoint) {
|
|
2773
|
+
if (typeof target === "string") return this._preflightUrl(target);
|
|
2774
|
+
const manifest = target;
|
|
2559
2775
|
assertManifestValid(manifest);
|
|
2560
2776
|
const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
|
|
2561
2777
|
await this._checkTrustline(manifest, selectedAsset);
|
|
@@ -2568,6 +2784,42 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2568
2784
|
...manifest.capabilities && { capabilities: manifest.capabilities }
|
|
2569
2785
|
};
|
|
2570
2786
|
}
|
|
2787
|
+
/** Manifest-only preflight for a URL — no trustline check, no payment. */
|
|
2788
|
+
async _preflightUrl(url) {
|
|
2789
|
+
const baseUrl = new URL(url).origin;
|
|
2790
|
+
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2791
|
+
assertEndpointActive(manifest, url);
|
|
2792
|
+
const costs = manifest.modes.map((mode) => {
|
|
2793
|
+
const asset = selectAsset(manifest, mode, url).asset;
|
|
2794
|
+
switch (mode) {
|
|
2795
|
+
case "x402":
|
|
2796
|
+
case "mpp-charge": {
|
|
2797
|
+
const pricing = manifest.pricing[mode];
|
|
2798
|
+
return { mode, amount: pricing.amount, per: "request", asset };
|
|
2799
|
+
}
|
|
2800
|
+
case "mpp-session":
|
|
2801
|
+
case "mpp-session-ws": {
|
|
2802
|
+
const pricing = manifest.pricing[mode];
|
|
2803
|
+
return { mode, amount: pricing.rate, per: "voucher", asset, min_deposit: pricing.min_deposit };
|
|
2804
|
+
}
|
|
2805
|
+
default:
|
|
2806
|
+
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
2809
|
+
const pathname = new URL(url).pathname;
|
|
2810
|
+
const matches = Object.values(manifest.endpoints).filter((descriptor) => descriptor.path === pathname);
|
|
2811
|
+
const endpointSunsets = matches.map((descriptor) => descriptor.sunset_at).filter((value) => typeof value === "string").sort((a, b) => Date.parse(a) - Date.parse(b));
|
|
2812
|
+
return {
|
|
2813
|
+
modes: manifest.modes,
|
|
2814
|
+
costs,
|
|
2815
|
+
compliance: {
|
|
2816
|
+
deprecated_modes: manifest.deprecated_modes ?? [],
|
|
2817
|
+
...manifest.sunset_at && { sunset_at: manifest.sunset_at },
|
|
2818
|
+
endpoint_deprecated: matches.some((descriptor) => descriptor.deprecated === true),
|
|
2819
|
+
...endpointSunsets[0] && { endpoint_sunset_at: endpointSunsets[0] }
|
|
2820
|
+
}
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2571
2823
|
/**
|
|
2572
2824
|
* Verify via Horizon that the payer account has a trustline for the
|
|
2573
2825
|
* payment asset declared in the manifest. Throws RouteDockTrustlineError
|
|
@@ -2579,8 +2831,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2579
2831
|
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
|
|
2580
2832
|
const cached = _RouteDockClient._trustlineCache.get(cacheKey);
|
|
2581
2833
|
if (cached && Date.now() < cached.expiresAt) return;
|
|
2582
|
-
const
|
|
2583
|
-
const server = new Horizon.Server(horizonUrl);
|
|
2834
|
+
const server = new Horizon.Server(HORIZON_URLS[this.network]);
|
|
2584
2835
|
try {
|
|
2585
2836
|
const account = await server.loadAccount(this.keypair.publicKey());
|
|
2586
2837
|
const balances = account.balances;
|
|
@@ -2616,9 +2867,11 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2616
2867
|
async pay(url, options) {
|
|
2617
2868
|
const baseUrl = new URL(url).origin;
|
|
2618
2869
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2619
|
-
|
|
2870
|
+
const request = options?.request;
|
|
2871
|
+
assertEndpointActive(manifest, url, this.logger, request?.method);
|
|
2620
2872
|
const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
|
|
2621
2873
|
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2874
|
+
this._assertCapAsset(selectedAsset);
|
|
2622
2875
|
await this._checkTrustline(manifest, selectedAsset);
|
|
2623
2876
|
let amount;
|
|
2624
2877
|
switch (mode) {
|
|
@@ -2636,18 +2889,18 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2636
2889
|
default:
|
|
2637
2890
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2638
2891
|
}
|
|
2639
|
-
const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
|
|
2892
|
+
const reserveId = await this._checkAndReserveSpend(amount, baseUrl, selectedAsset);
|
|
2640
2893
|
let result;
|
|
2641
2894
|
try {
|
|
2642
2895
|
if (this.vault?.mode === "nulth") {
|
|
2643
|
-
result = await this._payWithNulthVault(url, manifest, mode);
|
|
2896
|
+
result = await this._payWithNulthVault(url, manifest, mode, request);
|
|
2644
2897
|
} else {
|
|
2645
2898
|
switch (mode) {
|
|
2646
2899
|
case "x402":
|
|
2647
|
-
result = await this.x402.pay(url, manifest);
|
|
2900
|
+
result = await this.x402.pay(url, manifest, request);
|
|
2648
2901
|
break;
|
|
2649
2902
|
case "mpp-charge":
|
|
2650
|
-
result = await this.charge.pay(url, manifest);
|
|
2903
|
+
result = await this.charge.pay(url, manifest, request);
|
|
2651
2904
|
break;
|
|
2652
2905
|
default:
|
|
2653
2906
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
@@ -2666,7 +2919,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2666
2919
|
return result;
|
|
2667
2920
|
}
|
|
2668
2921
|
/** Nulth ZK vault path — proof built off-chain, attached as auth signature */
|
|
2669
|
-
async _payWithNulthVault(url, manifest, mode) {
|
|
2922
|
+
async _payWithNulthVault(url, manifest, mode, request) {
|
|
2670
2923
|
const prover = this.vault?.prover ?? "mock";
|
|
2671
2924
|
if (this.network === "mainnet" && prover === "mock") {
|
|
2672
2925
|
throw new RouteDockManifestError("nulth vault uses a MOCK Groth16 prover and cannot be used on mainnet");
|
|
@@ -2679,7 +2932,7 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2679
2932
|
try {
|
|
2680
2933
|
const { signer } = await prepareNulthSigner(this.vault, manifest, mode, this.network);
|
|
2681
2934
|
const x402 = this.x402.withSigner(signer);
|
|
2682
|
-
const result = await x402.pay(url, manifest);
|
|
2935
|
+
const result = await x402.pay(url, manifest, request);
|
|
2683
2936
|
return result;
|
|
2684
2937
|
} catch (err) {
|
|
2685
2938
|
if (err instanceof NulthPolicyError) {
|
|
@@ -2748,17 +3001,35 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2748
3001
|
);
|
|
2749
3002
|
}
|
|
2750
3003
|
const endpointKey = new URL(url).origin;
|
|
2751
|
-
|
|
3004
|
+
let onSpend;
|
|
3005
|
+
if (this.spendCap) {
|
|
3006
|
+
const sessionAsset = selectAsset(manifest, mode, url).asset;
|
|
3007
|
+
this._assertCapAsset(sessionAsset);
|
|
3008
|
+
onSpend = (amount) => this._recordSessionSpend(amount, endpointKey, sessionAsset);
|
|
3009
|
+
}
|
|
2752
3010
|
return this.session.openSession(url, manifest, secret, options, onSpend);
|
|
2753
3011
|
}
|
|
3012
|
+
/**
|
|
3013
|
+
* The local cap accumulates one USDC total. Refuse to count any other asset
|
|
3014
|
+
* against it (or let it through uncounted) when a cap is configured.
|
|
3015
|
+
*/
|
|
3016
|
+
_assertCapAsset(asset) {
|
|
3017
|
+
if (this.spendCap && asset !== this.spendCap.asset) {
|
|
3018
|
+
throw new RouteDockPolicyRejectError("local_cap_asset_mismatch", {
|
|
3019
|
+
cause: new Error(
|
|
3020
|
+
`spendCap is denominated in ${this.spendCap.asset} but the selected payment asset is ${asset}`
|
|
3021
|
+
)
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
2754
3025
|
/**
|
|
2755
3026
|
* Record spend for an MPP session voucher. Called before each voucher
|
|
2756
3027
|
* fetch in stream(). Enforces the same local daily and per-endpoint caps
|
|
2757
3028
|
* as discrete pay() calls. Returns a commit function to clean up the
|
|
2758
3029
|
* reservation after the voucher fetch succeeds.
|
|
2759
3030
|
*/
|
|
2760
|
-
async _recordSessionSpend(amount, endpointKey) {
|
|
2761
|
-
const reserveId = await this._checkAndReserveSpend(amount, endpointKey);
|
|
3031
|
+
async _recordSessionSpend(amount, endpointKey, asset) {
|
|
3032
|
+
const reserveId = await this._checkAndReserveSpend(amount, endpointKey, asset);
|
|
2762
3033
|
if (reserveId) {
|
|
2763
3034
|
await this._commitSpend(reserveId);
|
|
2764
3035
|
}
|
|
@@ -2797,15 +3068,16 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2797
3068
|
* Both limits are independent: a payment is only reserved after **both** pass.
|
|
2798
3069
|
* All spend (regardless of endpoint) counts toward the global accumulator.
|
|
2799
3070
|
*/
|
|
2800
|
-
async _checkAndReserveSpend(amount, endpointKey) {
|
|
3071
|
+
async _checkAndReserveSpend(amount, endpointKey, asset) {
|
|
2801
3072
|
if (!this.spendCap) return "";
|
|
2802
|
-
|
|
3073
|
+
this._assertCapAsset(asset);
|
|
3074
|
+
const spendCap = this.spendCap;
|
|
3075
|
+
const amountMicros = usdcToStroops(amount);
|
|
3076
|
+
const reserve = (prev) => {
|
|
2803
3077
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2804
|
-
const
|
|
2805
|
-
const current = persisted && persisted.date === today ? persisted : { date: today, totalMicros: "0", endpoints: {} };
|
|
2806
|
-
const amountMicros = usdcToStroops(amount);
|
|
3078
|
+
const current = prev && prev.date === today ? { ...prev, endpoints: { ...prev.endpoints } } : { date: today, totalMicros: "0", endpoints: {} };
|
|
2807
3079
|
const total = BigInt(current.totalMicros);
|
|
2808
|
-
const endpointCapStr =
|
|
3080
|
+
const endpointCapStr = spendCap.endpointCaps?.[endpointKey];
|
|
2809
3081
|
if (endpointCapStr !== void 0) {
|
|
2810
3082
|
const endpointCapMicros = usdcToStroops(endpointCapStr);
|
|
2811
3083
|
const endpointTotal = BigInt(current.endpoints[endpointKey] ?? "0");
|
|
@@ -2813,17 +3085,25 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2813
3085
|
throw new RouteDockPolicyRejectError("local_endpoint_cap_exceeded");
|
|
2814
3086
|
}
|
|
2815
3087
|
}
|
|
2816
|
-
const globalCapMicros = usdcToStroops(
|
|
3088
|
+
const globalCapMicros = usdcToStroops(spendCap.daily);
|
|
2817
3089
|
if (total + amountMicros > globalCapMicros) {
|
|
2818
3090
|
throw new RouteDockPolicyRejectError("local_daily_cap_exceeded");
|
|
2819
3091
|
}
|
|
2820
|
-
const reserveId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2821
|
-
this._pendingReserves.set(reserveId, { endpointKey, amountMicros });
|
|
2822
3092
|
current.totalMicros = (total + amountMicros).toString();
|
|
2823
3093
|
if (endpointCapStr !== void 0) {
|
|
2824
3094
|
current.endpoints[endpointKey] = (BigInt(current.endpoints[endpointKey] ?? "0") + amountMicros).toString();
|
|
2825
3095
|
}
|
|
2826
|
-
|
|
3096
|
+
return current;
|
|
3097
|
+
};
|
|
3098
|
+
return this._withSpendMutex(async () => {
|
|
3099
|
+
const store = this.spendStore;
|
|
3100
|
+
if (store.update) {
|
|
3101
|
+
await store.update(reserve);
|
|
3102
|
+
} else {
|
|
3103
|
+
await store.write(reserve(await store.read()));
|
|
3104
|
+
}
|
|
3105
|
+
const reserveId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3106
|
+
this._pendingReserves.set(reserveId, { endpointKey, amountMicros });
|
|
2827
3107
|
return reserveId;
|
|
2828
3108
|
});
|
|
2829
3109
|
}
|
|
@@ -2845,22 +3125,29 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2845
3125
|
if (!reserve) return;
|
|
2846
3126
|
this._pendingReserves.delete(reserveId);
|
|
2847
3127
|
const { endpointKey, amountMicros } = reserve;
|
|
2848
|
-
|
|
3128
|
+
const release = (prev) => {
|
|
2849
3129
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
persisted.totalMicros = (currentTotal - amountMicros).toString();
|
|
3130
|
+
if (!prev || prev.date !== today) return void 0;
|
|
3131
|
+
const persisted = { ...prev, endpoints: { ...prev.endpoints } };
|
|
3132
|
+
persisted.totalMicros = (BigInt(persisted.totalMicros) - amountMicros).toString();
|
|
2854
3133
|
if (persisted.endpoints[endpointKey]) {
|
|
2855
|
-
const
|
|
2856
|
-
const newEndpointTotal = endpointTotal - amountMicros;
|
|
3134
|
+
const newEndpointTotal = BigInt(persisted.endpoints[endpointKey]) - amountMicros;
|
|
2857
3135
|
if (newEndpointTotal <= 0n) {
|
|
2858
3136
|
delete persisted.endpoints[endpointKey];
|
|
2859
3137
|
} else {
|
|
2860
3138
|
persisted.endpoints[endpointKey] = newEndpointTotal.toString();
|
|
2861
3139
|
}
|
|
2862
3140
|
}
|
|
2863
|
-
|
|
3141
|
+
return persisted;
|
|
3142
|
+
};
|
|
3143
|
+
await this._withSpendMutex(async () => {
|
|
3144
|
+
const store = this.spendStore;
|
|
3145
|
+
if (store.update) {
|
|
3146
|
+
await store.update(release);
|
|
3147
|
+
} else {
|
|
3148
|
+
const next = release(await store.read());
|
|
3149
|
+
if (next) await store.write(next);
|
|
3150
|
+
}
|
|
2864
3151
|
});
|
|
2865
3152
|
}
|
|
2866
3153
|
};
|
|
@@ -2944,6 +3231,158 @@ var SupabaseSessionStore = class {
|
|
|
2944
3231
|
await this.setStatus(channelId, "closed");
|
|
2945
3232
|
}
|
|
2946
3233
|
};
|
|
3234
|
+
var DEFAULT_LOCK_STALE_MS = 1e4;
|
|
3235
|
+
var DEFAULT_LOCK_TIMEOUT_MS = 15e3;
|
|
3236
|
+
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3237
|
+
var FileSpendStore = class {
|
|
3238
|
+
filePath;
|
|
3239
|
+
lockPath;
|
|
3240
|
+
lockStaleMs;
|
|
3241
|
+
lockTimeoutMs;
|
|
3242
|
+
constructor(filePath, options = {}) {
|
|
3243
|
+
this.filePath = filePath;
|
|
3244
|
+
this.lockPath = `${filePath}.lock`;
|
|
3245
|
+
this.lockStaleMs = options.lockStaleMs ?? DEFAULT_LOCK_STALE_MS;
|
|
3246
|
+
this.lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
3247
|
+
}
|
|
3248
|
+
async read() {
|
|
3249
|
+
let data;
|
|
3250
|
+
try {
|
|
3251
|
+
data = await fs.readFile(this.filePath, "utf-8");
|
|
3252
|
+
} catch (err) {
|
|
3253
|
+
if (err.code === "ENOENT") {
|
|
3254
|
+
return null;
|
|
3255
|
+
}
|
|
3256
|
+
throw new Error(
|
|
3257
|
+
`[FileSpendStore] Failed to read spend file at ${this.filePath}: ${err.message}`
|
|
3258
|
+
);
|
|
3259
|
+
}
|
|
3260
|
+
try {
|
|
3261
|
+
return JSON.parse(data);
|
|
3262
|
+
} catch {
|
|
3263
|
+
throw new Error(
|
|
3264
|
+
`[FileSpendStore] Spend file at ${this.filePath} contains invalid JSON. Delete or repair it before resuming. Raw content: ${data.slice(0, 120)}`
|
|
3265
|
+
);
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
async write(state) {
|
|
3269
|
+
await this.withLock(async (assertHeld) => {
|
|
3270
|
+
await this.writeFile(state, assertHeld);
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
/**
|
|
3274
|
+
* Atomically read-modify-write the accumulator under the cross-process lock.
|
|
3275
|
+
* `fn` receives the current state (or null) and returns the state to persist,
|
|
3276
|
+
* or `undefined` to leave the file untouched. If `fn` throws, nothing is
|
|
3277
|
+
* written and the lock is released.
|
|
3278
|
+
*/
|
|
3279
|
+
async update(fn) {
|
|
3280
|
+
return this.withLock(async (assertHeld) => {
|
|
3281
|
+
const prev = await this.read();
|
|
3282
|
+
const next = fn(prev);
|
|
3283
|
+
if (next === void 0) return prev;
|
|
3284
|
+
await this.writeFile(next, assertHeld);
|
|
3285
|
+
return next;
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
// ── internals ──────────────────────────────────────────────────────────────
|
|
3289
|
+
async writeFile(state, assertHeld) {
|
|
3290
|
+
const tmpPath = `${this.filePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
3291
|
+
try {
|
|
3292
|
+
await fs.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
3293
|
+
await assertHeld();
|
|
3294
|
+
await fs.rename(tmpPath, this.filePath);
|
|
3295
|
+
} catch (err) {
|
|
3296
|
+
await fs.rm(tmpPath, { force: true }).catch(() => {
|
|
3297
|
+
});
|
|
3298
|
+
throw err;
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
/** Run `fn` while holding the lock file; always releases it. */
|
|
3302
|
+
async withLock(fn) {
|
|
3303
|
+
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
3304
|
+
const token = `${process.pid}:${randomBytes(8).toString("hex")}`;
|
|
3305
|
+
await this.acquireLock(token);
|
|
3306
|
+
const heartbeat = setInterval(() => {
|
|
3307
|
+
const now = /* @__PURE__ */ new Date();
|
|
3308
|
+
fs.utimes(this.lockPath, now, now).catch(() => {
|
|
3309
|
+
});
|
|
3310
|
+
}, Math.max(10, Math.floor(this.lockStaleMs / 3)));
|
|
3311
|
+
heartbeat.unref();
|
|
3312
|
+
const assertHeld = async () => {
|
|
3313
|
+
let current;
|
|
3314
|
+
try {
|
|
3315
|
+
current = await fs.readFile(this.lockPath, "utf-8");
|
|
3316
|
+
} catch {
|
|
3317
|
+
current = "";
|
|
3318
|
+
}
|
|
3319
|
+
if (current !== token) {
|
|
3320
|
+
throw new Error(
|
|
3321
|
+
`[FileSpendStore] Lost the lock on ${this.lockPath} before committing; refusing to overwrite the spend file. Retry the operation.`
|
|
3322
|
+
);
|
|
3323
|
+
}
|
|
3324
|
+
};
|
|
3325
|
+
try {
|
|
3326
|
+
return await fn(assertHeld);
|
|
3327
|
+
} finally {
|
|
3328
|
+
clearInterval(heartbeat);
|
|
3329
|
+
await this.releaseLock(token);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
async acquireLock(token) {
|
|
3333
|
+
const deadline = Date.now() + this.lockTimeoutMs;
|
|
3334
|
+
let delay2 = 5;
|
|
3335
|
+
for (; ; ) {
|
|
3336
|
+
try {
|
|
3337
|
+
const handle = await fs.open(this.lockPath, "wx");
|
|
3338
|
+
try {
|
|
3339
|
+
await handle.writeFile(token, "utf-8");
|
|
3340
|
+
} finally {
|
|
3341
|
+
await handle.close();
|
|
3342
|
+
}
|
|
3343
|
+
return;
|
|
3344
|
+
} catch (err) {
|
|
3345
|
+
if (err.code !== "EEXIST") {
|
|
3346
|
+
throw new Error(
|
|
3347
|
+
`[FileSpendStore] Failed to create lock file ${this.lockPath}: ${err.message}`
|
|
3348
|
+
);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
await this.breakIfStale();
|
|
3352
|
+
if (Date.now() >= deadline) {
|
|
3353
|
+
throw new Error(
|
|
3354
|
+
`[FileSpendStore] Timed out after ${this.lockTimeoutMs}ms waiting for ${this.lockPath}. Another process is holding the spend file lock; if none is running, delete the lock file.`
|
|
3355
|
+
);
|
|
3356
|
+
}
|
|
3357
|
+
await sleep2(delay2 + Math.floor(Math.random() * delay2));
|
|
3358
|
+
delay2 = Math.min(delay2 * 2, 50);
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
/** Remove the lock if its holder stopped refreshing it (crashed process). */
|
|
3362
|
+
async breakIfStale() {
|
|
3363
|
+
let before;
|
|
3364
|
+
try {
|
|
3365
|
+
before = await fs.stat(this.lockPath);
|
|
3366
|
+
} catch {
|
|
3367
|
+
return;
|
|
3368
|
+
}
|
|
3369
|
+
if (Date.now() - before.mtimeMs <= this.lockStaleMs) return;
|
|
3370
|
+
try {
|
|
3371
|
+
const after = await fs.stat(this.lockPath);
|
|
3372
|
+
if (after.ino !== before.ino || after.mtimeMs !== before.mtimeMs) return;
|
|
3373
|
+
await fs.unlink(this.lockPath);
|
|
3374
|
+
} catch {
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
async releaseLock(token) {
|
|
3378
|
+
try {
|
|
3379
|
+
if (await fs.readFile(this.lockPath, "utf-8") === token) {
|
|
3380
|
+
await fs.unlink(this.lockPath);
|
|
3381
|
+
}
|
|
3382
|
+
} catch {
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
};
|
|
2947
3386
|
|
|
2948
3387
|
// src/store/SupabaseSeenTxStore.ts
|
|
2949
3388
|
var SupabaseSeenTxStore = class {
|
|
@@ -2995,6 +3434,107 @@ var SupabaseSeenTxStore = class {
|
|
|
2995
3434
|
return data;
|
|
2996
3435
|
}
|
|
2997
3436
|
};
|
|
3437
|
+
var FLAT_MODES = ["x402", "mpp-charge"];
|
|
3438
|
+
var SESSION_MODES = ["mpp-session", "mpp-session-ws"];
|
|
3439
|
+
function sameAmount(adapter, label, optionValue, manifestValue) {
|
|
3440
|
+
const parse = (where, value) => {
|
|
3441
|
+
try {
|
|
3442
|
+
return usdcToStroops(value);
|
|
3443
|
+
} catch {
|
|
3444
|
+
throw new RouteDockManifestError(
|
|
3445
|
+
`${adapter}: ${label} has an unparseable ${where} amount "${value}"`
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
};
|
|
3449
|
+
return parse("option", optionValue) === parse("manifest", manifestValue);
|
|
3450
|
+
}
|
|
3451
|
+
function assertProviderConfig(opts, adapterName) {
|
|
3452
|
+
const { manifest } = opts;
|
|
3453
|
+
if (manifest === null || typeof manifest !== "object") {
|
|
3454
|
+
throw new RouteDockManifestError(`${adapterName}: opts.manifest is required and must be an object`);
|
|
3455
|
+
}
|
|
3456
|
+
try {
|
|
3457
|
+
validateManifestSchema(manifest);
|
|
3458
|
+
assertManifestValid(manifest);
|
|
3459
|
+
} catch (err) {
|
|
3460
|
+
if (err instanceof RouteDockManifestError) {
|
|
3461
|
+
throw new RouteDockManifestError(`${adapterName}: ${err.message}`, { cause: err });
|
|
3462
|
+
}
|
|
3463
|
+
throw err;
|
|
3464
|
+
}
|
|
3465
|
+
const served = new Set(opts.modes.filter((mode) => Boolean(opts.pricing[mode])));
|
|
3466
|
+
const advertised = new Set(manifest.modes);
|
|
3467
|
+
const notAdvertised = [...served].filter((mode) => !advertised.has(mode));
|
|
3468
|
+
const notServed = [...advertised].filter((mode) => !served.has(mode));
|
|
3469
|
+
if (notAdvertised.length > 0 || notServed.length > 0) {
|
|
3470
|
+
const parts = [];
|
|
3471
|
+
if (notAdvertised.length > 0) parts.push(`served but not in manifest.modes: ${notAdvertised.join(", ")}`);
|
|
3472
|
+
if (notServed.length > 0) parts.push(`in manifest.modes but not served (missing from modes or pricing): ${notServed.join(", ")}`);
|
|
3473
|
+
throw new RouteDockManifestError(`${adapterName}: modes do not match manifest.modes (${parts.join("; ")})`);
|
|
3474
|
+
}
|
|
3475
|
+
for (const mode of FLAT_MODES) {
|
|
3476
|
+
if (!served.has(mode)) continue;
|
|
3477
|
+
const optionValue = opts.pricing[mode];
|
|
3478
|
+
const manifestValue = manifest.pricing[mode]?.amount;
|
|
3479
|
+
if (manifestValue === void 0) {
|
|
3480
|
+
throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode}.amount is missing but ${mode} is served`);
|
|
3481
|
+
}
|
|
3482
|
+
if (!sameAmount(adapterName, `pricing.${mode}`, optionValue, manifestValue)) {
|
|
3483
|
+
throw new RouteDockManifestError(
|
|
3484
|
+
`${adapterName}: pricing.${mode} "${optionValue}" does not match manifest.pricing.${mode}.amount "${manifestValue}"`
|
|
3485
|
+
);
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
for (const mode of SESSION_MODES) {
|
|
3489
|
+
if (!served.has(mode)) continue;
|
|
3490
|
+
const optionValue = opts.pricing[mode];
|
|
3491
|
+
const manifestValue = manifest.pricing[mode];
|
|
3492
|
+
if (!manifestValue) {
|
|
3493
|
+
throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode} is missing but ${mode} is served`);
|
|
3494
|
+
}
|
|
3495
|
+
if (!sameAmount(adapterName, `pricing.${mode}.rate`, optionValue.rate, manifestValue.rate)) {
|
|
3496
|
+
throw new RouteDockManifestError(
|
|
3497
|
+
`${adapterName}: pricing.${mode}.rate "${optionValue.rate}" does not match manifest.pricing.${mode}.rate "${manifestValue.rate}"`
|
|
3498
|
+
);
|
|
3499
|
+
}
|
|
3500
|
+
if (optionValue.channelFactory !== manifestValue.channel_factory) {
|
|
3501
|
+
throw new RouteDockManifestError(
|
|
3502
|
+
`${adapterName}: pricing.${mode}.channelFactory "${optionValue.channelFactory}" does not match manifest.pricing.${mode}.channel_factory "${manifestValue.channel_factory}"`
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
if (opts.assetContract !== void 0 && opts.assetContract !== manifest.asset_contract) {
|
|
3507
|
+
throw new RouteDockManifestError(
|
|
3508
|
+
`${adapterName}: assetContract "${opts.assetContract}" does not match manifest.asset_contract "${manifest.asset_contract}"`
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3511
|
+
if (opts.asset !== void 0 && opts.asset !== manifest.asset) {
|
|
3512
|
+
throw new RouteDockManifestError(
|
|
3513
|
+
`${adapterName}: asset "${opts.asset}" does not match manifest.asset "${manifest.asset}"`
|
|
3514
|
+
);
|
|
3515
|
+
}
|
|
3516
|
+
if (opts.network !== manifest.network) {
|
|
3517
|
+
throw new RouteDockManifestError(
|
|
3518
|
+
`${adapterName}: network "${opts.network}" does not match manifest.network "${manifest.network}"`
|
|
3519
|
+
);
|
|
3520
|
+
}
|
|
3521
|
+
let signerPublicKey;
|
|
3522
|
+
try {
|
|
3523
|
+
signerPublicKey = Keypair.fromSecret(opts.payeeSecretKey).publicKey();
|
|
3524
|
+
} catch (err) {
|
|
3525
|
+
throw new RouteDockManifestError(`${adapterName}: payeeSecretKey is not a valid Stellar secret key`, { cause: err });
|
|
3526
|
+
}
|
|
3527
|
+
if (signerPublicKey !== manifest.payee) {
|
|
3528
|
+
throw new RouteDockManifestError(
|
|
3529
|
+
`${adapterName}: payeeSecretKey belongs to ${signerPublicKey}, not manifest.payee ${manifest.payee}; clients would reject the signed manifest`
|
|
3530
|
+
);
|
|
3531
|
+
}
|
|
3532
|
+
if (opts.payee !== manifest.payee) {
|
|
3533
|
+
throw new RouteDockManifestError(
|
|
3534
|
+
`${adapterName}: payee "${opts.payee}" does not match manifest.payee "${manifest.payee}"`
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
2998
3538
|
function extractPayerAddress(key) {
|
|
2999
3539
|
if (typeof key !== "string") return null;
|
|
3000
3540
|
return StrKey.isValidEd25519PublicKey(key) || StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
@@ -3003,6 +3543,18 @@ function extractPayerAddress(key) {
|
|
|
3003
3543
|
// src/provider/SeenTxStore.ts
|
|
3004
3544
|
var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
|
|
3005
3545
|
var MAX_SETTLEMENT_REPLAYS = 1;
|
|
3546
|
+
async function recordSettlement(store, key, record, ctx) {
|
|
3547
|
+
try {
|
|
3548
|
+
await store.set(key, record);
|
|
3549
|
+
} catch (err) {
|
|
3550
|
+
ctx.logger("error", `${ctx.label} seenTxStore.set failed after settlement`, { error: err });
|
|
3551
|
+
try {
|
|
3552
|
+
ctx.onCallbackError?.(err, "seenTxStore.set");
|
|
3553
|
+
} catch (cbErr) {
|
|
3554
|
+
ctx.logger("error", `${ctx.label} onCallbackError threw`, { error: cbErr });
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3006
3558
|
async function checkSettlementReplay(store, key, now = Date.now()) {
|
|
3007
3559
|
const record = await store.get(key);
|
|
3008
3560
|
if (!record) return { kind: "miss" };
|
|
@@ -3057,7 +3609,7 @@ var InMemorySeenTxStore = class {
|
|
|
3057
3609
|
return next;
|
|
3058
3610
|
}
|
|
3059
3611
|
};
|
|
3060
|
-
async function
|
|
3612
|
+
async function sha256Hex(input) {
|
|
3061
3613
|
const buf = new TextEncoder().encode(input);
|
|
3062
3614
|
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
3063
3615
|
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
@@ -3065,22 +3617,11 @@ async function sha256Hex2(input) {
|
|
|
3065
3617
|
async function paymentIdempotencyKey(getHeader, scope) {
|
|
3066
3618
|
const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
|
|
3067
3619
|
if (!material) return null;
|
|
3068
|
-
return
|
|
3620
|
+
return sha256Hex(
|
|
3069
3621
|
[scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
|
|
3070
3622
|
);
|
|
3071
3623
|
}
|
|
3072
3624
|
|
|
3073
|
-
// src/provider/settleResult.ts
|
|
3074
|
-
function readSettleResult(result) {
|
|
3075
|
-
const settle = result;
|
|
3076
|
-
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
3077
|
-
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
3078
|
-
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
3079
|
-
return failed(settle.errorReason);
|
|
3080
|
-
}
|
|
3081
|
-
return { ok: true, txHash: settle.transaction };
|
|
3082
|
-
}
|
|
3083
|
-
|
|
3084
3625
|
// src/provider/x402Handler.ts
|
|
3085
3626
|
var CAIP22 = {
|
|
3086
3627
|
testnet: "stellar:testnet",
|
|
@@ -3220,10 +3761,10 @@ function createX402Handler(opts) {
|
|
|
3220
3761
|
requirements
|
|
3221
3762
|
);
|
|
3222
3763
|
if (!verifyResult.isValid) {
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3764
|
+
await respondPaymentRequired(
|
|
3765
|
+
"Payment verification failed",
|
|
3766
|
+
verifyResult.invalidReason
|
|
3767
|
+
);
|
|
3227
3768
|
return;
|
|
3228
3769
|
}
|
|
3229
3770
|
const settleResult = await localFacilitator.settle(
|
|
@@ -3249,7 +3790,12 @@ function createX402Handler(opts) {
|
|
|
3249
3790
|
if (typeof paymentResponse === "string") {
|
|
3250
3791
|
headers["X-Payment-Response"] = paymentResponse;
|
|
3251
3792
|
}
|
|
3252
|
-
await
|
|
3793
|
+
await recordSettlement(
|
|
3794
|
+
seenTxStore,
|
|
3795
|
+
idempotencyKey,
|
|
3796
|
+
{ txHash, headers, createdAt: Date.now() },
|
|
3797
|
+
{ logger, label: "[x402]", onCallbackError: opts.onCallbackError }
|
|
3798
|
+
);
|
|
3253
3799
|
}
|
|
3254
3800
|
if (txHash && opts.onSettled) {
|
|
3255
3801
|
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
@@ -3279,21 +3825,21 @@ function createMppChargeHandler(opts) {
|
|
|
3279
3825
|
const logger = resolveLogger(opts.logger);
|
|
3280
3826
|
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
3281
3827
|
const mppxInstances = /* @__PURE__ */ new Map();
|
|
3282
|
-
function getMppx(
|
|
3283
|
-
let instance = mppxInstances.get(
|
|
3828
|
+
function getMppx(contract2) {
|
|
3829
|
+
let instance = mppxInstances.get(contract2);
|
|
3284
3830
|
if (!instance) {
|
|
3285
3831
|
instance = Mppx$1.create({
|
|
3286
3832
|
secretKey: opts.payeeSecretKey,
|
|
3287
3833
|
methods: [
|
|
3288
3834
|
stellar$3.charge({
|
|
3289
3835
|
recipient,
|
|
3290
|
-
currency:
|
|
3836
|
+
currency: contract2,
|
|
3291
3837
|
network: networkId,
|
|
3292
3838
|
feePayer: { envelopeSigner: opts.payeeSecretKey }
|
|
3293
3839
|
})
|
|
3294
3840
|
]
|
|
3295
3841
|
});
|
|
3296
|
-
mppxInstances.set(
|
|
3842
|
+
mppxInstances.set(contract2, instance);
|
|
3297
3843
|
}
|
|
3298
3844
|
return instance;
|
|
3299
3845
|
}
|
|
@@ -3314,7 +3860,7 @@ function createMppChargeHandler(opts) {
|
|
|
3314
3860
|
const credPart = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
|
|
3315
3861
|
if (credPart) {
|
|
3316
3862
|
const b64 = credPart.split("=").slice(1).join("=").replace(/^"|"$/g, "");
|
|
3317
|
-
const credJson =
|
|
3863
|
+
const credJson = base64ToUtf8(b64);
|
|
3318
3864
|
const cred = JSON.parse(credJson);
|
|
3319
3865
|
const key = cred.sender ?? cred.payload?.sender ?? cred.payload?.from;
|
|
3320
3866
|
payerAddress = extractPayerAddress(key);
|
|
@@ -3376,18 +3922,19 @@ function createMppChargeHandler(opts) {
|
|
|
3376
3922
|
if (receiptHeader) {
|
|
3377
3923
|
try {
|
|
3378
3924
|
const parsed = JSON.parse(
|
|
3379
|
-
|
|
3925
|
+
base64ToUtf8(receiptHeader)
|
|
3380
3926
|
);
|
|
3381
3927
|
reference = parsed.reference;
|
|
3382
3928
|
} catch {
|
|
3383
3929
|
}
|
|
3384
3930
|
}
|
|
3385
3931
|
if (idempotencyKey) {
|
|
3386
|
-
await
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
createdAt: Date.now()
|
|
3390
|
-
|
|
3932
|
+
await recordSettlement(
|
|
3933
|
+
seenTxStore,
|
|
3934
|
+
idempotencyKey,
|
|
3935
|
+
{ txHash: reference ?? null, headers: receiptHeaders, createdAt: Date.now() },
|
|
3936
|
+
{ logger, label: "[mpp-charge]", onCallbackError: opts.onCallbackError }
|
|
3937
|
+
);
|
|
3391
3938
|
}
|
|
3392
3939
|
if (reference && opts.onSettled) {
|
|
3393
3940
|
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
@@ -3401,6 +3948,14 @@ function createMppChargeHandler(opts) {
|
|
|
3401
3948
|
}
|
|
3402
3949
|
};
|
|
3403
3950
|
}
|
|
3951
|
+
|
|
3952
|
+
// src/provider/internal/vaultRpc.ts
|
|
3953
|
+
async function createSorobanRpcServer(rpcUrl) {
|
|
3954
|
+
const { Server } = await import('@stellar/stellar-sdk/rpc');
|
|
3955
|
+
return new Server(rpcUrl);
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
// src/provider/internal/vaultSettlement.ts
|
|
3404
3959
|
function resolveVaultSettlementAddresses(payer, payee) {
|
|
3405
3960
|
if (!payer) return null;
|
|
3406
3961
|
return { payer, payee };
|
|
@@ -3420,6 +3975,111 @@ function buildSessionSettlementTransaction(source, vaultContractId, channelId, a
|
|
|
3420
3975
|
networkPassphrase
|
|
3421
3976
|
}).addOperation(op).setTimeout(30).build();
|
|
3422
3977
|
}
|
|
3978
|
+
var DEFAULT_RPC_URLS = {
|
|
3979
|
+
testnet: "https://soroban-testnet.stellar.org",
|
|
3980
|
+
mainnet: "https://mainnet.sorobanrpc.com"
|
|
3981
|
+
};
|
|
3982
|
+
var CONFIRM_TIMEOUT_MS = 12e4;
|
|
3983
|
+
var CONFIRM_POLL_MS = 1e3;
|
|
3984
|
+
function delay(ms) {
|
|
3985
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3986
|
+
}
|
|
3987
|
+
function describeSendError(sent) {
|
|
3988
|
+
const errorResult = sent.errorResult;
|
|
3989
|
+
try {
|
|
3990
|
+
const name = errorResult?.result?.().switch?.().name;
|
|
3991
|
+
if (name) return `sendTransaction returned ERROR: ${name}`;
|
|
3992
|
+
} catch {
|
|
3993
|
+
}
|
|
3994
|
+
return `sendTransaction returned status ${sent.status}`;
|
|
3995
|
+
}
|
|
3996
|
+
async function waitForConfirmation(server, txHash) {
|
|
3997
|
+
const deadline = Date.now() + CONFIRM_TIMEOUT_MS;
|
|
3998
|
+
for (; ; ) {
|
|
3999
|
+
const tx = await server.getTransaction(txHash);
|
|
4000
|
+
if (tx.status === "SUCCESS") return "success";
|
|
4001
|
+
if (tx.status === "FAILED") return "failed";
|
|
4002
|
+
if (Date.now() >= deadline) return "timeout";
|
|
4003
|
+
await delay(CONFIRM_POLL_MS);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
async function recordSessionSettlement(request, { logger, onCallbackError }) {
|
|
4007
|
+
const reportFailure = (fields) => {
|
|
4008
|
+
logger("error", "[vault] failed to record session_settled", {
|
|
4009
|
+
channelId: request.channelId,
|
|
4010
|
+
...fields.txHash ? { txHash: fields.txHash } : {},
|
|
4011
|
+
reason: fields.reason,
|
|
4012
|
+
...fields.error !== void 0 ? { error: fields.error } : {}
|
|
4013
|
+
});
|
|
4014
|
+
if (onCallbackError) {
|
|
4015
|
+
const err = fields.error ?? new Error(`session_settled vault record failed: ${fields.reason}`);
|
|
4016
|
+
onCallbackError(err, "vaultSettlement");
|
|
4017
|
+
}
|
|
4018
|
+
return { ok: false, reason: fields.reason, ...fields.txHash ? { txHash: fields.txHash } : {} };
|
|
4019
|
+
};
|
|
4020
|
+
try {
|
|
4021
|
+
const payeeKeypair = Keypair.fromSecret(request.payeeSecretKey);
|
|
4022
|
+
const addresses = resolveVaultSettlementAddresses(request.payer, payeeKeypair.publicKey());
|
|
4023
|
+
if (!addresses) {
|
|
4024
|
+
logger("warn", "[vault] skipped session_settled record: payer address unavailable", {
|
|
4025
|
+
channelId: request.channelId
|
|
4026
|
+
});
|
|
4027
|
+
return { ok: false, reason: "payer address unavailable" };
|
|
4028
|
+
}
|
|
4029
|
+
const networkPassphrase = request.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
4030
|
+
const rpcUrl = request.config.rpcUrl ?? DEFAULT_RPC_URLS[request.network];
|
|
4031
|
+
const server = await createSorobanRpcServer(rpcUrl);
|
|
4032
|
+
const sourceAccount = await server.getAccount(payeeKeypair.publicKey());
|
|
4033
|
+
const tx = buildSessionSettlementTransaction(
|
|
4034
|
+
sourceAccount,
|
|
4035
|
+
request.config.contract,
|
|
4036
|
+
request.channelId,
|
|
4037
|
+
addresses,
|
|
4038
|
+
request.amount,
|
|
4039
|
+
request.voucherCount,
|
|
4040
|
+
networkPassphrase
|
|
4041
|
+
);
|
|
4042
|
+
const preparedTx = await server.prepareTransaction(tx);
|
|
4043
|
+
preparedTx.sign(payeeKeypair);
|
|
4044
|
+
const sent = await server.sendTransaction(preparedTx);
|
|
4045
|
+
if (sent.status === "ERROR") {
|
|
4046
|
+
return reportFailure({
|
|
4047
|
+
txHash: sent.hash || preparedTx.hash().toString("hex"),
|
|
4048
|
+
reason: describeSendError(sent)
|
|
4049
|
+
});
|
|
4050
|
+
}
|
|
4051
|
+
const txHash = sent.hash || preparedTx.hash().toString("hex");
|
|
4052
|
+
const confirmation = await waitForConfirmation(server, txHash);
|
|
4053
|
+
if (confirmation === "success") {
|
|
4054
|
+
logger("debug", "[vault] recorded session_settled", {
|
|
4055
|
+
channelId: request.channelId,
|
|
4056
|
+
txHash
|
|
4057
|
+
});
|
|
4058
|
+
return { ok: true, txHash };
|
|
4059
|
+
}
|
|
4060
|
+
return reportFailure({
|
|
4061
|
+
txHash,
|
|
4062
|
+
reason: confirmation === "failed" ? "transaction failed on-chain" : `confirmation timed out after ${CONFIRM_TIMEOUT_MS}ms`
|
|
4063
|
+
});
|
|
4064
|
+
} catch (err) {
|
|
4065
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
4066
|
+
return reportFailure({ reason, error: err });
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
// src/internal/vaultConfig.ts
|
|
4071
|
+
function resolveVaultSettlementConfig(option, logger) {
|
|
4072
|
+
if (option) return option;
|
|
4073
|
+
if (typeof process === "undefined") return void 0;
|
|
4074
|
+
const fromEnv = process.env.AGENT_VAULT_CONTRACT ?? process.env.AGENT_VAULT_CONTRACT_ID;
|
|
4075
|
+
if (!fromEnv) return void 0;
|
|
4076
|
+
logger(
|
|
4077
|
+
"warn",
|
|
4078
|
+
"[vault] configuring the vault contract via the AGENT_VAULT_CONTRACT/AGENT_VAULT_CONTRACT_ID environment variables is deprecated and will be removed in the next minor release \u2014 pass the vaultSettlement option instead",
|
|
4079
|
+
{}
|
|
4080
|
+
);
|
|
4081
|
+
return { contract: fromEnv };
|
|
4082
|
+
}
|
|
3423
4083
|
function channelAuthorizer(envelopeSigner) {
|
|
3424
4084
|
return { envelopeSigner };
|
|
3425
4085
|
}
|
|
@@ -3498,6 +4158,7 @@ function createMppSessionHandler(opts) {
|
|
|
3498
4158
|
let record = null;
|
|
3499
4159
|
let voucherCount = 0;
|
|
3500
4160
|
let sessionOpened = false;
|
|
4161
|
+
let orphanFlagged = false;
|
|
3501
4162
|
let idleTimer = null;
|
|
3502
4163
|
let settledCleanly = false;
|
|
3503
4164
|
let closeListenerArmed = false;
|
|
@@ -3523,6 +4184,14 @@ function createMppSessionHandler(opts) {
|
|
|
3523
4184
|
return null;
|
|
3524
4185
|
}
|
|
3525
4186
|
record = { amount: BigInt(stored.amount), signature: stored.signature, payer: stored.payer ?? null };
|
|
4187
|
+
if (typeof stored.voucherCount === "number") {
|
|
4188
|
+
voucherCount = stored.voucherCount;
|
|
4189
|
+
}
|
|
4190
|
+
if (typeof stored.sessionOpened === "boolean") {
|
|
4191
|
+
sessionOpened = stored.sessionOpened;
|
|
4192
|
+
} else if (record !== null) {
|
|
4193
|
+
sessionOpened = true;
|
|
4194
|
+
}
|
|
3526
4195
|
} catch {
|
|
3527
4196
|
return null;
|
|
3528
4197
|
}
|
|
@@ -3530,8 +4199,8 @@ function createMppSessionHandler(opts) {
|
|
|
3530
4199
|
}
|
|
3531
4200
|
async function flagOrphan(reason) {
|
|
3532
4201
|
await loadPersistedRecord();
|
|
3533
|
-
if (!sessionOpened || settledCleanly) return;
|
|
3534
|
-
|
|
4202
|
+
if (!sessionOpened || settledCleanly || orphanFlagged) return;
|
|
4203
|
+
orphanFlagged = true;
|
|
3535
4204
|
closeListenerArmed = false;
|
|
3536
4205
|
clearIdleTimer();
|
|
3537
4206
|
const cumulativeAmount = record ? (Number(record.amount) / 1e7).toFixed(7) : "0.0000000";
|
|
@@ -3579,16 +4248,20 @@ function createMppSessionHandler(opts) {
|
|
|
3579
4248
|
if (record && amount < record.amount) return;
|
|
3580
4249
|
const payer = record?.payer ?? extractPayerAddress(credential.source);
|
|
3581
4250
|
record = { amount, signature: payload.signature, payer };
|
|
4251
|
+
voucherCount++;
|
|
4252
|
+
settledCleanly = false;
|
|
4253
|
+
orphanFlagged = false;
|
|
4254
|
+
armIdleTimer();
|
|
4255
|
+
const wasOpened = sessionOpened;
|
|
4256
|
+
sessionOpened = true;
|
|
3582
4257
|
await innerStore.put(voucherRecordKey, {
|
|
3583
4258
|
amount: record.amount.toString(),
|
|
3584
4259
|
signature: record.signature,
|
|
3585
|
-
payer: record.payer
|
|
4260
|
+
payer: record.payer,
|
|
4261
|
+
voucherCount,
|
|
4262
|
+
sessionOpened: true
|
|
3586
4263
|
});
|
|
3587
|
-
|
|
3588
|
-
settledCleanly = false;
|
|
3589
|
-
armIdleTimer();
|
|
3590
|
-
if (!sessionOpened) {
|
|
3591
|
-
sessionOpened = true;
|
|
4264
|
+
if (!wasOpened) {
|
|
3592
4265
|
if (opts.onSessionOpen) {
|
|
3593
4266
|
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
3594
4267
|
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
@@ -3640,10 +4313,11 @@ function createMppSessionHandler(opts) {
|
|
|
3640
4313
|
}
|
|
3641
4314
|
if (closeAmount > 0n && closeSig) {
|
|
3642
4315
|
const closePayer = record?.payer ?? null;
|
|
4316
|
+
const vouchersClosed = voucherCount;
|
|
3643
4317
|
const closeTxHash = await close({
|
|
3644
4318
|
channel: opts.channelFactory,
|
|
3645
4319
|
amount: closeAmount,
|
|
3646
|
-
signature:
|
|
4320
|
+
signature: hexToBytes(closeSig),
|
|
3647
4321
|
feePayer: channelAuthorizer(payeeKeypair),
|
|
3648
4322
|
network: networkId
|
|
3649
4323
|
});
|
|
@@ -3656,46 +4330,28 @@ function createMppSessionHandler(opts) {
|
|
|
3656
4330
|
opts.onCallbackError?.(err, "onSettled");
|
|
3657
4331
|
});
|
|
3658
4332
|
}
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
4333
|
+
res.json({ closeTxHash });
|
|
4334
|
+
const vaultConfig = resolveVaultSettlementConfig(opts.vaultSettlement, logger);
|
|
4335
|
+
if (vaultConfig) {
|
|
4336
|
+
await recordSessionSettlement(
|
|
4337
|
+
{
|
|
4338
|
+
config: vaultConfig,
|
|
4339
|
+
network: opts.network,
|
|
4340
|
+
payeeSecretKey: opts.payeeSecretKey,
|
|
4341
|
+
channelId: opts.channelFactory,
|
|
4342
|
+
payer: closePayer,
|
|
4343
|
+
amount: closeAmount,
|
|
4344
|
+
voucherCount: vouchersClosed
|
|
4345
|
+
},
|
|
4346
|
+
{ logger, onCallbackError: opts.onCallbackError }
|
|
3664
4347
|
);
|
|
3665
|
-
if (!settlementAddresses) {
|
|
3666
|
-
logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
3667
|
-
} else {
|
|
3668
|
-
try {
|
|
3669
|
-
const { Networks: Networks2 } = await import('@stellar/stellar-sdk');
|
|
3670
|
-
const { Server } = await import('@stellar/stellar-sdk/rpc');
|
|
3671
|
-
const networkPassphrase = opts.network === "mainnet" ? Networks2.PUBLIC : Networks2.TESTNET;
|
|
3672
|
-
const rpcUrl = opts.network === "mainnet" ? "https://mainnet.sorobanrpc.com" : "https://soroban-testnet.stellar.org";
|
|
3673
|
-
const server = new Server(rpcUrl);
|
|
3674
|
-
const payeePublicKey = payeeKeypair.publicKey();
|
|
3675
|
-
const sourceAccount = await server.getAccount(payeePublicKey);
|
|
3676
|
-
const tx = buildSessionSettlementTransaction(
|
|
3677
|
-
sourceAccount,
|
|
3678
|
-
vaultContract,
|
|
3679
|
-
opts.channelFactory,
|
|
3680
|
-
settlementAddresses,
|
|
3681
|
-
closeAmount,
|
|
3682
|
-
voucherCount,
|
|
3683
|
-
networkPassphrase
|
|
3684
|
-
);
|
|
3685
|
-
const preparedTx = await server.prepareTransaction(tx);
|
|
3686
|
-
preparedTx.sign(payeeKeypair);
|
|
3687
|
-
await server.sendTransaction(preparedTx);
|
|
3688
|
-
} catch (recordErr) {
|
|
3689
|
-
logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
|
|
3690
|
-
}
|
|
3691
|
-
}
|
|
3692
4348
|
}
|
|
3693
|
-
res.json({ closeTxHash });
|
|
3694
4349
|
} else {
|
|
3695
4350
|
res.json({ closeTxHash: null, message: "no vouchers received" });
|
|
3696
4351
|
}
|
|
3697
4352
|
sessionOpened = false;
|
|
3698
4353
|
voucherCount = 0;
|
|
4354
|
+
orphanFlagged = false;
|
|
3699
4355
|
record = null;
|
|
3700
4356
|
await innerStore.delete(voucherRecordKey);
|
|
3701
4357
|
closeListenerArmed = false;
|
|
@@ -3732,6 +4388,7 @@ function createMppSessionHandler(opts) {
|
|
|
3732
4388
|
var MPP_MODES = ["mpp-session", "mpp-charge"];
|
|
3733
4389
|
function routedock(opts) {
|
|
3734
4390
|
const startTime = Date.now();
|
|
4391
|
+
assertProviderConfig(opts, "routedock");
|
|
3735
4392
|
const signedManifest = signManifest(opts.manifest, opts.payeeSecretKey);
|
|
3736
4393
|
const handlerMap = /* @__PURE__ */ new Map();
|
|
3737
4394
|
if (opts.modes.includes("x402")) {
|
|
@@ -3795,7 +4452,8 @@ function routedock(opts) {
|
|
|
3795
4452
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3796
4453
|
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
3797
4454
|
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
3798
|
-
...opts.logger ? { logger: opts.logger } : {}
|
|
4455
|
+
...opts.logger ? { logger: opts.logger } : {},
|
|
4456
|
+
...opts.vaultSettlement ? { vaultSettlement: opts.vaultSettlement } : {}
|
|
3799
4457
|
})
|
|
3800
4458
|
);
|
|
3801
4459
|
}
|
|
@@ -3885,6 +4543,8 @@ var MPP_NETWORK3 = {
|
|
|
3885
4543
|
testnet: "stellar:testnet",
|
|
3886
4544
|
mainnet: "stellar:pubnet"
|
|
3887
4545
|
};
|
|
4546
|
+
var DEFAULT_MAX_RECONCILE_ATTEMPTS = 5;
|
|
4547
|
+
var ALREADY_CLOSED_PATTERN = /channel has been closed|channel is closed|already closed|finalized/i;
|
|
3888
4548
|
function decimalToStroops(amountStr) {
|
|
3889
4549
|
return usdcToStroops(amountStr);
|
|
3890
4550
|
}
|
|
@@ -3894,12 +4554,15 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
3894
4554
|
recoveredCount: 0,
|
|
3895
4555
|
skippedCount: 0,
|
|
3896
4556
|
failedCount: 0,
|
|
4557
|
+
alreadyClosedCount: 0,
|
|
4558
|
+
terminalCount: 0,
|
|
3897
4559
|
errors: []
|
|
3898
4560
|
};
|
|
4561
|
+
const maxAttempts = opts.maxReconcileAttempts ?? DEFAULT_MAX_RECONCILE_ATTEMPTS;
|
|
3899
4562
|
const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
|
|
3900
4563
|
const networkId = MPP_NETWORK3[opts.network];
|
|
3901
4564
|
try {
|
|
3902
|
-
const { data: closingSessions, error } = await opts.supabase.from("sessions").select("channel_id, channel_contract, cumulative_amount, last_signature, settlement_tx_hash").eq("status", "closing").eq("network", opts.network).eq("payee", payeeKeypair.publicKey()).is("settlement_tx_hash", null).not("last_signature", "is", null).order("updated_at", { ascending: true }).limit(100);
|
|
4565
|
+
const { data: closingSessions, error } = await opts.supabase.from("sessions").select("channel_id, channel_contract, cumulative_amount, last_signature, settlement_tx_hash, reconcile_attempts").eq("status", "closing").eq("network", opts.network).eq("payee", payeeKeypair.publicKey()).is("settlement_tx_hash", null).not("last_signature", "is", null).order("updated_at", { ascending: true }).limit(100);
|
|
3903
4566
|
if (error) {
|
|
3904
4567
|
throw new Error(`Failed to query sessions: ${error.message}`);
|
|
3905
4568
|
}
|
|
@@ -3921,7 +4584,7 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
3921
4584
|
continue;
|
|
3922
4585
|
}
|
|
3923
4586
|
const closeAmount = decimalToStroops(cumulative_amount);
|
|
3924
|
-
const closeSig =
|
|
4587
|
+
const closeSig = hexToBytes(last_signature);
|
|
3925
4588
|
const closeFn = opts.channelClose ?? (await import('@stellar/mpp/channel/server')).close;
|
|
3926
4589
|
const closeTxHash = await closeFn({
|
|
3927
4590
|
channel: channelId,
|
|
@@ -3944,11 +4607,36 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
3944
4607
|
await opts.onRecovered(channelId, closeTxHash, totalPaid);
|
|
3945
4608
|
}
|
|
3946
4609
|
} catch (err) {
|
|
4610
|
+
const reason = formatMppError(err);
|
|
4611
|
+
if (ALREADY_CLOSED_PATTERN.test(reason)) {
|
|
4612
|
+
const { error: closeError } = await opts.supabase.from("sessions").update({
|
|
4613
|
+
status: "closed",
|
|
4614
|
+
last_reconcile_error: `channel already closed on-chain: ${reason}`,
|
|
4615
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4616
|
+
}).eq("channel_id", session.channel_id);
|
|
4617
|
+
if (!closeError) {
|
|
4618
|
+
stats.alreadyClosedCount++;
|
|
4619
|
+
continue;
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
3947
4622
|
stats.failedCount++;
|
|
3948
|
-
stats.errors.push({
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
})
|
|
4623
|
+
stats.errors.push({ channelId: session.channel_id, reason });
|
|
4624
|
+
const attempts = (Number(session.reconcile_attempts) || 0) + 1;
|
|
4625
|
+
const terminal = attempts >= maxAttempts;
|
|
4626
|
+
const { error: bookkeepingError } = await opts.supabase.from("sessions").update({
|
|
4627
|
+
reconcile_attempts: attempts,
|
|
4628
|
+
last_reconcile_error: reason,
|
|
4629
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4630
|
+
...terminal ? { status: "failed" } : {}
|
|
4631
|
+
}).eq("channel_id", session.channel_id);
|
|
4632
|
+
if (bookkeepingError) {
|
|
4633
|
+
stats.errors.push({
|
|
4634
|
+
channelId: session.channel_id,
|
|
4635
|
+
reason: `Failed to record reconcile failure: ${bookkeepingError.message}`
|
|
4636
|
+
});
|
|
4637
|
+
} else if (terminal) {
|
|
4638
|
+
stats.terminalCount++;
|
|
4639
|
+
}
|
|
3952
4640
|
}
|
|
3953
4641
|
}
|
|
3954
4642
|
return stats;
|
|
@@ -3969,7 +4657,7 @@ async function runStartupReconciliation(opts, logger) {
|
|
|
3969
4657
|
}
|
|
3970
4658
|
log(
|
|
3971
4659
|
"info",
|
|
3972
|
-
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
|
|
4660
|
+
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}, Already closed: ${stats.alreadyClosedCount}, Moved to failed: ${stats.terminalCount}`
|
|
3973
4661
|
);
|
|
3974
4662
|
if (stats.errors.length > 0) {
|
|
3975
4663
|
log("warn", "[SessionReconciler] Reconciliation errors:");
|
|
@@ -3986,7 +4674,7 @@ function tryDecodeBase64(value) {
|
|
|
3986
4674
|
return value;
|
|
3987
4675
|
}
|
|
3988
4676
|
try {
|
|
3989
|
-
const decoded =
|
|
4677
|
+
const decoded = base64ToUtf8(value);
|
|
3990
4678
|
if (decoded.length > 0 && decoded !== value) return decoded;
|
|
3991
4679
|
} catch {
|
|
3992
4680
|
}
|
|
@@ -4123,4 +4811,4 @@ var ProviderRegistry = class {
|
|
|
4123
4811
|
}
|
|
4124
4812
|
};
|
|
4125
4813
|
|
|
4126
|
-
export { DEFAULT_MAX_SESSION_DURATION_MS, DEFAULT_RETRY_POLICY, FileSpendStore, InMemorySpendStore, MppChargeClient,
|
|
4814
|
+
export { DEFAULT_MAX_SESSION_DURATION_MS, DEFAULT_RETRY_POLICY, FileSpendStore, InMemorySpendStore, MppChargeClient, OnChainRegistry, ProviderRegistry, RouteDockChannelStateError, RouteDockClient, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockManifestSunsetError, RouteDockManifestTimeoutError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPaymentRejectedError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SupabaseSeenTxStore, SupabaseSessionStore, X402Client, assertEndpointActive, assertManifestValid, assertNulthVaultManifest, backoffDelayMs, configureManifestCache, consoleLogger, createConsoleLogger, createNulthSigner, decimalToStroops, fetchLedgerSequence, fetchManifest, getEligibleAssets, httpStatusToError, invalidateManifest, isAssetEligible, manifestDigest, noopLogger, normalizeManifestAssets, parseRetryAfterMs, paymentRejectedError, prepareNulthSigner, rankProvidersByLatency, reconcileAbandonedSessions, registerProvider, resolveAssetContract, resolveLogger, routedock, runStartupReconciliation, selectAsset, selectMode, signManifest, usdcToMicros, usdcToUnits, verifyManifestSignature, withRetry, wrapFetchError, wrapMppError };
|