@openzeppelin/relayer-plugin-channels 0.18.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 +101 -2
- package/dist/plugin/config.d.ts +1 -0
- package/dist/plugin/config.d.ts.map +1 -1
- package/dist/plugin/config.js +10 -2
- package/dist/plugin/constants.d.ts +15 -8
- package/dist/plugin/constants.d.ts.map +1 -1
- package/dist/plugin/constants.js +24 -12
- package/dist/plugin/fee-stats.d.ts +17 -0
- package/dist/plugin/fee-stats.d.ts.map +1 -0
- package/dist/plugin/fee-stats.js +68 -0
- package/dist/plugin/fund-relayer-config.d.ts +64 -0
- package/dist/plugin/fund-relayer-config.d.ts.map +1 -0
- package/dist/plugin/fund-relayer-config.js +130 -0
- package/dist/plugin/handler.d.ts +14 -0
- package/dist/plugin/handler.d.ts.map +1 -1
- package/dist/plugin/handler.js +72 -17
- package/dist/plugin/pool.d.ts +8 -5
- package/dist/plugin/pool.d.ts.map +1 -1
- package/dist/plugin/pool.js +62 -65
- package/dist/plugin/simulation.d.ts +2 -2
- package/dist/plugin/simulation.d.ts.map +1 -1
- package/dist/plugin/simulation.js +4 -4
- package/dist/plugin/tx.d.ts +2 -1
- package/dist/plugin/tx.d.ts.map +1 -1
- package/dist/plugin/tx.js +4 -3
- package/package.json +1 -1
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
|
-
- [
|
|
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:
|
|
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.
|
package/dist/plugin/config.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/plugin/config.js
CHANGED
|
@@ -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 +
|
|
15
|
-
const DEFAULT_INCLUSION_FEE_LIMITED = Number(stellar_sdk_1.BASE_FEE) * 2 +
|
|
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,27 +17,27 @@ 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:
|
|
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;
|
|
24
24
|
};
|
|
25
25
|
export declare const POOL: {
|
|
26
26
|
readonly CLAIM_LOCK_TTL_SECONDS: 3;
|
|
27
|
-
readonly ACQUIRE_MAX_SPINS:
|
|
28
|
-
readonly
|
|
29
|
-
readonly
|
|
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;
|
|
30
31
|
readonly CHANNEL_COOLDOWN_MS: 6000;
|
|
31
|
-
readonly
|
|
32
|
+
readonly LRU_KEY_TTL_SECONDS: 86400;
|
|
32
33
|
};
|
|
34
|
+
export declare const RELAYER_INFO_CACHE_TTL_SECONDS = 1800;
|
|
33
35
|
export declare const TIME: {
|
|
36
|
+
readonly MIN_TIME_BOUND: 0;
|
|
34
37
|
readonly MAX_TIME_BOUND_OFFSET_SECONDS: 60;
|
|
35
38
|
};
|
|
36
39
|
export declare const SIMULATION: {
|
|
37
40
|
readonly DEFAULT_FEE: "100";
|
|
38
|
-
readonly MIN_TIME_BOUND: 0;
|
|
39
|
-
readonly MAX_TIME_BOUND_OFFSET_SECONDS: 60;
|
|
40
|
-
readonly MAX_FUTURE_TIME_BOUND_SECONDS: 60;
|
|
41
41
|
readonly SIMULATION_AUTH_MODE: "enforce";
|
|
42
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. */
|
|
43
43
|
readonly MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER: 2;
|
|
@@ -53,4 +53,11 @@ export declare const POLLING: {
|
|
|
53
53
|
export declare const FEE: {
|
|
54
54
|
readonly NON_SOROBAN_FEE: 100000;
|
|
55
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
|
+
};
|
|
56
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
|
|
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"}
|
package/dist/plugin/constants.js
CHANGED
|
@@ -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.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:
|
|
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,
|
|
@@ -32,25 +32,27 @@ exports.POOL = {
|
|
|
32
32
|
// Per-channel claim lock TTL — must exceed worst-case callback latency
|
|
33
33
|
// to prevent TTL expiry allowing a second worker into the same claim section
|
|
34
34
|
CLAIM_LOCK_TTL_SECONDS: 3,
|
|
35
|
-
// Retry policy
|
|
36
|
-
ACQUIRE_MAX_SPINS:
|
|
37
|
-
|
|
38
|
-
|
|
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,
|
|
39
41
|
// Hard-block cooldown for uncertain-outcome channels (~1 Stellar ledger with margin)
|
|
40
42
|
CHANNEL_COOLDOWN_MS: 6000,
|
|
41
|
-
// Housekeeping TTL for
|
|
42
|
-
|
|
43
|
+
// Housekeeping TTL for per-channel LRU keys
|
|
44
|
+
LRU_KEY_TTL_SECONDS: 86400,
|
|
43
45
|
};
|
|
44
|
-
//
|
|
46
|
+
// Relayer info cache — address and network_type are effectively immutable
|
|
47
|
+
exports.RELAYER_INFO_CACHE_TTL_SECONDS = 1800; // 30 minutes
|
|
48
|
+
// Time constants — used for both simulation tx construction and incoming tx validation
|
|
45
49
|
exports.TIME = {
|
|
50
|
+
MIN_TIME_BOUND: 0,
|
|
46
51
|
MAX_TIME_BOUND_OFFSET_SECONDS: 60,
|
|
47
52
|
};
|
|
48
53
|
// Simulation-related defaults
|
|
49
54
|
exports.SIMULATION = {
|
|
50
55
|
DEFAULT_FEE: '100',
|
|
51
|
-
MIN_TIME_BOUND: 0,
|
|
52
|
-
MAX_TIME_BOUND_OFFSET_SECONDS: 60,
|
|
53
|
-
MAX_FUTURE_TIME_BOUND_SECONDS: 60,
|
|
54
56
|
SIMULATION_AUTH_MODE: 'enforce',
|
|
55
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. */
|
|
56
58
|
MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER: 2,
|
|
@@ -69,3 +71,13 @@ exports.FEE = {
|
|
|
69
71
|
// For non-Soroban txs: 100,000 stroops (0.01 XLM) per Stellar best practice
|
|
70
72
|
NON_SOROBAN_FEE: 100000,
|
|
71
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
|
+
}
|
package/dist/plugin/handler.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/plugin/handler.js
CHANGED
|
@@ -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");
|
|
@@ -18,13 +20,53 @@ const constants_1 = require("./constants");
|
|
|
18
20
|
const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
19
21
|
const simulation_1 = require("./simulation");
|
|
20
22
|
const fee_1 = require("./fee");
|
|
23
|
+
const fund_relayer_config_1 = require("./fund-relayer-config");
|
|
21
24
|
const tx_1 = require("./tx");
|
|
22
25
|
const fee_tracking_1 = require("./fee-tracking");
|
|
23
26
|
const sequence_1 = require("./sequence");
|
|
27
|
+
/**
|
|
28
|
+
* In-memory cache for relayer info. Avoids a remote API call (getRelayer → HTTP GET)
|
|
29
|
+
* on every request while a channel lock is held. Keyed by `${network}:${relayerId}`.
|
|
30
|
+
* Entries expire after RELAYER_INFO_CACHE_TTL_SECONDS; stale entries are evicted on miss.
|
|
31
|
+
*/
|
|
32
|
+
const relayerInfoCache = new Map();
|
|
33
|
+
/** @internal Exported for test isolation only. */
|
|
34
|
+
function clearRelayerInfoCache() {
|
|
35
|
+
relayerInfoCache.clear();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Return cached relayer info or fetch from the API and cache the result.
|
|
39
|
+
* Returns null if the relayer has no address (misconfigured).
|
|
40
|
+
*/
|
|
41
|
+
async function getCachedRelayerInfo(network, relayerId, relayer) {
|
|
42
|
+
const cacheKey = `${network}:${relayerId}`;
|
|
43
|
+
const cached = relayerInfoCache.get(cacheKey);
|
|
44
|
+
if (cached && cached.expiresAt > Date.now())
|
|
45
|
+
return cached.info;
|
|
46
|
+
// Evict stale entry so it doesn't linger in memory
|
|
47
|
+
if (cached)
|
|
48
|
+
relayerInfoCache.delete(cacheKey);
|
|
49
|
+
const info = await relayer.getRelayer();
|
|
50
|
+
if (!info?.address)
|
|
51
|
+
return null;
|
|
52
|
+
const entry = { address: info.address, network_type: info.network_type };
|
|
53
|
+
relayerInfoCache.set(cacheKey, { info: entry, expiresAt: Date.now() + constants_1.RELAYER_INFO_CACHE_TTL_SECONDS * 1000 });
|
|
54
|
+
return entry;
|
|
55
|
+
}
|
|
24
56
|
function getApiKey(headers, headerName) {
|
|
25
57
|
const values = headers[headerName];
|
|
26
58
|
return values?.[0]?.trim() || undefined;
|
|
27
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
|
+
}
|
|
28
70
|
/**
|
|
29
71
|
* Extracts func and auth from an unsigned Soroban transaction.
|
|
30
72
|
* Returns null if the transaction is not a single invokeHostFunction operation.
|
|
@@ -66,7 +108,7 @@ async function handleXdrSubmit(xdrStr, ctx, skipWait) {
|
|
|
66
108
|
const updatedOptions = { ...ctx.acquireOptions, contractId };
|
|
67
109
|
return handleFuncAuthSubmit(extracted.func, extracted.auth, { ...ctx, acquireOptions: updatedOptions }, skipWait);
|
|
68
110
|
}
|
|
69
|
-
const validated = (0, tx_1.validateExistingTransactionForSubmitOnly)(tx);
|
|
111
|
+
const validated = (0, tx_1.validateExistingTransactionForSubmitOnly)(tx, ctx.config);
|
|
70
112
|
const maxFee = (0, fee_1.calculateMaxFee)(validated, ctx.acquireOptions.limitedContracts, ctx.fees);
|
|
71
113
|
const contractId = (0, fee_1.getContractIdFromTransaction)(validated);
|
|
72
114
|
await ctx.tracker?.checkBudget(maxFee);
|
|
@@ -78,7 +120,7 @@ async function handleXdrSubmit(xdrStr, ctx, skipWait) {
|
|
|
78
120
|
}
|
|
79
121
|
async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
|
|
80
122
|
// Simulate once — used for both read-only detection and transaction assembly
|
|
81
|
-
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);
|
|
82
124
|
if (simulation.isReadOnly) {
|
|
83
125
|
console.log(`[channels] Read-only call detected, returning simulation result`);
|
|
84
126
|
return {
|
|
@@ -94,7 +136,7 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
|
|
|
94
136
|
try {
|
|
95
137
|
poolLock = await ctx.pool.acquire(ctx.acquireOptions);
|
|
96
138
|
const channelRelayer = ctx.api.useRelayer(poolLock.relayerId);
|
|
97
|
-
const channelInfo = await channelRelayer
|
|
139
|
+
const channelInfo = await getCachedRelayerInfo(ctx.network, poolLock.relayerId, channelRelayer);
|
|
98
140
|
console.log(`[channels] Acquired channel: ${poolLock.relayerId}`);
|
|
99
141
|
if (!channelInfo || !channelInfo.address) {
|
|
100
142
|
throw (0, relayer_sdk_1.pluginError)('Channel relayer not found', {
|
|
@@ -112,7 +154,8 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
|
|
|
112
154
|
}
|
|
113
155
|
const sequence = await (0, sequence_1.getSequence)(ctx.kv, ctx.network, channelRelayer, channelInfo.address, ctx.config.sequenceNumberCacheMaxAgeMs);
|
|
114
156
|
// Assemble the transaction using the cached simulation result — no second RPC call
|
|
115
|
-
const
|
|
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);
|
|
116
159
|
console.debug(`[channels] After assembly: built.fee=${built.fee}, minResourceFee=${simulation.rawSimResult.minResourceFee}`);
|
|
117
160
|
const signedTx = await (0, submit_1.signWithChannelAndFund)(built, channelRelayer, ctx.fundRelayer, channelInfo.address, ctx.fundAddress, ctx.networkPassphrase);
|
|
118
161
|
const maxFee = (0, fee_1.calculateMaxFee)(signedTx, ctx.acquireOptions.limitedContracts, ctx.fees);
|
|
@@ -126,8 +169,9 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
|
|
|
126
169
|
try {
|
|
127
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);
|
|
128
171
|
if (result.status === 'pending' || result.status === 'sent' || result.status === 'submitted') {
|
|
129
|
-
|
|
130
|
-
|
|
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);
|
|
131
175
|
await (0, sequence_1.clearSequence)(ctx.kv, ctx.network, channelInfo.address);
|
|
132
176
|
poolLock = undefined; // skip release in finally
|
|
133
177
|
}
|
|
@@ -144,8 +188,9 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
|
|
|
144
188
|
catch (error) {
|
|
145
189
|
await (0, sequence_1.clearSequence)(ctx.kv, ctx.network, channelInfo.address);
|
|
146
190
|
if (error.code === 'WAIT_TIMEOUT' && poolLock) {
|
|
147
|
-
|
|
148
|
-
|
|
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);
|
|
149
194
|
poolLock = undefined; // skip release in finally
|
|
150
195
|
}
|
|
151
196
|
else if (error.code === 'ONCHAIN_FAILED') {
|
|
@@ -169,7 +214,7 @@ async function handleFuncAuthSubmit(func, auth, ctx, skipWait) {
|
|
|
169
214
|
}
|
|
170
215
|
async function channelAccounts(context) {
|
|
171
216
|
const startTime = Date.now();
|
|
172
|
-
const { api, kv, params, headers } = context;
|
|
217
|
+
const { api, kv, params, headers, config: pluginConfig } = context;
|
|
173
218
|
// Management branch: handle and return immediately
|
|
174
219
|
if ((0, management_1.isManagementRequest)(params)) {
|
|
175
220
|
return await (0, management_1.handleManagement)(context);
|
|
@@ -224,7 +269,12 @@ async function channelAccounts(context) {
|
|
|
224
269
|
hash: stellar.hash ?? null,
|
|
225
270
|
};
|
|
226
271
|
}
|
|
227
|
-
|
|
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
|
+
]);
|
|
228
278
|
if (!fundInfo || !fundInfo.address) {
|
|
229
279
|
throw (0, relayer_sdk_1.pluginError)('Fund relayer not found', {
|
|
230
280
|
code: 'RELAYER_UNAVAILABLE',
|
|
@@ -239,16 +289,21 @@ async function channelAccounts(context) {
|
|
|
239
289
|
details: { network_type: fundInfo.network_type, relayerId: fundRelayerId },
|
|
240
290
|
});
|
|
241
291
|
}
|
|
242
|
-
//
|
|
292
|
+
// 4. Build acquire options and resolve remaining overrides
|
|
243
293
|
const acquireOptions = {
|
|
244
294
|
limitedContracts: config.limitedContracts,
|
|
245
295
|
capacityRatio: config.contractCapacityRatio,
|
|
246
296
|
};
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
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,
|
|
250
305
|
};
|
|
251
|
-
//
|
|
306
|
+
// 5. Build pipeline context
|
|
252
307
|
const ctx = {
|
|
253
308
|
api,
|
|
254
309
|
kv,
|
|
@@ -260,10 +315,10 @@ async function channelAccounts(context) {
|
|
|
260
315
|
acquireOptions,
|
|
261
316
|
fees,
|
|
262
317
|
tracker,
|
|
263
|
-
config,
|
|
318
|
+
config: effectiveConfig,
|
|
264
319
|
startTime,
|
|
265
320
|
};
|
|
266
|
-
//
|
|
321
|
+
// 6. Branch by request type
|
|
267
322
|
if (request.type === 'xdr') {
|
|
268
323
|
console.log(`[channels] Flow: XDR submit-only`);
|
|
269
324
|
return await handleXdrSubmit(request.xdr, ctx, request.skipWait);
|
package/dist/plugin/pool.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* - Membership comes from KV: <network>:channel:relayer-ids
|
|
6
6
|
* - Per-relayer locks with tokens: <network>:channel:in-use:<relayerId>
|
|
7
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 = {
|
|
@@ -23,8 +24,8 @@ export declare class ChannelPool {
|
|
|
23
24
|
constructor(network: 'testnet' | 'mainnet', kv: PluginKVStore, lockTtlSeconds: number);
|
|
24
25
|
/** Acquire a relayerId with a token lock */
|
|
25
26
|
acquire(options: AcquireOptions): Promise<PoolLock>;
|
|
26
|
-
/**
|
|
27
|
-
private
|
|
27
|
+
/** Try to claim one channel from a batch of candidates */
|
|
28
|
+
private tryClaimBatch;
|
|
28
29
|
/** Attempt to claim a single channel under its per-channel lock */
|
|
29
30
|
private tryClaimChannel;
|
|
30
31
|
/** Extend the lock TTL if we still own it (e.g. after WAIT_TIMEOUT) */
|
|
@@ -34,11 +35,13 @@ export declare class ChannelPool {
|
|
|
34
35
|
/** Release with cooldown: keeps lock alive with short TTL to hard-block the channel. */
|
|
35
36
|
releaseWithCooldown(lock: PoolLock, cooldownMs?: 6000): Promise<void>;
|
|
36
37
|
private membershipKey;
|
|
37
|
-
private lockKeyPrefix;
|
|
38
38
|
private lockKey;
|
|
39
39
|
private claimKey;
|
|
40
|
-
private
|
|
41
|
-
|
|
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;
|
|
42
45
|
private getRelayerIdsFromKV;
|
|
43
46
|
private getPoolCapacityDetails;
|
|
44
47
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../src/plugin/pool.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|
package/dist/plugin/pool.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Membership comes from KV: <network>:channel:relayer-ids
|
|
7
7
|
* - Per-relayer locks with tokens: <network>:channel:in-use:<relayerId>
|
|
8
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 };
|
|
@@ -24,12 +25,46 @@ class ChannelPool {
|
|
|
24
25
|
/** Acquire a relayerId with a token lock */
|
|
25
26
|
async acquire(options) {
|
|
26
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;
|
|
27
54
|
for (let i = 0; i < maxSpins; i++) {
|
|
28
|
-
|
|
55
|
+
if (offset >= candidates.length) {
|
|
56
|
+
// Full sweep done — reshuffle for next sweep
|
|
57
|
+
offset = 0;
|
|
58
|
+
shuffle(candidates);
|
|
59
|
+
}
|
|
60
|
+
const batch = candidates.slice(offset, offset + batchSize);
|
|
61
|
+
offset += batchSize;
|
|
62
|
+
const result = await this.tryClaimBatch(batch);
|
|
29
63
|
if (result)
|
|
30
64
|
return result;
|
|
31
|
-
|
|
32
|
-
|
|
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);
|
|
33
68
|
await sleep(jitter);
|
|
34
69
|
}
|
|
35
70
|
const diagnostics = await this.getPoolCapacityDetails(options, maxSpins);
|
|
@@ -40,50 +75,9 @@ class ChannelPool {
|
|
|
40
75
|
details: diagnostics,
|
|
41
76
|
});
|
|
42
77
|
}
|
|
43
|
-
/**
|
|
44
|
-
async
|
|
45
|
-
|
|
46
|
-
let ids = await this.getRelayerIdsFromKV();
|
|
47
|
-
if (ids.length === 0) {
|
|
48
|
-
throw (0, relayer_sdk_1.pluginError)('No channel accounts configured. Use the management API to set channel accounts.', {
|
|
49
|
-
code: 'NO_CHANNELS_CONFIGURED',
|
|
50
|
-
status: constants_1.HTTP_STATUS.SERVICE_UNAVAILABLE,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
if (options.contractId && options.limitedContracts.has(options.contractId)) {
|
|
54
|
-
ids = filterChannelsForLimitedContract(ids, options.capacityRatio);
|
|
55
|
-
}
|
|
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) {
|
|
78
|
+
/** Try to claim one channel from a batch of candidates */
|
|
79
|
+
async tryClaimBatch(batch) {
|
|
80
|
+
for (const candidate of batch) {
|
|
87
81
|
const result = await this.tryClaimChannel(candidate);
|
|
88
82
|
if (result)
|
|
89
83
|
return result;
|
|
@@ -98,7 +92,8 @@ class ChannelPool {
|
|
|
98
92
|
return null;
|
|
99
93
|
const token = randomToken();
|
|
100
94
|
await this.kv.set(this.lockKey(relayerId), { token, lockedAt: new Date().toISOString() }, { ttlSec: this.channelLockTtlSec });
|
|
101
|
-
|
|
95
|
+
// Fire-and-forget: LRU is best-effort, don't hold the claim lock for it
|
|
96
|
+
this.updateLru(relayerId);
|
|
102
97
|
return { relayerId, token };
|
|
103
98
|
}, { ttlSec: constants_1.POOL.CLAIM_LOCK_TTL_SECONDS, onBusy: 'skip' });
|
|
104
99
|
}
|
|
@@ -145,28 +140,29 @@ class ChannelPool {
|
|
|
145
140
|
membershipKey() {
|
|
146
141
|
return `${this.network}:channel:relayer-ids`;
|
|
147
142
|
}
|
|
148
|
-
lockKeyPrefix() {
|
|
149
|
-
return `${this.network}:channel:in-use:`;
|
|
150
|
-
}
|
|
151
143
|
lockKey(relayerId) {
|
|
152
|
-
return `${this.
|
|
144
|
+
return `${this.network}:channel:in-use:${relayerId}`;
|
|
153
145
|
}
|
|
154
146
|
claimKey(relayerId) {
|
|
155
147
|
return `${this.network}:channel:claim:${relayerId}`;
|
|
156
148
|
}
|
|
157
|
-
|
|
158
|
-
return `${this.network}:channel:lru
|
|
149
|
+
lruKey(relayerId) {
|
|
150
|
+
return `${this.network}:channel:lru:${relayerId}`;
|
|
159
151
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
});
|
|
170
166
|
}
|
|
171
167
|
async getRelayerIdsFromKV() {
|
|
172
168
|
try {
|
|
@@ -232,8 +228,9 @@ function simpleHash(str) {
|
|
|
232
228
|
*/
|
|
233
229
|
function filterChannelsForLimitedContract(ids, ratio) {
|
|
234
230
|
const k = Math.max(1, Math.floor(ratio * ids.length));
|
|
231
|
+
const hashes = new Map(ids.map((id) => [id, simpleHash(id)]));
|
|
235
232
|
return ids
|
|
236
233
|
.slice()
|
|
237
|
-
.sort((a, b) =>
|
|
234
|
+
.sort((a, b) => hashes.get(a) - hashes.get(b) || a.localeCompare(b))
|
|
238
235
|
.slice(0, k);
|
|
239
236
|
}
|
|
@@ -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,
|
|
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.
|
|
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.
|
|
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({
|
package/dist/plugin/tx.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/plugin/tx.d.ts.map
CHANGED
|
@@ -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;
|
|
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 >
|
|
46
|
-
throw (0, relayer_sdk_1.pluginError)(`Transaction \`timeBounds.maxTime\` too far into the future. Must be no greater than ${
|
|
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
|
});
|