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