@openzeppelin/relayer-plugin-channels 0.6.0 → 0.7.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.
Files changed (50) hide show
  1. package/README.md +79 -43
  2. package/dist/client/channels-client.d.ts +1 -1
  3. package/dist/client/channels-client.d.ts.map +1 -1
  4. package/dist/client/channels-client.js +20 -21
  5. package/dist/client/errors.d.ts +1 -1
  6. package/dist/client/errors.js +12 -9
  7. package/dist/client/index.d.ts +3 -3
  8. package/dist/client/index.d.ts.map +1 -1
  9. package/dist/client/types.d.ts +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/plugin/build.d.ts +1 -1
  12. package/dist/plugin/build.d.ts.map +1 -1
  13. package/dist/plugin/build.js +11 -17
  14. package/dist/plugin/config.d.ts +4 -2
  15. package/dist/plugin/config.d.ts.map +1 -1
  16. package/dist/plugin/config.js +43 -13
  17. package/dist/plugin/constants.d.ts +1 -0
  18. package/dist/plugin/constants.d.ts.map +1 -1
  19. package/dist/plugin/constants.js +2 -1
  20. package/dist/plugin/fee-tracking.d.ts +2 -2
  21. package/dist/plugin/fee-tracking.d.ts.map +1 -1
  22. package/dist/plugin/fee-tracking.js +4 -9
  23. package/dist/plugin/fee.d.ts +12 -5
  24. package/dist/plugin/fee.d.ts.map +1 -1
  25. package/dist/plugin/fee.js +39 -23
  26. package/dist/plugin/handler.d.ts +2 -2
  27. package/dist/plugin/handler.d.ts.map +1 -1
  28. package/dist/plugin/handler.js +38 -33
  29. package/dist/plugin/index.d.ts +1 -1
  30. package/dist/plugin/management.d.ts +1 -1
  31. package/dist/plugin/management.d.ts.map +1 -1
  32. package/dist/plugin/management.js +53 -79
  33. package/dist/plugin/pool.d.ts +8 -3
  34. package/dist/plugin/pool.d.ts.map +1 -1
  35. package/dist/plugin/pool.js +45 -17
  36. package/dist/plugin/simulation.d.ts +2 -2
  37. package/dist/plugin/simulation.d.ts.map +1 -1
  38. package/dist/plugin/simulation.js +17 -28
  39. package/dist/plugin/submit.d.ts +5 -5
  40. package/dist/plugin/submit.d.ts.map +1 -1
  41. package/dist/plugin/submit.js +11 -14
  42. package/dist/plugin/tx.d.ts +1 -1
  43. package/dist/plugin/tx.d.ts.map +1 -1
  44. package/dist/plugin/tx.js +6 -8
  45. package/dist/plugin/types.d.ts +4 -4
  46. package/dist/plugin/types.d.ts.map +1 -1
  47. package/dist/plugin/validation.d.ts +1 -1
  48. package/dist/plugin/validation.d.ts.map +1 -1
  49. package/dist/plugin/validation.js +26 -27
  50. package/package.json +1 -1
@@ -18,95 +18,78 @@ const config_1 = require("./config");
18
18
  const constants_1 = require("./constants");
19
19
  const fee_tracking_1 = require("./fee-tracking");
20
20
  function isManagementRequest(params) {
21
- return Boolean(params &&
22
- typeof params === "object" &&
23
- params.management &&
24
- typeof params.management === "object");
21
+ return Boolean(params && typeof params === 'object' && params.management && typeof params.management === 'object');
25
22
  }
