@openzeppelin/relayer-plugin-channels 0.17.0 → 0.19.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.
@@ -19,6 +19,8 @@ export interface ChannelAccountsConfig {
19
19
  inclusionFeeLimited: number;
20
20
  sequenceNumberCacheMaxAgeMs: number;
21
21
  minSignatureExpirationLedgerBuffer: number;
22
+ globalTimeoutMs: number;
23
+ pollingTimeoutMs: number;
22
24
  }
23
25
  /**
24
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,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;CAC5C;AAsHD;;GAEG;AACH,wBAAgB,UAAU,IAAI,qBAAqB,CA2BlD;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"}
@@ -117,6 +117,20 @@ function parseAllowedFundRelayerIds() {
117
117
  }
118
118
  return ids;
119
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
+ }
120
134
  function parseContractCapacityRatio() {
121
135
  const raw = process.env.CONTRACT_CAPACITY_RATIO;
122
136
  if (!raw)
@@ -154,6 +168,8 @@ function loadConfig() {
154
168
  inclusionFeeLimited: parseInclusionFee('INCLUSION_FEE_LIMITED', DEFAULT_INCLUSION_FEE_LIMITED),
155
169
  sequenceNumberCacheMaxAgeMs: parseSequenceNumberCacheMaxAge(),
156
170
  minSignatureExpirationLedgerBuffer: parseMinAuthExpiryLedgerBuffer(),
171
+ globalTimeoutMs: parseGlobalTimeoutMs(),
172
+ pollingTimeoutMs: parsePollingTimeoutMs(),
157
173
  };
158
174
  }
159
175
  /**
@@ -23,11 +23,15 @@ 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;
30
- };
26
+ readonly CLAIM_LOCK_TTL_SECONDS: 3;
27
+ readonly ACQUIRE_MAX_SPINS: 12;
28
+ readonly ACQUIRE_BASE_DELAY_MS: 25;
29
+ readonly ACQUIRE_MAX_DELAY_MS: 500;
30
+ readonly MAX_CLAIMS_PER_SPIN: 3;
31
+ readonly CHANNEL_COOLDOWN_MS: 6000;
32
+ readonly LRU_KEY_TTL_SECONDS: 86400;
33
+ };
34
+ export declare const RELAYER_INFO_CACHE_TTL_SECONDS = 1800;
31
35
  export declare const TIME: {
32
36
  readonly MAX_TIME_BOUND_OFFSET_SECONDS: 60;
33
37
  };
@@ -41,12 +45,12 @@ export declare const SIMULATION: {
41
45
  readonly MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER: 2;
42
46
  };
43
47
  export declare const TIMEOUT: {
44
- readonly GLOBAL_TIMEOUT_MS: 30000;
48
+ readonly DEFAULT_GLOBAL_TIMEOUT_MS: 30000;
45
49
  readonly BUFFER_MS: 2000;
46
50
  };
47
51
  export declare const POLLING: {
48
52
  readonly INTERVAL_MS: 1000;
49
- readonly TIMEOUT_MS: 25000;
53
+ readonly DEFAULT_TIMEOUT_MS: 25000;
50
54
  };
51
55
  export declare const FEE: {
52
56
  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;;;;;;;;CAcP,CAAC;AAGX,eAAO,MAAM,8BAA8B,OAAQ,CAAC;AAGpD,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"}
@@ -5,7 +5,7 @@
5
5
  * Centralized constants for the channel accounts plugin.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.FEE = exports.POLLING = exports.TIMEOUT = exports.SIMULATION = exports.TIME = exports.POOL = exports.CONFIG = exports.HTTP_STATUS = void 0;
8
+ exports.FEE = exports.POLLING = exports.TIMEOUT = exports.SIMULATION = exports.TIME = exports.RELAYER_INFO_CACHE_TTL_SECONDS = exports.POOL = exports.CONFIG = exports.HTTP_STATUS = void 0;
9
9
  // HTTP Status Codes
10
10
  exports.HTTP_STATUS = {
11
11
  BAD_REQUEST: 400,
@@ -29,13 +29,22 @@ 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: exponential backoff with full jitter
36
+ ACQUIRE_MAX_SPINS: 12,
37
+ ACQUIRE_BASE_DELAY_MS: 25,
38
+ ACQUIRE_MAX_DELAY_MS: 500,
39
+ // Max claim-lock attempts per spin (batch size)
40
+ MAX_CLAIMS_PER_SPIN: 3,
41
+ // Hard-block cooldown for uncertain-outcome channels (~1 Stellar ledger with margin)
42
+ CHANNEL_COOLDOWN_MS: 6000,
43
+ // Housekeeping TTL for per-channel LRU keys
44
+ LRU_KEY_TTL_SECONDS: 86400,
38
45
  };
46
+ // Relayer info cache — address and network_type are effectively immutable
47
+ exports.RELAYER_INFO_CACHE_TTL_SECONDS = 1800; // 30 minutes
39
48
  // Time Constants
40
49
  exports.TIME = {
41
50
  MAX_TIME_BOUND_OFFSET_SECONDS: 60,
@@ -52,13 +61,13 @@ exports.SIMULATION = {
52
61
  };
53
62
  // Global plugin timeout budget
54
63
  exports.TIMEOUT = {
55
- GLOBAL_TIMEOUT_MS: 30000,
64
+ DEFAULT_GLOBAL_TIMEOUT_MS: 30000,
56
65
  BUFFER_MS: 2000,
57
66
  };
58
67
  // Polling for transactionWait
59
68
  exports.POLLING = {
60
69
  INTERVAL_MS: 1000,
61
- TIMEOUT_MS: 25000,
70
+ DEFAULT_TIMEOUT_MS: 25000,
62
71
  };
63
72
  exports.FEE = {
64
73
  // For non-Soroban txs: 100,000 stroops (0.01 XLM) per Stellar best practice
@@ -5,7 +5,20 @@
5
5
  * Orchestrates the transaction processing pipeline using channel accounts with fee bumping.
6
6
  */
