@deeeed/metamask-harness 0.22.0 → 0.23.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 CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.23.1 - 2026-07-29
6
+
7
+ ### Fixed
8
+
9
+ - Mobile recipe evidence no longer triggers Metro Fast Refresh: harness-managed Metro ignores checkout-local `temp` artifacts, and screenshot scratch files stay outside the watched project.
10
+
11
+ ## 0.23.0 - 2026-07-28
12
+
13
+ ### Added
14
+
15
+ - Core Perps actions support trigger orders, attached and partial TP/SL, and position TP/SL updates.
16
+
17
+ ### Fixed
18
+
19
+ - Core advanced-order mutations verify the submitted order fields, use executable trigger-limit defaults, honor bounded evidence polling, and classify mutation capabilities correctly.
20
+ - Core advanced-order camelCase aliases validate consistently with their documented snake_case forms.
21
+
5
22
  ## 0.22.0 - 2026-07-26
6
23
 
7
24
  ### Added
@@ -42,6 +42,14 @@
42
42
  "inputs": "--target --port --log --pid-file [--workers] [--clear]",
43
43
  "outputs": "Metro PID/launch metadata and redirected log; exit 0/1/2"
44
44
  },
45
+ {
46
+ "id": "mobile/metro-config",
47
+ "entry": "adapters/mobile/metro-config.cjs",
48
+ "kind": "module",
49
+ "purpose": "Preserve the product Metro config while excluding harness-owned checkout temp artifacts.",
50
+ "inputs": "base Metro config; env MM_HARNESS_METRO_PROJECT_ROOT, MM_HARNESS_METRO_BASE_CONFIG",
51
+ "outputs": "merged Metro config with resolver.blockList for <projectRoot>/temp"
52
+ },
45
53
  {
46
54
  "id": "mobile/stop-metro",
47
55
  "entry": "adapters/mobile/stop-metro.sh",
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const { createRequire } = require('node:module');
5
+ const path = require('node:path');
6
+ const { pathToFileURL } = require('node:url');
7
+
8
+ function escapeRegExp(value) {
9
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
+ }
11
+
12
+ function appendBlockList(blockList, pattern) {
13
+ if (Array.isArray(blockList)) return [...blockList, pattern];
14
+ return blockList instanceof RegExp ? [blockList, pattern] : [pattern];
15
+ }
16
+
17
+ function blockListFlags(blockList) {
18
+ const first = Array.isArray(blockList) ? blockList[0] : blockList;
19
+ return first instanceof RegExp ? first.flags : '';
20
+ }
21
+
22
+ function resolveExpoConfigLoader(projectRoot) {
23
+ const requireFromProject = createRequire(path.join(projectRoot, 'package.json'));
24
+ let requireUtilsPath;
25
+ try {
26
+ requireUtilsPath = requireFromProject.resolve('@expo/require-utils');
27
+ } catch (error) {
28
+ if (error?.code !== 'MODULE_NOT_FOUND') throw error;
29
+ return null;
30
+ }
31
+ const { loadModuleSync } = requireFromProject(requireUtilsPath);
32
+ return typeof loadModuleSync === 'function' ? loadModuleSync : null;
33
+ }
34
+
35
+ async function loadProductConfig(configPath, baseConfig, projectRoot) {
36
+ let loaded;
37
+ const expoConfigLoader = resolveExpoConfigLoader(projectRoot);
38
+ if (expoConfigLoader) {
39
+ loaded = expoConfigLoader(configPath);
40
+ } else {
41
+ try {
42
+ loaded = require(configPath);
43
+ } catch (error) {
44
+ if (error?.code !== 'ERR_REQUIRE_ESM') throw error;
45
+ loaded = await import(pathToFileURL(configPath).href);
46
+ }
47
+ }
48
+
49
+ const moduleValue = await loaded;
50
+ const exported =
51
+ moduleValue?.__esModule ||
52
+ moduleValue?.[Symbol.toStringTag] === 'Module'
53
+ ? moduleValue.default
54
+ : moduleValue;
55
+ const config = await exported;
56
+ return typeof config === 'function' ? config(baseConfig) : config;
57
+ }
58
+
59
+ module.exports = async function harnessMetroConfig(baseConfig) {
60
+ const projectRoot = path.resolve(
61
+ process.env.MM_HARNESS_METRO_PROJECT_ROOT || process.cwd(),
62
+ );
63
+ const productConfigPath = path.resolve(
64
+ process.env.MM_HARNESS_METRO_BASE_CONFIG ||
65
+ path.join(projectRoot, 'metro.config.js'),
66
+ );
67
+ let productConfig = baseConfig;
68
+ if (fs.existsSync(productConfigPath)) {
69
+ productConfig = await loadProductConfig(
70
+ productConfigPath,
71
+ baseConfig,
72
+ projectRoot,
73
+ );
74
+ }
75
+
76
+ const tempRoot = path.join(projectRoot, 'temp');
77
+ const existingBlockList =
78
+ productConfig.resolver?.blockList ?? baseConfig.resolver?.blockList;
79
+ const tempPattern = new RegExp(
80
+ `^${escapeRegExp(tempRoot)}(?:${escapeRegExp(path.sep)}|$)`,
81
+ blockListFlags(existingBlockList),
82
+ );
83
+ return {
84
+ ...productConfig,
85
+ resolver: {
86
+ ...productConfig.resolver,
87
+ blockList: appendBlockList(
88
+ existingBlockList,
89
+ tempPattern,
90
+ ),
91
+ },
92
+ };
93
+ };
@@ -49,6 +49,7 @@ fi
49
49
  # only ever inspect or signal a Metro listening on $PORT.
50
50
  # shellcheck disable=SC1091
51
51
  . "$SCRIPT_DIR/lib/metro-listener.sh"
52
+ MOBILE_METRO_REQUIRED_ENV="${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID} MM_HARNESS_METRO_PROJECT_ROOT EXPO_OVERRIDE_METRO_CONFIG"
52
53
 
53
54
  # RECIPE_RUNTIME_DIR (relative, validated) makes runtime state per-run so jobs
54
55
  # sharing one checkout do not collide on metro.log / metro.pid / metro.tmux.
@@ -98,6 +99,13 @@ set +a
98
99
  export EXPO_NO_TYPESCRIPT_SETUP=1
99
100
  export WATCHER_PORT="$(printf '%q' "$PORT")" METRO_PORT="$(printf '%q' "$PORT")"
100
101
  export METRO_MAX_WORKERS="$(printf '%q' "$METRO_WORKERS")"
102
+ export MM_HARNESS_METRO_PROJECT_ROOT="$(printf '%q' "$TARGET")"
103
+ if [ -n "\${EXPO_OVERRIDE_METRO_CONFIG:-}" ]; then
104
+ export MM_HARNESS_METRO_BASE_CONFIG="\$EXPO_OVERRIDE_METRO_CONFIG"
105
+ else
106
+ export MM_HARNESS_METRO_BASE_CONFIG=$(printf '%q' "$TARGET/metro.config.js")
107
+ fi
108
+ export EXPO_OVERRIDE_METRO_CONFIG="$(printf '%q' "$SCRIPT_DIR/metro-config.cjs")"
101
109
  yarn expo start --port "$(printf '%q' "$PORT")"$clear_flag 2>&1 | tee -a "$(printf '%q' "$LOG_FILE")"
102
110
  EOF
103
111
  chmod +x "$runner"
@@ -184,6 +192,9 @@ if ! start_metro_tmux; then
184
192
  export EXPO_NO_TYPESCRIPT_SETUP=1
185
193
  export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
186
194
  export METRO_MAX_WORKERS="${METRO_WORKERS}"
195
+ export MM_HARNESS_METRO_PROJECT_ROOT="$TARGET"
196
+ export MM_HARNESS_METRO_BASE_CONFIG="${EXPO_OVERRIDE_METRO_CONFIG:-$TARGET/metro.config.js}"
197
+ export EXPO_OVERRIDE_METRO_CONFIG="$SCRIPT_DIR/metro-config.cjs"
187
198
  launcher_args=(
188
199
  --target "$TARGET"
189
200
  --port "$PORT"
@@ -18,6 +18,7 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
18
18
  "metamask.perps.close_positions",
19
19
  "metamask.perps.close_orders",
20
20
  "metamask.perps.place_order",
21
+ "metamask.perps.update_position_tpsl",
21
22
  "metamask.perps.ensure_positions",
22
23
  "metamask.perps.ensure_orders",
23
24
  "metamask.perps.start_state",
@@ -27,6 +28,7 @@ const EXTERNAL_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
27
28
  "metamask.perps.close_positions",
28
29
  "metamask.perps.close_orders",
29
30
  "metamask.perps.place_order",
31
+ "metamask.perps.update_position_tpsl",
30
32
  "metamask.perps.ensure_positions",
31
33
  "metamask.perps.ensure_orders",
32
34
  "metamask.perps.start_state",
@@ -625,6 +625,13 @@ export function redactOrder(order) {
625
625
  size: order.size ?? order.sz ?? order.szi ?? null,
626
626
  price: order.price ?? order.limitPx ?? order.px ?? null,
627
627
  type: order.orderType ?? order.type ?? null,
628
+ // Trigger data, so evidence shows what a stop / take-profit placement
629
+ // actually round-tripped from the exchange.
630
+ triggerOrderType: order.triggerOrderType ?? null,
631
+ triggerPrice: order.triggerPrice ?? order.triggerPx ?? null,
632
+ detailedOrderType: order.detailedOrderType ?? null,
633
+ isTrigger: order.isTrigger ?? null,
634
+ reduceOnly: order.reduceOnly ?? null,
628
635
  };
629
636
  }
630
637
 
@@ -11,6 +11,13 @@ import {
11
11
  // over the controller's standalone getOpenOrders path (no signer / provider init
12
12
  // needed) — throws on mismatch so the recipe fails loudly. Mirrors
13
13
  // assert_positions.mjs.
14
+ //
15
+ // Optional expect_* fields additionally assert the TRIGGER DATA the exchange
16
+ // round-tripped for every matching order: expect_trigger_order_type (the
17
+ // normalized placement type, e.g. stop_market), expect_trigger_price,
18
+ // expect_execution (market | limit once triggered), expect_reduce_only, and
19
+ // expect_size (proves a partial TP/SL quantity). They use the controller's own
20
+ // field names (triggerOrderType, triggerPrice, orderType, reduceOnly, size).
14
21
 
15
22
  export function expectedOpen(input) {
16
23
  if (input.node?.state == null) throw new Error('metamask.perps.assert_orders requires state=open or state=none.');
@@ -20,6 +27,79 @@ export function expectedOpen(input) {
20
27
  throw new Error(`metamask.perps.assert_orders received unsupported state: ${state}`);
21
28
  }
22
29
 
30
+ /**
31
+ * Collect the optional trigger-data expectations from the node.
32
+ *
33
+ * @param input - Adapter input.
34
+ * @returns The expectations the node set, keyed by controller field name.
35
+ */
36
+ /**
37
+ * Whether the node narrowed matching to trigger orders only.
38
+ *
39
+ * @param input - Adapter input.
40
+ * @returns True when only trigger orders should be considered.
41
+ */
42
+ export function onlyTriggerOrders(input) {
43
+ const value = input.node?.only_trigger_orders ?? input.node?.onlyTriggerOrders;
44
+ return value === true || String(value).toLowerCase() === 'true';
45
+ }
46
+
47
+ export function expectedTriggerData(input) {
48
+ const node = input.node ?? {};
49
+ const expectations = {
50
+ triggerOrderType:
51
+ node.expect_trigger_order_type ?? node.expectTriggerOrderType,
52
+ triggerPrice: node.expect_trigger_price ?? node.expectTriggerPrice,
53
+ orderType: node.expect_execution ?? node.expectExecution,
54
+ reduceOnly: node.expect_reduce_only ?? node.expectReduceOnly,
55
+ size: node.expect_size ?? node.expectSize,
56
+ };
57
+ return Object.fromEntries(
58
+ Object.entries(expectations).filter(
59
+ ([, value]) => value !== undefined && value !== null,
60
+ ),
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Assert every matching order carries the expected trigger data.
66
+ *
67
+ * @param orders - Matching open orders.
68
+ * @param expectations - Expectations from `expectedTriggerData`.
69
+ */
70
+ export function assertTriggerData(orders, expectations) {
71
+ const fields = Object.keys(expectations);
72
+ if (fields.length === 0) return;
73
+
74
+ for (const order of orders) {
75
+ for (const field of fields) {
76
+ const expected = expectations[field];
77
+ const actual = order[field];
78
+ const expectedNumber = Number(expected);
79
+ const actualNumber = Number(actual);
80
+ const numeric =
81
+ typeof expected !== 'boolean' &&
82
+ expected !== '' &&
83
+ actual !== null &&
84
+ actual !== undefined &&
85
+ Number.isFinite(expectedNumber) &&
86
+ Number.isFinite(actualNumber);
87
+ const matches =
88
+ typeof expected === 'boolean'
89
+ ? Boolean(actual) === expected
90
+ : // Prices and sizes round-trip with exchange formatting
91
+ // ('44000' -> '44000.0'), so compare them numerically.
92
+ (numeric && expectedNumber === actualNumber) ||
93
+ String(actual) === String(expected);
94
+ if (!matches) {
95
+ throw new Error(
96
+ `Open Perps order ${order.orderId ?? '?'} (${order.symbol ?? '?'}) has ${field}=${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}.`,
97
+ );
98
+ }
99
+ }
100
+ }
101
+ }
102
+
23
103
  export async function assertOrders(input, expectOpen = expectedOpen(input)) {
24
104
  requireExplicitSelection(input);
25
105
  const { controller, accountAddress, network } = await getCoreController(input);
@@ -33,6 +113,11 @@ export async function assertOrders(input, expectOpen = expectedOpen(input)) {
33
113
  userAddress: accountAddress,
34
114
  });
35
115
  matching = selectedItems(input, orders);
116
+ if (onlyTriggerOrders(input)) {
117
+ // Narrow to the trigger orders on the market so expectations are not
118
+ // applied to an unrelated parent order resting alongside them.
119
+ matching = matching.filter((order) => order.isTrigger === true);
120
+ }
36
121
  if (expectOpen ? matching.length > 0 : matching.length === 0) break;
37
122
  if (Date.now() >= deadline) break;
38
123
  await new Promise((resolve) =>
@@ -44,6 +129,11 @@ export async function assertOrders(input, expectOpen = expectedOpen(input)) {
44
129
  if (expectOpen && !hasOrder) {
45
130
  throw new Error('Expected at least one matching open Perps order, but found none.');
46
131
  }
132
+
133
+ const triggerExpectations = expectedTriggerData(input);
134
+ if (expectOpen) {
135
+ assertTriggerData(matching, triggerExpectations);
136
+ }
47
137
  if (!expectOpen && hasOrder) {
48
138
  throw new Error(`Expected no matching open Perps orders, but found ${matching.length}.`);
49
139
  }
@@ -54,6 +144,8 @@ export async function assertOrders(input, expectOpen = expectedOpen(input)) {
54
144
  network,
55
145
  account: accountAddress,
56
146
  expectedOpen: expectOpen,
147
+ expectedTrigger:
148
+ Object.keys(triggerExpectations).length === 0 ? null : triggerExpectations,
57
149
  matchingCount: matching.length,
58
150
  orders: matching.map(redactOrder),
59
151
  proofPath: 'perps-controller-getOpenOrders',