@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +200 -0
  2. package/README.md +4 -7
  3. package/adapters/core/inject.sh +1 -6
  4. package/adapters/extension/inject.mjs +1 -4
  5. package/adapters/extension/verify.sh +1 -1
  6. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +33 -0
  7. package/adapters/mobile/inject.sh +5 -4
  8. package/adapters/mobile/verify.sh +15 -19
  9. package/adapters/shared/harness-source-fingerprint.mjs +14 -12
  10. package/dist/adapters/harness-freshness.js +49 -0
  11. package/dist/adapters.js +6 -1
  12. package/dist/cli.js +0 -4
  13. package/dist/command-contract.js +51 -40
  14. package/dist/command-journal.js +50 -12
  15. package/dist/commands/call.js +95 -20
  16. package/dist/commands/check.js +1 -1
  17. package/dist/commands/completion-candidates.js +7 -19
  18. package/dist/commands/fixtures.js +50 -1
  19. package/dist/commands/last.js +9 -1
  20. package/dist/commands/launch/extension.js +2 -2
  21. package/dist/commands/launch/mobile.js +2 -0
  22. package/dist/commands/list-executables.js +56 -20
  23. package/dist/commands/manifest.js +28 -12
  24. package/dist/commands/parse-args.js +55 -3
  25. package/dist/commands/provision.js +0 -1
  26. package/dist/commands/run-engine.js +514 -336
  27. package/dist/commands/run.js +25 -37
  28. package/dist/commands/shared.js +1 -1
  29. package/dist/heal-bounds.js +5 -0
  30. package/dist/live-adapter-contract.js +15 -2
  31. package/dist/mm-harness-cli.js +22 -30
  32. package/dist/run-diagnostics.js +1 -1
  33. package/dist/run-recording.js +1 -1
  34. package/dist/runner.js +24 -2
  35. package/docs/CONTRIBUTING.md +2 -3
  36. package/docs/QA.md +0 -1
  37. package/docs/RECIPES.md +67 -91
  38. package/library/README.md +5 -5
  39. package/library/actions/core/perps/_controller.mjs +33 -1
  40. package/library/actions/core/perps/assert_orders.mjs +22 -12
  41. package/library/actions/core/perps/assert_positions.mjs +22 -12
  42. package/library/actions/core/perps/close_orders.mjs +2 -0
  43. package/library/actions/core/perps/close_positions.mjs +2 -0
  44. package/library/actions/core/perps/ensure_orders.mjs +4 -2
  45. package/library/actions/core/perps/ensure_positions.mjs +4 -2
  46. package/library/actions/core/perps/place_order.mjs +7 -3
  47. package/library/actions/extension/perps/assert_orders.mjs +2 -1
  48. package/library/actions/extension/perps/assert_positions.mjs +2 -1
  49. package/library/actions/extension/perps/perps.mjs +151 -30
  50. package/library/actions/mobile/perps/assert_orders.mjs +2 -1
  51. package/library/actions/mobile/perps/assert_positions.mjs +2 -1
  52. package/library/actions/mobile/perps/perps.mjs +148 -22
  53. package/library/library.json +1 -1
  54. package/library/manifests/core.action-manifest.json +1204 -411
  55. package/library/manifests/extension.action-manifest.json +1536 -656
  56. package/library/manifests/mobile.action-manifest.json +1728 -758
  57. package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
  58. package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
  59. package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
  60. package/library/recipes/perps/lifecycle.recipe.json +136 -180
  61. package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
  62. package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
  63. package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
  64. package/library/recipes/perps/performance.mobile.recipe.json +37 -51
  65. package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
  66. package/library/recipes/perps/read-markets.core.recipe.json +29 -31
  67. package/library/recipes/perps/smoke.core.recipe.json +29 -32
  68. package/library/recipes/perps/smoke.extension.recipe.json +41 -44
  69. package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
  70. package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
  71. package/library/recipes/runner/action-validation.extension.recipe.json +312 -405
  72. package/library/recipes/runner/action-validation.mobile.recipe.json +316 -409
  73. package/library/recipes/runner/smoke.core.recipe.json +18 -20
  74. package/library/recipes/runner/smoke.extension.recipe.json +23 -24
  75. package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
  76. package/library/recipes/wallet/smoke.extension.recipe.json +33 -35
  77. package/library/recipes/wallet/smoke.mobile.recipe.json +33 -35
  78. package/package.json +3 -3
  79. package/scripts/completions.sh +1 -4
  80. package/dist/adapters/extension/harness-freshness.js +0 -39
  81. package/dist/commands/flows.js +0 -91
  82. 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
- return String(item?.side ?? item?.direction ?? '').toLowerCase();
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
- return [marketSymbol(input)];
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 symbols = new Set(configuredSymbols(input, items));
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
- const positions = await readPositions(input);
254
- const matching = selectedItems(input, positions);
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(`Expected selected Perps position(s), but none matched ${JSON.stringify(input.node)}.`);
257
- if (!expectedOpen && hasPosition) throw new Error(`Expected no selected Perps positions, but ${matching.length} matched ${JSON.stringify(input.node)}.`);
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
- const orders = await readOpenOrders(input);
263
- const matching = selectedItems(input, orders);
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(`Expected selected Perps order(s), but none matched ${JSON.stringify(input.node)}.`);
266
- if (!expectedOpen && hasOrders) throw new Error(`Expected no selected Perps orders, but ${matching.length} matched ${JSON.stringify(input.node)}.`);
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?.side ?? 'long').toLowerCase();
385
- const amount = String(input.node?.amount ?? input.node?.notional ?? '11');
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 result = await evalAsync(input, `Engine.context.PerpsController.placeOrder({ symbol: ${JSON.stringify(symbol)}, isBuy: ${JSON.stringify(isBuy)}, orderType: 'market', size: ${JSON.stringify(size)}, usdAmount: ${JSON.stringify(amount)}, leverage: ${JSON.stringify(leverage)}, currentPrice: ${JSON.stringify(currentPrice)}, maxSlippageBps: ${JSON.stringify(maxSlippageBps)} }).then(function(r){return JSON.stringify(r)})`);
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
- await waitForPositionPresent(input, symbol, Number(input.node?.timeout_ms ?? 30000));
400
- return { action: input.action, market: symbol, side, amount, size, leverage, submitted: true, result, refresh, proofPath: 'mobile-perps-controller-place-order' };
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
- const state = String(input.node?.state ?? input.node?.position ?? 'none').toLowerCase();
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
- const state = String(input.node?.state ?? input.node?.orders ?? 'none').toLowerCase();
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') return assertOrders(input, true);
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
 
@@ -3,5 +3,5 @@
3
3
  "schema_version": 1,
4
4
  "name": "metamask",
5
5
  "owner": "mm-harness-runner",
6
- "description": "Canonical MetaMask recipe library: minimal, stable flows every recipe can compose. Team and personal libraries layer on top and win resolution by default."
6
+ "description": "Canonical MetaMask library of reusable, parameterized recipes. Team and personal libraries layer on top and win resolution by default."
7
7
  }