@openzeppelin/relayer-plugin-channels 0.6.0 → 0.8.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 +5 -1
- package/dist/client/types.d.ts.map +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 +52 -34
- 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 +34 -6
- package/dist/plugin/simulation.d.ts.map +1 -1
- package/dist/plugin/simulation.js +86 -49
- 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 +8 -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
|
+
}
|
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* simulation.ts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* transaction with
|
|
4
|
+
* Simulate a Soroban transaction once (using a throwaway source account) and
|
|
5
|
+
* reuse the result to both detect read-only calls and assemble the final
|
|
6
|
+
* transaction with the real channel account. This avoids a redundant network
|
|
7
|
+
* round-trip to the RPC node.
|
|
7
8
|
*/
|
|
8
|
-
import { Transaction, xdr } from
|
|
9
|
-
import { Relayer } from
|
|
9
|
+
import { rpc, Transaction, xdr } from '@stellar/stellar-sdk';
|
|
10
|
+
import { Relayer } from '@openzeppelin/relayer-sdk';
|
|
10
11
|
export interface ChannelAccount {
|
|
11
12
|
address: string;
|
|
12
13
|
sequence: string;
|
|
13
14
|
}
|
|
14
|
-
export
|
|
15
|
+
export interface SimulationResult {
|
|
16
|
+
/** Whether the call is read-only (no auth + no read-write footprint). */
|
|
17
|
+
isReadOnly: boolean;
|
|
18
|
+
/** base64-encoded xdr.ScVal return value, present when isReadOnly is true. */
|
|
19
|
+
returnValue?: string;
|
|
20
|
+
/** Latest ledger at simulation time. */
|
|
21
|
+
latestLedger?: number;
|
|
22
|
+
/** Raw simulation response — used by buildWithChannel to assemble the tx. */
|
|
23
|
+
rawSimResult: rpc.Api.RawSimulateTransactionResponse;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Simulate a transaction to obtain the full simulation response and detect
|
|
27
|
+
* whether the call is read-only.
|
|
28
|
+
*
|
|
29
|
+
* Uses a throwaway source account (sequence "0") because `simulateTransaction`
|
|
30
|
+
* does not validate sequence numbers. This lets us simulate before acquiring a
|
|
31
|
+
* channel pool slot.
|
|
32
|
+
*
|
|
33
|
+
* A call is read-only when:
|
|
34
|
+
* 1. Zero auth entries — no one needs to authorize anything
|
|
35
|
+
* 2. Zero read-write footprint entries — no ledger state will be modified
|
|
36
|
+
*/
|
|
37
|
+
export declare function simulateTransaction(func: xdr.HostFunction, auth: xdr.SorobanAuthorizationEntry[] | undefined, sourceAddress: string, relayer: Relayer, networkPassphrase: string): Promise<SimulationResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Build and assemble a transaction using a channel account and an already-
|
|
40
|
+
* obtained simulation result. No additional network calls are made.
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildWithChannel(func: xdr.HostFunction, auth: xdr.SorobanAuthorizationEntry[] | undefined, channel: ChannelAccount, networkPassphrase: string, simResult: rpc.Api.RawSimulateTransactionResponse): Transaction;
|
|
15
43
|
/** Extract human-readable message + error type from simulation error diagnostic events */
|
|
16
44
|
export declare function parseSimulationError(error: string): string;
|
|
17
45
|
//# sourceMappingURL=simulation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"simulation.d.ts","sourceRoot":"","sources":["../../src/plugin/simulation.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|
|
@@ -2,94 +2,131 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* simulation.ts
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* transaction with
|
|
5
|
+
* Simulate a Soroban transaction once (using a throwaway source account) and
|
|
6
|
+
* reuse the result to both detect read-only calls and assemble the final
|
|
7
|
+
* transaction with the real channel account. This avoids a redundant network
|
|
8
|
+
* round-trip to the RPC node.
|
|
8
9
|
*/
|
|
9
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.
|
|
11
|
+
exports.simulateTransaction = simulateTransaction;
|
|
12
|
+
exports.buildWithChannel = buildWithChannel;
|
|
11
13
|
exports.parseSimulationError = parseSimulationError;
|
|
12
14
|
const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
13
15
|
const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
|
|
14
16
|
const constants_1 = require("./constants");
|
|
15
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Simulate a transaction to obtain the full simulation response and detect
|
|
19
|
+
* whether the call is read-only.
|
|
20
|
+
*
|
|
21
|
+
* Uses a throwaway source account (sequence "0") because `simulateTransaction`
|
|
22
|
+
* does not validate sequence numbers. This lets us simulate before acquiring a
|
|
23
|
+
* channel pool slot.
|
|
24
|
+
*
|
|
25
|
+
* A call is read-only when:
|
|
26
|
+
* 1. Zero auth entries — no one needs to authorize anything
|
|
27
|
+
* 2. Zero read-write footprint entries — no ledger state will be modified
|
|
28
|
+
*/
|
|
29
|
+
async function simulateTransaction(func, auth, sourceAddress, relayer, networkPassphrase) {
|
|
16
30
|
const now = Math.floor(Date.now() / 1000);
|
|
17
|
-
|
|
18
|
-
// Build inner transaction (source = channel, op source = fund)
|
|
19
|
-
const transaction = new stellar_sdk_1.TransactionBuilder(new stellar_sdk_1.Account(channel.address, channel.sequence), {
|
|
31
|
+
const transaction = new stellar_sdk_1.TransactionBuilder(new stellar_sdk_1.Account(sourceAddress, '0'), {
|
|
20
32
|
fee: constants_1.SIMULATION.DEFAULT_FEE,
|
|
21
33
|
networkPassphrase,
|
|
22
|
-
timebounds: {
|
|
23
|
-
minTime: constants_1.SIMULATION.MIN_TIME_BOUND,
|
|
24
|
-
maxTime: now + constants_1.SIMULATION.MAX_TIME_BOUND_OFFSET_SECONDS,
|
|
25
|
-
},
|
|
34
|
+
timebounds: { minTime: constants_1.SIMULATION.MIN_TIME_BOUND, maxTime: now + constants_1.SIMULATION.MAX_TIME_BOUND_OFFSET_SECONDS },
|
|
26
35
|
})
|
|
27
|
-
.addOperation(stellar_sdk_1.Operation.invokeHostFunction({
|
|
28
|
-
func,
|
|
29
|
-
auth,
|
|
30
|
-
// No explicit source: default to transaction source (channel account)
|
|
31
|
-
}))
|
|
36
|
+
.addOperation(stellar_sdk_1.Operation.invokeHostFunction({ func, auth }))
|
|
32
37
|
.build();
|
|
33
38
|
let rpcResponse;
|
|
34
39
|
try {
|
|
35
40
|
rpcResponse = await relayer.rpc({
|
|
36
|
-
jsonrpc:
|
|
41
|
+
jsonrpc: '2.0',
|
|
37
42
|
id: Math.floor(Math.random() * 1e8).toString(),
|
|
38
|
-
method:
|
|
39
|
-
params: {
|
|
40
|
-
transaction: transaction.toXDR(),
|
|
41
|
-
},
|
|
43
|
+
method: 'simulateTransaction',
|
|
44
|
+
params: { transaction: transaction.toXDR() },
|
|
42
45
|
});
|
|
43
46
|
}
|
|
44
47
|
catch (err) {
|
|
45
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
46
|
-
code:
|
|
48
|
+
throw (0, relayer_sdk_1.pluginError)('Simulation network request failed', {
|
|
49
|
+
code: 'SIMULATION_NETWORK_ERROR',
|
|
47
50
|
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
48
|
-
details: {
|
|
49
|
-
message: err instanceof Error ? err.message : String(err),
|
|
50
|
-
},
|
|
51
|
+
details: { message: err instanceof Error ? err.message : String(err) },
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
if (rpcResponse.error) {
|
|
54
55
|
const { code, message, description, data } = rpcResponse.error;
|
|
55
56
|
console.error(`[channels] RPC error: code=${code}, message=${message}, detail=${description || data}`);
|
|
56
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
57
|
-
code:
|
|
57
|
+
throw (0, relayer_sdk_1.pluginError)('Simulation RPC failed', {
|
|
58
|
+
code: 'SIMULATION_RPC_FAILURE',
|
|
58
59
|
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
59
|
-
details: { message:
|
|
60
|
+
details: { message: 'RPC provider error' },
|
|
60
61
|
});
|
|
61
62
|
}
|
|
62
63
|
const simResult = {
|
|
63
|
-
id: String(rpcResponse.id ??
|
|
64
|
+
id: String(rpcResponse.id ?? '1'),
|
|
64
65
|
...rpcResponse.result,
|
|
65
66
|
};
|
|
66
|
-
if (
|
|
67
|
+
if ('error' in simResult && simResult.error) {
|
|
67
68
|
console.error(`[channels] Simulation error: ${simResult.error}`);
|
|
68
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
69
|
-
code:
|
|
69
|
+
throw (0, relayer_sdk_1.pluginError)('Simulation failed', {
|
|
70
|
+
code: 'SIMULATION_FAILED',
|
|
70
71
|
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
71
72
|
details: { error: parseSimulationError(simResult.error) },
|
|
72
73
|
});
|
|
73
74
|
}
|
|
75
|
+
// Read-only detection
|
|
76
|
+
const results = simResult.results;
|
|
77
|
+
const hasAuth = (results?.[0]?.auth?.length ?? 0) > 0;
|
|
78
|
+
let hasReadWrite = false;
|
|
79
|
+
if (simResult.transactionData) {
|
|
80
|
+
try {
|
|
81
|
+
const sorobanData = stellar_sdk_1.xdr.SorobanTransactionData.fromXDR(simResult.transactionData, 'base64');
|
|
82
|
+
hasReadWrite = sorobanData.resources().footprint().readWrite().length > 0;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// If we can't parse transactionData, treat as not read-only (safe fallback)
|
|
86
|
+
hasReadWrite = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const isReadOnly = !hasAuth && !hasReadWrite;
|
|
90
|
+
if (isReadOnly) {
|
|
91
|
+
console.log(`[channels] Read-only call detected via simulation`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
isReadOnly,
|
|
95
|
+
returnValue: isReadOnly ? results?.[0]?.xdr : undefined,
|
|
96
|
+
latestLedger: simResult.latestLedger,
|
|
97
|
+
rawSimResult: simResult,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build and assemble a transaction using a channel account and an already-
|
|
102
|
+
* obtained simulation result. No additional network calls are made.
|
|
103
|
+
*/
|
|
104
|
+
function buildWithChannel(func, auth, channel, networkPassphrase, simResult) {
|
|
105
|
+
const now = Math.floor(Date.now() / 1000);
|
|
106
|
+
console.debug(`[channels] Building tx: channel=${channel.address}, seq=${channel.sequence}, auth_count=${auth?.length ?? 0}`);
|
|
107
|
+
// Build inner transaction (source = channel)
|
|
108
|
+
const transaction = new stellar_sdk_1.TransactionBuilder(new stellar_sdk_1.Account(channel.address, channel.sequence), {
|
|
109
|
+
fee: constants_1.SIMULATION.DEFAULT_FEE,
|
|
110
|
+
networkPassphrase,
|
|
111
|
+
timebounds: { minTime: constants_1.SIMULATION.MIN_TIME_BOUND, maxTime: now + constants_1.SIMULATION.MAX_TIME_BOUND_OFFSET_SECONDS },
|
|
112
|
+
})
|
|
113
|
+
.addOperation(stellar_sdk_1.Operation.invokeHostFunction({
|
|
114
|
+
func,
|
|
115
|
+
auth,
|
|
116
|
+
// No explicit source: default to transaction source (channel account)
|
|
117
|
+
}))
|
|
118
|
+
.build();
|
|
74
119
|
try {
|
|
75
|
-
// Use SDK's assembleTransaction to apply simulation results
|
|
76
|
-
const prepared = stellar_sdk_1.rpc
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const resourceFee = prepared
|
|
80
|
-
.toEnvelope()
|
|
81
|
-
.v1()
|
|
82
|
-
.tx()
|
|
83
|
-
.ext()
|
|
84
|
-
.sorobanData()
|
|
85
|
-
?.resourceFee();
|
|
86
|
-
console.debug(`[channels] Simulation complete: resourceFee=${resourceFee}`);
|
|
120
|
+
// Use SDK's assembleTransaction to apply the cached simulation results
|
|
121
|
+
const prepared = stellar_sdk_1.rpc.assembleTransaction(transaction, simResult).build();
|
|
122
|
+
const resourceFee = prepared.toEnvelope().v1().tx().ext().sorobanData()?.resourceFee();
|
|
123
|
+
console.debug(`[channels] Assembly complete: resourceFee=${resourceFee}`);
|
|
87
124
|
return prepared;
|
|
88
125
|
}
|
|
89
126
|
catch (err) {
|
|
90
127
|
console.error(`[channels] Assembly error: ${err instanceof Error ? err.message : String(err)}`);
|
|
91
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
92
|
-
code:
|
|
128
|
+
throw (0, relayer_sdk_1.pluginError)('Transaction assembly failed', {
|
|
129
|
+
code: 'ASSEMBLY_FAILED',
|
|
93
130
|
status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
|
|
94
131
|
details: {
|
|
95
132
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -99,7 +136,7 @@ async function simulateAndBuildWithChannel(func, auth, channel, _fundAddress, re
|
|
|
99
136
|
}
|
|
100
137
|
/** Extract human-readable message + error type from simulation error diagnostic events */
|
|
101
138
|
function parseSimulationError(error) {
|
|
102
|
-
const firstLine = error.split(
|
|
139
|
+
const firstLine = error.split('\n')[0]?.trim() || 'Simulation failed';
|
|
103
140
|
const errorType = firstLine.match(/Error\(([^)]+)\)/)?.[1];
|
|
104
141
|
const arrayMatch = error.match(/data:\s*\["((?:[^"\\]|\\.)*)"/);
|
|
105
142
|
if (arrayMatch?.[1] && arrayMatch[1].length > 3) {
|