@deeeed/metamask-harness 0.38.1 → 0.40.0
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 +58 -0
- package/adapters/extension/lib/playwright-cdp.cjs +17 -1
- package/adapters/extension/sidepanel-toggle.sh +135 -13
- package/dist/adapters/mobile/release-artifact.js +65 -0
- package/dist/adapters.js +27 -17
- package/dist/command-contract.js +10 -2
- package/dist/commands/call.js +7 -4
- package/dist/commands/launch/index.js +55 -3
- package/dist/commands/parse-args.js +6 -0
- package/dist/commands/run-engine.js +75 -39
- package/dist/funding-execution-context.js +1386 -0
- package/dist/live-adapter-contract.js +3 -1
- package/dist/metamask-action-validation.js +170 -3
- package/dist/mm-harness-cli.js +8 -2
- package/dist/recipe-security.js +5 -12
- package/dist/runner.js +102 -6
- package/docs/RECIPES.md +74 -1
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +6 -5
- package/library/actions/extension/assets/open_details.mjs +30 -0
- package/library/actions/extension/assets/set_token_visibility.mjs +5 -0
- package/library/actions/extension/networks/add_chainlist.mjs +4 -0
- package/library/actions/extension/networks/add_custom.mjs +4 -0
- package/library/actions/extension/networks/custom_network.mjs +48 -1
- package/library/actions/extension/perps/assert_visible_consistency.mjs +366 -0
- package/library/actions/extension/perps/close_visible_position.mjs +299 -37
- package/library/actions/extension/perps/compare_provider_market.mjs +77 -0
- package/library/actions/extension/perps/edit_margin.mjs +285 -44
- package/library/actions/extension/perps/mutation-receipt.mjs +781 -0
- package/library/actions/extension/perps/perps.mjs +1029 -363
- package/library/actions/extension/perps/read_funds_confirmation.mjs +126 -13
- package/library/actions/extension/perps/read_snapshot.mjs +124 -3
- package/library/actions/extension/perps/read_visible_state.mjs +63 -5
- package/library/actions/extension/perps/search_markets.mjs +32 -0
- package/library/actions/extension/perps/select_activity_filter.mjs +81 -18
- package/library/actions/extension/perps/select_market_filter.mjs +48 -4
- package/library/actions/extension/perps/set_market_favorite.mjs +6 -1
- package/library/actions/extension/perps/update_position_tpsl.mjs +251 -69
- package/library/actions/extension/perps/visible-mutation-identity.mjs +164 -0
- package/library/actions/extension/platform/cdp.mjs +88 -9
- package/library/actions/extension/settings/set_basic_functionality.mjs +35 -7
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +2 -6
- package/library/actions/extension/swap_bridge/set_amount.mjs +11 -2
- package/library/actions/extension/ui/navigate.mjs +45 -5
- package/library/actions/extension/wallet/import.mjs +70 -29
- package/library/actions/extension/wallet/lock.mjs +62 -5
- package/library/actions/extension/wallet/visible-session.mjs +218 -0
- package/library/actions/mobile/assets/import_custom_token.mjs +181 -0
- package/library/actions/mobile/assets/open_details.mjs +43 -0
- package/library/actions/mobile/assets/read_details.mjs +96 -0
- package/library/actions/mobile/assets/set_token_visibility.mjs +302 -0
- package/library/actions/mobile/assets/verify_sorting.mjs +104 -0
- package/library/actions/mobile/networks/add_custom.mjs +125 -0
- package/library/actions/mobile/networks/network-management.mjs +132 -0
- package/library/actions/mobile/networks/read_visible_state.mjs +43 -0
- package/library/actions/mobile/networks/remove_custom.mjs +100 -0
- package/library/actions/mobile/perps/search_markets.mjs +46 -0
- package/library/actions/mobile/perps/set_market_favorite.mjs +117 -0
- package/library/actions/mobile/platform/native-session-name.mjs +14 -0
- package/library/actions/mobile/platform/native-session.mjs +59 -0
- package/library/actions/mobile/platform/observe-ui.mjs +9 -1
- package/library/actions/mobile/swap_bridge/native-session.mjs +10 -45
- package/library/actions/mobile/swap_bridge/select_assets.mjs +15 -1
- package/library/actions/mobile/swap_bridge/set_amount.mjs +1 -1
- package/library/actions/mobile/swap_bridge/set_max_amount.mjs +55 -0
- package/library/actions/mobile/swap_bridge/set_slippage.mjs +96 -0
- package/library/actions/mobile/swap_bridge/submit_transaction.mjs +141 -0
- package/library/actions/mobile/ui/native-navigation.mjs +120 -23
- package/library/actions/mobile/ui/navigate.mjs +13 -2
- package/library/actions/mobile/wallet/import.mjs +17 -10
- package/library/actions/mobile/wallet/lock.mjs +14 -0
- package/library/actions/mobile/wallet/native-ui.mjs +88 -15
- package/library/actions/shared/perps/visible-state.mjs +33 -11
- package/library/actions/shared/swap-bridge/visible-state.mjs +15 -0
- package/library/manifests/extension.action-manifest.json +1707 -1241
- package/library/manifests/mobile.action-manifest.json +308 -1
- package/library/recipes/assets/release-token-details.recipe.json +5 -4
- package/library/recipes/assets/release-token-hide-readd.recipe.json +2 -0
- package/library/recipes/{assets → extension/assets}/release-custom-network-token-persistence.recipe.json +1 -0
- package/library/recipes/{perps → extension/perps}/release-activity-filters.recipe.json +5 -5
- package/library/recipes/{perps → extension/perps}/release-funds-flows.recipe.json +3 -3
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +24 -19
- package/library/recipes/extension/perps/release-market-filters.recipe.json +97 -0
- package/library/recipes/extension/perps/release-order-entry.recipe.json +4 -1
- package/library/recipes/extension/perps/release-order-validation.recipe.json +53 -14
- package/library/recipes/extension/perps/release-visible-consistency.recipe.json +47 -0
- package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +76 -0
- package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +90 -0
- package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +58 -0
- package/library/recipes/perps/release-live-market-order.recipe.json +30 -13
- package/library/recipes/perps/release-market-details.recipe.json +1 -1
- package/library/recipes/perps/release-market-search.recipe.json +8 -4
- package/library/recipes/perps/release-watchlist.recipe.json +31 -19
- package/library/recipes/swap-bridge/release-custom-slippage.recipe.json +5 -13
- package/library/recipes/swap-bridge/release-quote.recipe.json +1 -0
- package/library/recipes/wallet/import.recipe.json +4 -14
- package/package.json +1 -1
- package/library/actions/extension/assets/finish_send.mjs +0 -128
- package/library/actions/extension/swap_bridge/submit_transaction.mjs +0 -363
- package/library/actions/extension/ui/activate_and_observe.mjs +0 -96
- package/library/recipes/assets/release-custom-gas-send.recipe.json +0 -88
- package/library/recipes/assets/release-native-max-cancel.recipe.json +0 -70
- package/library/recipes/assets/release-native-max-send.recipe.json +0 -78
- package/library/recipes/assets/release-own-account-send.recipe.json +0 -80
- package/library/recipes/extension/runner/action-validation.recipe.json +0 -343
|
@@ -9,6 +9,7 @@ import { build } from "esbuild";
|
|
|
9
9
|
import { init, parse } from "es-module-lexer";
|
|
10
10
|
import { resolveRequiredLocalProtocolRoot, runnerDir } from "./paths.js";
|
|
11
11
|
import { corePnpNodeOptions } from "./commands/core-readiness.js";
|
|
12
|
+
import { serializeTrustedMutationContext } from "./funding-execution-context.js";
|
|
12
13
|
function actionFileStem(action) {
|
|
13
14
|
return String(action).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
14
15
|
}
|
|
@@ -306,7 +307,8 @@ async function runLiveAdapterScript({ platform, action, node, context, prepared
|
|
|
306
307
|
context: {
|
|
307
308
|
nodeId: context.nodeId,
|
|
308
309
|
projectRoot: context.projectRoot,
|
|
309
|
-
artifactsDir: context.artifactsDir
|
|
310
|
+
artifactsDir: context.artifactsDir,
|
|
311
|
+
...context.trustedMutation ? { trustedMutation: serializeTrustedMutationContext(context.trustedMutation) } : {}
|
|
310
312
|
},
|
|
311
313
|
outputPath
|
|
312
314
|
};
|
|
@@ -1,14 +1,41 @@
|
|
|
1
1
|
function isRecord(value) {
|
|
2
2
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3
3
|
}
|
|
4
|
-
function
|
|
4
|
+
function resolveMetaMaskParameterValue(value, params) {
|
|
5
|
+
if (typeof value === "string") {
|
|
6
|
+
const exact = /^\{\{params\.([A-Za-z0-9_.-]+)\}\}$/u.exec(value);
|
|
7
|
+
if (!exact) return value;
|
|
8
|
+
let current = params;
|
|
9
|
+
for (const segment of exact[1].split(".")) {
|
|
10
|
+
if (!isRecord(current) || !Object.hasOwn(current, segment)) return value;
|
|
11
|
+
current = current[segment];
|
|
12
|
+
}
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(value)) return value.map((entry) => resolveMetaMaskParameterValue(entry, params));
|
|
16
|
+
if (!isRecord(value)) return value;
|
|
17
|
+
return Object.fromEntries(
|
|
18
|
+
Object.entries(value).map(([key, entry]) => [key, resolveMetaMaskParameterValue(entry, params)])
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
function validateMetaMaskActionInputs(recipe, adapter, params) {
|
|
5
22
|
if (!isRecord(recipe) || !isRecord(recipe.workflow)) return [];
|
|
6
23
|
const nodes = isRecord(recipe.workflow.nodes) ? recipe.workflow.nodes : void 0;
|
|
7
24
|
if (!nodes) return [];
|
|
8
25
|
const findings = [];
|
|
9
|
-
for (const [nodeId,
|
|
26
|
+
for (const [nodeId, rawNode] of Object.entries(nodes)) {
|
|
27
|
+
const node = params ? resolveMetaMaskParameterValue(rawNode, params) : rawNode;
|
|
10
28
|
if (!isRecord(node)) continue;
|
|
11
29
|
const action = node.action;
|
|
30
|
+
const maximum = (name, limit) => {
|
|
31
|
+
if (typeof node[name] !== "number" || node[name] <= limit) return;
|
|
32
|
+
findings.push({
|
|
33
|
+
severity: "error",
|
|
34
|
+
code: "recipe.invalid_param",
|
|
35
|
+
path: `workflow.nodes.${nodeId}.${name}`,
|
|
36
|
+
message: `${action} ${name} must not exceed ${limit}`
|
|
37
|
+
});
|
|
38
|
+
};
|
|
12
39
|
if (action === "metamask.perps.read_snapshot" && adapter === "extension") {
|
|
13
40
|
if (node.market !== void 0 && node.markets !== void 0) {
|
|
14
41
|
findings.push({
|
|
@@ -54,9 +81,148 @@ function validateMetaMaskActionInputs(recipe, adapter) {
|
|
|
54
81
|
message: `${action} timeout_ms must not exceed 60000`
|
|
55
82
|
});
|
|
56
83
|
}
|
|
84
|
+
if (isRecord(node.sampling)) {
|
|
85
|
+
if (typeof node.sampling.count === "number" && node.sampling.count > 10) {
|
|
86
|
+
findings.push({
|
|
87
|
+
severity: "error",
|
|
88
|
+
code: "recipe.invalid_param",
|
|
89
|
+
path: `workflow.nodes.${nodeId}.sampling.count`,
|
|
90
|
+
message: `${action} sampling.count must not exceed 10`
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (typeof node.sampling.interval_ms === "number" && node.sampling.interval_ms > 3e4) {
|
|
94
|
+
findings.push({
|
|
95
|
+
severity: "error",
|
|
96
|
+
code: "recipe.invalid_param",
|
|
97
|
+
path: `workflow.nodes.${nodeId}.sampling.interval_ms`,
|
|
98
|
+
message: `${action} sampling.interval_ms must not exceed 30000`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (isRecord(node.expect)) {
|
|
103
|
+
if (typeof node.expect.minimum_changed_market_count === "number" && node.expect.minimum_changed_market_count > 100) {
|
|
104
|
+
findings.push({
|
|
105
|
+
severity: "error",
|
|
106
|
+
code: "recipe.invalid_param",
|
|
107
|
+
path: `workflow.nodes.${nodeId}.expect.minimum_changed_market_count`,
|
|
108
|
+
message: `${action} expect.minimum_changed_market_count must not exceed 100`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (typeof node.expect.minimum_history_count === "number" && node.expect.minimum_history_count > 500) {
|
|
112
|
+
findings.push({
|
|
113
|
+
severity: "error",
|
|
114
|
+
code: "recipe.invalid_param",
|
|
115
|
+
path: `workflow.nodes.${nodeId}.expect.minimum_history_count`,
|
|
116
|
+
message: `${action} expect.minimum_history_count must not exceed 500`
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (action === "metamask.perps.assert_visible_consistency" && adapter === "extension") {
|
|
122
|
+
maximum("sample_count", 10);
|
|
123
|
+
maximum("sample_interval_ms", 3e4);
|
|
124
|
+
maximum("minimum_changed_count", 200);
|
|
125
|
+
maximum("minimum_recent_activity_count", 50);
|
|
126
|
+
maximum("timeout_ms", 12e4);
|
|
127
|
+
maximum("max_items", 200);
|
|
128
|
+
}
|
|
129
|
+
const fundedVisibleMutation = adapter === "extension" && (action === "metamask.perps.close_visible_position" || action === "metamask.perps.close_orders" || action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl" || action === "metamask.perps.place_order" && node.confirm_live === true);
|
|
130
|
+
if (fundedVisibleMutation) {
|
|
131
|
+
const required = (name, pattern, message) => {
|
|
132
|
+
if (typeof node[name] === "string" && pattern.test(node[name])) return;
|
|
133
|
+
findings.push({
|
|
134
|
+
severity: "error",
|
|
135
|
+
code: "recipe.invalid_param",
|
|
136
|
+
path: `workflow.nodes.${nodeId}.${name}`,
|
|
137
|
+
message
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
const decimal = action === "metamask.perps.close_visible_position" || action === "metamask.perps.update_position_tpsl" ? node.funding_amount : node.amount ?? node.notional;
|
|
141
|
+
const decimalText = String(decimal ?? "");
|
|
142
|
+
const match = /^([1-9][0-9]*)(?:\.([0-9]{1,6}))?$/u.exec(decimalText);
|
|
143
|
+
if (!match) {
|
|
144
|
+
findings.push({
|
|
145
|
+
severity: "error",
|
|
146
|
+
code: "recipe.invalid_param",
|
|
147
|
+
path: `workflow.nodes.${nodeId}.${action === "metamask.perps.close_visible_position" || action === "metamask.perps.update_position_tpsl" ? "funding_amount" : "amount"}`,
|
|
148
|
+
message: `${action} funded decimal amount must be positive plain decimal text with at most six places`
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (action === "metamask.perps.place_order" && node.submit_attempts !== void 0 && node.submit_attempts !== 1) {
|
|
152
|
+
findings.push({
|
|
153
|
+
severity: "error",
|
|
154
|
+
code: "recipe.invalid_param",
|
|
155
|
+
path: `workflow.nodes.${nodeId}.submit_attempts`,
|
|
156
|
+
message: `${action} funded visible submission requires submit_attempts=1`
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (action === "metamask.perps.close_visible_position" || action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl") {
|
|
160
|
+
required("funding_side", /^(?:long|short)$/u, `${action} requires the authorized position side`);
|
|
161
|
+
}
|
|
162
|
+
if (action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl") {
|
|
163
|
+
required(
|
|
164
|
+
"funding_position_size",
|
|
165
|
+
/^(?=[0-9.]*[1-9])[0-9]+(?:\.[0-9]+)? [A-Z0-9:_-]{2,20}$/u,
|
|
166
|
+
`${action} requires the exact visible position size`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (action === "metamask.perps.close_visible_position" && node.cleanup_only === true) {
|
|
170
|
+
required(
|
|
171
|
+
"funding_position_size",
|
|
172
|
+
/^(?=[0-9.]*[1-9])[0-9]+(?:\.[0-9]+)? [A-Z0-9:_-]{2,20}$/u,
|
|
173
|
+
`${action} cleanup requires the exact visible position size`
|
|
174
|
+
);
|
|
175
|
+
if (node.amount != null) {
|
|
176
|
+
findings.push({
|
|
177
|
+
severity: "error",
|
|
178
|
+
code: "recipe.invalid_param",
|
|
179
|
+
path: `workflow.nodes.${nodeId}.amount`,
|
|
180
|
+
message: `${action} cleanup must close the full exact position`
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (action === "metamask.perps.edit_margin" && !/^[1-9][0-9]*(?:\.[0-9]{1,6})?$/u.test(String(node.funding_amount ?? ""))) {
|
|
185
|
+
findings.push({
|
|
186
|
+
severity: "error",
|
|
187
|
+
code: "recipe.invalid_param",
|
|
188
|
+
path: `workflow.nodes.${nodeId}.funding_amount`,
|
|
189
|
+
message: `${action} requires the authorized position amount`
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (action === "metamask.perps.close_orders") {
|
|
193
|
+
required("side", /^(?:long|short)$/u, `${action} requires the exact order side`);
|
|
194
|
+
if (!/^[1-9][0-9]*(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? ""))) {
|
|
195
|
+
findings.push({
|
|
196
|
+
severity: "error",
|
|
197
|
+
code: "recipe.invalid_param",
|
|
198
|
+
path: `workflow.nodes.${nodeId}.limit_price`,
|
|
199
|
+
message: `${action} requires an explicit positive limit_price for resumable cancellation`
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (action === "metamask.perps.place_order" && String(node.order_type ?? "market") === "limit" && !/^[1-9][0-9]*(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? ""))) {
|
|
204
|
+
findings.push({
|
|
205
|
+
severity: "error",
|
|
206
|
+
code: "recipe.invalid_param",
|
|
207
|
+
path: `workflow.nodes.${nodeId}.limit_price`,
|
|
208
|
+
message: `${action} funded limit submission requires explicit limit_price; offset-only pricing is not resumable`
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
for (const name of ["identity_rounding_tolerance_base_units", "balance_reconciliation_tolerance_base_units"]) {
|
|
212
|
+
if (node[name] === void 0) continue;
|
|
213
|
+
const value = String(node[name]);
|
|
214
|
+
if (!/^(?:0|[1-9][0-9]{0,6})$/u.test(value) || BigInt(value) > 1000000n) {
|
|
215
|
+
findings.push({
|
|
216
|
+
severity: "error",
|
|
217
|
+
code: "recipe.invalid_param",
|
|
218
|
+
path: `workflow.nodes.${nodeId}.${name}`,
|
|
219
|
+
message: `${action} ${name} must be an integer string from 0 through 1000000`
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
57
223
|
}
|
|
58
224
|
const state = typeof node.state === "string" ? node.state.toLowerCase() : void 0;
|
|
59
|
-
const createsOrder = action === "metamask.perps.place_order" || (action === "metamask.perps.ensure_positions" || action === "metamask.perps.ensure_orders") && (state === "open" || state === "present");
|
|
225
|
+
const createsOrder = action === "metamask.perps.place_order" || adapter !== "extension" && (action === "metamask.perps.ensure_positions" || action === "metamask.perps.ensure_orders") && (state === "open" || state === "present");
|
|
60
226
|
if (!createsOrder) continue;
|
|
61
227
|
const requireOneOf = (names, message) => {
|
|
62
228
|
const present = names.some((name) => {
|
|
@@ -87,5 +253,6 @@ function validateMetaMaskActionInputs(recipe, adapter) {
|
|
|
87
253
|
return findings;
|
|
88
254
|
}
|
|
89
255
|
export {
|
|
256
|
+
resolveMetaMaskParameterValue,
|
|
90
257
|
validateMetaMaskActionInputs
|
|
91
258
|
};
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -234,11 +234,16 @@ Common overrides:
|
|
|
234
234
|
--platform <ios|android> Mobile platform preference; use --adapter for the product adapter
|
|
235
235
|
--target <path> Checkout path (default: cwd)
|
|
236
236
|
--artifacts-dir <dir> Override evidence output (default: active task/artifacts, otherwise a checkout-local temp/recipe/runs/<run>)
|
|
237
|
+
--funding-request <absolute-file> Original consented mutation request for external-mutation actions
|
|
238
|
+
--funding-preflight <absolute-file> Eligible funding preflight bound to the request bytes
|
|
239
|
+
--funding-release-proof <absolute-file> Exact release proof independently bound to funding eligibility
|
|
240
|
+
--funding-balance-proof <absolute-file> Exact balance proof and contained visible evidence chain
|
|
237
241
|
--library <name=path> Add/override a recipe-library source (repeatable)
|
|
238
242
|
--heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
|
|
239
243
|
--json Machine-readable output
|
|
240
244
|
--json-stream Line-flushed JSONL progress + terminal event
|
|
241
245
|
--record-video=full-run Record a video of the run
|
|
246
|
+
--hud <auto|show|hide> Recipe HUD policy (default: auto)
|
|
242
247
|
|
|
243
248
|
Advanced integrations:
|
|
244
249
|
runtime: --cdp-port, --watcher-port/--metro-port, --runtime-dir, --slot,
|
|
@@ -250,7 +255,7 @@ Advanced integrations:
|
|
|
250
255
|
|
|
251
256
|
Example:
|
|
252
257
|
mm-harness run wallet.smoke --describe
|
|
253
|
-
mm-harness run perps.clean-market-testnet market=BTC --plan
|
|
258
|
+
mm-harness run perps.clean-market-testnet market=BTC --plan --adapter mobile
|
|
254
259
|
mm-harness run recipe.json market=ETH --adapter extension`
|
|
255
260
|
},
|
|
256
261
|
{
|
|
@@ -458,7 +463,8 @@ Example:
|
|
|
458
463
|
--verify Launch then poll CDP/bridge until ready (absorbs the old \`live\`)
|
|
459
464
|
--runway Post-launch runway check (mobile only; teaching error elsewhere)
|
|
460
465
|
--watch Persistent webpack watcher then relaunch (extension only)
|
|
461
|
-
--
|
|
466
|
+
--surface <fullscreen|sidepanel> Extension display mode (default: fullscreen)
|
|
467
|
+
--sidepanel | --fullscreen Legacy aliases for --surface
|
|
462
468
|
--url <dapp-url> Open a dapp beside the sidepanel (default: MetaMask test dapp)
|
|
463
469
|
--heal <off|infra-only|auto> Healing policy (default auto); bounds always enforced
|
|
464
470
|
--device <udid|name> Target simulator/device (env: IOS_SIMULATOR / ADB_SERIAL)
|
package/dist/recipe-security.js
CHANGED
|
@@ -3,7 +3,6 @@ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
5
5
|
"ui.locators",
|
|
6
|
-
"ui.activate_and_observe",
|
|
7
6
|
"metamask.wallet.fixture_status",
|
|
8
7
|
"metamask.wallet.validate_import",
|
|
9
8
|
"metamask.wallet.list_accounts",
|
|
@@ -20,21 +19,22 @@ const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
20
19
|
"metamask.perps.read_orders",
|
|
21
20
|
"metamask.perps.read_account",
|
|
22
21
|
"metamask.perps.read_snapshot",
|
|
22
|
+
"metamask.perps.assert_visible_consistency",
|
|
23
23
|
"metamask.perps.assert_positions",
|
|
24
24
|
"metamask.perps.assert_orders",
|
|
25
|
+
"metamask.perps.ensure_positions",
|
|
26
|
+
"metamask.perps.ensure_orders",
|
|
25
27
|
// Collector reads are host-only; starting capture remains arbitrary-code
|
|
26
28
|
// because it spawns an HTTP listener.
|
|
27
29
|
"metamask.analytics.read_events",
|
|
28
30
|
"metamask.analytics.assert_events"
|
|
29
31
|
]);
|
|
30
32
|
const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
31
|
-
"ui.activate_and_observe",
|
|
32
33
|
"metamask.assets.verify_sorting",
|
|
33
34
|
"metamask.assets.set_token_visibility",
|
|
34
35
|
"metamask.assets.import_custom_token",
|
|
35
36
|
"metamask.assets.prepare_send",
|
|
36
37
|
"metamask.assets.review_send",
|
|
37
|
-
"metamask.assets.finish_send",
|
|
38
38
|
"metamask.assets.set_custom_gas",
|
|
39
39
|
"metamask.assets.select_network_scope",
|
|
40
40
|
"metamask.wallet.lock",
|
|
@@ -61,8 +61,6 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
61
61
|
"metamask.perps.edit_margin",
|
|
62
62
|
"metamask.perps.edit_order",
|
|
63
63
|
"metamask.perps.update_position_tpsl",
|
|
64
|
-
"metamask.perps.ensure_positions",
|
|
65
|
-
"metamask.perps.ensure_orders",
|
|
66
64
|
"metamask.perps.clear_performance_caches",
|
|
67
65
|
"metamask.perps.measure_homepage_visible",
|
|
68
66
|
"metamask.perps.prepare_local_snapshot_endpoint",
|
|
@@ -70,14 +68,13 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
70
68
|
"metamask.perps.select_activity_filter",
|
|
71
69
|
"metamask.perps.select_market_filter",
|
|
72
70
|
"metamask.perps.open_balance_action",
|
|
71
|
+
"metamask.perps.assert_visible_consistency",
|
|
73
72
|
"metamask.perps.start_state",
|
|
74
73
|
"metamask.perps.teardown_state",
|
|
75
74
|
// Consent changes wallet state and is therefore an app mutation.
|
|
76
75
|
"metamask.analytics.set_consent"
|
|
77
76
|
]);
|
|
78
77
|
const EXTERNAL_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
79
|
-
"ui.activate_and_observe",
|
|
80
|
-
"metamask.assets.finish_send",
|
|
81
78
|
"metamask.swap_bridge.submit_transaction",
|
|
82
79
|
"metamask.perps.close_positions",
|
|
83
80
|
"metamask.perps.close_orders",
|
|
@@ -85,11 +82,7 @@ const EXTERNAL_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
85
82
|
"metamask.perps.close_visible_position",
|
|
86
83
|
"metamask.perps.edit_margin",
|
|
87
84
|
"metamask.perps.edit_order",
|
|
88
|
-
"metamask.perps.update_position_tpsl"
|
|
89
|
-
"metamask.perps.ensure_positions",
|
|
90
|
-
"metamask.perps.ensure_orders",
|
|
91
|
-
"metamask.perps.start_state",
|
|
92
|
-
"metamask.perps.teardown_state"
|
|
85
|
+
"metamask.perps.update_position_tpsl"
|
|
93
86
|
]);
|
|
94
87
|
function metaMaskActionExecutionCapabilities(action) {
|
|
95
88
|
const capabilities = [];
|
package/dist/runner.js
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
mobileSourceFingerprint,
|
|
7
7
|
recordMobileSourceBaseline
|
|
8
8
|
} from "./adapters/mobile/source-freshness.js";
|
|
9
|
+
import { readAndroidNativeRuntimeIdentity } from "./adapters/mobile/release-artifact.js";
|
|
10
|
+
import { readMobileReleaseArtifactState } from "./adapters/mobile/release-artifact-state.js";
|
|
9
11
|
import {
|
|
10
12
|
createAndroidVideoRecorder,
|
|
11
13
|
createIosSimulatorVideoRecorder
|
|
@@ -29,6 +31,10 @@ import {
|
|
|
29
31
|
importRecipeHarnessRuntimeReactNativeBridge,
|
|
30
32
|
runnerDir
|
|
31
33
|
} from "./paths.js";
|
|
34
|
+
import {
|
|
35
|
+
selectedIdentityFromWalletImport,
|
|
36
|
+
trustedMutationForAction
|
|
37
|
+
} from "./funding-execution-context.js";
|
|
32
38
|
const OFFICIAL_ACTIONS = new Set(OFFICIAL_RECIPE_ACTIONS);
|
|
33
39
|
async function createMetaMaskMobileRunner(options = {}) {
|
|
34
40
|
return createMetaMaskRunner(
|
|
@@ -89,7 +95,10 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
89
95
|
).filter(
|
|
90
96
|
(entry) => declaredActions.has(entry.action) && !existing.has(entry.action) && !lifecycle.some((lifecycleEntry) => lifecycleEntry.action === entry.action)
|
|
91
97
|
);
|
|
92
|
-
const autoHudDisabled = options.autoHud === false || process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false";
|
|
98
|
+
const autoHudDisabled = options.autoHud === false || options.autoHud === void 0 && (process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false");
|
|
99
|
+
const externalMutationActions = new Set(
|
|
100
|
+
Object.entries(actionManifest.actions).filter(([, declaration]) => Array.isArray(declaration.execution_capabilities) && declaration.execution_capabilities.includes("external-mutation")).map(([action]) => action)
|
|
101
|
+
);
|
|
93
102
|
const logger = deduplicateLibraryResolutionLogs(
|
|
94
103
|
options.quietStdout ? new console.Console(process.stderr) : console,
|
|
95
104
|
options.suppressLibraryResolutionLogs
|
|
@@ -99,10 +108,18 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
99
108
|
return createRecipeRunner({
|
|
100
109
|
actionManifest,
|
|
101
110
|
adapters: withActionProgress(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
withMobileNativeRuntimeIdentity(
|
|
112
|
+
adapter,
|
|
113
|
+
withTrustedMutationContext(
|
|
114
|
+
[...core, ...ui, ...lifecycle, ...custom].map((entry) => ({
|
|
115
|
+
...entry,
|
|
116
|
+
...adapterSecurity(entry.action, actionSources, options.trustTaskActions)
|
|
117
|
+
})),
|
|
118
|
+
options.trustedMutation,
|
|
119
|
+
externalMutationActions,
|
|
120
|
+
adapter
|
|
121
|
+
)
|
|
122
|
+
),
|
|
106
123
|
options.onActionEvent
|
|
107
124
|
),
|
|
108
125
|
logger,
|
|
@@ -130,6 +147,83 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
130
147
|
}
|
|
131
148
|
});
|
|
132
149
|
}
|
|
150
|
+
function withMobileNativeRuntimeIdentity(adapter, adapters, identity = {
|
|
151
|
+
state: readMobileReleaseArtifactState,
|
|
152
|
+
read: readAndroidNativeRuntimeIdentity
|
|
153
|
+
}) {
|
|
154
|
+
if (adapter !== "mobile") return adapters;
|
|
155
|
+
return adapters.map((entry) => ({
|
|
156
|
+
...entry,
|
|
157
|
+
async execute(node, context) {
|
|
158
|
+
const state = identity.state(context.projectRoot);
|
|
159
|
+
if (!state) {
|
|
160
|
+
if (context.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1") {
|
|
161
|
+
throw new Error("Mobile release-artifact actions require current trusted runtime state. Next: rerun runtime-launch with the exact artifact.");
|
|
162
|
+
}
|
|
163
|
+
return entry.execute(node, context);
|
|
164
|
+
}
|
|
165
|
+
identity.read(state);
|
|
166
|
+
const result = await entry.execute(node, context);
|
|
167
|
+
const nativeRuntimeIdentity = identity.read(state);
|
|
168
|
+
const output = result.output && typeof result.output === "object" && !Array.isArray(result.output) ? result.output : { result: result.output };
|
|
169
|
+
return {
|
|
170
|
+
...result,
|
|
171
|
+
output: {
|
|
172
|
+
...output,
|
|
173
|
+
nativeRuntimeIdentity,
|
|
174
|
+
runtimeSessionId: nativeRuntimeIdentity.runtimeSessionId
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
function withTrustedMutationContext(adapters, base, externalMutationActions, adapter = "extension") {
|
|
181
|
+
if (adapter !== "extension") return adapters;
|
|
182
|
+
let selectedIdentitySha256;
|
|
183
|
+
const identityProofAction = "metamask.wallet.import";
|
|
184
|
+
const identityInvalidatingActions = /* @__PURE__ */ new Set([
|
|
185
|
+
"metamask.wallet.ensure_unlocked",
|
|
186
|
+
"metamask.wallet.lock",
|
|
187
|
+
"metamask.wallet.reset",
|
|
188
|
+
"metamask.wallet.select_account",
|
|
189
|
+
"metamask.wallet.setup"
|
|
190
|
+
]);
|
|
191
|
+
const identityPreservingUiActions = /* @__PURE__ */ new Set([
|
|
192
|
+
"ui.capture_surface",
|
|
193
|
+
"ui.locators",
|
|
194
|
+
"ui.screenshot",
|
|
195
|
+
"ui.wait_for"
|
|
196
|
+
]);
|
|
197
|
+
return adapters.map((entry) => ({
|
|
198
|
+
...entry,
|
|
199
|
+
async execute(node, context) {
|
|
200
|
+
const identityProof = entry.action === identityProofAction;
|
|
201
|
+
const invalidatesIdentity = identityInvalidatingActions.has(entry.action) || entry.action.startsWith("ui.") && !identityPreservingUiActions.has(entry.action);
|
|
202
|
+
if (identityProof || invalidatesIdentity) selectedIdentitySha256 = void 0;
|
|
203
|
+
const externalMutation = externalMutationActions.has(entry.action);
|
|
204
|
+
if (externalMutation && !base) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
"External mutation requires runner-verified --funding-request and --funding-preflight files. Next: rerun with both funding files."
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const trustedMutation = base && externalMutation ? await trustedMutationForAction(base, context.recipe, selectedIdentitySha256) : void 0;
|
|
210
|
+
const trustedContext = trustedMutation ? { ...context, trustedMutation } : context;
|
|
211
|
+
const result = await entry.execute(node, trustedContext).catch((error) => {
|
|
212
|
+
if (identityProof || invalidatesIdentity) selectedIdentitySha256 = void 0;
|
|
213
|
+
throw error;
|
|
214
|
+
});
|
|
215
|
+
if (invalidatesIdentity) selectedIdentitySha256 = void 0;
|
|
216
|
+
const selected = selectedIdentityFromWalletImport(entry.action, result, base);
|
|
217
|
+
if (selected) {
|
|
218
|
+
if (base && base.selectedIdentitySha256 !== selected) {
|
|
219
|
+
throw new Error("Same-run wallet identity changed after its trusted import proof.");
|
|
220
|
+
}
|
|
221
|
+
selectedIdentitySha256 = selected;
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
133
227
|
function deduplicateLibraryResolutionLogs(logger, suppress = false) {
|
|
134
228
|
const seen = /* @__PURE__ */ new Set();
|
|
135
229
|
return {
|
|
@@ -412,5 +506,7 @@ export {
|
|
|
412
506
|
createMetaMaskRunner,
|
|
413
507
|
isAutomaticHudProgress,
|
|
414
508
|
mobileSourceAwareLifecycleAdapters,
|
|
415
|
-
resolveMobileLifecycleReadinessTimeout
|
|
509
|
+
resolveMobileLifecycleReadinessTimeout,
|
|
510
|
+
withMobileNativeRuntimeIdentity,
|
|
511
|
+
withTrustedMutationContext
|
|
416
512
|
};
|
package/docs/RECIPES.md
CHANGED
|
@@ -60,7 +60,9 @@ mm-harness run recipe.json account="Account 2"
|
|
|
60
60
|
|
|
61
61
|
Use the shared wallet setup recipe on either Mobile or Extension. `auto`
|
|
62
62
|
reuses a ready fixture-backed profile and otherwise completes visible
|
|
63
|
-
onboarding
|
|
63
|
+
onboarding. On Extension, `ui` completes fresh visible onboarding or visibly
|
|
64
|
+
unlocks and proves/selects the exact derived account in an existing session.
|
|
65
|
+
On Mobile, `ui` requires fresh onboarding.
|
|
64
66
|
|
|
65
67
|
```bash
|
|
66
68
|
mm-harness run wallet.import method=auto
|
|
@@ -74,6 +76,77 @@ fixture. Secret values are redacted from commands and evidence. Visible imports
|
|
|
74
76
|
default to MetaMetrics off and skip Mobile's optional interests; password terms
|
|
75
77
|
are required and always accepted by the action.
|
|
76
78
|
|
|
79
|
+
### Extension Perps release recipes
|
|
80
|
+
|
|
81
|
+
Discover these identifiers with `run --list --adapter extension --json` and
|
|
82
|
+
inspect the exact graph with `run <id> --describe --adapter extension --json`:
|
|
83
|
+
|
|
84
|
+
| Behavior | Canonical recipe |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| Activity filter routes | `perps.release-activity-filters` |
|
|
87
|
+
| Deposit and withdraw previews without submission | `perps.release-funds-flows` |
|
|
88
|
+
| Resting limit placement and exact cancellation | `perps.release-live-limit-order` |
|
|
89
|
+
| Filled market placement and exact position close | `perps.release-live-market-order` |
|
|
90
|
+
| Market header and statistics | `perps.release-market-details` |
|
|
91
|
+
| Market categories | `perps.release-market-filters` |
|
|
92
|
+
| Market text search | `perps.release-market-search` |
|
|
93
|
+
| Long/Short and Market/Limit entry controls | `perps.release-order-entry` |
|
|
94
|
+
| Amount, leverage, keyboard, auto-close, and TP/SL validation | `perps.release-order-validation` |
|
|
95
|
+
| Official-build rendered market/activity consistency | `perps.release-visible-consistency` |
|
|
96
|
+
| Favorite persistence and cleanup | `perps.release-watchlist` |
|
|
97
|
+
| Hook-backed controller snapshot consistency | `perps.source-dev-snapshot-consistency` |
|
|
98
|
+
|
|
99
|
+
The source-development snapshot recipe is compatibility evidence only and must
|
|
100
|
+
not support an official artifact verdict. Activity and funds recipes are
|
|
101
|
+
Extension-scoped because their semantic actions are not shared release
|
|
102
|
+
capabilities. Keep ticket-specific orchestration task-local and compose these
|
|
103
|
+
identifiers with `call` instead of copying their graphs.
|
|
104
|
+
|
|
105
|
+
Order-entry and order-validation callers must supply a market with no existing
|
|
106
|
+
position; the product exposes Modify/Close instead of New Long/Short for an
|
|
107
|
+
already-open market. Source-development snapshot callers may opt into a
|
|
108
|
+
positive `minimum_changed_market_count`; the default does not require a price
|
|
109
|
+
tick, while typed-history and visible-activity agreement remain strict.
|
|
110
|
+
|
|
111
|
+
The two live recipes require explicit `confirm_live=true`. The trusted runner
|
|
112
|
+
must also verify the exact consented funding request, preflight, release proof,
|
|
113
|
+
and contained balance proof. Market placement declares the exact provider and
|
|
114
|
+
network; background placement and cancellation refuse a mismatch without
|
|
115
|
+
toggling it. Immediately before publication and dispatch, the action also reads
|
|
116
|
+
the current visible Perps Home balance and requires the runner-trusted minimum.
|
|
117
|
+
The release onboarding session proves artifact, wallet, and origin provenance;
|
|
118
|
+
the fresh balance trace identifies the execution session, so a legitimate
|
|
119
|
+
release-artifact relaunch does not claim false same-session continuity. The
|
|
120
|
+
runner revalidates the current artifact tree and owned CDP profile, persists the
|
|
121
|
+
pre-dispatch receipt, and finishes receipt-bound cleanup and reconciliation. A
|
|
122
|
+
known-not-dispatched receipt is terminal and requires fresh consent and a new
|
|
123
|
+
claim. Selecting or resetting an account, changing its lock/setup session, or
|
|
124
|
+
using a generic UI press invalidates the cached identity proof; a fresh visible
|
|
125
|
+
`wallet.import` proof is required before another external mutation. Extension
|
|
126
|
+
`ensure_positions`, `ensure_orders`, and `teardown_state` only observe and assert
|
|
127
|
+
state that already exists; their schemas reject creation and cleanup inputs.
|
|
128
|
+
Extension does not expose `start_state`, readiness claims, multi-position
|
|
129
|
+
background closure, or the mutating `runner.action-validation` recipe. Financial
|
|
130
|
+
transitions must use an explicitly funded leaf action.
|
|
131
|
+
The shared `perps.clean-market-testnet` convergence recipe is therefore Mobile-
|
|
132
|
+
only: Extension discovery omits it and a direct Extension plan fails statically.
|
|
133
|
+
Planning or calling a recipe never weakens those gates.
|
|
134
|
+
|
|
135
|
+
### Extension submission boundary
|
|
136
|
+
|
|
137
|
+
Extension currently exposes read-only Assets and Swap/Bridge evidence plus send
|
|
138
|
+
preparation/review, quote, Max, and slippage actions. It does not expose
|
|
139
|
+
`metamask.assets.finish_send`, `metamask.swap_bridge.submit_transaction`, or
|
|
140
|
+
generic `ui.activate_and_observe`. Those operations remain unavailable until a
|
|
141
|
+
domain-specific funding policy and durable pre-dispatch receipt/recovery
|
|
142
|
+
contract exists. The four former Assets submit recipes are not shipped.
|
|
143
|
+
|
|
144
|
+
The shared live Swap recipes remain available to Mobile, whose manifest owns a
|
|
145
|
+
separate native submission action. Extension `run --list` omits those recipes,
|
|
146
|
+
and a direct Extension plan fails statically. Extension quote and visible-state
|
|
147
|
+
recipes remain discoverable and plan normally; generic pointer activation is
|
|
148
|
+
not a substitute for a funded financial leaf.
|
|
149
|
+
|
|
77
150
|
To prove the full real-user recovery journey, delete the current wallet through
|
|
78
151
|
visible client UI and import it again:
|
|
79
152
|
|
|
@@ -81,6 +81,7 @@ current manifests do not expose the required durable capability.
|
|
|
81
81
|
| Order entry | Funded Perps balance, market/limit inputs, validation, preview, fill, and duplicate protection | Parameterized order placement plus visible order-entry and validation recipes | **PARTIAL**: live state and provider outcomes remain release-dependent |
|
|
82
82
|
| Position management | Open position, close, margin, and TP/SL changes | Visible position actions, close, edit-margin, and TP/SL actions | **PARTIAL**: exact outcome depends on live account state |
|
|
83
83
|
| Deposit and withdrawal | Wallet funds, Perps balance, confirmations, and resulting activity | Balance actions and confirmation reads with a funds-flow recipe | **PARTIAL**: funds and provider availability are external |
|
|
84
|
+
| Perps state helpers | Deterministic position/order creation or cleanup before and after proof | Mobile state helpers can converge supported testnet state; Extension only exposes observation/assertion helpers and no start/readiness or multi-position background-close claim | **PARTIAL**: Extension convergence is **MISSING** and `perps.clean-market-testnet` plus mutating runner validation are omitted there |
|
|
84
85
|
| Snapshot assertions | Positions, orders, markets, history, and watchlist | Bounded redacted snapshot when the background hook exists; visible-state reads on untouched releases | **AVAILABLE** with runtime-specific observation paths |
|
|
85
86
|
| Deterministic seeded provider state | Balance, positions, limit orders, TP/SL, history, and controlled REST/stream behavior | Existing setup fixtures do not reproduce the complete QA provider model inside an untouched release artifact | **MISSING** |
|
|
86
87
|
|
|
@@ -90,7 +91,7 @@ current manifests do not expose the required durable capability.
|
|
|
90
91
|
| --- | --- | --- | --- |
|
|
91
92
|
| Token list, prices, details, and sorting | Funded account with known holdings and price data | Visible token-list/details reads, network scope, sorting assertions, and bundled recipes | **AVAILABLE** for EVM token UI |
|
|
92
93
|
| Token search/import/hide/persistence | Known contracts and a persistent profile | Custom-token import, visibility changes, restart-safe recipes, and custom-network composition | **AVAILABLE** for Extension EVM tokens |
|
|
93
|
-
| Native/token send and gas | Funded owned accounts and live gas |
|
|
94
|
+
| Native/token send and gas | Funded owned accounts and live gas | Extension can prepare and review a send and configure gas, but exposes no terminal send/cancel action or submit recipe | **MISSING** for terminal Extension send until domain-specific funding and durable receipt policy exists |
|
|
94
95
|
| Aggregate Android holdings | Imported wallet with multi-chain assets | Native-accessibility token-list read on an untouched physical-device release | **AVAILABLE** |
|
|
95
96
|
| Balance streaming and aggregation arithmetic | Triggered inbound update and expected cross-network totals | Visible snapshots exist; deterministic inbound trigger and tolerance assertions are not bundled | **PARTIAL** |
|
|
96
97
|
| NFTs, DeFi, Solana send, and Bitcoin details | Indexed owned positions/assets and chain-specific transaction flows | Generic visible UI only | **MISSING** |
|
|
@@ -101,8 +102,8 @@ current manifests do not expose the required durable capability.
|
|
|
101
102
|
| --- | --- | --- | --- |
|
|
102
103
|
| Surface, asset, network, and amount selection | Supported source/destination pair | Exact asset/network selection, amount/Max input, and visible-state reads on Extension; exact native selection and amount input on Android | **AVAILABLE** |
|
|
103
104
|
| Quote and slippage | Live quote services and supported route | Surface, quote, Max-quote, and custom-slippage recipes | **AVAILABLE** |
|
|
104
|
-
| Submit and confirmation | Sufficient source balance and gas |
|
|
105
|
-
| Sequential multi-network journey | Balances delivered by preceding swaps and bridges |
|
|
105
|
+
| Submit and confirmation | Sufficient source balance and gas | Mobile retains its typed native submit action; Extension exposes no submission action or live-submit recipe | **MISSING** on Extension until domain-specific funding and durable receipt policy exists |
|
|
106
|
+
| Sequential multi-network journey | Balances delivered by preceding swaps and bridges | Extension selection, amount, quote, Max, and slippage evidence remains composable, but no Extension terminal submit step exists | **MISSING** for an Extension end-to-end live journey |
|
|
106
107
|
| Activity, balance deltas, and completed bridge tracking | Confirmed transactions and indexed activity | Visible-state evidence exists; reusable terminal activity and arithmetic assertions are incomplete | **PARTIAL** |
|
|
107
108
|
|
|
108
109
|
### Networks
|
|
@@ -112,8 +113,8 @@ current manifests do not expose the required durable capability.
|
|
|
112
113
|
| Add, select, and remove custom networks | Known RPC configuration and chain identity | Typed custom-network and Chainlist add, visible-state read, select, and remove actions | **AVAILABLE** on Extension |
|
|
113
114
|
| Custom-network tokens and persistence | Known token contract and persistent profile | Network actions compose with Assets import/visibility/persistence recipes | **AVAILABLE** on Extension |
|
|
114
115
|
| Balance stability and fiat visibility | Funded custom network and live price APIs | Visible-state reads and a bounded balance-stability recipe | **PARTIAL**: external price availability remains uncontrolled |
|
|
115
|
-
| Send Max and activity | Funded custom network and recipient |
|
|
116
|
-
| Bridge tracking and details | Completed supported bridge route | Swap/Bridge
|
|
116
|
+
| Send Max and activity | Funded custom network and recipient | Extension can prepare and review the send, but cannot terminally submit or cancel it | **MISSING** until Extension send has domain-specific funding and durable receipts |
|
|
117
|
+
| Bridge tracking and details | Completed supported bridge route | Read-only Swap/Bridge evidence exists, but Extension cannot create the terminal bridge transaction | **MISSING** for a complete Extension bridge journey |
|
|
117
118
|
| Android multi-network visibility | Imported wallet with holdings on multiple chains | Native Assets read reports aggregate visible holdings without Metro or a development bridge | **AVAILABLE** |
|
|
118
119
|
|
|
119
120
|
## Platform scope
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
4
|
+
const symbol = String(input.node?.symbol ?? '').trim().toUpperCase();
|
|
5
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 30_000);
|
|
6
|
+
if (!/^[A-Z0-9._-]{2,20}$/u.test(symbol)) {
|
|
7
|
+
throw new Error('metamask.assets.open_details requires a valid token symbol.');
|
|
8
|
+
}
|
|
9
|
+
const selector = await page.evaluate(`(() => {
|
|
10
|
+
const symbol = ${JSON.stringify(symbol)};
|
|
11
|
+
for (const [index, row] of [...document.querySelectorAll('[data-testid="multichain-token-list-button"]')].entries()) {
|
|
12
|
+
const amount = String(row.querySelector('[data-testid="multichain-token-list-item-value"]')?.innerText || '').trim();
|
|
13
|
+
const observed = amount.match(/\\b([A-Za-z][A-Za-z0-9._-]{1,19})$/u)?.[1]?.toUpperCase();
|
|
14
|
+
if (observed !== symbol) continue;
|
|
15
|
+
row.setAttribute('data-mm-harness-asset-index', String(index));
|
|
16
|
+
return '[data-mm-harness-asset-index="' + index + '"]';
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
})()`);
|
|
20
|
+
if (!selector) throw new Error(`Visible asset ${symbol} was not found.`);
|
|
21
|
+
await page.click(selector);
|
|
22
|
+
const deadline = Date.now() + timeoutMs;
|
|
23
|
+
while (Date.now() <= deadline) {
|
|
24
|
+
if (String(await page.evaluate('location.hash')).startsWith('#/asset/')) {
|
|
25
|
+
return { action: input.action, symbol, proofPath: 'trusted-pointer-visible-ui' };
|
|
26
|
+
}
|
|
27
|
+
await page.evaluate('new Promise((resolve) => setTimeout(resolve, 100))');
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Asset details for ${symbol} did not open.`);
|
|
30
|
+
}));
|