@routedock/routedock 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -12
- package/dist/{MppSessionHandler-1EL8QBAn.d.cts → MppSessionHandler-3QfCHI5x.d.ts} +40 -2
- package/dist/{MppSessionHandler-Bq4S-JTE.d.ts → MppSessionHandler-xu4Kxu6N.d.cts} +40 -2
- package/dist/{ProviderRegistry-w4_hkI_1.d.ts → ProviderRegistry-D9KqyuH-.d.cts} +16 -36
- package/dist/{ProviderRegistry-CyZ5eryO.d.cts → ProviderRegistry-DVi2uzgk.d.ts} +16 -36
- package/dist/{RouteDockClient-CjM2VyOd.d.ts → RouteDockClient-6rmgkbYb.d.cts} +124 -144
- package/dist/{RouteDockClient-CXvWXix5.d.cts → RouteDockClient-CnAkxwrW.d.ts} +124 -144
- package/dist/{SessionReconciler-Bw-PoBVl.d.ts → SessionReconciler-Cce9MXO1.d.ts} +10 -3
- package/dist/{SessionReconciler-Cz2nuOlJ.d.cts → SessionReconciler-Dpe9qXdD.d.cts} +10 -3
- package/dist/{SessionStore-BaYiegP0.d.ts → SessionStore-DkLBxFQu.d.cts} +28 -3
- package/dist/{SessionStore-B4-Hi4H8.d.cts → SessionStore-_ZkoBzFY.d.ts} +28 -3
- package/dist/SpendStore-BUclV02i.d.ts +73 -0
- package/dist/SpendStore-s_dfcemj.d.cts +73 -0
- package/dist/client.cjs +767 -482
- package/dist/client.d.cts +80 -10
- package/dist/client.d.ts +80 -10
- package/dist/client.js +733 -463
- package/dist/index.cjs +1295 -585
- package/dist/index.d.cts +13 -10
- package/dist/index.d.ts +13 -10
- package/dist/index.js +1247 -554
- package/dist/logger-DVa7VT4c.d.cts +41 -0
- package/dist/logger-DVa7VT4c.d.ts +41 -0
- package/dist/network.cjs +57 -0
- package/dist/network.d.cts +72 -0
- package/dist/network.d.ts +72 -0
- package/dist/network.js +49 -0
- package/dist/node.cjs +166 -0
- package/dist/node.d.cts +77 -0
- package/dist/node.d.ts +77 -0
- package/dist/node.js +159 -0
- package/dist/provider/fastify.cjs +847 -62
- package/dist/provider/fastify.d.cts +10 -3
- package/dist/provider/fastify.d.ts +10 -3
- package/dist/provider/fastify.js +848 -63
- package/dist/provider/hono.cjs +936 -60
- package/dist/provider/hono.d.cts +23 -8
- package/dist/provider/hono.d.ts +23 -8
- package/dist/provider/hono.js +937 -61
- package/dist/provider.cjs +894 -70
- package/dist/provider.d.cts +9 -8
- package/dist/provider.d.ts +9 -8
- package/dist/provider.js +895 -72
- package/dist/react.cjs +736 -431
- package/dist/react.d.cts +7 -5
- package/dist/react.d.ts +7 -5
- package/dist/react.js +725 -420
- package/dist/{routedockMiddleware-57gUaEMf.d.ts → routedockMiddleware-Cq9HOJEa.d.ts} +9 -2
- package/dist/{routedockMiddleware-DlyLrxp7.d.cts → routedockMiddleware-CxsWkSKa.d.cts} +9 -2
- package/dist/schema.cjs +7 -0
- package/dist/schema.js +7 -0
- package/dist/{sign-AKH67sft.d.cts → sign-Bq08SeFq.d.ts} +2 -2
- package/dist/{sign-BEq-OsSq.d.ts → sign-DqGE0Plv.d.cts} +2 -2
- package/dist/testing.d.cts +4 -3
- package/dist/testing.d.ts +4 -3
- package/dist/{logger-D8_Q2ytp.d.cts → types-DXaS5Wip.d.cts} +85 -47
- package/dist/{logger-D8_Q2ytp.d.ts → types-DXaS5Wip.d.ts} +85 -47
- package/package.json +50 -4
package/dist/provider.cjs
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var stellarSdk = require('@stellar/stellar-sdk');
|
|
4
|
+
var nulthSdk = require('@routedock/nulth-sdk');
|
|
5
|
+
var jsonSchema = require('@cfworker/json-schema');
|
|
4
6
|
var facilitator = require('@x402/stellar/exact/facilitator');
|
|
5
7
|
var server$1 = require('@x402/stellar/exact/server');
|
|
6
8
|
var server = require('@x402/core/server');
|
|
7
9
|
var stellar = require('@x402/stellar');
|
|
8
10
|
var http = require('@x402/core/http');
|
|
9
|
-
var nulthSdk = require('@routedock/nulth-sdk');
|
|
10
11
|
var server$4 = require('@stellar/mpp/charge/server');
|
|
11
12
|
var server$2 = require('mppx/server');
|
|
12
13
|
var server$3 = require('@stellar/mpp/channel/server');
|
|
13
14
|
var mppx = require('mppx');
|
|
14
|
-
var
|
|
15
|
+
var sha2_js = require('@noble/hashes/sha2.js');
|
|
15
16
|
|
|
16
17
|
// src/errors.ts
|
|
17
18
|
var RouteDockError = class extends Error {
|
|
@@ -198,13 +199,618 @@ function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
|
|
|
198
199
|
(asset) => asset.asset === assetIdentifier || asset.asset_contract === assetIdentifier
|
|
199
200
|
);
|
|
200
201
|
}
|
|
202
|
+
var usdcToUnits = nulthSdk.usdcToStroops;
|
|
203
|
+
|
|
204
|
+
// src/schemas/routedock.schema.json
|
|
205
|
+
var routedock_schema_default = {
|
|
206
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
207
|
+
$id: "https://routedock.xyz/schemas/routedock.schema.json",
|
|
208
|
+
title: "RouteDock Manifest",
|
|
209
|
+
description: "Discovery manifest served at /.well-known/routedock.json on every RouteDock provider endpoint",
|
|
210
|
+
type: "object",
|
|
211
|
+
required: [
|
|
212
|
+
"routedock",
|
|
213
|
+
"name",
|
|
214
|
+
"description",
|
|
215
|
+
"modes",
|
|
216
|
+
"network",
|
|
217
|
+
"asset",
|
|
218
|
+
"asset_contract",
|
|
219
|
+
"payee",
|
|
220
|
+
"pricing",
|
|
221
|
+
"endpoints",
|
|
222
|
+
"tags"
|
|
223
|
+
],
|
|
224
|
+
additionalProperties: false,
|
|
225
|
+
properties: {
|
|
226
|
+
routedock: {
|
|
227
|
+
type: "string",
|
|
228
|
+
pattern: "^[0-9]+\\.[0-9]+$",
|
|
229
|
+
description: "Manifest schema version, e.g. '1.0', '1.1', '2.0'."
|
|
230
|
+
},
|
|
231
|
+
min_client_version: {
|
|
232
|
+
type: "string",
|
|
233
|
+
pattern: "^[0-9]+\\.[0-9]+$",
|
|
234
|
+
description: "Minimum SDK client version (major.minor) required to use this manifest. Clients below this version must refuse to pay."
|
|
235
|
+
},
|
|
236
|
+
deprecated_modes: {
|
|
237
|
+
type: "array",
|
|
238
|
+
items: {
|
|
239
|
+
$ref: "#/definitions/PaymentMode"
|
|
240
|
+
},
|
|
241
|
+
uniqueItems: true,
|
|
242
|
+
description: "Payment modes still listed in 'modes' for backwards compatibility but deprecated for new integrations."
|
|
243
|
+
},
|
|
244
|
+
sunset_at: {
|
|
245
|
+
type: "string",
|
|
246
|
+
format: "date-time",
|
|
247
|
+
description: "ISO 8601 timestamp after which this manifest version is no longer supported by the provider."
|
|
248
|
+
},
|
|
249
|
+
name: {
|
|
250
|
+
type: "string",
|
|
251
|
+
minLength: 1,
|
|
252
|
+
maxLength: 120,
|
|
253
|
+
description: "Human-readable provider name"
|
|
254
|
+
},
|
|
255
|
+
description: {
|
|
256
|
+
type: "string",
|
|
257
|
+
minLength: 1,
|
|
258
|
+
maxLength: 500,
|
|
259
|
+
description: "Human-readable description of the provider's data or service"
|
|
260
|
+
},
|
|
261
|
+
modes: {
|
|
262
|
+
type: "array",
|
|
263
|
+
minItems: 1,
|
|
264
|
+
uniqueItems: true,
|
|
265
|
+
items: {
|
|
266
|
+
$ref: "#/definitions/PaymentMode"
|
|
267
|
+
},
|
|
268
|
+
description: "Payment modes supported by this provider"
|
|
269
|
+
},
|
|
270
|
+
network: {
|
|
271
|
+
type: "string",
|
|
272
|
+
enum: ["testnet", "mainnet"],
|
|
273
|
+
description: "Stellar network this provider operates on"
|
|
274
|
+
},
|
|
275
|
+
asset: {
|
|
276
|
+
type: "string",
|
|
277
|
+
description: "Asset ticker symbol (e.g. 'USDC')"
|
|
278
|
+
},
|
|
279
|
+
asset_contract: {
|
|
280
|
+
type: "string",
|
|
281
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
282
|
+
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
283
|
+
},
|
|
284
|
+
assets: {
|
|
285
|
+
type: "array",
|
|
286
|
+
minItems: 1,
|
|
287
|
+
items: {
|
|
288
|
+
$ref: "#/definitions/AssetConfig"
|
|
289
|
+
},
|
|
290
|
+
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."
|
|
291
|
+
},
|
|
292
|
+
payee: {
|
|
293
|
+
type: "string",
|
|
294
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
295
|
+
description: "Stellar address (G...) that receives payments"
|
|
296
|
+
},
|
|
297
|
+
pricing: {
|
|
298
|
+
type: "object",
|
|
299
|
+
description: "Pricing configuration per supported payment mode",
|
|
300
|
+
additionalProperties: false,
|
|
301
|
+
properties: {
|
|
302
|
+
x402: {
|
|
303
|
+
$ref: "#/definitions/PricingConfig"
|
|
304
|
+
},
|
|
305
|
+
"mpp-charge": {
|
|
306
|
+
$ref: "#/definitions/PricingConfig"
|
|
307
|
+
},
|
|
308
|
+
"mpp-session": {
|
|
309
|
+
$ref: "#/definitions/SessionPricingConfig"
|
|
310
|
+
},
|
|
311
|
+
"mpp-session-ws": {
|
|
312
|
+
$ref: "#/definitions/SessionPricingConfig",
|
|
313
|
+
description: "WebSocket transport variant of mpp-session: channel opened and first voucher negotiated over HTTP, then the connection upgrades to WebSocket for push-based streaming."
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
minProperties: 1
|
|
317
|
+
},
|
|
318
|
+
endpoints: {
|
|
319
|
+
type: "object",
|
|
320
|
+
description: "Map of endpoint name to endpoint descriptors",
|
|
321
|
+
additionalProperties: {
|
|
322
|
+
$ref: "#/definitions/EndpointDescriptor"
|
|
323
|
+
},
|
|
324
|
+
minProperties: 1
|
|
325
|
+
},
|
|
326
|
+
sla: {
|
|
327
|
+
$ref: "#/definitions/SLAConfig"
|
|
328
|
+
},
|
|
329
|
+
tags: {
|
|
330
|
+
type: "array",
|
|
331
|
+
items: {
|
|
332
|
+
type: "string",
|
|
333
|
+
minLength: 1,
|
|
334
|
+
maxLength: 64
|
|
335
|
+
},
|
|
336
|
+
uniqueItems: true,
|
|
337
|
+
description: "Capability tags indexed with trigram search in the provider registry"
|
|
338
|
+
},
|
|
339
|
+
vault: {
|
|
340
|
+
$ref: "#/definitions/VaultMode"
|
|
341
|
+
},
|
|
342
|
+
nulth_account: {
|
|
343
|
+
type: "string",
|
|
344
|
+
description: "Optional Nulth smart account address associated with this provider"
|
|
345
|
+
},
|
|
346
|
+
capabilities: {
|
|
347
|
+
type: "object",
|
|
348
|
+
description: "Optional protocol features this provider supports",
|
|
349
|
+
properties: {
|
|
350
|
+
streaming: {
|
|
351
|
+
type: "array",
|
|
352
|
+
items: {
|
|
353
|
+
type: "string",
|
|
354
|
+
enum: ["sse", "websocket"]
|
|
355
|
+
},
|
|
356
|
+
description: "Supported streaming transports"
|
|
357
|
+
},
|
|
358
|
+
webhooks: {
|
|
359
|
+
type: "boolean",
|
|
360
|
+
description: "Whether webhook callbacks are supported"
|
|
361
|
+
},
|
|
362
|
+
idempotency_keys: {
|
|
363
|
+
type: "boolean",
|
|
364
|
+
description: "Whether idempotency keys are supported"
|
|
365
|
+
},
|
|
366
|
+
content_types: {
|
|
367
|
+
type: "array",
|
|
368
|
+
items: {
|
|
369
|
+
type: "string"
|
|
370
|
+
},
|
|
371
|
+
description: "Supported content types for request/response"
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
additionalProperties: false
|
|
375
|
+
},
|
|
376
|
+
regions: {
|
|
377
|
+
type: "array",
|
|
378
|
+
items: {
|
|
379
|
+
type: "string",
|
|
380
|
+
minLength: 2,
|
|
381
|
+
maxLength: 10
|
|
382
|
+
},
|
|
383
|
+
uniqueItems: true,
|
|
384
|
+
description: "IATA airport codes or UN/LOCODE identifiers for regions where this provider has infrastructure (e.g. ['IAD', 'AMS']). Used by agents to select the nearest provider."
|
|
385
|
+
},
|
|
386
|
+
latency_hints: {
|
|
387
|
+
type: "object",
|
|
388
|
+
additionalProperties: {
|
|
389
|
+
type: "number",
|
|
390
|
+
minimum: 0
|
|
391
|
+
},
|
|
392
|
+
description: "Provider-declared p50 latency in milliseconds per region code. Keys must be a subset of 'regions'. Example: { 'IAD': 14, 'AMS': 22 }."
|
|
393
|
+
},
|
|
394
|
+
signature: {
|
|
395
|
+
type: "string",
|
|
396
|
+
description: "Base64 Ed25519 signature (payee keypair) over the SHA-256 hash of this manifest with the signature field omitted. Clients must verify before trusting any routing field."
|
|
397
|
+
},
|
|
398
|
+
signature_version: {
|
|
399
|
+
type: "string",
|
|
400
|
+
const: "2",
|
|
401
|
+
description: "Manifest signature format. Version 2 recursively includes every nested manifest field in the signed digest."
|
|
402
|
+
},
|
|
403
|
+
categories: {
|
|
404
|
+
type: "array",
|
|
405
|
+
items: {
|
|
406
|
+
type: "string",
|
|
407
|
+
minLength: 1,
|
|
408
|
+
maxLength: 128
|
|
409
|
+
},
|
|
410
|
+
uniqueItems: true,
|
|
411
|
+
description: "Optional hierarchical categories from a published taxonomy (e.g. data/price/crypto)"
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
definitions: {
|
|
415
|
+
VaultMode: {
|
|
416
|
+
type: "string",
|
|
417
|
+
enum: ["local-key", "agent-vault", "nulth"],
|
|
418
|
+
description: "Agent vault / custody mode for payments from this provider"
|
|
419
|
+
},
|
|
420
|
+
PaymentMode: {
|
|
421
|
+
type: "string",
|
|
422
|
+
enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
|
|
423
|
+
description: "A supported RouteDock payment mode"
|
|
424
|
+
},
|
|
425
|
+
AssetConfig: {
|
|
426
|
+
type: "object",
|
|
427
|
+
description: "Payment asset configuration with optional mode and endpoint scoping",
|
|
428
|
+
required: ["asset", "asset_contract"],
|
|
429
|
+
additionalProperties: false,
|
|
430
|
+
properties: {
|
|
431
|
+
asset: {
|
|
432
|
+
type: "string",
|
|
433
|
+
description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
|
|
434
|
+
},
|
|
435
|
+
asset_contract: {
|
|
436
|
+
type: "string",
|
|
437
|
+
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
438
|
+
},
|
|
439
|
+
modes: {
|
|
440
|
+
type: "array",
|
|
441
|
+
items: {
|
|
442
|
+
$ref: "#/definitions/PaymentMode"
|
|
443
|
+
},
|
|
444
|
+
uniqueItems: true,
|
|
445
|
+
description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
|
|
446
|
+
},
|
|
447
|
+
endpoints: {
|
|
448
|
+
type: "array",
|
|
449
|
+
items: {
|
|
450
|
+
type: "string"
|
|
451
|
+
},
|
|
452
|
+
uniqueItems: true,
|
|
453
|
+
description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
PricingConfig: {
|
|
458
|
+
type: "object",
|
|
459
|
+
description: "Per-request pricing for x402 or mpp-charge modes",
|
|
460
|
+
required: ["amount", "per"],
|
|
461
|
+
additionalProperties: false,
|
|
462
|
+
properties: {
|
|
463
|
+
amount: {
|
|
464
|
+
type: "string",
|
|
465
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
466
|
+
description: "Cost per request in the payment asset (decimal string, e.g. '0.001')"
|
|
467
|
+
},
|
|
468
|
+
per: {
|
|
469
|
+
type: "string",
|
|
470
|
+
const: "request",
|
|
471
|
+
description: "Billing unit \u2014 always 'request' for non-session modes"
|
|
472
|
+
},
|
|
473
|
+
facilitator: {
|
|
474
|
+
type: "string",
|
|
475
|
+
description: "x402 facilitator URL (required for x402 mode). OpenZeppelin Channels endpoint."
|
|
476
|
+
},
|
|
477
|
+
payee: {
|
|
478
|
+
type: "string",
|
|
479
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
480
|
+
description: "Optional per-mode payee override (Stellar G... address). Directs settlements for this mode to a separate account for treasury separation. Falls back to the top-level 'payee' when omitted."
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
SessionPricingConfig: {
|
|
485
|
+
type: "object",
|
|
486
|
+
description: "Per-voucher pricing for mpp-session (one-way-channel) mode",
|
|
487
|
+
required: ["rate", "per", "channel_factory", "min_deposit", "refund_waiting_period_ledgers"],
|
|
488
|
+
additionalProperties: false,
|
|
489
|
+
properties: {
|
|
490
|
+
rate: {
|
|
491
|
+
type: "string",
|
|
492
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
493
|
+
description: "Cost per voucher in the payment asset (decimal string, e.g. '0.0001')"
|
|
494
|
+
},
|
|
495
|
+
per: {
|
|
496
|
+
type: "string",
|
|
497
|
+
const: "voucher",
|
|
498
|
+
description: "Billing unit \u2014 always 'voucher' for session mode"
|
|
499
|
+
},
|
|
500
|
+
channel_factory: {
|
|
501
|
+
type: "string",
|
|
502
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
503
|
+
description: "Stable Soroban factory address (C...) used to deploy a per-session channel"
|
|
504
|
+
},
|
|
505
|
+
min_deposit: {
|
|
506
|
+
type: "string",
|
|
507
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
508
|
+
description: "Minimum initial deposit to open a channel (decimal string)"
|
|
509
|
+
},
|
|
510
|
+
refund_waiting_period_ledgers: {
|
|
511
|
+
type: "integer",
|
|
512
|
+
minimum: 1,
|
|
513
|
+
description: "Ledgers the server has to settle before the funder can reclaim. Default: 17280 (~24h). Do not reduce in production."
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
SLAConfig: {
|
|
518
|
+
type: "object",
|
|
519
|
+
description: "Service Level Agreement for the provider",
|
|
520
|
+
required: ["uptime_30d_percent", "p95_latency_ms"],
|
|
521
|
+
additionalProperties: false,
|
|
522
|
+
properties: {
|
|
523
|
+
uptime_30d_percent: {
|
|
524
|
+
type: "number",
|
|
525
|
+
description: "Uptime percentage over the last 30 days"
|
|
526
|
+
},
|
|
527
|
+
p95_latency_ms: {
|
|
528
|
+
type: "number",
|
|
529
|
+
description: "P95 latency in milliseconds"
|
|
530
|
+
},
|
|
531
|
+
maintenance_windows: {
|
|
532
|
+
type: "array",
|
|
533
|
+
items: {
|
|
534
|
+
type: "object",
|
|
535
|
+
required: ["cron", "duration_minutes"],
|
|
536
|
+
additionalProperties: false,
|
|
537
|
+
properties: {
|
|
538
|
+
cron: {
|
|
539
|
+
type: "string",
|
|
540
|
+
description: "Cron expression for the maintenance window"
|
|
541
|
+
},
|
|
542
|
+
duration_minutes: {
|
|
543
|
+
type: "number",
|
|
544
|
+
description: "Duration of the maintenance window in minutes"
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
EndpointDescriptor: {
|
|
552
|
+
type: "object",
|
|
553
|
+
description: "Descriptor for a provider endpoint",
|
|
554
|
+
required: ["method", "path"],
|
|
555
|
+
additionalProperties: false,
|
|
556
|
+
properties: {
|
|
557
|
+
method: {
|
|
558
|
+
type: "string",
|
|
559
|
+
description: "HTTP method for the endpoint"
|
|
560
|
+
},
|
|
561
|
+
path: {
|
|
562
|
+
type: "string",
|
|
563
|
+
description: "Path for the endpoint"
|
|
564
|
+
},
|
|
565
|
+
deprecated: {
|
|
566
|
+
type: "boolean",
|
|
567
|
+
description: "Whether this endpoint is retained only for backwards compatibility"
|
|
568
|
+
},
|
|
569
|
+
sunset_at: {
|
|
570
|
+
type: "string",
|
|
571
|
+
format: "date-time",
|
|
572
|
+
description: "ISO 8601 timestamp after which callers should stop using this endpoint"
|
|
573
|
+
},
|
|
574
|
+
headers: {
|
|
575
|
+
type: "object",
|
|
576
|
+
additionalProperties: {
|
|
577
|
+
type: "string"
|
|
578
|
+
},
|
|
579
|
+
description: "Expected request headers"
|
|
580
|
+
},
|
|
581
|
+
request_schema: {
|
|
582
|
+
description: "JSON schema for the request body"
|
|
583
|
+
},
|
|
584
|
+
response_schema: {
|
|
585
|
+
description: "JSON schema for the response body"
|
|
586
|
+
},
|
|
587
|
+
rate_limit: {
|
|
588
|
+
type: "object",
|
|
589
|
+
required: ["requests", "window_seconds"],
|
|
590
|
+
additionalProperties: false,
|
|
591
|
+
properties: {
|
|
592
|
+
requests: {
|
|
593
|
+
type: "number"
|
|
594
|
+
},
|
|
595
|
+
window_seconds: {
|
|
596
|
+
type: "number"
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// src/manifest/validate.ts
|
|
606
|
+
var validator = new jsonSchema.Validator(routedock_schema_default, "7");
|
|
607
|
+
function validateManifestSchema(manifest, source) {
|
|
608
|
+
const result = validator.validate(manifest);
|
|
609
|
+
if (!result.valid) {
|
|
610
|
+
const msgs = result.errors.map((err) => err.error).join("; ");
|
|
611
|
+
throw new RouteDockManifestError(`Invalid manifest${""}: ${msgs}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function assertManifestValid(manifest, baseUrl) {
|
|
615
|
+
const context = "";
|
|
616
|
+
if (manifest.latency_hints !== void 0) {
|
|
617
|
+
if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
|
|
618
|
+
throw new RouteDockManifestError(
|
|
619
|
+
`Invalid manifest${context}: latency_hints defined without declaring regions`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
const regionSet = new Set(manifest.regions);
|
|
623
|
+
for (const region of Object.keys(manifest.latency_hints)) {
|
|
624
|
+
if (!regionSet.has(region)) {
|
|
625
|
+
throw new RouteDockManifestError(
|
|
626
|
+
`Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (manifest.assets !== void 0) {
|
|
632
|
+
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
633
|
+
throw new RouteDockManifestError(
|
|
634
|
+
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
for (let i = 0; i < manifest.assets.length; i++) {
|
|
638
|
+
const a = manifest.assets[i];
|
|
639
|
+
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
640
|
+
throw new RouteDockManifestError(
|
|
641
|
+
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const first = manifest.assets[0];
|
|
646
|
+
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
647
|
+
throw new RouteDockManifestError(
|
|
648
|
+
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
assertPricingMatchesModes(manifest, context);
|
|
653
|
+
assertDeprecatedModesListed(manifest, context);
|
|
654
|
+
assertPricesParse(manifest, context);
|
|
655
|
+
}
|
|
656
|
+
function assertPricingMatchesModes(manifest, context) {
|
|
657
|
+
const pricing = manifest.pricing ?? {};
|
|
658
|
+
for (const mode of manifest.modes ?? []) {
|
|
659
|
+
if (pricing[mode] === void 0 || pricing[mode] === null) {
|
|
660
|
+
throw new RouteDockManifestError(
|
|
661
|
+
`Invalid manifest${context}: mode '${mode}' is listed in modes but pricing.${mode} is missing`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function assertDeprecatedModesListed(manifest, context) {
|
|
667
|
+
if (manifest.deprecated_modes === void 0) return;
|
|
668
|
+
const modes = new Set(manifest.modes ?? []);
|
|
669
|
+
for (const mode of manifest.deprecated_modes) {
|
|
670
|
+
if (!modes.has(mode)) {
|
|
671
|
+
throw new RouteDockManifestError(
|
|
672
|
+
`Invalid manifest${context}: deprecated_modes entry '${mode}' is not listed in modes`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
var PRICE_FIELDS = {
|
|
678
|
+
x402: ["amount"],
|
|
679
|
+
"mpp-charge": ["amount"],
|
|
680
|
+
"mpp-session": ["rate", "min_deposit"],
|
|
681
|
+
"mpp-session-ws": ["rate", "min_deposit"]
|
|
682
|
+
};
|
|
683
|
+
function assertPricesParse(manifest, context) {
|
|
684
|
+
const pricing = manifest.pricing ?? {};
|
|
685
|
+
for (const [mode, fields] of Object.entries(PRICE_FIELDS)) {
|
|
686
|
+
const entry = pricing[mode];
|
|
687
|
+
if (entry === void 0 || entry === null) continue;
|
|
688
|
+
for (const field of fields) {
|
|
689
|
+
const value = entry[field];
|
|
690
|
+
try {
|
|
691
|
+
if (typeof value !== "string") {
|
|
692
|
+
throw new RangeError(`expected a decimal string, got ${typeof value}`);
|
|
693
|
+
}
|
|
694
|
+
nulthSdk.usdcToStroops(value);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
697
|
+
throw new RouteDockManifestError(
|
|
698
|
+
`Invalid manifest${context}: pricing.${mode}.${field} ${JSON.stringify(value)} is not a valid amount (${detail})`,
|
|
699
|
+
{ cause: err }
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/provider/internal/assertProviderConfig.ts
|
|
707
|
+
var FLAT_MODES = ["x402", "mpp-charge"];
|
|
708
|
+
var SESSION_MODES = ["mpp-session", "mpp-session-ws"];
|
|
709
|
+
function sameAmount(adapter, label, optionValue, manifestValue) {
|
|
710
|
+
const parse = (where, value) => {
|
|
711
|
+
try {
|
|
712
|
+
return nulthSdk.usdcToStroops(value);
|
|
713
|
+
} catch {
|
|
714
|
+
throw new RouteDockManifestError(
|
|
715
|
+
`${adapter}: ${label} has an unparseable ${where} amount "${value}"`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
return parse("option", optionValue) === parse("manifest", manifestValue);
|
|
720
|
+
}
|
|
721
|
+
function assertProviderConfig(opts, adapterName) {
|
|
722
|
+
const { manifest } = opts;
|
|
723
|
+
if (manifest === null || typeof manifest !== "object") {
|
|
724
|
+
throw new RouteDockManifestError(`${adapterName}: opts.manifest is required and must be an object`);
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
validateManifestSchema(manifest);
|
|
728
|
+
assertManifestValid(manifest);
|
|
729
|
+
} catch (err) {
|
|
730
|
+
if (err instanceof RouteDockManifestError) {
|
|
731
|
+
throw new RouteDockManifestError(`${adapterName}: ${err.message}`, { cause: err });
|
|
732
|
+
}
|
|
733
|
+
throw err;
|
|
734
|
+
}
|
|
735
|
+
const served = new Set(opts.modes.filter((mode) => Boolean(opts.pricing[mode])));
|
|
736
|
+
const advertised = new Set(manifest.modes);
|
|
737
|
+
const notAdvertised = [...served].filter((mode) => !advertised.has(mode));
|
|
738
|
+
const notServed = [...advertised].filter((mode) => !served.has(mode));
|
|
739
|
+
if (notAdvertised.length > 0 || notServed.length > 0) {
|
|
740
|
+
const parts = [];
|
|
741
|
+
if (notAdvertised.length > 0) parts.push(`served but not in manifest.modes: ${notAdvertised.join(", ")}`);
|
|
742
|
+
if (notServed.length > 0) parts.push(`in manifest.modes but not served (missing from modes or pricing): ${notServed.join(", ")}`);
|
|
743
|
+
throw new RouteDockManifestError(`${adapterName}: modes do not match manifest.modes (${parts.join("; ")})`);
|
|
744
|
+
}
|
|
745
|
+
for (const mode of FLAT_MODES) {
|
|
746
|
+
if (!served.has(mode)) continue;
|
|
747
|
+
const optionValue = opts.pricing[mode];
|
|
748
|
+
const manifestValue = manifest.pricing[mode]?.amount;
|
|
749
|
+
if (manifestValue === void 0) {
|
|
750
|
+
throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode}.amount is missing but ${mode} is served`);
|
|
751
|
+
}
|
|
752
|
+
if (!sameAmount(adapterName, `pricing.${mode}`, optionValue, manifestValue)) {
|
|
753
|
+
throw new RouteDockManifestError(
|
|
754
|
+
`${adapterName}: pricing.${mode} "${optionValue}" does not match manifest.pricing.${mode}.amount "${manifestValue}"`
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
for (const mode of SESSION_MODES) {
|
|
759
|
+
if (!served.has(mode)) continue;
|
|
760
|
+
const optionValue = opts.pricing[mode];
|
|
761
|
+
const manifestValue = manifest.pricing[mode];
|
|
762
|
+
if (!manifestValue) {
|
|
763
|
+
throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode} is missing but ${mode} is served`);
|
|
764
|
+
}
|
|
765
|
+
if (!sameAmount(adapterName, `pricing.${mode}.rate`, optionValue.rate, manifestValue.rate)) {
|
|
766
|
+
throw new RouteDockManifestError(
|
|
767
|
+
`${adapterName}: pricing.${mode}.rate "${optionValue.rate}" does not match manifest.pricing.${mode}.rate "${manifestValue.rate}"`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
if (optionValue.channelFactory !== manifestValue.channel_factory) {
|
|
771
|
+
throw new RouteDockManifestError(
|
|
772
|
+
`${adapterName}: pricing.${mode}.channelFactory "${optionValue.channelFactory}" does not match manifest.pricing.${mode}.channel_factory "${manifestValue.channel_factory}"`
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (opts.assetContract !== void 0 && opts.assetContract !== manifest.asset_contract) {
|
|
777
|
+
throw new RouteDockManifestError(
|
|
778
|
+
`${adapterName}: assetContract "${opts.assetContract}" does not match manifest.asset_contract "${manifest.asset_contract}"`
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
if (opts.asset !== void 0 && opts.asset !== manifest.asset) {
|
|
782
|
+
throw new RouteDockManifestError(
|
|
783
|
+
`${adapterName}: asset "${opts.asset}" does not match manifest.asset "${manifest.asset}"`
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
if (opts.network !== manifest.network) {
|
|
787
|
+
throw new RouteDockManifestError(
|
|
788
|
+
`${adapterName}: network "${opts.network}" does not match manifest.network "${manifest.network}"`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
let signerPublicKey;
|
|
792
|
+
try {
|
|
793
|
+
signerPublicKey = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey).publicKey();
|
|
794
|
+
} catch (err) {
|
|
795
|
+
throw new RouteDockManifestError(`${adapterName}: payeeSecretKey is not a valid Stellar secret key`, { cause: err });
|
|
796
|
+
}
|
|
797
|
+
if (signerPublicKey !== manifest.payee) {
|
|
798
|
+
throw new RouteDockManifestError(
|
|
799
|
+
`${adapterName}: payeeSecretKey belongs to ${signerPublicKey}, not manifest.payee ${manifest.payee}; clients would reject the signed manifest`
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
if (opts.payee !== manifest.payee) {
|
|
803
|
+
throw new RouteDockManifestError(
|
|
804
|
+
`${adapterName}: payee "${opts.payee}" does not match manifest.payee "${manifest.payee}"`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
201
808
|
|
|
202
809
|
// src/internal/payee.ts
|
|
203
810
|
function resolvePayee(manifest, mode) {
|
|
204
811
|
const override = manifest.pricing[mode]?.payee;
|
|
205
812
|
return override ?? manifest.payee;
|
|
206
813
|
}
|
|
207
|
-
var usdcToUnits = nulthSdk.usdcToStroops;
|
|
208
814
|
function extractPayerAddress(key) {
|
|
209
815
|
if (typeof key !== "string") return null;
|
|
210
816
|
return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
@@ -229,6 +835,18 @@ function resolveLogger(logger, fallback = consoleLogger) {
|
|
|
229
835
|
// src/provider/SeenTxStore.ts
|
|
230
836
|
var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
|
|
231
837
|
var MAX_SETTLEMENT_REPLAYS = 1;
|
|
838
|
+
async function recordSettlement(store, key, record, ctx) {
|
|
839
|
+
try {
|
|
840
|
+
await store.set(key, record);
|
|
841
|
+
} catch (err) {
|
|
842
|
+
ctx.logger("error", `${ctx.label} seenTxStore.set failed after settlement`, { error: err });
|
|
843
|
+
try {
|
|
844
|
+
ctx.onCallbackError?.(err, "seenTxStore.set");
|
|
845
|
+
} catch (cbErr) {
|
|
846
|
+
ctx.logger("error", `${ctx.label} onCallbackError threw`, { error: cbErr });
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
232
850
|
async function checkSettlementReplay(store, key, now = Date.now()) {
|
|
233
851
|
const record = await store.get(key);
|
|
234
852
|
if (!record) return { kind: "miss" };
|
|
@@ -296,7 +914,7 @@ async function paymentIdempotencyKey(getHeader, scope) {
|
|
|
296
914
|
);
|
|
297
915
|
}
|
|
298
916
|
|
|
299
|
-
// src/
|
|
917
|
+
// src/internal/settleResult.ts
|
|
300
918
|
function readSettleResult(result) {
|
|
301
919
|
const settle = result;
|
|
302
920
|
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
@@ -446,10 +1064,10 @@ function createX402Handler(opts) {
|
|
|
446
1064
|
requirements
|
|
447
1065
|
);
|
|
448
1066
|
if (!verifyResult.isValid) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
1067
|
+
await respondPaymentRequired(
|
|
1068
|
+
"Payment verification failed",
|
|
1069
|
+
verifyResult.invalidReason
|
|
1070
|
+
);
|
|
453
1071
|
return;
|
|
454
1072
|
}
|
|
455
1073
|
const settleResult = await localFacilitator.settle(
|
|
@@ -475,7 +1093,12 @@ function createX402Handler(opts) {
|
|
|
475
1093
|
if (typeof paymentResponse === "string") {
|
|
476
1094
|
headers["X-Payment-Response"] = paymentResponse;
|
|
477
1095
|
}
|
|
478
|
-
await
|
|
1096
|
+
await recordSettlement(
|
|
1097
|
+
seenTxStore,
|
|
1098
|
+
idempotencyKey,
|
|
1099
|
+
{ txHash, headers, createdAt: Date.now() },
|
|
1100
|
+
{ logger, label: "[x402]", onCallbackError: opts.onCallbackError }
|
|
1101
|
+
);
|
|
479
1102
|
}
|
|
480
1103
|
if (txHash && opts.onSettled) {
|
|
481
1104
|
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
@@ -494,6 +1117,51 @@ function createX402Handler(opts) {
|
|
|
494
1117
|
}
|
|
495
1118
|
};
|
|
496
1119
|
}
|
|
1120
|
+
|
|
1121
|
+
// src/internal/encoding.ts
|
|
1122
|
+
var encoder = new TextEncoder();
|
|
1123
|
+
var decoder = new TextDecoder();
|
|
1124
|
+
function utf8ToBytes(text) {
|
|
1125
|
+
return encoder.encode(text);
|
|
1126
|
+
}
|
|
1127
|
+
function bytesToUtf8(bytes) {
|
|
1128
|
+
return decoder.decode(bytes);
|
|
1129
|
+
}
|
|
1130
|
+
function bytesToBase64(bytes) {
|
|
1131
|
+
const chunk = 32768;
|
|
1132
|
+
let binary = "";
|
|
1133
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
1134
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
1135
|
+
}
|
|
1136
|
+
return btoa(binary);
|
|
1137
|
+
}
|
|
1138
|
+
function base64ToBytes(b64) {
|
|
1139
|
+
const end = b64.indexOf("=");
|
|
1140
|
+
let clean = (end === -1 ? b64 : b64.slice(0, end)).replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
|
|
1141
|
+
if (clean.length % 4 === 1) clean = clean.slice(0, -1);
|
|
1142
|
+
const binary = atob(clean);
|
|
1143
|
+
const bytes = new Uint8Array(binary.length);
|
|
1144
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1145
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1146
|
+
}
|
|
1147
|
+
return bytes;
|
|
1148
|
+
}
|
|
1149
|
+
function base64ToUtf8(b64) {
|
|
1150
|
+
return bytesToUtf8(base64ToBytes(b64));
|
|
1151
|
+
}
|
|
1152
|
+
function hexToBytes(hex) {
|
|
1153
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
1154
|
+
if (clean.length % 2 !== 0) {
|
|
1155
|
+
throw new Error("hexToBytes: hex string must have an even length");
|
|
1156
|
+
}
|
|
1157
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
1158
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1159
|
+
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
1160
|
+
}
|
|
1161
|
+
return bytes;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// src/provider/MppChargeHandler.ts
|
|
497
1165
|
var MPP_NETWORK = {
|
|
498
1166
|
testnet: "stellar:testnet",
|
|
499
1167
|
mainnet: "stellar:pubnet"
|
|
@@ -540,7 +1208,7 @@ function createMppChargeHandler(opts) {
|
|
|
540
1208
|
const credPart = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
|
|
541
1209
|
if (credPart) {
|
|
542
1210
|
const b64 = credPart.split("=").slice(1).join("=").replace(/^"|"$/g, "");
|
|
543
|
-
const credJson =
|
|
1211
|
+
const credJson = base64ToUtf8(b64);
|
|
544
1212
|
const cred = JSON.parse(credJson);
|
|
545
1213
|
const key = cred.sender ?? cred.payload?.sender ?? cred.payload?.from;
|
|
546
1214
|
payerAddress = extractPayerAddress(key);
|
|
@@ -602,18 +1270,19 @@ function createMppChargeHandler(opts) {
|
|
|
602
1270
|
if (receiptHeader) {
|
|
603
1271
|
try {
|
|
604
1272
|
const parsed = JSON.parse(
|
|
605
|
-
|
|
1273
|
+
base64ToUtf8(receiptHeader)
|
|
606
1274
|
);
|
|
607
1275
|
reference = parsed.reference;
|
|
608
1276
|
} catch {
|
|
609
1277
|
}
|
|
610
1278
|
}
|
|
611
1279
|
if (idempotencyKey) {
|
|
612
|
-
await
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
createdAt: Date.now()
|
|
616
|
-
|
|
1280
|
+
await recordSettlement(
|
|
1281
|
+
seenTxStore,
|
|
1282
|
+
idempotencyKey,
|
|
1283
|
+
{ txHash: reference ?? null, headers: receiptHeaders, createdAt: Date.now() },
|
|
1284
|
+
{ logger, label: "[mpp-charge]", onCallbackError: opts.onCallbackError }
|
|
1285
|
+
);
|
|
617
1286
|
}
|
|
618
1287
|
if (reference && opts.onSettled) {
|
|
619
1288
|
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
@@ -627,6 +1296,14 @@ function createMppChargeHandler(opts) {
|
|
|
627
1296
|
}
|
|
628
1297
|
};
|
|
629
1298
|
}
|
|
1299
|
+
|
|
1300
|
+
// src/provider/internal/vaultRpc.ts
|
|
1301
|
+
async function createSorobanRpcServer(rpcUrl) {
|
|
1302
|
+
const { Server } = await import('@stellar/stellar-sdk/rpc');
|
|
1303
|
+
return new Server(rpcUrl);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/provider/internal/vaultSettlement.ts
|
|
630
1307
|
function resolveVaultSettlementAddresses(payer, payee) {
|
|
631
1308
|
if (!payer) return null;
|
|
632
1309
|
return { payer, payee };
|
|
@@ -646,6 +1323,111 @@ function buildSessionSettlementTransaction(source, vaultContractId, channelId, a
|
|
|
646
1323
|
networkPassphrase
|
|
647
1324
|
}).addOperation(op).setTimeout(30).build();
|
|
648
1325
|
}
|
|
1326
|
+
var DEFAULT_RPC_URLS = {
|
|
1327
|
+
testnet: "https://soroban-testnet.stellar.org",
|
|
1328
|
+
mainnet: "https://mainnet.sorobanrpc.com"
|
|
1329
|
+
};
|
|
1330
|
+
var CONFIRM_TIMEOUT_MS = 12e4;
|
|
1331
|
+
var CONFIRM_POLL_MS = 1e3;
|
|
1332
|
+
function delay(ms) {
|
|
1333
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1334
|
+
}
|
|
1335
|
+
function describeSendError(sent) {
|
|
1336
|
+
const errorResult = sent.errorResult;
|
|
1337
|
+
try {
|
|
1338
|
+
const name = errorResult?.result?.().switch?.().name;
|
|
1339
|
+
if (name) return `sendTransaction returned ERROR: ${name}`;
|
|
1340
|
+
} catch {
|
|
1341
|
+
}
|
|
1342
|
+
return `sendTransaction returned status ${sent.status}`;
|
|
1343
|
+
}
|
|
1344
|
+
async function waitForConfirmation(server, txHash) {
|
|
1345
|
+
const deadline = Date.now() + CONFIRM_TIMEOUT_MS;
|
|
1346
|
+
for (; ; ) {
|
|
1347
|
+
const tx = await server.getTransaction(txHash);
|
|
1348
|
+
if (tx.status === "SUCCESS") return "success";
|
|
1349
|
+
if (tx.status === "FAILED") return "failed";
|
|
1350
|
+
if (Date.now() >= deadline) return "timeout";
|
|
1351
|
+
await delay(CONFIRM_POLL_MS);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
async function recordSessionSettlement(request, { logger, onCallbackError }) {
|
|
1355
|
+
const reportFailure = (fields) => {
|
|
1356
|
+
logger("error", "[vault] failed to record session_settled", {
|
|
1357
|
+
channelId: request.channelId,
|
|
1358
|
+
...fields.txHash ? { txHash: fields.txHash } : {},
|
|
1359
|
+
reason: fields.reason,
|
|
1360
|
+
...fields.error !== void 0 ? { error: fields.error } : {}
|
|
1361
|
+
});
|
|
1362
|
+
if (onCallbackError) {
|
|
1363
|
+
const err = fields.error ?? new Error(`session_settled vault record failed: ${fields.reason}`);
|
|
1364
|
+
onCallbackError(err, "vaultSettlement");
|
|
1365
|
+
}
|
|
1366
|
+
return { ok: false, reason: fields.reason, ...fields.txHash ? { txHash: fields.txHash } : {} };
|
|
1367
|
+
};
|
|
1368
|
+
try {
|
|
1369
|
+
const payeeKeypair = stellarSdk.Keypair.fromSecret(request.payeeSecretKey);
|
|
1370
|
+
const addresses = resolveVaultSettlementAddresses(request.payer, payeeKeypair.publicKey());
|
|
1371
|
+
if (!addresses) {
|
|
1372
|
+
logger("warn", "[vault] skipped session_settled record: payer address unavailable", {
|
|
1373
|
+
channelId: request.channelId
|
|
1374
|
+
});
|
|
1375
|
+
return { ok: false, reason: "payer address unavailable" };
|
|
1376
|
+
}
|
|
1377
|
+
const networkPassphrase = request.network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
|
|
1378
|
+
const rpcUrl = request.config.rpcUrl ?? DEFAULT_RPC_URLS[request.network];
|
|
1379
|
+
const server = await createSorobanRpcServer(rpcUrl);
|
|
1380
|
+
const sourceAccount = await server.getAccount(payeeKeypair.publicKey());
|
|
1381
|
+
const tx = buildSessionSettlementTransaction(
|
|
1382
|
+
sourceAccount,
|
|
1383
|
+
request.config.contract,
|
|
1384
|
+
request.channelId,
|
|
1385
|
+
addresses,
|
|
1386
|
+
request.amount,
|
|
1387
|
+
request.voucherCount,
|
|
1388
|
+
networkPassphrase
|
|
1389
|
+
);
|
|
1390
|
+
const preparedTx = await server.prepareTransaction(tx);
|
|
1391
|
+
preparedTx.sign(payeeKeypair);
|
|
1392
|
+
const sent = await server.sendTransaction(preparedTx);
|
|
1393
|
+
if (sent.status === "ERROR") {
|
|
1394
|
+
return reportFailure({
|
|
1395
|
+
txHash: sent.hash || preparedTx.hash().toString("hex"),
|
|
1396
|
+
reason: describeSendError(sent)
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
const txHash = sent.hash || preparedTx.hash().toString("hex");
|
|
1400
|
+
const confirmation = await waitForConfirmation(server, txHash);
|
|
1401
|
+
if (confirmation === "success") {
|
|
1402
|
+
logger("debug", "[vault] recorded session_settled", {
|
|
1403
|
+
channelId: request.channelId,
|
|
1404
|
+
txHash
|
|
1405
|
+
});
|
|
1406
|
+
return { ok: true, txHash };
|
|
1407
|
+
}
|
|
1408
|
+
return reportFailure({
|
|
1409
|
+
txHash,
|
|
1410
|
+
reason: confirmation === "failed" ? "transaction failed on-chain" : `confirmation timed out after ${CONFIRM_TIMEOUT_MS}ms`
|
|
1411
|
+
});
|
|
1412
|
+
} catch (err) {
|
|
1413
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1414
|
+
return reportFailure({ reason, error: err });
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/internal/vaultConfig.ts
|
|
1419
|
+
function resolveVaultSettlementConfig(option, logger) {
|
|
1420
|
+
if (option) return option;
|
|
1421
|
+
if (typeof process === "undefined") return void 0;
|
|
1422
|
+
const fromEnv = process.env.AGENT_VAULT_CONTRACT ?? process.env.AGENT_VAULT_CONTRACT_ID;
|
|
1423
|
+
if (!fromEnv) return void 0;
|
|
1424
|
+
logger(
|
|
1425
|
+
"warn",
|
|
1426
|
+
"[vault] configuring the vault contract via the AGENT_VAULT_CONTRACT/AGENT_VAULT_CONTRACT_ID environment variables is deprecated and will be removed in the next minor release \u2014 pass the vaultSettlement option instead",
|
|
1427
|
+
{}
|
|
1428
|
+
);
|
|
1429
|
+
return { contract: fromEnv };
|
|
1430
|
+
}
|
|
649
1431
|
function channelAuthorizer(envelopeSigner) {
|
|
650
1432
|
return { envelopeSigner };
|
|
651
1433
|
}
|
|
@@ -731,6 +1513,7 @@ function createMppSessionHandler(opts) {
|
|
|
731
1513
|
let record = null;
|
|
732
1514
|
let voucherCount = 0;
|
|
733
1515
|
let sessionOpened = false;
|
|
1516
|
+
let orphanFlagged = false;
|
|
734
1517
|
let idleTimer = null;
|
|
735
1518
|
let settledCleanly = false;
|
|
736
1519
|
let closeListenerArmed = false;
|
|
@@ -756,6 +1539,14 @@ function createMppSessionHandler(opts) {
|
|
|
756
1539
|
return null;
|
|
757
1540
|
}
|
|
758
1541
|
record = { amount: BigInt(stored.amount), signature: stored.signature, payer: stored.payer ?? null };
|
|
1542
|
+
if (typeof stored.voucherCount === "number") {
|
|
1543
|
+
voucherCount = stored.voucherCount;
|
|
1544
|
+
}
|
|
1545
|
+
if (typeof stored.sessionOpened === "boolean") {
|
|
1546
|
+
sessionOpened = stored.sessionOpened;
|
|
1547
|
+
} else if (record !== null) {
|
|
1548
|
+
sessionOpened = true;
|
|
1549
|
+
}
|
|
759
1550
|
} catch {
|
|
760
1551
|
return null;
|
|
761
1552
|
}
|
|
@@ -763,8 +1554,8 @@ function createMppSessionHandler(opts) {
|
|
|
763
1554
|
}
|
|
764
1555
|
async function flagOrphan(reason) {
|
|
765
1556
|
await loadPersistedRecord();
|
|
766
|
-
if (!sessionOpened || settledCleanly) return;
|
|
767
|
-
|
|
1557
|
+
if (!sessionOpened || settledCleanly || orphanFlagged) return;
|
|
1558
|
+
orphanFlagged = true;
|
|
768
1559
|
closeListenerArmed = false;
|
|
769
1560
|
clearIdleTimer();
|
|
770
1561
|
const cumulativeAmount = record ? (Number(record.amount) / 1e7).toFixed(7) : "0.0000000";
|
|
@@ -812,16 +1603,20 @@ function createMppSessionHandler(opts) {
|
|
|
812
1603
|
if (record && amount < record.amount) return;
|
|
813
1604
|
const payer = record?.payer ?? extractPayerAddress(credential.source);
|
|
814
1605
|
record = { amount, signature: payload.signature, payer };
|
|
1606
|
+
voucherCount++;
|
|
1607
|
+
settledCleanly = false;
|
|
1608
|
+
orphanFlagged = false;
|
|
1609
|
+
armIdleTimer();
|
|
1610
|
+
const wasOpened = sessionOpened;
|
|
1611
|
+
sessionOpened = true;
|
|
815
1612
|
await innerStore.put(voucherRecordKey, {
|
|
816
1613
|
amount: record.amount.toString(),
|
|
817
1614
|
signature: record.signature,
|
|
818
|
-
payer: record.payer
|
|
1615
|
+
payer: record.payer,
|
|
1616
|
+
voucherCount,
|
|
1617
|
+
sessionOpened: true
|
|
819
1618
|
});
|
|
820
|
-
|
|
821
|
-
settledCleanly = false;
|
|
822
|
-
armIdleTimer();
|
|
823
|
-
if (!sessionOpened) {
|
|
824
|
-
sessionOpened = true;
|
|
1619
|
+
if (!wasOpened) {
|
|
825
1620
|
if (opts.onSessionOpen) {
|
|
826
1621
|
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
827
1622
|
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
@@ -873,10 +1668,11 @@ function createMppSessionHandler(opts) {
|
|
|
873
1668
|
}
|
|
874
1669
|
if (closeAmount > 0n && closeSig) {
|
|
875
1670
|
const closePayer = record?.payer ?? null;
|
|
1671
|
+
const vouchersClosed = voucherCount;
|
|
876
1672
|
const closeTxHash = await server$3.close({
|
|
877
1673
|
channel: opts.channelFactory,
|
|
878
1674
|
amount: closeAmount,
|
|
879
|
-
signature:
|
|
1675
|
+
signature: hexToBytes(closeSig),
|
|
880
1676
|
feePayer: channelAuthorizer(payeeKeypair),
|
|
881
1677
|
network: networkId
|
|
882
1678
|
});
|
|
@@ -889,46 +1685,28 @@ function createMppSessionHandler(opts) {
|
|
|
889
1685
|
opts.onCallbackError?.(err, "onSettled");
|
|
890
1686
|
});
|
|
891
1687
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1688
|
+
res.json({ closeTxHash });
|
|
1689
|
+
const vaultConfig = resolveVaultSettlementConfig(opts.vaultSettlement, logger);
|
|
1690
|
+
if (vaultConfig) {
|
|
1691
|
+
await recordSessionSettlement(
|
|
1692
|
+
{
|
|
1693
|
+
config: vaultConfig,
|
|
1694
|
+
network: opts.network,
|
|
1695
|
+
payeeSecretKey: opts.payeeSecretKey,
|
|
1696
|
+
channelId: opts.channelFactory,
|
|
1697
|
+
payer: closePayer,
|
|
1698
|
+
amount: closeAmount,
|
|
1699
|
+
voucherCount: vouchersClosed
|
|
1700
|
+
},
|
|
1701
|
+
{ logger, onCallbackError: opts.onCallbackError }
|
|
897
1702
|
);
|
|
898
|
-
if (!settlementAddresses) {
|
|
899
|
-
logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
900
|
-
} else {
|
|
901
|
-
try {
|
|
902
|
-
const { Networks } = await import('@stellar/stellar-sdk');
|
|
903
|
-
const { Server } = await import('@stellar/stellar-sdk/rpc');
|
|
904
|
-
const networkPassphrase = opts.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
905
|
-
const rpcUrl = opts.network === "mainnet" ? "https://mainnet.sorobanrpc.com" : "https://soroban-testnet.stellar.org";
|
|
906
|
-
const server = new Server(rpcUrl);
|
|
907
|
-
const payeePublicKey = payeeKeypair.publicKey();
|
|
908
|
-
const sourceAccount = await server.getAccount(payeePublicKey);
|
|
909
|
-
const tx = buildSessionSettlementTransaction(
|
|
910
|
-
sourceAccount,
|
|
911
|
-
vaultContract,
|
|
912
|
-
opts.channelFactory,
|
|
913
|
-
settlementAddresses,
|
|
914
|
-
closeAmount,
|
|
915
|
-
voucherCount,
|
|
916
|
-
networkPassphrase
|
|
917
|
-
);
|
|
918
|
-
const preparedTx = await server.prepareTransaction(tx);
|
|
919
|
-
preparedTx.sign(payeeKeypair);
|
|
920
|
-
await server.sendTransaction(preparedTx);
|
|
921
|
-
} catch (recordErr) {
|
|
922
|
-
logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
1703
|
}
|
|
926
|
-
res.json({ closeTxHash });
|
|
927
1704
|
} else {
|
|
928
1705
|
res.json({ closeTxHash: null, message: "no vouchers received" });
|
|
929
1706
|
}
|
|
930
1707
|
sessionOpened = false;
|
|
931
1708
|
voucherCount = 0;
|
|
1709
|
+
orphanFlagged = false;
|
|
932
1710
|
record = null;
|
|
933
1711
|
await innerStore.delete(voucherRecordKey);
|
|
934
1712
|
closeListenerArmed = false;
|
|
@@ -960,6 +1738,11 @@ function createMppSessionHandler(opts) {
|
|
|
960
1738
|
}
|
|
961
1739
|
};
|
|
962
1740
|
}
|
|
1741
|
+
function sha256(data) {
|
|
1742
|
+
return sha2_js.sha256(typeof data === "string" ? utf8ToBytes(data) : data);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// src/manifest/sign.ts
|
|
963
1746
|
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
964
1747
|
function canonicalize(value) {
|
|
965
1748
|
if (Array.isArray(value)) {
|
|
@@ -975,7 +1758,7 @@ function canonicalize(value) {
|
|
|
975
1758
|
function manifestDigest(manifest) {
|
|
976
1759
|
const { signature: _omit, ...rest } = manifest;
|
|
977
1760
|
const canonical = JSON.stringify(canonicalize(rest));
|
|
978
|
-
return
|
|
1761
|
+
return sha256(utf8ToBytes(canonical));
|
|
979
1762
|
}
|
|
980
1763
|
function signManifest(manifest, payeeSecretKey) {
|
|
981
1764
|
const keypair = stellarSdk.Keypair.fromSecret(payeeSecretKey);
|
|
@@ -985,7 +1768,7 @@ function signManifest(manifest, payeeSecretKey) {
|
|
|
985
1768
|
};
|
|
986
1769
|
const digest = manifestDigest(versionedManifest);
|
|
987
1770
|
const sig = keypair.sign(digest);
|
|
988
|
-
return { ...versionedManifest, signature:
|
|
1771
|
+
return { ...versionedManifest, signature: bytesToBase64(sig) };
|
|
989
1772
|
}
|
|
990
1773
|
function verifyManifestSignature(manifest, expectedPayee) {
|
|
991
1774
|
if (!manifest.signature) {
|
|
@@ -998,11 +1781,19 @@ function verifyManifestSignature(manifest, expectedPayee) {
|
|
|
998
1781
|
`Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
|
|
999
1782
|
);
|
|
1000
1783
|
}
|
|
1001
|
-
|
|
1784
|
+
let keypair;
|
|
1785
|
+
try {
|
|
1786
|
+
keypair = stellarSdk.Keypair.fromPublicKey(manifest.payee);
|
|
1787
|
+
} catch (err) {
|
|
1788
|
+
throw new RouteDockSignatureError(
|
|
1789
|
+
`Manifest payee ${manifest.payee} is not a valid Stellar public key`,
|
|
1790
|
+
{ cause: err }
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1002
1793
|
const digest = manifestDigest(manifest);
|
|
1003
1794
|
let sigBytes;
|
|
1004
1795
|
try {
|
|
1005
|
-
sigBytes =
|
|
1796
|
+
sigBytes = base64ToBytes(manifest.signature);
|
|
1006
1797
|
} catch {
|
|
1007
1798
|
throw new RouteDockSignatureError("Manifest signature is not valid base64");
|
|
1008
1799
|
}
|
|
@@ -1025,6 +1816,7 @@ function verifyManifestSignature(manifest, expectedPayee) {
|
|
|
1025
1816
|
var MPP_MODES = ["mpp-session", "mpp-charge"];
|
|
1026
1817
|
function routedock(opts) {
|
|
1027
1818
|
const startTime = Date.now();
|
|
1819
|
+
assertProviderConfig(opts, "routedock");
|
|
1028
1820
|
const signedManifest = signManifest(opts.manifest, opts.payeeSecretKey);
|
|
1029
1821
|
const handlerMap = /* @__PURE__ */ new Map();
|
|
1030
1822
|
if (opts.modes.includes("x402")) {
|
|
@@ -1088,7 +1880,8 @@ function routedock(opts) {
|
|
|
1088
1880
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
1089
1881
|
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
1090
1882
|
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
1091
|
-
...opts.logger ? { logger: opts.logger } : {}
|
|
1883
|
+
...opts.logger ? { logger: opts.logger } : {},
|
|
1884
|
+
...opts.vaultSettlement ? { vaultSettlement: opts.vaultSettlement } : {}
|
|
1092
1885
|
})
|
|
1093
1886
|
);
|
|
1094
1887
|
}
|
|
@@ -1135,6 +1928,8 @@ var MPP_NETWORK3 = {
|
|
|
1135
1928
|
testnet: "stellar:testnet",
|
|
1136
1929
|
mainnet: "stellar:pubnet"
|
|
1137
1930
|
};
|
|
1931
|
+
var DEFAULT_MAX_RECONCILE_ATTEMPTS = 5;
|
|
1932
|
+
var ALREADY_CLOSED_PATTERN = /channel has been closed|channel is closed|already closed|finalized/i;
|
|
1138
1933
|
function decimalToStroops(amountStr) {
|
|
1139
1934
|
return nulthSdk.usdcToStroops(amountStr);
|
|
1140
1935
|
}
|
|
@@ -1144,12 +1939,15 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
1144
1939
|
recoveredCount: 0,
|
|
1145
1940
|
skippedCount: 0,
|
|
1146
1941
|
failedCount: 0,
|
|
1942
|
+
alreadyClosedCount: 0,
|
|
1943
|
+
terminalCount: 0,
|
|
1147
1944
|
errors: []
|
|
1148
1945
|
};
|
|
1946
|
+
const maxAttempts = opts.maxReconcileAttempts ?? DEFAULT_MAX_RECONCILE_ATTEMPTS;
|
|
1149
1947
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
1150
1948
|
const networkId = MPP_NETWORK3[opts.network];
|
|
1151
1949
|
try {
|
|
1152
|
-
const { data: closingSessions, error } = await opts.supabase.from("sessions").select("channel_id, channel_contract, cumulative_amount, last_signature, settlement_tx_hash").eq("status", "closing").eq("network", opts.network).eq("payee", payeeKeypair.publicKey()).is("settlement_tx_hash", null).not("last_signature", "is", null).order("updated_at", { ascending: true }).limit(100);
|
|
1950
|
+
const { data: closingSessions, error } = await opts.supabase.from("sessions").select("channel_id, channel_contract, cumulative_amount, last_signature, settlement_tx_hash, reconcile_attempts").eq("status", "closing").eq("network", opts.network).eq("payee", payeeKeypair.publicKey()).is("settlement_tx_hash", null).not("last_signature", "is", null).order("updated_at", { ascending: true }).limit(100);
|
|
1153
1951
|
if (error) {
|
|
1154
1952
|
throw new Error(`Failed to query sessions: ${error.message}`);
|
|
1155
1953
|
}
|
|
@@ -1171,7 +1969,7 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
1171
1969
|
continue;
|
|
1172
1970
|
}
|
|
1173
1971
|
const closeAmount = decimalToStroops(cumulative_amount);
|
|
1174
|
-
const closeSig =
|
|
1972
|
+
const closeSig = hexToBytes(last_signature);
|
|
1175
1973
|
const closeFn = opts.channelClose ?? (await import('@stellar/mpp/channel/server')).close;
|
|
1176
1974
|
const closeTxHash = await closeFn({
|
|
1177
1975
|
channel: channelId,
|
|
@@ -1194,11 +1992,36 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
1194
1992
|
await opts.onRecovered(channelId, closeTxHash, totalPaid);
|
|
1195
1993
|
}
|
|
1196
1994
|
} catch (err) {
|
|
1995
|
+
const reason = formatMppError(err);
|
|
1996
|
+
if (ALREADY_CLOSED_PATTERN.test(reason)) {
|
|
1997
|
+
const { error: closeError } = await opts.supabase.from("sessions").update({
|
|
1998
|
+
status: "closed",
|
|
1999
|
+
last_reconcile_error: `channel already closed on-chain: ${reason}`,
|
|
2000
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2001
|
+
}).eq("channel_id", session.channel_id);
|
|
2002
|
+
if (!closeError) {
|
|
2003
|
+
stats.alreadyClosedCount++;
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
1197
2007
|
stats.failedCount++;
|
|
1198
|
-
stats.errors.push({
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
})
|
|
2008
|
+
stats.errors.push({ channelId: session.channel_id, reason });
|
|
2009
|
+
const attempts = (Number(session.reconcile_attempts) || 0) + 1;
|
|
2010
|
+
const terminal = attempts >= maxAttempts;
|
|
2011
|
+
const { error: bookkeepingError } = await opts.supabase.from("sessions").update({
|
|
2012
|
+
reconcile_attempts: attempts,
|
|
2013
|
+
last_reconcile_error: reason,
|
|
2014
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2015
|
+
...terminal ? { status: "failed" } : {}
|
|
2016
|
+
}).eq("channel_id", session.channel_id);
|
|
2017
|
+
if (bookkeepingError) {
|
|
2018
|
+
stats.errors.push({
|
|
2019
|
+
channelId: session.channel_id,
|
|
2020
|
+
reason: `Failed to record reconcile failure: ${bookkeepingError.message}`
|
|
2021
|
+
});
|
|
2022
|
+
} else if (terminal) {
|
|
2023
|
+
stats.terminalCount++;
|
|
2024
|
+
}
|
|
1202
2025
|
}
|
|
1203
2026
|
}
|
|
1204
2027
|
return stats;
|
|
@@ -1219,7 +2042,7 @@ async function runStartupReconciliation(opts, logger) {
|
|
|
1219
2042
|
}
|
|
1220
2043
|
log(
|
|
1221
2044
|
"info",
|
|
1222
|
-
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
|
|
2045
|
+
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}, Already closed: ${stats.alreadyClosedCount}, Moved to failed: ${stats.terminalCount}`
|
|
1223
2046
|
);
|
|
1224
2047
|
if (stats.errors.length > 0) {
|
|
1225
2048
|
log("warn", "[SessionReconciler] Reconciliation errors:");
|
|
@@ -1438,6 +2261,7 @@ exports.isVoucherStoreValue = isVoucherStoreValue;
|
|
|
1438
2261
|
exports.normalizeManifestAssets = normalizeManifestAssets;
|
|
1439
2262
|
exports.paymentIdempotencyKey = paymentIdempotencyKey;
|
|
1440
2263
|
exports.reconcileAbandonedSessions = reconcileAbandonedSessions;
|
|
2264
|
+
exports.recordSettlement = recordSettlement;
|
|
1441
2265
|
exports.registerProvider = registerProvider;
|
|
1442
2266
|
exports.resolveAssetContract = resolveAssetContract;
|
|
1443
2267
|
exports.routedock = routedock;
|