@openzeppelin/relayer-plugin-channels 0.16.0 → 0.18.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 +88 -2
- package/dist/client/types.d.ts +4 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/plugin/config.d.ts +4 -0
- package/dist/plugin/config.d.ts.map +1 -1
- package/dist/plugin/config.js +30 -0
- package/dist/plugin/constants.d.ts +8 -6
- package/dist/plugin/constants.d.ts.map +1 -1
- package/dist/plugin/constants.js +13 -8
- package/dist/plugin/handler.d.ts.map +1 -1
- package/dist/plugin/handler.js +32 -9
- package/dist/plugin/management.js +11 -14
- package/dist/plugin/pool.d.ts +11 -5
- package/dist/plugin/pool.d.ts.map +1 -1
- package/dist/plugin/pool.js +93 -30
- package/dist/plugin/submit.d.ts +2 -1
- package/dist/plugin/submit.d.ts.map +1 -1
- package/dist/plugin/submit.js +5 -3
- package/dist/plugin/types.d.ts +3 -0
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/validation.d.ts.map +1 -1
- package/dist/plugin/validation.js +38 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ A plugin for OpenZeppelin Relayer that enables parallel transaction submission o
|
|
|
16
16
|
- [Development](#development)
|
|
17
17
|
- [Overview](#overview)
|
|
18
18
|
- [Architecture](#architecture)
|
|
19
|
+
- [x402 Fund Relayer](#x402-fund-relayer)
|
|
19
20
|
- [Contract Capacity Limits](#contract-capacity-limits)
|
|
20
21
|
- [Management API](#management-api)
|
|
21
22
|
- [List Channel Accounts](#list-channel-accounts)
|
|
@@ -221,6 +222,8 @@ export FUND_RELAYER_ID="channels-fund"
|
|
|
221
222
|
export PLUGIN_ADMIN_SECRET="your-secret-here" # Required for management API
|
|
222
223
|
|
|
223
224
|
# Optional environment variables
|
|
225
|
+
# Comma-separated list of allowed alternative fund relayers (e.g., for x402 or similar flows)
|
|
226
|
+
export ALLOWED_FUND_RELAYER_IDS="x402-channels-fund"
|
|
224
227
|
export LOCK_TTL_SECONDS=10 # default: 30, min: 3, max: 30
|
|
225
228
|
|
|
226
229
|
# Fee tracking (optional)
|
|
@@ -308,6 +311,71 @@ The Channels plugin accepts Soroban operations and handles all the complexity of
|
|
|
308
311
|
- **Channel Accounts**: Provide unique sequence numbers for parallel transaction submission
|
|
309
312
|
- The channel account is the transaction source and signer; the fund account wraps it in a fee bump
|
|
310
313
|
|
|
314
|
+
## Alternative Fund Relayers
|
|
315
|
+
|
|
316
|
+
The plugin supports optional secondary fund relayers for [x402](https://www.x402.org/) and similar payment flows. Pass `fundRelayerId` in the request and allowlist that ID through `ALLOWED_FUND_RELAYER_IDS`.
|
|
317
|
+
|
|
318
|
+
### Configuration
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
export ALLOWED_FUND_RELAYER_IDS="x402-fund"
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Every allowed alternative fund relayer must reference a relayer configured in your Relayer's `config.json`, just like the primary fund account. Add it alongside your existing relayers:
|
|
325
|
+
|
|
326
|
+
```json
|
|
327
|
+
{
|
|
328
|
+
"id": "x402-fund",
|
|
329
|
+
"name": "x402 Fund Account",
|
|
330
|
+
"network": "testnet",
|
|
331
|
+
"paused": false,
|
|
332
|
+
"network_type": "stellar",
|
|
333
|
+
"signer_id": "x402-fund-signer",
|
|
334
|
+
"policies": {
|
|
335
|
+
"concurrent_transactions": true,
|
|
336
|
+
"fee_payment_strategy": "relayer"
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Preferred usage
|
|
342
|
+
|
|
343
|
+
Pass `fundRelayerId` in any `xdr` or `func`+`auth` request to route fee bumping through the specified fund relayer:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
|
|
347
|
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
348
|
+
-H "Content-Type: application/json" \
|
|
349
|
+
-d '{
|
|
350
|
+
"params": {
|
|
351
|
+
"xdr": "AAAAAgAAAAB...",
|
|
352
|
+
"fundRelayerId": "x402-fund"
|
|
353
|
+
}
|
|
354
|
+
}'
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Or with `func`+`auth`:
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
|
|
361
|
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
362
|
+
-H "Content-Type: application/json" \
|
|
363
|
+
-d '{
|
|
364
|
+
"params": {
|
|
365
|
+
"func": "AAAABAAAAAEAAAAGc3ltYm9s...",
|
|
366
|
+
"auth": ["AAAACAAAAAEAAAA..."],
|
|
367
|
+
"fundRelayerId": "x402-fund"
|
|
368
|
+
}
|
|
369
|
+
}'
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Notes
|
|
373
|
+
|
|
374
|
+
- If `fundRelayerId` is provided but not listed in `ALLOWED_FUND_RELAYER_IDS`, the plugin rejects the request
|
|
375
|
+
- When `fundRelayerId` is omitted, the primary `FUND_RELAYER_ID` is used
|
|
376
|
+
- `fundRelayerId` must be a non-empty string
|
|
377
|
+
- `getTransaction` requests can also include `fundRelayerId`; polling should use the same relayer selection as submission
|
|
378
|
+
|
|
311
379
|
## Contract Capacity Limits
|
|
312
380
|
|
|
313
381
|
High-volume contracts can monopolize the channel pool, starving other traffic. Contract capacity limits allow you to reserve a portion of the pool for non-limited contracts.
|
|
@@ -671,6 +739,23 @@ const result = await client.submitSorobanTransaction({
|
|
|
671
739
|
console.log(result.hash);
|
|
672
740
|
```
|
|
673
741
|
|
|
742
|
+
#### Submit with Alternative Fund Relayer
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
// Submit using an alternative fund relayer for fee bumping
|
|
746
|
+
const result = await client.submitTransaction({
|
|
747
|
+
xdr: 'AAAAAgAAAAC...', // Complete transaction envelope XDR
|
|
748
|
+
fundRelayerId: 'x402-fund',
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// Also works with func+auth
|
|
752
|
+
const result2 = await client.submitSorobanTransaction({
|
|
753
|
+
func: 'AAAABAAAAAEAAAAGc3ltYm9s...',
|
|
754
|
+
auth: ['AAAACAAAAAEAAAA...'],
|
|
755
|
+
fundRelayerId: 'x402-fund',
|
|
756
|
+
});
|
|
757
|
+
```
|
|
758
|
+
|
|
674
759
|
#### List Channel Accounts (Management)
|
|
675
760
|
|
|
676
761
|
```typescript
|
|
@@ -800,8 +885,8 @@ All request and response types are fully typed:
|
|
|
800
885
|
|
|
801
886
|
```typescript
|
|
802
887
|
import type {
|
|
803
|
-
ChannelsXdrRequest,
|
|
804
|
-
ChannelsFuncAuthRequest,
|
|
888
|
+
ChannelsXdrRequest, // includes optional x402 field
|
|
889
|
+
ChannelsFuncAuthRequest, // includes optional x402 field
|
|
805
890
|
ChannelsTransactionResponse,
|
|
806
891
|
ListChannelAccountsResponse,
|
|
807
892
|
SetChannelAccountsResponse,
|
|
@@ -867,6 +952,7 @@ curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
|
|
|
867
952
|
- `func` (string): Soroban host function XDR (base64)
|
|
868
953
|
- `auth` (array): Array of Soroban authorization entry XDRs (base64)
|
|
869
954
|
- `skipWait` (boolean, optional): When `true`, returns immediately after submitting the transaction without waiting for confirmation. Defaults to `false`. Must be a boolean — non-boolean values (e.g., `"false"`, `1`) are rejected.
|
|
955
|
+
- `fundRelayerId` (string, optional): Uses the specified alternative fund relayer for fee bumping instead of the primary fund account. Must be present in `ALLOWED_FUND_RELAYER_IDS`.
|
|
870
956
|
- `getTransaction` (object, optional): Poll for a transaction's status by ID. Cannot be combined with other parameters.
|
|
871
957
|
|
|
872
958
|
**Note**: Provide either `xdr` OR `func`+`auth` OR `getTransaction`, not a combination.
|
package/dist/client/types.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface ChannelsXdrRequest {
|
|
|
44
44
|
xdr: string;
|
|
45
45
|
/** When true, returns immediately after submission without waiting for confirmation */
|
|
46
46
|
skipWait?: boolean;
|
|
47
|
+
/** Alternative fund relayer ID for fee bumping (must be in the allowed list) */
|
|
48
|
+
fundRelayerId?: string;
|
|
47
49
|
}
|
|
48
50
|
/**
|
|
49
51
|
* Transaction submission using Soroban function and authorization
|
|
@@ -55,6 +57,8 @@ export interface ChannelsFuncAuthRequest {
|
|
|
55
57
|
auth: string[];
|
|
56
58
|
/** When true, returns immediately after submission without waiting for confirmation */
|
|
57
59
|
skipWait?: boolean;
|
|
60
|
+
/** Alternative fund relayer ID for fee bumping (must be in the allowed list) */
|
|
61
|
+
fundRelayerId?: string;
|
|
58
62
|
}
|
|
59
63
|
/**
|
|
60
64
|
* Request to get a transaction by ID
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,gBAAgB,GAAG,aAAa,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,gBAAgB,GAAG,aAAa,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,sCAAsC;IACtC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,gCAAgC;IAChC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,yBAAyB;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,oEAAoE;IACpE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,wBAAwB;IACxB,EAAE,EAAE,OAAO,CAAC;IACZ,6CAA6C;IAC7C,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,0CAA0C;IAC1C,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,wBAAwB;IACxB,EAAE,EAAE,OAAO,CAAC;IACZ,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,wBAAwB;IACxB,EAAE,EAAE,OAAO,CAAC;IACZ,0CAA0C;IAC1C,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,MAAM,EAAE;QACN,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC;QAC/B,cAAc,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,qBAAqB,CAAC,EAAE,MAAM,CAAC;QAC/B,qBAAqB,EAAE,MAAM,CAAC;QAC9B,gBAAgB,EAAE,MAAM,EAAE,CAAC;KAC5B,CAAC;IACF,IAAI,EAAE;QACJ,mBAAmB,EAAE,MAAM,CAAC;QAC5B,mBAAmB,EAAE,MAAM,CAAC;KAC7B,CAAC;IACF,0CAA0C;IAC1C,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC,OAAO,EAAE,IAAI,CAAC;IACd,IAAI,EAAE,CAAC,CAAC;IACR,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;KAChB,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,qBAAqB,CAAC,CAAC,CAAC,GAAG,mBAAmB,CAAC"}
|
package/dist/plugin/config.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export interface ChannelAccountsConfig {
|
|
7
7
|
fundRelayerId: string;
|
|
8
|
+
/** Map of allowed alternative fund relayer IDs (e.g. { "x402-channels-fund": true }) */
|
|
9
|
+
allowedFundRelayerIds: Set<string>;
|
|
8
10
|
network: 'testnet' | 'mainnet';
|
|
9
11
|
lockTtlSeconds: number;
|
|
10
12
|
adminSecret?: string;
|
|
@@ -17,6 +19,8 @@ export interface ChannelAccountsConfig {
|
|
|
17
19
|
inclusionFeeLimited: number;
|
|
18
20
|
sequenceNumberCacheMaxAgeMs: number;
|
|
19
21
|
minSignatureExpirationLedgerBuffer: number;
|
|
22
|
+
globalTimeoutMs: number;
|
|
23
|
+
pollingTimeoutMs: number;
|
|
20
24
|
}
|
|
21
25
|
/**
|
|
22
26
|
* Load configuration from environment variables
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/plugin/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,2BAA2B,EAAE,MAAM,CAAC;IACpC,kCAAkC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/plugin/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,wFAAwF;IACxF,qBAAqB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,2BAA2B,EAAE,MAAM,CAAC;IACpC,kCAAkC,EAAE,MAAM,CAAC;IAC3C,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAoID;;GAEG;AACH,wBAAgB,UAAU,IAAI,qBAAqB,CA6BlD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAE3E"}
|
package/dist/plugin/config.js
CHANGED
|
@@ -105,6 +105,32 @@ function parseSequenceNumberCacheMaxAge() {
|
|
|
105
105
|
const n = Number(raw);
|
|
106
106
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : constants_1.CONFIG.DEFAULT_SEQUENCE_NUMBER_CACHE_MAX_AGE_MS;
|
|
107
107
|
}
|
|
108
|
+
function parseAllowedFundRelayerIds() {
|
|
109
|
+
const ids = new Set();
|
|
110
|
+
const raw = process.env.ALLOWED_FUND_RELAYER_IDS;
|
|
111
|
+
if (raw) {
|
|
112
|
+
for (const id of raw.split(',')) {
|
|
113
|
+
const trimmed = id.trim();
|
|
114
|
+
if (trimmed.length > 0)
|
|
115
|
+
ids.add(trimmed);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return ids;
|
|
119
|
+
}
|
|
120
|
+
function parseGlobalTimeoutMs() {
|
|
121
|
+
const raw = process.env.PLUGIN_GLOBAL_TIMEOUT_MS;
|
|
122
|
+
if (!raw)
|
|
123
|
+
return constants_1.TIMEOUT.DEFAULT_GLOBAL_TIMEOUT_MS;
|
|
124
|
+
const n = Number(raw);
|
|
125
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : constants_1.TIMEOUT.DEFAULT_GLOBAL_TIMEOUT_MS;
|
|
126
|
+
}
|
|
127
|
+
function parsePollingTimeoutMs() {
|
|
128
|
+
const raw = process.env.PLUGIN_POLLING_TIMEOUT_MS;
|
|
129
|
+
if (!raw)
|
|
130
|
+
return constants_1.POLLING.DEFAULT_TIMEOUT_MS;
|
|
131
|
+
const n = Number(raw);
|
|
132
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : constants_1.POLLING.DEFAULT_TIMEOUT_MS;
|
|
133
|
+
}
|
|
108
134
|
function parseContractCapacityRatio() {
|
|
109
135
|
const raw = process.env.CONTRACT_CAPACITY_RATIO;
|
|
110
136
|
if (!raw)
|
|
@@ -126,8 +152,10 @@ function loadConfig() {
|
|
|
126
152
|
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
127
153
|
});
|
|
128
154
|
}
|
|
155
|
+
const allowedFundRelayerIds = parseAllowedFundRelayerIds();
|
|
129
156
|
return {
|
|
130
157
|
fundRelayerId: requireEnv('FUND_RELAYER_ID'),
|
|
158
|
+
allowedFundRelayerIds,
|
|
131
159
|
network: networkRaw,
|
|
132
160
|
lockTtlSeconds: parseLockTtl(),
|
|
133
161
|
adminSecret: parseOptionalString('PLUGIN_ADMIN_SECRET'),
|
|
@@ -140,6 +168,8 @@ function loadConfig() {
|
|
|
140
168
|
inclusionFeeLimited: parseInclusionFee('INCLUSION_FEE_LIMITED', DEFAULT_INCLUSION_FEE_LIMITED),
|
|
141
169
|
sequenceNumberCacheMaxAgeMs: parseSequenceNumberCacheMaxAge(),
|
|
142
170
|
minSignatureExpirationLedgerBuffer: parseMinAuthExpiryLedgerBuffer(),
|
|
171
|
+
globalTimeoutMs: parseGlobalTimeoutMs(),
|
|
172
|
+
pollingTimeoutMs: parsePollingTimeoutMs(),
|
|
143
173
|
};
|
|
144
174
|
}
|
|
145
175
|
/**
|
|
@@ -23,10 +23,12 @@ export declare const CONFIG: {
|
|
|
23
23
|
readonly DEFAULT_MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER: 2;
|
|
24
24
|
};
|
|
25
25
|
export declare const POOL: {
|
|
26
|
-
readonly
|
|
27
|
-
readonly
|
|
28
|
-
readonly
|
|
29
|
-
readonly
|
|
26
|
+
readonly CLAIM_LOCK_TTL_SECONDS: 3;
|
|
27
|
+
readonly ACQUIRE_MAX_SPINS: 30;
|
|
28
|
+
readonly ACQUIRE_RETRY_MIN_MS: 10;
|
|
29
|
+
readonly ACQUIRE_RETRY_MAX_MS: 30;
|
|
30
|
+
readonly CHANNEL_COOLDOWN_MS: 6000;
|
|
31
|
+
readonly LRU_MAP_TTL_SECONDS: 86400;
|
|
30
32
|
};
|
|
31
33
|
export declare const TIME: {
|
|
32
34
|
readonly MAX_TIME_BOUND_OFFSET_SECONDS: 60;
|
|
@@ -41,12 +43,12 @@ export declare const SIMULATION: {
|
|
|
41
43
|
readonly MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER: 2;
|
|
42
44
|
};
|
|
43
45
|
export declare const TIMEOUT: {
|
|
44
|
-
readonly
|
|
46
|
+
readonly DEFAULT_GLOBAL_TIMEOUT_MS: 30000;
|
|
45
47
|
readonly BUFFER_MS: 2000;
|
|
46
48
|
};
|
|
47
49
|
export declare const POLLING: {
|
|
48
50
|
readonly INTERVAL_MS: 1000;
|
|
49
|
-
readonly
|
|
51
|
+
readonly DEFAULT_TIMEOUT_MS: 25000;
|
|
50
52
|
};
|
|
51
53
|
export declare const FEE: {
|
|
52
54
|
readonly NON_SOROBAN_FEE: 100000;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/plugin/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,eAAO,MAAM,WAAW;;;;;;;;;;CAUd,CAAC;AAGX,eAAO,MAAM,MAAM;;;;;;;CAOT,CAAC;AAGX,eAAO,MAAM,IAAI
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/plugin/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,eAAO,MAAM,WAAW;;;;;;;;;;CAUd,CAAC;AAGX,eAAO,MAAM,MAAM;;;;;;;CAOT,CAAC;AAGX,eAAO,MAAM,IAAI;;;;;;;CAYP,CAAC;AAGX,eAAO,MAAM,IAAI;;CAEP,CAAC;AAGX,eAAO,MAAM,UAAU;;;;;;IAMrB,uLAAuL;;CAE/K,CAAC;AAGX,eAAO,MAAM,OAAO;;;CAGV,CAAC;AAGX,eAAO,MAAM,OAAO;;;CAGV,CAAC;AAEX,eAAO,MAAM,GAAG;;CAGN,CAAC"}
|
package/dist/plugin/constants.js
CHANGED
|
@@ -29,12 +29,17 @@ exports.CONFIG = {
|
|
|
29
29
|
};
|
|
30
30
|
// Pool Constants
|
|
31
31
|
exports.POOL = {
|
|
32
|
-
// TTL
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
// Per-channel claim lock TTL — must exceed worst-case callback latency
|
|
33
|
+
// to prevent TTL expiry allowing a second worker into the same claim section
|
|
34
|
+
CLAIM_LOCK_TTL_SECONDS: 3,
|
|
35
|
+
// Retry policy when all candidates busy
|
|
36
|
+
ACQUIRE_MAX_SPINS: 30,
|
|
37
|
+
ACQUIRE_RETRY_MIN_MS: 10,
|
|
38
|
+
ACQUIRE_RETRY_MAX_MS: 30,
|
|
39
|
+
// Hard-block cooldown for uncertain-outcome channels (~1 Stellar ledger with margin)
|
|
40
|
+
CHANNEL_COOLDOWN_MS: 6000,
|
|
41
|
+
// Housekeeping TTL for the single LRU map document
|
|
42
|
+
LRU_MAP_TTL_SECONDS: 86400,
|
|
38
43
|
};
|
|
39
44
|
// Time Constants
|
|
40
45
|
exports.TIME = {
|
|
@@ -52,13 +57,13 @@ exports.SIMULATION = {
|
|
|
52
57
|
};
|
|
53
58
|
// Global plugin timeout budget
|
|
54
59
|
exports.TIMEOUT = {
|
|
55
|
-
|
|
60
|
+
DEFAULT_GLOBAL_TIMEOUT_MS: 30000,
|
|
56
61
|
BUFFER_MS: 2000,
|
|
57
62
|
};
|
|
58
63
|
// Polling for transactionWait
|
|
59
64
|
exports.POLLING = {
|
|
60
65
|
INTERVAL_MS: 1000,
|
|
61
|
-
|
|
66
|
+
DEFAULT_TIMEOUT_MS: 25000,
|
|
62
67
|
};
|
|
63
68
|
exports.FEE = {
|
|
64
69
|
// For non-Soroban txs: 100,000 stroops (0.01 XLM) per Stellar best practice
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/plugin/handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;AASvE,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AA2BxD;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,EAAE,EAAE,WAAW,GACd;IAAE,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC;IAAC,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,CAAA;CAAE,GAAG,IAAI,CAkB1E;
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/plugin/handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;AASvE,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AA2BxD;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,EAAE,EAAE,WAAW,GACd;IAAE,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC;IAAC,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,CAAA;CAAE,GAAG,IAAI,CAkB1E;AAyTD;;GAEG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAElE"}
|
package/dist/plugin/handler.js
CHANGED
|
@@ -74,7 +74,7 @@ async function handleXdrSubmit(xdrStr, ctx, skipWait) {
|
|
|
74
74
|
contractId,
|
|
75
75
|
isLimited: contractId ? ctx.acquireOptions.limitedContracts.has(contractId) : false,
|
|
76
76
|
};
|
|
77
|
-
return (0, submit_1.submitWithFeeBumpAndWait)(ctx.fundRelayer, validated.toXDR(), ctx.network, maxFee, ctx.api, ctx.startTime, ctx.tracker, submitContext, skipWait);
|
|
77
|
+
return (0, submit_1.submitWithFeeBumpAndWait)(ctx.fundRelayer, validated.toXDR(), ctx.network, maxFee, ctx.api, ctx.startTime, ctx.tracker, submitContext, skipWait, ctx.config);
|
|
78
78
|
}
|
|
79
79
|
async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
|
|
80
80
|
// Simulate once — used for both read-only detection and transaction assembly
|
|
@@ -90,6 +90,7 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
|
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
let poolLock;
|
|
93
|
+
let needsCooldown = false;
|
|
93
94
|
try {
|
|
94
95
|
poolLock = await ctx.pool.acquire(ctx.acquireOptions);
|
|
95
96
|
const channelRelayer = ctx.api.useRelayer(poolLock.relayerId);
|
|
@@ -123,9 +124,8 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
|
|
|
123
124
|
isLimited: contractId ? ctx.acquireOptions.limitedContracts.has(contractId) : false,
|
|
124
125
|
};
|
|
125
126
|
try {
|
|
126
|
-
const result = await (0, submit_1.submitWithFeeBumpAndWait)(ctx.fundRelayer, signedTx.toXDR(), ctx.network, maxFee, ctx.api, ctx.startTime, ctx.tracker, submitContext, skipWait);
|
|
127
|
+
const result = await (0, submit_1.submitWithFeeBumpAndWait)(ctx.fundRelayer, signedTx.toXDR(), ctx.network, maxFee, ctx.api, ctx.startTime, ctx.tracker, submitContext, skipWait, ctx.config);
|
|
127
128
|
if (result.status === 'pending' || result.status === 'sent' || result.status === 'submitted') {
|
|
128
|
-
// extend lock and clear sequence
|
|
129
129
|
console.log(`[channels]: extending lock and clearing sequence`);
|
|
130
130
|
await ctx.pool.extendLock(poolLock);
|
|
131
131
|
await (0, sequence_1.clearSequence)(ctx.kv, ctx.network, channelInfo.address);
|
|
@@ -135,7 +135,9 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
|
|
|
135
135
|
await (0, sequence_1.commitSequence)(ctx.kv, ctx.network, channelInfo.address, sequence);
|
|
136
136
|
}
|
|
137
137
|
else {
|
|
138
|
+
// Unknown status — uncertain outcome → release with cooldown
|
|
138
139
|
await (0, sequence_1.clearSequence)(ctx.kv, ctx.network, channelInfo.address);
|
|
140
|
+
needsCooldown = true;
|
|
139
141
|
}
|
|
140
142
|
return result;
|
|
141
143
|
}
|
|
@@ -146,12 +148,22 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
|
|
|
146
148
|
await ctx.pool.extendLock(poolLock);
|
|
147
149
|
poolLock = undefined; // skip release in finally
|
|
148
150
|
}
|
|
151
|
+
else if (error.code === 'ONCHAIN_FAILED') {
|
|
152
|
+
// Sequence was consumed (tx landed on chain) — cooldown prevents
|
|
153
|
+
// next acquirer from hitting stale RPC and reusing old sequence
|
|
154
|
+
needsCooldown = true;
|
|
155
|
+
}
|
|
149
156
|
throw error;
|
|
150
157
|
}
|
|
151
158
|
}
|
|
152
159
|
finally {
|
|
153
160
|
if (poolLock) {
|
|
154
|
-
|
|
161
|
+
if (needsCooldown) {
|
|
162
|
+
await ctx.pool.releaseWithCooldown(poolLock);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
await ctx.pool.release(poolLock);
|
|
166
|
+
}
|
|
155
167
|
}
|
|
156
168
|
}
|
|
157
169
|
}
|
|
@@ -186,11 +198,22 @@ async function channelAccounts(context) {
|
|
|
186
198
|
resetPeriodMs: config.feeResetPeriodMs,
|
|
187
199
|
});
|
|
188
200
|
}
|
|
189
|
-
//
|
|
201
|
+
// Validate and parse request (xdr OR func+auth)
|
|
190
202
|
const request = (0, validation_1.validateAndParseRequest)(params);
|
|
191
203
|
console.debug(`[channels] Request type: ${request.type}, auth entries: ${request.type === 'func-auth' ? request.auth.length : 'N/A'}`);
|
|
192
|
-
//
|
|
193
|
-
|
|
204
|
+
// Get fund relayer (use alternative fund relayer when requested)
|
|
205
|
+
let fundRelayerId = config.fundRelayerId;
|
|
206
|
+
if (request.fundRelayerId) {
|
|
207
|
+
if (!config.allowedFundRelayerIds.has(request.fundRelayerId)) {
|
|
208
|
+
throw (0, relayer_sdk_1.pluginError)(`Fund relayer '${request.fundRelayerId}' is not in the allowed list`, {
|
|
209
|
+
code: 'CONFIG_MISSING',
|
|
210
|
+
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
211
|
+
details: { fundRelayerId: request.fundRelayerId },
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
fundRelayerId = request.fundRelayerId;
|
|
215
|
+
}
|
|
216
|
+
const fundRelayer = api.useRelayer(fundRelayerId);
|
|
194
217
|
// 2a. Handle get-transaction early — no pool, channel, or simulation needed
|
|
195
218
|
if (request.type === 'get-transaction') {
|
|
196
219
|
const res = await fundRelayer.getTransaction({ transactionId: request.transactionId });
|
|
@@ -206,14 +229,14 @@ async function channelAccounts(context) {
|
|
|
206
229
|
throw (0, relayer_sdk_1.pluginError)('Fund relayer not found', {
|
|
207
230
|
code: 'RELAYER_UNAVAILABLE',
|
|
208
231
|
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
209
|
-
details: { relayerId:
|
|
232
|
+
details: { relayerId: fundRelayerId },
|
|
210
233
|
});
|
|
211
234
|
}
|
|
212
235
|
if (fundInfo.network_type !== 'stellar') {
|
|
213
236
|
throw (0, relayer_sdk_1.pluginError)('Fund relayer network type must be stellar', {
|
|
214
237
|
code: 'UNSUPPORTED_NETWORK',
|
|
215
238
|
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
216
|
-
details: { network_type: fundInfo.network_type, relayerId:
|
|
239
|
+
details: { network_type: fundInfo.network_type, relayerId: fundRelayerId },
|
|
217
240
|
});
|
|
218
241
|
}
|
|
219
242
|
// 3. Build acquire options for contract capacity limits
|
|
@@ -171,12 +171,8 @@ async function setChannelAccounts(kv, network, payload) {
|
|
|
171
171
|
const current = await readStoredRelayerIds(kv, listKey);
|
|
172
172
|
// Check for locked removals
|
|
173
173
|
const removals = current.filter((id) => !relayerIds.includes(id));
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
if (await isRelayerIdLocked(kv, network, id)) {
|
|
177
|
-
locked.push(id);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
174
|
+
const lockedSet = await getLockedRelayerIds(kv, network);
|
|
175
|
+
const locked = removals.filter((id) => lockedSet.has(id));
|
|
180
176
|
if (locked.length > 0) {
|
|
181
177
|
throw (0, relayer_sdk_1.pluginError)('Locked relayer IDs cannot be removed', {
|
|
182
178
|
code: 'LOCKED_CONFLICT',
|
|
@@ -211,12 +207,12 @@ async function getPoolStats(config, kv) {
|
|
|
211
207
|
status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
|
|
212
208
|
});
|
|
213
209
|
}
|
|
214
|
-
// 2. Check
|
|
210
|
+
// 2. Check locks via single listKeys scan — best-effort
|
|
215
211
|
let locked;
|
|
216
212
|
let available;
|
|
217
213
|
try {
|
|
218
|
-
const
|
|
219
|
-
locked =
|
|
214
|
+
const lockedSet = await getLockedRelayerIds(kv, network);
|
|
215
|
+
locked = relayerIds.filter((id) => lockedSet.has(id)).length;
|
|
220
216
|
available = relayerIds.length - locked;
|
|
221
217
|
}
|
|
222
218
|
catch {
|
|
@@ -268,16 +264,17 @@ async function readStoredRelayerIds(kv, key) {
|
|
|
268
264
|
});
|
|
269
265
|
}
|
|
270
266
|
}
|
|
271
|
-
async function
|
|
272
|
-
const
|
|
267
|
+
async function getLockedRelayerIds(kv, network) {
|
|
268
|
+
const prefix = `${network}:channel:in-use:`;
|
|
273
269
|
try {
|
|
274
|
-
|
|
270
|
+
const keys = await kv.listKeys(`${prefix}*`);
|
|
271
|
+
return new Set(keys.map((k) => k.slice(prefix.length)));
|
|
275
272
|
}
|
|
276
273
|
catch (error) {
|
|
277
|
-
throw (0, relayer_sdk_1.pluginError)('KV error while checking relayer
|
|
274
|
+
throw (0, relayer_sdk_1.pluginError)('KV error while checking relayer locks', {
|
|
278
275
|
code: 'KV_ERROR',
|
|
279
276
|
status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
|
|
280
|
-
details: {
|
|
277
|
+
details: { prefix, pattern: `${prefix}*`, message: error instanceof Error ? error.message : String(error) },
|
|
281
278
|
});
|
|
282
279
|
}
|
|
283
280
|
}
|
package/dist/plugin/pool.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* KV-backed, stateless channel pool.
|
|
5
5
|
* - Membership comes from KV: <network>:channel:relayer-ids
|
|
6
6
|
* - Per-relayer locks with tokens: <network>:channel:in-use:<relayerId>
|
|
7
|
-
* - Uses
|
|
7
|
+
* - Uses per-channel claim locks to make acquire safe across workers.
|
|
8
8
|
*/
|
|
9
9
|
import { PluginKVStore } from '@openzeppelin/relayer-sdk';
|
|
10
10
|
export type PoolLock = {
|
|
@@ -18,21 +18,27 @@ export type AcquireOptions = {
|
|
|
18
18
|
};
|
|
19
19
|
export declare class ChannelPool {
|
|
20
20
|
private readonly network;
|
|
21
|
-
private readonly globalLockKey;
|
|
22
21
|
private readonly channelLockTtlSec;
|
|
23
|
-
private readonly mutexTtlSec;
|
|
24
22
|
private readonly kv;
|
|
25
23
|
constructor(network: 'testnet' | 'mainnet', kv: PluginKVStore, lockTtlSeconds: number);
|
|
26
24
|
/** Acquire a relayerId with a token lock */
|
|
27
25
|
acquire(options: AcquireOptions): Promise<PoolLock>;
|
|
28
|
-
|
|
29
|
-
private
|
|
26
|
+
/** Read phase + claim phase (no global mutex) */
|
|
27
|
+
private tryAcquire;
|
|
28
|
+
/** Attempt to claim a single channel under its per-channel lock */
|
|
29
|
+
private tryClaimChannel;
|
|
30
30
|
/** Extend the lock TTL if we still own it (e.g. after WAIT_TIMEOUT) */
|
|
31
31
|
extendLock(lock: PoolLock, ttlSec?: number): Promise<void>;
|
|
32
32
|
/** Release the lock if we own it */
|
|
33
33
|
release(lock: PoolLock): Promise<void>;
|
|
34
|
+
/** Release with cooldown: keeps lock alive with short TTL to hard-block the channel. */
|
|
35
|
+
releaseWithCooldown(lock: PoolLock, cooldownMs?: 6000): Promise<void>;
|
|
34
36
|
private membershipKey;
|
|
37
|
+
private lockKeyPrefix;
|
|
35
38
|
private lockKey;
|
|
39
|
+
private claimKey;
|
|
40
|
+
private lruMapKey;
|
|
41
|
+
private updateLruMap;
|
|
36
42
|
private getRelayerIdsFromKV;
|
|
37
43
|
private getPoolCapacityDetails;
|
|
38
44
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../src/plugin/pool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;AAIvE,MAAM,MAAM,QAAQ,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAUF,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../src/plugin/pool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;AAIvE,MAAM,MAAM,QAAQ,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAUF,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgB;gBAEvB,OAAO,EAAE,SAAS,GAAG,SAAS,EAAE,EAAE,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM;IAMrF,4CAA4C;IACtC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC;IAuBzD,iDAAiD;YACnC,UAAU;IAqDxB,mEAAmE;YACrD,eAAe;IAsB7B,uEAAuE;IACjE,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBhE,oCAAoC;IAC9B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAY5C,wFAAwF;IAClF,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,OAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;IAa/F,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,SAAS;YAIH,YAAY;YAWZ,mBAAmB;YAYnB,sBAAsB;CAUrC"}
|
package/dist/plugin/pool.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* KV-backed, stateless channel pool.
|
|
6
6
|
* - Membership comes from KV: <network>:channel:relayer-ids
|
|
7
7
|
* - Per-relayer locks with tokens: <network>:channel:in-use:<relayerId>
|
|
8
|
-
* - Uses
|
|
8
|
+
* - Uses per-channel claim locks to make acquire safe across workers.
|
|
9
9
|
*/
|
|
10
10
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
11
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -19,21 +19,18 @@ class ChannelPool {
|
|
|
19
19
|
constructor(network, kv, lockTtlSeconds) {
|
|
20
20
|
this.network = network;
|
|
21
21
|
this.kv = kv;
|
|
22
|
-
this.globalLockKey = `${this.network}:channel-pool-lock`;
|
|
23
22
|
this.channelLockTtlSec = lockTtlSeconds;
|
|
24
|
-
this.mutexTtlSec = constants_1.POOL.MUTEX_TTL_SECONDS;
|
|
25
23
|
}
|
|
26
24
|
/** Acquire a relayerId with a token lock */
|
|
27
25
|
async acquire(options) {
|
|
28
|
-
const maxSpins = constants_1.POOL.
|
|
26
|
+
const maxSpins = constants_1.POOL.ACQUIRE_MAX_SPINS;
|
|
29
27
|
for (let i = 0; i < maxSpins; i++) {
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return r;
|
|
28
|
+
const result = await this.tryAcquire(options);
|
|
29
|
+
if (result)
|
|
30
|
+
return result;
|
|
31
|
+
const jitter = constants_1.POOL.ACQUIRE_RETRY_MIN_MS +
|
|
32
|
+
Math.floor(Math.random() * (constants_1.POOL.ACQUIRE_RETRY_MAX_MS - constants_1.POOL.ACQUIRE_RETRY_MIN_MS + 1));
|
|
33
|
+
await sleep(jitter);
|
|
37
34
|
}
|
|
38
35
|
const diagnostics = await this.getPoolCapacityDetails(options, maxSpins);
|
|
39
36
|
console.warn('[channels] Pool capacity exhausted', diagnostics);
|
|
@@ -43,12 +40,9 @@ class ChannelPool {
|
|
|
43
40
|
details: diagnostics,
|
|
44
41
|
});
|
|
45
42
|
}
|
|
46
|
-
|
|
47
|
-
async
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
// Inside the mutex: pick an available relayer and set its channel lock
|
|
51
|
-
async tryLockAnyRelayer(options) {
|
|
43
|
+
/** Read phase + claim phase (no global mutex) */
|
|
44
|
+
async tryAcquire(options) {
|
|
45
|
+
// --- READ PHASE (no lock) ---
|
|
52
46
|
let ids = await this.getRelayerIdsFromKV();
|
|
53
47
|
if (ids.length === 0) {
|
|
54
48
|
throw (0, relayer_sdk_1.pluginError)('No channel accounts configured. Use the management API to set channel accounts.', {
|
|
@@ -56,23 +50,58 @@ class ChannelPool {
|
|
|
56
50
|
status: constants_1.HTTP_STATUS.SERVICE_UNAVAILABLE,
|
|
57
51
|
});
|
|
58
52
|
}
|
|
59
|
-
// If contract is limited, filter to allowed subset
|
|
60
53
|
if (options.contractId && options.limitedContracts.has(options.contractId)) {
|
|
61
54
|
ids = filterChannelsForLimitedContract(ids, options.capacityRatio);
|
|
62
55
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
const lockPrefix = this.lockKeyPrefix();
|
|
57
|
+
let lruMap = {};
|
|
58
|
+
try {
|
|
59
|
+
lruMap = (await this.kv.get(this.lruMapKey())) ?? {};
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.warn('[channels] LRU map read failed, using empty ordering map', err);
|
|
63
|
+
}
|
|
64
|
+
let lockedSet;
|
|
65
|
+
try {
|
|
66
|
+
const lockedKeys = await this.kv.listKeys(`${lockPrefix}*`);
|
|
67
|
+
lockedSet = new Set(lockedKeys.map((k) => k.slice(lockPrefix.length)));
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
// Fallback: if listKeys fails, degrade to O(N) per-channel exists checks.
|
|
71
|
+
// This is expensive with many channels — log so persistent failures are observable.
|
|
72
|
+
console.warn('[channels] listKeys failed, falling back to per-channel exists checks', err);
|
|
73
|
+
const results = await Promise.all(ids.map((id) => this.kv.exists(this.lockKey(id))));
|
|
74
|
+
lockedSet = new Set(ids.filter((_, i) => results[i]));
|
|
75
|
+
}
|
|
76
|
+
const unlocked = ids.filter((id) => !lockedSet.has(id));
|
|
77
|
+
if (unlocked.length === 0)
|
|
78
|
+
return null;
|
|
79
|
+
// Sort by LRU ascending — oldest channel is always first (deterministic).
|
|
80
|
+
// Shuffle-then-stable-sort: tie-break among equal timestamps is random,
|
|
81
|
+
// spreading contention when multiple channels share the same LRU value.
|
|
82
|
+
shuffle(unlocked);
|
|
83
|
+
unlocked.sort((a, b) => (lruMap[a] ?? 0) - (lruMap[b] ?? 0));
|
|
84
|
+
const candidates = unlocked;
|
|
85
|
+
// --- CLAIM PHASE (per-channel lock) ---
|
|
86
|
+
for (const candidate of candidates) {
|
|
87
|
+
const result = await this.tryClaimChannel(candidate);
|
|
88
|
+
if (result)
|
|
89
|
+
return result;
|
|
73
90
|
}
|
|
74
91
|
return null;
|
|
75
92
|
}
|
|
93
|
+
/** Attempt to claim a single channel under its per-channel lock */
|
|
94
|
+
async tryClaimChannel(relayerId) {
|
|
95
|
+
return this.kv.withLock(this.claimKey(relayerId), async () => {
|
|
96
|
+
// Double-check: another worker may have claimed it between our scan and this lock
|
|
97
|
+
if (await this.kv.exists(this.lockKey(relayerId)))
|
|
98
|
+
return null;
|
|
99
|
+
const token = randomToken();
|
|
100
|
+
await this.kv.set(this.lockKey(relayerId), { token, lockedAt: new Date().toISOString() }, { ttlSec: this.channelLockTtlSec });
|
|
101
|
+
await this.updateLruMap(relayerId);
|
|
102
|
+
return { relayerId, token };
|
|
103
|
+
}, { ttlSec: constants_1.POOL.CLAIM_LOCK_TTL_SECONDS, onBusy: 'skip' });
|
|
104
|
+
}
|
|
76
105
|
/** Extend the lock TTL if we still own it (e.g. after WAIT_TIMEOUT) */
|
|
77
106
|
async extendLock(lock, ttlSec) {
|
|
78
107
|
try {
|
|
@@ -99,11 +128,45 @@ class ChannelPool {
|
|
|
99
128
|
// ignore release errors
|
|
100
129
|
}
|
|
101
130
|
}
|
|
131
|
+
/** Release with cooldown: keeps lock alive with short TTL to hard-block the channel. */
|
|
132
|
+
async releaseWithCooldown(lock, cooldownMs = constants_1.POOL.CHANNEL_COOLDOWN_MS) {
|
|
133
|
+
try {
|
|
134
|
+
const key = this.lockKey(lock.relayerId);
|
|
135
|
+
const current = await this.kv.get(key);
|
|
136
|
+
if (current?.token === lock.token) {
|
|
137
|
+
const cooldownSec = Math.max(1, Math.ceil(cooldownMs / 1000));
|
|
138
|
+
await this.kv.set(key, current, { ttlSec: cooldownSec });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// ignore
|
|
143
|
+
}
|
|
144
|
+
}
|
|
102
145
|
membershipKey() {
|
|
103
146
|
return `${this.network}:channel:relayer-ids`;
|
|
104
147
|
}
|
|
148
|
+
lockKeyPrefix() {
|
|
149
|
+
return `${this.network}:channel:in-use:`;
|
|
150
|
+
}
|
|
105
151
|
lockKey(relayerId) {
|
|
106
|
-
return `${this.
|
|
152
|
+
return `${this.lockKeyPrefix()}${relayerId}`;
|
|
153
|
+
}
|
|
154
|
+
claimKey(relayerId) {
|
|
155
|
+
return `${this.network}:channel:claim:${relayerId}`;
|
|
156
|
+
}
|
|
157
|
+
lruMapKey() {
|
|
158
|
+
return `${this.network}:channel:lru-map`;
|
|
159
|
+
}
|
|
160
|
+
async updateLruMap(relayerId) {
|
|
161
|
+
try {
|
|
162
|
+
const lruMap = (await this.kv.get(this.lruMapKey())) ?? {};
|
|
163
|
+
lruMap[relayerId] = Date.now();
|
|
164
|
+
await this.kv.set(this.lruMapKey(), lruMap, { ttlSec: constants_1.POOL.LRU_MAP_TTL_SECONDS });
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
console.debug('[channels] failed to update LRU map', err);
|
|
168
|
+
// Best-effort: stale LRU map only affects ordering, not correctness
|
|
169
|
+
}
|
|
107
170
|
}
|
|
108
171
|
async getRelayerIdsFromKV() {
|
|
109
172
|
try {
|
|
@@ -121,7 +184,7 @@ class ChannelPool {
|
|
|
121
184
|
async getPoolCapacityDetails(options, maxSpins) {
|
|
122
185
|
const isLimited = !!(options.contractId && options.limitedContracts.has(options.contractId));
|
|
123
186
|
return {
|
|
124
|
-
reason: isLimited ? 'limited_contract_capacity' : '
|
|
187
|
+
reason: isLimited ? 'limited_contract_capacity' : 'all_channels_busy_or_claim_contention',
|
|
125
188
|
contractId: options.contractId,
|
|
126
189
|
capacityRatio: options.capacityRatio,
|
|
127
190
|
maxSpins,
|
package/dist/plugin/submit.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Transaction } from '@stellar/stellar-sdk';
|
|
7
7
|
import { Relayer, PluginAPI } from '@openzeppelin/relayer-sdk';
|
|
8
|
+
import type { ChannelAccountsConfig } from './config';
|
|
8
9
|
import { ChannelAccountsResponse } from './types';
|
|
9
10
|
import { FeeTracker } from './fee-tracking';
|
|
10
11
|
export interface SubmitContext {
|
|
@@ -25,7 +26,7 @@ export declare function signWithChannelAndFund(transaction: Transaction, channel
|
|
|
25
26
|
/**
|
|
26
27
|
* Submit transaction with fee bump and wait for confirmation
|
|
27
28
|
*/
|
|
28
|
-
export declare function submitWithFeeBumpAndWait(fundRelayer: Relayer, signedXdr: string, network: 'testnet' | 'mainnet', maxFee: number, api: PluginAPI, startTime: number, tracker?: FeeTracker, context?: SubmitContext, skipWait?: boolean): Promise<ChannelAccountsResponse>;
|
|
29
|
+
export declare function submitWithFeeBumpAndWait(fundRelayer: Relayer, signedXdr: string, network: 'testnet' | 'mainnet', maxFee: number, api: PluginAPI, startTime: number, tracker?: FeeTracker, context?: SubmitContext, skipWait?: boolean, config?: Pick<ChannelAccountsConfig, 'globalTimeoutMs' | 'pollingTimeoutMs'>): Promise<ChannelAccountsResponse>;
|
|
29
30
|
/** Try to decode a transaction result XDR from the reason string */
|
|
30
31
|
export declare function decodeTransactionResult(reason: string): DecodedTransactionResult | null;
|
|
31
32
|
export declare function buildStellarLabTransactionUrl(network: 'testnet' | 'mainnet', txHash: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"submit.d.ts","sourceRoot":"","sources":["../../src/plugin/submit.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAO,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAEL,OAAO,EAGP,SAAS,EACV,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,wBAAwB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,OAAO,EACvB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,WAAW,CAAC,CAuBtB;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,WAAW,EAAE,OAAO,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,UAAU,EACpB,OAAO,CAAC,EAAE,aAAa,EACvB,QAAQ,CAAC,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"submit.d.ts","sourceRoot":"","sources":["../../src/plugin/submit.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAO,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAEL,OAAO,EAGP,SAAS,EACV,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,wBAAwB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,OAAO,EACvB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,WAAW,CAAC,CAuBtB;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,WAAW,EAAE,OAAO,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,UAAU,EACpB,OAAO,CAAC,EAAE,aAAa,EACvB,QAAQ,CAAC,EAAE,OAAO,EAClB,MAAM,CAAC,EAAE,IAAI,CAAC,qBAAqB,EAAE,iBAAiB,GAAG,kBAAkB,CAAC,GAC3E,OAAO,CAAC,uBAAuB,CAAC,CAmHlC;AASD,oEAAoE;AACpE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,wBAAwB,GAAG,IAAI,CAkCvF;AAeD,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAepG;AAED,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOrD"}
|
package/dist/plugin/submit.js
CHANGED
|
@@ -42,7 +42,7 @@ async function signWithChannelAndFund(transaction, channelRelayer, _fundRelayer,
|
|
|
42
42
|
/**
|
|
43
43
|
* Submit transaction with fee bump and wait for confirmation
|
|
44
44
|
*/
|
|
45
|
-
async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee, api, startTime, tracker, context, skipWait) {
|
|
45
|
+
async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee, api, startTime, tracker, context, skipWait, config) {
|
|
46
46
|
// Submit with fee bump
|
|
47
47
|
console.debug(`[channels] Sending fee bump tx: network=${network}, maxFee=${maxFee}, xdr_len=${signedXdr.length}`);
|
|
48
48
|
const payload = {
|
|
@@ -66,8 +66,10 @@ async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee,
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
// Compute dynamic timeout from remaining global budget
|
|
69
|
+
const globalTimeout = config?.globalTimeoutMs ?? constants_1.TIMEOUT.DEFAULT_GLOBAL_TIMEOUT_MS;
|
|
70
|
+
const pollingTimeout = config?.pollingTimeoutMs ?? constants_1.POLLING.DEFAULT_TIMEOUT_MS;
|
|
69
71
|
const elapsedMs = Date.now() - startTime;
|
|
70
|
-
const remainingMs =
|
|
72
|
+
const remainingMs = globalTimeout - constants_1.TIMEOUT.BUFFER_MS - elapsedMs;
|
|
71
73
|
if (remainingMs <= 0) {
|
|
72
74
|
throw (0, relayer_sdk_1.pluginError)('No time remaining for transaction wait', {
|
|
73
75
|
code: 'WAIT_TIMEOUT',
|
|
@@ -79,7 +81,7 @@ async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee,
|
|
|
79
81
|
},
|
|
80
82
|
});
|
|
81
83
|
}
|
|
82
|
-
const timeout = Math.min(remainingMs,
|
|
84
|
+
const timeout = Math.min(remainingMs, pollingTimeout);
|
|
83
85
|
console.debug(`[channels] Transaction wait: elapsed=${elapsedMs}ms, timeout=${timeout}ms`);
|
|
84
86
|
// Wait for confirmation
|
|
85
87
|
try {
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -13,14 +13,17 @@ export type ChannelAccountsRequest = {
|
|
|
13
13
|
type: 'xdr';
|
|
14
14
|
xdr: string;
|
|
15
15
|
skipWait: boolean;
|
|
16
|
+
fundRelayerId?: string;
|
|
16
17
|
} | {
|
|
17
18
|
type: 'func-auth';
|
|
18
19
|
func: xdr.HostFunction;
|
|
19
20
|
auth: xdr.SorobanAuthorizationEntry[];
|
|
20
21
|
skipWait: boolean;
|
|
22
|
+
fundRelayerId?: string;
|
|
21
23
|
} | {
|
|
22
24
|
type: 'get-transaction';
|
|
23
25
|
transactionId: string;
|
|
26
|
+
fundRelayerId?: string;
|
|
24
27
|
};
|
|
25
28
|
/**
|
|
26
29
|
* Plugin response format aligned with launchtube
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/plugin/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;GAIG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAEhD,MAAM,MAAM,sBAAsB,GAC9B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/plugin/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;GAIG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAEhD,MAAM,MAAM,sBAAsB,GAC9B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GACvE;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC;IACvB,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,CAAC;IACtC,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACD;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/E;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE;QACV,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,qBAAqB,GAAG,oBAAoB,CAAC;QACrD,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,OAAO,CAAC;IACZ,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/plugin/validation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/plugin/validation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAqBjD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,GAAG,GAAG,sBAAsB,CAmJ3E"}
|
|
@@ -10,6 +10,21 @@ exports.validateAndParseRequest = validateAndParseRequest;
|
|
|
10
10
|
const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
11
11
|
const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
|
|
12
12
|
const constants_1 = require("./constants");
|
|
13
|
+
function validateFundRelayerId(params) {
|
|
14
|
+
if (params.fundRelayerId !== undefined &&
|
|
15
|
+
(typeof params.fundRelayerId !== 'string' || params.fundRelayerId.trim() === '')) {
|
|
16
|
+
throw (0, relayer_sdk_1.pluginError)('`fundRelayerId` must be a non-empty string', {
|
|
17
|
+
code: 'INVALID_PARAMS',
|
|
18
|
+
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function parseFundRelayerId(params) {
|
|
23
|
+
if (params.fundRelayerId !== undefined && typeof params.fundRelayerId === 'string') {
|
|
24
|
+
return params.fundRelayerId.trim();
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
13
28
|
function validateAndParseRequest(params) {
|
|
14
29
|
if (!params || typeof params !== 'object') {
|
|
15
30
|
throw (0, relayer_sdk_1.pluginError)('Invalid request: params must be an object', {
|
|
@@ -21,7 +36,7 @@ function validateAndParseRequest(params) {
|
|
|
21
36
|
const keys = Object.keys(params);
|
|
22
37
|
// Mode: getTransaction
|
|
23
38
|
if ('getTransaction' in params) {
|
|
24
|
-
const unknown = keys.filter((k) =>
|
|
39
|
+
const unknown = keys.filter((k) => !['getTransaction', 'fundRelayerId'].includes(k));
|
|
25
40
|
if (unknown.length > 0) {
|
|
26
41
|
throw (0, relayer_sdk_1.pluginError)('`getTransaction` request must not include other parameters', {
|
|
27
42
|
code: 'INVALID_PARAMS',
|
|
@@ -29,6 +44,7 @@ function validateAndParseRequest(params) {
|
|
|
29
44
|
details: { unknown },
|
|
30
45
|
});
|
|
31
46
|
}
|
|
47
|
+
validateFundRelayerId(params);
|
|
32
48
|
const gt = params.getTransaction;
|
|
33
49
|
if (!gt || typeof gt !== 'object' || typeof gt.transactionId !== 'string' || gt.transactionId.trim() === '') {
|
|
34
50
|
throw (0, relayer_sdk_1.pluginError)('`getTransaction.transactionId` must be a non-empty string', {
|
|
@@ -44,7 +60,11 @@ function validateAndParseRequest(params) {
|
|
|
44
60
|
details: { unknown: unknownInner },
|
|
45
61
|
});
|
|
46
62
|
}
|
|
47
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
type: 'get-transaction',
|
|
65
|
+
transactionId: gt.transactionId.trim(),
|
|
66
|
+
fundRelayerId: parseFundRelayerId(params),
|
|
67
|
+
};
|
|
48
68
|
}
|
|
49
69
|
// Mode: XDR
|
|
50
70
|
if ('xdr' in params) {
|
|
@@ -55,7 +75,7 @@ function validateAndParseRequest(params) {
|
|
|
55
75
|
});
|
|
56
76
|
}
|
|
57
77
|
// Strict: cannot include func/auth when using xdr
|
|
58
|
-
const unknown = keys.filter((k) => !['xdr', 'skipWait'].includes(k));
|
|
78
|
+
const unknown = keys.filter((k) => !['xdr', 'skipWait', 'fundRelayerId'].includes(k));
|
|
59
79
|
if (unknown.length > 0) {
|
|
60
80
|
throw (0, relayer_sdk_1.pluginError)('`xdr` request must not include other parameters', {
|
|
61
81
|
code: 'INVALID_PARAMS',
|
|
@@ -69,7 +89,13 @@ function validateAndParseRequest(params) {
|
|
|
69
89
|
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
70
90
|
});
|
|
71
91
|
}
|
|
72
|
-
|
|
92
|
+
validateFundRelayerId(params);
|
|
93
|
+
return {
|
|
94
|
+
type: 'xdr',
|
|
95
|
+
xdr: params.xdr.trim(),
|
|
96
|
+
skipWait: params.skipWait === true,
|
|
97
|
+
fundRelayerId: parseFundRelayerId(params),
|
|
98
|
+
};
|
|
73
99
|
}
|
|
74
100
|
// Mode: func+auth
|
|
75
101
|
if ('func' in params || 'auth' in params) {
|
|
@@ -113,7 +139,14 @@ function validateAndParseRequest(params) {
|
|
|
113
139
|
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
114
140
|
});
|
|
115
141
|
}
|
|
116
|
-
|
|
142
|
+
validateFundRelayerId(params);
|
|
143
|
+
return {
|
|
144
|
+
type: 'func-auth',
|
|
145
|
+
func,
|
|
146
|
+
auth,
|
|
147
|
+
skipWait: params.skipWait === true,
|
|
148
|
+
fundRelayerId: parseFundRelayerId(params),
|
|
149
|
+
};
|
|
117
150
|
}
|
|
118
151
|
throw (0, relayer_sdk_1.pluginError)('Must pass either `xdr` or `func` and `auth`', {
|
|
119
152
|
code: 'INVALID_PARAMS',
|