7
7
  import { PluginContext } from '@openzeppelin/relayer-sdk';
8
+ import type { Relayer } from '@openzeppelin/relayer-sdk';
8
9
  import { Transaction, xdr } from '@stellar/stellar-sdk';
10
+ /** Subset of relayer metadata used by the plugin (address + network_type). */
11
+ type CachedRelayerInfo = {
12
+ address: string;
13
+ network_type: string;
14
+ };
15
+ /** @internal Exported for test isolation only. */
16
+ export declare function clearRelayerInfoCache(): void;
17
+ /**
18
+ * Return cached relayer info or fetch from the API and cache the result.
19
+ * Returns null if the relayer has no address (misconfigured).
20
+ */
21
+ export declare function getCachedRelayerInfo(network: string, relayerId: string, relayer: Relayer): Promise<CachedRelayerInfo | null>;
9
22
  /**
10
23
  * Extracts func and auth from an unsigned Soroban transaction.
11
24
  * Returns null if the transaction is not a single invokeHostFunction operation.
@@ -18,4 +31,5 @@ export declare function extractFuncAuthFromUnsignedXdr(tx: Transaction): {
18
31
  * Main plugin handler exported for OpenZeppelin Relayer
19
32
  */
20
33
  export declare function handler(context: PluginContext): Promise<any>;
34
+ export {};
21
35
  //# sourceMappingURL=handler.d.ts.map
@@ -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;AA4SD;;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;AACvE,OAAO,KAAK,EAA4B,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAQnF,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAOxD,8EAA8E;AAC9E,KAAK,iBAAiB,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AAUnE,kDAAkD;AAClD,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAanC;AAsBD;;;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"}
@@ -6,6 +6,8 @@
6
6
  * Orchestrates the transaction processing pipeline using channel accounts with fee bumping.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.clearRelayerInfoCache = clearRelayerInfoCache;
10
+ exports.getCachedRelayerInfo = getCachedRelayerInfo;
9
11
  exports.extractFuncAuthFromUnsignedXdr = extractFuncAuthFromUnsignedXdr;
10
12
  exports.handler = handler;
11
13
  const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
@@ -21,6 +23,35 @@ const fee_1 = require("./fee");
21
23
  const tx_1 = require("./tx");
22
24
  const fee_tracking_1 = require("./fee-tracking");
23
25
  const sequence_1 = require("./sequence");
