@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 +17 -0
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/metro-config.cjs +93 -0
- package/adapters/mobile/start-metro.sh +11 -0
- package/dist/recipe-security.js +2 -0
- package/library/actions/core/perps/_controller.mjs +7 -0
- package/library/actions/core/perps/assert_orders.mjs +92 -0
- package/library/actions/core/perps/place_order.mjs +273 -15
- package/library/actions/core/perps/update_position_tpsl.mjs +185 -0
- package/library/actions/mobile/platform/bridge.mjs +22 -16
- package/library/manifests/core.action-manifest.json +324 -5
- package/package.json +1 -1
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
|
package/adapters/manifest.json
CHANGED
|
@@ -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"
|
package/dist/recipe-security.js
CHANGED
|
@@ -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',
|