@openzeppelin/relayer-plugin-channels 0.19.0 → 0.20.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,7 +16,12 @@ 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
+ - [Alternative Fund Relayers](#alternative-fund-relayers)
20
+ - [Per-Fund-Relayer Configuration](#per-fund-relayer-configuration)
21
+ - [Dynamic Inclusion Fees](#dynamic-inclusion-fees)
22
+ - [Timeout Overrides](#timeout-overrides)
23
+ - [Transaction Parameter Overrides](#transaction-parameter-overrides)
24
+ - [Configuration Precedence](#configuration-precedence)
20
25
  - [Contract Capacity Limits](#contract-capacity-limits)
21
26
  - [Management API](#management-api)
22
27
  - [List Channel Accounts](#list-channel-accounts)
@@ -224,7 +229,7 @@ export PLUGIN_ADMIN_SECRET="your-secret-here" # Required for management API
224
229
  # Optional environment variables
225
230
  # Comma-separated list of allowed alternative fund relayers (e.g., for x402 or similar flows)
226
231
  export ALLOWED_FUND_RELAYER_IDS="x402-channels-fund"
227
- export LOCK_TTL_SECONDS=10 # default: 30, min: 3, max: 30
232
+ export LOCK_TTL_SECONDS=10 # default: 30, min: 3, max: 60
228
233
 
229
234
  # Fee tracking (optional)
230
235
  export FEE_LIMIT=1000000 # Default max fee per API key in stroops (disabled if not set)
@@ -244,6 +249,13 @@ export SEQUENCE_NUMBER_CACHE_MAX_AGE_MS=120000 # Max age of cached sequence num
244
249
 
245
250
  # Auth expiry validation (optional)
246
251
  export MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER=2 # Minimum ledger margin for auth entry signatureExpirationLedger (default: 2)
252
+
253
+ # Transaction timebounds (optional)
254
+ export MAX_TIME_BOUND_OFFSET_SECONDS=60 # Max future offset for tx maxTime in seconds (default: 60)
255
+
256
+ # Request timeouts (optional)
257
+ export PLUGIN_GLOBAL_TIMEOUT_MS=30000 # Overall request timeout in ms (default: 30000)
258
+ export PLUGIN_POLLING_TIMEOUT_MS=25000 # Transaction polling timeout in ms (default: 25000)
247
259
  ```
248
260
 
249
261
  Your Relayer should now contain:
@@ -376,6 +388,93 @@ curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
376
388
  - `fundRelayerId` must be a non-empty string
377
389
  - `getTransaction` requests can also include `fundRelayerId`; polling should use the same relayer selection as submission
378
390
 
391
+ ## Per-Fund-Relayer Configuration
392
+
393
+ Individual fund relayers can have custom settings for fees, timeouts, and transaction parameters. This is configured via the plugin's `config` field in the relayer's `config.json`, not via environment variables.
394
+
395
+ Environment variables define **global defaults**. Per-fund-relayer settings **override** them for requests that use that fund relayer. Requests using fund relayers without overrides (or the default fund relayer) use the global env-var defaults unchanged.
396
+
397
+ ### Setup
398
+
399
+ Add a `config` block to the plugin definition in your relayer's `config.json`:
400
+
401
+ ```json
402
+ {
403
+ "plugins": [
404
+ {
405
+ "id": "channels",
406
+ "path": "channels/index.ts",
407
+ "config": {
408
+ "fundRelayers": {
409
+ "x402-fund": {
410
+ "dynamicFee": {
411
+ "enabled": true,
412
+ "percentile": "p50",
413
+ "cacheTtlMs": 10000
414
+ },
415
+ "timeouts": {
416
+ "globalTimeoutMs": 45000,
417
+ "pollingTimeoutMs": 40000
418
+ },
419
+ "transactionParams": {
420
+ "maxTimeBoundOffsetSeconds": 120,
421
+ "minSignatureExpirationLedgerBuffer": 5
422
+ }
423
+ }
424
+ }
425
+ }
426
+ }
427
+ ]
428
+ }
429
+ ```
430
+
431
+ All sections (`dynamicFee`, `timeouts`, `transactionParams`) are optional. You can configure any combination.
432
+
433
+ ### Dynamic Inclusion Fees
434
+
435
+ When enabled, the plugin fetches real-time fee data from Soroban RPC `getFeeStats` and uses a percentile of `sorobanInclusionFee` as the inclusion fee, instead of the static 201/203 stroops.
436
+
437
+ | Field | Type | Default | Description |
438
+ | ------------ | ------- | ------- | ----------------------------------------------------------------------------------------------------------- |
439
+ | `enabled` | boolean | — | Must be `true` to activate dynamic fees |
440
+ | `percentile` | string | `"p50"` | Which percentile to use. Valid: `p10`, `p20`, `p30`, `p40`, `p50`, `p60`, `p70`, `p80`, `p90`, `p95`, `p99` |
441
+ | `cacheTtlMs` | number | `10000` | How long to cache the fee stats result (ms). Shared across all workers via KV store |
442
+
443
+ When dynamic fees are active, the same fee is used for both limited and non-limited contracts (the limited/non-limited distinction is static tuning that dynamic fees replace).
444
+
445
+ On any RPC or cache failure, the plugin falls back to the global static fees (`INCLUSION_FEE_DEFAULT` / `INCLUSION_FEE_LIMITED`), preserving the limited/non-limited split.
446
+
447
+ ### Timeout Overrides
448
+
449
+ Override request timeouts for specific fund relayers. Useful when certain traffic sources (e.g., x402) need longer processing windows.
450
+
451
+ | Field | Type | Default | Description |
452
+ | ------------------ | ------ | --------------------------------------- | ---------------------------------------------- |
453
+ | `globalTimeoutMs` | number | env `PLUGIN_GLOBAL_TIMEOUT_MS` (30000) | Overall request timeout in ms |
454
+ | `pollingTimeoutMs` | number | env `PLUGIN_POLLING_TIMEOUT_MS` (25000) | Transaction confirmation polling timeout in ms |
455
+
456
+ ### Transaction Parameter Overrides
457
+
458
+ Override transaction construction and validation parameters per fund relayer.
459
+
460
+ | Field | Type | Default | Description |
461
+ | ------------------------------------ | ------ | ------------------------------------------------ | --------------------------------------------------------------------------------------- |
462
+ | `maxTimeBoundOffsetSeconds` | number | env `MAX_TIME_BOUND_OFFSET_SECONDS` (60) | Max future offset for tx `maxTime`. Also controls validation of incoming XDR timebounds |
463
+ | `minSignatureExpirationLedgerBuffer` | number | env `MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER` (2) | Minimum ledger margin required for auth entry `signatureExpirationLedger` |
464
+
465
+ ### Configuration Precedence
466
+
467
+ Settings are resolved per request in this order (first match wins):
468
+
469
+ 1. **Per-fund-relayer override** — from `config.fundRelayers[fundRelayerId]` in `config.json`
470
+ 2. **Global env var** — from the corresponding environment variable
471
+ 3. **Built-in default** — hardcoded constant in the plugin
472
+
473
+ Example: a request with `fundRelayerId: "x402-fund"` where the plugin config has `timeouts.globalTimeoutMs: 45000` and the env var `PLUGIN_GLOBAL_TIMEOUT_MS=30000`:
474
+
475
+ - The x402-fund request uses 45000ms
476
+ - All other requests use 30000ms (env var default)
477
+
379
478
  ## Contract Capacity Limits
380
479
 
381
480
  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.
@@ -19,6 +19,7 @@ export interface ChannelAccountsConfig {
19
19
  inclusionFeeLimited: number;
20
20
  sequenceNumberCacheMaxAgeMs: number;
21
21
  minSignatureExpirationLedgerBuffer: number;
22
+ maxTimeBoundOffsetSeconds: number;
22
23
  globalTimeoutMs: number;
23
24
  pollingTimeoutMs: number;
24
25
  }
@@ -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;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"}
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,yBAAyB,EAAE,MAAM,CAAC;IAClC,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AA2ID;;GAEG;AACH,wBAAgB,UAAU,IAAI,qBAAqB,CA8BlD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAE3E"}
@@ -11,8 +11,8 @@ 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
13
  // Default inclusion fees (matching launchtube)
14
- const DEFAULT_INCLUSION_FEE_DEFAULT = Number(stellar_sdk_1.BASE_FEE) * 2 + 3; // 203
15
- const DEFAULT_INCLUSION_FEE_LIMITED = Number(stellar_sdk_1.BASE_FEE) * 2 + 1; // 201
14
+ const DEFAULT_INCLUSION_FEE_DEFAULT = Number(stellar_sdk_1.BASE_FEE) * 2 + constants_1.DYNAMIC_FEE.FEE_BUMP_MARGIN_DEFAULT; // 203
15
+ const DEFAULT_INCLUSION_FEE_LIMITED = Number(stellar_sdk_1.BASE_FEE) * 2 + constants_1.DYNAMIC_FEE.FEE_BUMP_MARGIN_LIMITED; // 201
16
16
  function requireEnv(name) {
17
17
  const v = process.env[name];
18
18
  if (!v || v.trim() === '') {
@@ -131,6 +131,13 @@ function parsePollingTimeoutMs() {
131
131
  const n = Number(raw);
132
132
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : constants_1.POLLING.DEFAULT_TIMEOUT_MS;
133
133
  }
134
+ function parseMaxTimeBoundOffsetSeconds() {
135
+ const raw = process.env.MAX_TIME_BOUND_OFFSET_SECONDS;
136
+ if (!raw)
137
+ return constants_1.TIME.MAX_TIME_BOUND_OFFSET_SECONDS;
138
+ const n = Number(raw);
139
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : constants_1.TIME.MAX_TIME_BOUND_OFFSET_SECONDS;
140
+ }
134
141
  function parseContractCapacityRatio() {
135
142
  const raw = process.env.CONTRACT_CAPACITY_RATIO;
136
143
  if (!raw)
@@ -168,6 +175,7 @@ function loadConfig() {
168
175
  inclusionFeeLimited: parseInclusionFee('INCLUSION_FEE_LIMITED', DEFAULT_INCLUSION_FEE_LIMITED),
169
176
  sequenceNumberCacheMaxAgeMs: parseSequenceNumberCacheMaxAge(),
170
177
  minSignatureExpirationLedgerBuffer: parseMinAuthExpiryLedgerBuffer(),
178
+ maxTimeBoundOffsetSeconds: parseMaxTimeBoundOffsetSeconds(),
171
179
  globalTimeoutMs: parseGlobalTimeoutMs(),
172
180
  pollingTimeoutMs: parsePollingTimeoutMs(),
173
181
  };
@@ -17,7 +17,7 @@ export declare const HTTP_STATUS: {
17
17
  export declare const CONFIG: {
18
18
  readonly DEFAULT_LOCK_TTL_SECONDS: 30;
19
19
  readonly MIN_LOCK_TTL_SECONDS: 3;
20
- readonly MAX_LOCK_TTL_SECONDS: 30;
20
+ readonly MAX_LOCK_TTL_SECONDS: 60;
21
21
  readonly DEFAULT_CONTRACT_CAPACITY_RATIO: 0.8;
22
22
  readonly DEFAULT_SEQUENCE_NUMBER_CACHE_MAX_AGE_MS: 120000;
23
23
  readonly DEFAULT_MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER: 2;
@@ -33,13 +33,11 @@ export declare const POOL: {
33
33
  };
34
34
  export declare const RELAYER_INFO_CACHE_TTL_SECONDS = 1800;
35
35
  export declare const TIME: {
36
+ readonly MIN_TIME_BOUND: 0;
36
37
  readonly MAX_TIME_BOUND_OFFSET_SECONDS: 60;
37
38
  };
38
39
  export declare const SIMULATION: {
39
40
  readonly DEFAULT_FEE: "100";
40
- readonly MIN_TIME_BOUND: 0;
41
- readonly MAX_TIME_BOUND_OFFSET_SECONDS: 60;
42
- readonly MAX_FUTURE_TIME_BOUND_SECONDS: 60;
43
41
  readonly SIMULATION_AUTH_MODE: "enforce";
44
42
  /** Minimum ledger margin required between latestLedger and auth signatureExpirationLedger. Must be > 1 since simulation already validates 1 ledger of validity. ~10s at ~5s/ledger. */
45
43
  readonly MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER: 2;
@@ -55,4 +53,11 @@ export declare const POLLING: {
55
53
  export declare const FEE: {
56
54
  readonly NON_SOROBAN_FEE: 100000;
57
55
  };
56
+ export declare const DYNAMIC_FEE: {
57
+ readonly DEFAULT_PERCENTILE: "p50";
58
+ readonly DEFAULT_CACHE_TTL_MS: 10000;
59
+ readonly VALID_PERCENTILES: readonly ["p10", "p20", "p30", "p40", "p50", "p60", "p70", "p80", "p90", "p95", "p99"];
60
+ readonly FEE_BUMP_MARGIN_DEFAULT: 3;
61
+ readonly FEE_BUMP_MARGIN_LIMITED: 1;
62
+ };
58
63
  //# sourceMappingURL=constants.d.ts.map
@@ -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;;;;;;;;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"}
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;;;CAGP,CAAC;AAGX,eAAO,MAAM,UAAU;;;IAGrB,uLAAuL;;CAE/K,CAAC;AAGX,eAAO,MAAM,OAAO;;;CAGV,CAAC;AAGX,eAAO,MAAM,OAAO;;;CAGV,CAAC;AAEX,eAAO,MAAM,GAAG;;CAGN,CAAC;AAGX,eAAO,MAAM,WAAW;;;;;;CAQd,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.RELAYER_INFO_CACHE_TTL_SECONDS = exports.POOL = exports.CONFIG = exports.HTTP_STATUS = void 0;
8
+ exports.DYNAMIC_FEE = 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,
@@ -22,7 +22,7 @@ exports.HTTP_STATUS = {
22
22
  exports.CONFIG = {
23
23
  DEFAULT_LOCK_TTL_SECONDS: 30,
24
24
  MIN_LOCK_TTL_SECONDS: 3,
25
- MAX_LOCK_TTL_SECONDS: 30,
25
+ MAX_LOCK_TTL_SECONDS: 60,
26
26
  DEFAULT_CONTRACT_CAPACITY_RATIO: 0.8,
27
27
  DEFAULT_SEQUENCE_NUMBER_CACHE_MAX_AGE_MS: 120000,
28
28
  DEFAULT_MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER: 2,
@@ -45,16 +45,14 @@ exports.POOL = {
45
45
  };
46
46
  // Relayer info cache — address and network_type are effectively immutable
47
47
  exports.RELAYER_INFO_CACHE_TTL_SECONDS = 1800; // 30 minutes
48
- // Time Constants
48
+ // Time constants — used for both simulation tx construction and incoming tx validation
49
49
  exports.TIME = {
50
+ MIN_TIME_BOUND: 0,
50
51
  MAX_TIME_BOUND_OFFSET_SECONDS: 60,
51
52
  };
52
53
  // Simulation-related defaults
53
54
  exports.SIMULATION = {
54
55
  DEFAULT_FEE: '100',
55
- MIN_TIME_BOUND: 0,
56
- MAX_TIME_BOUND_OFFSET_SECONDS: 60,
57
- MAX_FUTURE_TIME_BOUND_SECONDS: 60,
58
56
  SIMULATION_AUTH_MODE: 'enforce',
59
57
  /** Minimum ledger margin required between latestLedger and auth signatureExpirationLedger. Must be > 1 since simulation already validates 1 ledger of validity. ~10s at ~5s/ledger. */
60
58
  MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER: 2,
@@ -73,3 +71,13 @@ exports.FEE = {
73
71
  // For non-Soroban txs: 100,000 stroops (0.01 XLM) per Stellar best practice
74
72
  NON_SOROBAN_FEE: 100000,
75
73
  };
74
+ // Dynamic fee estimation defaults
75
+ exports.DYNAMIC_FEE = {
76
+ DEFAULT_PERCENTILE: 'p50',
77
+ DEFAULT_CACHE_TTL_MS: 10000,
78
+ VALID_PERCENTILES: ['p10', 'p20', 'p30', 'p40', 'p50', 'p60', 'p70', 'p80', 'p90', 'p95', 'p99'],
79
+ // Margins above the fee-bump minimum (BASE_FEE * 2) to avoid landing exactly
80
+ // at the protocol floor. Used by both static defaults and dynamic fee computation.
81
+ FEE_BUMP_MARGIN_DEFAULT: 3,
82
+ FEE_BUMP_MARGIN_LIMITED: 1,
83
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * fee-stats.ts
3
+ *
4
+ * Fetches dynamic inclusion fees from Soroban RPC `getFeeStats` with KV-backed
5
+ * caching so all worker processes share the same cached value.
6
+ * On any error, returns `null` so the caller can fall back to static fees.
7
+ */
8
+ import type { PluginKVStore, Relayer } from '@openzeppelin/relayer-sdk';
9
+ /**
10
+ * Fetch the dynamic inclusion fee for the given percentile from Soroban RPC.
11
+ *
12
+ * Returns a KV-cached value when fresh; otherwise calls `getFeeStats` via
13
+ * `relayer.rpc()` and stores the result in KV with a TTL.
14
+ * Returns `null` on any failure so the caller can apply its own fallback logic.
15
+ */
16
+ export declare function fetchDynamicInclusionFee(relayer: Relayer, kv: PluginKVStore, network: string, percentile: string, cacheTtlMs: number): Promise<number | null>;
17
+ //# sourceMappingURL=fee-stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fee-stats.d.ts","sourceRoot":"","sources":["../../src/plugin/fee-stats.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAWxE;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,aAAa,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoDxB"}
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ /**
3
+ * fee-stats.ts
4
+ *
5
+ * Fetches dynamic inclusion fees from Soroban RPC `getFeeStats` with KV-backed
6
+ * caching so all worker processes share the same cached value.
7
+ * On any error, returns `null` so the caller can fall back to static fees.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.fetchDynamicInclusionFee = fetchDynamicInclusionFee;
11
+ function cacheKey(network, percentile) {
12
+ return `${network}:fee-stats:${percentile}`;
13
+ }
14
+ /**
15
+ * Fetch the dynamic inclusion fee for the given percentile from Soroban RPC.
16
+ *
17
+ * Returns a KV-cached value when fresh; otherwise calls `getFeeStats` via
18
+ * `relayer.rpc()` and stores the result in KV with a TTL.
19
+ * Returns `null` on any failure so the caller can apply its own fallback logic.
20
+ */
21
+ async function fetchDynamicInclusionFee(relayer, kv, network, percentile, cacheTtlMs) {
22
+ const key = cacheKey(network, percentile);
23
+ const cacheTtlSec = Math.max(1, Math.ceil(cacheTtlMs / 1000));
24
+ try {
25
+ const cached = await kv.get(key);
26
+ if (cached && Date.now() - cached.storedAt < cacheTtlMs) {
27
+ return cached.fee;
28
+ }
29
+ }
30
+ catch {
31
+ // KV read failure — proceed to RPC fetch
32
+ }
33
+ try {
34
+ const rpcResponse = await relayer.rpc({
35
+ jsonrpc: '2.0',
36
+ id: Math.floor(Math.random() * 1e8).toString(),
37
+ method: 'getFeeStats',
38
+ params: {},
39
+ });
40
+ if (rpcResponse.error) {
41
+ console.warn('[channels] getFeeStats RPC error, falling back to static fee');
42
+ return null;
43
+ }
44
+ const result = rpcResponse.result;
45
+ const feeStr = result?.sorobanInclusionFee?.[percentile];
46
+ if (!feeStr) {
47
+ console.warn(`[channels] getFeeStats missing ${percentile}, falling back to static fee`);
48
+ return null;
49
+ }
50
+ const fee = Number(feeStr);
51
+ if (!Number.isFinite(fee) || fee < 0) {
52
+ console.warn(`[channels] getFeeStats invalid value for ${percentile}: ${feeStr}`);
53
+ return null;
54
+ }
55
+ try {
56
+ await kv.set(key, { fee, storedAt: Date.now() }, { ttlSec: cacheTtlSec });
57
+ }
58
+ catch {
59
+ // KV write failure is non-fatal — the value was already computed
60
+ }
61
+ console.debug(`[channels] Dynamic inclusion fee: ${fee} stroops (${percentile})`);
62
+ return fee;
63
+ }
64
+ catch (err) {
65
+ console.warn(`[channels] getFeeStats failed: ${err instanceof Error ? err.message : String(err)}`);
66
+ return null;
67
+ }
68
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * fund-relayer-config.ts
3
+ *
4
+ * Parse and validate per-fund-relayer overrides from the plugin config
5
+ * (`context.config` in PluginContext, sourced from config.json `plugins[].config`).
6
+ *
7
+ * When no per-fund config is present, all behaviour falls back to the global
8
+ * env-var-based defaults in ChannelAccountsConfig.
9
+ */
10
+ import type { PluginKVStore, Relayer } from '@openzeppelin/relayer-sdk';
11
+ import type { ChannelAccountsConfig } from './config';
12
+ import type { InclusionFees } from './fee';
13
+ import { DYNAMIC_FEE } from './constants';
14
+ export type FeePercentile = (typeof DYNAMIC_FEE.VALID_PERCENTILES)[number];
15
+ export interface DynamicFeeConfig {
16
+ enabled: boolean;
17
+ percentile: FeePercentile;
18
+ cacheTtlMs: number;
19
+ }
20
+ export interface FundRelayerTimeouts {
21
+ globalTimeoutMs?: number;
22
+ pollingTimeoutMs?: number;
23
+ }
24
+ export interface FundRelayerTransactionParams {
25
+ maxTimeBoundOffsetSeconds?: number;
26
+ minSignatureExpirationLedgerBuffer?: number;
27
+ }
28
+ export interface FundRelayerOverrides {
29
+ dynamicFee?: DynamicFeeConfig;
30
+ timeouts?: FundRelayerTimeouts;
31
+ transactionParams?: FundRelayerTransactionParams;
32
+ }
33
+ /**
34
+ * Extract and validate per-fund-relayer overrides from plugin config.
35
+ * Returns `undefined` when no overrides exist for the given fund relayer,
36
+ * which signals the caller to use global env-var defaults.
37
+ */
38
+ export declare function parseFundRelayerOverrides(pluginConfig: Record<string, any> | undefined, fundRelayerId: string): FundRelayerOverrides | undefined;
39
+ /**
40
+ * Resolve the inclusion fees for this request.
41
+ *
42
+ * When the fund relayer has `dynamicFee.enabled`, a single dynamic fee is
43
+ * used for both limited and non-limited contracts (the limited/non-limited
44
+ * distinction is static tuning that dynamic fees replace).
45
+ *
46
+ * On any dynamic-fee error the global static defaults are returned.
47
+ */
48
+ export declare function resolveInclusionFees(overrides: FundRelayerOverrides | undefined, globalConfig: ChannelAccountsConfig, relayer: Relayer, kv: PluginKVStore): Promise<InclusionFees>;
49
+ /**
50
+ * Resolve effective transaction params (timebounds, auth expiry) by merging
51
+ * per-fund overrides with global defaults.
52
+ */
53
+ export declare function resolveTransactionParams(overrides: FundRelayerOverrides | undefined, globalConfig: ChannelAccountsConfig): {
54
+ maxTimeBoundOffsetSeconds: number;
55
+ minSignatureExpirationLedgerBuffer: number;
56
+ };
57
+ /**
58
+ * Resolve effective timeouts by merging per-fund overrides with global defaults.
59
+ */
60
+ export declare function resolveTimeouts(overrides: FundRelayerOverrides | undefined, globalConfig: ChannelAccountsConfig): {
61
+ globalTimeoutMs: number;
62
+ pollingTimeoutMs: number;
63
+ };
64
+ //# sourceMappingURL=fund-relayer-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fund-relayer-config.d.ts","sourceRoot":"","sources":["../../src/plugin/fund-relayer-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAExE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAM1C,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,WAAW,CAAC,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3E,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,aAAa,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,4BAA4B;IAC3C,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,kCAAkC,CAAC,EAAE,MAAM,CAAC;CAC7C;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;CAClD;AAuDD;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,EAC7C,aAAa,EAAE,MAAM,GACpB,oBAAoB,GAAG,SAAS,CAelC;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,oBAAoB,GAAG,SAAS,EAC3C,YAAY,EAAE,qBAAqB,EACnC,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,aAAa,GAChB,OAAO,CAAC,aAAa,CAAC,CAuBxB;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,oBAAoB,GAAG,SAAS,EAC3C,YAAY,EAAE,qBAAqB,GAClC;IAAE,yBAAyB,EAAE,MAAM,CAAC;IAAC,kCAAkC,EAAE,MAAM,CAAA;CAAE,CAQnF;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,oBAAoB,GAAG,SAAS,EAC3C,YAAY,EAAE,qBAAqB,GAClC;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAKvD"}
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ /**
3
+ * fund-relayer-config.ts
4
+ *
5
+ * Parse and validate per-fund-relayer overrides from the plugin config
6
+ * (`context.config` in PluginContext, sourced from config.json `plugins[].config`).
7
+ *
8
+ * When no per-fund config is present, all behaviour falls back to the global
9
+ * env-var-based defaults in ChannelAccountsConfig.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.parseFundRelayerOverrides = parseFundRelayerOverrides;
13
+ exports.resolveInclusionFees = resolveInclusionFees;
14
+ exports.resolveTransactionParams = resolveTransactionParams;
15
+ exports.resolveTimeouts = resolveTimeouts;
16
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
17
+ const fee_stats_1 = require("./fee-stats");
18
+ const constants_1 = require("./constants");
19
+ // ---------------------------------------------------------------------------
20
+ // Parsing helpers
21
+ // ---------------------------------------------------------------------------
22
+ const validPercentiles = new Set(constants_1.DYNAMIC_FEE.VALID_PERCENTILES);
23
+ function isValidPercentile(v) {
24
+ return typeof v === 'string' && validPercentiles.has(v);
25
+ }
26
+ function parsePositiveNumber(v) {
27
+ if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0)
28
+ return undefined;
29
+ return v;
30
+ }
31
+ function parseDynamicFee(raw) {
32
+ if (!raw || typeof raw !== 'object')
33
+ return undefined;
34
+ const obj = raw;
35
+ if (typeof obj.enabled !== 'boolean' || !obj.enabled)
36
+ return undefined;
37
+ const percentile = isValidPercentile(obj.percentile) ? obj.percentile : constants_1.DYNAMIC_FEE.DEFAULT_PERCENTILE;
38
+ const cacheTtlMs = parsePositiveNumber(obj.cacheTtlMs) ?? constants_1.DYNAMIC_FEE.DEFAULT_CACHE_TTL_MS;
39
+ return { enabled: true, percentile, cacheTtlMs };
40
+ }
41
+ function parseTransactionParams(raw) {
42
+ if (!raw || typeof raw !== 'object')
43
+ return undefined;
44
+ const obj = raw;
45
+ const maxTimeBoundOffsetSeconds = parsePositiveNumber(obj.maxTimeBoundOffsetSeconds);
46
+ const minSignatureExpirationLedgerBuffer = parsePositiveNumber(obj.minSignatureExpirationLedgerBuffer);
47
+ if (maxTimeBoundOffsetSeconds === undefined && minSignatureExpirationLedgerBuffer === undefined)
48
+ return undefined;
49
+ return { maxTimeBoundOffsetSeconds, minSignatureExpirationLedgerBuffer };
50
+ }
51
+ function parseTimeouts(raw) {
52
+ if (!raw || typeof raw !== 'object')
53
+ return undefined;
54
+ const obj = raw;
55
+ const globalTimeoutMs = parsePositiveNumber(obj.globalTimeoutMs);
56
+ const pollingTimeoutMs = parsePositiveNumber(obj.pollingTimeoutMs);
57
+ if (globalTimeoutMs === undefined && pollingTimeoutMs === undefined)
58
+ return undefined;
59
+ return { globalTimeoutMs, pollingTimeoutMs };
60
+ }
61
+ // ---------------------------------------------------------------------------
62
+ // Public API
63
+ // ---------------------------------------------------------------------------
64
+ /**
65
+ * Extract and validate per-fund-relayer overrides from plugin config.
66
+ * Returns `undefined` when no overrides exist for the given fund relayer,
67
+ * which signals the caller to use global env-var defaults.
68
+ */
69
+ function parseFundRelayerOverrides(pluginConfig, fundRelayerId) {
70
+ if (!pluginConfig)
71
+ return undefined;
72
+ const fundRelayers = pluginConfig.fundRelayers;
73
+ if (!fundRelayers || typeof fundRelayers !== 'object')
74
+ return undefined;
75
+ const raw = fundRelayers[fundRelayerId];
76
+ if (!raw || typeof raw !== 'object')
77
+ return undefined;
78
+ const dynamicFee = parseDynamicFee(raw.dynamicFee);
79
+ const timeouts = parseTimeouts(raw.timeouts);
80
+ const transactionParams = parseTransactionParams(raw.transactionParams);
81
+ if (!dynamicFee && !timeouts && !transactionParams)
82
+ return undefined;
83
+ return { dynamicFee, timeouts, transactionParams };
84
+ }
85
+ /**
86
+ * Resolve the inclusion fees for this request.
87
+ *
88
+ * When the fund relayer has `dynamicFee.enabled`, a single dynamic fee is
89
+ * used for both limited and non-limited contracts (the limited/non-limited
90
+ * distinction is static tuning that dynamic fees replace).
91
+ *
92
+ * On any dynamic-fee error the global static defaults are returned.
93
+ */
94
+ async function resolveInclusionFees(overrides, globalConfig, relayer, kv) {
95
+ if (overrides?.dynamicFee?.enabled) {
96
+ const fee = await (0, fee_stats_1.fetchDynamicInclusionFee)(relayer, kv, globalConfig.network, overrides.dynamicFee.percentile, overrides.dynamicFee.cacheTtlMs);
97
+ if (fee !== null) {
98
+ // getFeeStats reports inner-tx inclusion fees. Fee-bump wrapping adds one
99
+ // extra virtual operation at BASE_FEE, plus a small margin to avoid
100
+ // landing exactly at the protocol minimum.
101
+ const feeBumpFee = fee + Number(stellar_sdk_1.BASE_FEE) + constants_1.DYNAMIC_FEE.FEE_BUMP_MARGIN_DEFAULT;
102
+ return { inclusionFeeDefault: feeBumpFee, inclusionFeeLimited: feeBumpFee };
103
+ }
104
+ // Dynamic fee fetch failed — fall through to static defaults
105
+ }
106
+ return {
107
+ inclusionFeeDefault: globalConfig.inclusionFeeDefault,
108
+ inclusionFeeLimited: globalConfig.inclusionFeeLimited,
109
+ };
110
+ }
111
+ /**
112
+ * Resolve effective transaction params (timebounds, auth expiry) by merging
113
+ * per-fund overrides with global defaults.
114
+ */
115
+ function resolveTransactionParams(overrides, globalConfig) {
116
+ return {
117
+ maxTimeBoundOffsetSeconds: overrides?.transactionParams?.maxTimeBoundOffsetSeconds ?? globalConfig.maxTimeBoundOffsetSeconds,
118
+ minSignatureExpirationLedgerBuffer: overrides?.transactionParams?.minSignatureExpirationLedgerBuffer ??
119
+ globalConfig.minSignatureExpirationLedgerBuffer,
120
+ };
121
+ }
122
+ /**
123
+ * Resolve effective timeouts by merging per-fund overrides with global defaults.
124
+ */
125
+ function resolveTimeouts(overrides, globalConfig) {
126
+ return {
127
+ globalTimeoutMs: overrides?.timeouts?.globalTimeoutMs ?? globalConfig.globalTimeoutMs,
128
+ pollingTimeoutMs: overrides?.timeouts?.pollingTimeoutMs ?? globalConfig.pollingTimeoutMs,
129
+ };
130
+ }
@@ -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;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"}
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;AAaxD,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;AAiCD;;;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;AA6UD;;GAEG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAElE"}
@@ -20,6 +20,7 @@ const constants_1 = require("./constants");
20
20
  const stellar_sdk_1 = require("@stellar/stellar-sdk");
21
21
  const simulation_1 = require("./simulation");
22
22
  const fee_1 = require("./fee");
23
+ const fund_relayer_config_1 = require("./fund-relayer-config");
23
24
  const tx_1 = require("./tx");
24
25
  const fee_tracking_1 = require("./fee-tracking");
25
26
  const sequence_1 = require("./sequence");
@@ -56,6 +57,16 @@ function getApiKey(headers, headerName) {
56
57
  const values = headers[headerName];
57
58
  return values?.[0]?.trim() || undefined;
58
59
  }
60
+ /**
61
+ * Compute how long a channel lock should be extended based on remaining tx validity.
62
+ * The tx maxTime was set to ~(txBuildTime + maxTimeBoundOffsetSeconds) at build time.
63
+ * We add one ledger cooldown as margin for the tx to finalize after expiry.
64
+ */
65
+ function computeExtendTtlSec(txBuildTime, maxTimeBoundOffsetSeconds) {
66
+ const txExpiryMs = txBuildTime + maxTimeBoundOffsetSeconds * 1000;
67
+ const remainingMs = txExpiryMs - Date.now() + constants_1.POOL.CHANNEL_COOLDOWN_MS;
68
+ return Math.max(1, Math.ceil(remainingMs / 1000));
69
+ }
59
70
  /**
60
71
  * Extracts func and auth from an unsigned Soroban transaction.
61
72
  * Returns null if the transaction is not a single invokeHostFunction operation.
@@ -97,7 +108,7 @@ async function handleXdrSubmit(xdrStr, ctx, skipWait) {
97
108
  const updatedOptions = { ...ctx.acquireOptions, contractId };
98
109
  return handleFuncAuthSubmit(extracted.func, extracted.auth, { ...ctx, acquireOptions: updatedOptions }, skipWait);
99
110
  }
100
- const validated = (0, tx_1.validateExistingTransactionForSubmitOnly)(tx);
111
+ const validated = (0, tx_1.validateExistingTransactionForSubmitOnly)(tx, ctx.config);
101
112
  const maxFee = (0, fee_1.calculateMaxFee)(validated, ctx.acquireOptions.limitedContracts, ctx.fees);
102
113
  const contractId = (0, fee_1.getContractIdFromTransaction)(validated);
103
114
  await ctx.tracker?.checkBudget(maxFee);
@@ -109,7 +120,7 @@ async function handleXdrSubmit(xdrStr, ctx, skipWait) {
109
120
  }
110
121
  async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
111
122
  // Simulate once — used for both read-only detection and transaction assembly
112
- const simulation = await (0, simulation_1.simulateTransaction)(func, auth, ctx.fundAddress, ctx.fundRelayer, ctx.networkPassphrase);
123
+ const simulation = await (0, simulation_1.simulateTransaction)(func, auth, ctx.fundAddress, ctx.fundRelayer, ctx.networkPassphrase, ctx.config.maxTimeBoundOffsetSeconds);
113
124
  if (simulation.isReadOnly) {
114
125
  console.log(`[channels] Read-only call detected, returning simulation result`);
115
126
  return {
@@ -143,7 +154,8 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
143
154
  }
144
155
  const sequence = await (0, sequence_1.getSequence)(ctx.kv, ctx.network, channelRelayer, channelInfo.address, ctx.config.sequenceNumberCacheMaxAgeMs);
145
156
  // Assemble the transaction using the cached simulation result — no second RPC call
146
- const built = (0, simulation_1.buildWithChannel)(func, auth, { address: channelInfo.address, sequence }, ctx.networkPassphrase, simulation.rawSimResult, ctx.config.minSignatureExpirationLedgerBuffer);
157
+ const txBuildTime = Date.now();
158
+ const built = (0, simulation_1.buildWithChannel)(func, auth, { address: channelInfo.address, sequence }, ctx.networkPassphrase, simulation.rawSimResult, ctx.config.minSignatureExpirationLedgerBuffer, ctx.config.maxTimeBoundOffsetSeconds);
147
159
  console.debug(`[channels] After assembly: built.fee=${built.fee}, minResourceFee=${simulation.rawSimResult.minResourceFee}`);
148
160
  const signedTx = await (0, submit_1.signWithChannelAndFund)(built, channelRelayer, ctx.fundRelayer, channelInfo.address, ctx.fundAddress, ctx.networkPassphrase);
149
161
  const maxFee = (0, fee_1.calculateMaxFee)(signedTx, ctx.acquireOptions.limitedContracts, ctx.fees);
@@ -157,8 +169,9 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
157
169
  try {
158
170
  const result = await (0, submit_1.submitWithFeeBumpAndWait)(ctx.fundRelayer, signedTx.toXDR(), ctx.network, maxFee, ctx.api, ctx.startTime, ctx.tracker, submitContext, skipWait, ctx.config);
159
171
  if (result.status === 'pending' || result.status === 'sent' || result.status === 'submitted') {
160
- console.log(`[channels]: extending lock and clearing sequence`);
161
- await ctx.pool.extendLock(poolLock);
172
+ const extendSec = computeExtendTtlSec(txBuildTime, ctx.config.maxTimeBoundOffsetSeconds);
173
+ console.log(`[channels]: extending lock (${extendSec}s) and clearing sequence`);
174
+ await ctx.pool.extendLock(poolLock, extendSec);
162
175
  await (0, sequence_1.clearSequence)(ctx.kv, ctx.network, channelInfo.address);
163
176
  poolLock = undefined; // skip release in finally
164
177
  }
@@ -175,8 +188,9 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
175
188
  catch (error) {
176
189
  await (0, sequence_1.clearSequence)(ctx.kv, ctx.network, channelInfo.address);
177
190
  if (error.code === 'WAIT_TIMEOUT' && poolLock) {
178
- console.log(`[channels] Extending lock for WAIT_TIMEOUT error`);
179
- await ctx.pool.extendLock(poolLock);
191
+ const extendSec = computeExtendTtlSec(txBuildTime, ctx.config.maxTimeBoundOffsetSeconds);
192
+ console.log(`[channels] Extending lock for WAIT_TIMEOUT (${extendSec}s)`);
193
+ await ctx.pool.extendLock(poolLock, extendSec);
180
194
  poolLock = undefined; // skip release in finally
181
195
  }
182
196
  else if (error.code === 'ONCHAIN_FAILED') {
@@ -200,7 +214,7 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
200
214
  }
201
215
  async function channelAccounts(context) {
202
216
  const startTime = Date.now();
203
- const { api, kv, params, headers } = context;
217
+ const { api, kv, params, headers, config: pluginConfig } = context;
204
218
  // Management branch: handle and return immediately
205
219
  if ((0, management_1.isManagementRequest)(params)) {
206
220
  return await (0, management_1.handleManagement)(context);
@@ -255,7 +269,12 @@ async function channelAccounts(context) {
255
269
  hash: stellar.hash ?? null,
256
270
  };
257
271
  }
258
- const fundInfo = await getCachedRelayerInfo(config.network, fundRelayerId, fundRelayer);
272
+ // 3. Resolve per-fund-relayer overrides and fetch fund relayer info in parallel
273
+ const fundOverrides = (0, fund_relayer_config_1.parseFundRelayerOverrides)(pluginConfig, fundRelayerId);
274
+ const [fundInfo, fees] = await Promise.all([
275
+ getCachedRelayerInfo(config.network, fundRelayerId, fundRelayer),
276
+ (0, fund_relayer_config_1.resolveInclusionFees)(fundOverrides, config, fundRelayer, kv),
277
+ ]);
259
278
  if (!fundInfo || !fundInfo.address) {
260
279
  throw (0, relayer_sdk_1.pluginError)('Fund relayer not found', {
261
280
  code: 'RELAYER_UNAVAILABLE',
@@ -270,16 +289,21 @@ async function channelAccounts(context) {
270
289
  details: { network_type: fundInfo.network_type, relayerId: fundRelayerId },
271
290
  });
272
291
  }
273
- // 3. Build acquire options for contract capacity limits
292
+ // 4. Build acquire options and resolve remaining overrides
274
293
  const acquireOptions = {
275
294
  limitedContracts: config.limitedContracts,
276
295
  capacityRatio: config.contractCapacityRatio,
277
296
  };
278
- const fees = {
279
- inclusionFeeDefault: config.inclusionFeeDefault,
280
- inclusionFeeLimited: config.inclusionFeeLimited,
297
+ const timeouts = (0, fund_relayer_config_1.resolveTimeouts)(fundOverrides, config);
298
+ const txParams = (0, fund_relayer_config_1.resolveTransactionParams)(fundOverrides, config);
299
+ const effectiveConfig = {
300
+ ...config,
301
+ globalTimeoutMs: timeouts.globalTimeoutMs,
302
+ pollingTimeoutMs: timeouts.pollingTimeoutMs,
303
+ maxTimeBoundOffsetSeconds: txParams.maxTimeBoundOffsetSeconds,
304
+ minSignatureExpirationLedgerBuffer: txParams.minSignatureExpirationLedgerBuffer,
281
305
  };
282
- // 4. Build pipeline context
306
+ // 5. Build pipeline context
283
307
  const ctx = {
284
308
  api,
285
309
  kv,
@@ -291,10 +315,10 @@ async function channelAccounts(context) {
291
315
  acquireOptions,
292
316
  fees,
293
317
  tracker,
294
- config,
318
+ config: effectiveConfig,
295
319
  startTime,
296
320
  };
297
- // 5. Branch by request type
321
+ // 6. Branch by request type
298
322
  if (request.type === 'xdr') {
299
323
  console.log(`[channels] Flow: XDR submit-only`);
300
324
  return await handleXdrSubmit(request.xdr, ctx, request.skipWait);
@@ -34,12 +34,12 @@ export interface SimulationResult {
34
34
  * 1. Zero auth entries — no one needs to authorize anything
35
35
  * 2. Zero read-write footprint entries — no ledger state will be modified
36
36
  */
37
- export declare function simulateTransaction(func: xdr.HostFunction, auth: xdr.SorobanAuthorizationEntry[] | undefined, sourceAddress: string, relayer: Relayer, networkPassphrase: string): Promise<SimulationResult>;
37
+ export declare function simulateTransaction(func: xdr.HostFunction, auth: xdr.SorobanAuthorizationEntry[] | undefined, sourceAddress: string, relayer: Relayer, networkPassphrase: string, maxTimeBoundOffsetSeconds?: number): Promise<SimulationResult>;
38
38
  /**
39
39
  * Build and assemble a transaction using a channel account and an already-
40
40
  * obtained simulation result. No additional network calls are made.
41
41
  */
42
- export declare function buildWithChannel(func: xdr.HostFunction, auth: xdr.SorobanAuthorizationEntry[] | undefined, channel: ChannelAccount, networkPassphrase: string, simResult: rpc.Api.RawSimulateTransactionResponse, minSignatureExpirationLedgerBuffer?: number): Transaction;
42
+ export declare function buildWithChannel(func: xdr.HostFunction, auth: xdr.SorobanAuthorizationEntry[] | undefined, channel: ChannelAccount, networkPassphrase: string, simResult: rpc.Api.RawSimulateTransactionResponse, minSignatureExpirationLedgerBuffer?: number, maxTimeBoundOffsetSeconds?: number): Transaction;
43
43
  /** Extract human-readable message + error type from simulation error diagnostic events */
44
44
  export declare function parseSimulationError(error: string): string;
45
45
  //# sourceMappingURL=simulation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"simulation.d.ts","sourceRoot":"","sources":["../../src/plugin/simulation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAsB,GAAG,EAAE,WAAW,EAAsB,GAAG,EAAE,MAAM,sBAAsB,CAAC;AACrG,OAAO,EAAgD,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAGlG,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,UAAU,EAAE,OAAO,CAAC;IACpB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,YAAY,EAAE,GAAG,CAAC,GAAG,CAAC,8BAA8B,CAAC;CACtD;AAOD;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,GAAG,CAAC,YAAY,EACtB,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,GAAG,SAAS,EACjD,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,OAAO,EAChB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,gBAAgB,CAAC,CAiF3B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,GAAG,CAAC,YAAY,EACtB,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,GAAG,SAAS,EACjD,OAAO,EAAE,cAAc,EACvB,iBAAiB,EAAE,MAAM,EACzB,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,8BAA8B,EACjD,kCAAkC,GAAE,MAA0D,GAC7F,WAAW,CA+Fb;AA4CD,0FAA0F;AAC1F,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAe1D"}
1
+ {"version":3,"file":"simulation.d.ts","sourceRoot":"","sources":["../../src/plugin/simulation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAsB,GAAG,EAAE,WAAW,EAAsB,GAAG,EAAE,MAAM,sBAAsB,CAAC;AACrG,OAAO,EAAgD,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAGlG,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,UAAU,EAAE,OAAO,CAAC;IACpB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,YAAY,EAAE,GAAG,CAAC,GAAG,CAAC,8BAA8B,CAAC;CACtD;AAOD;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,GAAG,CAAC,YAAY,EACtB,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,GAAG,SAAS,EACjD,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,OAAO,EAChB,iBAAiB,EAAE,MAAM,EACzB,yBAAyB,GAAE,MAA2C,GACrE,OAAO,CAAC,gBAAgB,CAAC,CAiF3B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,GAAG,CAAC,YAAY,EACtB,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,GAAG,SAAS,EACjD,OAAO,EAAE,cAAc,EACvB,iBAAiB,EAAE,MAAM,EACzB,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,8BAA8B,EACjD,kCAAkC,GAAE,MAA0D,EAC9F,yBAAyB,GAAE,MAA2C,GACrE,WAAW,CA+Fb;AA4CD,0FAA0F;AAC1F,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAe1D"}
@@ -26,12 +26,12 @@ const constants_1 = require("./constants");
26
26
  * 1. Zero auth entries — no one needs to authorize anything
27
27
  * 2. Zero read-write footprint entries — no ledger state will be modified
28
28
  */
29
- async function simulateTransaction(func, auth, sourceAddress, relayer, networkPassphrase) {
29
+ async function simulateTransaction(func, auth, sourceAddress, relayer, networkPassphrase, maxTimeBoundOffsetSeconds = constants_1.TIME.MAX_TIME_BOUND_OFFSET_SECONDS) {
30
30
  const now = Math.floor(Date.now() / 1000);
31
31
  const transaction = new stellar_sdk_1.TransactionBuilder(new stellar_sdk_1.Account(sourceAddress, '0'), {
32
32
  fee: constants_1.SIMULATION.DEFAULT_FEE,
33
33
  networkPassphrase,
34
- timebounds: { minTime: constants_1.SIMULATION.MIN_TIME_BOUND, maxTime: now + constants_1.SIMULATION.MAX_TIME_BOUND_OFFSET_SECONDS },
34
+ timebounds: { minTime: constants_1.TIME.MIN_TIME_BOUND, maxTime: now + maxTimeBoundOffsetSeconds },
35
35
  })
36
36
  .addOperation(stellar_sdk_1.Operation.invokeHostFunction({ func, auth }))
37
37
  .build();
@@ -104,7 +104,7 @@ async function simulateTransaction(func, auth, sourceAddress, relayer, networkPa
104
104
  * Build and assemble a transaction using a channel account and an already-
105
105
  * obtained simulation result. No additional network calls are made.
106
106
  */
107
- function buildWithChannel(func, auth, channel, networkPassphrase, simResult, minSignatureExpirationLedgerBuffer = constants_1.SIMULATION.MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER) {
107
+ function buildWithChannel(func, auth, channel, networkPassphrase, simResult, minSignatureExpirationLedgerBuffer = constants_1.SIMULATION.MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER, maxTimeBoundOffsetSeconds = constants_1.TIME.MAX_TIME_BOUND_OFFSET_SECONDS) {
108
108
  if (!simResult.transactionData) {
109
109
  throw (0, relayer_sdk_1.pluginError)('Simulation response missing transactionData', {
110
110
  code: 'SIMULATION_INVALID_RESPONSE',
@@ -168,7 +168,7 @@ function buildWithChannel(func, auth, channel, networkPassphrase, simResult, min
168
168
  const transaction = new stellar_sdk_1.TransactionBuilder(new stellar_sdk_1.Account(channel.address, channel.sequence), {
169
169
  fee: constants_1.SIMULATION.DEFAULT_FEE,
170
170
  networkPassphrase,
171
- timebounds: { minTime: constants_1.SIMULATION.MIN_TIME_BOUND, maxTime: now + constants_1.SIMULATION.MAX_TIME_BOUND_OFFSET_SECONDS },
171
+ timebounds: { minTime: constants_1.TIME.MIN_TIME_BOUND, maxTime: now + maxTimeBoundOffsetSeconds },
172
172
  sorobanData,
173
173
  })
174
174
  .addOperation(stellar_sdk_1.Operation.invokeHostFunction({
@@ -4,5 +4,6 @@
4
4
  * Transaction validation helpers for the XDR submit-only path.
5
5
  */
6
6
  import { Transaction } from '@stellar/stellar-sdk';
7
- export declare function validateExistingTransactionForSubmitOnly(tx: Transaction): Transaction;
7
+ import type { ChannelAccountsConfig } from './config';
8
+ export declare function validateExistingTransactionForSubmitOnly(tx: Transaction, config: Pick<ChannelAccountsConfig, 'maxTimeBoundOffsetSeconds'>): Transaction;
8
9
  //# sourceMappingURL=tx.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tx.d.ts","sourceRoot":"","sources":["../../src/plugin/tx.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAO,MAAM,sBAAsB,CAAC;AAIxD,wBAAgB,wCAAwC,CAAC,EAAE,EAAE,WAAW,GAAG,WAAW,CAkDrF"}
1
+ {"version":3,"file":"tx.d.ts","sourceRoot":"","sources":["../../src/plugin/tx.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAO,MAAM,sBAAsB,CAAC;AAExD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAGtD,wBAAgB,wCAAwC,CACtD,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,2BAA2B,CAAC,GAC/D,WAAW,CAmDb"}
package/dist/plugin/tx.js CHANGED
@@ -9,7 +9,8 @@ exports.validateExistingTransactionForSubmitOnly = validateExistingTransactionFo
9
9
  const stellar_sdk_1 = require("@stellar/stellar-sdk");
10
10
  const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
11
11
  const constants_1 = require("./constants");
12
- function validateExistingTransactionForSubmitOnly(tx) {
12
+ function validateExistingTransactionForSubmitOnly(tx, config) {
13
+ const { maxTimeBoundOffsetSeconds } = config;
13
14
  const now = Math.floor(Date.now() / 1000);
14
15
  // Reject fee-bump envelopes
15
16
  const envelope = tx.toEnvelope();
@@ -42,8 +43,8 @@ function validateExistingTransactionForSubmitOnly(tx) {
42
43
  details: { maxTime, now },
43
44
  });
44
45
  }
45
- if (maxTime - now > constants_1.SIMULATION.MAX_FUTURE_TIME_BOUND_SECONDS) {
46
- throw (0, relayer_sdk_1.pluginError)(`Transaction \`timeBounds.maxTime\` too far into the future. Must be no greater than ${constants_1.SIMULATION.MAX_FUTURE_TIME_BOUND_SECONDS} seconds`, {
46
+ if (maxTime - now > maxTimeBoundOffsetSeconds) {
47
+ throw (0, relayer_sdk_1.pluginError)(`Transaction \`timeBounds.maxTime\` too far into the future. Must be no greater than ${maxTimeBoundOffsetSeconds} seconds`, {
47
48
  code: 'TIMEBOUNDS_TOO_FAR',
48
49
  status: constants_1.HTTP_STATUS.BAD_REQUEST,
49
50
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openzeppelin/relayer-plugin-channels",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "OpenZeppelin Relayer Plugin for Stellar Channel Accounts",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",