26
+ /**
27
+ * In-memory cache for relayer info. Avoids a remote API call (getRelayer → HTTP GET)
28
+ * on every request while a channel lock is held. Keyed by `${network}:${relayerId}`.
29
+ * Entries expire after RELAYER_INFO_CACHE_TTL_SECONDS; stale entries are evicted on miss.
30
+ */
31
+ const relayerInfoCache = new Map();
32
+ /** @internal Exported for test isolation only. */
33
+ function clearRelayerInfoCache() {
34
+ relayerInfoCache.clear();
35
+ }
36
+ /**
37
+ * Return cached relayer info or fetch from the API and cache the result.
38
+ * Returns null if the relayer has no address (misconfigured).
39
+ */
40
+ async function getCachedRelayerInfo(network, relayerId, relayer) {
41
+ const cacheKey = `${network}:${relayerId}`;
42
+ const cached = relayerInfoCache.get(cacheKey);
43
+ if (cached && cached.expiresAt > Date.now())
44
+ return cached.info;
45
+ // Evict stale entry so it doesn't linger in memory
46
+ if (cached)
47
+ relayerInfoCache.delete(cacheKey);
48
+ const info = await relayer.getRelayer();
49
+ if (!info?.address)
50
+ return null;
51
+ const entry = { address: info.address, network_type: info.network_type };
52
+ relayerInfoCache.set(cacheKey, { info: entry, expiresAt: Date.now() + constants_1.RELAYER_INFO_CACHE_TTL_SECONDS * 1000 });
53
+ return entry;
54
+ }
24
55
  function getApiKey(headers, headerName) {
25
56
  const values = headers[headerName];
26
57
  return values?.[0]?.trim() || undefined;
@@ -74,7 +105,7 @@ async function handleXdrSubmit(xdrStr, ctx, skipWait) {
74
105
  contractId,
75
106
  isLimited: contractId ? ctx.acquireOptions.limitedContracts.has(contractId) : false,
76
107
  };
77
- return (0, submit_1.submitWithFeeBumpAndWait)(ctx.fundRelayer, validated.toXDR(), ctx.network, maxFee, ctx.api, ctx.startTime, ctx.tracker, submitContext, skipWait);
108
+ return (0, submit_1.submitWithFeeBumpAndWait)(ctx.fundRelayer, validated.toXDR(), ctx.network, maxFee, ctx.api, ctx.startTime, ctx.tracker, submitContext, skipWait, ctx.config);
78
109
  }
79
110
  async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
80
111
  // Simulate once — used for both read-only detection and transaction assembly
@@ -90,10 +121,11 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
90
121
  };
91
122
  }
92
123
  let poolLock;
124
+ let needsCooldown = false;
93
125
  try {
94
126
  poolLock = await ctx.pool.acquire(ctx.acquireOptions);
95
127
  const channelRelayer = ctx.api.useRelayer(poolLock.relayerId);
96
- const channelInfo = await channelRelayer.getRelayer();
128
+ const channelInfo = await getCachedRelayerInfo(ctx.network, poolLock.relayerId, channelRelayer);
97
129
  console.log(`[channels] Acquired channel: ${poolLock.relayerId}`);
98
130
  if (!channelInfo || !channelInfo.address) {
99
131
  throw (0, relayer_sdk_1.pluginError)('Channel relayer not found', {
@@ -123,9 +155,8 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
123
155
  isLimited: contractId ? ctx.acquireOptions.limitedContracts.has(contractId) : false,
124
156
  };
125
157
  try {
126
- const result = await (0, submit_1.submitWithFeeBumpAndWait)(ctx.fundRelayer, signedTx.toXDR(), ctx.network, maxFee, ctx.api, ctx.startTime, ctx.tracker, submitContext, skipWait);
158
+ 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
159
  if (result.status === 'pending' || result.status === 'sent' || result.status === 'submitted') {
128
- // extend lock and clear sequence
129
160
  console.log(`[channels]: extending lock and clearing sequence`);
130
161
  await ctx.pool.extendLock(poolLock);
131
162
  await (0, sequence_1.clearSequence)(ctx.kv, ctx.network, channelInfo.address);
@@ -135,7 +166,9 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
135
166
  await (0, sequence_1.commitSequence)(ctx.kv, ctx.network, channelInfo.address, sequence);
136
167
  }
137
168
  else {
169
+ // Unknown status — uncertain outcome → release with cooldown
138
170
  await (0, sequence_1.clearSequence)(ctx.kv, ctx.network, channelInfo.address);
171
+ needsCooldown = true;
139
172
  }
140
173
  return result;
141
174
  }
@@ -146,12 +179,22 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
146
179
  await ctx.pool.extendLock(poolLock);
147
180
  poolLock = undefined; // skip release in finally
148
181
  }
182
+ else if (error.code === 'ONCHAIN_FAILED') {
183
+ // Sequence was consumed (tx landed on chain) — cooldown prevents
184
+ // next acquirer from hitting stale RPC and reusing old sequence
185
+ needsCooldown = true;
186
+ }
149
187
  throw error;
150
188
  }
151
189
  }
