@openzeppelin/relayer-plugin-channels 0.9.0 → 0.11.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
@@ -229,6 +229,10 @@ export API_KEY_HEADER="x-api-key" # Header name to extract API key (defa
229
229
  # Contract capacity limits (optional)
230
230
  export LIMITED_CONTRACTS="CDL74RF5BLYR2YBLCCI7F5FB6TPSCLKEJUBSD2RSVWZ4YHF3VMFAIGWA" # Comma-separated contract addresses
231
231
  export CONTRACT_CAPACITY_RATIO=0.8 # Max ratio of pool for limited contracts (default: 0.8 = 80%)
232
+
233
+ # Inclusion fee overrides (optional)
234
+ export INCLUSION_FEE_DEFAULT=203 # Inclusion fee in stroops for regular contracts (default: BASE_FEE * 2 + 3 = 203)
235
+ export INCLUSION_FEE_LIMITED=201 # Inclusion fee in stroops for limited contracts (default: BASE_FEE * 2 + 1 = 201)
232
236
  ```
233
237
 
234
238
  Your Relayer should now contain:
@@ -510,6 +514,49 @@ curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
510
514
  }
511
515
  ```
512
516
 
517
+ ### Get Pool Stats
518
+
519
+ Returns pool health metrics, configuration, and fee info.
520
+
521
+ ```bash
522
+ curl -X POST http://localhost:8080/... \
523
+ -H "Content-Type: application/json" \
524
+ -d '{
525
+ "params": {
526
+ "management": {
527
+ "action": "stats",
528
+ "adminSecret": "your-secret-here"
529
+ }
530
+ }
531
+ }'
532
+ ```
533
+
534
+ **Response:**
535
+
536
+ ```json
537
+ {
538
+ "pool": {
539
+ "size": 5,
540
+ "locked": 2,
541
+ "available": 3
542
+ },
543
+ "config": {
544
+ "network": "testnet",
545
+ "lockTtlSeconds": 30,
546
+ "feeLimit": 10000,
547
+ "feeResetPeriodSeconds": 3600,
548
+ "contractCapacityRatio": 0.8,
549
+ "limitedContracts": []
550
+ },
551
+ "fees": {
552
+ "inclusionFeeDefault": 203,
553
+ "inclusionFeeLimited": 201
554
+ }
555
+ }
556
+ ```
557
+
558
+ > **Note:** If lock checks fail, `locked` and `available` will be `undefined` rather than causing the request to fail. The `size` and config info are always returned.
559
+
513
560
  **Important Notes:**
514
561
 
515
562
  - You must configure at least one channel account before the plugin can process transactions
@@ -683,6 +730,19 @@ const result = await adminClient.deleteFeeLimit('client-api-key');
683
730
  console.log(result.ok); // true
