@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/react.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createContext, useContext, useMemo, useEffect, useState, useRef, useCal
|
|
|
2
2
|
import { jsx } from 'react/jsx-runtime';
|
|
3
3
|
import { Keypair, Horizon, Networks, rpc, xdr, Address, scValToNative, hash } from '@stellar/stellar-sdk';
|
|
4
4
|
import { Validator } from '@cfworker/json-schema';
|
|
5
|
-
import { usdcToStroops } from '@routedock/nulth-sdk';
|
|
5
|
+
import { usdcToStroops, stroopsToUsdc } from '@routedock/nulth-sdk';
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
7
|
import { createEd25519Signer } from '@x402/stellar';
|
|
8
8
|
import { ExactStellarScheme } from '@x402/stellar/exact/client';
|
|
@@ -161,6 +161,31 @@ function wrapFetchError(err, context) {
|
|
|
161
161
|
if (err instanceof RouteDockError) return err;
|
|
162
162
|
return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
|
|
163
163
|
}
|
|
164
|
+
function wrapMppError(err, context) {
|
|
165
|
+
if (err instanceof RouteDockError) return err;
|
|
166
|
+
if (err instanceof TypeError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
|
|
167
|
+
return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
|
|
168
|
+
}
|
|
169
|
+
return new RouteDockSignatureError(`${context}: ${String(err)}`, { cause: err });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/internal/logger.ts
|
|
173
|
+
function createConsoleLogger() {
|
|
174
|
+
return (level, message, fields) => {
|
|
175
|
+
const values = fields ? Object.values(fields) : [];
|
|
176
|
+
if (values.length > 0) {
|
|
177
|
+
console[level](message, ...values);
|
|
178
|
+
} else {
|
|
179
|
+
console[level](message);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
var consoleLogger = createConsoleLogger();
|
|
184
|
+
var noopLogger = () => {
|
|
185
|
+
};
|
|
186
|
+
function resolveLogger(logger, fallback = consoleLogger) {
|
|
187
|
+
return logger ?? fallback;
|
|
188
|
+
}
|
|
164
189
|
|
|
165
190
|
// src/internal/retry.ts
|
|
166
191
|
var DEFAULT_RETRY_POLICY = {
|
|
@@ -176,10 +201,11 @@ function sleep(ms) {
|
|
|
176
201
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
177
202
|
}
|
|
178
203
|
async function withRetry(fn, policy = {}) {
|
|
179
|
-
const { maxAttempts, baseDelayMs, maxDelayMs, onRetry } = {
|
|
204
|
+
const { maxAttempts, baseDelayMs, maxDelayMs, onRetry, logger } = {
|
|
180
205
|
...DEFAULT_RETRY_POLICY,
|
|
181
206
|
...policy
|
|
182
207
|
};
|
|
208
|
+
const log = resolveLogger(logger);
|
|
183
209
|
let lastError;
|
|
184
210
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
185
211
|
try {
|
|
@@ -197,7 +223,7 @@ async function withRetry(fn, policy = {}) {
|
|
|
197
223
|
try {
|
|
198
224
|
await onRetry(attempt, err, delay);
|
|
199
225
|
} catch (cbErr) {
|
|
200
|
-
|
|
226
|
+
log("error", "[withRetry] onRetry callback threw:", { error: cbErr });
|
|
201
227
|
}
|
|
202
228
|
}
|
|
203
229
|
await sleep(delay);
|
|
@@ -289,6 +315,14 @@ var routedock_schema_default = {
|
|
|
289
315
|
type: "string",
|
|
290
316
|
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
291
317
|
},
|
|
318
|
+
assets: {
|
|
319
|
+
type: "array",
|
|
320
|
+
minItems: 1,
|
|
321
|
+
items: {
|
|
322
|
+
$ref: "#/definitions/AssetConfig"
|
|
323
|
+
},
|
|
324
|
+
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."
|
|
325
|
+
},
|
|
292
326
|
payee: {
|
|
293
327
|
type: "string",
|
|
294
328
|
description: "Stellar address (G...) that receives payments"
|
|
@@ -421,6 +455,38 @@ var routedock_schema_default = {
|
|
|
421
455
|
enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
|
|
422
456
|
description: "A supported RouteDock payment mode"
|
|
423
457
|
},
|
|
458
|
+
AssetConfig: {
|
|
459
|
+
type: "object",
|
|
460
|
+
description: "Payment asset configuration with optional mode and endpoint scoping",
|
|
461
|
+
required: ["asset", "asset_contract"],
|
|
462
|
+
additionalProperties: false,
|
|
463
|
+
properties: {
|
|
464
|
+
asset: {
|
|
465
|
+
type: "string",
|
|
466
|
+
description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
|
|
467
|
+
},
|
|
468
|
+
asset_contract: {
|
|
469
|
+
type: "string",
|
|
470
|
+
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
471
|
+
},
|
|
472
|
+
modes: {
|
|
473
|
+
type: "array",
|
|
474
|
+
items: {
|
|
475
|
+
$ref: "#/definitions/PaymentMode"
|
|
476
|
+
},
|
|
477
|
+
uniqueItems: true,
|
|
478
|
+
description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
|
|
479
|
+
},
|
|
480
|
+
endpoints: {
|
|
481
|
+
type: "array",
|
|
482
|
+
items: {
|
|
483
|
+
type: "string"
|
|
484
|
+
},
|
|
485
|
+
uniqueItems: true,
|
|
486
|
+
description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
},
|
|
424
490
|
PricingConfig: {
|
|
425
491
|
type: "object",
|
|
426
492
|
description: "Per-request pricing for x402 or mpp-charge modes",
|
|
@@ -566,7 +632,7 @@ var routedock_schema_default = {
|
|
|
566
632
|
|
|
567
633
|
// package.json
|
|
568
634
|
var package_default = {
|
|
569
|
-
version: "0.
|
|
635
|
+
version: "0.3.1"};
|
|
570
636
|
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
571
637
|
function canonicalize(value) {
|
|
572
638
|
if (Array.isArray(value)) {
|
|
@@ -680,6 +746,7 @@ function assertEndpointActive(manifest, url, logger) {
|
|
|
680
746
|
const [key, descriptor] = deprecated;
|
|
681
747
|
const sunsetSuffix = descriptor.sunset_at ? `; sunset_at ${descriptor.sunset_at}` : "";
|
|
682
748
|
logger(
|
|
749
|
+
"warn",
|
|
683
750
|
`[RouteDock] WARNING: ${manifest.name} \u2192 ${pathname}; endpoint '${key}' is deprecated${sunsetSuffix}`
|
|
684
751
|
);
|
|
685
752
|
}
|
|
@@ -701,6 +768,27 @@ function assertManifestValid(manifest, baseUrl) {
|
|
|
701
768
|
}
|
|
702
769
|
}
|
|
703
770
|
}
|
|
771
|
+
if (manifest.assets !== void 0) {
|
|
772
|
+
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
773
|
+
throw new RouteDockManifestError(
|
|
774
|
+
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
for (let i = 0; i < manifest.assets.length; i++) {
|
|
778
|
+
const a = manifest.assets[i];
|
|
779
|
+
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
780
|
+
throw new RouteDockManifestError(
|
|
781
|
+
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const first = manifest.assets[0];
|
|
786
|
+
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
787
|
+
throw new RouteDockManifestError(
|
|
788
|
+
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
704
792
|
}
|
|
705
793
|
var CACHE_TTL_MS = 6e4;
|
|
706
794
|
var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
|
|
@@ -874,16 +962,16 @@ function logSelection(manifest, selection, log, deprecated) {
|
|
|
874
962
|
const reason = selection.reason ? ` (${selection.reason})` : "";
|
|
875
963
|
if (deprecated) {
|
|
876
964
|
log(
|
|
965
|
+
"warn",
|
|
877
966
|
`[RouteDock] WARNING: ${manifest.name} \u2192 ${selection.mode}${reason}; selected deprecated mode because no active supported mode is available`
|
|
878
967
|
);
|
|
879
968
|
return;
|
|
880
969
|
}
|
|
881
|
-
log(`[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
|
|
970
|
+
log("info", `[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
|
|
882
971
|
}
|
|
883
972
|
function selectMode(manifest, options = {}) {
|
|
884
973
|
const modes = manifest.modes;
|
|
885
|
-
const log = options.logger ??
|
|
886
|
-
});
|
|
974
|
+
const log = options.logger ?? noopLogger;
|
|
887
975
|
const deprecatedSet = new Set(manifest.deprecated_modes ?? []);
|
|
888
976
|
if (options.forceMode) {
|
|
889
977
|
if (!modes.includes(options.forceMode)) {
|
|
@@ -893,10 +981,11 @@ function selectMode(manifest, options = {}) {
|
|
|
893
981
|
}
|
|
894
982
|
if (deprecatedSet.has(options.forceMode)) {
|
|
895
983
|
log(
|
|
984
|
+
"warn",
|
|
896
985
|
`[RouteDock] WARNING: ${manifest.name} \u2192 ${options.forceMode} (forced); selected mode is deprecated`
|
|
897
986
|
);
|
|
898
987
|
} else {
|
|
899
|
-
log(`[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
|
|
988
|
+
log("info", `[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
|
|
900
989
|
}
|
|
901
990
|
return options.forceMode;
|
|
902
991
|
}
|
|
@@ -916,6 +1005,69 @@ function selectMode(manifest, options = {}) {
|
|
|
916
1005
|
`No supported payment mode found in manifest (modes: ${modes.join(", ")})`
|
|
917
1006
|
);
|
|
918
1007
|
}
|
|
1008
|
+
|
|
1009
|
+
// src/internal/payee.ts
|
|
1010
|
+
function resolvePayee(manifest, mode) {
|
|
1011
|
+
const override = manifest.pricing[mode]?.payee;
|
|
1012
|
+
return override ?? manifest.payee;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/client/challenge.ts
|
|
1016
|
+
function isRecord(value) {
|
|
1017
|
+
return typeof value === "object" && value !== null;
|
|
1018
|
+
}
|
|
1019
|
+
function isPositiveIntegerString(value) {
|
|
1020
|
+
return typeof value === "string" && /^[1-9]\d*$/.test(value);
|
|
1021
|
+
}
|
|
1022
|
+
function checkAmount(amount, maxAmount) {
|
|
1023
|
+
if (!isPositiveIntegerString(amount)) {
|
|
1024
|
+
return new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1025
|
+
}
|
|
1026
|
+
let max;
|
|
1027
|
+
try {
|
|
1028
|
+
max = usdcToStroops(maxAmount);
|
|
1029
|
+
} catch {
|
|
1030
|
+
return new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1031
|
+
}
|
|
1032
|
+
if (BigInt(amount) > max) {
|
|
1033
|
+
return new RouteDockPolicyRejectError("challenge_amount_exceeds_manifest");
|
|
1034
|
+
}
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
function checkX402Accept(accept, manifest, network, maxAmount) {
|
|
1038
|
+
if (accept.network !== network) {
|
|
1039
|
+
return new RouteDockPolicyRejectError("challenge_network_mismatch");
|
|
1040
|
+
}
|
|
1041
|
+
if (accept.payTo !== resolvePayee(manifest, "x402")) {
|
|
1042
|
+
return new RouteDockPolicyRejectError("challenge_payee_mismatch");
|
|
1043
|
+
}
|
|
1044
|
+
if (accept.asset !== manifest.asset_contract) {
|
|
1045
|
+
return new RouteDockPolicyRejectError("challenge_asset_mismatch");
|
|
1046
|
+
}
|
|
1047
|
+
return checkAmount(accept.amount, maxAmount);
|
|
1048
|
+
}
|
|
1049
|
+
function filterX402Accepts(accepts, manifest, network, maxAmount) {
|
|
1050
|
+
return accepts.filter((accept) => checkX402Accept(accept, manifest, network, maxAmount) === null);
|
|
1051
|
+
}
|
|
1052
|
+
function checkChargeChallenge(request, manifest, network, maxAmount) {
|
|
1053
|
+
const methodDetails = request["methodDetails"];
|
|
1054
|
+
const rawNetwork = isRecord(methodDetails) ? methodDetails["network"] : void 0;
|
|
1055
|
+
const challengeNetwork = rawNetwork === void 0 ? "stellar:testnet" : rawNetwork;
|
|
1056
|
+
if (challengeNetwork !== network) {
|
|
1057
|
+
return new RouteDockPolicyRejectError("challenge_network_mismatch");
|
|
1058
|
+
}
|
|
1059
|
+
const recipient = request["recipient"];
|
|
1060
|
+
if (typeof recipient !== "string" || recipient !== resolvePayee(manifest, "mpp-charge")) {
|
|
1061
|
+
return new RouteDockPolicyRejectError("challenge_payee_mismatch");
|
|
1062
|
+
}
|
|
1063
|
+
const currency = request["currency"];
|
|
1064
|
+
if (typeof currency !== "string" || currency !== manifest.asset_contract) {
|
|
1065
|
+
return new RouteDockPolicyRejectError("challenge_asset_mismatch");
|
|
1066
|
+
}
|
|
1067
|
+
return checkAmount(request["amount"], maxAmount);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/client/x402Client.ts
|
|
919
1071
|
var CAIP2 = {
|
|
920
1072
|
testnet: "stellar:testnet",
|
|
921
1073
|
mainnet: "stellar:pubnet"
|
|
@@ -947,83 +1099,106 @@ var X402Client = class _X402Client {
|
|
|
947
1099
|
if (!pricing.facilitator) {
|
|
948
1100
|
throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
|
|
949
1101
|
}
|
|
950
|
-
|
|
951
|
-
|
|
1102
|
+
const caip2 = CAIP2[this.network];
|
|
1103
|
+
const probe = await withRetry(async () => {
|
|
1104
|
+
let init2;
|
|
952
1105
|
try {
|
|
953
|
-
|
|
1106
|
+
init2 = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
|
|
954
1107
|
} catch (err) {
|
|
955
1108
|
throw wrapFetchError(err, "x402 initial request");
|
|
956
1109
|
}
|
|
957
|
-
if (
|
|
958
|
-
if (!
|
|
1110
|
+
if (init2.status !== 402) {
|
|
1111
|
+
if (!init2.ok) {
|
|
959
1112
|
throw httpStatusToError(
|
|
960
|
-
`x402 initial request failed: HTTP ${
|
|
961
|
-
|
|
962
|
-
|
|
1113
|
+
`x402 initial request failed: HTTP ${init2.status}`,
|
|
1114
|
+
init2.status,
|
|
1115
|
+
init2
|
|
963
1116
|
);
|
|
964
1117
|
}
|
|
965
1118
|
let data2;
|
|
966
1119
|
try {
|
|
967
|
-
data2 = await
|
|
1120
|
+
data2 = await init2.json();
|
|
968
1121
|
} catch {
|
|
969
|
-
throw new RouteDockManifestError(
|
|
1122
|
+
throw new RouteDockManifestError(
|
|
1123
|
+
`Failed to parse JSON from non-402 response (HTTP ${init2.status})`
|
|
1124
|
+
);
|
|
970
1125
|
}
|
|
971
|
-
return {
|
|
972
|
-
}
|
|
973
|
-
const reqHeader = init.headers.get("X-Payment-Requirements");
|
|
974
|
-
if (!reqHeader) {
|
|
975
|
-
throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
|
|
1126
|
+
return { kind: "free", data: data2 };
|
|
976
1127
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1128
|
+
return { kind: "payment-required", init: init2 };
|
|
1129
|
+
}, this.retryPolicy);
|
|
1130
|
+
if (probe.kind === "free") {
|
|
1131
|
+
return { data: probe.data, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
|
|
1132
|
+
}
|
|
1133
|
+
const init = probe.init;
|
|
1134
|
+
const reqHeader = init.headers.get("X-Payment-Requirements");
|
|
1135
|
+
if (!reqHeader) {
|
|
1136
|
+
throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
|
|
1137
|
+
}
|
|
1138
|
+
let paymentRequired;
|
|
1139
|
+
try {
|
|
1140
|
+
paymentRequired = decodePaymentRequiredHeader(reqHeader);
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
|
|
1143
|
+
cause: err
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
const firstAccept = paymentRequired.accepts[0];
|
|
1147
|
+
if (!firstAccept) {
|
|
1148
|
+
throw new RouteDockManifestError("402 response carries no payment accepts");
|
|
1149
|
+
}
|
|
1150
|
+
const allowed = filterX402Accepts(paymentRequired.accepts, manifest, caip2, pricing.amount);
|
|
1151
|
+
if (allowed.length === 0) {
|
|
1152
|
+
throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
|
|
1153
|
+
}
|
|
1154
|
+
let paymentPayload;
|
|
1155
|
+
try {
|
|
1156
|
+
paymentPayload = await this.httpClient.createPaymentPayload({
|
|
1157
|
+
...paymentRequired,
|
|
1158
|
+
accepts: allowed
|
|
1159
|
+
});
|
|
1160
|
+
} catch (err) {
|
|
1161
|
+
throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
|
|
1162
|
+
cause: err
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
1166
|
+
const signedAmount = stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
|
|
1167
|
+
const settled = await withRetry(async () => {
|
|
1168
|
+
let response;
|
|
995
1169
|
try {
|
|
996
|
-
|
|
1170
|
+
response = await fetch(url, { headers: paymentHeaders });
|
|
997
1171
|
} catch (err) {
|
|
998
1172
|
throw wrapFetchError(err, "x402 settlement request");
|
|
999
1173
|
}
|
|
1000
|
-
if (!
|
|
1001
|
-
if (
|
|
1174
|
+
if (!response.ok) {
|
|
1175
|
+
if (response.status >= 500 || response.status === 429 || response.status === 503) {
|
|
1002
1176
|
throw httpStatusToError(
|
|
1003
|
-
`x402 payment failed: HTTP ${
|
|
1004
|
-
|
|
1005
|
-
|
|
1177
|
+
`x402 payment failed: HTTP ${response.status}`,
|
|
1178
|
+
response.status,
|
|
1179
|
+
response
|
|
1006
1180
|
);
|
|
1007
1181
|
}
|
|
1008
|
-
throw new RouteDockManifestError(`x402 payment failed: HTTP ${
|
|
1009
|
-
}
|
|
1010
|
-
let txHash = null;
|
|
1011
|
-
const responseHeader = settled.headers.get("X-Payment-Response");
|
|
1012
|
-
if (responseHeader) {
|
|
1013
|
-
try {
|
|
1014
|
-
const settleResponse = decodePaymentResponseHeader(responseHeader);
|
|
1015
|
-
txHash = settleResponse.transaction ?? null;
|
|
1016
|
-
} catch {
|
|
1017
|
-
}
|
|
1182
|
+
throw new RouteDockManifestError(`x402 payment failed: HTTP ${response.status}`);
|
|
1018
1183
|
}
|
|
1019
|
-
|
|
1184
|
+
return response;
|
|
1185
|
+
}, this.retryPolicy);
|
|
1186
|
+
let txHash = null;
|
|
1187
|
+
const responseHeader = settled.headers.get("X-Payment-Response");
|
|
1188
|
+
if (responseHeader) {
|
|
1020
1189
|
try {
|
|
1021
|
-
|
|
1190
|
+
const settleResponse = decodePaymentResponseHeader(responseHeader);
|
|
1191
|
+
txHash = settleResponse.transaction ?? null;
|
|
1022
1192
|
} catch {
|
|
1023
|
-
throw new RouteDockManifestError("Failed to parse JSON from settled response");
|
|
1024
1193
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1194
|
+
}
|
|
1195
|
+
let data;
|
|
1196
|
+
try {
|
|
1197
|
+
data = await settled.json();
|
|
1198
|
+
} catch {
|
|
1199
|
+
throw new RouteDockManifestError("Failed to parse JSON from settled response");
|
|
1200
|
+
}
|
|
1201
|
+
return { data, txHash, mode: "x402", amount: signedAmount, timestamp: Date.now() };
|
|
1027
1202
|
}
|
|
1028
1203
|
};
|
|
1029
1204
|
var MppChargeClient = class {
|
|
@@ -1040,27 +1215,42 @@ var MppChargeClient = class {
|
|
|
1040
1215
|
if (!pricing) {
|
|
1041
1216
|
throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
|
|
1042
1217
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1218
|
+
const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
|
|
1219
|
+
let txHash = null;
|
|
1220
|
+
let signedAmount;
|
|
1221
|
+
let credential;
|
|
1222
|
+
const mppx = Mppx.create({
|
|
1223
|
+
polyfill: false,
|
|
1224
|
+
methods: [
|
|
1225
|
+
stellar.charge({
|
|
1226
|
+
keypair: this.keypair,
|
|
1227
|
+
mode: "pull",
|
|
1228
|
+
onProgress(event) {
|
|
1229
|
+
if (event.type === "paid") {
|
|
1230
|
+
txHash = event.hash;
|
|
1055
1231
|
}
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1232
|
+
}
|
|
1233
|
+
})
|
|
1234
|
+
],
|
|
1235
|
+
onChallenge: async (challenge, { createCredential }) => {
|
|
1236
|
+
const request = challenge.request;
|
|
1237
|
+
const rejection = checkChargeChallenge(request, manifest, caip2, pricing.amount);
|
|
1238
|
+
if (rejection) throw rejection;
|
|
1239
|
+
const amount = request["amount"];
|
|
1240
|
+
if (typeof amount !== "string") {
|
|
1241
|
+
throw new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1242
|
+
}
|
|
1243
|
+
signedAmount = stroopsToUsdc(BigInt(amount));
|
|
1244
|
+
credential ??= await createCredential();
|
|
1245
|
+
return credential;
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
return withRetry(async () => {
|
|
1059
1249
|
let response;
|
|
1060
1250
|
try {
|
|
1061
|
-
response = await mppx.fetch(url);
|
|
1251
|
+
response = credential ? await mppx.rawFetch(url, { headers: { Authorization: credential } }) : await mppx.fetch(url);
|
|
1062
1252
|
} catch (err) {
|
|
1063
|
-
throw
|
|
1253
|
+
throw wrapMppError(err, "MPP charge request");
|
|
1064
1254
|
}
|
|
1065
1255
|
if (!response.ok) {
|
|
1066
1256
|
if (response.status >= 500 || response.status === 429 || response.status === 503) {
|
|
@@ -1081,7 +1271,13 @@ var MppChargeClient = class {
|
|
|
1081
1271
|
{ cause }
|
|
1082
1272
|
);
|
|
1083
1273
|
}
|
|
1084
|
-
return {
|
|
1274
|
+
return {
|
|
1275
|
+
data,
|
|
1276
|
+
txHash,
|
|
1277
|
+
mode: "mpp-charge",
|
|
1278
|
+
amount: signedAmount ?? pricing.amount,
|
|
1279
|
+
timestamp: Date.now()
|
|
1280
|
+
};
|
|
1085
1281
|
}, this.retryPolicy);
|
|
1086
1282
|
}
|
|
1087
1283
|
};
|
|
@@ -1125,16 +1321,18 @@ function resolveStoreNetworkId(network) {
|
|
|
1125
1321
|
return network === STELLAR_PUBNET ? STELLAR_PUBNET : STELLAR_TESTNET;
|
|
1126
1322
|
}
|
|
1127
1323
|
var MppSessionClient = class {
|
|
1128
|
-
constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory) {
|
|
1324
|
+
constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory, logger = consoleLogger) {
|
|
1129
1325
|
this.keypair = keypair;
|
|
1130
1326
|
this.network = network;
|
|
1131
1327
|
this.retryPolicy = retryPolicy;
|
|
1132
1328
|
this.webSocketFactory = webSocketFactory;
|
|
1329
|
+
this.logger = logger;
|
|
1133
1330
|
}
|
|
1134
1331
|
keypair;
|
|
1135
1332
|
network;
|
|
1136
1333
|
retryPolicy;
|
|
1137
1334
|
webSocketFactory;
|
|
1335
|
+
logger;
|
|
1138
1336
|
async openSession(url, manifest, commitmentSecret, options, onSpend) {
|
|
1139
1337
|
const mode = options?.mode ?? "mpp-session";
|
|
1140
1338
|
const pricing = manifest.pricing[mode];
|
|
@@ -1359,7 +1557,7 @@ var MppSessionClient = class {
|
|
|
1359
1557
|
if (abortController.signal.aborted) {
|
|
1360
1558
|
throw err;
|
|
1361
1559
|
}
|
|
1362
|
-
throw
|
|
1560
|
+
throw wrapMppError(err, "Voucher request");
|
|
1363
1561
|
}
|
|
1364
1562
|
if (!resp.ok) {
|
|
1365
1563
|
if (resp.status >= 500 || resp.status === 429 || resp.status === 503) {
|
|
@@ -1652,9 +1850,10 @@ var MppSessionClient = class {
|
|
|
1652
1850
|
emit("session:timeout", { maxDurationMs });
|
|
1653
1851
|
void handle.close().catch((error) => {
|
|
1654
1852
|
emit("session:close-failed", { maxDurationMs, error });
|
|
1655
|
-
|
|
1853
|
+
this.logger(
|
|
1854
|
+
"warn",
|
|
1656
1855
|
`RouteDock: maxDuration auto-close failed after ${maxDurationMs}ms \u2014 the channel may still hold collateral; retry close() or call requestRefund().`,
|
|
1657
|
-
error
|
|
1856
|
+
{ error }
|
|
1658
1857
|
);
|
|
1659
1858
|
});
|
|
1660
1859
|
}, maxDurationMs);
|
|
@@ -1704,7 +1903,7 @@ var MppSessionClient = class {
|
|
|
1704
1903
|
);
|
|
1705
1904
|
onSigned?.();
|
|
1706
1905
|
} catch (err) {
|
|
1707
|
-
throw
|
|
1906
|
+
throw wrapMppError(err, "Voucher credential");
|
|
1708
1907
|
}
|
|
1709
1908
|
const wsUrl = url.replace(/^http/, "ws");
|
|
1710
1909
|
const queue = [];
|
|
@@ -1881,11 +2080,6 @@ function encodeAuthSignature(proof) {
|
|
|
1881
2080
|
const payload = { nulth: "zk-v1", proof };
|
|
1882
2081
|
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
1883
2082
|
}
|
|
1884
|
-
function usdcToStroops2(amount) {
|
|
1885
|
-
const [whole = "0", frac = ""] = amount.split(".");
|
|
1886
|
-
const padded = (frac + "0000000").slice(0, 7);
|
|
1887
|
-
return BigInt(whole) * 10000000n + BigInt(padded);
|
|
1888
|
-
}
|
|
1889
2083
|
var NulthClient = class {
|
|
1890
2084
|
constructor(config) {
|
|
1891
2085
|
this.config = config;
|
|
@@ -1901,7 +2095,10 @@ var NulthClient = class {
|
|
|
1901
2095
|
`NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
|
|
1902
2096
|
);
|
|
1903
2097
|
}
|
|
1904
|
-
|
|
2098
|
+
resolveLogger(config.logger)(
|
|
2099
|
+
"warn",
|
|
2100
|
+
"NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
|
|
2101
|
+
);
|
|
1905
2102
|
}
|
|
1906
2103
|
config;
|
|
1907
2104
|
policy;
|
|
@@ -1953,6 +2150,12 @@ var NulthClient = class {
|
|
|
1953
2150
|
return proof;
|
|
1954
2151
|
}
|
|
1955
2152
|
enforcePolicy(context) {
|
|
2153
|
+
if (context.amountStroops < 0n) {
|
|
2154
|
+
throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
|
|
2155
|
+
}
|
|
2156
|
+
if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
|
|
2157
|
+
throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
|
|
2158
|
+
}
|
|
1956
2159
|
if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
|
|
1957
2160
|
throw new NulthPolicyError("session_expired", "Nulth session expired");
|
|
1958
2161
|
}
|
|
@@ -1987,14 +2190,14 @@ function paymentContextFromManifest(manifest, mode, ledgerSequence) {
|
|
|
1987
2190
|
const pricing = manifest.pricing[mode];
|
|
1988
2191
|
if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
|
|
1989
2192
|
return {
|
|
1990
|
-
payee: manifest.payee,
|
|
1991
|
-
amountStroops:
|
|
2193
|
+
payee: pricing.payee ?? manifest.payee,
|
|
2194
|
+
amountStroops: usdcToStroops(pricing.amount),
|
|
1992
2195
|
assetContract: manifest.asset_contract,
|
|
1993
2196
|
ledgerSequence
|
|
1994
2197
|
};
|
|
1995
2198
|
}
|
|
1996
2199
|
function createPolicyState(input) {
|
|
1997
|
-
const dailyCapStroops =
|
|
2200
|
+
const dailyCapStroops = usdcToStroops(input.dailyCapUsdc);
|
|
1998
2201
|
return {
|
|
1999
2202
|
dailyCapStroops,
|
|
2000
2203
|
allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
|
|
@@ -2056,7 +2259,8 @@ var InMemorySpendStore = class {
|
|
|
2056
2259
|
state = null;
|
|
2057
2260
|
constructor(options = {}) {
|
|
2058
2261
|
if (options.warn !== false) {
|
|
2059
|
-
|
|
2262
|
+
resolveLogger(options.logger)(
|
|
2263
|
+
"warn",
|
|
2060
2264
|
"[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."
|
|
2061
2265
|
);
|
|
2062
2266
|
}
|
|
@@ -2069,6 +2273,72 @@ var InMemorySpendStore = class {
|
|
|
2069
2273
|
}
|
|
2070
2274
|
};
|
|
2071
2275
|
|
|
2276
|
+
// src/internal/assetUtils.ts
|
|
2277
|
+
function normalizeManifestAssets(manifest) {
|
|
2278
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
2279
|
+
return manifest.assets;
|
|
2280
|
+
}
|
|
2281
|
+
if (manifest.asset && manifest.asset_contract) {
|
|
2282
|
+
return [
|
|
2283
|
+
{
|
|
2284
|
+
asset: manifest.asset,
|
|
2285
|
+
asset_contract: manifest.asset_contract
|
|
2286
|
+
}
|
|
2287
|
+
];
|
|
2288
|
+
}
|
|
2289
|
+
throw new RouteDockManifestError(
|
|
2290
|
+
"Manifest must define either assets[] or root asset/asset_contract fields"
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
function getEligibleAssets(manifest, mode, endpoint) {
|
|
2294
|
+
const assets = normalizeManifestAssets(manifest);
|
|
2295
|
+
return assets.filter((asset) => {
|
|
2296
|
+
if (asset.modes && !asset.modes.includes(mode)) {
|
|
2297
|
+
return false;
|
|
2298
|
+
}
|
|
2299
|
+
if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
|
|
2300
|
+
const trimmedEndpoint = endpoint.replace(/^\//, "");
|
|
2301
|
+
if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
|
|
2302
|
+
return true;
|
|
2303
|
+
}
|
|
2304
|
+
if (manifest.endpoints) {
|
|
2305
|
+
let pathname = endpoint;
|
|
2306
|
+
if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
|
|
2307
|
+
try {
|
|
2308
|
+
pathname = new URL(endpoint).pathname;
|
|
2309
|
+
} catch {
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
const trimmedPathname = pathname.replace(/^\//, "");
|
|
2313
|
+
const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
|
|
2314
|
+
const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
|
|
2315
|
+
const keyMatches = key === endpoint || key === trimmedEndpoint;
|
|
2316
|
+
if (pathMatches || keyMatches) {
|
|
2317
|
+
return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
|
|
2318
|
+
}
|
|
2319
|
+
return false;
|
|
2320
|
+
});
|
|
2321
|
+
if (!matchesEndpoint) {
|
|
2322
|
+
return false;
|
|
2323
|
+
}
|
|
2324
|
+
} else {
|
|
2325
|
+
return false;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
return true;
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
function selectAsset(manifest, mode, endpoint) {
|
|
2332
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
2333
|
+
if (eligible.length === 0) {
|
|
2334
|
+
const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
|
|
2335
|
+
throw new RouteDockManifestError(
|
|
2336
|
+
`No eligible assets found for mode '${mode}'${endpointMsg}`
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
return eligible[0];
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2072
2342
|
// src/client/RouteDockClient.ts
|
|
2073
2343
|
var _secrets = /* @__PURE__ */ new WeakMap();
|
|
2074
2344
|
function normalizeEndpointCaps(endpointCaps) {
|
|
@@ -2145,7 +2415,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2145
2415
|
this.network = config.network;
|
|
2146
2416
|
this.spendCap = config.spendCap ? normalizeSpendCap(config.spendCap) : void 0;
|
|
2147
2417
|
this.retryPolicy = config.retryPolicy;
|
|
2148
|
-
this.spendStore = config.spendStore ?? new InMemorySpendStore({
|
|
2418
|
+
this.spendStore = config.spendStore ?? new InMemorySpendStore({
|
|
2419
|
+
warn: !!config.spendCap,
|
|
2420
|
+
...config.logger && { logger: config.logger }
|
|
2421
|
+
});
|
|
2149
2422
|
this.logger = config.logger;
|
|
2150
2423
|
this.manifestTimeoutMs = config.manifestTimeoutMs;
|
|
2151
2424
|
this.expectedPayee = config.expectedPayee;
|
|
@@ -2157,9 +2430,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2157
2430
|
_secrets.set(this, config.commitmentSecret);
|
|
2158
2431
|
}
|
|
2159
2432
|
const secretKey = this.keypair.secret();
|
|
2160
|
-
this.
|
|
2161
|
-
this.
|
|
2162
|
-
this.
|
|
2433
|
+
const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
|
|
2434
|
+
this.x402 = new X402Client(secretKey, this.network, retryPolicy);
|
|
2435
|
+
this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
|
|
2436
|
+
this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
|
|
2163
2437
|
}
|
|
2164
2438
|
/** Fetch manifest and select mode — shared by pay() and estimateCost(). */
|
|
2165
2439
|
async _resolveManifest(url, options) {
|
|
@@ -2188,12 +2462,13 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2188
2462
|
* call before committing to a payment — for approval gates and manual
|
|
2189
2463
|
* trustline remediation.
|
|
2190
2464
|
*/
|
|
2191
|
-
async preflight(manifest) {
|
|
2465
|
+
async preflight(manifest, mode, endpoint) {
|
|
2192
2466
|
assertManifestValid(manifest);
|
|
2193
|
-
|
|
2467
|
+
const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
|
|
2468
|
+
await this._checkTrustline(manifest, selectedAsset);
|
|
2194
2469
|
return {
|
|
2195
2470
|
hasTrustline: true,
|
|
2196
|
-
asset:
|
|
2471
|
+
asset: selectedAsset,
|
|
2197
2472
|
modes: manifest.modes,
|
|
2198
2473
|
...manifest.regions && { regions: manifest.regions },
|
|
2199
2474
|
...manifest.latency_hints && { latency_hints: manifest.latency_hints },
|
|
@@ -2207,8 +2482,8 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2207
2482
|
* trustline is missing. Results are cached per (account, asset) so the
|
|
2208
2483
|
* per-payment overhead is one RPC call amortized to zero after the first.
|
|
2209
2484
|
*/
|
|
2210
|
-
async _checkTrustline(manifest) {
|
|
2211
|
-
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${
|
|
2485
|
+
async _checkTrustline(manifest, targetAsset = manifest.asset) {
|
|
2486
|
+
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
|
|
2212
2487
|
const cached = _RouteDockClient._trustlineCache.get(cacheKey);
|
|
2213
2488
|
if (cached && Date.now() < cached.expiresAt) return;
|
|
2214
2489
|
const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
|
|
@@ -2216,13 +2491,13 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2216
2491
|
try {
|
|
2217
2492
|
const account = await server.loadAccount(this.keypair.publicKey());
|
|
2218
2493
|
const balances = account.balances;
|
|
2219
|
-
const expectedIssuer = getAssetIssuer(
|
|
2494
|
+
const expectedIssuer = getAssetIssuer(targetAsset, this.network);
|
|
2220
2495
|
const hasTrustline = balances.some(
|
|
2221
|
-
(b) => typeof b === "object" && b !== null && "asset_code" in b && b.asset_code ===
|
|
2496
|
+
(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))
|
|
2222
2497
|
);
|
|
2223
2498
|
if (!hasTrustline) {
|
|
2224
|
-
const remediation = expectedIssuer ? `Run: stellar tx new --source ${this.keypair.publicKey()} --network ${this.network} change-trust --asset ${
|
|
2225
|
-
throw new RouteDockTrustlineError(
|
|
2499
|
+
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}`;
|
|
2500
|
+
throw new RouteDockTrustlineError(targetAsset, expectedIssuer || "unknown", remediation);
|
|
2226
2501
|
}
|
|
2227
2502
|
_RouteDockClient._trustlineCache.set(cacheKey, {
|
|
2228
2503
|
exists: true,
|
|
@@ -2231,7 +2506,8 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2231
2506
|
} catch (err) {
|
|
2232
2507
|
if (err instanceof RouteDockTrustlineError) throw err;
|
|
2233
2508
|
this.logger?.(
|
|
2234
|
-
|
|
2509
|
+
"warn",
|
|
2510
|
+
`[RouteDock] Trustline preflight: could not verify trustline for ${targetAsset} \u2014 continuing`
|
|
2235
2511
|
);
|
|
2236
2512
|
}
|
|
2237
2513
|
}
|
|
@@ -2239,18 +2515,18 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2239
2515
|
* Pay for one request at `url`. Fetches manifest, selects payment mode,
|
|
2240
2516
|
* runs trustline preflight, reserves local spend cap BEFORE executing the
|
|
2241
2517
|
* payment, then commits the spend record on success. Rolls back the
|
|
2242
|
-
* reservation if the on-chain payment fails
|
|
2243
|
-
*
|
|
2518
|
+
* reservation if the on-chain payment fails, and also when the call
|
|
2519
|
+
* completes without paying anything (an x402 endpoint answering a plain
|
|
2520
|
+
* 200 with no challenge). Concurrent pay() calls are serialized through a
|
|
2521
|
+
* per-instance mutex to prevent spend-cap overrun.
|
|
2244
2522
|
*/
|
|
2245
2523
|
async pay(url, options) {
|
|
2246
2524
|
const baseUrl = new URL(url).origin;
|
|
2247
2525
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2248
2526
|
assertEndpointActive(manifest, url, this.logger);
|
|
2249
2527
|
const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
return this._payWithNulthVault(url, manifest, mode);
|
|
2253
|
-
}
|
|
2528
|
+
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2529
|
+
await this._checkTrustline(manifest, selectedAsset);
|
|
2254
2530
|
let amount;
|
|
2255
2531
|
switch (mode) {
|
|
2256
2532
|
case "x402":
|
|
@@ -2270,22 +2546,30 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2270
2546
|
const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
|
|
2271
2547
|
let result;
|
|
2272
2548
|
try {
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2549
|
+
if (this.vault?.mode === "nulth") {
|
|
2550
|
+
result = await this._payWithNulthVault(url, manifest, mode);
|
|
2551
|
+
} else {
|
|
2552
|
+
switch (mode) {
|
|
2553
|
+
case "x402":
|
|
2554
|
+
result = await this.x402.pay(url, manifest);
|
|
2555
|
+
break;
|
|
2556
|
+
case "mpp-charge":
|
|
2557
|
+
result = await this.charge.pay(url, manifest);
|
|
2558
|
+
break;
|
|
2559
|
+
default:
|
|
2560
|
+
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2561
|
+
}
|
|
2282
2562
|
}
|
|
2283
2563
|
} catch (err) {
|
|
2284
2564
|
await this._rollbackSpend(reserveId).catch(() => {
|
|
2285
2565
|
});
|
|
2286
2566
|
throw err;
|
|
2287
2567
|
}
|
|
2288
|
-
|
|
2568
|
+
if (result.txHash === null && result.amount === "0") {
|
|
2569
|
+
await this._rollbackSpend(reserveId);
|
|
2570
|
+
} else {
|
|
2571
|
+
await this._commitSpend(reserveId);
|
|
2572
|
+
}
|
|
2289
2573
|
return result;
|
|
2290
2574
|
}
|
|
2291
2575
|
/** Nulth ZK vault path — proof built off-chain, attached as auth signature */
|
|
@@ -2335,9 +2619,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2335
2619
|
default:
|
|
2336
2620
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2337
2621
|
}
|
|
2622
|
+
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2338
2623
|
return {
|
|
2339
2624
|
amount,
|
|
2340
|
-
asset:
|
|
2625
|
+
asset: selectedAsset,
|
|
2341
2626
|
mode,
|
|
2342
2627
|
manifest,
|
|
2343
2628
|
...manifest.regions && { regions: manifest.regions },
|