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