@deeeed/metamask-harness 0.23.1 → 0.24.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/CHANGELOG.md +19 -0
- package/dist/recipe-security.js +13 -2
- package/library/actions/core/perps/_controller.mjs +138 -5
- package/library/actions/core/perps/assert_orders.mjs +174 -10
- package/library/actions/core/perps/assert_positions.mjs +20 -2
- package/library/actions/core/perps/close_orders.mjs +24 -5
- package/library/actions/core/perps/close_positions.mjs +22 -8
- package/library/actions/core/perps/edit_order.mjs +331 -0
- package/library/actions/core/perps/place_order.mjs +192 -43
- package/library/actions/core/perps/update_position_tpsl.mjs +121 -15
- package/library/manifests/core.action-manifest.json +468 -27
- package/package.json +1 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
configuredSymbols,
|
|
3
|
+
controllerRejection,
|
|
3
4
|
getCoreControllerWithSigner,
|
|
4
5
|
isDirectRun,
|
|
6
|
+
optionalParam,
|
|
5
7
|
redactOrder,
|
|
6
8
|
requireExplicitSelection,
|
|
7
9
|
runAdapter,
|
|
@@ -22,11 +24,6 @@ import {
|
|
|
22
24
|
// whole position. After submitting we re-read live open orders so the resulting
|
|
23
25
|
// trigger orders — including their partial sizes — are visible as evidence.
|
|
24
26
|
|
|
25
|
-
function optionalString(node, snake, camel) {
|
|
26
|
-
const value = node?.[snake] ?? node?.[camel];
|
|
27
|
-
return value === undefined || value === null ? undefined : String(value);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
27
|
function numericValuesMatch(actual, expected) {
|
|
31
28
|
const actualNumber = Number(actual);
|
|
32
29
|
const expectedNumber = Number(expected);
|
|
@@ -94,12 +91,21 @@ export async function updatePositionTpsl(input) {
|
|
|
94
91
|
const symbol = symbols[0];
|
|
95
92
|
|
|
96
93
|
const node = input.node ?? {};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
let takeProfitPrice = optionalParam(node, 'take_profit_price', 'takeProfitPrice');
|
|
95
|
+
let stopLossPrice = optionalParam(node, 'stop_loss_price', 'stopLossPrice');
|
|
96
|
+
let takeProfitSize = optionalParam(node, 'take_profit_size', 'takeProfitSize');
|
|
97
|
+
let stopLossSize = optionalParam(node, 'stop_loss_size', 'stopLossSize');
|
|
98
|
+
const takeProfitOffsetPct = optionalParam(node, 'take_profit_offset_pct', 'takeProfitOffsetPct');
|
|
99
|
+
const stopLossOffsetPct = optionalParam(node, 'stop_loss_offset_pct', 'stopLossOffsetPct');
|
|
100
|
+
const takeProfitFraction = optionalParam(node, 'take_profit_fraction', 'takeProfitFraction');
|
|
101
|
+
const stopLossFraction = optionalParam(node, 'stop_loss_fraction', 'stopLossFraction');
|
|
101
102
|
|
|
102
|
-
if (
|
|
103
|
+
if (
|
|
104
|
+
takeProfitPrice === undefined &&
|
|
105
|
+
stopLossPrice === undefined &&
|
|
106
|
+
takeProfitOffsetPct === undefined &&
|
|
107
|
+
stopLossOffsetPct === undefined
|
|
108
|
+
) {
|
|
103
109
|
throw new Error(
|
|
104
110
|
'metamask.perps.update_position_tpsl requires take_profit_price and/or stop_loss_price (omit both only to clear, which is not supported here).',
|
|
105
111
|
);
|
|
@@ -120,6 +126,93 @@ export async function updatePositionTpsl(input) {
|
|
|
120
126
|
);
|
|
121
127
|
}
|
|
122
128
|
|
|
129
|
+
// Prices and sizes are resolved from live mid and the actual position, so a
|
|
130
|
+
// recipe never has to name a number that only holds for one market at one
|
|
131
|
+
// moment. An absolute value still wins when the caller gives one.
|
|
132
|
+
// Both derivations below need the market, and each used to fetch it
|
|
133
|
+
// separately. getMarketDataWithPrices reads every market, so doing it twice in
|
|
134
|
+
// one adapter is what pushed this past the node timeout.
|
|
135
|
+
let market;
|
|
136
|
+
const readMarket = async () => {
|
|
137
|
+
if (market === undefined) {
|
|
138
|
+
const markets = await controller.getMarketDataWithPrices({
|
|
139
|
+
standalone: true,
|
|
140
|
+
});
|
|
141
|
+
market =
|
|
142
|
+
(Array.isArray(markets) ? markets : []).find(
|
|
143
|
+
(item) => (item?.symbol ?? '').toUpperCase() === symbol.toUpperCase(),
|
|
144
|
+
) ?? null;
|
|
145
|
+
}
|
|
146
|
+
return market;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (takeProfitOffsetPct !== undefined || stopLossOffsetPct !== undefined) {
|
|
150
|
+
// PerpsMarketData.price is a formatted string like '$103,245.00'.
|
|
151
|
+
const mid = Number(
|
|
152
|
+
String((await readMarket())?.price ?? '').replace(/[$,\s]/gu, ''),
|
|
153
|
+
);
|
|
154
|
+
if (!Number.isFinite(mid) || mid <= 0) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`metamask.perps.update_position_tpsl found no usable mid for ${symbol}.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const fromOffset = (raw, name) => {
|
|
160
|
+
const pct = Number(raw);
|
|
161
|
+
if (!Number.isFinite(pct)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`metamask.perps.update_position_tpsl received invalid ${name}: ${raw}.`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const price = mid * (1 + pct / 100);
|
|
167
|
+
if (!Number.isFinite(price) || price <= 0) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`metamask.perps.update_position_tpsl computed a non-positive price from ${name}=${raw}.`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return String(price);
|
|
173
|
+
};
|
|
174
|
+
if (takeProfitPrice === undefined && takeProfitOffsetPct !== undefined) {
|
|
175
|
+
takeProfitPrice = fromOffset(takeProfitOffsetPct, 'take_profit_offset_pct');
|
|
176
|
+
}
|
|
177
|
+
if (stopLossPrice === undefined && stopLossOffsetPct !== undefined) {
|
|
178
|
+
stopLossPrice = fromOffset(stopLossOffsetPct, 'stop_loss_offset_pct');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (takeProfitFraction !== undefined || stopLossFraction !== undefined) {
|
|
183
|
+
const absolutePositionSize = Math.abs(Number(position.size ?? position.szi ?? 0));
|
|
184
|
+
// Round to the market's own size precision before submitting. The venue
|
|
185
|
+
// rounds anyway, and the read-back below matches on size — so an unrounded
|
|
186
|
+
// fraction asks for a size that can never come back, and the poll spins
|
|
187
|
+
// until it times out.
|
|
188
|
+
const szDecimals = Number((await readMarket())?.szDecimals);
|
|
189
|
+
const fromFraction = (raw, name) => {
|
|
190
|
+
const fraction = Number(raw);
|
|
191
|
+
if (!Number.isFinite(fraction) || fraction <= 0 || fraction > 1) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`metamask.perps.update_position_tpsl received invalid ${name}: ${raw} (expected a fraction in (0, 1]).`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const exact = absolutePositionSize * fraction;
|
|
197
|
+
if (!Number.isFinite(szDecimals)) {
|
|
198
|
+
return String(exact);
|
|
199
|
+
}
|
|
200
|
+
const rounded = Number(exact.toFixed(szDecimals));
|
|
201
|
+
if (rounded <= 0) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`metamask.perps.update_position_tpsl: ${name}=${raw} of a ${absolutePositionSize} position rounds to zero at ${szDecimals} size decimals; use a larger position or a larger fraction.`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return String(rounded);
|
|
207
|
+
};
|
|
208
|
+
if (takeProfitSize === undefined && takeProfitFraction !== undefined) {
|
|
209
|
+
takeProfitSize = fromFraction(takeProfitFraction, 'take_profit_fraction');
|
|
210
|
+
}
|
|
211
|
+
if (stopLossSize === undefined && stopLossFraction !== undefined) {
|
|
212
|
+
stopLossSize = fromFraction(stopLossFraction, 'stop_loss_fraction');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
123
216
|
const params = {
|
|
124
217
|
symbol,
|
|
125
218
|
...(takeProfitPrice === undefined ? {} : { takeProfitPrice }),
|
|
@@ -130,9 +223,11 @@ export async function updatePositionTpsl(input) {
|
|
|
130
223
|
|
|
131
224
|
const result = await controller.updatePositionTPSL(params);
|
|
132
225
|
if (!result || result.success !== true) {
|
|
133
|
-
throw
|
|
134
|
-
|
|
135
|
-
|
|
226
|
+
throw controllerRejection({
|
|
227
|
+
action: 'core updatePositionTPSL',
|
|
228
|
+
detail: symbol,
|
|
229
|
+
code: result?.error ?? 'unknown error',
|
|
230
|
+
});
|
|
136
231
|
}
|
|
137
232
|
|
|
138
233
|
const requests = [
|
|
@@ -156,12 +251,23 @@ export async function updatePositionTpsl(input) {
|
|
|
156
251
|
accountAddress,
|
|
157
252
|
input,
|
|
158
253
|
requests,
|
|
159
|
-
|
|
254
|
+
// Poll for only part of the node's budget. Spending all of it means the
|
|
255
|
+
// runner kills this adapter before the mismatch below can be reported, so a
|
|
256
|
+
// read-back that never matches surfaces as an opaque timeout instead of
|
|
257
|
+
// saying which trigger order was missing.
|
|
258
|
+
Math.min(
|
|
259
|
+
Number(
|
|
260
|
+
optionalParam(input.node ?? {}, 'timeout_ms', 'timeoutMs') ?? 30000,
|
|
261
|
+
) * 0.6,
|
|
262
|
+
60000,
|
|
263
|
+
),
|
|
160
264
|
);
|
|
161
265
|
|
|
162
266
|
if (triggerOrders.length !== requests.length) {
|
|
163
267
|
throw new Error(
|
|
164
|
-
`core updatePositionTPSL for ${symbol} reported success but only ${triggerOrders.length}/${requests.length} requested trigger orders are visible
|
|
268
|
+
`core updatePositionTPSL for ${symbol} reported success but only ${triggerOrders.length}/${requests.length} requested trigger orders are visible. Requested: ${JSON.stringify(
|
|
269
|
+
requests,
|
|
270
|
+
)}.`,
|
|
165
271
|
);
|
|
166
272
|
}
|
|
167
273
|
|