684
731
  ```
685
732
 
733
+ #### Get Pool Stats (Management)
734
+
735
+ ```typescript
736
+ // Get pool health metrics (requires adminSecret)
737
+ const stats = await adminClient.getStats();
738
+
739
+ console.log(stats.pool.size); // total channels
740
+ console.log(stats.pool.locked); // currently in-use
741
+ console.log(stats.pool.available); // size - locked
742
+ console.log(stats.config.network); // 'testnet' or 'mainnet'
743
+ console.log(stats.fees); // inclusion fee values
744
+ ```
745
+
686
746
  ### Error Handling
687
747
 
688
748
  The client provides three types of errors:
@@ -1,4 +1,4 @@
1
- import type { ChannelsClientConfig, ChannelsXdrRequest, ChannelsFuncAuthRequest, ChannelsTransactionResponse, ListChannelAccountsResponse, SetChannelAccountsResponse, GetFeeUsageResponse, GetFeeLimitResponse, SetFeeLimitResponse, DeleteFeeLimitResponse } from './types';
1
+ import type { ChannelsClientConfig, ChannelsXdrRequest, ChannelsFuncAuthRequest, ChannelsTransactionResponse, ListChannelAccountsResponse, SetChannelAccountsResponse, GetFeeUsageResponse, GetFeeLimitResponse, SetFeeLimitResponse, DeleteFeeLimitResponse, GetStatsResponse } from './types';
2
2
  /**
3
3
  * Client for interacting with the Channels plugin
4
4
  *
@@ -137,6 +137,20 @@ export declare class ChannelsClient {
137
137
  * @throws {PluginUnexpectedError} Malformed response or client-side errors
138
138
  */
139
139
  deleteFeeLimit(apiKey: string): Promise<DeleteFeeLimitResponse>;
140
+ /**
141
+ * Get pool health stats including capacity, config, and fees (requires adminSecret)
142
+ *
143
+ * @returns Pool stats with size, locked/available counts, config, and fee info
144
+ * @throws {Error} If adminSecret not provided in config
145
+ * @throws {PluginTransportError} Network/HTTP failures
146
+ * @throws {PluginExecutionError} Plugin rejected the request
147
+ * @throws {PluginUnexpectedError} Malformed response or client-side errors
148
+ *
149
+ * @example
150
+ * const stats = await client.getStats();
151
+ * console.log(stats.pool.available);
152
+ */
153
+ getStats(): Promise<GetStatsResponse>;
140
154
  /**
141
155
  * Ensures adminSecret is configured
142
156
  *
@@ -1 +1 @@
1
- {"version":3,"file":"channels-client.d.ts","sourceRoot":"","sources":["../../src/client/channels-client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,EACvB,2BAA2B,EAC3B,2BAA2B,EAC3B,0BAA0B,EAC1B,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,sBAAsB,EAEvB,MAAM,SAAS,CAAC;AAEjB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAgB;IAC7C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAa;IACzC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;gBAEvB,MAAM,EAAE,oBAAoB;IAiCxC;;;;;;;;;;;;;OAaG;IACG,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAI1F;;;;;;;;;;;;;;;OAeG;IACG,wBAAwB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAItG;;;;;;;;;;;;OAYG;IACG,mBAAmB,IAAI,OAAO,CAAC,2BAA2B,CAAC;IASjE;;;;;;;;;;;;;;;OAeG;IACG,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,0BAA0B,CAAC;IAUnF;;;;;;;;;;;;;OAaG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAU/D;;;;;;;;;OASG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAU/D;;;;;;;;;;OAUG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAW9E;;;;;;;;;OASG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAUrE;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAO1B;;;;;;;OAOG;IAEH,OAAO,CAAC,eAAe;IAgBvB;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAcxB;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAUrB;;;;;;;;OAQG;YACW,IAAI;IAwBlB;;;;;;OAMG;YACW,QAAQ;CASvB"}
1
+ {"version":3,"file":"channels-client.d.ts","sourceRoot":"","sources":["../../src/client/channels-client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,EACvB,2BAA2B,EAC3B,2BAA2B,EAC3B,0BAA0B,EAC1B,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,sBAAsB,EACtB,gBAAgB,EAEjB,MAAM,SAAS,CAAC;AAEjB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAgB;IAC7C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAa;IACzC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;gBAEvB,MAAM,EAAE,oBAAoB;IAiCxC;;;;;;;;;;;;;OAaG;IACG,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAI1F;;;;;;;;;;;;;;;OAeG;IACG,wBAAwB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAItG;;;;;;;;;;;;OAYG;IACG,mBAAmB,IAAI,OAAO,CAAC,2BAA2B,CAAC;IASjE;;;;;;;;;;;;;;;OAeG;IACG,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,0BAA0B,CAAC;IAUnF;;;;;;;;;;;;;OAaG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAU/D;;;;;;;;;OASG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAU/D;;;;;;;;;;OAUG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAW9E;;;;;;;;;OASG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAUrE;;;;;;;;;;;;OAYG;IACG,QAAQ,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAS3C;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAO1B;;;;;;;OAOG;IAEH,OAAO,CAAC,eAAe;IAgBvB;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAcxB;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAUrB;;;;;;;;OAQG;YACW,IAAI;IAwBlB;;;;;;OAMG;YACW,QAAQ;CASvB"}
@@ -221,6 +221,27 @@ class ChannelsClient {
221
221
  },
222
222
  });
223
223
  }
224
+ /**
225
+ * Get pool health stats including capacity, config, and fees (requires adminSecret)
226
+ *
227
+ * @returns Pool stats with size, locked/available counts, config, and fee info
228
+ * @throws {Error} If adminSecret not provided in config
229
+ * @throws {PluginTransportError} Network/HTTP failures
230
+ * @throws {PluginExecutionError} Plugin rejected the request
231
+ * @throws {PluginUnexpectedError} Malformed response or client-side errors
232
+ *
233
+ * @example
234
+ * const stats = await client.getStats();
235
+ * console.log(stats.pool.available);
236
+ */
237
+ async getStats() {
238
+ return this.call({
239
+ management: {
240
+ action: 'stats',
241
+ adminSecret: this.requireAdminSecret(),
242
+ },
243
+ });
244
+ }
224
245
  /**
225
246
  * Ensures adminSecret is configured
226
247
  *
@@ -5,6 +5,6 @@
5
5
  * in both direct HTTP mode and OpenZeppelin Relayer mode.
6
6
  */
7
7
  export { ChannelsClient } from './channels-client';
8
- export { ChannelsClientConfig, DirectHttpConfig, RelayerConfig, ChannelsXdrRequest, ChannelsFuncAuthRequest, ChannelsTransactionResponse, ListChannelAccountsResponse, SetChannelAccountsResponse, GetFeeUsageResponse, GetFeeLimitResponse, SetFeeLimitResponse, DeleteFeeLimitResponse, } from './types';
8
+ export { ChannelsClientConfig, DirectHttpConfig, RelayerConfig, ChannelsXdrRequest, ChannelsFuncAuthRequest, ChannelsTransactionResponse, ListChannelAccountsResponse, SetChannelAccountsResponse, GetFeeUsageResponse, GetFeeLimitResponse, SetFeeLimitResponse, DeleteFeeLimitResponse, GetStatsResponse, } from './types';
9
9
  export { PluginClientError, PluginTransportError, PluginExecutionError, PluginUnexpectedError } from './errors';
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,uBAAuB,EACvB,2BAA2B,EAC3B,2BAA2B,EAC3B,0BAA0B,EAC1B,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,uBAAuB,EACvB,2BAA2B,EAC3B,2BAA2B,EAC3B,0BAA0B,EAC1B,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC"}
@@ -156,6 +156,33 @@ export interface DeleteFeeLimitResponse {
156
156
  traces?: any[];
157
157
  };
158
158
  }
159
+ /**
160
+ * Response from getting pool stats
161
+ */
162
+ export interface GetStatsResponse {
163
+ pool: {
164
+ size: number;
165
+ locked?: number;
166
+ available?: number;
167
+ };
168
+ config: {
169
+ network: 'testnet' | 'mainnet';
170
+ lockTtlSeconds: number;
171
+ feeLimit?: number;
172
+ feeResetPeriodSeconds?: number;
173
+ contractCapacityRatio: number;
174
+ limitedContracts: string[];
175
+ };
176
+ fees: {
177
+ inclusionFeeDefault: number;
178
+ inclusionFeeLimited: number;
179
+ };
180
+ /** Optional metadata (logs and traces) */
181
+ metadata?: {
182
+ logs?: LogEntry[];
183
+ traces?: any[];
184
+ };
185
+ }
159
186
  /**
160
187
  * Plugin response structure for successful operations
161
188
  */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"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"}
@@ -13,6 +13,9 @@ export interface ChannelAccountsConfig {
13
13
  apiKeyHeader: string;
14
14
  limitedContracts: Set<string>;
15
15
  contractCapacityRatio: number;
16
+ inclusionFeeDefault: number;
17
+ inclusionFeeLimited: number;
18
+ sequenceNumberCacheMaxAgeMs: number;
16
19
  }
17
20
  /**
18
21
  * Load configuration from environment variables
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/plugin/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAoFD;;GAEG;AACH,wBAAgB,UAAU,IAAI,qBAAqB,CAoBlD;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,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;CACrC;AAmGD;;GAEG;AACH,wBAAgB,UAAU,IAAI,qBAAqB,CAuBlD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAE3E"}
@@ -10,6 +10,9 @@ exports.getNetworkPassphrase = getNetworkPassphrase;
10
10
  const stellar_sdk_1 = require("@stellar/stellar-sdk");
11
11
  const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
12
12
  const constants_1 = require("./constants");
13
+ // 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
13
16
  function requireEnv(name) {
14
17
  const v = process.env[name];
15
18
  if (!v || v.trim() === '') {
@@ -79,6 +82,22 @@ function parseLimitedContracts() {
79
82
  }
80
83
  return new Set(contracts);
81
84
  }
85
+ function parseInclusionFee(envVar, defaultValue) {
86
+ const raw = process.env[envVar];
87
+ if (!raw)
88
+ return defaultValue;
89
+ const n = Number(raw);
90
+ if (!Number.isFinite(n) || n < 0)
91
+ return defaultValue;
92
+ return Math.floor(n);
93
+ }
94
+ function parseSequenceNumberCacheMaxAge() {
95
+ const raw = process.env.SEQUENCE_NUMBER_CACHE_MAX_AGE_MS;
96
+ if (!raw)
97
+ return constants_1.CONFIG.DEFAULT_SEQUENCE_NUMBER_CACHE_MAX_AGE_MS;
98
+ const n = Number(raw);
99
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : constants_1.CONFIG.DEFAULT_SEQUENCE_NUMBER_CACHE_MAX_AGE_MS;
100
+ }
82
101
  function parseContractCapacityRatio() {
83
102
  const raw = process.env.CONTRACT_CAPACITY_RATIO;
84
103
  if (!raw)
@@ -110,6 +129,9 @@ function loadConfig() {
110
129
  apiKeyHeader: parseApiKeyHeader(),
111
130
  limitedContracts: parseLimitedContracts(),
112
131
  contractCapacityRatio: parseContractCapacityRatio(),
132
+ inclusionFeeDefault: parseInclusionFee('INCLUSION_FEE_DEFAULT', DEFAULT_INCLUSION_FEE_DEFAULT),
133
+ inclusionFeeLimited: parseInclusionFee('INCLUSION_FEE_LIMITED', DEFAULT_INCLUSION_FEE_LIMITED),
134
+ sequenceNumberCacheMaxAgeMs: parseSequenceNumberCacheMaxAge(),
113
135
  };
114
136
  }
115
137
  /**
@@ -19,6 +19,7 @@ export declare const CONFIG: {
19
19
  readonly MIN_LOCK_TTL_SECONDS: 3;
20
20
  readonly MAX_LOCK_TTL_SECONDS: 30;
21
21
  readonly DEFAULT_CONTRACT_CAPACITY_RATIO: 0.8;
22
+ readonly DEFAULT_SEQUENCE_NUMBER_CACHE_MAX_AGE_MS: 120000;
22
23
  };
23
24
  export declare const POOL: {
24
25
  readonly MUTEX_TTL_SECONDS: 1;
@@ -34,6 +35,7 @@ export declare const SIMULATION: {
34
35
  readonly MIN_TIME_BOUND: 0;
35
36
  readonly MAX_TIME_BOUND_OFFSET_SECONDS: 120;
36
37
  readonly MAX_FUTURE_TIME_BOUND_SECONDS: 120;
38
+ readonly SIMULATION_AUTH_MODE: "enforce";
37
39
  };
38
40
  export declare const POLLING: {
39
41
  readonly INTERVAL_MS: 1000;
@@ -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;;;;;CAKT,CAAC;AAGX,eAAO,MAAM,IAAI;;;;;CAOP,CAAC;AAGX,eAAO,MAAM,IAAI;;CAEP,CAAC;AAGX,eAAO,MAAM,UAAU;;;;;CAKb,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;;;;;;CAMT,CAAC;AAGX,eAAO,MAAM,IAAI;;;;;CAOP,CAAC;AAGX,eAAO,MAAM,IAAI;;CAEP,CAAC;AAGX,eAAO,MAAM,UAAU;;;;;;CAMb,CAAC;AAGX,eAAO,MAAM,OAAO;;;CAGV,CAAC;AAEX,eAAO,MAAM,GAAG;;CAGN,CAAC"}
@@ -24,6 +24,7 @@ exports.CONFIG = {
24
24
  MIN_LOCK_TTL_SECONDS: 3,
25
25
  MAX_LOCK_TTL_SECONDS: 30,
26
26
  DEFAULT_CONTRACT_CAPACITY_RATIO: 0.8,
27
+ DEFAULT_SEQUENCE_NUMBER_CACHE_MAX_AGE_MS: 120000,
27
28
  };
28
29
  // Pool Constants
29
30
  exports.POOL = {
@@ -44,6 +45,7 @@ exports.SIMULATION = {
44
45
  MIN_TIME_BOUND: 0,
45
46
  MAX_TIME_BOUND_OFFSET_SECONDS: 120,
46
47
  MAX_FUTURE_TIME_BOUND_SECONDS: 120,
48
+ SIMULATION_AUTH_MODE: 'enforce',
47
49
  };
48
50
  // Polling for transactionWait
49
51
  exports.POLLING = {
@@ -2,13 +2,16 @@
2
2
  * fee.ts
3
3
  *
4
4
  * Static fee calculation for fee bump submissions matching launchtube.
5
- * - For Soroban transactions: use resourceFee + inclusion fee (BASE_FEE * 2 + 3)
5
+ * - For Soroban transactions: use resourceFee + inclusion fee
6
6
  * - For non-Soroban: use NON_SOROBAN_FEE + inclusion fee
7
- * - Limited contracts (from LIMITED_CONTRACTS env) get reduced fee (BASE_FEE * 2 + 1)
7
+ * - Limited contracts (from LIMITED_CONTRACTS env) get reduced fee
8
+ * - Inclusion fees are configurable via INCLUSION_FEE_DEFAULT and INCLUSION_FEE_LIMITED env vars
8
9
  */
9
10
  import { Transaction, xdr } from '@stellar/stellar-sdk';
10
- export declare const INCLUSION_FEE_DEFAULT: number;
11
- export declare const INCLUSION_FEE_LIMITED: number;
11
+ export interface InclusionFees {
12
+ inclusionFeeDefault: number;
13
+ inclusionFeeLimited: number;
14
+ }
12
15
  /**
13
16
  * Extract contract ID from a HostFunction (for func+auth flow)
14
17
  */
@@ -17,5 +20,5 @@ export declare function getContractIdFromFunc(func: xdr.HostFunction): string |
17
20
  * Extract contract ID from a Transaction (for XDR flow)
18
21
  */
19
22
  export declare function getContractIdFromTransaction(transaction: Transaction): string | undefined;
20
- export declare function calculateMaxFee(transaction: Transaction, limitedContracts?: Set<string>): number;
23
+ export declare function calculateMaxFee(transaction: Transaction, limitedContracts: Set<string>, fees: InclusionFees): number;
21
24
  //# sourceMappingURL=fee.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fee.d.ts","sourceRoot":"","sources":["../../src/plugin/fee.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAY,WAAW,EAAE,GAAG,EAAqB,MAAM,sBAAsB,CAAC;AAGrF,eAAO,MAAM,qBAAqB,QAA2B,CAAC;AAC9D,eAAO,MAAM,qBAAqB,QAA2B,CAAC;AAE9D;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,GAAG,CAAC,YAAY,GAAG,MAAM,GAAG,SAAS,CAUhF;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAYzF;AASD,wBAAgB,eAAe,CAAC,WAAW,EAAE,WAAW,EAAE,gBAAgB,GAAE,GAAG,CAAC,MAAM,CAAa,GAAG,MAAM,CAoB3G"}
1
+ {"version":3,"file":"fee.d.ts","sourceRoot":"","sources":["../../src/plugin/fee.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,GAAG,EAAqB,MAAM,sBAAsB,CAAC;AAG3E,MAAM,WAAW,aAAa;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,GAAG,CAAC,YAAY,GAAG,MAAM,GAAG,SAAS,CAUhF;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAYzF;AASD,wBAAgB,eAAe,CAAC,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,aAAa,GAAG,MAAM,CAoBpH"}
@@ -3,19 +3,17 @@
3
3
  * fee.ts
4
4
  *
5
5
  * Static fee calculation for fee bump submissions matching launchtube.
6
- * - For Soroban transactions: use resourceFee + inclusion fee (BASE_FEE * 2 + 3)
6
+ * - For Soroban transactions: use resourceFee + inclusion fee
7
7
  * - For non-Soroban: use NON_SOROBAN_FEE + inclusion fee
8
- * - Limited contracts (from LIMITED_CONTRACTS env) get reduced fee (BASE_FEE * 2 + 1)
8
+ * - Limited contracts (from LIMITED_CONTRACTS env) get reduced fee
9
+ * - Inclusion fees are configurable via INCLUSION_FEE_DEFAULT and INCLUSION_FEE_LIMITED env vars
9
10
  */
10
11
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.INCLUSION_FEE_LIMITED = exports.INCLUSION_FEE_DEFAULT = void 0;
12
12
  exports.getContractIdFromFunc = getContractIdFromFunc;
13
13
  exports.getContractIdFromTransaction = getContractIdFromTransaction;
14
14
  exports.calculateMaxFee = calculateMaxFee;
15
15
  const stellar_sdk_1 = require("@stellar/stellar-sdk");
16
16
  const constants_1 = require("./constants");
17
- exports.INCLUSION_FEE_DEFAULT = Number(stellar_sdk_1.BASE_FEE) * 2 + 3;
18
- exports.INCLUSION_FEE_LIMITED = Number(stellar_sdk_1.BASE_FEE) * 2 + 1;
19
17
  /**
20
18
  * Extract contract ID from a HostFunction (for func+auth flow)
21
19
  */
@@ -48,13 +46,13 @@ function getContractIdFromTransaction(transaction) {
48
46
  return undefined;
49
47
  }
50
48
  }
51
- function getInclusionFee(contractId, limitedContracts) {
49
+ function getInclusionFee(contractId, limitedContracts, fees) {
52
50
  if (contractId && limitedContracts.has(contractId)) {
53
- return exports.INCLUSION_FEE_LIMITED;
51
+ return fees.inclusionFeeLimited;
54
52
  }
55
- return exports.INCLUSION_FEE_DEFAULT;
53
+ return fees.inclusionFeeDefault;
56
54
  }
57
- function calculateMaxFee(transaction, limitedContracts = new Set()) {
55
+ function calculateMaxFee(transaction, limitedContracts, fees) {
58
56
  const envelope = transaction.toEnvelope();
59
57
  let resourceFee = 0n;
60
58
  if (envelope.switch() === stellar_sdk_1.xdr.EnvelopeType.envelopeTypeTx()) {
@@ -64,7 +62,7 @@ function calculateMaxFee(transaction, limitedContracts = new Set()) {
64
62
  }
65
63
  }
66
64
  const contractId = getContractIdFromTransaction(transaction);
67
- const inclusionFee = getInclusionFee(contractId, limitedContracts);
65
+ const inclusionFee = getInclusionFee(contractId, limitedContracts, fees);
68
66
  const fee = resourceFee > 0n ? resourceFee + BigInt(inclusionFee) : BigInt(constants_1.FEE.NON_SOROBAN_FEE + inclusionFee);
69
67
  console.debug(`[channels] Calculated max_fee: ${Number(fee)} stroops (resourceFee: ${resourceFee}, inclusionFee: ${inclusionFee})`);
70
68
  return Number(fee);
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/plugin/handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;AASvE,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAWxD;;;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;AAsOD;;GAEG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAElE"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/plugin/handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;AASvE,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AA0BxD;;;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;AAqPD;;GAEG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAElE"}
@@ -20,6 +20,7 @@ const simulation_1 = require("./simulation");
20
20
  const fee_1 = require("./fee");
21
21
  const tx_1 = require("./tx");
22
22
  const fee_tracking_1 = require("./fee-tracking");
23
+ const sequence_1 = require("./sequence");
23
24
  function getApiKey(headers, headerName) {
24
25
  const values = headers[headerName];
25
26
  return values?.[0]?.trim() || undefined;
@@ -44,8 +45,8 @@ function extractFuncAuthFromUnsignedXdr(tx) {
44
45
  auth: invokeHostFn.auth(),
45
46
  };
46
47
  }
47
- async function handleXdrSubmit(xdrStr, fundRelayer, fundAddress, network, networkPassphrase, api, pool, acquireOptions, tracker) {
48
- const tx = new stellar_sdk_1.Transaction(xdrStr, networkPassphrase);
48
+ async function handleXdrSubmit(xdrStr, ctx) {
49
+ const tx = new stellar_sdk_1.Transaction(xdrStr, ctx.networkPassphrase);
49
50
  // Unsigned XDR: extract func+auth and route through channel path
50
51
  if (tx.signatures.length === 0) {
51
52
  const extracted = extractFuncAuthFromUnsignedXdr(tx);
@@ -62,17 +63,22 @@ async function handleXdrSubmit(xdrStr, fundRelayer, fundAddress, network, networ
62
63
  console.log(`[channels] Unsigned XDR detected, extracting func+auth and routing through channel path`);
63
64
  // Update acquireOptions with contractId from extracted func
64
65
  const contractId = (0, fee_1.getContractIdFromFunc)(extracted.func);
65
- const updatedOptions = { ...acquireOptions, contractId };
66
- return handleFuncAuthSubmit(extracted.func, extracted.auth, api, pool, fundRelayer, fundAddress, network, networkPassphrase, updatedOptions, tracker);
66
+ const updatedOptions = { ...ctx.acquireOptions, contractId };
67
+ return handleFuncAuthSubmit(extracted.func, extracted.auth, { ...ctx, acquireOptions: updatedOptions });
67
68
  }
68
69
  const validated = (0, tx_1.validateExistingTransactionForSubmitOnly)(tx);
69
- const maxFee = (0, fee_1.calculateMaxFee)(validated, acquireOptions.limitedContracts);
70
- await tracker?.checkBudget(maxFee);
71
- return (0, submit_1.submitWithFeeBumpAndWait)(fundRelayer, validated.toXDR(), network, maxFee, api, tracker);
70
+ const maxFee = (0, fee_1.calculateMaxFee)(validated, ctx.acquireOptions.limitedContracts, ctx.fees);
71
+ const contractId = (0, fee_1.getContractIdFromTransaction)(validated);
72
+ await ctx.tracker?.checkBudget(maxFee);
73
+ const submitContext = {
74
+ contractId,
75
+ isLimited: contractId ? ctx.acquireOptions.limitedContracts.has(contractId) : false,
76
+ };
77
+ return (0, submit_1.submitWithFeeBumpAndWait)(ctx.fundRelayer, validated.toXDR(), ctx.network, maxFee, ctx.api, ctx.tracker, submitContext);
72
78
  }
73
- async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddress, network, networkPassphrase, acquireOptions, tracker) {
79
+ async function handleFuncAuthSubmit(func, auth, ctx) {
74
80
  // Simulate once — used for both read-only detection and transaction assembly
75
- const simulation = await (0, simulation_1.simulateTransaction)(func, auth, fundAddress, fundRelayer, networkPassphrase);
81
+ const simulation = await (0, simulation_1.simulateTransaction)(func, auth, ctx.fundAddress, ctx.fundRelayer, ctx.networkPassphrase);
76
82
  if (simulation.isReadOnly) {
77
83
  console.log(`[channels] Read-only call detected, returning simulation result`);
78
84
  return {
@@ -85,8 +91,8 @@ async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddr
85
91
  }
86
92
  let poolLock;
87
93
  try {
88
- poolLock = await pool.acquire(acquireOptions);
89
- const channelRelayer = api.useRelayer(poolLock.relayerId);
94
+ poolLock = await ctx.pool.acquire(ctx.acquireOptions);
95
+ const channelRelayer = ctx.api.useRelayer(poolLock.relayerId);
90
96
  const channelInfo = await channelRelayer.getRelayer();
91
97
  console.log(`[channels] Acquired channel: ${poolLock.relayerId}`);
92
98
  if (!channelInfo || !channelInfo.address) {
@@ -96,24 +102,42 @@ async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddr
96
102
  details: { relayerId: poolLock.relayerId },
97
103
  });
98
104
  }
99
- const channelStatus = await channelRelayer.getRelayerStatus();
100
- if (channelStatus.network_type !== 'stellar') {
105
+ if (channelInfo.network_type !== 'stellar') {
101
106
  throw (0, relayer_sdk_1.pluginError)('Channel relayer network type must be stellar', {
102
107
  code: 'UNSUPPORTED_NETWORK',
103
108
  status: constants_1.HTTP_STATUS.BAD_REQUEST,
104
- details: { network_type: channelStatus.network_type, relayerId: poolLock.relayerId },
109
+ details: { network_type: channelInfo.network_type, relayerId: poolLock.relayerId },
105
110
  });
106
111
  }
112
+ const sequence = await (0, sequence_1.getSequence)(ctx.kv, ctx.network, channelRelayer, channelInfo.address, ctx.sequenceNumberCacheMaxAgeMs);
107
113
  // Assemble the transaction using the cached simulation result — no second RPC call
108
- const built = (0, simulation_1.buildWithChannel)(func, auth, { address: channelInfo.address, sequence: channelStatus.sequence_number }, networkPassphrase, simulation.rawSimResult);
109
- const signedTx = await (0, submit_1.signWithChannelAndFund)(built, channelRelayer, fundRelayer, channelInfo.address, fundAddress, networkPassphrase);
110
- const maxFee = (0, fee_1.calculateMaxFee)(signedTx, acquireOptions.limitedContracts);
111
- await tracker?.checkBudget(maxFee);
112
- return await (0, submit_1.submitWithFeeBumpAndWait)(fundRelayer, signedTx.toXDR(), network, maxFee, api, tracker);
114
+ const built = (0, simulation_1.buildWithChannel)(func, auth, { address: channelInfo.address, sequence }, ctx.networkPassphrase, simulation.rawSimResult);
115
+ const signedTx = await (0, submit_1.signWithChannelAndFund)(built, channelRelayer, ctx.fundRelayer, channelInfo.address, ctx.fundAddress, ctx.networkPassphrase);
116
+ const maxFee = (0, fee_1.calculateMaxFee)(signedTx, ctx.acquireOptions.limitedContracts, ctx.fees);
117
+ const contractId = (0, fee_1.getContractIdFromFunc)(func);
118
+ await ctx.tracker?.checkBudget(maxFee);
119
+ const submitContext = {
120
+ contractId,
121
+ isLimited: contractId ? ctx.acquireOptions.limitedContracts.has(contractId) : false,
122
+ };
123
+ try {
124
+ const result = await (0, submit_1.submitWithFeeBumpAndWait)(ctx.fundRelayer, signedTx.toXDR(), ctx.network, maxFee, ctx.api, ctx.tracker, submitContext);
125
+ if (result.status === 'confirmed') {
126
+ await (0, sequence_1.commitSequence)(ctx.kv, ctx.network, channelInfo.address, sequence);
127
+ }
128
+ else {
129
+ await (0, sequence_1.clearSequence)(ctx.kv, ctx.network, channelInfo.address);
130
+ }
131
+ return result;
132
+ }
133
+ catch (error) {
134
+ await (0, sequence_1.clearSequence)(ctx.kv, ctx.network, channelInfo.address);
135
+ throw error;
136
+ }
113
137
  }
114
138
  finally {
115
139
  if (poolLock) {
116
- await pool.release(poolLock);
140
+ await ctx.pool.release(poolLock);
117
141
  }
118
142
  }
119
143
  }
@@ -172,16 +196,34 @@ async function channelAccounts(context) {
172
196
  limitedContracts: config.limitedContracts,
173
197
  capacityRatio: config.contractCapacityRatio,
174
198
  };
175
- // 4. Branch by request type
199
+ const fees = {
200
+ inclusionFeeDefault: config.inclusionFeeDefault,
201
+ inclusionFeeLimited: config.inclusionFeeLimited,
202
+ };
203
+ // 4. Build pipeline context
204
+ const ctx = {
205
+ api,
206
+ kv,
207
+ pool,
208
+ fundRelayer: fundRelayer,
209
+ fundAddress: fundInfo.address,
210
+ network: config.network,
211
+ networkPassphrase,
212
+ acquireOptions,
213
+ fees,
214
+ tracker,
215
+ sequenceNumberCacheMaxAgeMs: config.sequenceNumberCacheMaxAgeMs,
216
+ };
217
+ // 5. Branch by request type
176
218
  if (request.type === 'xdr') {
177
219
  console.log(`[channels] Flow: XDR submit-only`);
178
- return await handleXdrSubmit(request.xdr, fundRelayer, fundInfo.address, config.network, networkPassphrase, api, pool, acquireOptions, tracker);
220
+ return await handleXdrSubmit(request.xdr, ctx);
179
221
  }
180
222
  // Extract contractId for func+auth flow
181
223
  const contractId = (0, fee_1.getContractIdFromFunc)(request.func);
182
224
  const funcAcquireOptions = { ...acquireOptions, contractId };
183
225
  console.log(`[channels] Flow: func+auth with channel account`);
184
- return await handleFuncAuthSubmit(request.func, request.auth, api, pool, fundRelayer, fundInfo.address, config.network, networkPassphrase, funcAcquireOptions, tracker);
226
+ return await handleFuncAuthSubmit(request.func, request.auth, { ...ctx, acquireOptions: funcAcquireOptions });
185
227
  }
186
228
  /**
187
229
  * Main plugin handler exported for OpenZeppelin Relayer
@@ -1 +1 @@
1
- {"version":3,"file":"management.d.ts","sourceRoot":"","sources":["../../src/plugin/management.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,2BAA2B,CAAC;AAM9E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAIxD;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAkC3E"}
1
+ {"version":3,"file":"management.d.ts","sourceRoot":"","sources":["../../src/plugin/management.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,2BAA2B,CAAC;AAO9E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAIxD;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAoC3E"}
@@ -48,6 +48,8 @@ async function handleManagement(context) {
48
48
  return await setFeeLimit(kv, config.network, m);
49
49
  case 'deleteFeeLimit':
50
50
  return await deleteFeeLimit(kv, config.network, m);
51
+ case 'stats':
52
+ return await getPoolStats(config, kv);
51
53
  default:
52
54
  throw (0, relayer_sdk_1.pluginError)('Invalid management action', { code: 'INVALID_ACTION', status: constants_1.HTTP_STATUS.BAD_REQUEST });
53
55
  }
@@ -194,6 +196,52 @@ async function setChannelAccounts(kv, network, payload) {
194
196
  });
195
197
  }
196
198
  }
199
+ async function getPoolStats(config, kv) {
200
+ const { network } = config;
201
+ // 1. Get relayer IDs (1 KV call)
202
+ const key = `${network}:channel:relayer-ids`;
203
+ let relayerIds;
204
+ try {
205
+ const doc = await kv.get?.(key);
206
+ relayerIds = Array.isArray(doc?.relayerIds) ? doc.relayerIds : [];
207
+ }
208
+ catch (e) {
209
+ throw (0, relayer_sdk_1.pluginError)('KV error while reading pool stats', {
210
+ code: 'KV_ERROR',
211
+ status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
212
+ });
213
+ }
214
+ // 2. Check all locks in parallel (N KV calls) — best-effort
215
+ let locked;
216
+ let available;
217
+ try {
218
+ const results = await Promise.all(relayerIds.map((id) => kv.exists(`${network}:channel:in-use:${id}`)));
219
+ locked = results.filter(Boolean).length;
220
+ available = relayerIds.length - locked;
221
+ }
222
+ catch {
223
+ // Non-fatal: return pool size without lock info
224
+ }
225
+ return {
226
+ pool: {
227
+ size: relayerIds.length,
228
+ locked,
229
+ available,
230
+ },
231
+ config: {
232
+ network,
233
+ lockTtlSeconds: config.lockTtlSeconds,
234
+ feeLimit: config.feeLimit,
235
+ feeResetPeriodSeconds: config.feeResetPeriodMs ? config.feeResetPeriodMs / 1000 : undefined,
236
+ contractCapacityRatio: config.contractCapacityRatio,
237
+ limitedContracts: Array.from(config.limitedContracts),
238
+ },
239
+ fees: {
240
+ inclusionFeeDefault: config.inclusionFeeDefault,
241
+ inclusionFeeLimited: config.inclusionFeeLimited,
242
+ },
243
+ };
244
+ }
197
245
  function normalizeId(id) {
198
246
  return String(id).trim().toLowerCase();
199
247
  }
@@ -32,5 +32,6 @@ export declare class ChannelPool {
32
32
  private membershipKey;
33
33
  private lockKey;
34
34
  private getRelayerIdsFromKV;
35
+ private getPoolCapacityDetails;
35
36
  }
36
37
  //# sourceMappingURL=pool.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../src/plugin/pool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;AAIvE,MAAM,MAAM,QAAQ,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAIF,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgB;gBAEvB,OAAO,EAAE,SAAS,GAAG,SAAS,EAAE,EAAE,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM;IAQrF,4CAA4C;IACtC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC;YA4B3C,eAAe;YAKf,iBAAiB;IA2B/B,oCAAoC;IAC9B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAY5C,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,OAAO;YAID,mBAAmB;CAWlC"}
1
+ {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../src/plugin/pool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;AAIvE,MAAM,MAAM,QAAQ,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAUF,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgB;gBAEvB,OAAO,EAAE,SAAS,GAAG,SAAS,EAAE,EAAE,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM;IAQrF,4CAA4C;IACtC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC;YAyB3C,eAAe;YAKf,iBAAiB;IA2B/B,oCAAoC;IAC9B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAY5C,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,OAAO;YAID,mBAAmB;YAYnB,sBAAsB;CAUrC"}
@@ -26,7 +26,6 @@ class ChannelPool {
26
26
  /** Acquire a relayerId with a token lock */
27
27
  async acquire(options) {
28
28
  const maxSpins = constants_1.POOL.MUTEX_MAX_SPINS;
29
- const isLimited = options.contractId && options.limitedContracts.has(options.contractId);
30
29
  for (let i = 0; i < maxSpins; i++) {
31
30
  const r = await this.withGlobalMutex(() => this.tryLockAnyRelayer(options));
32
31
  if (r === null) {
@@ -36,12 +35,12 @@ class ChannelPool {
36
35
  }
37
36
  return r;
38
37
  }
39
- if (isLimited) {
40
- console.log(`[channels] Contract ${options.contractId} limited to ${Math.round(options.capacityRatio * 100)}% capacity - no channels available`);
41
- }
38
+ const diagnostics = await this.getPoolCapacityDetails(options, maxSpins);
39
+ console.warn('[channels] Pool capacity exhausted', diagnostics);
42
40
  throw (0, relayer_sdk_1.pluginError)('Too many transactions queued. Please try again later', {
43
41
  code: 'POOL_CAPACITY',
44
42
  status: constants_1.HTTP_STATUS.SERVICE_UNAVAILABLE,
43
+ details: diagnostics,
45
44
  });
46
45
  }
47
46
  // Run a function under the short-lived global mutex; returns null if busy
@@ -106,6 +105,15 @@ class ChannelPool {
106
105
  return [];
107
106
  }
108
107
  }
108
+ async getPoolCapacityDetails(options, maxSpins) {
109
+ const isLimited = !!(options.contractId && options.limitedContracts.has(options.contractId));
110
+ return {
111
+ reason: isLimited ? 'limited_contract_capacity' : 'all_channels_busy_or_mutex_contention',
112
+ contractId: options.contractId,
113
+ capacityRatio: options.capacityRatio,
114
+ maxSpins,
115
+ };
116
+ }
109
117
  }
110
118
  exports.ChannelPool = ChannelPool;
111
119
  function shuffle(arr) {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * sequence.ts
3
+ *
4
+ * KV-based sequence number cache for channel accounts.
5
+ * After a transaction is confirmed on-chain, the RPC `getLedgerEntries` can
6
+ * still return the old (pre-increment) sequence number due to read-after-write
7
+ * lag. Caching the next expected sequence in KV avoids `tx_bad_seq` errors.
8
+ */
9
+ import type { PluginKVStore, Relayer } from '@openzeppelin/relayer-sdk';
10
+ /**
11
+ * Fetch the current sequence number for an account directly from chain
12
+ * via `getLedgerEntries` RPC. Throws on any RPC or decoding failure.
13
+ */
14
+ export declare function getAccountSequence(relayer: Relayer, address: string): Promise<string>;
15
+ /**
16
+ * Get the current sequence number for a channel account.
17
+ * Reads from KV cache first; falls back to chain via `getAccountSequence`.
18
+ */
19
+ export declare function getSequence(kv: PluginKVStore, network: string, relayer: Relayer, address: string, sequenceNumberCacheMaxAgeMs: number): Promise<string>;
20
+ /**
21
+ * Store the next expected sequence number in KV after a transaction
22
+ * has been confirmed on-chain.
23
+ */
24
+ export declare function commitSequence(kv: PluginKVStore, network: string, address: string, usedSequence: string): Promise<void>;
25
+ /**
26
+ * Clear cached sequence — forces a re-fetch from chain on next request.
27
+ * Used when transaction outcome is uncertain (e.g. timeout).
28
+ */
29
+ export declare function clearSequence(kv: PluginKVStore, network: string, address: string): Promise<void>;
30
+ //# sourceMappingURL=sequence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sequence.d.ts","sourceRoot":"","sources":["../../src/plugin/sequence.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAwBxE;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsH3F;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,aAAa,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,2BAA2B,EAAE,MAAM,GAClC,OAAO,CAAC,MAAM,CAAC,CAwBjB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,aAAa,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAaf;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWtG"}
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ /**
3
+ * sequence.ts
4
+ *
5
+ * KV-based sequence number cache for channel accounts.
6
+ * After a transaction is confirmed on-chain, the RPC `getLedgerEntries` can
7
+ * still return the old (pre-increment) sequence number due to read-after-write
8
+ * lag. Caching the next expected sequence in KV avoids `tx_bad_seq` errors.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.getAccountSequence = getAccountSequence;
12
+ exports.getSequence = getSequence;
13
+ exports.commitSequence = commitSequence;
14
+ exports.clearSequence = clearSequence;
15
+ const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
16
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
17
+ const constants_1 = require("./constants");
18
+ function seqKey(network, address) {
19
+ return `${network}:channel:seq:${address}`;
20
+ }
21
+ /**
22
+ * Validates that a sequence number string is a non-negative integer.
23
+ * Returns true for strings like "0", "42", "123456789012345".
24
+ */
25
+ function isValidSequence(seq) {
26
+ return /^\d+$/.test(seq);
27
+ }
28
+ /**
29
+ * Fetch the current sequence number for an account directly from chain
30
+ * via `getLedgerEntries` RPC. Throws on any RPC or decoding failure.
31
+ */
32
+ async function getAccountSequence(relayer, address) {
33
+ let accountKey;
34
+ try {
35
+ accountKey = stellar_sdk_1.xdr.LedgerKey.account(new stellar_sdk_1.xdr.LedgerKeyAccount({
36
+ accountId: stellar_sdk_1.Keypair.fromPublicKey(address).xdrPublicKey(),
37
+ }));
38
+ }
39
+ catch (error) {
40
+ console.error('[channels] Sequence fetch failed', {
41
+ event: 'invalid_channel_account_address',
42
+ code: 'FAILED_TO_GET_SEQUENCE',
43
+ address,
44
+ message: error instanceof Error ? error.message : String(error),
45
+ });
46
+ throw (0, relayer_sdk_1.pluginError)('Invalid channel account address', {
47
+ code: 'FAILED_TO_GET_SEQUENCE',
48
+ status: constants_1.HTTP_STATUS.BAD_GATEWAY,
49
+ details: { address, message: error instanceof Error ? error.message : String(error) },
50
+ });
51
+ }
52
+ let response;
53
+ try {
54
+ response = await relayer.rpc({
55
+ jsonrpc: '2.0',
56
+ id: Math.floor(Math.random() * 1e8).toString(),
57
+ method: 'getLedgerEntries',
58
+ params: {
59
+ keys: [accountKey.toXDR('base64')],
60
+ },
61
+ });
62
+ }
63
+ catch (error) {
64
+ console.error('[channels] Sequence fetch failed', {
65
+ event: 'sequence_rpc_request_failed',
66
+ code: 'FAILED_TO_GET_SEQUENCE',
67
+ address,
68
+ message: error instanceof Error ? error.message : String(error),
69
+ });
70
+ throw (0, relayer_sdk_1.pluginError)('Failed to get sequence from channel relayer', {
71
+ code: 'FAILED_TO_GET_SEQUENCE',
72
+ status: constants_1.HTTP_STATUS.BAD_GATEWAY,
73
+ details: { message: error instanceof Error ? error.message : String(error) },
74
+ });
75
+ }
76
+ if (response.error) {
77
+ console.error('[channels] Sequence fetch failed', {
78
+ event: 'sequence_rpc_error_response',
79
+ code: 'FAILED_TO_GET_SEQUENCE',
80
+ address,
81
+ message: response.error.message,
82
+ });
83
+ throw (0, relayer_sdk_1.pluginError)('Failed to get sequence from channel relayer', {
84
+ code: 'FAILED_TO_GET_SEQUENCE',
85
+ status: constants_1.HTTP_STATUS.BAD_GATEWAY,
86
+ details: { message: response.error.message },
87
+ });
88
+ }
89
+ const result = response.result;
90
+ const entries = result?.entries;
91
+ if (!Array.isArray(entries)) {
92
+ console.error('[channels] Sequence fetch failed', {
93
+ event: 'sequence_rpc_invalid_result_shape',
94
+ code: 'FAILED_TO_GET_SEQUENCE',
95
+ address,
96
+ });
97
+ throw (0, relayer_sdk_1.pluginError)('Invalid RPC response for account sequence', {
98
+ code: 'FAILED_TO_GET_SEQUENCE',
99
+ status: constants_1.HTTP_STATUS.BAD_GATEWAY,
100
+ details: { address },
101
+ });
102
+ }
103
+ if (!entries || entries.length === 0) {
104
+ console.warn('[channels] Sequence fetch returned no account entries', {
105
+ event: 'sequence_account_not_found',
106
+ code: 'ACCOUNT_NOT_FOUND',
107
+ address,
108
+ });
109
+ throw (0, relayer_sdk_1.pluginError)('Channel account not found on ledger', {
110
+ code: 'ACCOUNT_NOT_FOUND',
111
+ status: constants_1.HTTP_STATUS.BAD_GATEWAY,
112
+ details: { address },
113
+ });
114
+ }
115
+ const firstEntryXdr = entries[0]?.xdr;
116
+ if (typeof firstEntryXdr !== 'string') {
117
+ console.error('[channels] Sequence fetch failed', {
118
+ event: 'sequence_rpc_invalid_entry_xdr',
119
+ code: 'FAILED_TO_GET_SEQUENCE',
120
+ address,
121
+ });
122
+ throw (0, relayer_sdk_1.pluginError)('Invalid RPC response for account sequence', {
123
+ code: 'FAILED_TO_GET_SEQUENCE',
124
+ status: constants_1.HTTP_STATUS.BAD_GATEWAY,
125
+ details: { address },
126
+ });
127
+ }
128
+ try {
129
+ const accountEntry = stellar_sdk_1.xdr.LedgerEntryData.fromXDR(firstEntryXdr, 'base64');
130
+ return accountEntry.account().seqNum().toString();
131
+ }
132
+ catch (error) {
133
+ console.error('[channels] Sequence fetch failed', {
134
+ event: 'sequence_xdr_decode_failed',
135
+ code: 'FAILED_TO_GET_SEQUENCE',
136
+ address,
137
+ message: error instanceof Error ? error.message : String(error),
138
+ });
139
+ throw (0, relayer_sdk_1.pluginError)('Failed to decode account sequence from ledger entry', {
140
+ code: 'FAILED_TO_GET_SEQUENCE',
141
+ status: constants_1.HTTP_STATUS.BAD_GATEWAY,
142
+ details: { address, message: error instanceof Error ? error.message : String(error) },
143
+ });
144
+ }
145
+ }
146
+ /**
147
+ * Get the current sequence number for a channel account.
148
+ * Reads from KV cache first; falls back to chain via `getAccountSequence`.
149
+ */
150
+ async function getSequence(kv, network, relayer, address, sequenceNumberCacheMaxAgeMs) {
151
+ const key = seqKey(network, address);
152
+ try {
153
+ const cached = await kv.get(key);
154
+ if (cached?.sequence) {
155
+ if (!isValidSequence(cached.sequence)) {
156
+ console.warn(`[channels] Sequence cache invalid: address=${address}, value=${cached.sequence}, refetching`);
157
+ }
158
+ else {
159
+ const age = Date.now() - (cached.storedAt ?? 0);
160
+ if (age < sequenceNumberCacheMaxAgeMs) {
161
+ console.debug(`[channels] Sequence cache hit: address=${address}, seq=${cached.sequence}, age=${age}ms`);
162
+ return cached.sequence;
163
+ }
164
+ console.debug(`[channels] Sequence cache stale: address=${address}, age=${age}ms, refetching`);
165
+ }
166
+ }
167
+ }
168
+ catch (err) {
169
+ console.warn(`[channels] Failed to read sequence from KV, falling back to chain`, {
170
+ address,
171
+ key,
172
+ error: err instanceof Error ? err.message : String(err),
173
+ });
174
+ }
175
+ return getAccountSequence(relayer, address);
176
+ }
177
+ /**
178
+ * Store the next expected sequence number in KV after a transaction
179
+ * has been confirmed on-chain.
180
+ */
181
+ async function commitSequence(kv, network, address, usedSequence) {
182
+ const next = (BigInt(usedSequence) + 1n).toString();
183
+ const key = seqKey(network, address);
184
+ try {
185
+ await kv.set(key, { sequence: next, storedAt: Date.now() });
186
+ console.debug(`[channels] Sequence committed: address=${address}, next=${next}`);
187
+ }
188
+ catch (err) {
189
+ console.warn(`[channels] Failed to commit sequence to KV`, {
190
+ address,
191
+ next,
192
+ error: err instanceof Error ? err.message : String(err),
193
+ });
194
+ }
195
+ }
196
+ /**
197
+ * Clear cached sequence — forces a re-fetch from chain on next request.
198
+ * Used when transaction outcome is uncertain (e.g. timeout).
199
+ */
200
+ async function clearSequence(kv, network, address) {
201
+ const key = seqKey(network, address);
202
+ try {
203
+ await kv.del(key);
204
+ console.debug(`[channels] Sequence cleared: address=${address}`);
205
+ }
206
+ catch (err) {
207
+ console.warn(`[channels] Failed to clear sequence from KV`, {
208
+ address,
209
+ error: err instanceof Error ? err.message : String(err),
210
+ });
211
+ }
212
+ }
@@ -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;AAED;;;;;;;;;;;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,CA8E3B;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,GAChD,WAAW,CAsCb;AACD,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,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,GAChD,WAAW,CAsCb;AACD,0FAA0F;AAC1F,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAe1D"}
@@ -41,7 +41,8 @@ async function simulateTransaction(func, auth, sourceAddress, relayer, networkPa
41
41
  jsonrpc: '2.0',
42
42
  id: Math.floor(Math.random() * 1e8).toString(),
43
43
  method: 'simulateTransaction',
44
- params: { transaction: transaction.toXDR() },
44
+ // Enforce mode validates auth entry signatures during simulation.
45
+ params: { transaction: transaction.toXDR(), authMode: constants_1.SIMULATION.SIMULATION_AUTH_MODE },
45
46
  });
46
47
  }
47
48
  catch (err) {
@@ -65,11 +66,13 @@ async function simulateTransaction(func, auth, sourceAddress, relayer, networkPa
65
66
  ...rpcResponse.result,
66
67
  };
67
68
  if ('error' in simResult && simResult.error) {
69
+ const parsedError = parseSimulationError(simResult.error);
70
+ const failure = classifySimulationFailure(simResult.error, parsedError);
68
71
  console.error(`[channels] Simulation error: ${simResult.error}`);
69
- throw (0, relayer_sdk_1.pluginError)('Simulation failed', {
70
- code: 'SIMULATION_FAILED',
72
+ throw (0, relayer_sdk_1.pluginError)(failure.message, {
73
+ code: failure.code,
71
74
  status: constants_1.HTTP_STATUS.BAD_REQUEST,
72
- details: { error: parseSimulationError(simResult.error) },
75
+ details: { error: parsedError, authMode: constants_1.SIMULATION.SIMULATION_AUTH_MODE },
73
76
  });
74
77
  }
75
78
  // Read-only detection
@@ -148,3 +151,25 @@ function parseSimulationError(error) {
148
151
  }
149
152
  return firstLine;
150
153
  }
154
+ function classifySimulationFailure(rawError, parsedError) {
155
+ const isEnforcedAuthValidation = constants_1.SIMULATION.SIMULATION_AUTH_MODE === 'enforce' &&
156
+ (/\bError\(Auth,/i.test(rawError) ||
157
+ /\brequire_auth\b/i.test(rawError) ||
158
+ /\binvalid\s+signature\b/i.test(rawError) ||
159
+ /\bsignature\s+has\s+expired\b/i.test(rawError) ||
160
+ /\bsignature\s+expired\b/i.test(rawError) ||
161
+ /\bsignature\s+verification\s+failed\b/i.test(rawError) ||
162
+ /\bbad[_\s]?signature\b/i.test(rawError) ||
163
+ /\btx_bad_auth\b/i.test(rawError) ||
164
+ /\bbad[_\s]?auth\b/i.test(rawError));
165
+ if (isEnforcedAuthValidation) {
166
+ return {
167
+ code: 'SIMULATION_SIGNED_AUTH_VALIDATION_FAILED',
168
+ message: `Signed auth entry validation failed in enforce simulation: ${parsedError}`,
169
+ };
170
+ }
171
+ return {
172
+ code: 'SIMULATION_FAILED',
173
+ message: 'Simulation failed',
174
+ };
175
+ }
@@ -7,6 +7,14 @@ import { Transaction } from '@stellar/stellar-sdk';
7
7
  import { Relayer, PluginAPI } from '@openzeppelin/relayer-sdk';
8
8
  import { ChannelAccountsResponse } from './types';
9
9
  import { FeeTracker } from './fee-tracking';
10
+ export interface SubmitContext {
11
+ contractId?: string;
12
+ isLimited?: boolean;
13
+ }
14
+ interface DecodedTransactionResult {
15
+ feeCharged: number;
16
+ resultCode: string;
17
+ }
10
18
  /**
11
19
  * Sign transaction with both channel and fund relayers
12
20
  * - First sign with channel account
@@ -17,7 +25,11 @@ export declare function signWithChannelAndFund(transaction: Transaction, channel
17
25
  /**
18
26
  * Submit transaction with fee bump and wait for confirmation
19
27
  */
20
- export declare function submitWithFeeBumpAndWait(fundRelayer: Relayer, signedXdr: string, network: 'testnet' | 'mainnet', maxFee: number, api: PluginAPI, tracker?: FeeTracker): Promise<ChannelAccountsResponse>;
28
+ export declare function submitWithFeeBumpAndWait(fundRelayer: Relayer, signedXdr: string, network: 'testnet' | 'mainnet', maxFee: number, api: PluginAPI, tracker?: FeeTracker, context?: SubmitContext): Promise<ChannelAccountsResponse>;
29
+ /** Try to decode a transaction result XDR from the reason string */
30
+ export declare function decodeTransactionResult(reason: string): DecodedTransactionResult | null;
31
+ export declare function buildStellarLabTransactionUrl(network: 'testnet' | 'mainnet', txHash: string): string;
21
32
  /** Strip provider wrapper text, extract last segment (e.g., "TxInsufficientBalance") */
22
33
  export declare function sanitizeReason(reason: string): string;
34
+ export {};
23
35
  //# sourceMappingURL=submit.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"submit.d.ts","sourceRoot":"","sources":["../../src/plugin/submit.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAEL,OAAO,EAGP,SAAS,EACV,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,OAAO,EACvB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,WAAW,CAAC,CAuBtB;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,WAAW,EAAE,OAAO,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,EACd,OAAO,CAAC,EAAE,UAAU,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAkElC;AASD,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOrD"}
1
+ {"version":3,"file":"submit.d.ts","sourceRoot":"","sources":["../../src/plugin/submit.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAO,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAEL,OAAO,EAGP,SAAS,EACV,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,wBAAwB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,OAAO,EACvB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,WAAW,CAAC,CAuBtB;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,WAAW,EAAE,OAAO,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,EACd,OAAO,CAAC,EAAE,UAAU,EACpB,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,uBAAuB,CAAC,CAmFlC;AASD,oEAAoE;AACpE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,wBAAwB,GAAG,IAAI,CAkCvF;AAeD,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAepG;AAED,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOrD"}
@@ -7,6 +7,8 @@
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.signWithChannelAndFund = signWithChannelAndFund;
9
9
  exports.submitWithFeeBumpAndWait = submitWithFeeBumpAndWait;
10
+ exports.decodeTransactionResult = decodeTransactionResult;
11
+ exports.buildStellarLabTransactionUrl = buildStellarLabTransactionUrl;
10
12
  exports.sanitizeReason = sanitizeReason;
11
13
  const stellar_sdk_1 = require("@stellar/stellar-sdk");
12
14
  const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
@@ -40,7 +42,7 @@ async function signWithChannelAndFund(transaction, channelRelayer, _fundRelayer,
40
42
  /**
41
43
  * Submit transaction with fee bump and wait for confirmation
42
44
  */
43
- async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee, api, tracker) {
45
+ async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee, api, tracker, context) {
44
46
  // Submit with fee bump
45
47
  console.debug(`[channels] Sending fee bump tx: network=${network}, maxFee=${maxFee}, xdr_len=${signedXdr.length}`);
46
48
  const payload = {
@@ -64,9 +66,25 @@ async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee,
64
66
  await tracker.recordUsage(maxFee);
65
67
  }
66
68
  const rawReason = final.status_reason || 'Transaction failed';
67
- console.error(`[channels] Transaction failed: ${rawReason}`);
69
+ const decoded = decodeTransactionResult(rawReason);
70
+ const labUrl = final.hash ? buildStellarLabTransactionUrl(network, final.hash) : null;
71
+ const contractType = context?.isLimited ? 'limited' : 'default';
72
+ const base = `[channels] Transaction failed: contractId=${context?.contractId ?? 'unknown'}, contractType=${contractType}, maxFee=${maxFee}`;
73
+ if (decoded && isTxInsufficientFeeError(decoded.resultCode)) {
74
+ const feeInfo = decoded.feeCharged != null
75
+ ? `, requiredFee=${decoded.feeCharged}, shortfall=${decoded.feeCharged - maxFee}`
76
+ : '';
77
+ console.error(`${base}, reason=txInsufficientFee${feeInfo}`);
78
+ }
79
+ else if (decoded) {
80
+ console.error(`${base}, reason=${decoded.resultCode}`);
81
+ }
82
+ else {
83
+ console.error(`${base}, reason=${rawReason}`);
84
+ }
68
85
  const reason = sanitizeReason(rawReason);
69
- throw (0, relayer_sdk_1.pluginError)(reason, {
86
+ const reasonWithLab = labUrl ? `${reason}. Debug in Stellar Lab (click "Load Transaction"): ${labUrl}` : reason;
87
+ throw (0, relayer_sdk_1.pluginError)(reasonWithLab, {
70
88
  code: 'ONCHAIN_FAILED',
71
89
  status: constants_1.HTTP_STATUS.BAD_REQUEST,
72
90
  details: {
@@ -74,6 +92,8 @@ async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee,
74
92
  reason,
75
93
  id: final.id,
76
94
  hash: final.hash ?? null,
95
+ resultCode: decoded?.resultCode ?? null,
96
+ labUrl: labUrl ? `Debug this failure in Stellar Lab (click "Load Transaction"): ${labUrl}` : null,
77
97
  },
78
98
  });
79
99
  }
@@ -109,6 +129,67 @@ async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee,
109
129
  function isSignTransactionResponseStellar(data) {
110
130
  return data !== null && typeof data === 'object' && 'signature' in data && 'signedXdr' in data;
111
131
  }
132
+ /** Try to decode a transaction result XDR from the reason string */
133
+ function decodeTransactionResult(reason) {
134
+ const fromReasonText = extractResultCodeFromReasonText(reason);
135
+ if (fromReasonText) {
136
+ return {
137
+ feeCharged: 0,
138
+ resultCode: fromReasonText,
139
+ };
140
+ }
141
+ try {
142
+ const match = reason.match(/([A-Za-z0-9+/=]{20,})$/);
143
+ if (!match)
144
+ return null;
145
+ const result = stellar_sdk_1.xdr.TransactionResult.fromXDR(match[1], 'base64');
146
+ const outerResultCode = String(result.result().switch().name);
147
+ let resultCode = outerResultCode;
148
+ // Unwrap fee bump inner failure to get the actual result code
149
+ if (outerResultCode === 'txFeeBumpInnerFailed') {
150
+ try {
151
+ const innerResult = result.result().innerResultPair().result();
152
+ const innerResultCode = String(innerResult.result().switch().name);
153
+ resultCode = `${outerResultCode}:${innerResultCode}`;
154
+ }
155
+ catch {
156
+ // keep outer result code if unwrap fails
157
+ }
158
+ }
159
+ return {
160
+ feeCharged: Number(result.feeCharged().toBigInt()),
161
+ resultCode,
162
+ };
163
+ }
164
+ catch {
165
+ return null;
166
+ }
167
+ }
168
+ function extractResultCodeFromReasonText(reason) {
169
+ const outerMatch = reason.match(/\bSpecific XDR reason:\s*([A-Za-z0-9_]+)\b/i);
170
+ if (!outerMatch?.[1])
171
+ return null;
172
+ const innerMatch = reason.match(/\bInner result:\s*([A-Za-z0-9_]+)\b/i);
173
+ return innerMatch?.[1] ? `${outerMatch[1]}:${innerMatch[1]}` : outerMatch[1];
174
+ }
175
+ /** Check if the result code indicates an insufficient fee error (case-insensitive). */
176
+ function isTxInsufficientFeeError(resultCode) {
177
+ return resultCode?.toLowerCase() === 'txinsufficientfee';
178
+ }
179
+ function buildStellarLabTransactionUrl(network, txHash) {
180
+ const isMainnet = network === 'mainnet';
181
+ const networkId = isMainnet ? 'mainnet' : 'testnet';
182
+ const label = isMainnet ? 'Mainnet' : 'Testnet';
183
+ const horizonUrl = isMainnet ? 'https://horizon.stellar.org' : 'https://horizon-testnet.stellar.org';
184
+ const rpcUrl = isMainnet ? 'https://mainnet.sorobanrpc.com' : 'https://soroban-testnet.stellar.org';
185
+ const passphrase = isMainnet ? 'Public Global Stellar Network ; September 2015' : 'Test SDF Network ; September 2015';
186
+ // Stellar Lab expects protocol values encoded as https://// in query params.
187
+ // txHash is intentionally left unencoded because it is a hex string.
188
+ const horizonParam = horizonUrl.replace('https://', 'https:////');
189
+ const rpcParam = rpcUrl.replace('https://', 'https:////');
190
+ const passphraseParam = passphrase.replace(/ /g, '%20').replace(/;/g, '%3B');
191
+ return `https://lab.stellar.org/transaction/dashboard?$=network$id=${networkId}&label=${label}&horizonUrl=${horizonParam}&rpcUrl=${rpcParam}&passphrase=${passphraseParam}&txDashboard$transactionHash=${txHash};;`;
192
+ }
112
193
  /** Strip provider wrapper text, extract last segment (e.g., "TxInsufficientBalance") */
113
194
  function sanitizeReason(reason) {
114
195
  const segments = reason.split(/:\s*/);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openzeppelin/relayer-plugin-channels",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "OpenZeppelin Relayer Plugin for Stellar Channel Accounts",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",