26
23
  async function handleManagement(context) {
27
24
  const { kv, params } = context;
28
25
  const config = (0, config_1.loadConfig)();
29
26
  if (!config.adminSecret) {
30
- throw (0, relayer_sdk_1.pluginError)("Management API disabled", {
31
- code: "MANAGEMENT_DISABLED",
27
+ throw (0, relayer_sdk_1.pluginError)('Management API disabled', {
28
+ code: 'MANAGEMENT_DISABLED',
32
29
  status: constants_1.HTTP_STATUS.FORBIDDEN,
33
30
  });
34
31
  }
35
32
  const m = params?.management || {};
36
- const provided = (m.adminSecret ?? "").toString();
33
+ const provided = (m.adminSecret ?? '').toString();
37
34
  if (!provided || provided !== config.adminSecret) {
38
- throw (0, relayer_sdk_1.pluginError)("Unauthorized", {
39
- code: "UNAUTHORIZED",
40
- status: constants_1.HTTP_STATUS.UNAUTHORIZED,
41
- });
35
+ throw (0, relayer_sdk_1.pluginError)('Unauthorized', { code: 'UNAUTHORIZED', status: constants_1.HTTP_STATUS.UNAUTHORIZED });
42
36
  }
43
- const action = String(m.action || "");
37
+ const action = String(m.action || '');
44
38
  switch (action) {
45
- case "listChannelAccounts":
39
+ case 'listChannelAccounts':
46
40
  return await listChannelAccounts(kv, config.network);
47
- case "setChannelAccounts":
41
+ case 'setChannelAccounts':
48
42
  return await setChannelAccounts(kv, config.network, m);
49
- case "getFeeUsage":
43
+ case 'getFeeUsage':
50
44
  return await getFeeUsage(kv, config.network, config.feeLimit, config.feeResetPeriodMs, m);
51
- case "getFeeLimit":
45
+ case 'getFeeLimit':
52
46
  return await getFeeLimit(kv, config.network, config.feeLimit, m);
53
- case "setFeeLimit":
47
+ case 'setFeeLimit':
54
48
  return await setFeeLimit(kv, config.network, m);
55
- case "deleteFeeLimit":
49
+ case 'deleteFeeLimit':
56
50
  return await deleteFeeLimit(kv, config.network, m);
57
51
  default:
58
- throw (0, relayer_sdk_1.pluginError)("Invalid management action", {
59
- code: "INVALID_ACTION",
60
- status: constants_1.HTTP_STATUS.BAD_REQUEST,
61
- });
52
+ throw (0, relayer_sdk_1.pluginError)('Invalid management action', { code: 'INVALID_ACTION', status: constants_1.HTTP_STATUS.BAD_REQUEST });
62
53
  }
63
54
  }
64
55
  async function listChannelAccounts(kv, network) {
65
56
  const key = `${network}:channel:relayer-ids`;
66
57
  try {
67
58
  const doc = await kv.get?.(key);
68
- const relayerIds = Array.isArray(doc?.relayerIds)
69
- ? doc.relayerIds.map(normalizeId)
70
- : [];
59
+ const relayerIds = Array.isArray(doc?.relayerIds) ? doc.relayerIds.map(normalizeId) : [];
71
60
  return { relayerIds };
72
61
  }
73
62
  catch (e) {
74
- throw (0, relayer_sdk_1.pluginError)("KV error while listing channel accounts", {
75
- code: "KV_ERROR",
63
+ throw (0, relayer_sdk_1.pluginError)('KV error while listing channel accounts', {
64
+ code: 'KV_ERROR',
76
65
  status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
77
66
  });
78
67
  }
79
68
  }
80
69
  async function getFeeUsage(kv, network, defaultLimit, resetPeriodMs, payload) {
81
70
  const apiKey = payload?.apiKey;
82
- if (!apiKey || typeof apiKey !== "string") {
83
- throw (0, relayer_sdk_1.pluginError)("Invalid payload: apiKey is required", {
84
- code: "INVALID_PAYLOAD",
71
+ if (!apiKey || typeof apiKey !== 'string') {
72
+ throw (0, relayer_sdk_1.pluginError)('Invalid payload: apiKey is required', {
73
+ code: 'INVALID_PAYLOAD',
85
74
  status: constants_1.HTTP_STATUS.BAD_REQUEST,
86
75
  });
87
76
  }
88
77
  try {
89
- const tracker = new fee_tracking_1.FeeTracker({
90
- kv,
91
- network,
92
- apiKey,
93
- defaultLimit,
94
- resetPeriodMs,
95
- });
78
+ const tracker = new fee_tracking_1.FeeTracker({ kv, network, apiKey, defaultLimit, resetPeriodMs });
96
79
  return await tracker.getUsageInfo();
97
80
  }
98
81
  catch (e) {
99
- throw (0, relayer_sdk_1.pluginError)("KV error while reading fee usage", {
100
- code: "KV_ERROR",
82
+ throw (0, relayer_sdk_1.pluginError)('KV error while reading fee usage', {
83
+ code: 'KV_ERROR',
101
84
  status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
102
85
  });
103
86
  }
104
87
  }
105
88
  async function getFeeLimit(kv, network, defaultLimit, payload) {
106
89
  const apiKey = payload?.apiKey;
107
- if (!apiKey || typeof apiKey !== "string") {
108
- throw (0, relayer_sdk_1.pluginError)("Invalid payload: apiKey is required", {
109
- code: "INVALID_PAYLOAD",
90
+ if (!apiKey || typeof apiKey !== 'string') {
91
+ throw (0, relayer_sdk_1.pluginError)('Invalid payload: apiKey is required', {
92
+ code: 'INVALID_PAYLOAD',
110
93
  status: constants_1.HTTP_STATUS.BAD_REQUEST,
111
94
  });
112
95
  }
@@ -118,8 +101,8 @@ async function getFeeLimit(kv, network, defaultLimit, payload) {
118
101
  };
119
102
  }
120
103
  catch (e) {
121
- throw (0, relayer_sdk_1.pluginError)("KV error while reading fee limit", {
122
- code: "KV_ERROR",
104
+ throw (0, relayer_sdk_1.pluginError)('KV error while reading fee limit', {
105
+ code: 'KV_ERROR',
123
106
  status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
124
107
  });
125
108
  }
@@ -127,15 +110,15 @@ async function getFeeLimit(kv, network, defaultLimit, payload) {
127
110
  async function setFeeLimit(kv, network, payload) {
128
111
  const apiKey = payload?.apiKey;
129
112
  const limit = payload?.limit;
130
- if (!apiKey || typeof apiKey !== "string") {
131
- throw (0, relayer_sdk_1.pluginError)("Invalid payload: apiKey is required", {
132
- code: "INVALID_PAYLOAD",
113
+ if (!apiKey || typeof apiKey !== 'string') {
114
+ throw (0, relayer_sdk_1.pluginError)('Invalid payload: apiKey is required', {
115
+ code: 'INVALID_PAYLOAD',
133
116
  status: constants_1.HTTP_STATUS.BAD_REQUEST,
134
117
  });
135
118
  }
136
- if (typeof limit !== "number" || !Number.isFinite(limit) || limit < 0) {
137
- throw (0, relayer_sdk_1.pluginError)("Invalid payload: limit must be a non-negative number", {
138
- code: "INVALID_PAYLOAD",
119
+ if (typeof limit !== 'number' || !Number.isFinite(limit) || limit < 0) {
120
+ throw (0, relayer_sdk_1.pluginError)('Invalid payload: limit must be a non-negative number', {
121
+ code: 'INVALID_PAYLOAD',
139
122
  status: constants_1.HTTP_STATUS.BAD_REQUEST,
140
123
  });
141
124
  }
@@ -145,17 +128,17 @@ async function setFeeLimit(kv, network, payload) {
145
128
  return { ok: true, limit: Math.floor(limit) };
146
129
  }
147
130
  catch (e) {
148
- throw (0, relayer_sdk_1.pluginError)("KV error while setting fee limit", {
149
- code: "KV_ERROR",
131
+ throw (0, relayer_sdk_1.pluginError)('KV error while setting fee limit', {
132
+ code: 'KV_ERROR',
150
133
  status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
151
134
  });
152
135
  }
153
136
  }
154
137
  async function deleteFeeLimit(kv, network, payload) {
155
138
  const apiKey = payload?.apiKey;
156
- if (!apiKey || typeof apiKey !== "string") {
157
- throw (0, relayer_sdk_1.pluginError)("Invalid payload: apiKey is required", {
158
- code: "INVALID_PAYLOAD",
139
+ if (!apiKey || typeof apiKey !== 'string') {
140
+ throw (0, relayer_sdk_1.pluginError)('Invalid payload: apiKey is required', {
141
+ code: 'INVALID_PAYLOAD',
159
142
  status: constants_1.HTTP_STATUS.BAD_REQUEST,
160
143
  });
161
144
  }
@@ -165,8 +148,8 @@ async function deleteFeeLimit(kv, network, payload) {
165
148
  return { ok: true };
166
149
  }
167
150
  catch (e) {
168
- throw (0, relayer_sdk_1.pluginError)("KV error while deleting fee limit", {
169
- code: "KV_ERROR",
151
+ throw (0, relayer_sdk_1.pluginError)('KV error while deleting fee limit', {
152
+ code: 'KV_ERROR',
170
153
  status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
171
154
  });
172
155
  }
@@ -174,8 +157,8 @@ async function deleteFeeLimit(kv, network, payload) {
174
157
  async function setChannelAccounts(kv, network, payload) {
175
158
  const incoming = payload?.relayerIds;
176
159
  if (!Array.isArray(incoming)) {
177
- throw (0, relayer_sdk_1.pluginError)("Invalid payload: relayerIds must be an array", {
178
- code: "INVALID_PAYLOAD",
160
+ throw (0, relayer_sdk_1.pluginError)('Invalid payload: relayerIds must be an array', {
161
+ code: 'INVALID_PAYLOAD',
179
162
  status: constants_1.HTTP_STATUS.BAD_REQUEST,
180
163
  });
181
164
  }
@@ -193,8 +176,8 @@ async function setChannelAccounts(kv, network, payload) {
193
176
  }
194
177
  }
195
178
  if (locked.length > 0) {
196
- throw (0, relayer_sdk_1.pluginError)("Locked relayer IDs cannot be removed", {
197
- code: "LOCKED_CONFLICT",
179
+ throw (0, relayer_sdk_1.pluginError)('Locked relayer IDs cannot be removed', {
180
+ code: 'LOCKED_CONFLICT',
198
181
  status: constants_1.HTTP_STATUS.CONFLICT,
199
182
  details: { locked },
200
183
  });
@@ -205,8 +188,8 @@ async function setChannelAccounts(kv, network, payload) {
205
188
  return { ok: true, appliedRelayerIds: relayerIds };
206
189
  }
207
190
  catch (e) {
208
- throw (0, relayer_sdk_1.pluginError)("KV error while saving channel accounts", {
209
- code: "KV_ERROR",
191
+ throw (0, relayer_sdk_1.pluginError)('KV error while saving channel accounts', {
192
+ code: 'KV_ERROR',
210
193
  status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
211
194
  });
212
195
  }
@@ -227,18 +210,13 @@ function unique(arr) {
227
210
  async function readStoredRelayerIds(kv, key) {
228
211
  try {
229
212
  const doc = await kv.get?.(key);
230
- return Array.isArray(doc?.relayerIds)
231
- ? doc.relayerIds.map(normalizeId)
232
- : [];
213
+ return Array.isArray(doc?.relayerIds) ? doc.relayerIds.map(normalizeId) : [];
233
214
  }
234
215
  catch (error) {
235
- throw (0, relayer_sdk_1.pluginError)("KV error while reading channel accounts", {
236
- code: "KV_ERROR",
216
+ throw (0, relayer_sdk_1.pluginError)('KV error while reading channel accounts', {
217
+ code: 'KV_ERROR',
237
218
  status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
238
- details: {
239
- key,
240
- message: error instanceof Error ? error.message : String(error),
241
- },
219
+ details: { key, message: error instanceof Error ? error.message : String(error) },
242
220
  });
243
221
  }
244
222
  }
@@ -248,14 +226,10 @@ async function isRelayerIdLocked(kv, network, id) {
248
226
  return await kv.exists(key);
249
227
  }
250
228
  catch (error) {
251
- throw (0, relayer_sdk_1.pluginError)("KV error while checking relayer lock", {
252
- code: "KV_ERROR",
229
+ throw (0, relayer_sdk_1.pluginError)('KV error while checking relayer lock', {
230
+ code: 'KV_ERROR',
253
231
  status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
254
- details: {
255
- relayerId: id,
256
- key,
257
- message: error instanceof Error ? error.message : String(error),
258
- },
232
+ details: { relayerId: id, key, message: error instanceof Error ? error.message : String(error) },
259
233
  });
260
234
  }
261
235
  }
@@ -6,20 +6,25 @@
6
6
  * - Per-relayer locks with tokens: <network>:channel:in-use:<relayerId>
7
7
  * - Uses a short global mutex to make acquire atomic across workers.
8
8
  */
9
- import { PluginKVStore } from "@openzeppelin/relayer-sdk";
9
+ import { PluginKVStore } from '@openzeppelin/relayer-sdk';
10
10
  export type PoolLock = {
11
11
  relayerId: string;
12
12
  token: string;
13
13
  };
14
+ export type AcquireOptions = {
15
+ contractId?: string;
16
+ limitedContracts: Set<string>;
17
+ capacityRatio: number;
18
+ };
14
19
  export declare class ChannelPool {
15
20
  private readonly network;
16
21
  private readonly globalLockKey;
17
22
  private readonly channelLockTtlSec;
18
23
  private readonly mutexTtlSec;
19
24
  private readonly kv;
20
- constructor(network: "testnet" | "mainnet", kv: PluginKVStore, lockTtlSeconds: number);
25
+ constructor(network: 'testnet' | 'mainnet', kv: PluginKVStore, lockTtlSeconds: number);
21
26
  /** Acquire a relayerId with a token lock */
22
- acquire(): Promise<PoolLock>;
27
+ acquire(options: AcquireOptions): Promise<PoolLock>;
23
28
  private withGlobalMutex;
24
29
  private tryLockAnyRelayer;
25
30
  /** Release the lock if we own it */
@@ -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;AAI5D,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;gBAGjC,OAAO,EAAE,SAAS,GAAG,SAAS,EAC9B,EAAE,EAAE,aAAa,EACjB,cAAc,EAAE,MAAM;IASxB,4CAA4C;IACtC,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC;YAuBpB,eAAe;YAQf,iBAAiB;IAwB/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;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"}
@@ -24,40 +24,43 @@ class ChannelPool {
24
24
  this.mutexTtlSec = constants_1.POOL.MUTEX_TTL_SECONDS;
25
25
  }
26
26
  /** Acquire a relayerId with a token lock */
27
- async acquire() {
27
+ async acquire(options) {
28
28
  const maxSpins = constants_1.POOL.MUTEX_MAX_SPINS;
29
+ const isLimited = options.contractId && options.limitedContracts.has(options.contractId);
29
30
  for (let i = 0; i < maxSpins; i++) {
30
- const r = await this.withGlobalMutex(() => this.tryLockAnyRelayer());
31
+ const r = await this.withGlobalMutex(() => this.tryLockAnyRelayer(options));
31
32
  if (r === null) {
32
- const jitter = constants_1.POOL.MUTEX_RETRY_MIN_MS +
33
- Math.floor(Math.random() *
34
- (constants_1.POOL.MUTEX_RETRY_MAX_MS - constants_1.POOL.MUTEX_RETRY_MIN_MS + 1));
33
+ const jitter = constants_1.POOL.MUTEX_RETRY_MIN_MS + Math.floor(Math.random() * (constants_1.POOL.MUTEX_RETRY_MAX_MS - constants_1.POOL.MUTEX_RETRY_MIN_MS + 1));
35
34
  await sleep(jitter);
36
35
  continue;
37
36
  }
38
37
  return r;
39
38
  }
40
- throw (0, relayer_sdk_1.pluginError)("Too many transactions queued. Please try again later", {
41
- code: "POOL_CAPACITY",
39
+ if (isLimited) {
40
+ console.log(`[channels] Contract ${options.contractId} limited to ${Math.round(options.capacityRatio * 100)}% capacity - no channels available`);
41
+ }
42
+ throw (0, relayer_sdk_1.pluginError)('Too many transactions queued. Please try again later', {
43
+ code: 'POOL_CAPACITY',
42
44
  status: constants_1.HTTP_STATUS.SERVICE_UNAVAILABLE,
43
45
  });
44
46
  }
45
47
  // Run a function under the short-lived global mutex; returns null if busy
46
48
  async withGlobalMutex(fn) {
47
- return this.kv.withLock(this.globalLockKey, fn, {
48
- ttlSec: this.mutexTtlSec,
49
- onBusy: "skip",
50
- });
49
+ return this.kv.withLock(this.globalLockKey, fn, { ttlSec: this.mutexTtlSec, onBusy: 'skip' });
51
50
  }
52
51
  // Inside the mutex: pick an available relayer and set its channel lock
53
- async tryLockAnyRelayer() {
54
- const ids = await this.getRelayerIdsFromKV();
52
+ async tryLockAnyRelayer(options) {
53
+ let ids = await this.getRelayerIdsFromKV();
55
54
  if (ids.length === 0) {
56
- throw (0, relayer_sdk_1.pluginError)("No channel accounts configured. Use the management API to set channel accounts.", {
57
- code: "NO_CHANNELS_CONFIGURED",
55
+ throw (0, relayer_sdk_1.pluginError)('No channel accounts configured. Use the management API to set channel accounts.', {
56
+ code: 'NO_CHANNELS_CONFIGURED',
58
57
  status: constants_1.HTTP_STATUS.SERVICE_UNAVAILABLE,
59
58
  });
60
59
  }
60
+ // If contract is limited, filter to allowed subset
61
+ if (options.contractId && options.limitedContracts.has(options.contractId)) {
62
+ ids = filterChannelsForLimitedContract(ids, options.capacityRatio);
63
+ }
61
64
  shuffle(ids);
62
65
  for (const relayerId of ids) {
63
66
  const key = this.lockKey(relayerId);
@@ -113,10 +116,10 @@ function shuffle(arr) {
113
116
  }
114
117
  function randomToken() {
115
118
  try {
116
- return crypto_1.default.randomBytes(16).toString("hex");
119
+ return crypto_1.default.randomBytes(16).toString('hex');
117
120
  }
118
121
  catch {
119
- return (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2));
122
+ return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
120
123
  }
121
124
  }
122
125
  function normalizeId(id) {
@@ -125,3 +128,28 @@ function normalizeId(id) {
125
128
  function sleep(ms) {
126
129
  return new Promise((r) => setTimeout(r, ms));
127
130
  }
131
+ /**
132
+ * Simple hash for deterministic channel partitioning.
133
+ * Returns a number 0-99 for modulo-based filtering.
134
+ */
135
+ function simpleHash(str) {
136
+ let hash = 0;
137
+ for (let i = 0; i < str.length; i++) {
138
+ const char = str.charCodeAt(i);
139
+ hash = (hash << 5) - hash + char;
140
+ hash = hash & hash; // Convert to 32-bit integer
141
+ }
142
+ return Math.abs(hash) % 100;
143
+ }
144
+ /**
145
+ * Filter channels for limited contracts using deterministic partitioning.
146
+ * Returns exactly floor(ratio * N) channels, sorted by hash for stability.
147
+ * Always returns at least 1 channel (min guarantee).
148
+ */
149
+ function filterChannelsForLimitedContract(ids, ratio) {
150
+ const k = Math.max(1, Math.floor(ratio * ids.length));
151
+ return ids
152
+ .slice()
153
+ .sort((a, b) => simpleHash(a) - simpleHash(b) || a.localeCompare(b))
154
+ .slice(0, k);
155
+ }
@@ -5,8 +5,8 @@
5
5
  * and the fund account as the operation source. Returns an unsigned inner
6
6
  * transaction with sorobanData and correct fee set to the resource fee.
7
7
  */
8
- import { Transaction, xdr } from "@stellar/stellar-sdk";
9
- import { Relayer } from "@openzeppelin/relayer-sdk";
8
+ import { Transaction, xdr } from '@stellar/stellar-sdk';
9
+ import { Relayer } from '@openzeppelin/relayer-sdk';
10
10
  export interface ChannelAccount {
11
11
  address: string;
12
12
  sequence: string;
@@ -1 +1 @@
1
- {"version":3,"file":"simulation.d.ts","sourceRoot":"","sources":["../../src/plugin/simulation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAIL,WAAW,EAEX,GAAG,EACJ,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAGL,OAAO,EACR,MAAM,2BAA2B,CAAC;AAGnC,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,GAAG,CAAC,YAAY,EACtB,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,GAAG,SAAS,EACjD,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,OAAO,EAChB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,WAAW,CAAC,CAoGtB;AAED,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;;;;;;GAMG;AAEH,OAAO,EAA2B,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,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,GAAG,CAAC,YAAY,EACtB,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,GAAG,SAAS,EACjD,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,OAAO,EAChB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,WAAW,CAAC,CAkFtB;AAED,0FAA0F;AAC1F,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAe1D"}
@@ -19,10 +19,7 @@ async function simulateAndBuildWithChannel(func, auth, channel, _fundAddress, re
19
19
  const transaction = new stellar_sdk_1.TransactionBuilder(new stellar_sdk_1.Account(channel.address, channel.sequence), {
20
20
  fee: constants_1.SIMULATION.DEFAULT_FEE,
21
21
  networkPassphrase,
22
- timebounds: {
23
- minTime: constants_1.SIMULATION.MIN_TIME_BOUND,
24
- maxTime: now + constants_1.SIMULATION.MAX_TIME_BOUND_OFFSET_SECONDS,
25
- },
22
+ timebounds: { minTime: constants_1.SIMULATION.MIN_TIME_BOUND, maxTime: now + constants_1.SIMULATION.MAX_TIME_BOUND_OFFSET_SECONDS },
26
23
  })
27
24
  .addOperation(stellar_sdk_1.Operation.invokeHostFunction({
28
25
  func,
@@ -33,17 +30,17 @@ async function simulateAndBuildWithChannel(func, auth, channel, _fundAddress, re
33
30
  let rpcResponse;
34
31
  try {
35
32
  rpcResponse = await relayer.rpc({
36
- jsonrpc: "2.0",
33
+ jsonrpc: '2.0',
37
34
  id: Math.floor(Math.random() * 1e8).toString(),
38
- method: "simulateTransaction",
35
+ method: 'simulateTransaction',
39
36
  params: {
40
37
  transaction: transaction.toXDR(),
41
38
  },
42
39
  });
43
40
  }
44
41
  catch (err) {
45
- throw (0, relayer_sdk_1.pluginError)("Simulation network request failed", {
46
- code: "SIMULATION_NETWORK_ERROR",
42
+ throw (0, relayer_sdk_1.pluginError)('Simulation network request failed', {
43
+ code: 'SIMULATION_NETWORK_ERROR',
47
44
  status: constants_1.HTTP_STATUS.BAD_GATEWAY,
48
45
  details: {
49
46
  message: err instanceof Error ? err.message : String(err),
@@ -53,43 +50,35 @@ async function simulateAndBuildWithChannel(func, auth, channel, _fundAddress, re
53
50
  if (rpcResponse.error) {
54
51
  const { code, message, description, data } = rpcResponse.error;
55
52
  console.error(`[channels] RPC error: code=${code}, message=${message}, detail=${description || data}`);
56
- throw (0, relayer_sdk_1.pluginError)("Simulation RPC failed", {
57
- code: "SIMULATION_RPC_FAILURE",
53
+ throw (0, relayer_sdk_1.pluginError)('Simulation RPC failed', {
54
+ code: 'SIMULATION_RPC_FAILURE',
58
55
  status: constants_1.HTTP_STATUS.BAD_GATEWAY,
59
- details: { message: "RPC provider error" },
56
+ details: { message: 'RPC provider error' },
60
57
  });
61
58
  }
62
59
  const simResult = {
63
- id: String(rpcResponse.id ?? "1"),
60
+ id: String(rpcResponse.id ?? '1'),
64
61
  ...rpcResponse.result,
65
62
  };
66
- if ("error" in simResult && simResult.error) {
63
+ if ('error' in simResult && simResult.error) {
67
64
  console.error(`[channels] Simulation error: ${simResult.error}`);
68
- throw (0, relayer_sdk_1.pluginError)("Simulation failed", {
69
- code: "SIMULATION_FAILED",
65
+ throw (0, relayer_sdk_1.pluginError)('Simulation failed', {
66
+ code: 'SIMULATION_FAILED',
70
67
  status: constants_1.HTTP_STATUS.BAD_REQUEST,
71
68
  details: { error: parseSimulationError(simResult.error) },
72
69
  });
73
70
  }
74
71
  try {
75
72
  // Use SDK's assembleTransaction to apply simulation results
76
- const prepared = stellar_sdk_1.rpc
77
- .assembleTransaction(transaction, simResult)
78
- .build();
79
- const resourceFee = prepared
80
- .toEnvelope()
81
- .v1()
82
- .tx()
83
- .ext()
84
- .sorobanData()
85
- ?.resourceFee();
73
+ const prepared = stellar_sdk_1.rpc.assembleTransaction(transaction, simResult).build();
74
+ const resourceFee = prepared.toEnvelope().v1().tx().ext().sorobanData()?.resourceFee();
86
75
  console.debug(`[channels] Simulation complete: resourceFee=${resourceFee}`);
87
76
  return prepared;
88
77
  }
89
78
  catch (err) {
90
79
  console.error(`[channels] Assembly error: ${err instanceof Error ? err.message : String(err)}`);
91
- throw (0, relayer_sdk_1.pluginError)("Simulation failed", {
92
- code: "SIMULATION_FAILED",
80
+ throw (0, relayer_sdk_1.pluginError)('Simulation failed', {
81
+ code: 'SIMULATION_FAILED',
93
82
  status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
94
83
  details: {
95
84
  message: err instanceof Error ? err.message : String(err),
@@ -99,7 +88,7 @@ async function simulateAndBuildWithChannel(func, auth, channel, _fundAddress, re
99
88
  }
100
89
  /** Extract human-readable message + error type from simulation error diagnostic events */
101
90
  function parseSimulationError(error) {
102
- const firstLine = error.split("\n")[0]?.trim() || "Simulation failed";
91
+ const firstLine = error.split('\n')[0]?.trim() || 'Simulation failed';
103
92
  const errorType = firstLine.match(/Error\(([^)]+)\)/)?.[1];
104
93
  const arrayMatch = error.match(/data:\s*\["((?:[^"\\]|\\.)*)"/);
105
94
  if (arrayMatch?.[1] && arrayMatch[1].length > 3) {
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * Signing and submission logic for channel account transactions.
5
5
  */
6
- import { Transaction } from "@stellar/stellar-sdk";
7
- import { Relayer, PluginAPI } from "@openzeppelin/relayer-sdk";
8
- import { ChannelAccountsResponse } from "./types";
9
- import { FeeTracker } from "./fee-tracking";
6
+ import { Transaction } from '@stellar/stellar-sdk';
7
+ import { Relayer, PluginAPI } from '@openzeppelin/relayer-sdk';
8
+ import { ChannelAccountsResponse } from './types';
9
+ import { FeeTracker } from './fee-tracking';
10
10
  /**
11
11
  * Sign transaction with both channel and fund relayers
12
12
  * - First sign with channel account
@@ -17,7 +17,7 @@ export declare function signWithChannelAndFund(transaction: Transaction, channel
17
17
  /**
18
18
  * Submit transaction with fee bump and wait for confirmation
19
19
  */
20
- export declare function submitWithFeeBumpAndWait(fundRelayer: Relayer, signedXdr: string, network: "testnet" | "mainnet", maxFee: number, api: PluginAPI, tracker?: FeeTracker): Promise<ChannelAccountsResponse>;
20
+ export declare function submitWithFeeBumpAndWait(fundRelayer: Relayer, signedXdr: string, network: 'testnet' | 'mainnet', maxFee: number, api: PluginAPI, tracker?: FeeTracker): Promise<ChannelAccountsResponse>;
21
21
  /** Strip provider wrapper text, extract last segment (e.g., "TxInsufficientBalance") */
22
22
  export declare function sanitizeReason(reason: string): string;
23
23
  //# 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,CA2BtB;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,CAoElC;AAgBD,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,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"}
@@ -26,8 +26,8 @@ async function signWithChannelAndFund(transaction, channelRelayer, _fundRelayer,
26
26
  unsigned_xdr: txXdr,
27
27
  });
28
28
  if (!isSignTransactionResponseStellar(channelSignResult)) {
29
- throw (0, relayer_sdk_1.pluginError)("Invalid channel signature response", {
30
- code: "INVALID_SIGNATURE",
29
+ throw (0, relayer_sdk_1.pluginError)('Invalid channel signature response', {
30
+ code: 'INVALID_SIGNATURE',
31
31
  status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
32
32
  });
33
33
  }
@@ -58,16 +58,16 @@ async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee,
58
58
  timeout: 25000,
59
59
  }));
60
60
  // Check if transaction actually succeeded
61
- if (final.status === "failed") {
61
+ if (final.status === 'failed') {
62
62
  // Record fee on on-chain failure (transaction was still submitted and consumed fees)
63
63
  if (tracker) {
64
64
  await tracker.recordUsage(maxFee);
65
65
  }
66
- const rawReason = final.status_reason || "Transaction failed";
66
+ const rawReason = final.status_reason || 'Transaction failed';
67
67
  console.error(`[channels] Transaction failed: ${rawReason}`);
68
68
  const reason = sanitizeReason(rawReason);
69
69
  throw (0, relayer_sdk_1.pluginError)(reason, {
70
- code: "ONCHAIN_FAILED",
70
+ code: 'ONCHAIN_FAILED',
71
71
  status: constants_1.HTTP_STATUS.BAD_REQUEST,
72
72
  details: {
73
73
  status: String(final.status),
@@ -89,12 +89,12 @@ async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee,
89
89
  }
90
90
  catch (error) {
91
91
  // If it's already a pluginError with ONCHAIN_FAILED, rethrow it
92
- if (error.code === "ONCHAIN_FAILED") {
92
+ if (error.code === 'ONCHAIN_FAILED') {
93
93
  throw error;
94
94
  }
95
95
  // Otherwise, it's a timeout - don't track fees (status unknown)
96
- throw (0, relayer_sdk_1.pluginError)("Transaction wait timeout. It may still submit.", {
97
- code: "WAIT_TIMEOUT",
96
+ throw (0, relayer_sdk_1.pluginError)('Transaction wait timeout. It may still submit.', {
97
+ code: 'WAIT_TIMEOUT',
98
98
  status: constants_1.HTTP_STATUS.GATEWAY_TIMEOUT,
99
99
  details: {
100
100
  id: submission.id,
@@ -107,17 +107,14 @@ async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee,
107
107
  * Type guard for SignTransactionResponseStellar
108
108
  */
109
109
  function isSignTransactionResponseStellar(data) {
110
- return (data !== null &&
111
- typeof data === "object" &&
112
- "signature" in data &&
113
- "signedXdr" in data);
110
+ return data !== null && typeof data === 'object' && 'signature' in data && 'signedXdr' in data;
114
111
  }
115
112
  /** Strip provider wrapper text, extract last segment (e.g., "TxInsufficientBalance") */
116
113
  function sanitizeReason(reason) {
117
114
  const segments = reason.split(/:\s*/);
118
115
  const last = segments[segments.length - 1]?.trim();
119
- if (last && last.length > 2 && !last.toLowerCase().includes("provider")) {
116
+ if (last && last.length > 2 && !last.toLowerCase().includes('provider')) {
120
117
  return last;
121
118
  }
122
- return reason.length > 100 ? reason.slice(0, 100) + "..." : reason;
119
+ return reason.length > 100 ? reason.slice(0, 100) + '...' : reason;
123
120
  }
@@ -3,6 +3,6 @@
3
3
  *
4
4
  * Transaction validation helpers for the XDR submit-only path.
5
5
  */
6
- import { Transaction } from "@stellar/stellar-sdk";
6
+ import { Transaction } from '@stellar/stellar-sdk';
7
7
  export declare function validateExistingTransactionForSubmitOnly(tx: Transaction): Transaction;
8
8
  //# sourceMappingURL=tx.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tx.d.ts","sourceRoot":"","sources":["../../src/plugin/tx.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAO,MAAM,sBAAsB,CAAC;AAIxD,wBAAgB,wCAAwC,CACtD,EAAE,EAAE,WAAW,GACd,WAAW,CA6Cb"}
1
+ {"version":3,"file":"tx.d.ts","sourceRoot":"","sources":["../../src/plugin/tx.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAO,MAAM,sBAAsB,CAAC;AAIxD,wBAAgB,wCAAwC,CAAC,EAAE,EAAE,WAAW,GAAG,WAAW,CAsCrF"}