@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.
- package/README.md +79 -43
- package/dist/client/channels-client.d.ts +1 -1
- package/dist/client/channels-client.d.ts.map +1 -1
- package/dist/client/channels-client.js +20 -21
- package/dist/client/errors.d.ts +1 -1
- package/dist/client/errors.js +12 -9
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/types.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/plugin/build.d.ts +1 -1
- package/dist/plugin/build.d.ts.map +1 -1
- package/dist/plugin/build.js +11 -17
- package/dist/plugin/config.d.ts +4 -2
- package/dist/plugin/config.d.ts.map +1 -1
- package/dist/plugin/config.js +43 -13
- package/dist/plugin/constants.d.ts +1 -0
- package/dist/plugin/constants.d.ts.map +1 -1
- package/dist/plugin/constants.js +2 -1
- package/dist/plugin/fee-tracking.d.ts +2 -2
- package/dist/plugin/fee-tracking.d.ts.map +1 -1
- package/dist/plugin/fee-tracking.js +4 -9
- package/dist/plugin/fee.d.ts +12 -5
- package/dist/plugin/fee.d.ts.map +1 -1
- package/dist/plugin/fee.js +39 -23
- package/dist/plugin/handler.d.ts +2 -2
- package/dist/plugin/handler.d.ts.map +1 -1
- package/dist/plugin/handler.js +38 -33
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/management.d.ts +1 -1
- package/dist/plugin/management.d.ts.map +1 -1
- package/dist/plugin/management.js +53 -79
- package/dist/plugin/pool.d.ts +8 -3
- package/dist/plugin/pool.d.ts.map +1 -1
- package/dist/plugin/pool.js +45 -17
- package/dist/plugin/simulation.d.ts +2 -2
- package/dist/plugin/simulation.d.ts.map +1 -1
- package/dist/plugin/simulation.js +17 -28
- package/dist/plugin/submit.d.ts +5 -5
- package/dist/plugin/submit.d.ts.map +1 -1
- package/dist/plugin/submit.js +11 -14
- package/dist/plugin/tx.d.ts +1 -1
- package/dist/plugin/tx.d.ts.map +1 -1
- package/dist/plugin/tx.js +6 -8
- package/dist/plugin/types.d.ts +4 -4
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/validation.d.ts +1 -1
- package/dist/plugin/validation.d.ts.map +1 -1
- package/dist/plugin/validation.js +26 -27
- 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)(
|
|
31
|
-
code:
|
|
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 ??
|
|
33
|
+
const provided = (m.adminSecret ?? '').toString();
|
|
37
34
|
if (!provided || provided !== config.adminSecret) {
|
|
38
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
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
|
|
39
|
+
case 'listChannelAccounts':
|
|
46
40
|
return await listChannelAccounts(kv, config.network);
|
|
47
|
-
case
|
|
41
|
+
case 'setChannelAccounts':
|
|
48
42
|
return await setChannelAccounts(kv, config.network, m);
|
|
49
|
-
case
|
|
43
|
+
case 'getFeeUsage':
|
|
50
44
|
return await getFeeUsage(kv, config.network, config.feeLimit, config.feeResetPeriodMs, m);
|
|
51
|
-
case
|
|
45
|
+
case 'getFeeLimit':
|
|
52
46
|
return await getFeeLimit(kv, config.network, config.feeLimit, m);
|
|
53
|
-
case
|
|
47
|
+
case 'setFeeLimit':
|
|
54
48
|
return await setFeeLimit(kv, config.network, m);
|
|
55
|
-
case
|
|
49
|
+
case 'deleteFeeLimit':
|
|
56
50
|
return await deleteFeeLimit(kv, config.network, m);
|
|
57
51
|
default:
|
|
58
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
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)(
|
|
75
|
-
code:
|
|
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 !==
|
|
83
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
84
|
-
code:
|
|
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)(
|
|
100
|
-
code:
|
|
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 !==
|
|
108
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
109
|
-
code:
|
|
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)(
|
|
122
|
-
code:
|
|
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 !==
|
|
131
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
132
|
-
code:
|
|
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 !==
|
|
137
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
138
|
-
code:
|
|
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)(
|
|
149
|
-
code:
|
|
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 !==
|
|
157
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
158
|
-
code:
|
|
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)(
|
|
169
|
-
code:
|
|
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)(
|
|
178
|
-
code:
|
|
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)(
|
|
197
|
-
code:
|
|
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)(
|
|
209
|
-
code:
|
|
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)(
|
|
236
|
-
code:
|
|
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)(
|
|
252
|
-
code:
|
|
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
|
}
|
package/dist/plugin/pool.d.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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;
|
|
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"}
|
package/dist/plugin/pool.js
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
52
|
+
async tryLockAnyRelayer(options) {
|
|
53
|
+
let ids = await this.getRelayerIdsFromKV();
|
|
55
54
|
if (ids.length === 0) {
|
|
56
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
57
|
-
code:
|
|
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(
|
|
119
|
+
return crypto_1.default.randomBytes(16).toString('hex');
|
|
117
120
|
}
|
|
118
121
|
catch {
|
|
119
|
-
return
|
|
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
|
|
9
|
-
import { Relayer } from
|
|
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,
|
|
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:
|
|
33
|
+
jsonrpc: '2.0',
|
|
37
34
|
id: Math.floor(Math.random() * 1e8).toString(),
|
|
38
|
-
method:
|
|
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)(
|
|
46
|
-
code:
|
|
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)(
|
|
57
|
-
code:
|
|
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:
|
|
56
|
+
details: { message: 'RPC provider error' },
|
|
60
57
|
});
|
|
61
58
|
}
|
|
62
59
|
const simResult = {
|
|
63
|
-
id: String(rpcResponse.id ??
|
|
60
|
+
id: String(rpcResponse.id ?? '1'),
|
|
64
61
|
...rpcResponse.result,
|
|
65
62
|
};
|
|
66
|
-
if (
|
|
63
|
+
if ('error' in simResult && simResult.error) {
|
|
67
64
|
console.error(`[channels] Simulation error: ${simResult.error}`);
|
|
68
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
69
|
-
code:
|
|
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
|
-
|
|
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)(
|
|
92
|
-
code:
|
|
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(
|
|
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) {
|
package/dist/plugin/submit.d.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Signing and submission logic for channel account transactions.
|
|
5
5
|
*/
|
|
6
|
-
import { Transaction } from
|
|
7
|
-
import { Relayer, PluginAPI } from
|
|
8
|
-
import { ChannelAccountsResponse } from
|
|
9
|
-
import { FeeTracker } from
|
|
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:
|
|
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,
|
|
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"}
|
package/dist/plugin/submit.js
CHANGED
|
@@ -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)(
|
|
30
|
-
code:
|
|
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 ===
|
|
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 ||
|
|
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:
|
|
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 ===
|
|
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)(
|
|
97
|
-
code:
|
|
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
|
|
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(
|
|
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) +
|
|
119
|
+
return reason.length > 100 ? reason.slice(0, 100) + '...' : reason;
|
|
123
120
|
}
|
package/dist/plugin/tx.d.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Transaction validation helpers for the XDR submit-only path.
|
|
5
5
|
*/
|
|
6
|
-
import { Transaction } from
|
|
6
|
+
import { Transaction } from '@stellar/stellar-sdk';
|
|
7
7
|
export declare function validateExistingTransactionForSubmitOnly(tx: Transaction): Transaction;
|
|
8
8
|
//# sourceMappingURL=tx.d.ts.map
|
package/dist/plugin/tx.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tx.d.ts","sourceRoot":"","sources":["../../src/plugin/tx.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAO,MAAM,sBAAsB,CAAC;AAIxD,wBAAgB,wCAAwC,
|
|
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"}
|