@deeeed/metamask-harness 0.18.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 +200 -0
- package/README.md +4 -7
- package/adapters/core/inject.sh +1 -6
- package/adapters/extension/inject.mjs +1 -4
- package/adapters/extension/verify.sh +1 -1
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +33 -0
- package/adapters/mobile/inject.sh +5 -4
- package/adapters/mobile/verify.sh +15 -19
- package/adapters/shared/harness-source-fingerprint.mjs +14 -12
- package/dist/adapters/harness-freshness.js +49 -0
- package/dist/adapters.js +6 -1
- package/dist/cli.js +0 -4
- package/dist/command-contract.js +51 -40
- package/dist/command-journal.js +50 -12
- package/dist/commands/call.js +95 -20
- package/dist/commands/check.js +1 -1
- package/dist/commands/completion-candidates.js +7 -19
- package/dist/commands/fixtures.js +50 -1
- package/dist/commands/last.js +9 -1
- package/dist/commands/launch/extension.js +2 -2
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/list-executables.js +56 -20
- package/dist/commands/manifest.js +28 -12
- package/dist/commands/parse-args.js +55 -3
- package/dist/commands/provision.js +0 -1
- package/dist/commands/run-engine.js +514 -336
- package/dist/commands/run.js +25 -37
- package/dist/commands/shared.js +1 -1
- package/dist/heal-bounds.js +5 -0
- package/dist/live-adapter-contract.js +15 -2
- package/dist/mm-harness-cli.js +22 -30
- package/dist/run-diagnostics.js +1 -1
- package/dist/run-recording.js +1 -1
- package/dist/runner.js +24 -2
- package/docs/CONTRIBUTING.md +2 -3
- package/docs/QA.md +0 -1
- package/docs/RECIPES.md +67 -91
- package/library/README.md +5 -5
- package/library/actions/core/perps/_controller.mjs +33 -1
- package/library/actions/core/perps/assert_orders.mjs +22 -12
- package/library/actions/core/perps/assert_positions.mjs +22 -12
- package/library/actions/core/perps/close_orders.mjs +2 -0
- package/library/actions/core/perps/close_positions.mjs +2 -0
- package/library/actions/core/perps/ensure_orders.mjs +4 -2
- package/library/actions/core/perps/ensure_positions.mjs +4 -2
- package/library/actions/core/perps/place_order.mjs +7 -3
- package/library/actions/extension/perps/assert_orders.mjs +2 -1
- package/library/actions/extension/perps/assert_positions.mjs +2 -1
- package/library/actions/extension/perps/perps.mjs +151 -30
- package/library/actions/mobile/perps/assert_orders.mjs +2 -1
- package/library/actions/mobile/perps/assert_positions.mjs +2 -1
- package/library/actions/mobile/perps/perps.mjs +148 -22
- package/library/library.json +1 -1
- package/library/manifests/core.action-manifest.json +1204 -411
- package/library/manifests/extension.action-manifest.json +1536 -656
- package/library/manifests/mobile.action-manifest.json +1728 -758
- package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
- package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
- package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
- package/library/recipes/perps/lifecycle.recipe.json +136 -180
- package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
- package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.mobile.recipe.json +37 -51
- package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
- package/library/recipes/perps/read-markets.core.recipe.json +29 -31
- package/library/recipes/perps/smoke.core.recipe.json +29 -32
- package/library/recipes/perps/smoke.extension.recipe.json +41 -44
- package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
- package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
- package/library/recipes/runner/action-validation.extension.recipe.json +312 -405
- package/library/recipes/runner/action-validation.mobile.recipe.json +316 -409
- package/library/recipes/runner/smoke.core.recipe.json +18 -20
- package/library/recipes/runner/smoke.extension.recipe.json +23 -24
- package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
- package/library/recipes/wallet/smoke.extension.recipe.json +33 -35
- package/library/recipes/wallet/smoke.mobile.recipe.json +33 -35
- package/package.json +3 -3
- package/scripts/completions.sh +1 -4
- package/dist/adapters/extension/harness-freshness.js +0 -39
- package/dist/commands/flows.js +0 -91
- package/library/flows/perps.flows.json +0 -64
|
@@ -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) {
|
|
@@ -34,20 +37,68 @@ function configuredSymbols(input, items) {
|
|
|
34
37
|
const explicit = input.node?.markets ?? input.node?.symbols ?? selector.markets ?? selector.symbols;
|
|
35
38
|
if (Array.isArray(explicit) && explicit.length > 0) return uniqueSymbols(explicit.map(normalizeMarketSymbol));
|
|
36
39
|
if (typeof explicit === 'string' && explicit.length > 0) return uniqueSymbols(explicit.split(',').map((part) => normalizeMarketSymbol(part.trim())).filter(Boolean));
|
|
37
|
-
|
|
40
|
+
const single = input.node?.market ?? input.node?.symbol ?? selector.market ?? selector.symbol;
|
|
41
|
+
return single ? [normalizeMarketSymbol(single)] : [];
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
function selectedItems(input, items) {
|
|
41
|
-
const
|
|
44
|
+
export function selectedItems(input, items) {
|
|
45
|
+
const requested = configuredSymbols(input, items);
|
|
46
|
+
const symbols = requested.length > 0 ? new Set(requested) : null;
|
|
42
47
|
const selector = input.node?.selector && typeof input.node.selector === 'object' ? input.node.selector : {};
|
|
43
48
|
const side = input.node?.side ?? selector.side;
|
|
44
49
|
return items.filter((item) => {
|
|
45
|
-
if (!symbols.has(symbolForItem(item))) return false;
|
|
50
|
+
if (symbols && !symbols.has(symbolForItem(item))) return false;
|
|
46
51
|
if (side && sideForItem(item) && sideForItem(item) !== String(side).toLowerCase()) return false;
|
|
47
52
|
return true;
|
|
48
53
|
});
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
function requireExplicitSelection(input) {
|
|
57
|
+
const node = input.node ?? {};
|
|
58
|
+
const selector = node.selector && typeof node.selector === 'object' ? node.selector : {};
|
|
59
|
+
const values = [node.market, node.symbol, node.markets, node.symbols, selector.market, selector.symbol, selector.markets, selector.symbols];
|
|
60
|
+
const selected = values.some((value) => Array.isArray(value) ? value.length > 0 : value != null && String(value).trim() !== '');
|
|
61
|
+
if (!selected && node.mode !== 'all' && selector.mode !== 'all') {
|
|
62
|
+
throw new Error(`${input.action} requires market=<symbol> or mode=all.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function requirePlaceOrderInputs(input) {
|
|
67
|
+
const node = input.node ?? {};
|
|
68
|
+
if (node.market == null && node.symbol == null) throw new Error(`${input.action} requires market=<symbol>.`);
|
|
69
|
+
if (node.side == null) throw new Error(`${input.action} requires side=long or side=short.`);
|
|
70
|
+
if (node.amount == null && node.notional == null) throw new Error(`${input.action} requires amount=<usd> or notional=<usd>.`);
|
|
71
|
+
}
|
|
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
|
+
|
|
51
102
|
function uniqueSymbols(symbols) {
|
|
52
103
|
return Array.from(new Set(symbols.filter(Boolean)));
|
|
53
104
|
}
|
|
@@ -124,6 +175,20 @@ async function waitForPositionPresent(input, symbol, timeoutMs = 30000) {
|
|
|
124
175
|
return last;
|
|
125
176
|
}
|
|
126
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
|
+
|
|
127
192
|
function redactPosition(position) {
|
|
128
193
|
return {
|
|
129
194
|
symbol: position.symbol ?? position.coin ?? null,
|
|
@@ -250,20 +315,32 @@ export async function readOrders(input) {
|
|
|
250
315
|
}
|
|
251
316
|
|
|
252
317
|
export async function assertPositions(input, expectedOpen) {
|
|
253
|
-
|
|
254
|
-
const
|
|
318
|
+
requireExplicitSelection(input);
|
|
319
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
320
|
+
const matching = await waitForSelectedState(
|
|
321
|
+
input,
|
|
322
|
+
readPositions,
|
|
323
|
+
expectedOpen,
|
|
324
|
+
timeoutMs,
|
|
325
|
+
);
|
|
255
326
|
const hasPosition = matching.length > 0;
|
|
256
|
-
if (expectedOpen && !hasPosition) throw new Error(
|
|
257
|
-
if (!expectedOpen && hasPosition) throw new Error(`Expected no
|
|
327
|
+
if (expectedOpen && !hasPosition) throw new Error('Expected at least one matching open Perps position, but found none.');
|
|
328
|
+
if (!expectedOpen && hasPosition) throw new Error(`Expected no matching open Perps positions, but found ${matching.length}.`);
|
|
258
329
|
return { action: input.action, expectedOpen, matchingCount: matching.length, positions: matching.map(redactPosition), source: 'mobile-perps-controller' };
|
|
259
330
|
}
|
|
260
331
|
|
|
261
332
|
export async function assertOrders(input, expectedOpen) {
|
|
262
|
-
|
|
263
|
-
const
|
|
333
|
+
requireExplicitSelection(input);
|
|
334
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
335
|
+
const matching = await waitForSelectedState(
|
|
336
|
+
input,
|
|
337
|
+
readOpenOrders,
|
|
338
|
+
expectedOpen,
|
|
339
|
+
timeoutMs,
|
|
340
|
+
);
|
|
264
341
|
const hasOrders = matching.length > 0;
|
|
265
|
-
if (expectedOpen && !hasOrders) throw new Error(
|
|
266
|
-
if (!expectedOpen && hasOrders) throw new Error(`Expected no
|
|
342
|
+
if (expectedOpen && !hasOrders) throw new Error('Expected at least one matching open Perps order, but found none.');
|
|
343
|
+
if (!expectedOpen && hasOrders) throw new Error(`Expected no matching open Perps orders, but found ${matching.length}.`);
|
|
267
344
|
return { action: input.action, expectedOpen, matchingCount: matching.length, orders: matching.map(redactOrder), source: 'mobile-perps-controller-open-orders' };
|
|
268
345
|
}
|
|
269
346
|
|
|
@@ -315,6 +392,7 @@ async function closePositionItem(input, position) {
|
|
|
315
392
|
}
|
|
316
393
|
|
|
317
394
|
export async function closePositions(input) {
|
|
395
|
+
requireExplicitSelection(input);
|
|
318
396
|
const before = await readPositions(input);
|
|
319
397
|
const matching = selectedItems(input, before);
|
|
320
398
|
const symbols = uniqueSymbols(matching.map(symbolForItem));
|
|
@@ -336,6 +414,7 @@ export async function closePositions(input) {
|
|
|
336
414
|
}
|
|
337
415
|
|
|
338
416
|
export async function closeOrders(input) {
|
|
417
|
+
requireExplicitSelection(input);
|
|
339
418
|
const before = await readOpenOrders(input);
|
|
340
419
|
const matching = selectedItems(input, before);
|
|
341
420
|
const symbols = uniqueSymbols(matching.map(symbolForItem));
|
|
@@ -380,15 +459,47 @@ export async function closeOrders(input) {
|
|
|
380
459
|
}
|
|
381
460
|
|
|
382
461
|
export async function placeOrder(input) {
|
|
462
|
+
requirePlaceOrderInputs(input);
|
|
383
463
|
const symbol = marketSymbol(input);
|
|
384
|
-
const side = String(input.node
|
|
385
|
-
const amount = String(input.node
|
|
386
|
-
const size = String(input.node?.size ?? '0.0001');
|
|
464
|
+
const side = String(input.node.side).toLowerCase();
|
|
465
|
+
const amount = String(input.node.amount ?? input.node.notional);
|
|
387
466
|
const leverage = Number(input.node?.leverage ?? 3);
|
|
388
467
|
const maxSlippageBps = Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300);
|
|
389
468
|
const isBuy = orderSideIsBuy(side);
|
|
469
|
+
const orderType = resolveOrderType(input);
|
|
390
470
|
const currentPrice = await currentPriceForOrder(input, symbol);
|
|
391
|
-
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)})`);
|
|
392
503
|
if (result?.success === false || result == null) {
|
|
393
504
|
throw new Error(`Failed to place ${symbol} ${side}: ${result?.error || JSON.stringify(result)}`);
|
|
394
505
|
}
|
|
@@ -396,12 +507,18 @@ export async function placeOrder(input) {
|
|
|
396
507
|
input,
|
|
397
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" }))',
|
|
398
509
|
);
|
|
399
|
-
|
|
400
|
-
|
|
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' };
|
|
401
516
|
}
|
|
402
517
|
|
|
403
518
|
export async function ensurePositions(input) {
|
|
404
|
-
|
|
519
|
+
if (input.node?.state == null) throw new Error('metamask.perps.ensure_positions requires state=open or state=none.');
|
|
520
|
+
requireExplicitSelection(input);
|
|
521
|
+
const state = String(input.node.state).toLowerCase();
|
|
405
522
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
406
523
|
const close = await closePositions(input);
|
|
407
524
|
return { ...(await assertPositions(input, false)), close };
|
|
@@ -415,12 +532,21 @@ export async function ensurePositions(input) {
|
|
|
415
532
|
}
|
|
416
533
|
|
|
417
534
|
export async function ensureOrders(input) {
|
|
418
|
-
|
|
535
|
+
if (input.node?.state == null) throw new Error('metamask.perps.ensure_orders requires state=open or state=none.');
|
|
536
|
+
requireExplicitSelection(input);
|
|
537
|
+
const state = String(input.node.state).toLowerCase();
|
|
419
538
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
420
539
|
const close = await closeOrders(input);
|
|
421
540
|
return { ...(await assertOrders(input, false)), close };
|
|
422
541
|
}
|
|
423
|
-
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
|
+
}
|
|
424
550
|
throw new Error(`metamask.perps.ensure_orders received unsupported state: ${state}`);
|
|
425
551
|
}
|
|
426
552
|
|
package/library/library.json
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
"schema_version": 1,
|
|
4
4
|
"name": "metamask",
|
|
5
5
|
"owner": "mm-harness-runner",
|
|
6
|
-
"description": "Canonical MetaMask
|
|
6
|
+
"description": "Canonical MetaMask library of reusable, parameterized recipes. Team and personal libraries layer on top and win resolution by default."
|
|
7
7
|
}
|