152
190
  finally {
153
191
  if (poolLock) {
154
- await ctx.pool.release(poolLock);
192
+ if (needsCooldown) {
193
+ await ctx.pool.releaseWithCooldown(poolLock);
194
+ }
195
+ else {
196
+ await ctx.pool.release(poolLock);
197
+ }
155
198
  }
156
199
  }
157
200
  }
@@ -212,7 +255,7 @@ async function channelAccounts(context) {
212
255
  hash: stellar.hash ?? null,
213
256
  };
214
257
  }
215
- const fundInfo = await fundRelayer.getRelayer();
258
+ const fundInfo = await getCachedRelayerInfo(config.network, fundRelayerId, fundRelayer);
216
259
  if (!fundInfo || !fundInfo.address) {
217
260
  throw (0, relayer_sdk_1.pluginError)('Fund relayer not found', {
218
261
  code: 'RELAYER_UNAVAILABLE',
@@ -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,8 @@
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
+ * - LRU ordering via per-channel keys: <network>:channel:lru:<relayerId>
8
9
  */
9
10
  import { PluginKVStore } from '@openzeppelin/relayer-sdk';
10
11
  export type PoolLock = {
@@ -18,21 +19,29 @@ export type AcquireOptions = {
18
19
  };
19
20
  export declare class ChannelPool {
20
21
  private readonly network;
21
- private readonly globalLockKey;
22
22
  private readonly channelLockTtlSec;
23
- private readonly mutexTtlSec;
24
23
  private readonly kv;
25
24
  constructor(network: 'testnet' | 'mainnet', kv: PluginKVStore, lockTtlSeconds: number);
26
25
  /** Acquire a relayerId with a token lock */
27
26
  acquire(options: AcquireOptions): Promise<PoolLock>;
28
- private withGlobalMutex;
29
- private tryLockAnyRelayer;
27
+ /** Try to claim one channel from a batch of candidates */
28
+ private tryClaimBatch;
29
+ /** Attempt to claim a single channel under its per-channel lock */
30
+ private tryClaimChannel;
30
31
  /** Extend the lock TTL if we still own it (e.g. after WAIT_TIMEOUT) */
31
32
  extendLock(lock: PoolLock, ttlSec?: number): Promise<void>;
32
33
  /** Release the lock if we own it */
33
34
  release(lock: PoolLock): Promise<void>;
35
+ /** Release with cooldown: keeps lock alive with short TTL to hard-block the channel. */
36
+ releaseWithCooldown(lock: PoolLock, cooldownMs?: 6000): Promise<void>;
34
37
  private membershipKey;
35
38
  private lockKey;
39
+ private claimKey;
40
+ private lruKey;
41
+ /** Read per-channel LRU timestamps in parallel (partial failures keep successful reads) */
42
+ private readLruMap;
43
+ /** Fire-and-forget LRU timestamp update for a single channel */
44
+ private updateLru;
36
45
  private getRelayerIdsFromKV;
37
46
  private getPoolCapacityDetails;
38
47
  }
@@ -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;;;;;;;;GAQG;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;IA8DzD,0DAA0D;YAC5C,aAAa;IAQ3B,mEAAmE;YACrD,eAAe;IAuB7B,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,OAAO;IAIf,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,MAAM;IAId,2FAA2F;YAC7E,UAAU;IASxB,gEAAgE;IAChE,OAAO,CAAC,SAAS;YAMH,mBAAmB;YAYnB,sBAAsB;CAUrC"}
@@ -5,7 +5,8 @@
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
+ * - LRU ordering via per-channel keys: <network>:channel:lru:<relayerId>
9
10
  */
10
11
  var __importDefault = (this && this.__importDefault) || function (mod) {
11
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -19,21 +20,52 @@ class ChannelPool {
19
20
  constructor(network, kv, lockTtlSeconds) {
20
21
  this.network = network;
21
22
  this.kv = kv;
22
- this.globalLockKey = `${this.network}:channel-pool-lock`;
23
23
  this.channelLockTtlSec = lockTtlSeconds;
24
- this.mutexTtlSec = constants_1.POOL.MUTEX_TTL_SECONDS;
25
24
  }
26
25
  /** Acquire a relayerId with a token lock */
27
26
  async acquire(options) {
28
- const maxSpins = constants_1.POOL.MUTEX_MAX_SPINS;
27
+ const maxSpins = constants_1.POOL.ACQUIRE_MAX_SPINS;
28
+ // --- Read state ONCE for the entire retry loop ---
29
+ let ids = await this.getRelayerIdsFromKV();
30
+ if (ids.length === 0) {
31
+ throw (0, relayer_sdk_1.pluginError)('No channel accounts configured. Use the management API to set channel accounts.', {
32
+ code: 'NO_CHANNELS_CONFIGURED',
33
+ status: constants_1.HTTP_STATUS.SERVICE_UNAVAILABLE,
34
+ });
35
+ }
36
+ if (options.contractId && options.limitedContracts.has(options.contractId)) {
37
+ ids = filterChannelsForLimitedContract(ids, options.capacityRatio);
38
+ }
39
+ // Shuffle all candidates; batch size scales so every spin covers an
40
+ // equal slice of the pool, guaranteeing full coverage across all spins.
41
+ const candidates = ids.slice();
42
+ shuffle(candidates);
43
+ // Read LRU for a small prefix only (capped at maxSpins * POOL.MAX_CLAIMS_PER_SPIN) to guide initial ordering.
44
+ // The rest stay in random shuffled order — no extra Redis reads.
45
+ const lruSampleSize = Math.min(candidates.length, maxSpins * constants_1.POOL.MAX_CLAIMS_PER_SPIN);
46
+ const lruMap = await this.readLruMap(candidates.slice(0, lruSampleSize));
47
+ const prefix = candidates.slice(0, lruSampleSize);
48
+ prefix.sort((a, b) => (lruMap[a] ?? 0) - (lruMap[b] ?? 0));
49
+ for (let j = 0; j < prefix.length; j++)
50
+ candidates[j] = prefix[j];
51
+ // Batch size: cover the full pool across maxSpins iterations
52
+ const batchSize = Math.max(constants_1.POOL.MAX_CLAIMS_PER_SPIN, Math.ceil(candidates.length / maxSpins));
53
+ let offset = 0;
29
54
  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;
55
+ if (offset >= candidates.length) {
56
+ // Full sweep done — reshuffle for next sweep
57
+ offset = 0;
58
+ shuffle(candidates);
35
59
  }
36
- return r;
60
+ const batch = candidates.slice(offset, offset + batchSize);
61
+ offset += batchSize;
62
+ const result = await this.tryClaimBatch(batch);
63
+ if (result)
64
+ return result;
65
+ // Exponential backoff with full jitter
66
+ const baseDelay = Math.min(constants_1.POOL.ACQUIRE_MAX_DELAY_MS, constants_1.POOL.ACQUIRE_BASE_DELAY_MS * Math.pow(2, i));
67
+ const jitter = Math.floor(Math.random() * baseDelay);
68
+ await sleep(jitter);
37
69
  }
38
70
  const diagnostics = await this.getPoolCapacityDetails(options, maxSpins);
39
71
  console.warn('[channels] Pool capacity exhausted', diagnostics);
@@ -43,35 +75,27 @@ class ChannelPool {
43
75
  details: diagnostics,
44
76
  });
45
77
  }
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) {
52
- let ids = await this.getRelayerIdsFromKV();
53
- if (ids.length === 0) {
54
- throw (0, relayer_sdk_1.pluginError)('No channel accounts configured. Use the management API to set channel accounts.', {
55
- code: 'NO_CHANNELS_CONFIGURED',
56
- status: constants_1.HTTP_STATUS.SERVICE_UNAVAILABLE,
57
- });
58
- }
59
- // If contract is limited, filter to allowed subset
60
- if (options.contractId && options.limitedContracts.has(options.contractId)) {
61
- ids = filterChannelsForLimitedContract(ids, options.capacityRatio);
78
+ /** Try to claim one channel from a batch of candidates */
79
+ async tryClaimBatch(batch) {
80
+ for (const candidate of batch) {
81
+ const result = await this.tryClaimChannel(candidate);
82
+ if (result)
83
+ return result;
62
84
  }
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;
85
+ return null;
86
+ }
87
+ /** Attempt to claim a single channel under its per-channel lock */
88
+ async tryClaimChannel(relayerId) {
89
+ return this.kv.withLock(this.claimKey(relayerId), async () => {
90
+ // Double-check: another worker may have claimed it between our scan and this lock
91
+ if (await this.kv.exists(this.lockKey(relayerId)))
92
+ return null;
69
93
  const token = randomToken();
70
- const entry = { token, lockedAt: new Date().toISOString() };
71
- await this.kv.set(key, entry, { ttlSec: this.channelLockTtlSec });
94
+ await this.kv.set(this.lockKey(relayerId), { token, lockedAt: new Date().toISOString() }, { ttlSec: this.channelLockTtlSec });
95
+ // Fire-and-forget: LRU is best-effort, don't hold the claim lock for it
96
+ this.updateLru(relayerId);
72
97
  return { relayerId, token };
73
- }
74
- return null;
98
+ }, { ttlSec: constants_1.POOL.CLAIM_LOCK_TTL_SECONDS, onBusy: 'skip' });
75
99
  }
76
100
  /** Extend the lock TTL if we still own it (e.g. after WAIT_TIMEOUT) */
77
101
  async extendLock(lock, ttlSec) {
@@ -99,12 +123,47 @@ class ChannelPool {
99
123
  // ignore release errors
100
124
  }
101
125
  }
126
+ /** Release with cooldown: keeps lock alive with short TTL to hard-block the channel. */
127
+ async releaseWithCooldown(lock, cooldownMs = constants_1.POOL.CHANNEL_COOLDOWN_MS) {
128
+ try {
129
+ const key = this.lockKey(lock.relayerId);
130
+ const current = await this.kv.get(key);
131
+ if (current?.token === lock.token) {
132
+ const cooldownSec = Math.max(1, Math.ceil(cooldownMs / 1000));
133
+ await this.kv.set(key, current, { ttlSec: cooldownSec });
134
+ }
135
+ }
136
+ catch {
137
+ // ignore
138
+ }
139
+ }
102
140
  membershipKey() {
103
141
  return `${this.network}:channel:relayer-ids`;
104
142
  }
105
143
  lockKey(relayerId) {
106
144
  return `${this.network}:channel:in-use:${relayerId}`;
107
145
  }
146
+ claimKey(relayerId) {
147
+ return `${this.network}:channel:claim:${relayerId}`;
148
+ }
149
+ lruKey(relayerId) {
150
+ return `${this.network}:channel:lru:${relayerId}`;
151
+ }
152
+ /** Read per-channel LRU timestamps in parallel (partial failures keep successful reads) */
153
+ async readLruMap(ids) {
154
+ const lruMap = {};
155
+ const results = await Promise.allSettled(ids.map((id) => this.kv.get(this.lruKey(id))));
156
+ results.forEach((r, i) => {
157
+ lruMap[ids[i]] = r.status === 'fulfilled' ? (r.value?.ts ?? 0) : 0;
158
+ });
159
+ return lruMap;
160
+ }
161
+ /** Fire-and-forget LRU timestamp update for a single channel */
162
+ updateLru(relayerId) {
163
+ this.kv.set(this.lruKey(relayerId), { ts: Date.now() }, { ttlSec: constants_1.POOL.LRU_KEY_TTL_SECONDS }).catch((err) => {
164
+ console.debug('[channels] failed to update LRU key', err);
165
+ });
166
+ }
108
167
  async getRelayerIdsFromKV() {
109
168
  try {
110
169
  const doc = await this.kv.get(this.membershipKey());
@@ -121,7 +180,7 @@ class ChannelPool {
121
180
  async getPoolCapacityDetails(options, maxSpins) {
122
181
  const isLimited = !!(options.contractId && options.limitedContracts.has(options.contractId));
123
182
  return {
124
- reason: isLimited ? 'limited_contract_capacity' : 'all_channels_busy_or_mutex_contention',
183
+ reason: isLimited ? 'limited_contract_capacity' : 'all_channels_busy_or_claim_contention',
125
184
  contractId: options.contractId,
126
185
  capacityRatio: options.capacityRatio,
127
186
  maxSpins,
@@ -169,8 +228,9 @@ function simpleHash(str) {
169
228
  */
170
229
  function filterChannelsForLimitedContract(ids, ratio) {
171
230
  const k = Math.max(1, Math.floor(ratio * ids.length));
231
+ const hashes = new Map(ids.map((id) => [id, simpleHash(id)]));
172
232
  return ids
173
233
  .slice()
174
- .sort((a, b) => simpleHash(a) - simpleHash(b) || a.localeCompare(b))
234
+ .sort((a, b) => hashes.get(a) - hashes.get(b) || a.localeCompare(b))
175
235
  .slice(0, k);
176
236
  }
@@ -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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openzeppelin/relayer-plugin-channels",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "OpenZeppelin Relayer Plugin for Stellar Channel Accounts",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",