@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
package/dist/provider/hono.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Keypair, StrKey } from '@stellar/stellar-sdk';
|
|
1
|
+
import { Keypair, StrKey, Networks, Contract, nativeToScVal, TransactionBuilder, BASE_FEE } from '@stellar/stellar-sdk';
|
|
2
|
+
import { usdcToStroops } from '@routedock/nulth-sdk';
|
|
3
|
+
import { Validator } from '@cfworker/json-schema';
|
|
2
4
|
import { ExactStellarScheme } from '@x402/stellar/exact/facilitator';
|
|
3
5
|
import { ExactStellarScheme as ExactStellarScheme$1 } from '@x402/stellar/exact/server';
|
|
4
6
|
import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server';
|
|
@@ -7,11 +9,10 @@ import { encodePaymentRequiredHeader, decodePaymentSignatureHeader, encodePaymen
|
|
|
7
9
|
import { stellar as stellar$1 } from '@stellar/mpp/charge/server';
|
|
8
10
|
import { Store, stellar, close } from '@stellar/mpp/channel/server';
|
|
9
11
|
import { Mppx } from 'mppx/server';
|
|
10
|
-
import {
|
|
11
|
-
import { usdcToStroops } from '@routedock/nulth-sdk';
|
|
12
|
+
import { sha256 as sha256$1 } from '@noble/hashes/sha2.js';
|
|
12
13
|
import { Errors } from 'mppx';
|
|
13
14
|
|
|
14
|
-
// src/provider/
|
|
15
|
+
// src/provider/internal/assertProviderConfig.ts
|
|
15
16
|
|
|
16
17
|
// src/errors.ts
|
|
17
18
|
var RouteDockError = class extends Error {
|
|
@@ -46,6 +47,658 @@ var RouteDockSignatureError = class extends RouteDockError {
|
|
|
46
47
|
this.name = "RouteDockSignatureError";
|
|
47
48
|
}
|
|
48
49
|
};
|
|
50
|
+
var usdcToUnits = usdcToStroops;
|
|
51
|
+
|
|
52
|
+
// src/schemas/routedock.schema.json
|
|
53
|
+
var routedock_schema_default = {
|
|
54
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
55
|
+
$id: "https://routedock.xyz/schemas/routedock.schema.json",
|
|
56
|
+
title: "RouteDock Manifest",
|
|
57
|
+
description: "Discovery manifest served at /.well-known/routedock.json on every RouteDock provider endpoint",
|
|
58
|
+
type: "object",
|
|
59
|
+
required: [
|
|
60
|
+
"routedock",
|
|
61
|
+
"name",
|
|
62
|
+
"description",
|
|
63
|
+
"modes",
|
|
64
|
+
"network",
|
|
65
|
+
"asset",
|
|
66
|
+
"asset_contract",
|
|
67
|
+
"payee",
|
|
68
|
+
"pricing",
|
|
69
|
+
"endpoints",
|
|
70
|
+
"tags"
|
|
71
|
+
],
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
properties: {
|
|
74
|
+
routedock: {
|
|
75
|
+
type: "string",
|
|
76
|
+
pattern: "^[0-9]+\\.[0-9]+$",
|
|
77
|
+
description: "Manifest schema version, e.g. '1.0', '1.1', '2.0'."
|
|
78
|
+
},
|
|
79
|
+
min_client_version: {
|
|
80
|
+
type: "string",
|
|
81
|
+
pattern: "^[0-9]+\\.[0-9]+$",
|
|
82
|
+
description: "Minimum SDK client version (major.minor) required to use this manifest. Clients below this version must refuse to pay."
|
|
83
|
+
},
|
|
84
|
+
deprecated_modes: {
|
|
85
|
+
type: "array",
|
|
86
|
+
items: {
|
|
87
|
+
$ref: "#/definitions/PaymentMode"
|
|
88
|
+
},
|
|
89
|
+
uniqueItems: true,
|
|
90
|
+
description: "Payment modes still listed in 'modes' for backwards compatibility but deprecated for new integrations."
|
|
91
|
+
},
|
|
92
|
+
sunset_at: {
|
|
93
|
+
type: "string",
|
|
94
|
+
format: "date-time",
|
|
95
|
+
description: "ISO 8601 timestamp after which this manifest version is no longer supported by the provider."
|
|
96
|
+
},
|
|
97
|
+
name: {
|
|
98
|
+
type: "string",
|
|
99
|
+
minLength: 1,
|
|
100
|
+
maxLength: 120,
|
|
101
|
+
description: "Human-readable provider name"
|
|
102
|
+
},
|
|
103
|
+
description: {
|
|
104
|
+
type: "string",
|
|
105
|
+
minLength: 1,
|
|
106
|
+
maxLength: 500,
|
|
107
|
+
description: "Human-readable description of the provider's data or service"
|
|
108
|
+
},
|
|
109
|
+
modes: {
|
|
110
|
+
type: "array",
|
|
111
|
+
minItems: 1,
|
|
112
|
+
uniqueItems: true,
|
|
113
|
+
items: {
|
|
114
|
+
$ref: "#/definitions/PaymentMode"
|
|
115
|
+
},
|
|
116
|
+
description: "Payment modes supported by this provider"
|
|
117
|
+
},
|
|
118
|
+
network: {
|
|
119
|
+
type: "string",
|
|
120
|
+
enum: ["testnet", "mainnet"],
|
|
121
|
+
description: "Stellar network this provider operates on"
|
|
122
|
+
},
|
|
123
|
+
asset: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "Asset ticker symbol (e.g. 'USDC')"
|
|
126
|
+
},
|
|
127
|
+
asset_contract: {
|
|
128
|
+
type: "string",
|
|
129
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
130
|
+
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
131
|
+
},
|
|
132
|
+
assets: {
|
|
133
|
+
type: "array",
|
|
134
|
+
minItems: 1,
|
|
135
|
+
items: {
|
|
136
|
+
$ref: "#/definitions/AssetConfig"
|
|
137
|
+
},
|
|
138
|
+
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."
|
|
139
|
+
},
|
|
140
|
+
payee: {
|
|
141
|
+
type: "string",
|
|
142
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
143
|
+
description: "Stellar address (G...) that receives payments"
|
|
144
|
+
},
|
|
145
|
+
pricing: {
|
|
146
|
+
type: "object",
|
|
147
|
+
description: "Pricing configuration per supported payment mode",
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
properties: {
|
|
150
|
+
x402: {
|
|
151
|
+
$ref: "#/definitions/PricingConfig"
|
|
152
|
+
},
|
|
153
|
+
"mpp-charge": {
|
|
154
|
+
$ref: "#/definitions/PricingConfig"
|
|
155
|
+
},
|
|
156
|
+
"mpp-session": {
|
|
157
|
+
$ref: "#/definitions/SessionPricingConfig"
|
|
158
|
+
},
|
|
159
|
+
"mpp-session-ws": {
|
|
160
|
+
$ref: "#/definitions/SessionPricingConfig",
|
|
161
|
+
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."
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
minProperties: 1
|
|
165
|
+
},
|
|
166
|
+
endpoints: {
|
|
167
|
+
type: "object",
|
|
168
|
+
description: "Map of endpoint name to endpoint descriptors",
|
|
169
|
+
additionalProperties: {
|
|
170
|
+
$ref: "#/definitions/EndpointDescriptor"
|
|
171
|
+
},
|
|
172
|
+
minProperties: 1
|
|
173
|
+
},
|
|
174
|
+
sla: {
|
|
175
|
+
$ref: "#/definitions/SLAConfig"
|
|
176
|
+
},
|
|
177
|
+
tags: {
|
|
178
|
+
type: "array",
|
|
179
|
+
items: {
|
|
180
|
+
type: "string",
|
|
181
|
+
minLength: 1,
|
|
182
|
+
maxLength: 64
|
|
183
|
+
},
|
|
184
|
+
uniqueItems: true,
|
|
185
|
+
description: "Capability tags indexed with trigram search in the provider registry"
|
|
186
|
+
},
|
|
187
|
+
vault: {
|
|
188
|
+
$ref: "#/definitions/VaultMode"
|
|
189
|
+
},
|
|
190
|
+
nulth_account: {
|
|
191
|
+
type: "string",
|
|
192
|
+
description: "Optional Nulth smart account address associated with this provider"
|
|
193
|
+
},
|
|
194
|
+
capabilities: {
|
|
195
|
+
type: "object",
|
|
196
|
+
description: "Optional protocol features this provider supports",
|
|
197
|
+
properties: {
|
|
198
|
+
streaming: {
|
|
199
|
+
type: "array",
|
|
200
|
+
items: {
|
|
201
|
+
type: "string",
|
|
202
|
+
enum: ["sse", "websocket"]
|
|
203
|
+
},
|
|
204
|
+
description: "Supported streaming transports"
|
|
205
|
+
},
|
|
206
|
+
webhooks: {
|
|
207
|
+
type: "boolean",
|
|
208
|
+
description: "Whether webhook callbacks are supported"
|
|
209
|
+
},
|
|
210
|
+
idempotency_keys: {
|
|
211
|
+
type: "boolean",
|
|
212
|
+
description: "Whether idempotency keys are supported"
|
|
213
|
+
},
|
|
214
|
+
content_types: {
|
|
215
|
+
type: "array",
|
|
216
|
+
items: {
|
|
217
|
+
type: "string"
|
|
218
|
+
},
|
|
219
|
+
description: "Supported content types for request/response"
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
additionalProperties: false
|
|
223
|
+
},
|
|
224
|
+
regions: {
|
|
225
|
+
type: "array",
|
|
226
|
+
items: {
|
|
227
|
+
type: "string",
|
|
228
|
+
minLength: 2,
|
|
229
|
+
maxLength: 10
|
|
230
|
+
},
|
|
231
|
+
uniqueItems: true,
|
|
232
|
+
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."
|
|
233
|
+
},
|
|
234
|
+
latency_hints: {
|
|
235
|
+
type: "object",
|
|
236
|
+
additionalProperties: {
|
|
237
|
+
type: "number",
|
|
238
|
+
minimum: 0
|
|
239
|
+
},
|
|
240
|
+
description: "Provider-declared p50 latency in milliseconds per region code. Keys must be a subset of 'regions'. Example: { 'IAD': 14, 'AMS': 22 }."
|
|
241
|
+
},
|
|
242
|
+
signature: {
|
|
243
|
+
type: "string",
|
|
244
|
+
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."
|
|
245
|
+
},
|
|
246
|
+
signature_version: {
|
|
247
|
+
type: "string",
|
|
248
|
+
const: "2",
|
|
249
|
+
description: "Manifest signature format. Version 2 recursively includes every nested manifest field in the signed digest."
|
|
250
|
+
},
|
|
251
|
+
categories: {
|
|
252
|
+
type: "array",
|
|
253
|
+
items: {
|
|
254
|
+
type: "string",
|
|
255
|
+
minLength: 1,
|
|
256
|
+
maxLength: 128
|
|
257
|
+
},
|
|
258
|
+
uniqueItems: true,
|
|
259
|
+
description: "Optional hierarchical categories from a published taxonomy (e.g. data/price/crypto)"
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
definitions: {
|
|
263
|
+
VaultMode: {
|
|
264
|
+
type: "string",
|
|
265
|
+
enum: ["local-key", "agent-vault", "nulth"],
|
|
266
|
+
description: "Agent vault / custody mode for payments from this provider"
|
|
267
|
+
},
|
|
268
|
+
PaymentMode: {
|
|
269
|
+
type: "string",
|
|
270
|
+
enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
|
|
271
|
+
description: "A supported RouteDock payment mode"
|
|
272
|
+
},
|
|
273
|
+
AssetConfig: {
|
|
274
|
+
type: "object",
|
|
275
|
+
description: "Payment asset configuration with optional mode and endpoint scoping",
|
|
276
|
+
required: ["asset", "asset_contract"],
|
|
277
|
+
additionalProperties: false,
|
|
278
|
+
properties: {
|
|
279
|
+
asset: {
|
|
280
|
+
type: "string",
|
|
281
|
+
description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
|
|
282
|
+
},
|
|
283
|
+
asset_contract: {
|
|
284
|
+
type: "string",
|
|
285
|
+
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
286
|
+
},
|
|
287
|
+
modes: {
|
|
288
|
+
type: "array",
|
|
289
|
+
items: {
|
|
290
|
+
$ref: "#/definitions/PaymentMode"
|
|
291
|
+
},
|
|
292
|
+
uniqueItems: true,
|
|
293
|
+
description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
|
|
294
|
+
},
|
|
295
|
+
endpoints: {
|
|
296
|
+
type: "array",
|
|
297
|
+
items: {
|
|
298
|
+
type: "string"
|
|
299
|
+
},
|
|
300
|
+
uniqueItems: true,
|
|
301
|
+
description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
PricingConfig: {
|
|
306
|
+
type: "object",
|
|
307
|
+
description: "Per-request pricing for x402 or mpp-charge modes",
|
|
308
|
+
required: ["amount", "per"],
|
|
309
|
+
additionalProperties: false,
|
|
310
|
+
properties: {
|
|
311
|
+
amount: {
|
|
312
|
+
type: "string",
|
|
313
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
314
|
+
description: "Cost per request in the payment asset (decimal string, e.g. '0.001')"
|
|
315
|
+
},
|
|
316
|
+
per: {
|
|
317
|
+
type: "string",
|
|
318
|
+
const: "request",
|
|
319
|
+
description: "Billing unit \u2014 always 'request' for non-session modes"
|
|
320
|
+
},
|
|
321
|
+
facilitator: {
|
|
322
|
+
type: "string",
|
|
323
|
+
description: "x402 facilitator URL (required for x402 mode). OpenZeppelin Channels endpoint."
|
|
324
|
+
},
|
|
325
|
+
payee: {
|
|
326
|
+
type: "string",
|
|
327
|
+
pattern: "^G[A-Z2-7]{55}$",
|
|
328
|
+
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."
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
SessionPricingConfig: {
|
|
333
|
+
type: "object",
|
|
334
|
+
description: "Per-voucher pricing for mpp-session (one-way-channel) mode",
|
|
335
|
+
required: ["rate", "per", "channel_factory", "min_deposit", "refund_waiting_period_ledgers"],
|
|
336
|
+
additionalProperties: false,
|
|
337
|
+
properties: {
|
|
338
|
+
rate: {
|
|
339
|
+
type: "string",
|
|
340
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
341
|
+
description: "Cost per voucher in the payment asset (decimal string, e.g. '0.0001')"
|
|
342
|
+
},
|
|
343
|
+
per: {
|
|
344
|
+
type: "string",
|
|
345
|
+
const: "voucher",
|
|
346
|
+
description: "Billing unit \u2014 always 'voucher' for session mode"
|
|
347
|
+
},
|
|
348
|
+
channel_factory: {
|
|
349
|
+
type: "string",
|
|
350
|
+
pattern: "^C[A-Z2-7]{55}$",
|
|
351
|
+
description: "Stable Soroban factory address (C...) used to deploy a per-session channel"
|
|
352
|
+
},
|
|
353
|
+
min_deposit: {
|
|
354
|
+
type: "string",
|
|
355
|
+
pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
|
|
356
|
+
description: "Minimum initial deposit to open a channel (decimal string)"
|
|
357
|
+
},
|
|
358
|
+
refund_waiting_period_ledgers: {
|
|
359
|
+
type: "integer",
|
|
360
|
+
minimum: 1,
|
|
361
|
+
description: "Ledgers the server has to settle before the funder can reclaim. Default: 17280 (~24h). Do not reduce in production."
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
SLAConfig: {
|
|
366
|
+
type: "object",
|
|
367
|
+
description: "Service Level Agreement for the provider",
|
|
368
|
+
required: ["uptime_30d_percent", "p95_latency_ms"],
|
|
369
|
+
additionalProperties: false,
|
|
370
|
+
properties: {
|
|
371
|
+
uptime_30d_percent: {
|
|
372
|
+
type: "number",
|
|
373
|
+
description: "Uptime percentage over the last 30 days"
|
|
374
|
+
},
|
|
375
|
+
p95_latency_ms: {
|
|
376
|
+
type: "number",
|
|
377
|
+
description: "P95 latency in milliseconds"
|
|
378
|
+
},
|
|
379
|
+
maintenance_windows: {
|
|
380
|
+
type: "array",
|
|
381
|
+
items: {
|
|
382
|
+
type: "object",
|
|
383
|
+
required: ["cron", "duration_minutes"],
|
|
384
|
+
additionalProperties: false,
|
|
385
|
+
properties: {
|
|
386
|
+
cron: {
|
|
387
|
+
type: "string",
|
|
388
|
+
description: "Cron expression for the maintenance window"
|
|
389
|
+
},
|
|
390
|
+
duration_minutes: {
|
|
391
|
+
type: "number",
|
|
392
|
+
description: "Duration of the maintenance window in minutes"
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
EndpointDescriptor: {
|
|
400
|
+
type: "object",
|
|
401
|
+
description: "Descriptor for a provider endpoint",
|
|
402
|
+
required: ["method", "path"],
|
|
403
|
+
additionalProperties: false,
|
|
404
|
+
properties: {
|
|
405
|
+
method: {
|
|
406
|
+
type: "string",
|
|
407
|
+
description: "HTTP method for the endpoint"
|
|
408
|
+
},
|
|
409
|
+
path: {
|
|
410
|
+
type: "string",
|
|
411
|
+
description: "Path for the endpoint"
|
|
412
|
+
},
|
|
413
|
+
deprecated: {
|
|
414
|
+
type: "boolean",
|
|
415
|
+
description: "Whether this endpoint is retained only for backwards compatibility"
|
|
416
|
+
},
|
|
417
|
+
sunset_at: {
|
|
418
|
+
type: "string",
|
|
419
|
+
format: "date-time",
|
|
420
|
+
description: "ISO 8601 timestamp after which callers should stop using this endpoint"
|
|
421
|
+
},
|
|
422
|
+
headers: {
|
|
423
|
+
type: "object",
|
|
424
|
+
additionalProperties: {
|
|
425
|
+
type: "string"
|
|
426
|
+
},
|
|
427
|
+
description: "Expected request headers"
|
|
428
|
+
},
|
|
429
|
+
request_schema: {
|
|
430
|
+
description: "JSON schema for the request body"
|
|
431
|
+
},
|
|
432
|
+
response_schema: {
|
|
433
|
+
description: "JSON schema for the response body"
|
|
434
|
+
},
|
|
435
|
+
rate_limit: {
|
|
436
|
+
type: "object",
|
|
437
|
+
required: ["requests", "window_seconds"],
|
|
438
|
+
additionalProperties: false,
|
|
439
|
+
properties: {
|
|
440
|
+
requests: {
|
|
441
|
+
type: "number"
|
|
442
|
+
},
|
|
443
|
+
window_seconds: {
|
|
444
|
+
type: "number"
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// src/manifest/validate.ts
|
|
454
|
+
var validator = new Validator(routedock_schema_default, "7");
|
|
455
|
+
function validateManifestSchema(manifest, source) {
|
|
456
|
+
const result = validator.validate(manifest);
|
|
457
|
+
if (!result.valid) {
|
|
458
|
+
const msgs = result.errors.map((err) => err.error).join("; ");
|
|
459
|
+
throw new RouteDockManifestError(`Invalid manifest${""}: ${msgs}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function assertManifestValid(manifest, baseUrl) {
|
|
463
|
+
const context = "";
|
|
464
|
+
if (manifest.latency_hints !== void 0) {
|
|
465
|
+
if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
|
|
466
|
+
throw new RouteDockManifestError(
|
|
467
|
+
`Invalid manifest${context}: latency_hints defined without declaring regions`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
const regionSet = new Set(manifest.regions);
|
|
471
|
+
for (const region of Object.keys(manifest.latency_hints)) {
|
|
472
|
+
if (!regionSet.has(region)) {
|
|
473
|
+
throw new RouteDockManifestError(
|
|
474
|
+
`Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (manifest.assets !== void 0) {
|
|
480
|
+
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
481
|
+
throw new RouteDockManifestError(
|
|
482
|
+
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
for (let i = 0; i < manifest.assets.length; i++) {
|
|
486
|
+
const a = manifest.assets[i];
|
|
487
|
+
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
488
|
+
throw new RouteDockManifestError(
|
|
489
|
+
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const first = manifest.assets[0];
|
|
494
|
+
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
495
|
+
throw new RouteDockManifestError(
|
|
496
|
+
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
assertPricingMatchesModes(manifest, context);
|
|
501
|
+
assertDeprecatedModesListed(manifest, context);
|
|
502
|
+
assertPricesParse(manifest, context);
|
|
503
|
+
}
|
|
504
|
+
function assertPricingMatchesModes(manifest, context) {
|
|
505
|
+
const pricing = manifest.pricing ?? {};
|
|
506
|
+
for (const mode of manifest.modes ?? []) {
|
|
507
|
+
if (pricing[mode] === void 0 || pricing[mode] === null) {
|
|
508
|
+
throw new RouteDockManifestError(
|
|
509
|
+
`Invalid manifest${context}: mode '${mode}' is listed in modes but pricing.${mode} is missing`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function assertDeprecatedModesListed(manifest, context) {
|
|
515
|
+
if (manifest.deprecated_modes === void 0) return;
|
|
516
|
+
const modes = new Set(manifest.modes ?? []);
|
|
517
|
+
for (const mode of manifest.deprecated_modes) {
|
|
518
|
+
if (!modes.has(mode)) {
|
|
519
|
+
throw new RouteDockManifestError(
|
|
520
|
+
`Invalid manifest${context}: deprecated_modes entry '${mode}' is not listed in modes`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
var PRICE_FIELDS = {
|
|
526
|
+
x402: ["amount"],
|
|
527
|
+
"mpp-charge": ["amount"],
|
|
528
|
+
"mpp-session": ["rate", "min_deposit"],
|
|
529
|
+
"mpp-session-ws": ["rate", "min_deposit"]
|
|
530
|
+
};
|
|
531
|
+
function assertPricesParse(manifest, context) {
|
|
532
|
+
const pricing = manifest.pricing ?? {};
|
|
533
|
+
for (const [mode, fields] of Object.entries(PRICE_FIELDS)) {
|
|
534
|
+
const entry = pricing[mode];
|
|
535
|
+
if (entry === void 0 || entry === null) continue;
|
|
536
|
+
for (const field of fields) {
|
|
537
|
+
const value = entry[field];
|
|
538
|
+
try {
|
|
539
|
+
if (typeof value !== "string") {
|
|
540
|
+
throw new RangeError(`expected a decimal string, got ${typeof value}`);
|
|
541
|
+
}
|
|
542
|
+
usdcToStroops(value);
|
|
543
|
+
} catch (err) {
|
|
544
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
545
|
+
throw new RouteDockManifestError(
|
|
546
|
+
`Invalid manifest${context}: pricing.${mode}.${field} ${JSON.stringify(value)} is not a valid amount (${detail})`,
|
|
547
|
+
{ cause: err }
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/provider/internal/assertProviderConfig.ts
|
|
555
|
+
var FLAT_MODES = ["x402", "mpp-charge"];
|
|
556
|
+
var SESSION_MODES = ["mpp-session", "mpp-session-ws"];
|
|
557
|
+
function sameAmount(adapter, label, optionValue, manifestValue) {
|
|
558
|
+
const parse = (where, value) => {
|
|
559
|
+
try {
|
|
560
|
+
return usdcToStroops(value);
|
|
561
|
+
} catch {
|
|
562
|
+
throw new RouteDockManifestError(
|
|
563
|
+
`${adapter}: ${label} has an unparseable ${where} amount "${value}"`
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
return parse("option", optionValue) === parse("manifest", manifestValue);
|
|
568
|
+
}
|
|
569
|
+
function assertProviderConfig(opts, adapterName) {
|
|
570
|
+
const { manifest } = opts;
|
|
571
|
+
if (manifest === null || typeof manifest !== "object") {
|
|
572
|
+
throw new RouteDockManifestError(`${adapterName}: opts.manifest is required and must be an object`);
|
|
573
|
+
}
|
|
574
|
+
try {
|
|
575
|
+
validateManifestSchema(manifest);
|
|
576
|
+
assertManifestValid(manifest);
|
|
577
|
+
} catch (err) {
|
|
578
|
+
if (err instanceof RouteDockManifestError) {
|
|
579
|
+
throw new RouteDockManifestError(`${adapterName}: ${err.message}`, { cause: err });
|
|
580
|
+
}
|
|
581
|
+
throw err;
|
|
582
|
+
}
|
|
583
|
+
const served = new Set(opts.modes.filter((mode) => Boolean(opts.pricing[mode])));
|
|
584
|
+
const advertised = new Set(manifest.modes);
|
|
585
|
+
const notAdvertised = [...served].filter((mode) => !advertised.has(mode));
|
|
586
|
+
const notServed = [...advertised].filter((mode) => !served.has(mode));
|
|
587
|
+
if (notAdvertised.length > 0 || notServed.length > 0) {
|
|
588
|
+
const parts = [];
|
|
589
|
+
if (notAdvertised.length > 0) parts.push(`served but not in manifest.modes: ${notAdvertised.join(", ")}`);
|
|
590
|
+
if (notServed.length > 0) parts.push(`in manifest.modes but not served (missing from modes or pricing): ${notServed.join(", ")}`);
|
|
591
|
+
throw new RouteDockManifestError(`${adapterName}: modes do not match manifest.modes (${parts.join("; ")})`);
|
|
592
|
+
}
|
|
593
|
+
for (const mode of FLAT_MODES) {
|
|
594
|
+
if (!served.has(mode)) continue;
|
|
595
|
+
const optionValue = opts.pricing[mode];
|
|
596
|
+
const manifestValue = manifest.pricing[mode]?.amount;
|
|
597
|
+
if (manifestValue === void 0) {
|
|
598
|
+
throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode}.amount is missing but ${mode} is served`);
|
|
599
|
+
}
|
|
600
|
+
if (!sameAmount(adapterName, `pricing.${mode}`, optionValue, manifestValue)) {
|
|
601
|
+
throw new RouteDockManifestError(
|
|
602
|
+
`${adapterName}: pricing.${mode} "${optionValue}" does not match manifest.pricing.${mode}.amount "${manifestValue}"`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
for (const mode of SESSION_MODES) {
|
|
607
|
+
if (!served.has(mode)) continue;
|
|
608
|
+
const optionValue = opts.pricing[mode];
|
|
609
|
+
const manifestValue = manifest.pricing[mode];
|
|
610
|
+
if (!manifestValue) {
|
|
611
|
+
throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode} is missing but ${mode} is served`);
|
|
612
|
+
}
|
|
613
|
+
if (!sameAmount(adapterName, `pricing.${mode}.rate`, optionValue.rate, manifestValue.rate)) {
|
|
614
|
+
throw new RouteDockManifestError(
|
|
615
|
+
`${adapterName}: pricing.${mode}.rate "${optionValue.rate}" does not match manifest.pricing.${mode}.rate "${manifestValue.rate}"`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
if (optionValue.channelFactory !== manifestValue.channel_factory) {
|
|
619
|
+
throw new RouteDockManifestError(
|
|
620
|
+
`${adapterName}: pricing.${mode}.channelFactory "${optionValue.channelFactory}" does not match manifest.pricing.${mode}.channel_factory "${manifestValue.channel_factory}"`
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (opts.assetContract !== void 0 && opts.assetContract !== manifest.asset_contract) {
|
|
625
|
+
throw new RouteDockManifestError(
|
|
626
|
+
`${adapterName}: assetContract "${opts.assetContract}" does not match manifest.asset_contract "${manifest.asset_contract}"`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
if (opts.asset !== void 0 && opts.asset !== manifest.asset) {
|
|
630
|
+
throw new RouteDockManifestError(
|
|
631
|
+
`${adapterName}: asset "${opts.asset}" does not match manifest.asset "${manifest.asset}"`
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (opts.network !== manifest.network) {
|
|
635
|
+
throw new RouteDockManifestError(
|
|
636
|
+
`${adapterName}: network "${opts.network}" does not match manifest.network "${manifest.network}"`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
let signerPublicKey;
|
|
640
|
+
try {
|
|
641
|
+
signerPublicKey = Keypair.fromSecret(opts.payeeSecretKey).publicKey();
|
|
642
|
+
} catch (err) {
|
|
643
|
+
throw new RouteDockManifestError(`${adapterName}: payeeSecretKey is not a valid Stellar secret key`, { cause: err });
|
|
644
|
+
}
|
|
645
|
+
if (signerPublicKey !== manifest.payee) {
|
|
646
|
+
throw new RouteDockManifestError(
|
|
647
|
+
`${adapterName}: payeeSecretKey belongs to ${signerPublicKey}, not manifest.payee ${manifest.payee}; clients would reject the signed manifest`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (opts.payee !== manifest.payee) {
|
|
651
|
+
throw new RouteDockManifestError(
|
|
652
|
+
`${adapterName}: payee "${opts.payee}" does not match manifest.payee "${manifest.payee}"`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/internal/encoding.ts
|
|
658
|
+
var encoder = new TextEncoder();
|
|
659
|
+
var decoder = new TextDecoder();
|
|
660
|
+
function utf8ToBytes(text) {
|
|
661
|
+
return encoder.encode(text);
|
|
662
|
+
}
|
|
663
|
+
function bytesToUtf8(bytes) {
|
|
664
|
+
return decoder.decode(bytes);
|
|
665
|
+
}
|
|
666
|
+
function bytesToBase64(bytes) {
|
|
667
|
+
const chunk = 32768;
|
|
668
|
+
let binary = "";
|
|
669
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
670
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
671
|
+
}
|
|
672
|
+
return btoa(binary);
|
|
673
|
+
}
|
|
674
|
+
function base64ToBytes(b64) {
|
|
675
|
+
const end = b64.indexOf("=");
|
|
676
|
+
let clean = (end === -1 ? b64 : b64.slice(0, end)).replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
|
|
677
|
+
if (clean.length % 4 === 1) clean = clean.slice(0, -1);
|
|
678
|
+
const binary = atob(clean);
|
|
679
|
+
const bytes = new Uint8Array(binary.length);
|
|
680
|
+
for (let i = 0; i < binary.length; i++) {
|
|
681
|
+
bytes[i] = binary.charCodeAt(i);
|
|
682
|
+
}
|
|
683
|
+
return bytes;
|
|
684
|
+
}
|
|
685
|
+
function base64ToUtf8(b64) {
|
|
686
|
+
return bytesToUtf8(base64ToBytes(b64));
|
|
687
|
+
}
|
|
688
|
+
function hexToBytes(hex) {
|
|
689
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
690
|
+
if (clean.length % 2 !== 0) {
|
|
691
|
+
throw new Error("hexToBytes: hex string must have an even length");
|
|
692
|
+
}
|
|
693
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
694
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
695
|
+
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
696
|
+
}
|
|
697
|
+
return bytes;
|
|
698
|
+
}
|
|
699
|
+
function sha256(data) {
|
|
700
|
+
return sha256$1(typeof data === "string" ? utf8ToBytes(data) : data);
|
|
701
|
+
}
|
|
49
702
|
|
|
50
703
|
// src/manifest/sign.ts
|
|
51
704
|
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
@@ -63,7 +716,7 @@ function canonicalize(value) {
|
|
|
63
716
|
function manifestDigest(manifest) {
|
|
64
717
|
const { signature: _omit, ...rest } = manifest;
|
|
65
718
|
const canonical = JSON.stringify(canonicalize(rest));
|
|
66
|
-
return
|
|
719
|
+
return sha256(utf8ToBytes(canonical));
|
|
67
720
|
}
|
|
68
721
|
function signManifest(manifest, payeeSecretKey) {
|
|
69
722
|
const keypair = Keypair.fromSecret(payeeSecretKey);
|
|
@@ -73,7 +726,7 @@ function signManifest(manifest, payeeSecretKey) {
|
|
|
73
726
|
};
|
|
74
727
|
const digest = manifestDigest(versionedManifest);
|
|
75
728
|
const sig = keypair.sign(digest);
|
|
76
|
-
return { ...versionedManifest, signature:
|
|
729
|
+
return { ...versionedManifest, signature: bytesToBase64(sig) };
|
|
77
730
|
}
|
|
78
731
|
function verifyManifestSignature(manifest, expectedPayee) {
|
|
79
732
|
if (!manifest.signature) {
|
|
@@ -86,11 +739,19 @@ function verifyManifestSignature(manifest, expectedPayee) {
|
|
|
86
739
|
`Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
|
|
87
740
|
);
|
|
88
741
|
}
|
|
89
|
-
|
|
742
|
+
let keypair;
|
|
743
|
+
try {
|
|
744
|
+
keypair = Keypair.fromPublicKey(manifest.payee);
|
|
745
|
+
} catch (err) {
|
|
746
|
+
throw new RouteDockSignatureError(
|
|
747
|
+
`Manifest payee ${manifest.payee} is not a valid Stellar public key`,
|
|
748
|
+
{ cause: err }
|
|
749
|
+
);
|
|
750
|
+
}
|
|
90
751
|
const digest = manifestDigest(manifest);
|
|
91
752
|
let sigBytes;
|
|
92
753
|
try {
|
|
93
|
-
sigBytes =
|
|
754
|
+
sigBytes = base64ToBytes(manifest.signature);
|
|
94
755
|
} catch {
|
|
95
756
|
throw new RouteDockSignatureError("Manifest signature is not valid base64");
|
|
96
757
|
}
|
|
@@ -114,7 +775,6 @@ function resolvePayee(manifest, mode) {
|
|
|
114
775
|
const override = manifest.pricing[mode]?.payee;
|
|
115
776
|
return override ?? manifest.payee;
|
|
116
777
|
}
|
|
117
|
-
var usdcToUnits = usdcToStroops;
|
|
118
778
|
function extractPayerAddress(key) {
|
|
119
779
|
if (typeof key !== "string") return null;
|
|
120
780
|
return StrKey.isValidEd25519PublicKey(key) || StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
@@ -198,30 +858,21 @@ function mapChannelError(error) {
|
|
|
198
858
|
return new Errors.VerificationFailedError({ reason });
|
|
199
859
|
}
|
|
200
860
|
|
|
201
|
-
// src/provider/encoding.ts
|
|
202
|
-
function base64ToUtf8(b64) {
|
|
203
|
-
const binary = atob(b64);
|
|
204
|
-
const bytes = new Uint8Array(binary.length);
|
|
205
|
-
for (let i = 0; i < binary.length; i++) {
|
|
206
|
-
bytes[i] = binary.charCodeAt(i);
|
|
207
|
-
}
|
|
208
|
-
return new TextDecoder().decode(bytes);
|
|
209
|
-
}
|
|
210
|
-
function hexToBytes(hex) {
|
|
211
|
-
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
212
|
-
if (clean.length % 2 !== 0) {
|
|
213
|
-
throw new Error("hexToBytes: hex string must have an even length");
|
|
214
|
-
}
|
|
215
|
-
const bytes = new Uint8Array(clean.length / 2);
|
|
216
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
217
|
-
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
218
|
-
}
|
|
219
|
-
return bytes;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
861
|
// src/provider/SeenTxStore.ts
|
|
223
862
|
var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
|
|
224
863
|
var MAX_SETTLEMENT_REPLAYS = 1;
|
|
864
|
+
async function recordSettlement(store, key, record, ctx) {
|
|
865
|
+
try {
|
|
866
|
+
await store.set(key, record);
|
|
867
|
+
} catch (err) {
|
|
868
|
+
ctx.logger("error", `${ctx.label} seenTxStore.set failed after settlement`, { error: err });
|
|
869
|
+
try {
|
|
870
|
+
ctx.onCallbackError?.(err, "seenTxStore.set");
|
|
871
|
+
} catch (cbErr) {
|
|
872
|
+
ctx.logger("error", `${ctx.label} onCallbackError threw`, { error: cbErr });
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
225
876
|
async function checkSettlementReplay(store, key, now = Date.now()) {
|
|
226
877
|
const record = await store.get(key);
|
|
227
878
|
if (!record) return { kind: "miss" };
|
|
@@ -361,7 +1012,7 @@ function selectAsset(manifest, mode, endpoint) {
|
|
|
361
1012
|
return eligible[0];
|
|
362
1013
|
}
|
|
363
1014
|
|
|
364
|
-
// src/
|
|
1015
|
+
// src/internal/settleResult.ts
|
|
365
1016
|
function readSettleResult(result) {
|
|
366
1017
|
const settle = result;
|
|
367
1018
|
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
@@ -372,6 +1023,138 @@ function readSettleResult(result) {
|
|
|
372
1023
|
return { ok: true, txHash: settle.transaction };
|
|
373
1024
|
}
|
|
374
1025
|
|
|
1026
|
+
// src/provider/internal/vaultRpc.ts
|
|
1027
|
+
async function createSorobanRpcServer(rpcUrl) {
|
|
1028
|
+
const { Server } = await import('@stellar/stellar-sdk/rpc');
|
|
1029
|
+
return new Server(rpcUrl);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/provider/internal/vaultSettlement.ts
|
|
1033
|
+
function resolveVaultSettlementAddresses(payer, payee) {
|
|
1034
|
+
if (!payer) return null;
|
|
1035
|
+
return { payer, payee };
|
|
1036
|
+
}
|
|
1037
|
+
function buildSessionSettlementTransaction(source, vaultContractId, channelId, addresses, amount, voucherCount, networkPassphrase) {
|
|
1038
|
+
const vault = new Contract(vaultContractId);
|
|
1039
|
+
const op = vault.call(
|
|
1040
|
+
"record_session_settlement",
|
|
1041
|
+
nativeToScVal(channelId, { type: "address" }),
|
|
1042
|
+
nativeToScVal(addresses.payer, { type: "address" }),
|
|
1043
|
+
nativeToScVal(addresses.payee, { type: "address" }),
|
|
1044
|
+
nativeToScVal(amount, { type: "i128" }),
|
|
1045
|
+
nativeToScVal(voucherCount, { type: "u32" })
|
|
1046
|
+
);
|
|
1047
|
+
return new TransactionBuilder(source, {
|
|
1048
|
+
fee: BASE_FEE,
|
|
1049
|
+
networkPassphrase
|
|
1050
|
+
}).addOperation(op).setTimeout(30).build();
|
|
1051
|
+
}
|
|
1052
|
+
var DEFAULT_RPC_URLS = {
|
|
1053
|
+
testnet: "https://soroban-testnet.stellar.org",
|
|
1054
|
+
mainnet: "https://mainnet.sorobanrpc.com"
|
|
1055
|
+
};
|
|
1056
|
+
var CONFIRM_TIMEOUT_MS = 12e4;
|
|
1057
|
+
var CONFIRM_POLL_MS = 1e3;
|
|
1058
|
+
function delay(ms) {
|
|
1059
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1060
|
+
}
|
|
1061
|
+
function describeSendError(sent) {
|
|
1062
|
+
const errorResult = sent.errorResult;
|
|
1063
|
+
try {
|
|
1064
|
+
const name = errorResult?.result?.().switch?.().name;
|
|
1065
|
+
if (name) return `sendTransaction returned ERROR: ${name}`;
|
|
1066
|
+
} catch {
|
|
1067
|
+
}
|
|
1068
|
+
return `sendTransaction returned status ${sent.status}`;
|
|
1069
|
+
}
|
|
1070
|
+
async function waitForConfirmation(server, txHash) {
|
|
1071
|
+
const deadline = Date.now() + CONFIRM_TIMEOUT_MS;
|
|
1072
|
+
for (; ; ) {
|
|
1073
|
+
const tx = await server.getTransaction(txHash);
|
|
1074
|
+
if (tx.status === "SUCCESS") return "success";
|
|
1075
|
+
if (tx.status === "FAILED") return "failed";
|
|
1076
|
+
if (Date.now() >= deadline) return "timeout";
|
|
1077
|
+
await delay(CONFIRM_POLL_MS);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
async function recordSessionSettlement(request, { logger, onCallbackError }) {
|
|
1081
|
+
const reportFailure = (fields) => {
|
|
1082
|
+
logger("error", "[vault] failed to record session_settled", {
|
|
1083
|
+
channelId: request.channelId,
|
|
1084
|
+
...fields.txHash ? { txHash: fields.txHash } : {},
|
|
1085
|
+
reason: fields.reason,
|
|
1086
|
+
...fields.error !== void 0 ? { error: fields.error } : {}
|
|
1087
|
+
});
|
|
1088
|
+
if (onCallbackError) {
|
|
1089
|
+
const err = fields.error ?? new Error(`session_settled vault record failed: ${fields.reason}`);
|
|
1090
|
+
onCallbackError(err, "vaultSettlement");
|
|
1091
|
+
}
|
|
1092
|
+
return { ok: false, reason: fields.reason, ...fields.txHash ? { txHash: fields.txHash } : {} };
|
|
1093
|
+
};
|
|
1094
|
+
try {
|
|
1095
|
+
const payeeKeypair = Keypair.fromSecret(request.payeeSecretKey);
|
|
1096
|
+
const addresses = resolveVaultSettlementAddresses(request.payer, payeeKeypair.publicKey());
|
|
1097
|
+
if (!addresses) {
|
|
1098
|
+
logger("warn", "[vault] skipped session_settled record: payer address unavailable", {
|
|
1099
|
+
channelId: request.channelId
|
|
1100
|
+
});
|
|
1101
|
+
return { ok: false, reason: "payer address unavailable" };
|
|
1102
|
+
}
|
|
1103
|
+
const networkPassphrase = request.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
1104
|
+
const rpcUrl = request.config.rpcUrl ?? DEFAULT_RPC_URLS[request.network];
|
|
1105
|
+
const server = await createSorobanRpcServer(rpcUrl);
|
|
1106
|
+
const sourceAccount = await server.getAccount(payeeKeypair.publicKey());
|
|
1107
|
+
const tx = buildSessionSettlementTransaction(
|
|
1108
|
+
sourceAccount,
|
|
1109
|
+
request.config.contract,
|
|
1110
|
+
request.channelId,
|
|
1111
|
+
addresses,
|
|
1112
|
+
request.amount,
|
|
1113
|
+
request.voucherCount,
|
|
1114
|
+
networkPassphrase
|
|
1115
|
+
);
|
|
1116
|
+
const preparedTx = await server.prepareTransaction(tx);
|
|
1117
|
+
preparedTx.sign(payeeKeypair);
|
|
1118
|
+
const sent = await server.sendTransaction(preparedTx);
|
|
1119
|
+
if (sent.status === "ERROR") {
|
|
1120
|
+
return reportFailure({
|
|
1121
|
+
txHash: sent.hash || preparedTx.hash().toString("hex"),
|
|
1122
|
+
reason: describeSendError(sent)
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
const txHash = sent.hash || preparedTx.hash().toString("hex");
|
|
1126
|
+
const confirmation = await waitForConfirmation(server, txHash);
|
|
1127
|
+
if (confirmation === "success") {
|
|
1128
|
+
logger("debug", "[vault] recorded session_settled", {
|
|
1129
|
+
channelId: request.channelId,
|
|
1130
|
+
txHash
|
|
1131
|
+
});
|
|
1132
|
+
return { ok: true, txHash };
|
|
1133
|
+
}
|
|
1134
|
+
return reportFailure({
|
|
1135
|
+
txHash,
|
|
1136
|
+
reason: confirmation === "failed" ? "transaction failed on-chain" : `confirmation timed out after ${CONFIRM_TIMEOUT_MS}ms`
|
|
1137
|
+
});
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1140
|
+
return reportFailure({ reason, error: err });
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/internal/vaultConfig.ts
|
|
1145
|
+
function resolveVaultSettlementConfig(option, logger) {
|
|
1146
|
+
if (option) return option;
|
|
1147
|
+
if (typeof process === "undefined") return void 0;
|
|
1148
|
+
const fromEnv = process.env.AGENT_VAULT_CONTRACT ?? process.env.AGENT_VAULT_CONTRACT_ID;
|
|
1149
|
+
if (!fromEnv) return void 0;
|
|
1150
|
+
logger(
|
|
1151
|
+
"warn",
|
|
1152
|
+
"[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",
|
|
1153
|
+
{}
|
|
1154
|
+
);
|
|
1155
|
+
return { contract: fromEnv };
|
|
1156
|
+
}
|
|
1157
|
+
|
|
375
1158
|
// src/provider/registerProvider.ts
|
|
376
1159
|
async function registerProvider(optionsOrSupabase, manifestArg, baseUrlArg, verifiedArg) {
|
|
377
1160
|
let supabase;
|
|
@@ -469,6 +1252,8 @@ var MPP_NETWORK = {
|
|
|
469
1252
|
testnet: "stellar:testnet",
|
|
470
1253
|
mainnet: "stellar:pubnet"
|
|
471
1254
|
};
|
|
1255
|
+
var DEFAULT_MAX_RECONCILE_ATTEMPTS = 5;
|
|
1256
|
+
var ALREADY_CLOSED_PATTERN = /channel has been closed|channel is closed|already closed|finalized/i;
|
|
472
1257
|
function decimalToStroops(amountStr) {
|
|
473
1258
|
return usdcToStroops(amountStr);
|
|
474
1259
|
}
|
|
@@ -478,12 +1263,15 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
478
1263
|
recoveredCount: 0,
|
|
479
1264
|
skippedCount: 0,
|
|
480
1265
|
failedCount: 0,
|
|
1266
|
+
alreadyClosedCount: 0,
|
|
1267
|
+
terminalCount: 0,
|
|
481
1268
|
errors: []
|
|
482
1269
|
};
|
|
1270
|
+
const maxAttempts = opts.maxReconcileAttempts ?? DEFAULT_MAX_RECONCILE_ATTEMPTS;
|
|
483
1271
|
const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
|
|
484
1272
|
const networkId = MPP_NETWORK[opts.network];
|
|
485
1273
|
try {
|
|
486
|
-
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);
|
|
1274
|
+
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);
|
|
487
1275
|
if (error) {
|
|
488
1276
|
throw new Error(`Failed to query sessions: ${error.message}`);
|
|
489
1277
|
}
|
|
@@ -505,7 +1293,7 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
505
1293
|
continue;
|
|
506
1294
|
}
|
|
507
1295
|
const closeAmount = decimalToStroops(cumulative_amount);
|
|
508
|
-
const closeSig =
|
|
1296
|
+
const closeSig = hexToBytes(last_signature);
|
|
509
1297
|
const closeFn = opts.channelClose ?? (await import('@stellar/mpp/channel/server')).close;
|
|
510
1298
|
const closeTxHash = await closeFn({
|
|
511
1299
|
channel: channelId,
|
|
@@ -528,11 +1316,36 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
528
1316
|
await opts.onRecovered(channelId, closeTxHash, totalPaid);
|
|
529
1317
|
}
|
|
530
1318
|
} catch (err) {
|
|
1319
|
+
const reason = formatMppError(err);
|
|
1320
|
+
if (ALREADY_CLOSED_PATTERN.test(reason)) {
|
|
1321
|
+
const { error: closeError } = await opts.supabase.from("sessions").update({
|
|
1322
|
+
status: "closed",
|
|
1323
|
+
last_reconcile_error: `channel already closed on-chain: ${reason}`,
|
|
1324
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1325
|
+
}).eq("channel_id", session.channel_id);
|
|
1326
|
+
if (!closeError) {
|
|
1327
|
+
stats.alreadyClosedCount++;
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
531
1331
|
stats.failedCount++;
|
|
532
|
-
stats.errors.push({
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
})
|
|
1332
|
+
stats.errors.push({ channelId: session.channel_id, reason });
|
|
1333
|
+
const attempts = (Number(session.reconcile_attempts) || 0) + 1;
|
|
1334
|
+
const terminal = attempts >= maxAttempts;
|
|
1335
|
+
const { error: bookkeepingError } = await opts.supabase.from("sessions").update({
|
|
1336
|
+
reconcile_attempts: attempts,
|
|
1337
|
+
last_reconcile_error: reason,
|
|
1338
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1339
|
+
...terminal ? { status: "failed" } : {}
|
|
1340
|
+
}).eq("channel_id", session.channel_id);
|
|
1341
|
+
if (bookkeepingError) {
|
|
1342
|
+
stats.errors.push({
|
|
1343
|
+
channelId: session.channel_id,
|
|
1344
|
+
reason: `Failed to record reconcile failure: ${bookkeepingError.message}`
|
|
1345
|
+
});
|
|
1346
|
+
} else if (terminal) {
|
|
1347
|
+
stats.terminalCount++;
|
|
1348
|
+
}
|
|
536
1349
|
}
|
|
537
1350
|
}
|
|
538
1351
|
return stats;
|
|
@@ -553,7 +1366,7 @@ async function runStartupReconciliation(opts, logger) {
|
|
|
553
1366
|
}
|
|
554
1367
|
log(
|
|
555
1368
|
"info",
|
|
556
|
-
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
|
|
1369
|
+
`[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}`
|
|
557
1370
|
);
|
|
558
1371
|
if (stats.errors.length > 0) {
|
|
559
1372
|
log("warn", "[SessionReconciler] Reconciliation errors:");
|
|
@@ -695,12 +1508,9 @@ function createX402HonoHandler(opts) {
|
|
|
695
1508
|
requirements
|
|
696
1509
|
);
|
|
697
1510
|
if (!verifyResult.isValid) {
|
|
698
|
-
return
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
reason: verifyResult.invalidReason
|
|
702
|
-
},
|
|
703
|
-
401
|
|
1511
|
+
return respondPaymentRequired(
|
|
1512
|
+
"Payment verification failed",
|
|
1513
|
+
verifyResult.invalidReason
|
|
704
1514
|
);
|
|
705
1515
|
}
|
|
706
1516
|
const settleResult = await localFacilitator.settle(
|
|
@@ -720,7 +1530,12 @@ function createX402HonoHandler(opts) {
|
|
|
720
1530
|
if (idempotencyKey) {
|
|
721
1531
|
const headers = {};
|
|
722
1532
|
if (paymentResponseHeader) headers["X-Payment-Response"] = paymentResponseHeader;
|
|
723
|
-
await
|
|
1533
|
+
await recordSettlement(
|
|
1534
|
+
seenTxStore,
|
|
1535
|
+
idempotencyKey,
|
|
1536
|
+
{ txHash, headers, createdAt: Date.now() },
|
|
1537
|
+
{ logger, label: "[x402]", onCallbackError: opts.onCallbackError }
|
|
1538
|
+
);
|
|
724
1539
|
}
|
|
725
1540
|
if (txHash && opts.onSettled) {
|
|
726
1541
|
Promise.resolve().then(() => opts.onSettled(txHash, x402Price, "x402", payerAddress)).catch((err) => {
|
|
@@ -843,11 +1658,12 @@ function createMppChargeHonoHandler(opts) {
|
|
|
843
1658
|
}
|
|
844
1659
|
}
|
|
845
1660
|
if (idempotencyKey) {
|
|
846
|
-
await
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
createdAt: Date.now()
|
|
850
|
-
|
|
1661
|
+
await recordSettlement(
|
|
1662
|
+
seenTxStore,
|
|
1663
|
+
idempotencyKey,
|
|
1664
|
+
{ txHash: reference ?? null, headers: receiptHeaders, createdAt: Date.now() },
|
|
1665
|
+
{ logger, label: "[mpp-charge]", onCallbackError: opts.onCallbackError }
|
|
1666
|
+
);
|
|
851
1667
|
}
|
|
852
1668
|
if (reference && opts.onSettled) {
|
|
853
1669
|
Promise.resolve().then(() => opts.onSettled(reference, chargePrice, "mpp-charge", payerAddress)).catch((err) => {
|
|
@@ -857,7 +1673,11 @@ function createMppChargeHonoHandler(opts) {
|
|
|
857
1673
|
}
|
|
858
1674
|
await next();
|
|
859
1675
|
} catch (err) {
|
|
860
|
-
|
|
1676
|
+
if (err instanceof RouteDockManifestError) {
|
|
1677
|
+
throw err;
|
|
1678
|
+
}
|
|
1679
|
+
logger("error", "[mpp-charge] Settlement error", { error: err });
|
|
1680
|
+
return c.json({ error: "Payment settlement failed" }, 500);
|
|
861
1681
|
}
|
|
862
1682
|
};
|
|
863
1683
|
}
|
|
@@ -871,9 +1691,11 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
|
|
|
871
1691
|
const logger = resolveLogger(opts.logger);
|
|
872
1692
|
const voucherRecordKey = `routedock:session:voucher:${sessionPricing.channelFactory}`;
|
|
873
1693
|
const innerStore = opts.sessionStore ?? Store.memory();
|
|
1694
|
+
const cumulativeKey = `stellar:channel:cumulative:${sessionPricing.channelFactory}`;
|
|
874
1695
|
let record = null;
|
|
875
1696
|
let voucherCount = 0;
|
|
876
1697
|
let sessionOpened = false;
|
|
1698
|
+
let orphanFlagged = false;
|
|
877
1699
|
let idleTimer = null;
|
|
878
1700
|
let settledCleanly = false;
|
|
879
1701
|
let abortListenerArmed = false;
|
|
@@ -900,15 +1722,32 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
|
|
|
900
1722
|
return null;
|
|
901
1723
|
}
|
|
902
1724
|
record = { amount: BigInt(stored.amount), signature: stored.signature, payer: stored.payer ?? null };
|
|
1725
|
+
if (typeof stored.voucherCount === "number") {
|
|
1726
|
+
voucherCount = stored.voucherCount;
|
|
1727
|
+
}
|
|
1728
|
+
if (typeof stored.sessionOpened === "boolean") {
|
|
1729
|
+
sessionOpened = stored.sessionOpened;
|
|
1730
|
+
} else if (record !== null) {
|
|
1731
|
+
sessionOpened = true;
|
|
1732
|
+
}
|
|
903
1733
|
} catch {
|
|
904
1734
|
return null;
|
|
905
1735
|
}
|
|
906
1736
|
return record;
|
|
907
1737
|
}
|
|
1738
|
+
async function readStoredCumulative() {
|
|
1739
|
+
try {
|
|
1740
|
+
const raw = await innerStore.get(cumulativeKey);
|
|
1741
|
+
const amount = typeof raw === "object" && raw !== null ? raw.amount : void 0;
|
|
1742
|
+
return typeof amount === "string" && /^\d+$/.test(amount) ? BigInt(amount) : null;
|
|
1743
|
+
} catch {
|
|
1744
|
+
return null;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
908
1747
|
async function flagOrphan(reason) {
|
|
909
1748
|
await loadPersistedRecord();
|
|
910
|
-
if (!sessionOpened || settledCleanly) return;
|
|
911
|
-
|
|
1749
|
+
if (!sessionOpened || settledCleanly || orphanFlagged) return;
|
|
1750
|
+
orphanFlagged = true;
|
|
912
1751
|
abortListenerArmed = false;
|
|
913
1752
|
clearIdleTimer();
|
|
914
1753
|
const cumulativeAmount = record ? (Number(record.amount) / 1e7).toFixed(7) : "0.0000000";
|
|
@@ -956,16 +1795,20 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
|
|
|
956
1795
|
if (record && amount < record.amount) return;
|
|
957
1796
|
const payer = record?.payer ?? extractPayerAddress(credential.source);
|
|
958
1797
|
record = { amount, signature: payload.signature, payer };
|
|
1798
|
+
voucherCount++;
|
|
1799
|
+
settledCleanly = false;
|
|
1800
|
+
orphanFlagged = false;
|
|
1801
|
+
armIdleTimer();
|
|
1802
|
+
const wasOpened = sessionOpened;
|
|
1803
|
+
sessionOpened = true;
|
|
959
1804
|
await innerStore.put(voucherRecordKey, {
|
|
960
1805
|
amount: record.amount.toString(),
|
|
961
1806
|
signature: record.signature,
|
|
962
|
-
payer: record.payer
|
|
1807
|
+
payer: record.payer,
|
|
1808
|
+
voucherCount,
|
|
1809
|
+
sessionOpened: true
|
|
963
1810
|
});
|
|
964
|
-
|
|
965
|
-
settledCleanly = false;
|
|
966
|
-
armIdleTimer();
|
|
967
|
-
if (!sessionOpened) {
|
|
968
|
-
sessionOpened = true;
|
|
1811
|
+
if (!wasOpened) {
|
|
969
1812
|
if (opts.onSessionOpen) {
|
|
970
1813
|
Promise.resolve().then(() => opts.onSessionOpen(sessionPricing.channelFactory, record.payer)).catch((err) => {
|
|
971
1814
|
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
@@ -1020,6 +1863,16 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
|
|
|
1020
1863
|
closeSig = record?.signature || (bodyAmount === recordAmount ? body?.signature ?? "" : "");
|
|
1021
1864
|
}
|
|
1022
1865
|
if (closeAmount > 0n && closeSig) {
|
|
1866
|
+
const storedCumulative = await readStoredCumulative();
|
|
1867
|
+
if (storedCumulative !== null && closeAmount < storedCumulative) {
|
|
1868
|
+
return c.json(
|
|
1869
|
+
{
|
|
1870
|
+
closeTxHash: null,
|
|
1871
|
+
error: "close amount below the highest verified voucher"
|
|
1872
|
+
},
|
|
1873
|
+
409
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1023
1876
|
const closeTxHash = await close({
|
|
1024
1877
|
channel: sessionPricing.channelFactory,
|
|
1025
1878
|
amount: closeAmount,
|
|
@@ -1037,8 +1890,25 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
|
|
|
1037
1890
|
opts.onCallbackError?.(err, "onSettled");
|
|
1038
1891
|
});
|
|
1039
1892
|
}
|
|
1893
|
+
const vaultConfig = resolveVaultSettlementConfig(opts.vaultSettlement, logger);
|
|
1894
|
+
if (vaultConfig) {
|
|
1895
|
+
const settlement = recordSessionSettlement(
|
|
1896
|
+
{
|
|
1897
|
+
config: vaultConfig,
|
|
1898
|
+
network: opts.network,
|
|
1899
|
+
payeeSecretKey: opts.payeeSecretKey,
|
|
1900
|
+
channelId: sessionPricing.channelFactory,
|
|
1901
|
+
payer: closePayer,
|
|
1902
|
+
amount: closeAmount,
|
|
1903
|
+
voucherCount
|
|
1904
|
+
},
|
|
1905
|
+
{ logger, onCallbackError: opts.onCallbackError }
|
|
1906
|
+
);
|
|
1907
|
+
if (opts.waitUntil) opts.waitUntil(settlement);
|
|
1908
|
+
}
|
|
1040
1909
|
sessionOpened = false;
|
|
1041
1910
|
voucherCount = 0;
|
|
1911
|
+
orphanFlagged = false;
|
|
1042
1912
|
record = null;
|
|
1043
1913
|
await innerStore.delete(voucherRecordKey);
|
|
1044
1914
|
abortListenerArmed = false;
|
|
@@ -1063,10 +1933,11 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
|
|
|
1063
1933
|
}
|
|
1064
1934
|
function createMppSessionHonoHandler(opts, config, state) {
|
|
1065
1935
|
const sessionPricing = opts.pricing[config.mode];
|
|
1936
|
+
const logger = resolveLogger(opts.logger);
|
|
1066
1937
|
return async (c, next) => {
|
|
1067
1938
|
try {
|
|
1068
1939
|
if (c.req.method === "DELETE") {
|
|
1069
|
-
return state.handleDelete(c);
|
|
1940
|
+
return await state.handleDelete(c);
|
|
1070
1941
|
}
|
|
1071
1942
|
const result = await state.mppx.channel({
|
|
1072
1943
|
amount: sessionPricing.rate,
|
|
@@ -1088,7 +1959,11 @@ function createMppSessionHonoHandler(opts, config, state) {
|
|
|
1088
1959
|
}
|
|
1089
1960
|
await next();
|
|
1090
1961
|
} catch (err) {
|
|
1091
|
-
|
|
1962
|
+
if (err instanceof RouteDockManifestError) {
|
|
1963
|
+
throw err;
|
|
1964
|
+
}
|
|
1965
|
+
logger("error", `[${config.mode}] Session error`, { error: err });
|
|
1966
|
+
return c.json({ error: "Session payment failed" }, 500);
|
|
1092
1967
|
}
|
|
1093
1968
|
};
|
|
1094
1969
|
}
|
|
@@ -1097,6 +1972,7 @@ function isWebSocketUpgradeRequest(c) {
|
|
|
1097
1972
|
}
|
|
1098
1973
|
function routedockHono(opts) {
|
|
1099
1974
|
const handlers = [];
|
|
1975
|
+
assertProviderConfig(opts, "routedockHono");
|
|
1100
1976
|
const signedManifest = signManifest(opts.manifest, opts.payeeSecretKey);
|
|
1101
1977
|
if (opts.modes.includes("x402") && opts.pricing.x402) {
|
|
1102
1978
|
handlers.push(createX402HonoHandler({ ...opts, manifest: signedManifest }));
|