@deeeed/metamask-harness 0.19.0 → 0.19.1
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 +13 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +33 -0
- package/dist/adapters.js +6 -1
- package/dist/live-adapter-contract.js +15 -2
- 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/mobile/perps/perps.mjs +108 -10
- package/library/manifests/core.action-manifest.json +40 -4
- package/library/manifests/extension.action-manifest.json +28 -2
- package/library/manifests/mobile.action-manifest.json +36 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.19.1 - 2026-07-23
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Mobile `ui.press` supports `long_press=true` for real long-press gestures such as resetting a keypad value.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Perps position and order assertions honor `timeout_ms` consistently on Mobile, Extension, and Core.
|
|
14
|
+
- Core Perps reads and assertions honor their explicit network instead of silently defaulting isolated actions to testnet.
|
|
15
|
+
- Mobile and Extension `ensure_orders state=open` create a resting testnet limit order when missing and recognize live buy/sell sides consistently.
|
|
16
|
+
- Reject invalid live-adapter process timeouts and use the safe default.
|
|
17
|
+
|
|
5
18
|
## 0.19.0 - 2026-07-22
|
|
6
19
|
|
|
7
20
|
### Added
|
|
@@ -254,6 +254,38 @@ const COMMANDS = {
|
|
|
254
254
|
return { ...result, testId, deviceName };
|
|
255
255
|
},
|
|
256
256
|
|
|
257
|
+
async 'long-press-test-id'(client, args, { deviceName } = {}) {
|
|
258
|
+
const testId = args[0];
|
|
259
|
+
if (!testId) {
|
|
260
|
+
throw new Error('Usage: long-press-test-id <testId>');
|
|
261
|
+
}
|
|
262
|
+
const expr = `(function() {
|
|
263
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
264
|
+
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
265
|
+
var renderers = hook.renderers;
|
|
266
|
+
if (!renderers) return { ok: false, error: 'No renderers' };
|
|
267
|
+
var getFiberRoots = hook.getFiberRoots;
|
|
268
|
+
function walk(fiber) {
|
|
269
|
+
if (!fiber) return false;
|
|
270
|
+
var props = fiber.memoizedProps;
|
|
271
|
+
if (props && props.testID === ${JSON.stringify(testId)}) {
|
|
272
|
+
if (typeof props.onLongPress === 'function') { props.onLongPress(); return true; }
|
|
273
|
+
}
|
|
274
|
+
return walk(fiber.child) || walk(fiber.sibling);
|
|
275
|
+
}
|
|
276
|
+
for (var [id] of renderers) {
|
|
277
|
+
var roots = getFiberRoots ? getFiberRoots(id) : undefined;
|
|
278
|
+
if (!roots) continue;
|
|
279
|
+
var found = false;
|
|
280
|
+
roots.forEach(function(r) { if (!found) found = walk(r.current); });
|
|
281
|
+
if (found) return { ok: true, testId: ${JSON.stringify(testId)} };
|
|
282
|
+
}
|
|
283
|
+
return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onLongPress' };
|
|
284
|
+
})()`;
|
|
285
|
+
const result = await cdpEval(client, expr);
|
|
286
|
+
return { ...result, testId, deviceName };
|
|
287
|
+
},
|
|
288
|
+
|
|
257
289
|
async 'press-text'(client, args, { deviceName } = {}) {
|
|
258
290
|
const text = args[0];
|
|
259
291
|
if (!text) {
|
|
@@ -693,6 +725,7 @@ Commands:
|
|
|
693
725
|
get-selected-account Get the currently selected account
|
|
694
726
|
switch-account <address> Switch to account by address
|
|
695
727
|
press-test-id <testId> Press a component by its testID prop
|
|
728
|
+
long-press-test-id <testId> Invoke a component's long-press gesture by testID
|
|
696
729
|
press-text <text> Press a component containing visible text
|
|
697
730
|
scroll-view [--test-id <id>] [--offset <n>] [--animated]
|
|
698
731
|
Scroll a ScrollView/FlatList
|
package/dist/adapters.js
CHANGED
|
@@ -320,8 +320,13 @@ async function handleMobilePress(payload, context) {
|
|
|
320
320
|
}
|
|
321
321
|
const target = text ?? identifiers[0];
|
|
322
322
|
if (target.trim() === "") throw new Error("ui.press target must be non-empty.");
|
|
323
|
+
const longPress = payload.long_press === true;
|
|
324
|
+
if (longPress && text !== void 0) {
|
|
325
|
+
throw new Error("ui.press long_press=true requires test_id, testID, or selector targeting.");
|
|
326
|
+
}
|
|
323
327
|
const input = mobileUiInput(context, "press", payload);
|
|
324
|
-
|
|
328
|
+
if (text !== void 0) return bridgeCommand(input, ["press-text", target]);
|
|
329
|
+
return bridgeCommand(input, [longPress ? "long-press-test-id" : "press-test-id", target]);
|
|
325
330
|
}
|
|
326
331
|
async function handleMobileSetInput(payload, context) {
|
|
327
332
|
const input = mobileUiInput(context, "set_input", payload);
|
|
@@ -312,6 +312,7 @@ async function runLiveAdapterScript({ platform, action, node, context, prepared
|
|
|
312
312
|
`);
|
|
313
313
|
const command = prepared ? commandForPrepared(context.projectRoot, platform) : commandFor(script, context.projectRoot, platform);
|
|
314
314
|
const platformEnv = await platformAdapterEnv(platform, context.projectRoot, tempDir);
|
|
315
|
+
const processTimeoutMs = liveAdapterProcessTimeoutMs(node);
|
|
315
316
|
const result = await runProcess(command.command, [...command.args, inputPath], {
|
|
316
317
|
cwd: context.projectRoot,
|
|
317
318
|
env: {
|
|
@@ -321,12 +322,12 @@ async function runLiveAdapterScript({ platform, action, node, context, prepared
|
|
|
321
322
|
METAMASK_RECIPE_ADAPTER_INPUT: inputPath,
|
|
322
323
|
METAMASK_RECIPE_ADAPTER_OUTPUT: outputPath
|
|
323
324
|
},
|
|
324
|
-
timeoutMs:
|
|
325
|
+
timeoutMs: processTimeoutMs,
|
|
325
326
|
...prepared ? { stdin: prepared.sourceText } : {}
|
|
326
327
|
});
|
|
327
328
|
try {
|
|
328
329
|
if (result.timedOut) {
|
|
329
|
-
throw new Error(`Live adapter ${script} timed out after ${
|
|
330
|
+
throw new Error(`Live adapter ${script} timed out after ${processTimeoutMs}ms.`);
|
|
330
331
|
}
|
|
331
332
|
if (result.exitCode !== 0) {
|
|
332
333
|
throw new Error(`Live adapter ${script} exited ${result.exitCode}: ${result.stderr || result.stdout}`);
|
|
@@ -344,6 +345,17 @@ async function runLiveAdapterScript({ platform, action, node, context, prepared
|
|
|
344
345
|
await rm(tempDir, { recursive: true, force: true });
|
|
345
346
|
}
|
|
346
347
|
}
|
|
348
|
+
function liveAdapterProcessTimeoutMs(node) {
|
|
349
|
+
if (node.live_adapter_timeout_ms != null) {
|
|
350
|
+
const timeoutMs = Number(node.live_adapter_timeout_ms);
|
|
351
|
+
return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 6e4;
|
|
352
|
+
}
|
|
353
|
+
if (node.timeout_ms != null) {
|
|
354
|
+
const timeoutMs = Number(node.timeout_ms);
|
|
355
|
+
return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs + 5e3 : 6e4;
|
|
356
|
+
}
|
|
357
|
+
return 6e4;
|
|
358
|
+
}
|
|
347
359
|
function commandForPrepared(projectRoot, platform) {
|
|
348
360
|
if (platform !== "core") {
|
|
349
361
|
return { command: process.execPath, args: ["--input-type=module", "-"] };
|
|
@@ -364,6 +376,7 @@ function isPathWithin(root, candidate) {
|
|
|
364
376
|
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
365
377
|
}
|
|
366
378
|
export {
|
|
379
|
+
liveAdapterProcessTimeoutMs,
|
|
367
380
|
prepareLiveAdapterScript,
|
|
368
381
|
resolveLiveAdapter,
|
|
369
382
|
runLiveAdapterScript
|
package/docs/RECIPES.md
CHANGED
|
@@ -67,6 +67,21 @@ Rules:
|
|
|
67
67
|
- Parameterize repeated behavior instead of multiplying names.
|
|
68
68
|
- Keep secrets out of recipes, libraries, and evidence.
|
|
69
69
|
|
|
70
|
+
Keep responsibilities narrow:
|
|
71
|
+
|
|
72
|
+
- **Parameters** expose caller choices that may vary without changing the
|
|
73
|
+
journey's meaning.
|
|
74
|
+
- **Recipes** compose actions and own journey order, safe defaults, invariants,
|
|
75
|
+
and proof.
|
|
76
|
+
- **Actions** translate one stable operation to existing UI, CDP, or controller
|
|
77
|
+
capabilities; they do not reimplement product business logic.
|
|
78
|
+
- **Product/controllers** remain the source of truth for state transitions,
|
|
79
|
+
validation, transactions, and domain behavior.
|
|
80
|
+
|
|
81
|
+
If changing a value would violate the recipe's postcondition, keep it as a
|
|
82
|
+
documented recipe invariant rather than a parameter. Add a shared action only
|
|
83
|
+
when repeated direct access has a stable cross-task contract.
|
|
84
|
+
|
|
70
85
|
The protocol is authoritative:
|
|
71
86
|
<https://farmslot.io/docs/reference/recipe-protocol-v1>.
|
|
72
87
|
|
|
@@ -23,11 +23,22 @@ export function expectedOpen(input) {
|
|
|
23
23
|
export async function assertOrders(input, expectOpen = expectedOpen(input)) {
|
|
24
24
|
requireExplicitSelection(input);
|
|
25
25
|
const { controller, accountAddress, network } = await getCoreController(input);
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
27
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
28
|
+
let orders;
|
|
29
|
+
let matching;
|
|
30
|
+
while (true) {
|
|
31
|
+
orders = await controller.getOpenOrders({
|
|
32
|
+
standalone: true,
|
|
33
|
+
userAddress: accountAddress,
|
|
34
|
+
});
|
|
35
|
+
matching = selectedItems(input, orders);
|
|
36
|
+
if (expectOpen ? matching.length > 0 : matching.length === 0) break;
|
|
37
|
+
if (Date.now() >= deadline) break;
|
|
38
|
+
await new Promise((resolve) =>
|
|
39
|
+
setTimeout(resolve, Math.min(500, Math.max(1, deadline - Date.now()))),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
31
42
|
const hasOrder = matching.length > 0;
|
|
32
43
|
|
|
33
44
|
if (expectOpen && !hasOrder) {
|
|
@@ -22,11 +22,22 @@ export function expectedOpen(input) {
|
|
|
22
22
|
export async function assertPositions(input, expectOpen = expectedOpen(input)) {
|
|
23
23
|
requireExplicitSelection(input);
|
|
24
24
|
const { controller, accountAddress, network } = await getCoreController(input);
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
26
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
27
|
+
let positions;
|
|
28
|
+
let matching;
|
|
29
|
+
while (true) {
|
|
30
|
+
positions = await controller.getPositions({
|
|
31
|
+
standalone: true,
|
|
32
|
+
userAddress: accountAddress,
|
|
33
|
+
});
|
|
34
|
+
matching = selectedItems(input, positions);
|
|
35
|
+
if (expectOpen ? matching.length > 0 : matching.length === 0) break;
|
|
36
|
+
if (Date.now() >= deadline) break;
|
|
37
|
+
await new Promise((resolve) =>
|
|
38
|
+
setTimeout(resolve, Math.min(500, Math.max(1, deadline - Date.now()))),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
30
41
|
const hasPosition = matching.length > 0;
|
|
31
42
|
|
|
32
43
|
if (expectOpen && !hasPosition) {
|
|
@@ -10,7 +10,10 @@ function symbolForItem(item) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function sideForItem(item) {
|
|
13
|
-
|
|
13
|
+
const side = String(item?.side ?? item?.direction ?? '').toLowerCase();
|
|
14
|
+
if (side === 'buy' || side === 'b') return 'long';
|
|
15
|
+
if (side === 'sell' || side === 'a' || side === 's') return 'short';
|
|
16
|
+
return side;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
function uniqueSymbols(symbols) {
|
|
@@ -57,6 +60,35 @@ function requirePlaceOrderInputs(input) {
|
|
|
57
60
|
if (node.amount == null && node.notional == null) throw new Error(`${input.action} requires amount=<usd> or notional=<usd>.`);
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
function resolveOrderType(input) {
|
|
64
|
+
const value = String(input.node?.order_type ?? input.node?.orderType ?? 'market').toLowerCase();
|
|
65
|
+
if (value !== 'market' && value !== 'limit') {
|
|
66
|
+
throw new Error(`${input.action} received unsupported order_type: ${value}.`);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveLimitPrice(input, isBuy, currentPrice) {
|
|
72
|
+
const explicit = input.node?.limit_price ?? input.node?.limitPrice ?? input.node?.price;
|
|
73
|
+
if (explicit != null && String(explicit).length > 0) {
|
|
74
|
+
const value = Number(explicit);
|
|
75
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
76
|
+
throw new Error(`${input.action} received invalid limit_price: ${explicit}.`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
const rawOffset = input.node?.offset_pct ?? input.node?.offsetPct;
|
|
81
|
+
const offset = rawOffset == null ? (isBuy ? -30 : 30) : Number(rawOffset);
|
|
82
|
+
if (!Number.isFinite(offset)) {
|
|
83
|
+
throw new Error(`${input.action} received invalid offset_pct: ${rawOffset}.`);
|
|
84
|
+
}
|
|
85
|
+
const value = currentPrice * (1 + offset / 100);
|
|
86
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
87
|
+
throw new Error(`${input.action} computed a non-positive limit price (${value}).`);
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
|
|
60
92
|
export function positionSelectionState(input, positions) {
|
|
61
93
|
return { hasMatching: selectedItems(input, positions).length > 0 };
|
|
62
94
|
}
|
|
@@ -97,9 +129,9 @@ export async function readPositions(input) {
|
|
|
97
129
|
|
|
98
130
|
export async function readOrders(input) {
|
|
99
131
|
return withExtensionPage(input, async (page) => {
|
|
100
|
-
const
|
|
101
|
-
const matching = selectedItems(input, orders);
|
|
102
|
-
return { action: input.action, source:
|
|
132
|
+
const state = await readOpenOrdersState(page);
|
|
133
|
+
const matching = selectedItems(input, state.orders);
|
|
134
|
+
return { action: input.action, source: state.source, count: state.orders.length, matchingCount: matching.length, orders: matching.map(redactOrder) };
|
|
103
135
|
});
|
|
104
136
|
}
|
|
105
137
|
|
|
@@ -107,11 +139,7 @@ export async function assertPositions(input, expectedOpen) {
|
|
|
107
139
|
requireExplicitSelection(input);
|
|
108
140
|
return withExtensionPage(input, async (page) => {
|
|
109
141
|
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
110
|
-
const
|
|
111
|
-
if (expectedOpen && timeoutMs > 0 && requested.length === 1) {
|
|
112
|
-
await waitForPositionPresent(page, requested[0], timeoutMs);
|
|
113
|
-
}
|
|
114
|
-
const state = await page.readPositions();
|
|
142
|
+
const state = await waitForSelectedPositionState(page, input, expectedOpen, timeoutMs);
|
|
115
143
|
if (!state.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot assert live Perps positions.');
|
|
116
144
|
const matching = selectedItems(input, state.positions);
|
|
117
145
|
const hasPosition = matching.length > 0;
|
|
@@ -124,12 +152,13 @@ export async function assertPositions(input, expectedOpen) {
|
|
|
124
152
|
export async function assertOrders(input, expectedOpen) {
|
|
125
153
|
requireExplicitSelection(input);
|
|
126
154
|
return withExtensionPage(input, async (page) => {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
155
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
156
|
+
const state = await waitForSelectedOrderState(page, input, expectedOpen, timeoutMs);
|
|
157
|
+
const matching = selectedItems(input, state.orders);
|
|
129
158
|
const hasOrders = matching.length > 0;
|
|
130
159
|
if (expectedOpen && !hasOrders) throw new Error('Expected at least one matching open Perps order, but found none.');
|
|
131
160
|
if (!expectedOpen && hasOrders) throw new Error(`Expected no matching open Perps orders, but found ${matching.length}.`);
|
|
132
|
-
return { action: input.action, expectedOpen, matchingCount: matching.length, orders: matching.map(redactOrder), source:
|
|
161
|
+
return { action: input.action, expectedOpen, matchingCount: matching.length, orders: matching.map(redactOrder), source: state.source };
|
|
133
162
|
});
|
|
134
163
|
}
|
|
135
164
|
|
|
@@ -306,6 +335,10 @@ async function readCurrentMarketPrice(page, symbol, explicitPrice) {
|
|
|
306
335
|
}
|
|
307
336
|
|
|
308
337
|
async function readOpenOrders(page) {
|
|
338
|
+
return (await readOpenOrdersState(page)).orders;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function readOpenOrdersState(page) {
|
|
309
342
|
const state = await page.evaluate(`(async () => {
|
|
310
343
|
const request = globalThis.stateHooks?.submitRequestToBackground;
|
|
311
344
|
const manager = globalThis.stateHooks?.getPerpsStreamManager?.();
|
|
@@ -326,7 +359,7 @@ async function readOpenOrders(page) {
|
|
|
326
359
|
};
|
|
327
360
|
})()`);
|
|
328
361
|
if (!state.available) throw new Error('Perps orders are unavailable from both background and stream manager cache.');
|
|
329
|
-
return state
|
|
362
|
+
return state;
|
|
330
363
|
}
|
|
331
364
|
|
|
332
365
|
async function readAccountState(page) {
|
|
@@ -393,12 +426,41 @@ async function waitForPositionPresent(page, symbol, timeoutMs) {
|
|
|
393
426
|
return lastState;
|
|
394
427
|
}
|
|
395
428
|
|
|
429
|
+
async function waitForSelectedPositionState(page, input, expectedOpen, timeoutMs) {
|
|
430
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
431
|
+
let state;
|
|
432
|
+
while (true) {
|
|
433
|
+
state = await page.readPositions();
|
|
434
|
+
if (!state.available) {
|
|
435
|
+
throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot assert live Perps positions.');
|
|
436
|
+
}
|
|
437
|
+
const matching = selectedItems(input, state.positions);
|
|
438
|
+
if (expectedOpen ? matching.length > 0 : matching.length === 0) return state;
|
|
439
|
+
if (Date.now() >= deadline) return state;
|
|
440
|
+
await sleep(Math.min(500, Math.max(1, deadline - Date.now())));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function waitForSelectedOrderState(page, input, expectedOpen, timeoutMs) {
|
|
445
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
446
|
+
let state;
|
|
447
|
+
while (true) {
|
|
448
|
+
state = await readOpenOrdersState(page);
|
|
449
|
+
const matching = selectedItems(input, state.orders);
|
|
450
|
+
if (expectedOpen ? matching.length > 0 : matching.length === 0) return state;
|
|
451
|
+
if (Date.now() >= deadline) return state;
|
|
452
|
+
await sleep(Math.min(500, Math.max(1, deadline - Date.now())));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
396
456
|
export async function placeOrder(input) {
|
|
397
457
|
requirePlaceOrderInputs(input);
|
|
398
458
|
const symbol = marketSymbol(input);
|
|
399
459
|
const side = String(input.node.side).toLowerCase();
|
|
400
460
|
const amount = String(input.node.amount ?? input.node.notional);
|
|
401
461
|
const leverage = Number(input.node?.leverage ?? 3);
|
|
462
|
+
const isBuy = side !== 'short';
|
|
463
|
+
const orderType = resolveOrderType(input);
|
|
402
464
|
return withExtensionPage(input, async (page) => {
|
|
403
465
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
404
466
|
await page.evaluate(`(async () => {
|
|
@@ -415,6 +477,9 @@ export async function placeOrder(input) {
|
|
|
415
477
|
symbol,
|
|
416
478
|
input.node?.current_price ?? input.node?.currentPrice,
|
|
417
479
|
);
|
|
480
|
+
const limitPrice = orderType === 'limit'
|
|
481
|
+
? resolveLimitPrice(input, isBuy, resolvedPrice)
|
|
482
|
+
: null;
|
|
418
483
|
await page.navigateHash(`#/perps/trade/${encodeURIComponent(symbol)}?direction=${encodeURIComponent(side)}&mode=new`);
|
|
419
484
|
await page.waitForSelector(dataTestId('perps-order-entry-page'), { timeoutMs: 20000 });
|
|
420
485
|
let order;
|
|
@@ -426,13 +491,27 @@ export async function placeOrder(input) {
|
|
|
426
491
|
if (!Number.isFinite(currentPrice) || currentPrice <= 0) throw new Error('Unable to determine current Perps price for order placement.');
|
|
427
492
|
const usdAmount = ${JSON.stringify(amount)};
|
|
428
493
|
const leverage = ${JSON.stringify(leverage)};
|
|
429
|
-
const
|
|
494
|
+
const orderType = ${JSON.stringify(orderType)};
|
|
495
|
+
const limitPrice = ${JSON.stringify(limitPrice)};
|
|
496
|
+
const orderParams = orderType === 'limit' ? {
|
|
430
497
|
symbol: ${JSON.stringify(symbol)},
|
|
431
|
-
isBuy: ${JSON.stringify(
|
|
498
|
+
isBuy: ${JSON.stringify(isBuy)},
|
|
499
|
+
size: ((Number(usdAmount) * Number(leverage)) / Number(limitPrice)).toString(),
|
|
500
|
+
orderType,
|
|
501
|
+
price: String(limitPrice),
|
|
502
|
+
timeInForce: 'GTC',
|
|
503
|
+
leverage,
|
|
504
|
+
currentPrice,
|
|
505
|
+
priceAtCalculation: currentPrice,
|
|
506
|
+
maxSlippageBps: ${JSON.stringify(Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300))},
|
|
507
|
+
} : {
|
|
508
|
+
symbol: ${JSON.stringify(symbol)},
|
|
509
|
+
isBuy: ${JSON.stringify(isBuy)},
|
|
432
510
|
size: ((Number(usdAmount) * Number(leverage)) / currentPrice).toString(),
|
|
433
|
-
orderType
|
|
511
|
+
orderType,
|
|
434
512
|
leverage,
|
|
435
513
|
currentPrice,
|
|
514
|
+
priceAtCalculation: currentPrice,
|
|
436
515
|
usdAmount,
|
|
437
516
|
maxSlippageBps: ${JSON.stringify(Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300))},
|
|
438
517
|
};
|
|
@@ -446,8 +525,12 @@ export async function placeOrder(input) {
|
|
|
446
525
|
const message = error instanceof Error ? error.message : String(error);
|
|
447
526
|
throw new Error(`${message} (resolvedPrice=${resolvedPrice})`);
|
|
448
527
|
}
|
|
449
|
-
|
|
450
|
-
|
|
528
|
+
if (orderType === 'limit') {
|
|
529
|
+
await waitForSelectedOrderState(page, input, true, Number(input.node?.timeout_ms ?? 30000));
|
|
530
|
+
} else {
|
|
531
|
+
await waitForPositionPresent(page, symbol, Number(input.node?.timeout_ms ?? 30000));
|
|
532
|
+
}
|
|
533
|
+
return { action: input.action, market: symbol, side, orderType, amount, leverage, limitPrice, submitted: true, order, proofPath: 'background-perpsPlaceOrder' };
|
|
451
534
|
});
|
|
452
535
|
}
|
|
453
536
|
|
|
@@ -479,7 +562,16 @@ export async function ensureOrders(input) {
|
|
|
479
562
|
const close = await closeOrders(input);
|
|
480
563
|
return { ...(await assertOrders(input, false)), close };
|
|
481
564
|
}
|
|
482
|
-
if (state === 'open' || state === 'present')
|
|
565
|
+
if (state === 'open' || state === 'present') {
|
|
566
|
+
const current = await withExtensionPage(input, async (page) => ({
|
|
567
|
+
orders: await readOpenOrders(page),
|
|
568
|
+
}));
|
|
569
|
+
let order = null;
|
|
570
|
+
if (selectedItems(input, current.orders).length === 0) {
|
|
571
|
+
order = await placeOrder({ ...input, node: { ...input.node, order_type: 'limit' } });
|
|
572
|
+
}
|
|
573
|
+
return { ...(await assertOrders(input, true)), order };
|
|
574
|
+
}
|
|
483
575
|
throw new Error(`metamask.perps.ensure_orders received unsupported state: ${state}`);
|
|
484
576
|
}
|
|
485
577
|
|
|
@@ -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
|
|
|
@@ -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
|
{
|
|
@@ -1101,6 +1119,15 @@
|
|
|
1101
1119
|
"type": "string",
|
|
1102
1120
|
"description": "EVM address to read for; defaults to the canonical wallet fixture account."
|
|
1103
1121
|
},
|
|
1122
|
+
"network": {
|
|
1123
|
+
"type": "string",
|
|
1124
|
+
"enum": [
|
|
1125
|
+
"testnet",
|
|
1126
|
+
"mainnet"
|
|
1127
|
+
],
|
|
1128
|
+
"default": "testnet",
|
|
1129
|
+
"description": "Controller network for this isolated Core assertion."
|
|
1130
|
+
},
|
|
1104
1131
|
"timeout_ms": {
|
|
1105
1132
|
"type": "number"
|
|
1106
1133
|
}
|
|
@@ -1122,7 +1149,7 @@
|
|
|
1122
1149
|
}
|
|
1123
1150
|
],
|
|
1124
1151
|
"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
|
|
1152
|
+
"safety_notes": "Read-only HyperLiquid query on the explicitly selected network; must not inject controller state to fabricate proof.",
|
|
1126
1153
|
"execution_capabilities": []
|
|
1127
1154
|
},
|
|
1128
1155
|
{
|
|
@@ -1458,6 +1485,15 @@
|
|
|
1458
1485
|
],
|
|
1459
1486
|
"description": "Optional position/order side filter."
|
|
1460
1487
|
},
|
|
1488
|
+
"network": {
|
|
1489
|
+
"type": "string",
|
|
1490
|
+
"enum": [
|
|
1491
|
+
"testnet",
|
|
1492
|
+
"mainnet"
|
|
1493
|
+
],
|
|
1494
|
+
"default": "testnet",
|
|
1495
|
+
"description": "Controller network for this isolated Core assertion."
|
|
1496
|
+
},
|
|
1461
1497
|
"timeout_ms": {
|
|
1462
1498
|
"type": "number"
|
|
1463
1499
|
},
|
|
@@ -1490,7 +1526,7 @@
|
|
|
1490
1526
|
}
|
|
1491
1527
|
],
|
|
1492
1528
|
"proof_effect": "E2E validation must record this action in trace.json; live-proof actions include liveAdapter/proofPath or target runtime output.",
|
|
1493
|
-
"safety_notes": "
|
|
1529
|
+
"safety_notes": "Read-only HyperLiquid query on the explicitly selected network; must not inject mid-recipe state to fabricate proof.",
|
|
1494
1530
|
"execution_capabilities": []
|
|
1495
1531
|
},
|
|
1496
1532
|
{
|
|
@@ -782,7 +782,11 @@
|
|
|
782
782
|
"type": "string"
|
|
783
783
|
},
|
|
784
784
|
"value": {
|
|
785
|
-
"type":
|
|
785
|
+
"type": [
|
|
786
|
+
"string",
|
|
787
|
+
"number"
|
|
788
|
+
],
|
|
789
|
+
"description": "Text or a typed numeric recipe parameter; numbers are entered as their decimal text."
|
|
786
790
|
},
|
|
787
791
|
"text": {
|
|
788
792
|
"type": "string"
|
|
@@ -2015,7 +2019,7 @@
|
|
|
2015
2019
|
{
|
|
2016
2020
|
"name": "metamask.perps.ensure_orders",
|
|
2017
2021
|
"owner": "metamask",
|
|
2018
|
-
"description": "extension
|
|
2022
|
+
"description": "extension Converge selected testnet orders to open or absent, then independently assert the final state. Creating an open state uses a resting limit order.",
|
|
2019
2023
|
"schema": {
|
|
2020
2024
|
"type": "object",
|
|
2021
2025
|
"properties": {
|
|
@@ -2098,6 +2102,16 @@
|
|
|
2098
2102
|
"timeout_ms": {
|
|
2099
2103
|
"type": "number"
|
|
2100
2104
|
},
|
|
2105
|
+
"notional": {
|
|
2106
|
+
"type": "number",
|
|
2107
|
+
"default": 10,
|
|
2108
|
+
"description": "Small testnet USD notional used only when creating a missing open order."
|
|
2109
|
+
},
|
|
2110
|
+
"leverage": {
|
|
2111
|
+
"type": "number",
|
|
2112
|
+
"default": 3,
|
|
2113
|
+
"description": "Testnet leverage used only when creating a missing open order."
|
|
2114
|
+
},
|
|
2101
2115
|
"state": {
|
|
2102
2116
|
"type": "string",
|
|
2103
2117
|
"enum": [
|
|
@@ -2124,6 +2138,18 @@
|
|
|
2124
2138
|
"mode": "all",
|
|
2125
2139
|
"intent": "Converge Perps orders to the requested state"
|
|
2126
2140
|
}
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
"description": "Ensure one small resting ETH short order exists",
|
|
2144
|
+
"node": {
|
|
2145
|
+
"action": "metamask.perps.ensure_orders",
|
|
2146
|
+
"market": "ETH",
|
|
2147
|
+
"side": "short",
|
|
2148
|
+
"state": "open",
|
|
2149
|
+
"notional": 10,
|
|
2150
|
+
"leverage": 2,
|
|
2151
|
+
"intent": "Converge ETH open orders to one deterministic testnet precondition"
|
|
2152
|
+
}
|
|
2127
2153
|
}
|
|
2128
2154
|
],
|
|
2129
2155
|
"proof_effect": "E2E validation must record this action in trace.json; live-proof actions include liveAdapter/proofPath or target runtime output.",
|
|
@@ -316,6 +316,10 @@
|
|
|
316
316
|
},
|
|
317
317
|
"settle": {
|
|
318
318
|
"type": "boolean"
|
|
319
|
+
},
|
|
320
|
+
"long_press": {
|
|
321
|
+
"type": "boolean",
|
|
322
|
+
"description": "Invoke the target component's long-press gesture. Requires identifier targeting."
|
|
319
323
|
}
|
|
320
324
|
},
|
|
321
325
|
"additionalProperties": false
|
|
@@ -336,6 +340,15 @@
|
|
|
336
340
|
"text": "Ethereum",
|
|
337
341
|
"intent": "Press the component containing the visible Ethereum label"
|
|
338
342
|
}
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"description": "Long-press a component by stable React Native testID",
|
|
346
|
+
"node": {
|
|
347
|
+
"action": "ui.press",
|
|
348
|
+
"test_id": "keypad-delete-button",
|
|
349
|
+
"long_press": true,
|
|
350
|
+
"intent": "Clear the current keypad value through its human reset gesture"
|
|
351
|
+
}
|
|
339
352
|
}
|
|
340
353
|
]
|
|
341
354
|
},
|
|
@@ -2143,7 +2156,7 @@
|
|
|
2143
2156
|
{
|
|
2144
2157
|
"name": "metamask.perps.ensure_orders",
|
|
2145
2158
|
"owner": "metamask",
|
|
2146
|
-
"description": "mobile
|
|
2159
|
+
"description": "mobile Converge selected testnet orders to open or absent, then independently assert the final state. Creating an open state uses a resting limit order.",
|
|
2147
2160
|
"schema": {
|
|
2148
2161
|
"type": "object",
|
|
2149
2162
|
"properties": {
|
|
@@ -2226,6 +2239,16 @@
|
|
|
2226
2239
|
"timeout_ms": {
|
|
2227
2240
|
"type": "number"
|
|
2228
2241
|
},
|
|
2242
|
+
"notional": {
|
|
2243
|
+
"type": "number",
|
|
2244
|
+
"default": 10,
|
|
2245
|
+
"description": "Small testnet USD notional used only when creating a missing open order."
|
|
2246
|
+
},
|
|
2247
|
+
"leverage": {
|
|
2248
|
+
"type": "number",
|
|
2249
|
+
"default": 3,
|
|
2250
|
+
"description": "Testnet leverage used only when creating a missing open order."
|
|
2251
|
+
},
|
|
2229
2252
|
"state": {
|
|
2230
2253
|
"type": "string",
|
|
2231
2254
|
"enum": [
|
|
@@ -2252,6 +2275,18 @@
|
|
|
2252
2275
|
"mode": "all",
|
|
2253
2276
|
"intent": "Converge Perps orders to the requested state"
|
|
2254
2277
|
}
|
|
2278
|
+
},
|
|
2279
|
+
{
|
|
2280
|
+
"description": "Ensure one small resting ETH short order exists",
|
|
2281
|
+
"node": {
|
|
2282
|
+
"action": "metamask.perps.ensure_orders",
|
|
2283
|
+
"market": "ETH",
|
|
2284
|
+
"side": "short",
|
|
2285
|
+
"state": "open",
|
|
2286
|
+
"notional": 10,
|
|
2287
|
+
"leverage": 2,
|
|
2288
|
+
"intent": "Converge ETH open orders to one deterministic testnet precondition"
|
|
2289
|
+
}
|
|
2255
2290
|
}
|
|
2256
2291
|
],
|
|
2257
2292
|
"proof_effect": "E2E validation must record this action in trace.json; live-proof actions include liveAdapter/proofPath or target runtime output.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@farmslot/agent-runtime": "^0.2.0",
|
|
23
23
|
"@farmslot/protocol": "^0.11.0",
|
|
24
|
-
"@farmslot/recipe-harness": "^0.9.
|
|
24
|
+
"@farmslot/recipe-harness": "^0.9.1",
|
|
25
25
|
"commander": "^12.0.0",
|
|
26
26
|
"es-module-lexer": "2.3.1",
|
|
27
27
|
"esbuild": "0.28.1",
|