@routedock/routedock 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -12
- package/dist/{MppSessionHandler-1EL8QBAn.d.cts → MppSessionHandler-3QfCHI5x.d.ts} +40 -2
- package/dist/{MppSessionHandler-Bq4S-JTE.d.ts → MppSessionHandler-xu4Kxu6N.d.cts} +40 -2
- package/dist/{ProviderRegistry-CA_TdREG.d.ts → ProviderRegistry-D9KqyuH-.d.cts} +16 -36
- package/dist/{ProviderRegistry-D-0rqK0N.d.cts → ProviderRegistry-DVi2uzgk.d.ts} +16 -36
- package/dist/{RouteDockClient-go9aopfF.d.cts → RouteDockClient-6rmgkbYb.d.cts} +124 -142
- package/dist/{RouteDockClient-Bh86bVly.d.ts → RouteDockClient-CnAkxwrW.d.ts} +124 -142
- package/dist/{SessionReconciler-Bw-PoBVl.d.ts → SessionReconciler-Cce9MXO1.d.ts} +10 -3
- package/dist/{SessionReconciler-Cz2nuOlJ.d.cts → SessionReconciler-Dpe9qXdD.d.cts} +10 -3
- package/dist/{SessionStore-BaYiegP0.d.ts → SessionStore-DkLBxFQu.d.cts} +28 -3
- package/dist/{SessionStore-B4-Hi4H8.d.cts → SessionStore-_ZkoBzFY.d.ts} +28 -3
- package/dist/SpendStore-BUclV02i.d.ts +73 -0
- package/dist/SpendStore-s_dfcemj.d.cts +73 -0
- package/dist/client.cjs +767 -487
- package/dist/client.d.cts +80 -10
- package/dist/client.d.ts +80 -10
- package/dist/client.js +733 -468
- package/dist/index.cjs +1295 -590
- package/dist/index.d.cts +13 -10
- package/dist/index.d.ts +13 -10
- package/dist/index.js +1247 -559
- package/dist/logger-DVa7VT4c.d.cts +41 -0
- package/dist/logger-DVa7VT4c.d.ts +41 -0
- package/dist/network.cjs +57 -0
- package/dist/network.d.cts +72 -0
- package/dist/network.d.ts +72 -0
- package/dist/network.js +49 -0
- package/dist/node.cjs +166 -0
- package/dist/node.d.cts +77 -0
- package/dist/node.d.ts +77 -0
- package/dist/node.js +159 -0
- package/dist/provider/fastify.cjs +847 -62
- package/dist/provider/fastify.d.cts +10 -3
- package/dist/provider/fastify.d.ts +10 -3
- package/dist/provider/fastify.js +848 -63
- package/dist/provider/hono.cjs +936 -60
- package/dist/provider/hono.d.cts +23 -8
- package/dist/provider/hono.d.ts +23 -8
- package/dist/provider/hono.js +937 -61
- package/dist/provider.cjs +894 -70
- package/dist/provider.d.cts +9 -8
- package/dist/provider.d.ts +9 -8
- package/dist/provider.js +895 -72
- package/dist/react.cjs +736 -436
- package/dist/react.d.cts +7 -5
- package/dist/react.d.ts +7 -5
- package/dist/react.js +725 -425
- package/dist/{routedockMiddleware-57gUaEMf.d.ts → routedockMiddleware-Cq9HOJEa.d.ts} +9 -2
- package/dist/{routedockMiddleware-DlyLrxp7.d.cts → routedockMiddleware-CxsWkSKa.d.cts} +9 -2
- package/dist/schema.cjs +7 -0
- package/dist/schema.js +7 -0
- package/dist/{sign-AKH67sft.d.cts → sign-Bq08SeFq.d.ts} +2 -2
- package/dist/{sign-BEq-OsSq.d.ts → sign-DqGE0Plv.d.cts} +2 -2
- package/dist/testing.d.cts +4 -3
- package/dist/testing.d.ts +4 -3
- package/dist/{logger-D8_Q2ytp.d.cts → types-DXaS5Wip.d.cts} +85 -47
- package/dist/{logger-D8_Q2ytp.d.ts → types-DXaS5Wip.d.ts} +85 -47
- package/package.json +50 -4
|
@@ -1,19 +1,20 @@
|
|
|
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
|
-
// src/provider/
|
|
17
|
+
// src/provider/internal/assertProviderConfig.ts
|
|
17
18
|
|
|
18
19
|
// src/errors.ts
|
|
19
20
|
var RouteDockError = class extends Error {
|
|
@@ -36,13 +37,618 @@ var RouteDockManifestError = class extends RouteDockError {
|
|
|
36
37
|
this.name = "RouteDockManifestError";
|
|
37
38
|
}
|
|
38
39
|
};
|
|
40
|
+
var usdcToUnits = nulthSdk.usdcToStroops;
|
|
41
|
+
|
|
42
|
+
// src/schemas/routedock.schema.json
|
|
43
|
+
var routedock_schema_default = {
|
|
44
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
45
|
+
$id: "https://routedock.xyz/schemas/routedock.schema.json",
|
|
46
|
+
title: "RouteDock Manifest",
|
|
47
|
+
description: "Discovery manifest served at /.well-known/routedock.json on every RouteDock provider endpoint",
|
|
48
|
+
type: "object",
|
|
49
|
+
required: [
|
|
50
|
+
"routedock",
|
|
51
|
+
"name",
|
|
52
|
+
"description",
|
|
53
|
+
"modes",
|
|
54
|
+
"network",
|
|
55
|
+
"asset",
|
|
56
|
+
"asset_contract",
|
|
57
|
+
"payee",
|
|
58
|
+
"pricing",
|
|
59
|
+
"endpoints",
|
|
60
|
+
"tags"
|
|
61
|
+
],
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
properties: {
|
|
64
|
+
routedock: {
|
|
65
|
+
type: "string",
|
|
66
|
+
pattern: "^[0-9]+\\.[0-9]+$",
|
|
67
|
+
description: "Manifest schema version, e.g. '1.0', '1.1', '2.0'."
|
|
68
|
+
},
|
|
69
|
+
min_client_version: {
|
|
70
|
+
type: "string",
|
|
71
|
+
pattern: "^[0-9]+\\.[0-9]+$",
|
|
72
|
+
description: "Minimum SDK client version (major.minor) required to use this manifest. Clients below this version must refuse to pay."
|
|
73
|
+
},
|
|
74
|
+
deprecated_modes: {
|
|
75
|
+
type: "array",
|
|
76
|
+
items: {
|
|
77
|
+
$ref: "#/definitions/PaymentMode"
|
|
78
|
+
},
|
|
79
|
+
uniqueItems: true,
|
|
80
|
+
description: "Payment modes still listed in 'modes' for backwards compatibility but deprecated for new integrations."
|
|
81
|
+
},
|
|
82
|
+
sunset_at: {
|
|
83
|
+
type: "string",
|
|
84
|
+
format: "date-time",
|
|
85
|
+
description: "ISO 8601 timestamp after which this manifest version is no longer supported by the provider."
|
|
86
|
+
},
|
|
87
|
+
name: {
|
|
88
|
+
type: "string",
|
|
89
|
+
minLength: 1,
|
|
90
|
+
maxLength: 120,
|
|
91
|
+
description: "Human-readable provider name"
|
|
92
|
+
},
|
|
93
|
+
description: {
|
|
94
|
+
type: "string",
|
|
95
|
+
minLength: 1,
|
|
96
|
+
maxLength: 500,
|
|
97
|
+
description: "Human-readable description of the provider's data or service"
|
|
98
|
+
},
|
|
99
|
+
modes: {
|
|
100
|
+
type: "array",
|
|
101
|
+
minItems: 1,
|
|
102
|
+
uniqueItems: true,
|
|
103
|
+
items: {
|
|
104
|
+
$ref: "#/definitions/PaymentMode"
|
|
105
|
+
},
|
|
106
|
+
description: "Payment modes supported by this provider"
|
|
107
|
+
},
|
|
108
|
+
network: {
|
|
109
|
+
type: "string",
|
|
110
|
+
enum: ["testnet", "mainnet"],
|
|
111
|
+
description: "Stellar network this provider operates on"
|
|
112
|
+
},
|
|
113
|
+
asset: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "Asset ticker symbol (e.g. 'USDC')"
|
|
116
|
+
},
|
|
117
|
+
asset_contract: {
|
|
118
|
+
type: "string",
|
|
119
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
120
|
+
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
121
|
+
},
|
|
122
|
+
assets: {
|
|
123
|
+
type: "array",
|
|
124
|
+
minItems: 1,
|
|
125
|
+
items: {
|
|
126
|
+
$ref: "#/definitions/AssetConfig"
|
|
127
|
+
},
|
|
128
|
+
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."
|
|
129
|
+
},
|
|
130
|
+
payee: {
|
|
131
|
+
type: "string",
|
|
132
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
133
|
+
description: "Stellar address (G...) that receives payments"
|
|
134
|
+
},
|
|
135
|
+
pricing: {
|
|
136
|
+
type: "object",
|
|
137
|
+
description: "Pricing configuration per supported payment mode",
|
|
138
|
+
additionalProperties: false,
|
|
139
|
+
properties: {
|
|
140
|
+
x402: {
|
|
141
|
+
$ref: "#/definitions/PricingConfig"
|
|
142
|
+
},
|
|
143
|
+
"mpp-charge": {
|
|
144
|
+
$ref: "#/definitions/PricingConfig"
|
|
145
|
+
},
|
|
146
|
+
"mpp-session": {
|
|
147
|
+
$ref: "#/definitions/SessionPricingConfig"
|
|
148
|
+
},
|
|
149
|
+
"mpp-session-ws": {
|
|
150
|
+
$ref: "#/definitions/SessionPricingConfig",
|
|
151
|
+
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."
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
minProperties: 1
|
|
155
|
+
},
|
|
156
|
+
endpoints: {
|
|
157
|
+
type: "object",
|
|
158
|
+
description: "Map of endpoint name to endpoint descriptors",
|
|
159
|
+
additionalProperties: {
|
|
160
|
+
$ref: "#/definitions/EndpointDescriptor"
|
|
161
|
+
},
|
|
162
|
+
minProperties: 1
|
|
163
|
+
},
|
|
164
|
+
sla: {
|
|
165
|
+
$ref: "#/definitions/SLAConfig"
|
|
166
|
+
},
|
|
167
|
+
tags: {
|
|
168
|
+
type: "array",
|
|
169
|
+
items: {
|
|
170
|
+
type: "string",
|
|
171
|
+
minLength: 1,
|
|
172
|
+
maxLength: 64
|
|
173
|
+
},
|
|
174
|
+
uniqueItems: true,
|
|
175
|
+
description: "Capability tags indexed with trigram search in the provider registry"
|
|
176
|
+
},
|
|
177
|
+
vault: {
|
|
178
|
+
$ref: "#/definitions/VaultMode"
|
|
179
|
+
},
|
|
180
|
+
nulth_account: {
|
|
181
|
+
type: "string",
|
|
182
|
+
description: "Optional Nulth smart account address associated with this provider"
|
|
183
|
+
},
|
|
184
|
+
capabilities: {
|
|
185
|
+
type: "object",
|
|
186
|
+
description: "Optional protocol features this provider supports",
|
|
187
|
+
properties: {
|
|
188
|
+
streaming: {
|
|
189
|
+
type: "array",
|
|
190
|
+
items: {
|
|
191
|
+
type: "string",
|
|
192
|
+
enum: ["sse", "websocket"]
|
|
193
|
+
},
|
|
194
|
+
description: "Supported streaming transports"
|
|
195
|
+
},
|
|
196
|
+
webhooks: {
|
|
197
|
+
type: "boolean",
|
|
198
|
+
description: "Whether webhook callbacks are supported"
|
|
199
|
+
},
|
|
200
|
+
idempotency_keys: {
|
|
201
|
+
type: "boolean",
|
|
202
|
+
description: "Whether idempotency keys are supported"
|
|
203
|
+
},
|
|
204
|
+
content_types: {
|
|
205
|
+
type: "array",
|
|
206
|
+
items: {
|
|
207
|
+
type: "string"
|
|
208
|
+
},
|
|
209
|
+
description: "Supported content types for request/response"
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
additionalProperties: false
|
|
213
|
+
},
|
|
214
|
+
regions: {
|
|
215
|
+
type: "array",
|
|
216
|
+
items: {
|
|
217
|
+
type: "string",
|
|
218
|
+
minLength: 2,
|
|
219
|
+
maxLength: 10
|
|
220
|
+
},
|
|
221
|
+
uniqueItems: true,
|
|
222
|
+
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."
|
|
223
|
+
},
|
|
224
|
+
latency_hints: {
|
|
225
|
+
type: "object",
|
|
226
|
+
additionalProperties: {
|
|
227
|
+
type: "number",
|
|
228
|
+
minimum: 0
|
|
229
|
+
},
|
|
230
|
+
description: "Provider-declared p50 latency in milliseconds per region code. Keys must be a subset of 'regions'. Example: { 'IAD': 14, 'AMS': 22 }."
|
|
231
|
+
},
|
|
232
|
+
signature: {
|
|
233
|
+
type: "string",
|
|
234
|
+
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."
|
|
235
|
+
},
|
|
236
|
+
signature_version: {
|
|
237
|
+
type: "string",
|
|
238
|
+
const: "2",
|
|
239
|
+
description: "Manifest signature format. Version 2 recursively includes every nested manifest field in the signed digest."
|
|
240
|
+
},
|
|
241
|
+
categories: {
|
|
242
|
+
type: "array",
|
|
243
|
+
items: {
|
|
244
|
+
type: "string",
|
|
245
|
+
minLength: 1,
|
|
246
|
+
maxLength: 128
|
|
247
|
+
},
|
|
248
|
+
uniqueItems: true,
|
|
249
|
+
description: "Optional hierarchical categories from a published taxonomy (e.g. data/price/crypto)"
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
definitions: {
|
|
253
|
+
VaultMode: {
|
|
254
|
+
type: "string",
|
|
255
|
+
enum: ["local-key", "agent-vault", "nulth"],
|
|
256
|
+
description: "Agent vault / custody mode for payments from this provider"
|
|
257
|
+
},
|
|
258
|
+
PaymentMode: {
|
|
259
|
+
type: "string",
|
|
260
|
+
enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
|
|
261
|
+
description: "A supported RouteDock payment mode"
|
|
262
|
+
},
|
|
263
|
+
AssetConfig: {
|
|
264
|
+
type: "object",
|
|
265
|
+
description: "Payment asset configuration with optional mode and endpoint scoping",
|
|
266
|
+
required: ["asset", "asset_contract"],
|
|
267
|
+
additionalProperties: false,
|
|
268
|
+
properties: {
|
|
269
|
+
asset: {
|
|
270
|
+
type: "string",
|
|
271
|
+
description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
|
|
272
|
+
},
|
|
273
|
+
asset_contract: {
|
|
274
|
+
type: "string",
|
|
275
|
+
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
276
|
+
},
|
|
277
|
+
modes: {
|
|
278
|
+
type: "array",
|
|
279
|
+
items: {
|
|
280
|
+
$ref: "#/definitions/PaymentMode"
|
|
281
|
+
},
|
|
282
|
+
uniqueItems: true,
|
|
283
|
+
description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
|
|
284
|
+
},
|
|
285
|
+
endpoints: {
|
|
286
|
+
type: "array",
|
|
287
|
+
items: {
|
|
288
|
+
type: "string"
|
|
289
|
+
},
|
|
290
|
+
uniqueItems: true,
|
|
291
|
+
description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
PricingConfig: {
|
|
296
|
+
type: "object",
|
|
297
|
+
description: "Per-request pricing for x402 or mpp-charge modes",
|
|
298
|
+
required: ["amount", "per"],
|
|
299
|
+
additionalProperties: false,
|
|
300
|
+
properties: {
|
|
301
|
+
amount: {
|
|
302
|
+
type: "string",
|
|
303
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
304
|
+
description: "Cost per request in the payment asset (decimal string, e.g. '0.001')"
|
|
305
|
+
},
|
|
306
|
+
per: {
|
|
307
|
+
type: "string",
|
|
308
|
+
const: "request",
|
|
309
|
+
description: "Billing unit \u2014 always 'request' for non-session modes"
|
|
310
|
+
},
|
|
311
|
+
facilitator: {
|
|
312
|
+
type: "string",
|
|
313
|
+
description: "x402 facilitator URL (required for x402 mode). OpenZeppelin Channels endpoint."
|
|
314
|
+
},
|
|
315
|
+
payee: {
|
|
316
|
+
type: "string",
|
|
317
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
318
|
+
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."
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
SessionPricingConfig: {
|
|
323
|
+
type: "object",
|
|
324
|
+
description: "Per-voucher pricing for mpp-session (one-way-channel) mode",
|
|
325
|
+
required: ["rate", "per", "channel_factory", "min_deposit", "refund_waiting_period_ledgers"],
|
|
326
|
+
additionalProperties: false,
|
|
327
|
+
properties: {
|
|
328
|
+
rate: {
|
|
329
|
+
type: "string",
|
|
330
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
331
|
+
description: "Cost per voucher in the payment asset (decimal string, e.g. '0.0001')"
|
|
332
|
+
},
|
|
333
|
+
per: {
|
|
334
|
+
type: "string",
|
|
335
|
+
const: "voucher",
|
|
336
|
+
description: "Billing unit \u2014 always 'voucher' for session mode"
|
|
337
|
+
},
|
|
338
|
+
channel_factory: {
|
|
339
|
+
type: "string",
|
|
340
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
341
|
+
description: "Stable Soroban factory address (C...) used to deploy a per-session channel"
|
|
342
|
+
},
|
|
343
|
+
min_deposit: {
|
|
344
|
+
type: "string",
|
|
345
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
346
|
+
description: "Minimum initial deposit to open a channel (decimal string)"
|
|
347
|
+
},
|
|
348
|
+
refund_waiting_period_ledgers: {
|
|
349
|
+
type: "integer",
|
|
350
|
+
minimum: 1,
|
|
351
|
+
description: "Ledgers the server has to settle before the funder can reclaim. Default: 17280 (~24h). Do not reduce in production."
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
SLAConfig: {
|
|
356
|
+
type: "object",
|
|
357
|
+
description: "Service Level Agreement for the provider",
|
|
358
|
+
required: ["uptime_30d_percent", "p95_latency_ms"],
|
|
359
|
+
additionalProperties: false,
|
|
360
|
+
properties: {
|
|
361
|
+
uptime_30d_percent: {
|
|
362
|
+
type: "number",
|
|
363
|
+
description: "Uptime percentage over the last 30 days"
|
|
364
|
+
},
|
|
365
|
+
p95_latency_ms: {
|
|
366
|
+
type: "number",
|
|
367
|
+
description: "P95 latency in milliseconds"
|
|
368
|
+
},
|
|
369
|
+
maintenance_windows: {
|
|
370
|
+
type: "array",
|
|
371
|
+
items: {
|
|
372
|
+
type: "object",
|
|
373
|
+
required: ["cron", "duration_minutes"],
|
|
374
|
+
additionalProperties: false,
|
|
375
|
+
properties: {
|
|
376
|
+
cron: {
|
|
377
|
+
type: "string",
|
|
378
|
+
description: "Cron expression for the maintenance window"
|
|
379
|
+
},
|
|
380
|
+
duration_minutes: {
|
|
381
|
+
type: "number",
|
|
382
|
+
description: "Duration of the maintenance window in minutes"
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
EndpointDescriptor: {
|
|
390
|
+
type: "object",
|
|
391
|
+
description: "Descriptor for a provider endpoint",
|
|
392
|
+
required: ["method", "path"],
|
|
393
|
+
additionalProperties: false,
|
|
394
|
+
properties: {
|
|
395
|
+
method: {
|
|
396
|
+
type: "string",
|
|
397
|
+
description: "HTTP method for the endpoint"
|
|
398
|
+
},
|
|
399
|
+
path: {
|
|
400
|
+
type: "string",
|
|
401
|
+
description: "Path for the endpoint"
|
|
402
|
+
},
|
|
403
|
+
deprecated: {
|
|
404
|
+
type: "boolean",
|
|
405
|
+
description: "Whether this endpoint is retained only for backwards compatibility"
|
|
406
|
+
},
|
|
407
|
+
sunset_at: {
|
|
408
|
+
type: "string",
|
|
409
|
+
format: "date-time",
|
|
410
|
+
description: "ISO 8601 timestamp after which callers should stop using this endpoint"
|
|
411
|
+
},
|
|
412
|
+
headers: {
|
|
413
|
+
type: "object",
|
|
414
|
+
additionalProperties: {
|
|
415
|
+
type: "string"
|
|
416
|
+
},
|
|
417
|
+
description: "Expected request headers"
|
|
418
|
+
},
|
|
419
|
+
request_schema: {
|
|
420
|
+
description: "JSON schema for the request body"
|
|
421
|
+
},
|
|
422
|
+
response_schema: {
|
|
423
|
+
description: "JSON schema for the response body"
|
|
424
|
+
},
|
|
425
|
+
rate_limit: {
|
|
426
|
+
type: "object",
|
|
427
|
+
required: ["requests", "window_seconds"],
|
|
428
|
+
additionalProperties: false,
|
|
429
|
+
properties: {
|
|
430
|
+
requests: {
|
|
431
|
+
type: "number"
|
|
432
|
+
},
|
|
433
|
+
window_seconds: {
|
|
434
|
+
type: "number"
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// src/manifest/validate.ts
|
|
444
|
+
var validator = new jsonSchema.Validator(routedock_schema_default, "7");
|
|
445
|
+
function validateManifestSchema(manifest, source) {
|
|
446
|
+
const result = validator.validate(manifest);
|
|
447
|
+
if (!result.valid) {
|
|
448
|
+
const msgs = result.errors.map((err) => err.error).join("; ");
|
|
449
|
+
throw new RouteDockManifestError(`Invalid manifest${""}: ${msgs}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function assertManifestValid(manifest, baseUrl) {
|
|
453
|
+
const context = "";
|
|
454
|
+
if (manifest.latency_hints !== void 0) {
|
|
455
|
+
if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
|
|
456
|
+
throw new RouteDockManifestError(
|
|
457
|
+
`Invalid manifest${context}: latency_hints defined without declaring regions`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
const regionSet = new Set(manifest.regions);
|
|
461
|
+
for (const region of Object.keys(manifest.latency_hints)) {
|
|
462
|
+
if (!regionSet.has(region)) {
|
|
463
|
+
throw new RouteDockManifestError(
|
|
464
|
+
`Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (manifest.assets !== void 0) {
|
|
470
|
+
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
471
|
+
throw new RouteDockManifestError(
|
|
472
|
+
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
for (let i = 0; i < manifest.assets.length; i++) {
|
|
476
|
+
const a = manifest.assets[i];
|
|
477
|
+
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
478
|
+
throw new RouteDockManifestError(
|
|
479
|
+
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const first = manifest.assets[0];
|
|
484
|
+
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
485
|
+
throw new RouteDockManifestError(
|
|
486
|
+
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
assertPricingMatchesModes(manifest, context);
|
|
491
|
+
assertDeprecatedModesListed(manifest, context);
|
|
492
|
+
assertPricesParse(manifest, context);
|
|
493
|
+
}
|
|
494
|
+
function assertPricingMatchesModes(manifest, context) {
|
|
495
|
+
const pricing = manifest.pricing ?? {};
|
|
496
|
+
for (const mode of manifest.modes ?? []) {
|
|
497
|
+
if (pricing[mode] === void 0 || pricing[mode] === null) {
|
|
498
|
+
throw new RouteDockManifestError(
|
|
499
|
+
`Invalid manifest${context}: mode '${mode}' is listed in modes but pricing.${mode} is missing`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function assertDeprecatedModesListed(manifest, context) {
|
|
505
|
+
if (manifest.deprecated_modes === void 0) return;
|
|
506
|
+
const modes = new Set(manifest.modes ?? []);
|
|
507
|
+
for (const mode of manifest.deprecated_modes) {
|
|
508
|
+
if (!modes.has(mode)) {
|
|
509
|
+
throw new RouteDockManifestError(
|
|
510
|
+
`Invalid manifest${context}: deprecated_modes entry '${mode}' is not listed in modes`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
var PRICE_FIELDS = {
|
|
516
|
+
x402: ["amount"],
|
|
517
|
+
"mpp-charge": ["amount"],
|
|
518
|
+
"mpp-session": ["rate", "min_deposit"],
|
|
519
|
+
"mpp-session-ws": ["rate", "min_deposit"]
|
|
520
|
+
};
|
|
521
|
+
function assertPricesParse(manifest, context) {
|
|
522
|
+
const pricing = manifest.pricing ?? {};
|
|
523
|
+
for (const [mode, fields] of Object.entries(PRICE_FIELDS)) {
|
|
524
|
+
const entry = pricing[mode];
|
|
525
|
+
if (entry === void 0 || entry === null) continue;
|
|
526
|
+
for (const field of fields) {
|
|
527
|
+
const value = entry[field];
|
|
528
|
+
try {
|
|
529
|
+
if (typeof value !== "string") {
|
|
530
|
+
throw new RangeError(`expected a decimal string, got ${typeof value}`);
|
|
531
|
+
}
|
|
532
|
+
nulthSdk.usdcToStroops(value);
|
|
533
|
+
} catch (err) {
|
|
534
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
535
|
+
throw new RouteDockManifestError(
|
|
536
|
+
`Invalid manifest${context}: pricing.${mode}.${field} ${JSON.stringify(value)} is not a valid amount (${detail})`,
|
|
537
|
+
{ cause: err }
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/provider/internal/assertProviderConfig.ts
|
|
545
|
+
var FLAT_MODES = ["x402", "mpp-charge"];
|
|
546
|
+
var SESSION_MODES = ["mpp-session", "mpp-session-ws"];
|
|
547
|
+
function sameAmount(adapter, label, optionValue, manifestValue) {
|
|
548
|
+
const parse = (where, value) => {
|
|
549
|
+
try {
|
|
550
|
+
return nulthSdk.usdcToStroops(value);
|
|
551
|
+
} catch {
|
|
552
|
+
throw new RouteDockManifestError(
|
|
553
|
+
`${adapter}: ${label} has an unparseable ${where} amount "${value}"`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
return parse("option", optionValue) === parse("manifest", manifestValue);
|
|
558
|
+
}
|
|
559
|
+
function assertProviderConfig(opts, adapterName) {
|
|
560
|
+
const { manifest } = opts;
|
|
561
|
+
if (manifest === null || typeof manifest !== "object") {
|
|
562
|
+
throw new RouteDockManifestError(`${adapterName}: opts.manifest is required and must be an object`);
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
validateManifestSchema(manifest);
|
|
566
|
+
assertManifestValid(manifest);
|
|
567
|
+
} catch (err) {
|
|
568
|
+
if (err instanceof RouteDockManifestError) {
|
|
569
|
+
throw new RouteDockManifestError(`${adapterName}: ${err.message}`, { cause: err });
|
|
570
|
+
}
|
|
571
|
+
throw err;
|
|
572
|
+
}
|
|
573
|
+
const served = new Set(opts.modes.filter((mode) => Boolean(opts.pricing[mode])));
|
|
574
|
+
const advertised = new Set(manifest.modes);
|
|
575
|
+
const notAdvertised = [...served].filter((mode) => !advertised.has(mode));
|
|
576
|
+
const notServed = [...advertised].filter((mode) => !served.has(mode));
|
|
577
|
+
if (notAdvertised.length > 0 || notServed.length > 0) {
|
|
578
|
+
const parts = [];
|
|
579
|
+
if (notAdvertised.length > 0) parts.push(`served but not in manifest.modes: ${notAdvertised.join(", ")}`);
|
|
580
|
+
if (notServed.length > 0) parts.push(`in manifest.modes but not served (missing from modes or pricing): ${notServed.join(", ")}`);
|
|
581
|
+
throw new RouteDockManifestError(`${adapterName}: modes do not match manifest.modes (${parts.join("; ")})`);
|
|
582
|
+
}
|
|
583
|
+
for (const mode of FLAT_MODES) {
|
|
584
|
+
if (!served.has(mode)) continue;
|
|
585
|
+
const optionValue = opts.pricing[mode];
|
|
586
|
+
const manifestValue = manifest.pricing[mode]?.amount;
|
|
587
|
+
if (manifestValue === void 0) {
|
|
588
|
+
throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode}.amount is missing but ${mode} is served`);
|
|
589
|
+
}
|
|
590
|
+
if (!sameAmount(adapterName, `pricing.${mode}`, optionValue, manifestValue)) {
|
|
591
|
+
throw new RouteDockManifestError(
|
|
592
|
+
`${adapterName}: pricing.${mode} "${optionValue}" does not match manifest.pricing.${mode}.amount "${manifestValue}"`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
for (const mode of SESSION_MODES) {
|
|
597
|
+
if (!served.has(mode)) continue;
|
|
598
|
+
const optionValue = opts.pricing[mode];
|
|
599
|
+
const manifestValue = manifest.pricing[mode];
|
|
600
|
+
if (!manifestValue) {
|
|
601
|
+
throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode} is missing but ${mode} is served`);
|
|
602
|
+
}
|
|
603
|
+
if (!sameAmount(adapterName, `pricing.${mode}.rate`, optionValue.rate, manifestValue.rate)) {
|
|
604
|
+
throw new RouteDockManifestError(
|
|
605
|
+
`${adapterName}: pricing.${mode}.rate "${optionValue.rate}" does not match manifest.pricing.${mode}.rate "${manifestValue.rate}"`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
if (optionValue.channelFactory !== manifestValue.channel_factory) {
|
|
609
|
+
throw new RouteDockManifestError(
|
|
610
|
+
`${adapterName}: pricing.${mode}.channelFactory "${optionValue.channelFactory}" does not match manifest.pricing.${mode}.channel_factory "${manifestValue.channel_factory}"`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (opts.assetContract !== void 0 && opts.assetContract !== manifest.asset_contract) {
|
|
615
|
+
throw new RouteDockManifestError(
|
|
616
|
+
`${adapterName}: assetContract "${opts.assetContract}" does not match manifest.asset_contract "${manifest.asset_contract}"`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
if (opts.asset !== void 0 && opts.asset !== manifest.asset) {
|
|
620
|
+
throw new RouteDockManifestError(
|
|
621
|
+
`${adapterName}: asset "${opts.asset}" does not match manifest.asset "${manifest.asset}"`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
if (opts.network !== manifest.network) {
|
|
625
|
+
throw new RouteDockManifestError(
|
|
626
|
+
`${adapterName}: network "${opts.network}" does not match manifest.network "${manifest.network}"`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
let signerPublicKey;
|
|
630
|
+
try {
|
|
631
|
+
signerPublicKey = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey).publicKey();
|
|
632
|
+
} catch (err) {
|
|
633
|
+
throw new RouteDockManifestError(`${adapterName}: payeeSecretKey is not a valid Stellar secret key`, { cause: err });
|
|
634
|
+
}
|
|
635
|
+
if (signerPublicKey !== manifest.payee) {
|
|
636
|
+
throw new RouteDockManifestError(
|
|
637
|
+
`${adapterName}: payeeSecretKey belongs to ${signerPublicKey}, not manifest.payee ${manifest.payee}; clients would reject the signed manifest`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
if (opts.payee !== manifest.payee) {
|
|
641
|
+
throw new RouteDockManifestError(
|
|
642
|
+
`${adapterName}: payee "${opts.payee}" does not match manifest.payee "${manifest.payee}"`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
39
646
|
|
|
40
647
|
// src/internal/payee.ts
|
|
41
648
|
function resolvePayee(manifest, mode) {
|
|
42
649
|
const override = manifest.pricing[mode]?.payee;
|
|
43
650
|
return override ?? manifest.payee;
|
|
44
651
|
}
|
|
45
|
-
var usdcToUnits = nulthSdk.usdcToStroops;
|
|
46
652
|
function extractPayerAddress(key) {
|
|
47
653
|
if (typeof key !== "string") return null;
|
|
48
654
|
return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
@@ -139,6 +745,18 @@ function resolveLogger(logger, fallback = consoleLogger) {
|
|
|
139
745
|
// src/provider/SeenTxStore.ts
|
|
140
746
|
var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
|
|
141
747
|
var MAX_SETTLEMENT_REPLAYS = 1;
|
|
748
|
+
async function recordSettlement(store, key, record, ctx) {
|
|
749
|
+
try {
|
|
750
|
+
await store.set(key, record);
|
|
751
|
+
} catch (err) {
|
|
752
|
+
ctx.logger("error", `${ctx.label} seenTxStore.set failed after settlement`, { error: err });
|
|
753
|
+
try {
|
|
754
|
+
ctx.onCallbackError?.(err, "seenTxStore.set");
|
|
755
|
+
} catch (cbErr) {
|
|
756
|
+
ctx.logger("error", `${ctx.label} onCallbackError threw`, { error: cbErr });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
142
760
|
async function checkSettlementReplay(store, key, now = Date.now()) {
|
|
143
761
|
const record = await store.get(key);
|
|
144
762
|
if (!record) return { kind: "miss" };
|
|
@@ -206,7 +824,7 @@ async function paymentIdempotencyKey(getHeader, scope) {
|
|
|
206
824
|
);
|
|
207
825
|
}
|
|
208
826
|
|
|
209
|
-
// src/
|
|
827
|
+
// src/internal/settleResult.ts
|
|
210
828
|
function readSettleResult(result) {
|
|
211
829
|
const settle = result;
|
|
212
830
|
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
@@ -356,10 +974,10 @@ function createX402Handler(opts) {
|
|
|
356
974
|
requirements
|
|
357
975
|
);
|
|
358
976
|
if (!verifyResult.isValid) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
977
|
+
await respondPaymentRequired(
|
|
978
|
+
"Payment verification failed",
|
|
979
|
+
verifyResult.invalidReason
|
|
980
|
+
);
|
|
363
981
|
return;
|
|
364
982
|
}
|
|
365
983
|
const settleResult = await localFacilitator.settle(
|
|
@@ -385,7 +1003,12 @@ function createX402Handler(opts) {
|
|
|
385
1003
|
if (typeof paymentResponse === "string") {
|
|
386
1004
|
headers["X-Payment-Response"] = paymentResponse;
|
|
387
1005
|
}
|
|
388
|
-
await
|
|
1006
|
+
await recordSettlement(
|
|
1007
|
+
seenTxStore,
|
|
1008
|
+
idempotencyKey,
|
|
1009
|
+
{ txHash, headers, createdAt: Date.now() },
|
|
1010
|
+
{ logger, label: "[x402]", onCallbackError: opts.onCallbackError }
|
|
1011
|
+
);
|
|
389
1012
|
}
|
|
390
1013
|
if (txHash && opts.onSettled) {
|
|
391
1014
|
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
@@ -404,6 +1027,51 @@ function createX402Handler(opts) {
|
|
|
404
1027
|
}
|
|
405
1028
|
};
|
|
406
1029
|
}
|
|
1030
|
+
|
|
1031
|
+
// src/internal/encoding.ts
|
|
1032
|
+
var encoder = new TextEncoder();
|
|
1033
|
+
var decoder = new TextDecoder();
|
|
1034
|
+
function utf8ToBytes(text) {
|
|
1035
|
+
return encoder.encode(text);
|
|
1036
|
+
}
|
|
1037
|
+
function bytesToUtf8(bytes) {
|
|
1038
|
+
return decoder.decode(bytes);
|
|
1039
|
+
}
|
|
1040
|
+
function bytesToBase64(bytes) {
|
|
1041
|
+
const chunk = 32768;
|
|
1042
|
+
let binary = "";
|
|
1043
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
1044
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
1045
|
+
}
|
|
1046
|
+
return btoa(binary);
|
|
1047
|
+
}
|
|
1048
|
+
function base64ToBytes(b64) {
|
|
1049
|
+
const end = b64.indexOf("=");
|
|
1050
|
+
let clean = (end === -1 ? b64 : b64.slice(0, end)).replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
|
|
1051
|
+
if (clean.length % 4 === 1) clean = clean.slice(0, -1);
|
|
1052
|
+
const binary = atob(clean);
|
|
1053
|
+
const bytes = new Uint8Array(binary.length);
|
|
1054
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1055
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1056
|
+
}
|
|
1057
|
+
return bytes;
|
|
1058
|
+
}
|
|
1059
|
+
function base64ToUtf8(b64) {
|
|
1060
|
+
return bytesToUtf8(base64ToBytes(b64));
|
|
1061
|
+
}
|
|
1062
|
+
function hexToBytes(hex) {
|
|
1063
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
1064
|
+
if (clean.length % 2 !== 0) {
|
|
1065
|
+
throw new Error("hexToBytes: hex string must have an even length");
|
|
1066
|
+
}
|
|
1067
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
1068
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1069
|
+
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
1070
|
+
}
|
|
1071
|
+
return bytes;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/provider/MppChargeHandler.ts
|
|
407
1075
|
var MPP_NETWORK = {
|
|
408
1076
|
testnet: "stellar:testnet",
|
|
409
1077
|
mainnet: "stellar:pubnet"
|
|
@@ -450,7 +1118,7 @@ function createMppChargeHandler(opts) {
|
|
|
450
1118
|
const credPart = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
|
|
451
1119
|
if (credPart) {
|
|
452
1120
|
const b64 = credPart.split("=").slice(1).join("=").replace(/^"|"$/g, "");
|
|
453
|
-
const credJson =
|
|
1121
|
+
const credJson = base64ToUtf8(b64);
|
|
454
1122
|
const cred = JSON.parse(credJson);
|
|
455
1123
|
const key = cred.sender ?? cred.payload?.sender ?? cred.payload?.from;
|
|
456
1124
|
payerAddress = extractPayerAddress(key);
|
|
@@ -512,18 +1180,19 @@ function createMppChargeHandler(opts) {
|
|
|
512
1180
|
if (receiptHeader) {
|
|
513
1181
|
try {
|
|
514
1182
|
const parsed = JSON.parse(
|
|
515
|
-
|
|
1183
|
+
base64ToUtf8(receiptHeader)
|
|
516
1184
|
);
|
|
517
1185
|
reference = parsed.reference;
|
|
518
1186
|
} catch {
|
|
519
1187
|
}
|
|
520
1188
|
}
|
|
521
1189
|
if (idempotencyKey) {
|
|
522
|
-
await
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
createdAt: Date.now()
|
|
526
|
-
|
|
1190
|
+
await recordSettlement(
|
|
1191
|
+
seenTxStore,
|
|
1192
|
+
idempotencyKey,
|
|
1193
|
+
{ txHash: reference ?? null, headers: receiptHeaders, createdAt: Date.now() },
|
|
1194
|
+
{ logger, label: "[mpp-charge]", onCallbackError: opts.onCallbackError }
|
|
1195
|
+
);
|
|
527
1196
|
}
|
|
528
1197
|
if (reference && opts.onSettled) {
|
|
529
1198
|
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
@@ -537,6 +1206,14 @@ function createMppChargeHandler(opts) {
|
|
|
537
1206
|
}
|
|
538
1207
|
};
|
|
539
1208
|
}
|
|
1209
|
+
|
|
1210
|
+
// src/provider/internal/vaultRpc.ts
|
|
1211
|
+
async function createSorobanRpcServer(rpcUrl) {
|
|
1212
|
+
const { Server } = await import('@stellar/stellar-sdk/rpc');
|
|
1213
|
+
return new Server(rpcUrl);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// src/provider/internal/vaultSettlement.ts
|
|
540
1217
|
function resolveVaultSettlementAddresses(payer, payee) {
|
|
541
1218
|
if (!payer) return null;
|
|
542
1219
|
return { payer, payee };
|
|
@@ -556,6 +1233,111 @@ function buildSessionSettlementTransaction(source, vaultContractId, channelId, a
|
|
|
556
1233
|
networkPassphrase
|
|
557
1234
|
}).addOperation(op).setTimeout(30).build();
|
|
558
1235
|
}
|
|
1236
|
+
var DEFAULT_RPC_URLS = {
|
|
1237
|
+
testnet: "https://soroban-testnet.stellar.org",
|
|
1238
|
+
mainnet: "https://mainnet.sorobanrpc.com"
|
|
1239
|
+
};
|
|
1240
|
+
var CONFIRM_TIMEOUT_MS = 12e4;
|
|
1241
|
+
var CONFIRM_POLL_MS = 1e3;
|
|
1242
|
+
function delay(ms) {
|
|
1243
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1244
|
+
}
|
|
1245
|
+
function describeSendError(sent) {
|
|
1246
|
+
const errorResult = sent.errorResult;
|
|
1247
|
+
try {
|
|
1248
|
+
const name = errorResult?.result?.().switch?.().name;
|
|
1249
|
+
if (name) return `sendTransaction returned ERROR: ${name}`;
|
|
1250
|
+
} catch {
|
|
1251
|
+
}
|
|
1252
|
+
return `sendTransaction returned status ${sent.status}`;
|
|
1253
|
+
}
|
|
1254
|
+
async function waitForConfirmation(server, txHash) {
|
|
1255
|
+
const deadline = Date.now() + CONFIRM_TIMEOUT_MS;
|
|
1256
|
+
for (; ; ) {
|
|
1257
|
+
const tx = await server.getTransaction(txHash);
|
|
1258
|
+
if (tx.status === "SUCCESS") return "success";
|
|
1259
|
+
if (tx.status === "FAILED") return "failed";
|
|
1260
|
+
if (Date.now() >= deadline) return "timeout";
|
|
1261
|
+
await delay(CONFIRM_POLL_MS);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
async function recordSessionSettlement(request, { logger, onCallbackError }) {
|
|
1265
|
+
const reportFailure = (fields) => {
|
|
1266
|
+
logger("error", "[vault] failed to record session_settled", {
|
|
1267
|
+
channelId: request.channelId,
|
|
1268
|
+
...fields.txHash ? { txHash: fields.txHash } : {},
|
|
1269
|
+
reason: fields.reason,
|
|
1270
|
+
...fields.error !== void 0 ? { error: fields.error } : {}
|
|
1271
|
+
});
|
|
1272
|
+
if (onCallbackError) {
|
|
1273
|
+
const err = fields.error ?? new Error(`session_settled vault record failed: ${fields.reason}`);
|
|
1274
|
+
onCallbackError(err, "vaultSettlement");
|
|
1275
|
+
}
|
|
1276
|
+
return { ok: false, reason: fields.reason, ...fields.txHash ? { txHash: fields.txHash } : {} };
|
|
1277
|
+
};
|
|
1278
|
+
try {
|
|
1279
|
+
const payeeKeypair = stellarSdk.Keypair.fromSecret(request.payeeSecretKey);
|
|
1280
|
+
const addresses = resolveVaultSettlementAddresses(request.payer, payeeKeypair.publicKey());
|
|
1281
|
+
if (!addresses) {
|
|
1282
|
+
logger("warn", "[vault] skipped session_settled record: payer address unavailable", {
|
|
1283
|
+
channelId: request.channelId
|
|
1284
|
+
});
|
|
1285
|
+
return { ok: false, reason: "payer address unavailable" };
|
|
1286
|
+
}
|
|
1287
|
+
const networkPassphrase = request.network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
|
|
1288
|
+
const rpcUrl = request.config.rpcUrl ?? DEFAULT_RPC_URLS[request.network];
|
|
1289
|
+
const server = await createSorobanRpcServer(rpcUrl);
|
|
1290
|
+
const sourceAccount = await server.getAccount(payeeKeypair.publicKey());
|
|
1291
|
+
const tx = buildSessionSettlementTransaction(
|
|
1292
|
+
sourceAccount,
|
|
1293
|
+
request.config.contract,
|
|
1294
|
+
request.channelId,
|
|
1295
|
+
addresses,
|
|
1296
|
+
request.amount,
|
|
1297
|
+
request.voucherCount,
|
|
1298
|
+
networkPassphrase
|
|
1299
|
+
);
|
|
1300
|
+
const preparedTx = await server.prepareTransaction(tx);
|
|
1301
|
+
preparedTx.sign(payeeKeypair);
|
|
1302
|
+
const sent = await server.sendTransaction(preparedTx);
|
|
1303
|
+
if (sent.status === "ERROR") {
|
|
1304
|
+
return reportFailure({
|
|
1305
|
+
txHash: sent.hash || preparedTx.hash().toString("hex"),
|
|
1306
|
+
reason: describeSendError(sent)
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
const txHash = sent.hash || preparedTx.hash().toString("hex");
|
|
1310
|
+
const confirmation = await waitForConfirmation(server, txHash);
|
|
1311
|
+
if (confirmation === "success") {
|
|
1312
|
+
logger("debug", "[vault] recorded session_settled", {
|
|
1313
|
+
channelId: request.channelId,
|
|
1314
|
+
txHash
|
|
1315
|
+
});
|
|
1316
|
+
return { ok: true, txHash };
|
|
1317
|
+
}
|
|
1318
|
+
return reportFailure({
|
|
1319
|
+
txHash,
|
|
1320
|
+
reason: confirmation === "failed" ? "transaction failed on-chain" : `confirmation timed out after ${CONFIRM_TIMEOUT_MS}ms`
|
|
1321
|
+
});
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1324
|
+
return reportFailure({ reason, error: err });
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// src/internal/vaultConfig.ts
|
|
1329
|
+
function resolveVaultSettlementConfig(option, logger) {
|
|
1330
|
+
if (option) return option;
|
|
1331
|
+
if (typeof process === "undefined") return void 0;
|
|
1332
|
+
const fromEnv = process.env.AGENT_VAULT_CONTRACT ?? process.env.AGENT_VAULT_CONTRACT_ID;
|
|
1333
|
+
if (!fromEnv) return void 0;
|
|
1334
|
+
logger(
|
|
1335
|
+
"warn",
|
|
1336
|
+
"[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",
|
|
1337
|
+
{}
|
|
1338
|
+
);
|
|
1339
|
+
return { contract: fromEnv };
|
|
1340
|
+
}
|
|
559
1341
|
function channelAuthorizer(envelopeSigner) {
|
|
560
1342
|
return { envelopeSigner };
|
|
561
1343
|
}
|
|
@@ -634,6 +1416,7 @@ function createMppSessionHandler(opts) {
|
|
|
634
1416
|
let record = null;
|
|
635
1417
|
let voucherCount = 0;
|
|
636
1418
|
let sessionOpened = false;
|
|
1419
|
+
let orphanFlagged = false;
|
|
637
1420
|
let idleTimer = null;
|
|
638
1421
|
let settledCleanly = false;
|
|
639
1422
|
let closeListenerArmed = false;
|
|
@@ -659,6 +1442,14 @@ function createMppSessionHandler(opts) {
|
|
|
659
1442
|
return null;
|
|
660
1443
|
}
|
|
661
1444
|
record = { amount: BigInt(stored.amount), signature: stored.signature, payer: stored.payer ?? null };
|
|
1445
|
+
if (typeof stored.voucherCount === "number") {
|
|
1446
|
+
voucherCount = stored.voucherCount;
|
|
1447
|
+
}
|
|
1448
|
+
if (typeof stored.sessionOpened === "boolean") {
|
|
1449
|
+
sessionOpened = stored.sessionOpened;
|
|
1450
|
+
} else if (record !== null) {
|
|
1451
|
+
sessionOpened = true;
|
|
1452
|
+
}
|
|
662
1453
|
} catch {
|
|
663
1454
|
return null;
|
|
664
1455
|
}
|
|
@@ -666,8 +1457,8 @@ function createMppSessionHandler(opts) {
|
|
|
666
1457
|
}
|
|
667
1458
|
async function flagOrphan(reason) {
|
|
668
1459
|
await loadPersistedRecord();
|
|
669
|
-
if (!sessionOpened || settledCleanly) return;
|
|
670
|
-
|
|
1460
|
+
if (!sessionOpened || settledCleanly || orphanFlagged) return;
|
|
1461
|
+
orphanFlagged = true;
|
|
671
1462
|
closeListenerArmed = false;
|
|
672
1463
|
clearIdleTimer();
|
|
673
1464
|
const cumulativeAmount = record ? (Number(record.amount) / 1e7).toFixed(7) : "0.0000000";
|
|
@@ -715,16 +1506,20 @@ function createMppSessionHandler(opts) {
|
|
|
715
1506
|
if (record && amount < record.amount) return;
|
|
716
1507
|
const payer = record?.payer ?? extractPayerAddress(credential.source);
|
|
717
1508
|
record = { amount, signature: payload.signature, payer };
|
|
1509
|
+
voucherCount++;
|
|
1510
|
+
settledCleanly = false;
|
|
1511
|
+
orphanFlagged = false;
|
|
1512
|
+
armIdleTimer();
|
|
1513
|
+
const wasOpened = sessionOpened;
|
|
1514
|
+
sessionOpened = true;
|
|
718
1515
|
await innerStore.put(voucherRecordKey, {
|
|
719
1516
|
amount: record.amount.toString(),
|
|
720
1517
|
signature: record.signature,
|
|
721
|
-
payer: record.payer
|
|
1518
|
+
payer: record.payer,
|
|
1519
|
+
voucherCount,
|
|
1520
|
+
sessionOpened: true
|
|
722
1521
|
});
|
|
723
|
-
|
|
724
|
-
settledCleanly = false;
|
|
725
|
-
armIdleTimer();
|
|
726
|
-
if (!sessionOpened) {
|
|
727
|
-
sessionOpened = true;
|
|
1522
|
+
if (!wasOpened) {
|
|
728
1523
|
if (opts.onSessionOpen) {
|
|
729
1524
|
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
730
1525
|
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
@@ -776,10 +1571,11 @@ function createMppSessionHandler(opts) {
|
|
|
776
1571
|
}
|
|
777
1572
|
if (closeAmount > 0n && closeSig) {
|
|
778
1573
|
const closePayer = record?.payer ?? null;
|
|
1574
|
+
const vouchersClosed = voucherCount;
|
|
779
1575
|
const closeTxHash = await server$3.close({
|
|
780
1576
|
channel: opts.channelFactory,
|
|
781
1577
|
amount: closeAmount,
|
|
782
|
-
signature:
|
|
1578
|
+
signature: hexToBytes(closeSig),
|
|
783
1579
|
feePayer: channelAuthorizer(payeeKeypair),
|
|
784
1580
|
network: networkId
|
|
785
1581
|
});
|
|
@@ -792,46 +1588,28 @@ function createMppSessionHandler(opts) {
|
|
|
792
1588
|
opts.onCallbackError?.(err, "onSettled");
|
|
793
1589
|
});
|
|
794
1590
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1591
|
+
res.json({ closeTxHash });
|
|
1592
|
+
const vaultConfig = resolveVaultSettlementConfig(opts.vaultSettlement, logger);
|
|
1593
|
+
if (vaultConfig) {
|
|
1594
|
+
await recordSessionSettlement(
|
|
1595
|
+
{
|
|
1596
|
+
config: vaultConfig,
|
|
1597
|
+
network: opts.network,
|
|
1598
|
+
payeeSecretKey: opts.payeeSecretKey,
|
|
1599
|
+
channelId: opts.channelFactory,
|
|
1600
|
+
payer: closePayer,
|
|
1601
|
+
amount: closeAmount,
|
|
1602
|
+
voucherCount: vouchersClosed
|
|
1603
|
+
},
|
|
1604
|
+
{ logger, onCallbackError: opts.onCallbackError }
|
|
800
1605
|
);
|
|
801
|
-
if (!settlementAddresses) {
|
|
802
|
-
logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
803
|
-
} else {
|
|
804
|
-
try {
|
|
805
|
-
const { Networks } = await import('@stellar/stellar-sdk');
|
|
806
|
-
const { Server } = await import('@stellar/stellar-sdk/rpc');
|
|
807
|
-
const networkPassphrase = opts.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
808
|
-
const rpcUrl = opts.network === "mainnet" ? "https://mainnet.sorobanrpc.com" : "https://soroban-testnet.stellar.org";
|
|
809
|
-
const server = new Server(rpcUrl);
|
|
810
|
-
const payeePublicKey = payeeKeypair.publicKey();
|
|
811
|
-
const sourceAccount = await server.getAccount(payeePublicKey);
|
|
812
|
-
const tx = buildSessionSettlementTransaction(
|
|
813
|
-
sourceAccount,
|
|
814
|
-
vaultContract,
|
|
815
|
-
opts.channelFactory,
|
|
816
|
-
settlementAddresses,
|
|
817
|
-
closeAmount,
|
|
818
|
-
voucherCount,
|
|
819
|
-
networkPassphrase
|
|
820
|
-
);
|
|
821
|
-
const preparedTx = await server.prepareTransaction(tx);
|
|
822
|
-
preparedTx.sign(payeeKeypair);
|
|
823
|
-
await server.sendTransaction(preparedTx);
|
|
824
|
-
} catch (recordErr) {
|
|
825
|
-
logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
1606
|
}
|
|
829
|
-
res.json({ closeTxHash });
|
|
830
1607
|
} else {
|
|
831
1608
|
res.json({ closeTxHash: null, message: "no vouchers received" });
|
|
832
1609
|
}
|
|
833
1610
|
sessionOpened = false;
|
|
834
1611
|
voucherCount = 0;
|
|
1612
|
+
orphanFlagged = false;
|
|
835
1613
|
record = null;
|
|
836
1614
|
await innerStore.delete(voucherRecordKey);
|
|
837
1615
|
closeListenerArmed = false;
|
|
@@ -863,6 +1641,11 @@ function createMppSessionHandler(opts) {
|
|
|
863
1641
|
}
|
|
864
1642
|
};
|
|
865
1643
|
}
|
|
1644
|
+
function sha256(data) {
|
|
1645
|
+
return sha2_js.sha256(typeof data === "string" ? utf8ToBytes(data) : data);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// src/manifest/sign.ts
|
|
866
1649
|
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
867
1650
|
function canonicalize(value) {
|
|
868
1651
|
if (Array.isArray(value)) {
|
|
@@ -878,7 +1661,7 @@ function canonicalize(value) {
|
|
|
878
1661
|
function manifestDigest(manifest) {
|
|
879
1662
|
const { signature: _omit, ...rest } = manifest;
|
|
880
1663
|
const canonical = JSON.stringify(canonicalize(rest));
|
|
881
|
-
return
|
|
1664
|
+
return sha256(utf8ToBytes(canonical));
|
|
882
1665
|
}
|
|
883
1666
|
function signManifest(manifest, payeeSecretKey) {
|
|
884
1667
|
const keypair = stellarSdk.Keypair.fromSecret(payeeSecretKey);
|
|
@@ -888,7 +1671,7 @@ function signManifest(manifest, payeeSecretKey) {
|
|
|
888
1671
|
};
|
|
889
1672
|
const digest = manifestDigest(versionedManifest);
|
|
890
1673
|
const sig = keypair.sign(digest);
|
|
891
|
-
return { ...versionedManifest, signature:
|
|
1674
|
+
return { ...versionedManifest, signature: bytesToBase64(sig) };
|
|
892
1675
|
}
|
|
893
1676
|
|
|
894
1677
|
// src/provider/fastify.ts
|
|
@@ -1004,6 +1787,7 @@ function runExpressHandler(handler, fastifyRequest, fastifyReply) {
|
|
|
1004
1787
|
}
|
|
1005
1788
|
var MPP_MODES = ["mpp-session", "mpp-charge"];
|
|
1006
1789
|
function routedockFastify(opts) {
|
|
1790
|
+
assertProviderConfig(opts, "routedockFastify");
|
|
1007
1791
|
const signedManifest = signManifest(opts.manifest, opts.payeeSecretKey);
|
|
1008
1792
|
const handlerMap = /* @__PURE__ */ new Map();
|
|
1009
1793
|
if (opts.modes.includes("x402")) {
|
|
@@ -1067,7 +1851,8 @@ function routedockFastify(opts) {
|
|
|
1067
1851
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
1068
1852
|
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
1069
1853
|
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
1070
|
-
...opts.logger ? { logger: opts.logger } : {}
|
|
1854
|
+
...opts.logger ? { logger: opts.logger } : {},
|
|
1855
|
+
...opts.vaultSettlement ? { vaultSettlement: opts.vaultSettlement } : {}
|
|
1071
1856
|
})
|
|
1072
1857
|
);
|
|
1073
1858
|
}
|