@deeeed/metamask-harness 0.23.1 → 0.25.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 +44 -0
- package/adapters/extension/live.sh +2 -2
- package/adapters/extension/wallet-fixture-state.cjs +57 -19
- package/adapters/shared/ensure-runner-deps.sh +64 -5
- package/bin/mm-harness +26 -8
- package/dist/adapters/extension/runtime.js +1 -10
- package/dist/adapters.js +5 -1
- package/dist/commands/run-engine.js +18 -13
- package/dist/commands/runtime-launch.js +7 -2
- package/dist/live-adapter-contract.js +4 -2
- package/dist/recipe-security.js +10 -2
- package/dist/run-recording.js +156 -39
- 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/actions/extension/analytics/set_consent.mjs +165 -0
- package/library/actions/extension/platform/cdp.mjs +112 -14
- package/library/actions/mobile/analytics/set_consent.mjs +90 -0
- package/library/actions/shared/analytics/_adapter.mjs +24 -0
- package/library/actions/shared/analytics/assert_events.mjs +168 -0
- package/library/actions/shared/analytics/collector.mjs +505 -0
- package/library/actions/shared/analytics/consent.mjs +14 -0
- package/library/actions/shared/analytics/read_events.mjs +22 -0
- package/library/actions/shared/analytics/start_capture.mjs +24 -0
- package/library/manifests/core.action-manifest.json +468 -27
- package/library/manifests/extension.action-manifest.json +188 -1
- package/library/manifests/mobile.action-manifest.json +161 -0
- package/package.json +3 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getCoreController,
|
|
3
3
|
isDirectRun,
|
|
4
|
+
optionalParam,
|
|
4
5
|
redactOrder,
|
|
5
6
|
requireExplicitSelection,
|
|
6
7
|
runAdapter,
|
|
@@ -19,6 +20,123 @@ import {
|
|
|
19
20
|
// expect_size (proves a partial TP/SL quantity). They use the controller's own
|
|
20
21
|
// field names (triggerOrderType, triggerPrice, orderType, reduceOnly, size).
|
|
21
22
|
|
|
23
|
+
// A count proves only how many orders exist, never that one is the linked TP/SL
|
|
24
|
+
// the caller asked for: two unrelated orders satisfy `expect_count: 2` just as
|
|
25
|
+
// well as a parent and its child. So assert the LINK the controller exposes —
|
|
26
|
+
// the parent's own takeProfitOrderId / stopLossOrderId — and that the id it
|
|
27
|
+
// names is really resting as a reduce-only trigger.
|
|
28
|
+
export function expectedChildLinks(input) {
|
|
29
|
+
const node = input.node ?? {};
|
|
30
|
+
const wanted = [];
|
|
31
|
+
const truthy = (value) =>
|
|
32
|
+
value !== undefined && value !== null && String(value).trim() !== '' &&
|
|
33
|
+
String(value).toLowerCase() !== 'false';
|
|
34
|
+
if (truthy(optionalParam(node, 'expect_take_profit_child', 'expectTakeProfitChild'))) {
|
|
35
|
+
wanted.push({ label: 'take profit', idField: 'takeProfitOrderId' });
|
|
36
|
+
}
|
|
37
|
+
if (truthy(optionalParam(node, 'expect_stop_loss_child', 'expectStopLossChild'))) {
|
|
38
|
+
wanted.push({ label: 'stop loss', idField: 'stopLossOrderId' });
|
|
39
|
+
}
|
|
40
|
+
return wanted;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function assertChildLinks(matching, allOrders, wanted) {
|
|
44
|
+
const resolved = [];
|
|
45
|
+
if (wanted.length === 0) return resolved;
|
|
46
|
+
|
|
47
|
+
const hasLink = (order, idField) =>
|
|
48
|
+
order[idField] !== undefined && String(order[idField]).trim() !== '';
|
|
49
|
+
const parent = matching.find((order) =>
|
|
50
|
+
wanted.every(({ idField }) => hasLink(order, idField)),
|
|
51
|
+
);
|
|
52
|
+
if (!parent) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Expected one matching order to expose ${wanted.map(({ idField }) => idField).join(' and ')}, but none did. Matching orders: ${JSON.stringify(matching.map((order) => ({ id: order.orderId, takeProfitOrderId: order.takeProfitOrderId, stopLossOrderId: order.stopLossOrderId })))}.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const normalizedSide = (order) => {
|
|
59
|
+
const value = String(order.side ?? order.dir ?? '').toLowerCase();
|
|
60
|
+
if (value === 'buy' || value === 'b' || value.includes('long')) return 'buy';
|
|
61
|
+
if (value === 'sell' || value === 'a' || value.includes('short')) return 'sell';
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
const parentSymbol = parent.symbol ?? parent.coin;
|
|
65
|
+
const parentSide = normalizedSide(parent);
|
|
66
|
+
|
|
67
|
+
for (const { label, idField } of wanted) {
|
|
68
|
+
const isTakeProfit = idField === 'takeProfitOrderId';
|
|
69
|
+
const priceField = isTakeProfit ? 'takeProfitPrice' : 'stopLossPrice';
|
|
70
|
+
const triggerFamily = isTakeProfit ? 'take_profit_' : 'stop_';
|
|
71
|
+
const childId = String(parent[idField]);
|
|
72
|
+
const child = allOrders.find(
|
|
73
|
+
(order) => String(order.orderId ?? order.oid) === childId,
|
|
74
|
+
);
|
|
75
|
+
if (!child) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Order ${parent.orderId} names ${idField}=${childId} as its ${label} child, but no such order is live — the child was dropped after being linked.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (child.reduceOnly !== true) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`The ${label} child ${childId} is live but not reduce-only, so it would open exposure rather than close it.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (child.isTrigger !== true) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`The ${label} child ${childId} is live but is not a trigger order, so it cannot fire as a ${label}.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const childType = String(child.triggerOrderType ?? '').toLowerCase();
|
|
91
|
+
if (!childType.startsWith(triggerFamily)) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`The ${label} child ${childId} has triggerOrderType=${JSON.stringify(child.triggerOrderType)}, expected the ${triggerFamily} family.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const childSymbol = child.symbol ?? child.coin;
|
|
97
|
+
if (parentSymbol === undefined || childSymbol !== parentSymbol) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`The ${label} child ${childId} is on ${childSymbol ?? 'an unknown market'}, expected ${parentSymbol ?? 'the parent market'}.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const childSide = normalizedSide(child);
|
|
103
|
+
if (
|
|
104
|
+
parentSide === undefined ||
|
|
105
|
+
childSide === undefined ||
|
|
106
|
+
childSide === parentSide
|
|
107
|
+
) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`The ${label} child ${childId} has side=${child.side ?? child.dir ?? 'unknown'}, expected the opposite of parent side=${parent.side ?? parent.dir ?? 'unknown'}.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const parentPrice = parent[priceField];
|
|
113
|
+
const childPrice = child.triggerPrice ?? child.triggerPx;
|
|
114
|
+
if (
|
|
115
|
+
parentPrice === undefined ||
|
|
116
|
+
childPrice === undefined ||
|
|
117
|
+
Number(parentPrice) !== Number(childPrice)
|
|
118
|
+
) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`The ${label} child ${childId} has triggerPrice=${JSON.stringify(childPrice)}, expected the parent's ${priceField}=${JSON.stringify(parentPrice)}.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
resolved.push({
|
|
124
|
+
link: idField,
|
|
125
|
+
parentOrderId: String(parent.orderId ?? parent.oid),
|
|
126
|
+
childOrderId: childId,
|
|
127
|
+
child: {
|
|
128
|
+
symbol: childSymbol,
|
|
129
|
+
side: childSide,
|
|
130
|
+
triggerOrderType: child.triggerOrderType,
|
|
131
|
+
triggerPrice: childPrice,
|
|
132
|
+
size: child.size ?? child.sz,
|
|
133
|
+
reduceOnly: child.reduceOnly,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return resolved;
|
|
138
|
+
}
|
|
139
|
+
|
|
22
140
|
export function expectedOpen(input) {
|
|
23
141
|
if (input.node?.state == null) throw new Error('metamask.perps.assert_orders requires state=open or state=none.');
|
|
24
142
|
const state = String(input.node.state).toLowerCase();
|
|
@@ -40,23 +158,42 @@ export function expectedOpen(input) {
|
|
|
40
158
|
* @returns True when only trigger orders should be considered.
|
|
41
159
|
*/
|
|
42
160
|
export function onlyTriggerOrders(input) {
|
|
43
|
-
const value =
|
|
161
|
+
const value = optionalParam(
|
|
162
|
+
input.node ?? {},
|
|
163
|
+
'only_trigger_orders',
|
|
164
|
+
'onlyTriggerOrders',
|
|
165
|
+
);
|
|
44
166
|
return value === true || String(value).toLowerCase() === 'true';
|
|
45
167
|
}
|
|
46
168
|
|
|
47
169
|
export function expectedTriggerData(input) {
|
|
48
170
|
const node = input.node ?? {};
|
|
171
|
+
const pick = (snake, camel) => {
|
|
172
|
+
const normalize = (value) =>
|
|
173
|
+
typeof value === 'string' && value.trim().length === 0
|
|
174
|
+
? undefined
|
|
175
|
+
: value ?? undefined;
|
|
176
|
+
return normalize(node[snake]) ?? normalize(node[camel]);
|
|
177
|
+
};
|
|
49
178
|
const expectations = {
|
|
50
|
-
triggerOrderType:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
179
|
+
triggerOrderType: pick(
|
|
180
|
+
'expect_trigger_order_type',
|
|
181
|
+
'expectTriggerOrderType',
|
|
182
|
+
),
|
|
183
|
+
triggerPrice: pick('expect_trigger_price', 'expectTriggerPrice'),
|
|
184
|
+
orderType: pick('expect_execution', 'expectExecution'),
|
|
185
|
+
reduceOnly: pick('expect_reduce_only', 'expectReduceOnly'),
|
|
186
|
+
size: pick('expect_size', 'expectSize'),
|
|
56
187
|
};
|
|
188
|
+
// A blank expectation is an unset recipe template, not a demand that the
|
|
189
|
+
// value equal ''. Keeping it turned an unused param into an assertion against
|
|
190
|
+
// an empty string — which for a price compares as 0.
|
|
57
191
|
return Object.fromEntries(
|
|
58
192
|
Object.entries(expectations).filter(
|
|
59
|
-
([, value]) =>
|
|
193
|
+
([, value]) =>
|
|
194
|
+
value !== undefined &&
|
|
195
|
+
value !== null &&
|
|
196
|
+
!(typeof value === 'string' && value.trim().length === 0),
|
|
60
197
|
),
|
|
61
198
|
);
|
|
62
199
|
}
|
|
@@ -103,8 +240,15 @@ export function assertTriggerData(orders, expectations) {
|
|
|
103
240
|
export async function assertOrders(input, expectOpen = expectedOpen(input)) {
|
|
104
241
|
requireExplicitSelection(input);
|
|
105
242
|
const { controller, accountAddress, network } = await getCoreController(input);
|
|
106
|
-
const timeoutMs = Number(
|
|
243
|
+
const timeoutMs = Number(
|
|
244
|
+
optionalParam(input.node ?? {}, 'timeout_ms', 'timeoutMs') ?? 0,
|
|
245
|
+
);
|
|
107
246
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
247
|
+
const expectedCount = optionalParam(
|
|
248
|
+
input.node ?? {},
|
|
249
|
+
'expect_count',
|
|
250
|
+
'expectCount',
|
|
251
|
+
);
|
|
108
252
|
let orders;
|
|
109
253
|
let matching;
|
|
110
254
|
while (true) {
|
|
@@ -118,7 +262,13 @@ export async function assertOrders(input, expectOpen = expectedOpen(input)) {
|
|
|
118
262
|
// applied to an unrelated parent order resting alongside them.
|
|
119
263
|
matching = matching.filter((order) => order.isTrigger === true);
|
|
120
264
|
}
|
|
121
|
-
|
|
265
|
+
// When an exact count is demanded, stopping at the first positive result
|
|
266
|
+
// reads a mid-replace moment as final: the previous order may not have been
|
|
267
|
+
// swept yet, or the new one not yet placed. Wait for the count itself.
|
|
268
|
+
const reached = expectedCount === undefined
|
|
269
|
+
? matching.length > 0
|
|
270
|
+
: matching.length === Number(expectedCount);
|
|
271
|
+
if (expectOpen ? reached : matching.length === 0) break;
|
|
122
272
|
if (Date.now() >= deadline) break;
|
|
123
273
|
await new Promise((resolve) =>
|
|
124
274
|
setTimeout(resolve, Math.min(500, Math.max(1, deadline - Date.now()))),
|
|
@@ -130,9 +280,20 @@ export async function assertOrders(input, expectOpen = expectedOpen(input)) {
|
|
|
130
280
|
throw new Error('Expected at least one matching open Perps order, but found none.');
|
|
131
281
|
}
|
|
132
282
|
|
|
283
|
+
// "at least one" cannot distinguish a clean replace from one that added a
|
|
284
|
+
// second order beside the first, so a caller can pin the exact count.
|
|
285
|
+
if (expectedCount !== undefined && matching.length !== Number(expectedCount)) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`Expected exactly ${expectedCount} matching open Perps order(s), but found ${matching.length}.`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
133
291
|
const triggerExpectations = expectedTriggerData(input);
|
|
292
|
+
const childLinks = expectedChildLinks(input);
|
|
293
|
+
let resolvedChildLinks = [];
|
|
134
294
|
if (expectOpen) {
|
|
135
295
|
assertTriggerData(matching, triggerExpectations);
|
|
296
|
+
resolvedChildLinks = assertChildLinks(matching, orders, childLinks);
|
|
136
297
|
}
|
|
137
298
|
if (!expectOpen && hasOrder) {
|
|
138
299
|
throw new Error(`Expected no matching open Perps orders, but found ${matching.length}.`);
|
|
@@ -146,7 +307,10 @@ export async function assertOrders(input, expectOpen = expectedOpen(input)) {
|
|
|
146
307
|
expectedOpen: expectOpen,
|
|
147
308
|
expectedTrigger:
|
|
148
309
|
Object.keys(triggerExpectations).length === 0 ? null : triggerExpectations,
|
|
310
|
+
expectedChildLinks: childLinks.map((link) => link.idField),
|
|
311
|
+
resolvedChildLinks,
|
|
149
312
|
matchingCount: matching.length,
|
|
313
|
+
expectedCount: expectedCount === undefined ? null : Number(expectedCount),
|
|
150
314
|
orders: matching.map(redactOrder),
|
|
151
315
|
proofPath: 'perps-controller-getOpenOrders',
|
|
152
316
|
};
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
getCoreController,
|
|
3
3
|
isDirectRun,
|
|
4
4
|
redactPosition,
|
|
5
|
+
optionalParam,
|
|
5
6
|
requireExplicitSelection,
|
|
6
7
|
runAdapter,
|
|
7
8
|
selectedItems,
|
|
@@ -22,8 +23,15 @@ export function expectedOpen(input) {
|
|
|
22
23
|
export async function assertPositions(input, expectOpen = expectedOpen(input)) {
|
|
23
24
|
requireExplicitSelection(input);
|
|
24
25
|
const { controller, accountAddress, network } = await getCoreController(input);
|
|
25
|
-
const timeoutMs = Number(
|
|
26
|
+
const timeoutMs = Number(
|
|
27
|
+
optionalParam(input.node ?? {}, 'timeout_ms', 'timeoutMs') ?? 0,
|
|
28
|
+
);
|
|
26
29
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
30
|
+
const expectedCount = optionalParam(
|
|
31
|
+
input.node ?? {},
|
|
32
|
+
'expect_count',
|
|
33
|
+
'expectCount',
|
|
34
|
+
);
|
|
27
35
|
let positions;
|
|
28
36
|
let matching;
|
|
29
37
|
while (true) {
|
|
@@ -32,7 +40,10 @@ export async function assertPositions(input, expectOpen = expectedOpen(input)) {
|
|
|
32
40
|
userAddress: accountAddress,
|
|
33
41
|
});
|
|
34
42
|
matching = selectedItems(input, positions);
|
|
35
|
-
|
|
43
|
+
const reached = expectedCount === undefined
|
|
44
|
+
? matching.length > 0
|
|
45
|
+
: matching.length === Number(expectedCount);
|
|
46
|
+
if (expectOpen ? reached : matching.length === 0) break;
|
|
36
47
|
if (Date.now() >= deadline) break;
|
|
37
48
|
await new Promise((resolve) =>
|
|
38
49
|
setTimeout(resolve, Math.min(500, Math.max(1, deadline - Date.now()))),
|
|
@@ -43,6 +54,12 @@ export async function assertPositions(input, expectOpen = expectedOpen(input)) {
|
|
|
43
54
|
if (expectOpen && !hasPosition) {
|
|
44
55
|
throw new Error('Expected at least one matching open Perps position, but found none.');
|
|
45
56
|
}
|
|
57
|
+
|
|
58
|
+
if (expectedCount !== undefined && matching.length !== Number(expectedCount)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Expected exactly ${expectedCount} matching open Perps position(s), but found ${matching.length}.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
46
63
|
if (!expectOpen && hasPosition) {
|
|
47
64
|
throw new Error(`Expected no matching open Perps positions, but found ${matching.length}.`);
|
|
48
65
|
}
|
|
@@ -54,6 +71,7 @@ export async function assertPositions(input, expectOpen = expectedOpen(input)) {
|
|
|
54
71
|
account: accountAddress,
|
|
55
72
|
expectedOpen: expectOpen,
|
|
56
73
|
matchingCount: matching.length,
|
|
74
|
+
expectedCount: expectedCount === undefined ? null : Number(expectedCount),
|
|
57
75
|
positions: matching.map(redactPosition),
|
|
58
76
|
proofPath: 'perps-controller-getPositions',
|
|
59
77
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
controllerRejection,
|
|
2
3
|
getCoreControllerWithSigner,
|
|
3
4
|
isDirectRun,
|
|
4
5
|
redactOrder,
|
|
@@ -6,6 +7,7 @@ import {
|
|
|
6
7
|
runAdapter,
|
|
7
8
|
selectedItems,
|
|
8
9
|
symbolForItem,
|
|
10
|
+
optionalParam,
|
|
9
11
|
} from './_controller.mjs';
|
|
10
12
|
|
|
11
13
|
// core Cancel selected live Perps open orders on HyperLiquid testnet by driving
|
|
@@ -37,10 +39,24 @@ async function waitForOrdersAbsent(controller, accountAddress, symbols, timeoutM
|
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
export function cancelOrdersFailure(result) {
|
|
43
|
+
if (!result) return 'unknown error';
|
|
44
|
+
const failed = Array.isArray(result.results)
|
|
45
|
+
? result.results.find((entry) => entry?.success !== true)
|
|
46
|
+
: undefined;
|
|
47
|
+
const hasFailure =
|
|
48
|
+
result.success !== true ||
|
|
49
|
+
Number(result.failureCount ?? 0) > 0 ||
|
|
50
|
+
failed !== undefined;
|
|
51
|
+
return hasFailure ? failed?.error ?? 'unknown error' : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
export async function closeOrders(input) {
|
|
41
55
|
requireExplicitSelection(input);
|
|
42
56
|
const { controller, accountAddress, network } = await getCoreControllerWithSigner(input);
|
|
43
|
-
const timeoutMs = Number(
|
|
57
|
+
const timeoutMs = Number(
|
|
58
|
+
optionalParam(input.node ?? {}, 'timeout_ms', 'timeoutMs') ?? 30000,
|
|
59
|
+
);
|
|
44
60
|
|
|
45
61
|
const orders = await controller.getOpenOrders({
|
|
46
62
|
standalone: true,
|
|
@@ -66,10 +82,13 @@ export async function closeOrders(input) {
|
|
|
66
82
|
// Cancel by the selected symbols. cancelOrders() filters the active provider's
|
|
67
83
|
// open orders to these symbols and batch-cancels them on the exchange.
|
|
68
84
|
const result = await controller.cancelOrders({ symbols });
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
const failure = cancelOrdersFailure(result);
|
|
86
|
+
if (failure !== undefined) {
|
|
87
|
+
throw controllerRejection({
|
|
88
|
+
action: 'core cancelOrders',
|
|
89
|
+
detail: `${JSON.stringify(symbols)}: ${JSON.stringify(result)}`,
|
|
90
|
+
code: failure,
|
|
91
|
+
});
|
|
73
92
|
}
|
|
74
93
|
|
|
75
94
|
const after = await waitForOrdersAbsent(controller, accountAddress, symbols, timeoutMs);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
controllerRejection,
|
|
2
3
|
currentMarketPrice,
|
|
3
4
|
getCoreControllerWithSigner,
|
|
4
5
|
isDirectRun,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
runAdapter,
|
|
8
9
|
selectedItems,
|
|
9
10
|
symbolForItem,
|
|
11
|
+
optionalParam,
|
|
10
12
|
} from './_controller.mjs';
|
|
11
13
|
|
|
12
14
|
// core Close selected live Perps positions on HyperLiquid testnet by driving the
|
|
@@ -36,12 +38,23 @@ async function waitForPositionsAbsent(controller, accountAddress, symbols, timeo
|
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
export function closePositionsFailure(results) {
|
|
42
|
+
const failed = results.find((entry) => entry?.success !== true);
|
|
43
|
+
return failed ? failed.result?.error ?? 'unknown error' : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
export async function closePositions(input) {
|
|
40
47
|
requireExplicitSelection(input);
|
|
41
48
|
const { controller, accountAddress, network } = await getCoreControllerWithSigner(input);
|
|
42
|
-
const timeoutMs = Number(
|
|
49
|
+
const timeoutMs = Number(
|
|
50
|
+
optionalParam(input.node ?? {}, 'timeout_ms', 'timeoutMs') ?? 30000,
|
|
51
|
+
);
|
|
43
52
|
const maxSlippageBps = Number(
|
|
44
|
-
|
|
53
|
+
optionalParam(
|
|
54
|
+
input.node ?? {},
|
|
55
|
+
'max_slippage_bps',
|
|
56
|
+
'maxSlippageBps',
|
|
57
|
+
) ?? 300,
|
|
45
58
|
);
|
|
46
59
|
|
|
47
60
|
const positions = await controller.getPositions({
|
|
@@ -86,12 +99,13 @@ export async function closePositions(input) {
|
|
|
86
99
|
if (success) successCount += 1;
|
|
87
100
|
}
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
const failure = closePositionsFailure(results);
|
|
103
|
+
if (failure !== undefined) {
|
|
104
|
+
throw controllerRejection({
|
|
105
|
+
action: 'core closePosition',
|
|
106
|
+
detail: `${matching.length} call(s): ${JSON.stringify(results)}`,
|
|
107
|
+
code: failure,
|
|
108
|
+
});
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
const after = await waitForPositionsAbsent(controller, accountAddress, symbols, timeoutMs);
|