@routedock/routedock 0.2.0 → 0.3.1
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 +305 -0
- package/dist/MppSessionHandler-1EL8QBAn.d.cts +215 -0
- package/dist/MppSessionHandler-Bq4S-JTE.d.ts +215 -0
- package/dist/{ProviderRegistry-BJ9oJLVz.d.cts → ProviderRegistry-CyZ5eryO.d.cts} +2 -2
- package/dist/{ProviderRegistry-C50nyhaF.d.ts → ProviderRegistry-w4_hkI_1.d.ts} +2 -2
- package/dist/{RouteDockClient-BbkqjhMG.d.ts → RouteDockClient-CXvWXix5.d.cts} +25 -9
- package/dist/{RouteDockClient-B1roCa8h.d.cts → RouteDockClient-CjM2VyOd.d.ts} +25 -9
- package/dist/{SessionReconciler-Dscw43F3.d.ts → SessionReconciler-Bw-PoBVl.d.ts} +14 -5
- package/dist/{SessionReconciler-bi0z2qtN.d.cts → SessionReconciler-Cz2nuOlJ.d.cts} +14 -5
- package/dist/{SessionStore-DvkZv8-E.d.ts → SessionStore-B4-Hi4H8.d.cts} +34 -2
- package/dist/{SessionStore-BpK8vv0L.d.cts → SessionStore-BaYiegP0.d.ts} +34 -2
- package/dist/client.cjs +432 -125
- package/dist/client.d.cts +9 -8
- package/dist/client.d.ts +9 -8
- package/dist/client.js +424 -127
- package/dist/index.cjs +655 -228
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +646 -229
- package/dist/{types-DmhmfLzP.d.cts → logger-D8_Q2ytp.d.cts} +58 -1
- package/dist/{types-DmhmfLzP.d.ts → logger-D8_Q2ytp.d.ts} +58 -1
- package/dist/provider/fastify.cjs +306 -89
- package/dist/provider/fastify.d.cts +9 -4
- package/dist/provider/fastify.d.ts +9 -4
- package/dist/provider/fastify.js +305 -88
- package/dist/provider/hono.cjs +286 -82
- package/dist/provider/hono.d.cts +9 -7
- package/dist/provider/hono.d.ts +9 -7
- package/dist/provider/hono.js +282 -78
- package/dist/provider.cjs +326 -98
- package/dist/provider.d.cts +14 -10
- package/dist/provider.d.ts +14 -10
- package/dist/provider.js +318 -98
- package/dist/react.cjs +410 -125
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +411 -126
- package/dist/{routedockMiddleware-BlDsHuPl.d.ts → routedockMiddleware-57gUaEMf.d.ts} +9 -4
- package/dist/{routedockMiddleware-D97A5ptQ.d.cts → routedockMiddleware-DlyLrxp7.d.cts} +9 -4
- package/dist/schema.cjs +40 -0
- package/dist/schema.js +40 -0
- package/dist/{sign-Dpj2Mp7k.d.ts → sign-AKH67sft.d.cts} +1 -1
- package/dist/{sign-CUf3rhTx.d.cts → sign-BEq-OsSq.d.ts} +1 -1
- package/dist/testing.cjs +6 -4
- package/dist/testing.d.cts +32 -13
- package/dist/testing.d.ts +32 -13
- package/dist/testing.js +6 -4
- package/package.json +2 -2
- package/dist/MppSessionHandler-B4TyD7Xg.d.cts +0 -121
- package/dist/MppSessionHandler-CLB5SzgN.d.ts +0 -121
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { usdcToStroops } from '@routedock/nulth-sdk';
|
|
1
|
+
import { usdcToStroops, stroopsToUsdc } from '@routedock/nulth-sdk';
|
|
2
2
|
export { USDC_DECIMALS, usdcToStroops } from '@routedock/nulth-sdk';
|
|
3
3
|
import { Keypair, rpc, Horizon, Networks, xdr, Address, scValToNative, hash } from '@stellar/stellar-sdk';
|
|
4
4
|
import { Validator } from '@cfworker/json-schema';
|
|
@@ -159,10 +159,35 @@ function wrapFetchError(err, context) {
|
|
|
159
159
|
if (err instanceof RouteDockError) return err;
|
|
160
160
|
return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
|
|
161
161
|
}
|
|
162
|
+
function wrapMppError(err, context) {
|
|
163
|
+
if (err instanceof RouteDockError) return err;
|
|
164
|
+
if (err instanceof TypeError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
|
|
165
|
+
return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
|
|
166
|
+
}
|
|
167
|
+
return new RouteDockSignatureError(`${context}: ${String(err)}`, { cause: err });
|
|
168
|
+
}
|
|
162
169
|
|
|
163
170
|
// src/types.ts
|
|
164
171
|
var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
|
|
165
172
|
|
|
173
|
+
// src/internal/logger.ts
|
|
174
|
+
function createConsoleLogger() {
|
|
175
|
+
return (level, message, fields) => {
|
|
176
|
+
const values = fields ? Object.values(fields) : [];
|
|
177
|
+
if (values.length > 0) {
|
|
178
|
+
console[level](message, ...values);
|
|
179
|
+
} else {
|
|
180
|
+
console[level](message);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
var consoleLogger = createConsoleLogger();
|
|
185
|
+
var noopLogger = () => {
|
|
186
|
+
};
|
|
187
|
+
function resolveLogger(logger, fallback = consoleLogger) {
|
|
188
|
+
return logger ?? fallback;
|
|
189
|
+
}
|
|
190
|
+
|
|
166
191
|
// src/internal/retry.ts
|
|
167
192
|
var DEFAULT_RETRY_POLICY = {
|
|
168
193
|
maxAttempts: 4,
|
|
@@ -177,10 +202,11 @@ function sleep(ms) {
|
|
|
177
202
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
178
203
|
}
|
|
179
204
|
async function withRetry(fn, policy = {}) {
|
|
180
|
-
const { maxAttempts, baseDelayMs, maxDelayMs, onRetry } = {
|
|
205
|
+
const { maxAttempts, baseDelayMs, maxDelayMs, onRetry, logger } = {
|
|
181
206
|
...DEFAULT_RETRY_POLICY,
|
|
182
207
|
...policy
|
|
183
208
|
};
|
|
209
|
+
const log = resolveLogger(logger);
|
|
184
210
|
let lastError;
|
|
185
211
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
186
212
|
try {
|
|
@@ -198,7 +224,7 @@ async function withRetry(fn, policy = {}) {
|
|
|
198
224
|
try {
|
|
199
225
|
await onRetry(attempt, err, delay);
|
|
200
226
|
} catch (cbErr) {
|
|
201
|
-
|
|
227
|
+
log("error", "[withRetry] onRetry callback threw:", { error: cbErr });
|
|
202
228
|
}
|
|
203
229
|
}
|
|
204
230
|
await sleep(delay);
|
|
@@ -212,6 +238,84 @@ var USDC_ISSUERS = {
|
|
|
212
238
|
};
|
|
213
239
|
var usdcToUnits = usdcToStroops;
|
|
214
240
|
|
|
241
|
+
// src/internal/assetUtils.ts
|
|
242
|
+
function normalizeManifestAssets(manifest) {
|
|
243
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
244
|
+
return manifest.assets;
|
|
245
|
+
}
|
|
246
|
+
if (manifest.asset && manifest.asset_contract) {
|
|
247
|
+
return [
|
|
248
|
+
{
|
|
249
|
+
asset: manifest.asset,
|
|
250
|
+
asset_contract: manifest.asset_contract
|
|
251
|
+
}
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
throw new RouteDockManifestError(
|
|
255
|
+
"Manifest must define either assets[] or root asset/asset_contract fields"
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
function getEligibleAssets(manifest, mode, endpoint) {
|
|
259
|
+
const assets = normalizeManifestAssets(manifest);
|
|
260
|
+
return assets.filter((asset) => {
|
|
261
|
+
if (asset.modes && !asset.modes.includes(mode)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
|
|
265
|
+
const trimmedEndpoint = endpoint.replace(/^\//, "");
|
|
266
|
+
if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
if (manifest.endpoints) {
|
|
270
|
+
let pathname = endpoint;
|
|
271
|
+
if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
|
|
272
|
+
try {
|
|
273
|
+
pathname = new URL(endpoint).pathname;
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const trimmedPathname = pathname.replace(/^\//, "");
|
|
278
|
+
const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
|
|
279
|
+
const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
|
|
280
|
+
const keyMatches = key === endpoint || key === trimmedEndpoint;
|
|
281
|
+
if (pathMatches || keyMatches) {
|
|
282
|
+
return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
});
|
|
286
|
+
if (!matchesEndpoint) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
|
|
297
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
298
|
+
return selectAsset(manifest, mode, endpoint).asset_contract;
|
|
299
|
+
}
|
|
300
|
+
return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
|
|
301
|
+
}
|
|
302
|
+
function selectAsset(manifest, mode, endpoint) {
|
|
303
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
304
|
+
if (eligible.length === 0) {
|
|
305
|
+
const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
|
|
306
|
+
throw new RouteDockManifestError(
|
|
307
|
+
`No eligible assets found for mode '${mode}'${endpointMsg}`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return eligible[0];
|
|
311
|
+
}
|
|
312
|
+
function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
|
|
313
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
314
|
+
return eligible.some(
|
|
315
|
+
(asset) => asset.asset === assetIdentifier || asset.asset_contract === assetIdentifier
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
215
319
|
// src/schemas/routedock.schema.json
|
|
216
320
|
var routedock_schema_default = {
|
|
217
321
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
@@ -291,6 +395,14 @@ var routedock_schema_default = {
|
|
|
291
395
|
type: "string",
|
|
292
396
|
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
293
397
|
},
|
|
398
|
+
assets: {
|
|
399
|
+
type: "array",
|
|
400
|
+
minItems: 1,
|
|
401
|
+
items: {
|
|
402
|
+
$ref: "#/definitions/AssetConfig"
|
|
403
|
+
},
|
|
404
|
+
description: "Payment assets accepted by this provider. Each asset can be scoped to specific modes and endpoints. When present, assets[0] must match the root-level asset and asset_contract."
|
|
405
|
+
},
|
|
294
406
|
payee: {
|
|
295
407
|
type: "string",
|
|
296
408
|
description: "Stellar address (G...) that receives payments"
|
|
@@ -423,6 +535,38 @@ var routedock_schema_default = {
|
|
|
423
535
|
enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
|
|
424
536
|
description: "A supported RouteDock payment mode"
|
|
425
537
|
},
|
|
538
|
+
AssetConfig: {
|
|
539
|
+
type: "object",
|
|
540
|
+
description: "Payment asset configuration with optional mode and endpoint scoping",
|
|
541
|
+
required: ["asset", "asset_contract"],
|
|
542
|
+
additionalProperties: false,
|
|
543
|
+
properties: {
|
|
544
|
+
asset: {
|
|
545
|
+
type: "string",
|
|
546
|
+
description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
|
|
547
|
+
},
|
|
548
|
+
asset_contract: {
|
|
549
|
+
type: "string",
|
|
550
|
+
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
551
|
+
},
|
|
552
|
+
modes: {
|
|
553
|
+
type: "array",
|
|
554
|
+
items: {
|
|
555
|
+
$ref: "#/definitions/PaymentMode"
|
|
556
|
+
},
|
|
557
|
+
uniqueItems: true,
|
|
558
|
+
description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
|
|
559
|
+
},
|
|
560
|
+
endpoints: {
|
|
561
|
+
type: "array",
|
|
562
|
+
items: {
|
|
563
|
+
type: "string"
|
|
564
|
+
},
|
|
565
|
+
uniqueItems: true,
|
|
566
|
+
description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
},
|
|
426
570
|
PricingConfig: {
|
|
427
571
|
type: "object",
|
|
428
572
|
description: "Per-request pricing for x402 or mpp-charge modes",
|
|
@@ -568,7 +712,7 @@ var routedock_schema_default = {
|
|
|
568
712
|
|
|
569
713
|
// package.json
|
|
570
714
|
var package_default = {
|
|
571
|
-
version: "0.
|
|
715
|
+
version: "0.3.1"};
|
|
572
716
|
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
573
717
|
function canonicalize(value) {
|
|
574
718
|
if (Array.isArray(value)) {
|
|
@@ -682,6 +826,7 @@ function assertEndpointActive(manifest, url, logger) {
|
|
|
682
826
|
const [key, descriptor] = deprecated;
|
|
683
827
|
const sunsetSuffix = descriptor.sunset_at ? `; sunset_at ${descriptor.sunset_at}` : "";
|
|
684
828
|
logger(
|
|
829
|
+
"warn",
|
|
685
830
|
`[RouteDock] WARNING: ${manifest.name} \u2192 ${pathname}; endpoint '${key}' is deprecated${sunsetSuffix}`
|
|
686
831
|
);
|
|
687
832
|
}
|
|
@@ -703,6 +848,27 @@ function assertManifestValid(manifest, baseUrl) {
|
|
|
703
848
|
}
|
|
704
849
|
}
|
|
705
850
|
}
|
|
851
|
+
if (manifest.assets !== void 0) {
|
|
852
|
+
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
853
|
+
throw new RouteDockManifestError(
|
|
854
|
+
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
for (let i = 0; i < manifest.assets.length; i++) {
|
|
858
|
+
const a = manifest.assets[i];
|
|
859
|
+
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
860
|
+
throw new RouteDockManifestError(
|
|
861
|
+
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const first = manifest.assets[0];
|
|
866
|
+
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
867
|
+
throw new RouteDockManifestError(
|
|
868
|
+
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
706
872
|
}
|
|
707
873
|
var CACHE_TTL_MS = 6e4;
|
|
708
874
|
var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
|
|
@@ -882,16 +1048,16 @@ function logSelection(manifest, selection, log, deprecated) {
|
|
|
882
1048
|
const reason = selection.reason ? ` (${selection.reason})` : "";
|
|
883
1049
|
if (deprecated) {
|
|
884
1050
|
log(
|
|
1051
|
+
"warn",
|
|
885
1052
|
`[RouteDock] WARNING: ${manifest.name} \u2192 ${selection.mode}${reason}; selected deprecated mode because no active supported mode is available`
|
|
886
1053
|
);
|
|
887
1054
|
return;
|
|
888
1055
|
}
|
|
889
|
-
log(`[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
|
|
1056
|
+
log("info", `[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
|
|
890
1057
|
}
|
|
891
1058
|
function selectMode(manifest, options = {}) {
|
|
892
1059
|
const modes = manifest.modes;
|
|
893
|
-
const log = options.logger ??
|
|
894
|
-
});
|
|
1060
|
+
const log = options.logger ?? noopLogger;
|
|
895
1061
|
const deprecatedSet = new Set(manifest.deprecated_modes ?? []);
|
|
896
1062
|
if (options.forceMode) {
|
|
897
1063
|
if (!modes.includes(options.forceMode)) {
|
|
@@ -901,10 +1067,11 @@ function selectMode(manifest, options = {}) {
|
|
|
901
1067
|
}
|
|
902
1068
|
if (deprecatedSet.has(options.forceMode)) {
|
|
903
1069
|
log(
|
|
1070
|
+
"warn",
|
|
904
1071
|
`[RouteDock] WARNING: ${manifest.name} \u2192 ${options.forceMode} (forced); selected mode is deprecated`
|
|
905
1072
|
);
|
|
906
1073
|
} else {
|
|
907
|
-
log(`[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
|
|
1074
|
+
log("info", `[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
|
|
908
1075
|
}
|
|
909
1076
|
return options.forceMode;
|
|
910
1077
|
}
|
|
@@ -940,6 +1107,69 @@ function rankProvidersByLatency(manifests, targetRegion) {
|
|
|
940
1107
|
return 0;
|
|
941
1108
|
});
|
|
942
1109
|
}
|
|
1110
|
+
|
|
1111
|
+
// src/internal/payee.ts
|
|
1112
|
+
function resolvePayee(manifest, mode) {
|
|
1113
|
+
const override = manifest.pricing[mode]?.payee;
|
|
1114
|
+
return override ?? manifest.payee;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/client/challenge.ts
|
|
1118
|
+
function isRecord(value) {
|
|
1119
|
+
return typeof value === "object" && value !== null;
|
|
1120
|
+
}
|
|
1121
|
+
function isPositiveIntegerString(value) {
|
|
1122
|
+
return typeof value === "string" && /^[1-9]\d*$/.test(value);
|
|
1123
|
+
}
|
|
1124
|
+
function checkAmount(amount, maxAmount) {
|
|
1125
|
+
if (!isPositiveIntegerString(amount)) {
|
|
1126
|
+
return new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1127
|
+
}
|
|
1128
|
+
let max;
|
|
1129
|
+
try {
|
|
1130
|
+
max = usdcToStroops(maxAmount);
|
|
1131
|
+
} catch {
|
|
1132
|
+
return new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1133
|
+
}
|
|
1134
|
+
if (BigInt(amount) > max) {
|
|
1135
|
+
return new RouteDockPolicyRejectError("challenge_amount_exceeds_manifest");
|
|
1136
|
+
}
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
function checkX402Accept(accept, manifest, network, maxAmount) {
|
|
1140
|
+
if (accept.network !== network) {
|
|
1141
|
+
return new RouteDockPolicyRejectError("challenge_network_mismatch");
|
|
1142
|
+
}
|
|
1143
|
+
if (accept.payTo !== resolvePayee(manifest, "x402")) {
|
|
1144
|
+
return new RouteDockPolicyRejectError("challenge_payee_mismatch");
|
|
1145
|
+
}
|
|
1146
|
+
if (accept.asset !== manifest.asset_contract) {
|
|
1147
|
+
return new RouteDockPolicyRejectError("challenge_asset_mismatch");
|
|
1148
|
+
}
|
|
1149
|
+
return checkAmount(accept.amount, maxAmount);
|
|
1150
|
+
}
|
|
1151
|
+
function filterX402Accepts(accepts, manifest, network, maxAmount) {
|
|
1152
|
+
return accepts.filter((accept) => checkX402Accept(accept, manifest, network, maxAmount) === null);
|
|
1153
|
+
}
|
|
1154
|
+
function checkChargeChallenge(request, manifest, network, maxAmount) {
|
|
1155
|
+
const methodDetails = request["methodDetails"];
|
|
1156
|
+
const rawNetwork = isRecord(methodDetails) ? methodDetails["network"] : void 0;
|
|
1157
|
+
const challengeNetwork = rawNetwork === void 0 ? "stellar:testnet" : rawNetwork;
|
|
1158
|
+
if (challengeNetwork !== network) {
|
|
1159
|
+
return new RouteDockPolicyRejectError("challenge_network_mismatch");
|
|
1160
|
+
}
|
|
1161
|
+
const recipient = request["recipient"];
|
|
1162
|
+
if (typeof recipient !== "string" || recipient !== resolvePayee(manifest, "mpp-charge")) {
|
|
1163
|
+
return new RouteDockPolicyRejectError("challenge_payee_mismatch");
|
|
1164
|
+
}
|
|
1165
|
+
const currency = request["currency"];
|
|
1166
|
+
if (typeof currency !== "string" || currency !== manifest.asset_contract) {
|
|
1167
|
+
return new RouteDockPolicyRejectError("challenge_asset_mismatch");
|
|
1168
|
+
}
|
|
1169
|
+
return checkAmount(request["amount"], maxAmount);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/client/x402Client.ts
|
|
943
1173
|
var CAIP2 = {
|
|
944
1174
|
testnet: "stellar:testnet",
|
|
945
1175
|
mainnet: "stellar:pubnet"
|
|
@@ -971,83 +1201,106 @@ var X402Client = class _X402Client {
|
|
|
971
1201
|
if (!pricing.facilitator) {
|
|
972
1202
|
throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
|
|
973
1203
|
}
|
|
974
|
-
|
|
975
|
-
|
|
1204
|
+
const caip2 = CAIP2[this.network];
|
|
1205
|
+
const probe = await withRetry(async () => {
|
|
1206
|
+
let init2;
|
|
976
1207
|
try {
|
|
977
|
-
|
|
1208
|
+
init2 = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
|
|
978
1209
|
} catch (err) {
|
|
979
1210
|
throw wrapFetchError(err, "x402 initial request");
|
|
980
1211
|
}
|
|
981
|
-
if (
|
|
982
|
-
if (!
|
|
1212
|
+
if (init2.status !== 402) {
|
|
1213
|
+
if (!init2.ok) {
|
|
983
1214
|
throw httpStatusToError(
|
|
984
|
-
`x402 initial request failed: HTTP ${
|
|
985
|
-
|
|
986
|
-
|
|
1215
|
+
`x402 initial request failed: HTTP ${init2.status}`,
|
|
1216
|
+
init2.status,
|
|
1217
|
+
init2
|
|
987
1218
|
);
|
|
988
1219
|
}
|
|
989
1220
|
let data2;
|
|
990
1221
|
try {
|
|
991
|
-
data2 = await
|
|
1222
|
+
data2 = await init2.json();
|
|
992
1223
|
} catch {
|
|
993
|
-
throw new RouteDockManifestError(
|
|
1224
|
+
throw new RouteDockManifestError(
|
|
1225
|
+
`Failed to parse JSON from non-402 response (HTTP ${init2.status})`
|
|
1226
|
+
);
|
|
994
1227
|
}
|
|
995
|
-
return {
|
|
996
|
-
}
|
|
997
|
-
const reqHeader = init.headers.get("X-Payment-Requirements");
|
|
998
|
-
if (!reqHeader) {
|
|
999
|
-
throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
|
|
1000
|
-
}
|
|
1001
|
-
let paymentRequired;
|
|
1002
|
-
try {
|
|
1003
|
-
paymentRequired = decodePaymentRequiredHeader(reqHeader);
|
|
1004
|
-
} catch (err) {
|
|
1005
|
-
throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
|
|
1006
|
-
cause: err
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
let paymentPayload;
|
|
1010
|
-
try {
|
|
1011
|
-
paymentPayload = await this.httpClient.createPaymentPayload(paymentRequired);
|
|
1012
|
-
} catch (err) {
|
|
1013
|
-
throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
|
|
1014
|
-
cause: err
|
|
1015
|
-
});
|
|
1228
|
+
return { kind: "free", data: data2 };
|
|
1016
1229
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1230
|
+
return { kind: "payment-required", init: init2 };
|
|
1231
|
+
}, this.retryPolicy);
|
|
1232
|
+
if (probe.kind === "free") {
|
|
1233
|
+
return { data: probe.data, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
|
|
1234
|
+
}
|
|
1235
|
+
const init = probe.init;
|
|
1236
|
+
const reqHeader = init.headers.get("X-Payment-Requirements");
|
|
1237
|
+
if (!reqHeader) {
|
|
1238
|
+
throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
|
|
1239
|
+
}
|
|
1240
|
+
let paymentRequired;
|
|
1241
|
+
try {
|
|
1242
|
+
paymentRequired = decodePaymentRequiredHeader(reqHeader);
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
|
|
1245
|
+
cause: err
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
const firstAccept = paymentRequired.accepts[0];
|
|
1249
|
+
if (!firstAccept) {
|
|
1250
|
+
throw new RouteDockManifestError("402 response carries no payment accepts");
|
|
1251
|
+
}
|
|
1252
|
+
const allowed = filterX402Accepts(paymentRequired.accepts, manifest, caip2, pricing.amount);
|
|
1253
|
+
if (allowed.length === 0) {
|
|
1254
|
+
throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
|
|
1255
|
+
}
|
|
1256
|
+
let paymentPayload;
|
|
1257
|
+
try {
|
|
1258
|
+
paymentPayload = await this.httpClient.createPaymentPayload({
|
|
1259
|
+
...paymentRequired,
|
|
1260
|
+
accepts: allowed
|
|
1261
|
+
});
|
|
1262
|
+
} catch (err) {
|
|
1263
|
+
throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
|
|
1264
|
+
cause: err
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
1268
|
+
const signedAmount = stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
|
|
1269
|
+
const settled = await withRetry(async () => {
|
|
1270
|
+
let response;
|
|
1019
1271
|
try {
|
|
1020
|
-
|
|
1272
|
+
response = await fetch(url, { headers: paymentHeaders });
|
|
1021
1273
|
} catch (err) {
|
|
1022
1274
|
throw wrapFetchError(err, "x402 settlement request");
|
|
1023
1275
|
}
|
|
1024
|
-
if (!
|
|
1025
|
-
if (
|
|
1276
|
+
if (!response.ok) {
|
|
1277
|
+
if (response.status >= 500 || response.status === 429 || response.status === 503) {
|
|
1026
1278
|
throw httpStatusToError(
|
|
1027
|
-
`x402 payment failed: HTTP ${
|
|
1028
|
-
|
|
1029
|
-
|
|
1279
|
+
`x402 payment failed: HTTP ${response.status}`,
|
|
1280
|
+
response.status,
|
|
1281
|
+
response
|
|
1030
1282
|
);
|
|
1031
1283
|
}
|
|
1032
|
-
throw new RouteDockManifestError(`x402 payment failed: HTTP ${
|
|
1033
|
-
}
|
|
1034
|
-
let txHash = null;
|
|
1035
|
-
const responseHeader = settled.headers.get("X-Payment-Response");
|
|
1036
|
-
if (responseHeader) {
|
|
1037
|
-
try {
|
|
1038
|
-
const settleResponse = decodePaymentResponseHeader(responseHeader);
|
|
1039
|
-
txHash = settleResponse.transaction ?? null;
|
|
1040
|
-
} catch {
|
|
1041
|
-
}
|
|
1284
|
+
throw new RouteDockManifestError(`x402 payment failed: HTTP ${response.status}`);
|
|
1042
1285
|
}
|
|
1043
|
-
|
|
1286
|
+
return response;
|
|
1287
|
+
}, this.retryPolicy);
|
|
1288
|
+
let txHash = null;
|
|
1289
|
+
const responseHeader = settled.headers.get("X-Payment-Response");
|
|
1290
|
+
if (responseHeader) {
|
|
1044
1291
|
try {
|
|
1045
|
-
|
|
1292
|
+
const settleResponse = decodePaymentResponseHeader(responseHeader);
|
|
1293
|
+
txHash = settleResponse.transaction ?? null;
|
|
1046
1294
|
} catch {
|
|
1047
|
-
throw new RouteDockManifestError("Failed to parse JSON from settled response");
|
|
1048
1295
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1296
|
+
}
|
|
1297
|
+
let data;
|
|
1298
|
+
try {
|
|
1299
|
+
data = await settled.json();
|
|
1300
|
+
} catch {
|
|
1301
|
+
throw new RouteDockManifestError("Failed to parse JSON from settled response");
|
|
1302
|
+
}
|
|
1303
|
+
return { data, txHash, mode: "x402", amount: signedAmount, timestamp: Date.now() };
|
|
1051
1304
|
}
|
|
1052
1305
|
};
|
|
1053
1306
|
var MppChargeClient = class {
|
|
@@ -1064,27 +1317,42 @@ var MppChargeClient = class {
|
|
|
1064
1317
|
if (!pricing) {
|
|
1065
1318
|
throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
|
|
1066
1319
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1320
|
+
const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
|
|
1321
|
+
let txHash = null;
|
|
1322
|
+
let signedAmount;
|
|
1323
|
+
let credential;
|
|
1324
|
+
const mppx = Mppx.create({
|
|
1325
|
+
polyfill: false,
|
|
1326
|
+
methods: [
|
|
1327
|
+
stellar.charge({
|
|
1328
|
+
keypair: this.keypair,
|
|
1329
|
+
mode: "pull",
|
|
1330
|
+
onProgress(event) {
|
|
1331
|
+
if (event.type === "paid") {
|
|
1332
|
+
txHash = event.hash;
|
|
1079
1333
|
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1334
|
+
}
|
|
1335
|
+
})
|
|
1336
|
+
],
|
|
1337
|
+
onChallenge: async (challenge, { createCredential }) => {
|
|
1338
|
+
const request = challenge.request;
|
|
1339
|
+
const rejection = checkChargeChallenge(request, manifest, caip2, pricing.amount);
|
|
1340
|
+
if (rejection) throw rejection;
|
|
1341
|
+
const amount = request["amount"];
|
|
1342
|
+
if (typeof amount !== "string") {
|
|
1343
|
+
throw new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1344
|
+
}
|
|
1345
|
+
signedAmount = stroopsToUsdc(BigInt(amount));
|
|
1346
|
+
credential ??= await createCredential();
|
|
1347
|
+
return credential;
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
return withRetry(async () => {
|
|
1083
1351
|
let response;
|
|
1084
1352
|
try {
|
|
1085
|
-
response = await mppx.fetch(url);
|
|
1353
|
+
response = credential ? await mppx.rawFetch(url, { headers: { Authorization: credential } }) : await mppx.fetch(url);
|
|
1086
1354
|
} catch (err) {
|
|
1087
|
-
throw
|
|
1355
|
+
throw wrapMppError(err, "MPP charge request");
|
|
1088
1356
|
}
|
|
1089
1357
|
if (!response.ok) {
|
|
1090
1358
|
if (response.status >= 500 || response.status === 429 || response.status === 503) {
|
|
@@ -1105,7 +1373,13 @@ var MppChargeClient = class {
|
|
|
1105
1373
|
{ cause }
|
|
1106
1374
|
);
|
|
1107
1375
|
}
|
|
1108
|
-
return {
|
|
1376
|
+
return {
|
|
1377
|
+
data,
|
|
1378
|
+
txHash,
|
|
1379
|
+
mode: "mpp-charge",
|
|
1380
|
+
amount: signedAmount ?? pricing.amount,
|
|
1381
|
+
timestamp: Date.now()
|
|
1382
|
+
};
|
|
1109
1383
|
}, this.retryPolicy);
|
|
1110
1384
|
}
|
|
1111
1385
|
};
|
|
@@ -1144,16 +1418,18 @@ function resolveStoreNetworkId(network) {
|
|
|
1144
1418
|
return network === STELLAR_PUBNET ? STELLAR_PUBNET : STELLAR_TESTNET;
|
|
1145
1419
|
}
|
|
1146
1420
|
var MppSessionClient = class {
|
|
1147
|
-
constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory) {
|
|
1421
|
+
constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory, logger = consoleLogger) {
|
|
1148
1422
|
this.keypair = keypair;
|
|
1149
1423
|
this.network = network;
|
|
1150
1424
|
this.retryPolicy = retryPolicy;
|
|
1151
1425
|
this.webSocketFactory = webSocketFactory;
|
|
1426
|
+
this.logger = logger;
|
|
1152
1427
|
}
|
|
1153
1428
|
keypair;
|
|
1154
1429
|
network;
|
|
1155
1430
|
retryPolicy;
|
|
1156
1431
|
webSocketFactory;
|
|
1432
|
+
logger;
|
|
1157
1433
|
async openSession(url, manifest, commitmentSecret, options, onSpend) {
|
|
1158
1434
|
const mode = options?.mode ?? "mpp-session";
|
|
1159
1435
|
const pricing = manifest.pricing[mode];
|
|
@@ -1378,7 +1654,7 @@ var MppSessionClient = class {
|
|
|
1378
1654
|
if (abortController.signal.aborted) {
|
|
1379
1655
|
throw err;
|
|
1380
1656
|
}
|
|
1381
|
-
throw
|
|
1657
|
+
throw wrapMppError(err, "Voucher request");
|
|
1382
1658
|
}
|
|
1383
1659
|
if (!resp.ok) {
|
|
1384
1660
|
if (resp.status >= 500 || resp.status === 429 || resp.status === 503) {
|
|
@@ -1671,9 +1947,10 @@ var MppSessionClient = class {
|
|
|
1671
1947
|
emit("session:timeout", { maxDurationMs });
|
|
1672
1948
|
void handle.close().catch((error) => {
|
|
1673
1949
|
emit("session:close-failed", { maxDurationMs, error });
|
|
1674
|
-
|
|
1950
|
+
this.logger(
|
|
1951
|
+
"warn",
|
|
1675
1952
|
`RouteDock: maxDuration auto-close failed after ${maxDurationMs}ms \u2014 the channel may still hold collateral; retry close() or call requestRefund().`,
|
|
1676
|
-
error
|
|
1953
|
+
{ error }
|
|
1677
1954
|
);
|
|
1678
1955
|
});
|
|
1679
1956
|
}, maxDurationMs);
|
|
@@ -1723,7 +2000,7 @@ var MppSessionClient = class {
|
|
|
1723
2000
|
);
|
|
1724
2001
|
onSigned?.();
|
|
1725
2002
|
} catch (err) {
|
|
1726
|
-
throw
|
|
2003
|
+
throw wrapMppError(err, "Voucher credential");
|
|
1727
2004
|
}
|
|
1728
2005
|
const wsUrl = url.replace(/^http/, "ws");
|
|
1729
2006
|
const queue = [];
|
|
@@ -1900,11 +2177,6 @@ function encodeAuthSignature(proof) {
|
|
|
1900
2177
|
const payload = { nulth: "zk-v1", proof };
|
|
1901
2178
|
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
1902
2179
|
}
|
|
1903
|
-
function usdcToStroops2(amount) {
|
|
1904
|
-
const [whole = "0", frac = ""] = amount.split(".");
|
|
1905
|
-
const padded = (frac + "0000000").slice(0, 7);
|
|
1906
|
-
return BigInt(whole) * 10000000n + BigInt(padded);
|
|
1907
|
-
}
|
|
1908
2180
|
var NulthClient = class {
|
|
1909
2181
|
constructor(config) {
|
|
1910
2182
|
this.config = config;
|
|
@@ -1920,7 +2192,10 @@ var NulthClient = class {
|
|
|
1920
2192
|
`NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
|
|
1921
2193
|
);
|
|
1922
2194
|
}
|
|
1923
|
-
|
|
2195
|
+
resolveLogger(config.logger)(
|
|
2196
|
+
"warn",
|
|
2197
|
+
"NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
|
|
2198
|
+
);
|
|
1924
2199
|
}
|
|
1925
2200
|
config;
|
|
1926
2201
|
policy;
|
|
@@ -1972,6 +2247,12 @@ var NulthClient = class {
|
|
|
1972
2247
|
return proof;
|
|
1973
2248
|
}
|
|
1974
2249
|
enforcePolicy(context) {
|
|
2250
|
+
if (context.amountStroops < 0n) {
|
|
2251
|
+
throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
|
|
2252
|
+
}
|
|
2253
|
+
if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
|
|
2254
|
+
throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
|
|
2255
|
+
}
|
|
1975
2256
|
if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
|
|
1976
2257
|
throw new NulthPolicyError("session_expired", "Nulth session expired");
|
|
1977
2258
|
}
|
|
@@ -2006,14 +2287,14 @@ function paymentContextFromManifest(manifest, mode, ledgerSequence) {
|
|
|
2006
2287
|
const pricing = manifest.pricing[mode];
|
|
2007
2288
|
if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
|
|
2008
2289
|
return {
|
|
2009
|
-
payee: manifest.payee,
|
|
2010
|
-
amountStroops:
|
|
2290
|
+
payee: pricing.payee ?? manifest.payee,
|
|
2291
|
+
amountStroops: usdcToStroops(pricing.amount),
|
|
2011
2292
|
assetContract: manifest.asset_contract,
|
|
2012
2293
|
ledgerSequence
|
|
2013
2294
|
};
|
|
2014
2295
|
}
|
|
2015
2296
|
function createPolicyState(input) {
|
|
2016
|
-
const dailyCapStroops =
|
|
2297
|
+
const dailyCapStroops = usdcToStroops(input.dailyCapUsdc);
|
|
2017
2298
|
return {
|
|
2018
2299
|
dailyCapStroops,
|
|
2019
2300
|
allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
|
|
@@ -2115,7 +2396,8 @@ var InMemorySpendStore = class {
|
|
|
2115
2396
|
state = null;
|
|
2116
2397
|
constructor(options = {}) {
|
|
2117
2398
|
if (options.warn !== false) {
|
|
2118
|
-
|
|
2399
|
+
resolveLogger(options.logger)(
|
|
2400
|
+
"warn",
|
|
2119
2401
|
"[RouteDock] Using in-memory SpendStore: the daily spend cap is NOT durable and resets on every process restart. Inject a persistent SpendStore via RouteDockClientConfig.spendStore for production safety."
|
|
2120
2402
|
);
|
|
2121
2403
|
}
|
|
@@ -2205,7 +2487,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2205
2487
|
this.network = config.network;
|
|
2206
2488
|
this.spendCap = config.spendCap ? normalizeSpendCap(config.spendCap) : void 0;
|
|
2207
2489
|
this.retryPolicy = config.retryPolicy;
|
|
2208
|
-
this.spendStore = config.spendStore ?? new InMemorySpendStore({
|
|
2490
|
+
this.spendStore = config.spendStore ?? new InMemorySpendStore({
|
|
2491
|
+
warn: !!config.spendCap,
|
|
2492
|
+
...config.logger && { logger: config.logger }
|
|
2493
|
+
});
|
|
2209
2494
|
this.logger = config.logger;
|
|
2210
2495
|
this.manifestTimeoutMs = config.manifestTimeoutMs;
|
|
2211
2496
|
this.expectedPayee = config.expectedPayee;
|
|
@@ -2217,9 +2502,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2217
2502
|
_secrets.set(this, config.commitmentSecret);
|
|
2218
2503
|
}
|
|
2219
2504
|
const secretKey = this.keypair.secret();
|
|
2220
|
-
this.
|
|
2221
|
-
this.
|
|
2222
|
-
this.
|
|
2505
|
+
const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
|
|
2506
|
+
this.x402 = new X402Client(secretKey, this.network, retryPolicy);
|
|
2507
|
+
this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
|
|
2508
|
+
this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
|
|
2223
2509
|
}
|
|
2224
2510
|
/** Fetch manifest and select mode — shared by pay() and estimateCost(). */
|
|
2225
2511
|
async _resolveManifest(url, options) {
|
|
@@ -2248,12 +2534,13 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2248
2534
|
* call before committing to a payment — for approval gates and manual
|
|
2249
2535
|
* trustline remediation.
|
|
2250
2536
|
*/
|
|
2251
|
-
async preflight(manifest) {
|
|
2537
|
+
async preflight(manifest, mode, endpoint) {
|
|
2252
2538
|
assertManifestValid(manifest);
|
|
2253
|
-
|
|
2539
|
+
const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
|
|
2540
|
+
await this._checkTrustline(manifest, selectedAsset);
|
|
2254
2541
|
return {
|
|
2255
2542
|
hasTrustline: true,
|
|
2256
|
-
asset:
|
|
2543
|
+
asset: selectedAsset,
|
|
2257
2544
|
modes: manifest.modes,
|
|
2258
2545
|
...manifest.regions && { regions: manifest.regions },
|
|
2259
2546
|
...manifest.latency_hints && { latency_hints: manifest.latency_hints },
|
|
@@ -2267,8 +2554,8 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2267
2554
|
* trustline is missing. Results are cached per (account, asset) so the
|
|
2268
2555
|
* per-payment overhead is one RPC call amortized to zero after the first.
|
|
2269
2556
|
*/
|
|
2270
|
-
async _checkTrustline(manifest) {
|
|
2271
|
-
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${
|
|
2557
|
+
async _checkTrustline(manifest, targetAsset = manifest.asset) {
|
|
2558
|
+
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
|
|
2272
2559
|
const cached = _RouteDockClient._trustlineCache.get(cacheKey);
|
|
2273
2560
|
if (cached && Date.now() < cached.expiresAt) return;
|
|
2274
2561
|
const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
|
|
@@ -2276,13 +2563,13 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2276
2563
|
try {
|
|
2277
2564
|
const account = await server.loadAccount(this.keypair.publicKey());
|
|
2278
2565
|
const balances = account.balances;
|
|
2279
|
-
const expectedIssuer = getAssetIssuer(
|
|
2566
|
+
const expectedIssuer = getAssetIssuer(targetAsset, this.network);
|
|
2280
2567
|
const hasTrustline = balances.some(
|
|
2281
|
-
(b) => typeof b === "object" && b !== null && "asset_code" in b && b.asset_code ===
|
|
2568
|
+
(b) => typeof b === "object" && b !== null && (targetAsset === "XLM" && b.asset_type === "native" || "asset_code" in b && b.asset_code === targetAsset && (!expectedIssuer || b.asset_issuer === expectedIssuer))
|
|
2282
2569
|
);
|
|
2283
2570
|
if (!hasTrustline) {
|
|
2284
|
-
const remediation = expectedIssuer ? `Run: stellar tx new --source ${this.keypair.publicKey()} --network ${this.network} change-trust --asset ${
|
|
2285
|
-
throw new RouteDockTrustlineError(
|
|
2571
|
+
const remediation = expectedIssuer ? `Run: stellar tx new --source ${this.keypair.publicKey()} --network ${this.network} change-trust --asset ${targetAsset}:${expectedIssuer} --limit 100000` : `Establish a trustline for ${targetAsset} with the appropriate issuer on ${this.network}`;
|
|
2572
|
+
throw new RouteDockTrustlineError(targetAsset, expectedIssuer || "unknown", remediation);
|
|
2286
2573
|
}
|
|
2287
2574
|
_RouteDockClient._trustlineCache.set(cacheKey, {
|
|
2288
2575
|
exists: true,
|
|
@@ -2291,7 +2578,8 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2291
2578
|
} catch (err) {
|
|
2292
2579
|
if (err instanceof RouteDockTrustlineError) throw err;
|
|
2293
2580
|
this.logger?.(
|
|
2294
|
-
|
|
2581
|
+
"warn",
|
|
2582
|
+
`[RouteDock] Trustline preflight: could not verify trustline for ${targetAsset} \u2014 continuing`
|
|
2295
2583
|
);
|
|
2296
2584
|
}
|
|
2297
2585
|
}
|
|
@@ -2299,18 +2587,18 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2299
2587
|
* Pay for one request at `url`. Fetches manifest, selects payment mode,
|
|
2300
2588
|
* runs trustline preflight, reserves local spend cap BEFORE executing the
|
|
2301
2589
|
* payment, then commits the spend record on success. Rolls back the
|
|
2302
|
-
* reservation if the on-chain payment fails
|
|
2303
|
-
*
|
|
2590
|
+
* reservation if the on-chain payment fails, and also when the call
|
|
2591
|
+
* completes without paying anything (an x402 endpoint answering a plain
|
|
2592
|
+
* 200 with no challenge). Concurrent pay() calls are serialized through a
|
|
2593
|
+
* per-instance mutex to prevent spend-cap overrun.
|
|
2304
2594
|
*/
|
|
2305
2595
|
async pay(url, options) {
|
|
2306
2596
|
const baseUrl = new URL(url).origin;
|
|
2307
2597
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2308
2598
|
assertEndpointActive(manifest, url, this.logger);
|
|
2309
2599
|
const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
return this._payWithNulthVault(url, manifest, mode);
|
|
2313
|
-
}
|
|
2600
|
+
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2601
|
+
await this._checkTrustline(manifest, selectedAsset);
|
|
2314
2602
|
let amount;
|
|
2315
2603
|
switch (mode) {
|
|
2316
2604
|
case "x402":
|
|
@@ -2330,22 +2618,30 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2330
2618
|
const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
|
|
2331
2619
|
let result;
|
|
2332
2620
|
try {
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2621
|
+
if (this.vault?.mode === "nulth") {
|
|
2622
|
+
result = await this._payWithNulthVault(url, manifest, mode);
|
|
2623
|
+
} else {
|
|
2624
|
+
switch (mode) {
|
|
2625
|
+
case "x402":
|
|
2626
|
+
result = await this.x402.pay(url, manifest);
|
|
2627
|
+
break;
|
|
2628
|
+
case "mpp-charge":
|
|
2629
|
+
result = await this.charge.pay(url, manifest);
|
|
2630
|
+
break;
|
|
2631
|
+
default:
|
|
2632
|
+
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2633
|
+
}
|
|
2342
2634
|
}
|
|
2343
2635
|
} catch (err) {
|
|
2344
2636
|
await this._rollbackSpend(reserveId).catch(() => {
|
|
2345
2637
|
});
|
|
2346
2638
|
throw err;
|
|
2347
2639
|
}
|
|
2348
|
-
|
|
2640
|
+
if (result.txHash === null && result.amount === "0") {
|
|
2641
|
+
await this._rollbackSpend(reserveId);
|
|
2642
|
+
} else {
|
|
2643
|
+
await this._commitSpend(reserveId);
|
|
2644
|
+
}
|
|
2349
2645
|
return result;
|
|
2350
2646
|
}
|
|
2351
2647
|
/** Nulth ZK vault path — proof built off-chain, attached as auth signature */
|
|
@@ -2395,9 +2691,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2395
2691
|
default:
|
|
2396
2692
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2397
2693
|
}
|
|
2694
|
+
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2398
2695
|
return {
|
|
2399
2696
|
amount,
|
|
2400
|
-
asset:
|
|
2697
|
+
asset: selectedAsset,
|
|
2401
2698
|
mode,
|
|
2402
2699
|
manifest,
|
|
2403
2700
|
...manifest.regions && { regions: manifest.regions },
|
|
@@ -2768,4 +3065,4 @@ var ProviderRegistry = class {
|
|
|
2768
3065
|
}
|
|
2769
3066
|
};
|
|
2770
3067
|
|
|
2771
|
-
export { DEFAULT_MAX_SESSION_DURATION_MS, DEFAULT_RETRY_POLICY, FileSpendStore, InMemorySpendStore, MppChargeClient, MppSessionClient, NulthPolicyError, OnChainRegistry, ProviderRegistry, RouteDockChannelStateError, RouteDockClient, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockManifestSunsetError, RouteDockManifestTimeoutError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SupabaseSessionStore, X402Client, assertEndpointActive, assertManifestValid, assertNulthVaultManifest, backoffDelayMs, configureManifestCache, createNulthSigner, createPolicyState, decodeAuthSignature, fetchLedgerSequence, fetchManifest, httpStatusToError, invalidateManifest, parseRetryAfterMs, paymentContextFromManifest, prepareNulthSigner, rankProvidersByLatency, selectMode, setNulthPaymentContext, usdcToMicros, usdcToUnits, withRetry, wrapFetchError };
|
|
3068
|
+
export { DEFAULT_MAX_SESSION_DURATION_MS, DEFAULT_RETRY_POLICY, FileSpendStore, InMemorySpendStore, MppChargeClient, MppSessionClient, NulthPolicyError, OnChainRegistry, ProviderRegistry, RouteDockChannelStateError, RouteDockClient, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockManifestSunsetError, RouteDockManifestTimeoutError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SupabaseSessionStore, X402Client, assertEndpointActive, assertManifestValid, assertNulthVaultManifest, backoffDelayMs, configureManifestCache, consoleLogger, createConsoleLogger, createNulthSigner, createPolicyState, decodeAuthSignature, fetchLedgerSequence, fetchManifest, getEligibleAssets, httpStatusToError, invalidateManifest, isAssetEligible, noopLogger, normalizeManifestAssets, parseRetryAfterMs, paymentContextFromManifest, prepareNulthSigner, rankProvidersByLatency, resolveAssetContract, resolveLogger, selectAsset, selectMode, setNulthPaymentContext, usdcToMicros, usdcToUnits, withRetry, wrapFetchError, wrapMppError };
|