@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 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.
@@ -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":"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"}
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"}
@@ -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;CAC5C;AA0GD;;GAEG;AACH,wBAAgB,UAAU,IAAI,qBAAqB,CAwBlD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAE3E"}
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"}
@@ -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 MUTEX_TTL_SECONDS: 1;
27
- readonly MUTEX_MAX_SPINS: 30;
28
- readonly MUTEX_RETRY_MIN_MS: 10;
29
- readonly MUTEX_RETRY_MAX_MS: 30;
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 GLOBAL_TIMEOUT_MS: 30000;
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 TIMEOUT_MS: 25000;
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;;;;;CAOP,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"}
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"}
@@ -29,12 +29,17 @@ exports.CONFIG = {
29
29
  };
30
30
  // Pool Constants
31
31
  exports.POOL = {
32
- // TTL for the short global mutex guarding acquire()
33
- MUTEX_TTL_SECONDS: 1,
34
- // Retry policy when mutex is busy
35
- MUTEX_MAX_SPINS: 30,
36
- MUTEX_RETRY_MIN_MS: 10,
37
- MUTEX_RETRY_MAX_MS: 30,
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
- GLOBAL_TIMEOUT_MS: 30000,
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
- TIMEOUT_MS: 25000,
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;AAiSD;;GAEG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAElE"}
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"}
@@ -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
- await ctx.pool.release(poolLock);
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
- // 1. Validate and parse request (xdr OR func+auth)
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
- // 2. Get fund relayer
193
- const fundRelayer = api.useRelayer(config.fundRelayerId);
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: config.fundRelayerId },
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: config.fundRelayerId },
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 locked = [];
175
- for (const id of removals) {
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 all locks in parallel (N KV calls) — best-effort
210
+ // 2. Check locks via single listKeys scan — best-effort
215
211
  let locked;
216
212
  let available;
217
213
  try {
218
- const results = await Promise.all(relayerIds.map((id) => kv.exists(`${network}:channel:in-use:${id}`)));
219
- locked = results.filter(Boolean).length;
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 isRelayerIdLocked(kv, network, id) {
272
- const key = `${network}:channel:in-use:${id}`;
267
+ async function getLockedRelayerIds(kv, network) {
268
+ const prefix = `${network}:channel:in-use:`;
273
269
  try {
274
- return await kv.exists(key);
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 lock', {
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: { relayerId: id, key, message: error instanceof Error ? error.message : String(error) },
277
+ details: { prefix, pattern: `${prefix}*`, message: error instanceof Error ? error.message : String(error) },
281
278
  });
282
279
  }
283
280
  }
@@ -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 a short global mutex to make acquire atomic across workers.
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
- private withGlobalMutex;
29
- private tryLockAnyRelayer;
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,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgB;gBAEvB,OAAO,EAAE,SAAS,GAAG,SAAS,EAAE,EAAE,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM;IAQrF,4CAA4C;IACtC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC;YAyB3C,eAAe;YAKf,iBAAiB;IA2B/B,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,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,OAAO;YAID,mBAAmB;YAYnB,sBAAsB;CAUrC"}
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"}
@@ -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 a short global mutex to make acquire atomic across workers.
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.MUTEX_MAX_SPINS;
26
+ const maxSpins = constants_1.POOL.ACQUIRE_MAX_SPINS;
29
27
  for (let i = 0; i < maxSpins; i++) {
30
- const r = await this.withGlobalMutex(() => this.tryLockAnyRelayer(options));
31
- if (r === null) {
32
- const jitter = constants_1.POOL.MUTEX_RETRY_MIN_MS + Math.floor(Math.random() * (constants_1.POOL.MUTEX_RETRY_MAX_MS - constants_1.POOL.MUTEX_RETRY_MIN_MS + 1));
33
- await sleep(jitter);
34
- continue;
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
- // Run a function under the short-lived global mutex; returns null if busy
47
- async withGlobalMutex(fn) {
48
- return this.kv.withLock(this.globalLockKey, fn, { ttlSec: this.mutexTtlSec, onBusy: 'skip' });
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
- shuffle(ids);
64
- for (const relayerId of ids) {
65
- const key = this.lockKey(relayerId);
66
- const exists = await this.kv.exists(key);
67
- if (exists)
68
- continue;
69
- const token = randomToken();
70
- const entry = { token, lockedAt: new Date().toISOString() };
71
- await this.kv.set(key, entry, { ttlSec: this.channelLockTtlSec });
72
- return { relayerId, token };
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.network}:channel:in-use:${relayerId}`;
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' : 'all_channels_busy_or_mutex_contention',
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,
@@ -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,GACjB,OAAO,CAAC,uBAAuB,CAAC,CAiHlC;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"}
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"}
@@ -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 = constants_1.TIMEOUT.GLOBAL_TIMEOUT_MS - constants_1.TIMEOUT.BUFFER_MS - elapsedMs;
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, constants_1.POLLING.TIMEOUT_MS);
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 {
@@ -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,GAC/C;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC;IAAC,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GACvG;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvD;;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
+ {"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;AAEjD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,GAAG,GAAG,sBAAsB,CA8H3E"}
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) => k !== 'getTransaction');
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 { type: 'get-transaction', transactionId: gt.transactionId.trim() };
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
- return { type: 'xdr', xdr: params.xdr.trim(), skipWait: params.skipWait === true };
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
- return { type: 'func-auth', func, auth, skipWait: params.skipWait === true };
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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openzeppelin/relayer-plugin-channels",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "OpenZeppelin Relayer Plugin for Stellar Channel Accounts",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",