@deeeed/metamask-harness 0.19.0 → 0.20.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 +31 -0
- package/adapters/extension/ensure-browser.sh +6 -4
- package/adapters/extension/launch-browser.cjs +6 -1
- package/adapters/extension/lib/chrome-args.cjs +11 -1
- package/adapters/extension/start-watch.sh +1 -1
- package/adapters/extension/verify.sh +17 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +33 -0
- package/adapters/mobile/open-device.sh +12 -1
- package/adapters/mobile/wait-for-bridge.sh +1 -1
- package/dist/adapters/extension/ensure-ready.js +7 -2
- package/dist/adapters/extension/runtime-decision.js +4 -6
- package/dist/adapters/extension/runtime.js +114 -17
- package/dist/adapters/mobile/prepare.js +40 -6
- package/dist/adapters.js +6 -1
- package/dist/command-contract.js +58 -5
- package/dist/commands/call.js +6 -3
- package/dist/commands/check.js +9 -2
- package/dist/commands/checklist.js +117 -14
- package/dist/commands/launch/extension.js +8 -3
- package/dist/commands/launch/index.js +38 -25
- package/dist/commands/launch/mobile.js +2 -2
- package/dist/commands/manifest.js +72 -5
- package/dist/commands/run-engine.js +7 -2
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +15 -2
- package/dist/metamask-action-validation.js +45 -0
- package/dist/mm-harness-cli.js +7 -4
- package/docs/CONTRIBUTING.md +8 -0
- package/docs/RECIPES.md +15 -0
- package/library/actions/core/perps/assert_orders.mjs +16 -5
- package/library/actions/core/perps/assert_positions.mjs +16 -5
- package/library/actions/extension/perps/perps.mjs +111 -19
- package/library/actions/extension/platform/cdp.mjs +8 -3
- package/library/actions/extension/ui/navigate.mjs +239 -16
- package/library/actions/mobile/perps/perps.mjs +108 -10
- package/library/actions/mobile/ui/navigate.mjs +1 -1
- package/library/manifests/core.action-manifest.json +100 -11
- package/library/manifests/extension.action-manifest.json +80 -12
- package/library/manifests/mobile.action-manifest.json +85 -10
- package/library/recipes/perps/lifecycle.recipe.json +3 -9
- package/library/recipes/runner/action-validation.extension.recipe.json +5 -4
- package/package.json +5 -4
- package/scripts/completions.sh +2 -2
|
@@ -1,31 +1,249 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import {
|
|
4
|
+
dataTestId,
|
|
5
|
+
normalizeMarketSymbol,
|
|
6
|
+
runAdapter,
|
|
7
|
+
withExtensionPage,
|
|
8
|
+
} from '../platform/cdp.mjs';
|
|
9
|
+
|
|
10
|
+
const BACK_CONTROLS = [
|
|
11
|
+
'perps-order-entry-back-button',
|
|
12
|
+
'perps-market-detail-back-button',
|
|
13
|
+
'perps-activity-back-button',
|
|
14
|
+
'perps-withdraw-back-button',
|
|
15
|
+
'back-button',
|
|
16
|
+
].map(dataTestId);
|
|
7
17
|
|
|
8
18
|
function text(value) {
|
|
9
19
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
10
20
|
}
|
|
11
21
|
|
|
12
|
-
function
|
|
22
|
+
function pageIntent(node) {
|
|
13
23
|
const page = text(node?.page);
|
|
14
24
|
if (!page) return undefined;
|
|
15
|
-
if (
|
|
25
|
+
if (page === 'home' || page === 'perps') return { page };
|
|
16
26
|
if (page === 'perps-market') {
|
|
17
27
|
const market = text(node.market) ?? text(node.symbol);
|
|
18
28
|
if (!market) throw new Error('extension ui.navigate page=perps-market requires market or symbol.');
|
|
19
|
-
return { page,
|
|
29
|
+
return { page, market };
|
|
20
30
|
}
|
|
21
|
-
throw new Error('extension ui.navigate supported page
|
|
31
|
+
throw new Error('extension ui.navigate supported page intents: home, perps, perps-market.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function hasVisibleSelector(page, selector) {
|
|
35
|
+
return page.evaluate(`(() => {
|
|
36
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
37
|
+
if (!element) return false;
|
|
38
|
+
const style = getComputedStyle(element);
|
|
39
|
+
const rect = element.getBoundingClientRect();
|
|
40
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
41
|
+
})()`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function pauseForUi(page) {
|
|
45
|
+
await page.evaluate('new Promise((resolve) => setTimeout(resolve, 200))');
|
|
22
46
|
}
|
|
23
47
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
async function waitForFirstVisible(page, selectors, deadline) {
|
|
49
|
+
while (Date.now() < deadline) {
|
|
50
|
+
for (const selector of selectors) {
|
|
51
|
+
if (await hasVisibleSelector(page, selector)) return selector;
|
|
52
|
+
}
|
|
53
|
+
await pauseForUi(page);
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function currentHref(page) {
|
|
59
|
+
return page.evaluate('location.href');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function activateSemanticControl(page, selector) {
|
|
63
|
+
return {
|
|
64
|
+
activation: 'trusted-pointer',
|
|
65
|
+
interaction: await page.click(selector),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function openHome(page, timeoutMs) {
|
|
70
|
+
const home = dataTestId('bottom-nav-home');
|
|
71
|
+
const selectedHome = `${home}[aria-current="page"]`;
|
|
72
|
+
const deadline = Date.now() + timeoutMs;
|
|
73
|
+
const steps = [];
|
|
74
|
+
let pendingActivation;
|
|
75
|
+
|
|
76
|
+
while (Date.now() < deadline) {
|
|
77
|
+
if (await hasVisibleSelector(page, selectedHome)) {
|
|
78
|
+
return { method: 'visible-ui', href: await currentHref(page), steps };
|
|
79
|
+
}
|
|
80
|
+
const href = await currentHref(page);
|
|
81
|
+
if (pendingActivation && pendingActivation.href !== href) {
|
|
82
|
+
pendingActivation = undefined;
|
|
83
|
+
await pauseForUi(page);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (await hasVisibleSelector(page, home)) {
|
|
87
|
+
if (pendingActivation?.selector !== home) pendingActivation = undefined;
|
|
88
|
+
if (!pendingActivation) {
|
|
89
|
+
await activateSemanticControl(page, home);
|
|
90
|
+
steps.push(home);
|
|
91
|
+
pendingActivation = { selector: home, href };
|
|
92
|
+
}
|
|
93
|
+
await pauseForUi(page);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
let backControl;
|
|
97
|
+
for (const selector of BACK_CONTROLS) {
|
|
98
|
+
if (await hasVisibleSelector(page, selector)) {
|
|
99
|
+
backControl = selector;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (backControl) {
|
|
104
|
+
if (pendingActivation?.selector !== backControl) pendingActivation = undefined;
|
|
105
|
+
if (!pendingActivation || pendingActivation.selector !== backControl) {
|
|
106
|
+
await activateSemanticControl(page, backControl);
|
|
107
|
+
steps.push(backControl);
|
|
108
|
+
pendingActivation = { selector: backControl, href };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
await pauseForUi(page);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
throw new Error(
|
|
115
|
+
`extension ui.navigate page=home could not resolve Home through visible controls; observed href ${JSON.stringify(await currentHref(page))}.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function openPerpsHome(page, timeoutMs) {
|
|
120
|
+
const bottomNav = dataTestId('bottom-nav-perps');
|
|
121
|
+
const legacyTab = dataTestId('account-overview__perps-tab');
|
|
122
|
+
const perpsHome = dataTestId('perps-home-page');
|
|
123
|
+
const steps = [];
|
|
124
|
+
const deadline = Date.now() + timeoutMs;
|
|
125
|
+
let pendingActivation;
|
|
126
|
+
let perpsActivation;
|
|
127
|
+
|
|
128
|
+
while (Date.now() < deadline) {
|
|
129
|
+
if (await hasVisibleSelector(page, perpsHome)) {
|
|
130
|
+
return {
|
|
131
|
+
...(perpsActivation ? { navigation: perpsActivation, selector: bottomNav } : {}),
|
|
132
|
+
method: 'visible-ui',
|
|
133
|
+
href: await currentHref(page),
|
|
134
|
+
steps,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (await hasVisibleSelector(page, bottomNav)) {
|
|
138
|
+
if (!perpsActivation) {
|
|
139
|
+
perpsActivation = await activateSemanticControl(page, bottomNav);
|
|
140
|
+
steps.push(bottomNav);
|
|
141
|
+
}
|
|
142
|
+
await pauseForUi(page);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (await hasVisibleSelector(page, legacyTab)) {
|
|
146
|
+
const navigation = await activateSemanticControl(page, legacyTab);
|
|
147
|
+
await page.waitForSelector(dataTestId('perps-view'), {
|
|
148
|
+
timeoutMs: Math.max(1, deadline - Date.now()),
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
navigation,
|
|
152
|
+
method: 'visible-ui',
|
|
153
|
+
href: await currentHref(page),
|
|
154
|
+
selector: legacyTab,
|
|
155
|
+
steps,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let backControl;
|
|
160
|
+
for (const selector of BACK_CONTROLS) {
|
|
161
|
+
if (await hasVisibleSelector(page, selector)) {
|
|
162
|
+
backControl = selector;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (backControl) {
|
|
167
|
+
const href = await currentHref(page);
|
|
168
|
+
if (pendingActivation && pendingActivation.href !== href) {
|
|
169
|
+
pendingActivation = undefined;
|
|
170
|
+
}
|
|
171
|
+
if (!pendingActivation || pendingActivation.selector !== backControl) {
|
|
172
|
+
await activateSemanticControl(page, backControl);
|
|
173
|
+
steps.push(backControl);
|
|
174
|
+
pendingActivation = { selector: backControl, href };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
await pauseForUi(page);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const state = await page.evaluate(`({
|
|
181
|
+
href: location.href,
|
|
182
|
+
title: document.title,
|
|
183
|
+
visibleTestIds: [...document.querySelectorAll('[data-testid]')]
|
|
184
|
+
.filter((element) => {
|
|
185
|
+
const style = getComputedStyle(element);
|
|
186
|
+
const rect = element.getBoundingClientRect();
|
|
187
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
188
|
+
})
|
|
189
|
+
.map((element) => element.getAttribute('data-testid'))
|
|
190
|
+
.filter(Boolean)
|
|
191
|
+
.slice(0, 30),
|
|
192
|
+
})`);
|
|
193
|
+
throw new Error(
|
|
194
|
+
`extension ui.navigate page=perps could not resolve the Perps home through visible UI; observed ${JSON.stringify(state)}. Capture the current UI with mm-harness call ui.screenshot, then use a verified visible control or raw hash/path.`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function openPerpsMarket(page, market, timeoutMs) {
|
|
199
|
+
const deadline = Date.now() + timeoutMs;
|
|
200
|
+
const home = await openPerpsHome(page, Math.max(1, deadline - Date.now()));
|
|
201
|
+
const normalizedMarket = normalizeMarketSymbol(market.trim());
|
|
202
|
+
const selectors = [
|
|
203
|
+
dataTestId(`perps-watchlist-${normalizedMarket}`),
|
|
204
|
+
dataTestId(`explore-markets-${normalizedMarket.replaceAll(':', '-')}`),
|
|
205
|
+
];
|
|
206
|
+
const marketControl = await waitForFirstVisible(page, selectors, deadline);
|
|
207
|
+
if (!marketControl) {
|
|
208
|
+
const visibleMarketControls = await page.evaluate(`[
|
|
209
|
+
...document.querySelectorAll('[data-testid^="perps-watchlist-"], [data-testid^="explore-markets-"]')
|
|
210
|
+
].filter((element) => {
|
|
211
|
+
const style = getComputedStyle(element);
|
|
212
|
+
const rect = element.getBoundingClientRect();
|
|
213
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
214
|
+
}).map((element) => element.getAttribute('data-testid')).filter(Boolean)`);
|
|
215
|
+
throw new Error(
|
|
216
|
+
`extension ui.navigate page=perps-market could not find ${JSON.stringify(normalizedMarket)} in the current visible market controls ${JSON.stringify(visibleMarketControls)}.`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const navigation = await activateSemanticControl(page, marketControl);
|
|
220
|
+
await page.waitForSelector(dataTestId('perps-market-detail-page'), {
|
|
221
|
+
timeoutMs: Math.max(1, deadline - Date.now()),
|
|
222
|
+
});
|
|
223
|
+
return {
|
|
224
|
+
navigation,
|
|
225
|
+
method: 'visible-ui',
|
|
226
|
+
href: await currentHref(page),
|
|
227
|
+
selector: marketControl,
|
|
228
|
+
steps: [...home.steps, marketControl],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function navigateExtensionUi(input) {
|
|
233
|
+
return withExtensionPage(input, async (page) => {
|
|
234
|
+
const intent = pageIntent(input.node);
|
|
235
|
+
if (intent) {
|
|
236
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 20000);
|
|
237
|
+
if (intent.page === 'home') {
|
|
238
|
+
const result = await openHome(page, timeoutMs);
|
|
239
|
+
return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
|
|
240
|
+
}
|
|
241
|
+
if (intent.page === 'perps') {
|
|
242
|
+
const result = await openPerpsHome(page, timeoutMs);
|
|
243
|
+
return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
|
|
244
|
+
}
|
|
245
|
+
const result = await openPerpsMarket(page, intent.market, timeoutMs);
|
|
246
|
+
return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
|
|
29
247
|
}
|
|
30
248
|
|
|
31
249
|
const url = input.node?.url;
|
|
@@ -40,5 +258,10 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
|
40
258
|
return { action: input.action, hash, navigation, proofPath: 'ui-navigation' };
|
|
41
259
|
}
|
|
42
260
|
|
|
43
|
-
|
|
44
|
-
})
|
|
261
|
+
throw new Error('extension ui.navigate requires page, raw extension url, hash, or path.');
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
266
|
+
runAdapter(navigateExtensionUi);
|
|
267
|
+
}
|
|
@@ -24,7 +24,10 @@ function symbolForItem(item) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function sideForItem(item) {
|
|
27
|
-
|
|
27
|
+
const side = String(item?.side ?? item?.direction ?? '').toLowerCase();
|
|
28
|
+
if (side === 'buy' || side === 'b') return 'long';
|
|
29
|
+
if (side === 'sell' || side === 'a' || side === 's') return 'short';
|
|
30
|
+
return side;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
function configuredSymbols(input, items) {
|
|
@@ -67,6 +70,35 @@ function requirePlaceOrderInputs(input) {
|
|
|
67
70
|
if (node.amount == null && node.notional == null) throw new Error(`${input.action} requires amount=<usd> or notional=<usd>.`);
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
function resolveOrderType(input) {
|
|
74
|
+
const value = String(input.node?.order_type ?? input.node?.orderType ?? 'market').toLowerCase();
|
|
75
|
+
if (value !== 'market' && value !== 'limit') {
|
|
76
|
+
throw new Error(`${input.action} received unsupported order_type: ${value}.`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveLimitPrice(input, isBuy, currentPrice) {
|
|
82
|
+
const explicit = input.node?.limit_price ?? input.node?.limitPrice ?? input.node?.price;
|
|
83
|
+
if (explicit != null && String(explicit).length > 0) {
|
|
84
|
+
const value = Number(explicit);
|
|
85
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
86
|
+
throw new Error(`${input.action} received invalid limit_price: ${explicit}.`);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
const rawOffset = input.node?.offset_pct ?? input.node?.offsetPct;
|
|
91
|
+
const offset = rawOffset == null ? (isBuy ? -30 : 30) : Number(rawOffset);
|
|
92
|
+
if (!Number.isFinite(offset)) {
|
|
93
|
+
throw new Error(`${input.action} received invalid offset_pct: ${rawOffset}.`);
|
|
94
|
+
}
|
|
95
|
+
const value = currentPrice * (1 + offset / 100);
|
|
96
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
97
|
+
throw new Error(`${input.action} computed a non-positive limit price (${value}).`);
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
70
102
|
function uniqueSymbols(symbols) {
|
|
71
103
|
return Array.from(new Set(symbols.filter(Boolean)));
|
|
72
104
|
}
|
|
@@ -143,6 +175,20 @@ async function waitForPositionPresent(input, symbol, timeoutMs = 30000) {
|
|
|
143
175
|
return last;
|
|
144
176
|
}
|
|
145
177
|
|
|
178
|
+
async function waitForSelectedState(input, readItems, expectedOpen, timeoutMs) {
|
|
179
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
180
|
+
let items;
|
|
181
|
+
while (true) {
|
|
182
|
+
items = await readItems(input);
|
|
183
|
+
const matching = selectedItems(input, items);
|
|
184
|
+
if (expectedOpen ? matching.length > 0 : matching.length === 0) {
|
|
185
|
+
return matching;
|
|
186
|
+
}
|
|
187
|
+
if (Date.now() >= deadline) return matching;
|
|
188
|
+
await sleep(Math.min(500, Math.max(1, deadline - Date.now())));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
146
192
|
function redactPosition(position) {
|
|
147
193
|
return {
|
|
148
194
|
symbol: position.symbol ?? position.coin ?? null,
|
|
@@ -270,8 +316,13 @@ export async function readOrders(input) {
|
|
|
270
316
|
|
|
271
317
|
export async function assertPositions(input, expectedOpen) {
|
|
272
318
|
requireExplicitSelection(input);
|
|
273
|
-
const
|
|
274
|
-
const matching =
|
|
319
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
320
|
+
const matching = await waitForSelectedState(
|
|
321
|
+
input,
|
|
322
|
+
readPositions,
|
|
323
|
+
expectedOpen,
|
|
324
|
+
timeoutMs,
|
|
325
|
+
);
|
|
275
326
|
const hasPosition = matching.length > 0;
|
|
276
327
|
if (expectedOpen && !hasPosition) throw new Error('Expected at least one matching open Perps position, but found none.');
|
|
277
328
|
if (!expectedOpen && hasPosition) throw new Error(`Expected no matching open Perps positions, but found ${matching.length}.`);
|
|
@@ -280,8 +331,13 @@ export async function assertPositions(input, expectedOpen) {
|
|
|
280
331
|
|
|
281
332
|
export async function assertOrders(input, expectedOpen) {
|
|
282
333
|
requireExplicitSelection(input);
|
|
283
|
-
const
|
|
284
|
-
const matching =
|
|
334
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
335
|
+
const matching = await waitForSelectedState(
|
|
336
|
+
input,
|
|
337
|
+
readOpenOrders,
|
|
338
|
+
expectedOpen,
|
|
339
|
+
timeoutMs,
|
|
340
|
+
);
|
|
285
341
|
const hasOrders = matching.length > 0;
|
|
286
342
|
if (expectedOpen && !hasOrders) throw new Error('Expected at least one matching open Perps order, but found none.');
|
|
287
343
|
if (!expectedOpen && hasOrders) throw new Error(`Expected no matching open Perps orders, but found ${matching.length}.`);
|
|
@@ -407,12 +463,43 @@ export async function placeOrder(input) {
|
|
|
407
463
|
const symbol = marketSymbol(input);
|
|
408
464
|
const side = String(input.node.side).toLowerCase();
|
|
409
465
|
const amount = String(input.node.amount ?? input.node.notional);
|
|
410
|
-
const size = String(input.node?.size ?? '0.0001');
|
|
411
466
|
const leverage = Number(input.node?.leverage ?? 3);
|
|
412
467
|
const maxSlippageBps = Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300);
|
|
413
468
|
const isBuy = orderSideIsBuy(side);
|
|
469
|
+
const orderType = resolveOrderType(input);
|
|
414
470
|
const currentPrice = await currentPriceForOrder(input, symbol);
|
|
415
|
-
const
|
|
471
|
+
const limitPrice = orderType === 'limit'
|
|
472
|
+
? resolveLimitPrice(input, isBuy, currentPrice)
|
|
473
|
+
: null;
|
|
474
|
+
const size = String(
|
|
475
|
+
input.node?.size
|
|
476
|
+
?? ((Number(amount) * leverage) / (limitPrice ?? currentPrice)),
|
|
477
|
+
);
|
|
478
|
+
const orderParams = orderType === 'limit'
|
|
479
|
+
? {
|
|
480
|
+
symbol,
|
|
481
|
+
isBuy,
|
|
482
|
+
orderType,
|
|
483
|
+
size,
|
|
484
|
+
price: String(limitPrice),
|
|
485
|
+
timeInForce: 'GTC',
|
|
486
|
+
leverage,
|
|
487
|
+
currentPrice,
|
|
488
|
+
priceAtCalculation: currentPrice,
|
|
489
|
+
maxSlippageBps,
|
|
490
|
+
}
|
|
491
|
+
: {
|
|
492
|
+
symbol,
|
|
493
|
+
isBuy,
|
|
494
|
+
orderType,
|
|
495
|
+
size,
|
|
496
|
+
usdAmount: amount,
|
|
497
|
+
leverage,
|
|
498
|
+
currentPrice,
|
|
499
|
+
priceAtCalculation: currentPrice,
|
|
500
|
+
maxSlippageBps,
|
|
501
|
+
};
|
|
502
|
+
const result = await evalAsync(input, `Engine.context.PerpsController.placeOrder(${JSON.stringify(orderParams)}).then(function(r){return JSON.stringify(r)})`);
|
|
416
503
|
if (result?.success === false || result == null) {
|
|
417
504
|
throw new Error(`Failed to place ${symbol} ${side}: ${result?.error || JSON.stringify(result)}`);
|
|
418
505
|
}
|
|
@@ -420,8 +507,12 @@ export async function placeOrder(input) {
|
|
|
420
507
|
input,
|
|
421
508
|
'globalThis.__AGENTIC__ && globalThis.__AGENTIC__.refreshPerpsStreams ? globalThis.__AGENTIC__.refreshPerpsStreams().then(function(r){return JSON.stringify(r)}) : Promise.resolve(JSON.stringify({ ok: false, reason: "refreshPerpsStreams unavailable" }))',
|
|
422
509
|
);
|
|
423
|
-
|
|
424
|
-
|
|
510
|
+
if (orderType === 'limit') {
|
|
511
|
+
await waitForSelectedState(input, readOpenOrders, true, Number(input.node?.timeout_ms ?? 30000));
|
|
512
|
+
} else {
|
|
513
|
+
await waitForPositionPresent(input, symbol, Number(input.node?.timeout_ms ?? 30000));
|
|
514
|
+
}
|
|
515
|
+
return { action: input.action, market: symbol, side, orderType, amount, size, leverage, limitPrice, submitted: true, result, refresh, proofPath: 'mobile-perps-controller-place-order' };
|
|
425
516
|
}
|
|
426
517
|
|
|
427
518
|
export async function ensurePositions(input) {
|
|
@@ -448,7 +539,14 @@ export async function ensureOrders(input) {
|
|
|
448
539
|
const close = await closeOrders(input);
|
|
449
540
|
return { ...(await assertOrders(input, false)), close };
|
|
450
541
|
}
|
|
451
|
-
if (state === 'open' || state === 'present')
|
|
542
|
+
if (state === 'open' || state === 'present') {
|
|
543
|
+
const current = await readOpenOrders(input);
|
|
544
|
+
let order = null;
|
|
545
|
+
if (selectedItems(input, current).length === 0) {
|
|
546
|
+
order = await placeOrder({ ...input, node: { ...input.node, order_type: 'limit' } });
|
|
547
|
+
}
|
|
548
|
+
return { ...(await assertOrders(input, true)), order };
|
|
549
|
+
}
|
|
452
550
|
throw new Error(`metamask.perps.ensure_orders received unsupported state: ${state}`);
|
|
453
551
|
}
|
|
454
552
|
|
|
@@ -18,7 +18,7 @@ function pageRoute(node) {
|
|
|
18
18
|
if (!market) throw new Error('mobile ui.navigate page=perps-market requires market or symbol.');
|
|
19
19
|
return { page, route: 'PerpsMarketDetails', params: { market: { symbol: market } } };
|
|
20
20
|
}
|
|
21
|
-
throw new Error('mobile ui.navigate supported page
|
|
21
|
+
throw new Error('mobile ui.navigate supported page intents: home, perps, perps-market.');
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
runAdapter(async (input) => {
|
|
@@ -590,6 +590,15 @@
|
|
|
590
590
|
"type": "string",
|
|
591
591
|
"description": "EVM address to read positions for; defaults to the canonical wallet fixture account."
|
|
592
592
|
},
|
|
593
|
+
"network": {
|
|
594
|
+
"type": "string",
|
|
595
|
+
"enum": [
|
|
596
|
+
"testnet",
|
|
597
|
+
"mainnet"
|
|
598
|
+
],
|
|
599
|
+
"default": "testnet",
|
|
600
|
+
"description": "Controller network for this isolated Core read."
|
|
601
|
+
},
|
|
593
602
|
"timeout_ms": {
|
|
594
603
|
"type": "number"
|
|
595
604
|
}
|
|
@@ -607,7 +616,7 @@
|
|
|
607
616
|
}
|
|
608
617
|
],
|
|
609
618
|
"proof_effect": "E2E validation must record this action in trace.json; live-proof actions include liveAdapter/proofPath or controller output.",
|
|
610
|
-
"safety_notes": "Read-only HyperLiquid
|
|
619
|
+
"safety_notes": "Read-only HyperLiquid query on the explicitly selected network; must not inject controller/UI state to fabricate proof.",
|
|
611
620
|
"execution_capabilities": []
|
|
612
621
|
},
|
|
613
622
|
{
|
|
@@ -697,6 +706,15 @@
|
|
|
697
706
|
"type": "string",
|
|
698
707
|
"description": "EVM address to read orders for; defaults to the canonical wallet fixture account."
|
|
699
708
|
},
|
|
709
|
+
"network": {
|
|
710
|
+
"type": "string",
|
|
711
|
+
"enum": [
|
|
712
|
+
"testnet",
|
|
713
|
+
"mainnet"
|
|
714
|
+
],
|
|
715
|
+
"default": "testnet",
|
|
716
|
+
"description": "Controller network for this isolated Core read."
|
|
717
|
+
},
|
|
700
718
|
"timeout_ms": {
|
|
701
719
|
"type": "number"
|
|
702
720
|
}
|
|
@@ -714,7 +732,7 @@
|
|
|
714
732
|
}
|
|
715
733
|
],
|
|
716
734
|
"proof_effect": "E2E validation must record this action in trace.json; live-proof actions include liveAdapter/proofPath or controller output.",
|
|
717
|
-
"safety_notes": "Read-only HyperLiquid
|
|
735
|
+
"safety_notes": "Read-only HyperLiquid query on the explicitly selected network; must not inject controller/UI state to fabricate proof.",
|
|
718
736
|
"execution_capabilities": []
|
|
719
737
|
},
|
|
720
738
|
{
|
|
@@ -820,7 +838,7 @@
|
|
|
820
838
|
"long",
|
|
821
839
|
"short"
|
|
822
840
|
],
|
|
823
|
-
"description": "
|
|
841
|
+
"description": "Required order direction."
|
|
824
842
|
},
|
|
825
843
|
"order_type": {
|
|
826
844
|
"type": "string",
|
|
@@ -839,11 +857,17 @@
|
|
|
839
857
|
"description": "Resting limit price as a percent offset from live mid for limit orders (e.g. -30 = 30%% below mid for a non-filling BUY; default -30 buy / +30 sell). Alias: offsetPct."
|
|
840
858
|
},
|
|
841
859
|
"amount": {
|
|
842
|
-
"type":
|
|
860
|
+
"type": [
|
|
861
|
+
"string",
|
|
862
|
+
"number"
|
|
863
|
+
],
|
|
843
864
|
"description": "USD notional alias."
|
|
844
865
|
},
|
|
845
866
|
"notional": {
|
|
846
|
-
"type":
|
|
867
|
+
"type": [
|
|
868
|
+
"string",
|
|
869
|
+
"number"
|
|
870
|
+
],
|
|
847
871
|
"description": "USD notional alias."
|
|
848
872
|
},
|
|
849
873
|
"leverage": {
|
|
@@ -1101,6 +1125,15 @@
|
|
|
1101
1125
|
"type": "string",
|
|
1102
1126
|
"description": "EVM address to read for; defaults to the canonical wallet fixture account."
|
|
1103
1127
|
},
|
|
1128
|
+
"network": {
|
|
1129
|
+
"type": "string",
|
|
1130
|
+
"enum": [
|
|
1131
|
+
"testnet",
|
|
1132
|
+
"mainnet"
|
|
1133
|
+
],
|
|
1134
|
+
"default": "testnet",
|
|
1135
|
+
"description": "Controller network for this isolated Core assertion."
|
|
1136
|
+
},
|
|
1104
1137
|
"timeout_ms": {
|
|
1105
1138
|
"type": "number"
|
|
1106
1139
|
}
|
|
@@ -1122,7 +1155,7 @@
|
|
|
1122
1155
|
}
|
|
1123
1156
|
],
|
|
1124
1157
|
"proof_effect": "E2E validation must record this action in trace.json; live-proof actions include liveAdapter/proofPath or controller output.",
|
|
1125
|
-
"safety_notes": "Read-only HyperLiquid
|
|
1158
|
+
"safety_notes": "Read-only HyperLiquid query on the explicitly selected network; must not inject controller state to fabricate proof.",
|
|
1126
1159
|
"execution_capabilities": []
|
|
1127
1160
|
},
|
|
1128
1161
|
{
|
|
@@ -1206,7 +1239,7 @@
|
|
|
1206
1239
|
"long",
|
|
1207
1240
|
"short"
|
|
1208
1241
|
],
|
|
1209
|
-
"description": "
|
|
1242
|
+
"description": "Required when state=open/present; otherwise filters selected positions."
|
|
1210
1243
|
},
|
|
1211
1244
|
"state": {
|
|
1212
1245
|
"type": "string",
|
|
@@ -1220,11 +1253,17 @@
|
|
|
1220
1253
|
"description": "Desired selected position state to converge to."
|
|
1221
1254
|
},
|
|
1222
1255
|
"amount": {
|
|
1223
|
-
"type":
|
|
1256
|
+
"type": [
|
|
1257
|
+
"string",
|
|
1258
|
+
"number"
|
|
1259
|
+
],
|
|
1224
1260
|
"description": "USD notional alias (used when placing to reach state open)."
|
|
1225
1261
|
},
|
|
1226
1262
|
"notional": {
|
|
1227
|
-
"type":
|
|
1263
|
+
"type": [
|
|
1264
|
+
"string",
|
|
1265
|
+
"number"
|
|
1266
|
+
],
|
|
1228
1267
|
"description": "USD notional alias."
|
|
1229
1268
|
},
|
|
1230
1269
|
"leverage": {
|
|
@@ -1252,6 +1291,17 @@
|
|
|
1252
1291
|
"market": "BTC",
|
|
1253
1292
|
"intent": "Converge Perps positions to the requested state via the headless controller"
|
|
1254
1293
|
}
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
"description": "Ensure one ETH long position exists",
|
|
1297
|
+
"node": {
|
|
1298
|
+
"action": "metamask.perps.ensure_positions",
|
|
1299
|
+
"market": "ETH",
|
|
1300
|
+
"side": "long",
|
|
1301
|
+
"state": "open",
|
|
1302
|
+
"notional": "10",
|
|
1303
|
+
"intent": "Converge ETH positions to one explicit testnet precondition"
|
|
1304
|
+
}
|
|
1255
1305
|
}
|
|
1256
1306
|
],
|
|
1257
1307
|
"proof_effect": "E2E validation must record this action in trace.json; live-proof actions include liveAdapter/proofPath or controller output.",
|
|
@@ -1458,6 +1508,15 @@
|
|
|
1458
1508
|
],
|
|
1459
1509
|
"description": "Optional position/order side filter."
|
|
1460
1510
|
},
|
|
1511
|
+
"network": {
|
|
1512
|
+
"type": "string",
|
|
1513
|
+
"enum": [
|
|
1514
|
+
"testnet",
|
|
1515
|
+
"mainnet"
|
|
1516
|
+
],
|
|
1517
|
+
"default": "testnet",
|
|
1518
|
+
"description": "Controller network for this isolated Core assertion."
|
|
1519
|
+
},
|
|
1461
1520
|
"timeout_ms": {
|
|
1462
1521
|
"type": "number"
|
|
1463
1522
|
},
|
|
@@ -1490,7 +1549,7 @@
|
|
|
1490
1549
|
}
|
|
1491
1550
|
],
|
|
1492
1551
|
"proof_effect": "E2E validation must record this action in trace.json; live-proof actions include liveAdapter/proofPath or target runtime output.",
|
|
1493
|
-
"safety_notes": "
|
|
1552
|
+
"safety_notes": "Read-only HyperLiquid query on the explicitly selected network; must not inject mid-recipe state to fabricate proof.",
|
|
1494
1553
|
"execution_capabilities": []
|
|
1495
1554
|
},
|
|
1496
1555
|
{
|
|
@@ -1574,11 +1633,29 @@
|
|
|
1574
1633
|
"long",
|
|
1575
1634
|
"short"
|
|
1576
1635
|
],
|
|
1577
|
-
"description": "
|
|
1636
|
+
"description": "Required when state=open/present; otherwise filters selected orders."
|
|
1578
1637
|
},
|
|
1579
1638
|
"timeout_ms": {
|
|
1580
1639
|
"type": "number"
|
|
1581
1640
|
},
|
|
1641
|
+
"amount": {
|
|
1642
|
+
"type": [
|
|
1643
|
+
"string",
|
|
1644
|
+
"number"
|
|
1645
|
+
],
|
|
1646
|
+
"description": "Explicit USD notional used only when creating a missing open order. Alias: notional."
|
|
1647
|
+
},
|
|
1648
|
+
"notional": {
|
|
1649
|
+
"type": [
|
|
1650
|
+
"string",
|
|
1651
|
+
"number"
|
|
1652
|
+
],
|
|
1653
|
+
"description": "Alias for amount."
|
|
1654
|
+
},
|
|
1655
|
+
"leverage": {
|
|
1656
|
+
"type": "number",
|
|
1657
|
+
"description": "Leverage used only when creating a missing open order."
|
|
1658
|
+
},
|
|
1582
1659
|
"state": {
|
|
1583
1660
|
"type": "string",
|
|
1584
1661
|
"enum": [
|
|
@@ -1605,6 +1682,18 @@
|
|
|
1605
1682
|
"mode": "all",
|
|
1606
1683
|
"intent": "Converge Perps orders to the requested state"
|
|
1607
1684
|
}
|
|
1685
|
+
},
|
|
1686
|
+
{
|
|
1687
|
+
"description": "Ensure one resting ETH short order exists",
|
|
1688
|
+
"node": {
|
|
1689
|
+
"action": "metamask.perps.ensure_orders",
|
|
1690
|
+
"market": "ETH",
|
|
1691
|
+
"side": "short",
|
|
1692
|
+
"state": "open",
|
|
1693
|
+
"notional": "10",
|
|
1694
|
+
"leverage": 2,
|
|
1695
|
+
"intent": "Converge ETH open orders to one explicit testnet precondition"
|
|
1696
|
+
}
|
|
1608
1697
|
}
|
|
1609
1698
|
],
|
|
1610
1699
|
"proof_effect": "E2E validation must record this action in trace.json; live-proof actions include liveAdapter/proofPath or target runtime output.",
|