@deeeed/metamask-harness 0.47.4 → 0.48.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 +6 -0
- package/README.md +3 -2
- package/adapters/extension/lib/macos-focus.cjs +2 -2
- package/adapters/extension/live.sh +6 -8
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +1 -0
- package/adapters/shared/ensure-runner-deps.sh +6 -0
- package/dist/adapters/extension/runtime-decision.js +18 -1
- package/dist/adapters/extension/runtime.js +5 -5
- package/dist/adapters/extension/surface.js +1 -0
- package/dist/app-lifecycle.js +3 -1
- package/dist/command-contract.js +2 -0
- package/dist/commands/call.js +1 -8
- package/dist/commands/device-target.js +12 -3
- package/dist/commands/doctor.js +7 -5
- package/dist/commands/fixtures.js +1 -1
- package/dist/commands/launch/index.js +10 -0
- package/dist/commands/manifest.js +3 -1
- package/dist/commands/mobile-device-view.js +13 -3
- package/dist/commands/parse-args.js +2 -0
- package/dist/commands/run-engine.js +29 -4
- package/dist/commands/run.js +1 -1
- package/dist/commands/runtime-launch.js +10 -1
- package/dist/commands/status.js +8 -4
- package/dist/devices.js +31 -9
- package/dist/execution-provenance.js +8 -7
- package/dist/funding-execution-context.js +43 -13
- package/dist/heal-bounds.js +3 -3
- package/dist/live-adapter-contract.js +4 -0
- package/dist/manifest.js +2 -13
- package/dist/mm-harness-cli.js +2 -1
- package/dist/runner.js +14 -3
- package/docs/RECIPES.md +133 -22
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +3 -2
- package/library/actions/core/perps/_controller.mjs +10 -0
- package/library/actions/core/perps/close_orders.mjs +13 -17
- package/library/actions/extension/assets/home-token-identity.mjs +30 -0
- package/library/actions/extension/assets/import_custom_token.mjs +61 -79
- package/library/actions/extension/assets/open_details.mjs +18 -7
- package/library/actions/extension/assets/read_details.mjs +61 -29
- package/library/actions/extension/assets/read_visible_state.mjs +4 -1
- package/library/actions/extension/assets/select_network_scope.mjs +16 -9
- package/library/actions/extension/assets/set_token_visibility.mjs +84 -17
- package/library/actions/extension/assets/verify_sorting.mjs +62 -14
- package/library/actions/extension/networks/add_custom.mjs +19 -17
- package/library/actions/extension/networks/custom_network.mjs +8 -3
- package/library/actions/extension/networks/read_visible_state.mjs +1 -0
- package/library/actions/extension/performance/_navigation-memory.mjs +35 -6
- package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +3 -2
- package/library/actions/extension/performance/measure_detached_dom.mjs +3 -2
- package/library/actions/extension/performance/measure_navigation_memory.mjs +3 -2
- package/library/actions/extension/perps/assert_visible_consistency.mjs +9 -4
- package/library/actions/extension/perps/close_visible_position.mjs +3 -3
- package/library/actions/extension/perps/edit_margin.mjs +1 -1
- package/library/actions/extension/perps/open_balance_action.mjs +16 -7
- package/library/actions/extension/perps/perps.mjs +150 -45
- package/library/actions/extension/perps/read_funds_confirmation.mjs +8 -0
- package/library/actions/extension/perps/read_snapshot.mjs +2 -1
- package/library/actions/extension/perps/read_visible_state.mjs +10 -6
- package/library/actions/extension/perps/search_markets.mjs +7 -2
- package/library/actions/extension/perps/select_activity_filter.mjs +1 -1
- package/library/actions/extension/perps/select_market_filter.mjs +74 -45
- package/library/actions/extension/perps/set_market_favorite.mjs +6 -3
- package/library/actions/extension/perps/state.mjs +1 -0
- package/library/actions/extension/perps/update_position_tpsl.mjs +1 -1
- package/library/actions/extension/perps/visible-mutation-identity.mjs +4 -2
- package/library/actions/extension/platform/cdp.mjs +23 -142
- package/library/actions/extension/swap_bridge/quote-state.mjs +54 -0
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +4 -1
- package/library/actions/extension/swap_bridge/select_assets.mjs +11 -1
- package/library/actions/extension/swap_bridge/set_amount.mjs +10 -8
- package/library/actions/extension/swap_bridge/set_max_amount.mjs +17 -18
- package/library/actions/extension/swap_bridge/set_slippage.mjs +0 -9
- package/library/actions/extension/ui/navigate.mjs +58 -14
- package/library/actions/extension/wallet/lock.mjs +2 -1
- package/library/actions/mobile/assets/import_custom_token.mjs +1 -0
- package/library/actions/mobile/assets/read_visible_state.mjs +3 -1
- package/library/actions/mobile/assets/set_token_visibility.mjs +13 -9
- package/library/actions/mobile/networks/network-management.mjs +22 -15
- package/library/actions/mobile/networks/remove_custom.mjs +6 -2
- package/library/actions/mobile/perps/perps.mjs +3 -0
- package/library/actions/mobile/perps/read-visible-state-loop.mjs +1 -0
- package/library/actions/mobile/perps/set_market_favorite.mjs +15 -19
- package/library/actions/mobile/platform/native-session.mjs +26 -17
- package/library/actions/mobile/platform/observe-ui.mjs +2 -2
- package/library/actions/mobile/ui/navigate.mjs +1 -1
- package/library/actions/shared/perps/visible-state.mjs +14 -3
- package/library/manifests/core.action-manifest.json +14 -2
- package/library/manifests/extension.action-manifest.json +49 -16
- package/library/manifests/mobile.action-manifest.json +8 -7
- package/library/recipes/core/perps/smoke.recipe.json +23 -0
- package/library/recipes/core/perps/snapshot.recipe.json +26 -1
- package/library/recipes/extension/assets/release-custom-network-token-persistence.recipe.json +25 -8
- package/library/recipes/extension/performance/navigation-memory-suite.recipe.json +219 -0
- package/library/recipes/extension/performance/navigation-memory.recipe.json +61 -156
- package/library/recipes/extension/wallet/smoke.recipe.json +17 -8
- package/library/recipes/mobile/app/lifecycle.android-smoke.recipe.json +23 -4
- package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +31 -6
- package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +5 -4
- package/library/recipes/mobile/wallet/smoke.recipe.json +17 -8
- package/library/recipes/{assets → shared/assets}/release-token-address-import.recipe.json +7 -0
- package/library/recipes/{assets → shared/assets}/release-token-details.recipe.json +2 -2
- package/library/recipes/shared/assets/release-token-hide-readd.recipe.json +91 -0
- package/library/recipes/{assets → shared/assets}/release-token-persistence.recipe.json +3 -0
- package/library/recipes/{assets → shared/assets}/release-token-search-import.recipe.json +4 -1
- package/library/recipes/{assets → shared/assets}/release-token-sorting.recipe.json +13 -2
- package/library/recipes/{swap-bridge → shared/swap-bridge}/release-quote.recipe.json +29 -3
- package/package.json +3 -3
- package/site/assets/help-recipes.json +63 -23
- package/library/recipes/assets/release-token-hide-readd.recipe.json +0 -57
- package/library/recipes/core/perps/clean-market-testnet.recipe.json +0 -44
- package/library/recipes/core/perps/order-lifecycle.recipe.json +0 -82
- package/library/recipes/core/perps/read-markets.recipe.json +0 -36
- package/library/recipes/core/perps/trading-lifecycle.recipe.json +0 -80
- package/library/recipes/extension/perps/release-activity-filters.recipe.json +0 -51
- package/library/recipes/extension/perps/release-funds-flows.recipe.json +0 -76
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +0 -109
- package/library/recipes/extension/perps/release-market-filters.recipe.json +0 -97
- package/library/recipes/extension/perps/release-order-entry.recipe.json +0 -158
- package/library/recipes/extension/perps/release-order-validation.recipe.json +0 -259
- package/library/recipes/extension/perps/release-visible-consistency.recipe.json +0 -47
- package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +0 -76
- package/library/recipes/mobile/perps/chase-assert-running.recipe.json +0 -110
- package/library/recipes/mobile/perps/chase-place.recipe.json +0 -145
- package/library/recipes/mobile/perps/chase-terminate.recipe.json +0 -98
- package/library/recipes/mobile/perps/performance.recipe.json +0 -722
- package/library/recipes/mobile/perps/pro-order-setup.recipe.json +0 -111
- package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +0 -53
- package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +0 -126
- package/library/recipes/mobile/perps/scale-place.recipe.json +0 -186
- package/library/recipes/mobile/perps/twap-assert-active.recipe.json +0 -97
- package/library/recipes/mobile/perps/twap-place.recipe.json +0 -146
- package/library/recipes/mobile/runner/action-validation.recipe.json +0 -336
- package/library/recipes/perps/clean-market-testnet.recipe.json +0 -49
- package/library/recipes/perps/lifecycle.recipe.json +0 -144
- package/library/recipes/perps/release-live-market-order.recipe.json +0 -139
- package/library/recipes/perps/release-watchlist.recipe.json +0 -111
- package/library/recipes/swap-bridge/release-max-quote.recipe.json +0 -72
- /package/library/recipes/{assets → shared/assets}/release-token-list.recipe.json +0 -0
- /package/library/recipes/{perps → shared/perps}/release-market-details.recipe.json +0 -0
- /package/library/recipes/{perps → shared/perps}/release-market-search.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-custom-slippage.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-max-swap.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-swap.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-surface.recipe.json +0 -0
- /package/library/recipes/{wallet → shared/wallet}/import.recipe.json +0 -0
- /package/library/recipes/{wallet → shared/wallet}/reset-import.recipe.json +0 -0
package/docs/RECIPES.md
CHANGED
|
@@ -32,7 +32,7 @@ task proof and may call other recipes.
|
|
|
32
32
|
"paramsSchema": {
|
|
33
33
|
"type": "object",
|
|
34
34
|
"properties": {
|
|
35
|
-
"
|
|
35
|
+
"timeout_ms": { "type": "integer", "minimum": 1, "default": 15000 }
|
|
36
36
|
},
|
|
37
37
|
"additionalProperties": false
|
|
38
38
|
},
|
|
@@ -41,6 +41,7 @@ task proof and may call other recipes.
|
|
|
41
41
|
"nodes": {
|
|
42
42
|
"unlock": {
|
|
43
43
|
"action": "metamask.wallet.ensure_unlocked",
|
|
44
|
+
"timeout_ms": "{{params.timeout_ms}}",
|
|
44
45
|
"intent": "Make the fixture-backed wallet ready for use",
|
|
45
46
|
"next": "done"
|
|
46
47
|
},
|
|
@@ -54,8 +55,8 @@ Defaults are applied before validation. Root values use `key=value`; a nested
|
|
|
54
55
|
recipe call uses `action: "call"`, `ref`, and `params`.
|
|
55
56
|
|
|
56
57
|
```bash
|
|
57
|
-
mm-harness run recipe.json
|
|
58
|
-
mm-harness run recipe.json
|
|
58
|
+
mm-harness run recipe.json timeout_ms=20000 --plan
|
|
59
|
+
mm-harness run recipe.json timeout_ms=20000
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
Use the shared wallet setup recipe on either Mobile or Extension. `auto`
|
|
@@ -76,11 +77,40 @@ fixture. Secret values are redacted from commands and evidence. Visible imports
|
|
|
76
77
|
default to MetaMetrics off and skip Mobile's optional interests; password terms
|
|
77
78
|
are required and always accepted by the action.
|
|
78
79
|
|
|
80
|
+
### Retained-memory measurements
|
|
81
|
+
|
|
82
|
+
Budget failures retain their samples and limits in a private JSON report linked
|
|
83
|
+
from the failed node's error. Inspect that report before attributing growth or
|
|
84
|
+
changing a budget; it is supporting evidence, not a passing measurement.
|
|
85
|
+
|
|
86
|
+
Use `performance.navigation-memory` for one screen in an unlocked Extension.
|
|
87
|
+
Choose its current route/marker, sample count, and growth bounds before running:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
mm-harness run performance.navigation-memory \
|
|
91
|
+
'screen_hash=#/settings' screen_test_id=settings-tab-bar-grouped --hud hide
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`performance.navigation-memory-suite` retains the full eight-screen audit,
|
|
95
|
+
detached-DOM check, and idle-control comparison. A one-screen slope does not
|
|
96
|
+
prove those broader claims. Check the suite's reference markers against the
|
|
97
|
+
tested build before running it.
|
|
98
|
+
|
|
79
99
|
### Extension Perps release recipes
|
|
80
100
|
|
|
81
101
|
Discover these identifiers with `run --list --adapter extension --json` and
|
|
82
102
|
inspect the exact graph with `run <id> --describe --adapter extension --json`:
|
|
83
103
|
|
|
104
|
+
Market/activity filters, visible consistency, funds previews, order-form cases,
|
|
105
|
+
live order journeys, watchlist and source-dev snapshot consistency are owned by the
|
|
106
|
+
[Perps team library](https://github.com/MetaMask/experimental-metamask-recipe-perps).
|
|
107
|
+
Register the library checkout to discover and compose those unchanged names:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
export RECIPE_LIBRARY_PATH="perps=/path/to/experimental-metamask-recipe-perps"
|
|
111
|
+
mm-harness run perps.release-market-filters --describe --adapter extension --json
|
|
112
|
+
```
|
|
113
|
+
|
|
84
114
|
| Behavior | Canonical recipe |
|
|
85
115
|
| --- | --- |
|
|
86
116
|
| Activity filter routes | `perps.release-activity-filters` |
|
|
@@ -90,10 +120,10 @@ inspect the exact graph with `run <id> --describe --adapter extension --json`:
|
|
|
90
120
|
| Market header and statistics | `perps.release-market-details` |
|
|
91
121
|
| Market categories | `perps.release-market-filters` |
|
|
92
122
|
| 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` |
|
|
123
|
+
| Long/Short and Market/Limit entry controls | `perps.release-order-entry case=configuration` |
|
|
124
|
+
| Amount, leverage, keyboard, auto-close, and TP/SL validation | `perps.release-order-entry case=validation` |
|
|
95
125
|
| Official-build rendered market/activity consistency | `perps.release-visible-consistency` |
|
|
96
|
-
| Favorite persistence and
|
|
126
|
+
| Favorite add/remove persistence and observed-baseline restoration | `perps.release-watchlist` |
|
|
97
127
|
| Hook-backed controller snapshot consistency | `perps.source-dev-snapshot-consistency` |
|
|
98
128
|
|
|
99
129
|
The source-development snapshot recipe is compatibility evidence only and must
|
|
@@ -102,11 +132,22 @@ Extension-scoped because their semantic actions are not shared release
|
|
|
102
132
|
capabilities. Keep ticket-specific orchestration task-local and compose these
|
|
103
133
|
identifiers with `call` instead of copying their graphs.
|
|
104
134
|
|
|
105
|
-
|
|
135
|
+
Watchlist proof requires a visible market-bound favorite baseline before any
|
|
136
|
+
toggle. Unknown is not false. Restoration is verified on the audited Extension
|
|
137
|
+
runtime; the audited Mobile reader cannot establish that baseline, so it must
|
|
138
|
+
stop before mutation rather than infer identity from unrelated market text.
|
|
139
|
+
|
|
140
|
+
The order-form recipe is in the registered Perps team library. Both cases require a market with no existing
|
|
106
141
|
position; the product exposes Modify/Close instead of New Long/Short for an
|
|
107
|
-
already-open market.
|
|
142
|
+
already-open market. Validation prices and leverage must match the selected
|
|
143
|
+
market's current rules; the examples are not universal defaults. Give each
|
|
144
|
+
composed case a distinct `capture_path`.
|
|
145
|
+
Source-development snapshot callers may opt into a
|
|
108
146
|
positive `minimum_changed_market_count`; the default does not require a price
|
|
109
147
|
tick, while typed-history and visible-activity agreement remain strict.
|
|
148
|
+
Use the snapshot recipe's `history` object to bound existing record dates and
|
|
149
|
+
`capture_path` to separate evidence when composing calls. Market sampling does
|
|
150
|
+
not repeatedly fetch history.
|
|
110
151
|
|
|
111
152
|
The two live recipes require explicit `confirm_live=true`. The trusted runner
|
|
112
153
|
must also verify the exact consented funding request, preflight, release proof,
|
|
@@ -126,15 +167,56 @@ using a generic UI press invalidates the cached identity proof; a fresh visible
|
|
|
126
167
|
`ensure_positions`, `ensure_orders`, and `teardown_state` only observe and assert
|
|
127
168
|
state that already exists; their schemas reject creation and cleanup inputs.
|
|
128
169
|
Extension does not expose `start_state`, readiness claims, multi-position
|
|
129
|
-
background closure
|
|
170
|
+
background closure. The former `runner.action-validation` is now a repository-only
|
|
171
|
+
maintainer exercise at `scripts/qa/mobile-action-validation.recipe.json`, outside
|
|
172
|
+
the installed catalog. It requires explicit disposable setup and reserved testnet
|
|
173
|
+
account/market inputs; it is not a full action audit or a normal task recipe. Financial
|
|
130
174
|
transitions must use an explicitly funded leaf action.
|
|
131
|
-
The
|
|
132
|
-
|
|
175
|
+
The Perps team library owns `perps.clean-market-testnet` for Mobile and Core,
|
|
176
|
+
not Extension. Register that library before discovery or execution.
|
|
177
|
+
They require explicit `account`, `market` and `exclusive_testnet_market=true`.
|
|
178
|
+
That confirmation does not reserve the market or authorize deleting someone
|
|
179
|
+
else's trades. Plan first and execute only with actual task ownership.
|
|
133
180
|
Planning or calling a recipe never weakens those gates.
|
|
134
181
|
|
|
182
|
+
### Mobile advanced-order setup
|
|
183
|
+
|
|
184
|
+
These recipes belong to the Perps team library. Register it once:
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
export RECIPE_LIBRARY_PATH="perps=/path/to/experimental-metamask-recipe-perps"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`perps.pro-order-setup` preserves existing orders, positions, and strategies by
|
|
191
|
+
default. Its Scale, Chase, and TWAP placement callers inherit that policy.
|
|
192
|
+
When a proof requires a clean testnet market, reserve the account/market and
|
|
193
|
+
compose explicit setup with `trading_state=clean_selected_testnet_market`.
|
|
194
|
+
This parameter never authorizes cleanup of another task's trades.
|
|
195
|
+
|
|
196
|
+
`perps.scale-assert-orders` checks the complete market order count before
|
|
197
|
+
capturing three visible rows. The generic `assert_output` action compares a
|
|
198
|
+
prior node's structured field, for example `$.matchingCount` with `eq` and
|
|
199
|
+
`{{params.orders}}`; it is not limited to command output. Count and visibility
|
|
200
|
+
still need task-owned submission IDs to prove a specific ladder.
|
|
201
|
+
|
|
202
|
+
### Fixed and Max quotes
|
|
203
|
+
|
|
204
|
+
`swap-bridge.release-quote` uses one graph for both amount modes. It defaults to
|
|
205
|
+
a fixed 0.0001 ETH-to-WETH quote on Ethereum with a 60-second transition budget.
|
|
206
|
+
Max ignores `amount` and uses the selected asset's available balance. Neither
|
|
207
|
+
mode submits a transaction.
|
|
208
|
+
|
|
209
|
+
```sh
|
|
210
|
+
mm-harness run swap-bridge.release-quote amount=0.0001
|
|
211
|
+
mm-harness run swap-bridge.release-quote amount_mode=max
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Set `capture_path` to a distinct filename when composing multiple quotes in
|
|
215
|
+
one run. Pair, chain, asset identity, and timeout inputs remain explicit options.
|
|
216
|
+
|
|
135
217
|
### Extension submission boundary
|
|
136
218
|
|
|
137
|
-
Extension currently exposes
|
|
219
|
+
Extension currently exposes non-submitting Assets and Swap/Bridge evidence plus send
|
|
138
220
|
preparation/review, quote, Max, and slippage actions. It does not expose
|
|
139
221
|
`metamask.assets.finish_send`, `metamask.swap_bridge.submit_transaction`, or
|
|
140
222
|
generic `ui.activate_and_observe`. Those operations remain unavailable until a
|
|
@@ -178,12 +260,13 @@ as supporting stills.
|
|
|
178
260
|
|
|
179
261
|
### Perps loading lifecycle validation
|
|
180
262
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
263
|
+
The Mobile loading recipe shares one graph across Android and iOS. It requires
|
|
264
|
+
the product's source-dev instrumentation; platform parity still needs live
|
|
265
|
+
validation. Register the Perps team library, then pin the device:
|
|
184
266
|
|
|
185
267
|
```bash
|
|
186
268
|
mm-harness run perps.performance \
|
|
269
|
+
--library perps=/path/to/experimental-metamask-recipe-perps \
|
|
187
270
|
--device <android-serial-or-ios-udid> \
|
|
188
271
|
account=<fixture-account-name> \
|
|
189
272
|
content_variant=trending \
|
|
@@ -196,10 +279,10 @@ The Mobile recipe HUD is shown by default, including during performance
|
|
|
196
279
|
validation. Use `--hud hide` only when a specific opaque/release proof requires
|
|
197
280
|
an unobstructed product surface; do not hide it merely to collect timings.
|
|
198
281
|
|
|
199
|
-
|
|
200
|
-
`
|
|
201
|
-
|
|
202
|
-
never
|
|
282
|
+
Declared lifecycle values are `navigate_return`, `cold_no_cache`,
|
|
283
|
+
`background_short`, `background_reconnect`, `account_switch`, and
|
|
284
|
+
`network_switch`. The graph requires an existing position or an account with
|
|
285
|
+
no positions or orders; it never trades to create either prerequisite. It checks native
|
|
203
286
|
visible content with `ui.wait_for`, captures production `[PerpsLoadProof]` and
|
|
204
287
|
WebSocket milestones, and leaves detailed TTC/DFD to the app's Sentry traces.
|
|
205
288
|
It never relabels Metro, fixture, or process-start duration as TTC.
|
|
@@ -207,6 +290,12 @@ The recipe does not label a checkout as main or candidate; pin and record the
|
|
|
207
290
|
exact checkout commit externally, then use captured source milestones to verify
|
|
208
291
|
which data path actually ran.
|
|
209
292
|
|
|
293
|
+
Warm return and short resume require resident-content milestones. The other
|
|
294
|
+
lifecycles also require live positions, orders and account streams. Each group
|
|
295
|
+
shares one capture ending, with the lifecycle and content identity preserved.
|
|
296
|
+
Cold preparation clears Perps disk/controller caches and disconnects its
|
|
297
|
+
channels before capture; an explicit process restart begins the proof.
|
|
298
|
+
|
|
210
299
|
Rules:
|
|
211
300
|
|
|
212
301
|
- Validate with `--plan` before side effects.
|
|
@@ -237,8 +326,9 @@ Locators, labels, routes, and screen structure can legitimately change, so a
|
|
|
237
326
|
failed recipe is not by itself evidence of either recipe drift or a product
|
|
238
327
|
regression.
|
|
239
328
|
|
|
240
|
-
1.
|
|
241
|
-
criteria, product contracts, or known-good evidence.
|
|
329
|
+
1. Preserve the first failure and compare it with authoritative acceptance
|
|
330
|
+
criteria, product contracts, or known-good evidence. Reproduce only after
|
|
331
|
+
confirming the action is safe to repeat.
|
|
242
332
|
2. Inspect the current product surface and resolved recipe source, then classify
|
|
243
333
|
the cause as product regression, recipe drift, fixture/runtime drift, or
|
|
244
334
|
harness defect.
|
|
@@ -268,6 +358,23 @@ If changing a value would violate the recipe's postcondition, keep it as a
|
|
|
268
358
|
documented recipe invariant rather than a parameter. Add a shared action only
|
|
269
359
|
when repeated direct access has a stable cross-task contract.
|
|
270
360
|
|
|
361
|
+
Core actions construct isolated controllers. Pass account and network into
|
|
362
|
+
each action; a separate initialization node cannot prepare the next process.
|
|
363
|
+
Skip setup that does no work, while retaining the independent read assertions.
|
|
364
|
+
Signing requires a non-empty fixture account name. Address-only inputs support
|
|
365
|
+
reads; they must not silently choose the default signing account.
|
|
366
|
+
|
|
367
|
+
The Perps team library's `perps.trading-lifecycle` combines the Core long market
|
|
368
|
+
and limit examples. The harness retains read-only `perps.smoke` and
|
|
369
|
+
`perps.snapshot` examples; trading policy belongs to the team library.
|
|
370
|
+
It requires an explicit fixture account, market, and confirmation of exclusive
|
|
371
|
+
testnet ownership. Initial assertions refuse non-flat state rather than clearing
|
|
372
|
+
it. Failure-path cleanup is guarded by completed initial assertions and remains
|
|
373
|
+
market-scoped, so it cannot safely share that account/market with another task.
|
|
374
|
+
The limit branch uses the original 30%-below-mid offset; it must prove the order
|
|
375
|
+
rests rather than assume it cannot fill. These trading paths still require live
|
|
376
|
+
validation with a reserved funded testnet account.
|
|
377
|
+
|
|
271
378
|
The protocol is authoritative:
|
|
272
379
|
<https://farmslot.io/docs/reference/recipe-protocol-v1>.
|
|
273
380
|
|
|
@@ -307,7 +414,11 @@ mm-harness run onboarding.smoke --plan
|
|
|
307
414
|
Use `--library wallet=/path/to/library` for one command. The alias is the
|
|
308
415
|
source name; without one, the directory name is used. Resolution follows the
|
|
309
416
|
explicit library order, then bundled MetaMask. Adapter-specific variants are
|
|
310
|
-
selected deterministically from `recipes/<adapter>/<domain>/`.
|
|
417
|
+
selected deterministically from `recipes/<adapter>/<domain>/`. Identical
|
|
418
|
+
cross-adapter graphs live once in `recipes/shared/<domain>/`; an adapter-specific
|
|
419
|
+
variant takes precedence. Both resolve as `<domain>.<recipe>`, without a platform
|
|
420
|
+
or `shared` prefix. Keep domains below `core`, `extension`, `mobile`, or `shared`.
|
|
421
|
+
Legacy
|
|
311
422
|
`*.<adapter>.recipe.json` paths remain readable during migration, but new and
|
|
312
423
|
migrated libraries use the adapter-first layout. A file cannot declare its
|
|
313
424
|
adapter through both forms. Every run records the root recipe, exact resolved
|
|
@@ -22,8 +22,9 @@ and trusted SHA-256. mm-harness never discovers or downloads a release.
|
|
|
22
22
|
|
|
23
23
|
For Extension, `runtime-launch` verifies the ZIP digest and manifest version,
|
|
24
24
|
extracts it safely, records archive and tree provenance, copies the exact tree
|
|
25
|
-
to an isolated runtime directory,
|
|
26
|
-
launches without building or patching the checkout.
|
|
25
|
+
to an isolated runtime directory, preserves the selected Chrome profile, and
|
|
26
|
+
launches without building or patching the checkout. A profile reset requires
|
|
27
|
+
explicit `--reset-profile`; an ordinary launch does not apply a wallet fixture.
|
|
27
28
|
|
|
28
29
|
For Android, `runtime-launch` requires one physical-device serial, verifies the
|
|
29
30
|
local APK digest, package ID, version name, and version code, installs it, and
|
|
@@ -498,6 +498,16 @@ function registerSignerHandlers(rootMessenger, childMessenger, account) {
|
|
|
498
498
|
* @returns { controller, projectRoot, network, accountAddress, signerAddress }.
|
|
499
499
|
*/
|
|
500
500
|
export async function getCoreControllerWithSigner(input) {
|
|
501
|
+
const requestedName = input.node?.account_name ?? input.node?.account;
|
|
502
|
+
if (requestedName !== undefined && (
|
|
503
|
+
typeof requestedName !== 'string' || !requestedName.trim() ||
|
|
504
|
+
/^0x[0-9a-fA-F]{40}$/u.test(requestedName.trim())
|
|
505
|
+
)) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
'Core signing requires a non-empty fixture account name, not an address.\n' +
|
|
508
|
+
'Next: pass account=<reserved-fixture-name>; use addresses only for read-only actions.',
|
|
509
|
+
);
|
|
510
|
+
}
|
|
501
511
|
const base = await getCoreController(input);
|
|
502
512
|
const { controller, messenger: childMessenger, rootMessenger, accountAddress } = base;
|
|
503
513
|
if (!rootMessenger || !childMessenger) {
|
|
@@ -10,21 +10,14 @@ import {
|
|
|
10
10
|
optionalParam,
|
|
11
11
|
} from './_controller.mjs';
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// path (Slice 2). Mirrors close_positions.mjs: read open orders → cancel the
|
|
16
|
-
// selected subset via the controller → verify the selected orders are gone.
|
|
17
|
-
//
|
|
18
|
-
// cancelOrders() resolves the concrete orders to cancel via the ACTIVE provider's
|
|
19
|
-
// getOpenOrders() (PerpsController wires getOpenOrders: () => this.getOpenOrders()
|
|
20
|
-
// into the cancel context) and then batch-cancels by symbol. We pass the selected
|
|
21
|
-
// symbols; with mode=all (no explicit market) we cancel every open order.
|
|
13
|
+
// Snapshot selection into exact IDs so dispatch and postcondition checks cannot
|
|
14
|
+
// expand to opposite-side or newly created orders sharing a market symbol.
|
|
22
15
|
|
|
23
16
|
function sleep(ms) {
|
|
24
17
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
18
|
}
|
|
26
19
|
|
|
27
|
-
async function waitForOrdersAbsent(controller, accountAddress,
|
|
20
|
+
async function waitForOrdersAbsent(controller, accountAddress, orderIds, timeoutMs) {
|
|
28
21
|
const deadline = Date.now() + timeoutMs;
|
|
29
22
|
let orders = [];
|
|
30
23
|
for (;;) {
|
|
@@ -32,7 +25,7 @@ async function waitForOrdersAbsent(controller, accountAddress, symbols, timeoutM
|
|
|
32
25
|
standalone: true,
|
|
33
26
|
userAddress: accountAddress,
|
|
34
27
|
});
|
|
35
|
-
const remaining = orders.filter((order) =>
|
|
28
|
+
const remaining = orders.filter((order) => orderIds.includes(order.orderId));
|
|
36
29
|
if (remaining.length === 0) return orders;
|
|
37
30
|
if (Date.now() >= deadline) return orders;
|
|
38
31
|
await sleep(1000);
|
|
@@ -79,20 +72,22 @@ export async function closeOrders(input) {
|
|
|
79
72
|
};
|
|
80
73
|
}
|
|
81
74
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
75
|
+
const orderIds = matching.map((order) => order.orderId);
|
|
76
|
+
if (orderIds.some((id) => typeof id !== 'string' || !id.trim()) || new Set(orderIds).size !== orderIds.length) {
|
|
77
|
+
throw new Error('Selected Core orders require unique non-empty orderId values before cancellation.\nNext: mm-harness actions metamask.perps.read_orders --adapter core --json');
|
|
78
|
+
}
|
|
79
|
+
const result = await controller.cancelOrders({ orderIds });
|
|
85
80
|
const failure = cancelOrdersFailure(result);
|
|
86
81
|
if (failure !== undefined) {
|
|
87
82
|
throw controllerRejection({
|
|
88
83
|
action: 'core cancelOrders',
|
|
89
|
-
detail: `${JSON.stringify(
|
|
84
|
+
detail: `${JSON.stringify(orderIds)}: ${JSON.stringify(result)}`,
|
|
90
85
|
code: failure,
|
|
91
86
|
});
|
|
92
87
|
}
|
|
93
88
|
|
|
94
|
-
const after = await waitForOrdersAbsent(controller, accountAddress,
|
|
95
|
-
const stillOpen = after.filter((order) =>
|
|
89
|
+
const after = await waitForOrdersAbsent(controller, accountAddress, orderIds, timeoutMs);
|
|
90
|
+
const stillOpen = after.filter((order) => orderIds.includes(order.orderId));
|
|
96
91
|
if (stillOpen.length > 0) {
|
|
97
92
|
throw new Error(
|
|
98
93
|
`Expected selected orders to cancel, but ${stillOpen.length} remain: ${JSON.stringify(result)}`,
|
|
@@ -109,6 +104,7 @@ export async function closeOrders(input) {
|
|
|
109
104
|
successCount: result.successCount,
|
|
110
105
|
failureCount: result.failureCount,
|
|
111
106
|
symbols,
|
|
107
|
+
orderIds,
|
|
112
108
|
results: result.results,
|
|
113
109
|
orders: stillOpen.map(redactOrder),
|
|
114
110
|
proofPath: 'perps-controller-cancelOrders',
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { openHome } from '../ui/navigate.mjs';
|
|
2
|
+
|
|
3
|
+
export async function verifyHomeIdentity(page, token, assetId, timeoutMs) {
|
|
4
|
+
try {
|
|
5
|
+
await page.clickPoint(token.point.x, token.point.y);
|
|
6
|
+
const deadline = Date.now() + timeoutMs;
|
|
7
|
+
let route;
|
|
8
|
+
while (Date.now() < deadline) {
|
|
9
|
+
const value = String(await page.evaluate('location.hash'));
|
|
10
|
+
if (value.startsWith('#/asset/')) {
|
|
11
|
+
route = value;
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
await page.evaluate('new Promise((resolve) => setTimeout(resolve, 150))');
|
|
15
|
+
}
|
|
16
|
+
if (!route) throw new Error('The selected Home token did not expose an asset-details route.');
|
|
17
|
+
const [, , chainId, address] = route.split('/');
|
|
18
|
+
if (!/^0x[0-9a-f]+$/iu.test(chainId ?? '') || !/^0x[0-9a-f]{40}$/iu.test(address ?? '')) {
|
|
19
|
+
throw new Error('The selected Home route does not expose an ERC-20 identity.');
|
|
20
|
+
}
|
|
21
|
+
const observed = `eip155:${BigInt(chainId).toString()}/erc20:${address.toLowerCase()}`;
|
|
22
|
+
if (observed !== assetId) {
|
|
23
|
+
throw new Error(`Home asset identity ${observed} does not match ${assetId}.\nNext: select a network filter with an unambiguous matching token before continuing.`);
|
|
24
|
+
}
|
|
25
|
+
return { assetId, route, verified: true };
|
|
26
|
+
} finally {
|
|
27
|
+
await openHome(page, timeoutMs);
|
|
28
|
+
delete token.point;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import { readWalletStateExpression } from '../wallet/state.mjs';
|
|
3
|
+
import { verifyHomeIdentity } from './home-token-identity.mjs';
|
|
2
4
|
|
|
3
5
|
const OPTIONS = dataTestId('asset-list-control-bar-action-button');
|
|
4
6
|
const MANAGE = dataTestId('manageTokens__button');
|
|
5
7
|
const ADD = dataTestId('token-management-add-custom-token-button');
|
|
6
8
|
const PAGE = dataTestId('custom-token-import-page');
|
|
7
9
|
const NETWORK = dataTestId('network-selector');
|
|
10
|
+
const NETWORK_PICKER = dataTestId('custom-token-import-network-selector');
|
|
8
11
|
const ADDRESS = dataTestId('custom-token-import-address-input');
|
|
9
12
|
const SYMBOL = dataTestId('custom-token-import-symbol-input');
|
|
10
13
|
const DECIMALS = dataTestId('custom-token-import-decimal-input');
|
|
@@ -33,96 +36,69 @@ async function waitFor(page, predicate, message, timeoutMs) {
|
|
|
33
36
|
throw new Error(message);
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
async function
|
|
37
|
-
const marker = `mm-harness-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
38
|
-
const found = await page.evaluate(`(() => {
|
|
39
|
-
const label = ${JSON.stringify(label)};
|
|
40
|
-
const marker = ${JSON.stringify(marker)};
|
|
41
|
-
for (const element of document.querySelectorAll('button')) {
|
|
42
|
-
const text = String(element.innerText || element.textContent || '').replace(/\\s+/gu, ' ').trim();
|
|
43
|
-
const style = getComputedStyle(element);
|
|
44
|
-
const rect = element.getBoundingClientRect();
|
|
45
|
-
if (text === label && style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0) {
|
|
46
|
-
element.setAttribute('data-mm-harness-target', marker);
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return false;
|
|
51
|
-
})()`);
|
|
52
|
-
if (!found) throw new Error(`Visible button ${JSON.stringify(label)} was not found.`);
|
|
53
|
-
return `[data-mm-harness-target="${marker}"]`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function networkOption(page, network, chainId) {
|
|
39
|
+
async function networkOption(page, chainId) {
|
|
57
40
|
return page.evaluate(`(() => {
|
|
58
|
-
const
|
|
41
|
+
const picker = document.querySelector(${JSON.stringify(NETWORK_PICKER)});
|
|
42
|
+
if (!picker) return null;
|
|
59
43
|
const chainIds = new Set(${JSON.stringify([
|
|
60
44
|
String(chainId),
|
|
61
45
|
`0x${chainId.toString(16)}`,
|
|
62
46
|
`eip155:${chainId}`,
|
|
63
47
|
].map((value) => value.toLowerCase()))});
|
|
64
|
-
for (const element of
|
|
48
|
+
for (const element of picker.querySelectorAll('[data-testid^="network-list-item-"]')) {
|
|
65
49
|
const testId = String(element.getAttribute('data-testid') || '');
|
|
66
50
|
const suffix = testId.slice('network-list-item-'.length).toLowerCase();
|
|
51
|
+
if (!chainIds.has(suffix)) continue;
|
|
52
|
+
element.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
53
|
+
const rect = element.getBoundingClientRect();
|
|
54
|
+
const hit = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
55
|
+
if (!element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
|
|
56
|
+
|| rect.width <= 0 || rect.height <= 0 || !hit || !element.contains(hit)) continue;
|
|
67
57
|
const label = String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500);
|
|
68
|
-
|
|
58
|
+
return { testId, label };
|
|
69
59
|
}
|
|
70
60
|
return null;
|
|
71
61
|
})()`);
|
|
72
62
|
}
|
|
73
63
|
|
|
74
|
-
async function homeToken(page, symbol) {
|
|
64
|
+
async function homeToken(page, symbol, verifyIdentity = false) {
|
|
75
65
|
return page.evaluate(`(() => {
|
|
76
66
|
const symbol = ${JSON.stringify(symbol.toLowerCase())};
|
|
67
|
+
const verifyIdentity = ${JSON.stringify(verifyIdentity)};
|
|
68
|
+
const matches = [];
|
|
77
69
|
for (const row of document.querySelectorAll('[data-testid="multichain-token-list-button"]')) {
|
|
78
70
|
const name = String(row.querySelector('[data-testid="multichain-token-list-item-token-name"]')?.innerText || '').replace(/\\s+/gu, ' ').trim();
|
|
79
71
|
const amount = String(row.querySelector('[data-testid="multichain-token-list-item-value"]')?.innerText || '').replace(/\\s+/gu, ' ').trim();
|
|
80
72
|
const observed = amount.match(/\\b([A-Za-z][A-Za-z0-9]{1,11})$/u)?.[1] || '';
|
|
81
|
-
|
|
73
|
+
const style = getComputedStyle(row);
|
|
74
|
+
const rect = row.getBoundingClientRect();
|
|
75
|
+
if (observed.toLowerCase() !== symbol || style.display === 'none' || style.visibility === 'hidden'
|
|
76
|
+
|| rect.width <= 0 || rect.height <= 0) continue;
|
|
77
|
+
matches.push({ row, name, symbol: observed.toUpperCase(), amount });
|
|
82
78
|
}
|
|
83
|
-
|
|
79
|
+
if (matches.length > 1) throw new Error('Visible token symbol matches multiple rows; identity is ambiguous.\\nNext: narrow the product network filter before importing.');
|
|
80
|
+
if (!matches.length) return null;
|
|
81
|
+
const { row, ...token } = matches[0];
|
|
82
|
+
if (verifyIdentity) {
|
|
83
|
+
row.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
84
|
+
const rect = row.getBoundingClientRect();
|
|
85
|
+
const x = rect.left + rect.width / 2;
|
|
86
|
+
const y = rect.top + rect.height / 2;
|
|
87
|
+
const hit = document.elementFromPoint(x, y);
|
|
88
|
+
if (!hit || !row.contains(hit)) throw new Error('The matching Home token is covered; identity cannot be verified.\\nNext: uncover the token row before importing.');
|
|
89
|
+
token.point = { x, y };
|
|
90
|
+
}
|
|
91
|
+
return token;
|
|
84
92
|
})()`);
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
async function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (!allTokens || typeof allTokens !== 'object' || !selectedAddress) {
|
|
95
|
-
return { available: false, token: null };
|
|
96
|
-
}
|
|
97
|
-
const expectedChainId = ${JSON.stringify(chainId)};
|
|
98
|
-
const chainEntry = Object.entries(allTokens).find(([key]) => {
|
|
99
|
-
const normalized = String(key).toLowerCase();
|
|
100
|
-
const value = normalized.startsWith('eip155:')
|
|
101
|
-
? Number(normalized.slice('eip155:'.length))
|
|
102
|
-
: normalized.startsWith('0x')
|
|
103
|
-
? Number.parseInt(normalized.slice(2), 16)
|
|
104
|
-
: Number(normalized);
|
|
105
|
-
return value === expectedChainId;
|
|
106
|
-
});
|
|
107
|
-
const accountEntry = chainEntry && Object.entries(chainEntry[1] || {})
|
|
108
|
-
.find(([account]) => String(account).toLowerCase() === selectedAddress);
|
|
109
|
-
const tokens = Array.isArray(accountEntry?.[1]) ? accountEntry[1] : [];
|
|
110
|
-
const expectedAddress = ${JSON.stringify(address.toLowerCase())};
|
|
111
|
-
const expectedSymbol = ${JSON.stringify(expectedSymbol)};
|
|
112
|
-
const token = tokens.find((entry) => {
|
|
113
|
-
const tokenAddress = String(entry?.address || entry?.contractAddress || '').toLowerCase();
|
|
114
|
-
return tokenAddress === expectedAddress && String(entry?.symbol || '').toUpperCase() === expectedSymbol;
|
|
115
|
-
});
|
|
116
|
-
return {
|
|
117
|
-
available: true,
|
|
118
|
-
token: token ? {
|
|
119
|
-
address: String(token.address || token.contractAddress),
|
|
120
|
-
symbol: String(token.symbol),
|
|
121
|
-
decimals: token.decimals == null ? null : String(token.decimals),
|
|
122
|
-
chainId: chainEntry?.[0] || null,
|
|
123
|
-
} : null,
|
|
124
|
-
};
|
|
125
|
-
})()`, { awaitPromise: true });
|
|
95
|
+
async function selectedEvmAccount(page) {
|
|
96
|
+
const { selectedAccount } = await page.evaluate(readWalletStateExpression(), { awaitPromise: true });
|
|
97
|
+
if (!selectedAccount?.id || !['eip155:eoa', 'eip155:erc4337'].includes(selectedAccount.type)
|
|
98
|
+
|| !/^0x[0-9a-f]{40}$/iu.test(selectedAccount.address ?? '')) {
|
|
99
|
+
throw new Error('The selected EVM account is unavailable; existing token identity cannot be verified.\nNext: select the intended EVM account before importing.');
|
|
100
|
+
}
|
|
101
|
+
return { id: selectedAccount.id, address: selectedAccount.address.toLowerCase(), type: selectedAccount.type };
|
|
126
102
|
}
|
|
127
103
|
|
|
128
104
|
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
@@ -136,39 +112,44 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
136
112
|
throw new Error('metamask.assets.import_custom_token requires network, positive chain_id, ERC-20 address, and expected_symbol.');
|
|
137
113
|
}
|
|
138
114
|
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
115
|
+
const account = await selectedEvmAccount(page);
|
|
116
|
+
const existingToken = await homeToken(page, expectedSymbol, true);
|
|
117
|
+
if (existingToken) {
|
|
118
|
+
const existingIdentity = await verifyHomeIdentity(
|
|
119
|
+
page, existingToken, `eip155:${chainId}/erc20:${address.toLowerCase()}`, timeoutMs,
|
|
120
|
+
);
|
|
121
|
+
const afterAccount = await selectedEvmAccount(page);
|
|
122
|
+
if (JSON.stringify(afterAccount) !== JSON.stringify(account)) {
|
|
123
|
+
throw new Error('The selected EVM account changed while checking the existing token.\nNext: keep the intended account selected for the full import check.');
|
|
124
|
+
}
|
|
144
125
|
return {
|
|
145
126
|
action: input.action,
|
|
146
127
|
network,
|
|
147
128
|
chainId,
|
|
148
129
|
address,
|
|
149
130
|
alreadyImported: true,
|
|
131
|
+
changed: false,
|
|
150
132
|
token: existingToken,
|
|
151
|
-
identity: existingIdentity.
|
|
133
|
+
identity: { ...existingIdentity, account, address, symbol: expectedSymbol, decimals: null, chainId: `0x${chainId.toString(16)}` },
|
|
152
134
|
proofPath: 'trusted-pointer-visible-ui',
|
|
153
135
|
};
|
|
154
136
|
}
|
|
155
137
|
|
|
156
138
|
if (!(await visible(page, MANAGE))) await page.click(OPTIONS);
|
|
157
139
|
await waitFor(page, () => visible(page, MANAGE), 'Manage tokens did not become visible.', timeoutMs);
|
|
158
|
-
await page.click(
|
|
140
|
+
await page.click(MANAGE);
|
|
159
141
|
await waitFor(page, () => visible(page, ADD), 'Add a custom token did not become visible.', timeoutMs);
|
|
160
|
-
await page.click(
|
|
142
|
+
await page.click(ADD);
|
|
161
143
|
await waitFor(page, () => visible(page, PAGE), 'Custom token import did not open.', timeoutMs);
|
|
162
144
|
await page.click(NETWORK);
|
|
163
145
|
const selectedNetwork = await waitFor(
|
|
164
146
|
page,
|
|
165
|
-
() => networkOption(page,
|
|
147
|
+
() => networkOption(page, chainId),
|
|
166
148
|
`${network} (${chainId}) was not available in the custom token network selector.`,
|
|
167
149
|
timeoutMs,
|
|
168
150
|
);
|
|
169
|
-
await page.click(dataTestId(selectedNetwork.testId));
|
|
170
|
-
await waitFor(page, () => visible(page, ADDRESS), 'Custom token
|
|
171
|
-
await page.evaluate('new Promise((resolve) => setTimeout(resolve, 2000))');
|
|
151
|
+
await page.click(`${NETWORK_PICKER} ${dataTestId(selectedNetwork.testId)}`);
|
|
152
|
+
await waitFor(page, async () => !(await visible(page, NETWORK_PICKER)) && await visible(page, ADDRESS), 'Custom token network selection did not return to the address form.', timeoutMs);
|
|
172
153
|
await page.setInput(ADDRESS, address);
|
|
173
154
|
|
|
174
155
|
let metadataSource = 'network';
|
|
@@ -183,7 +164,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
183
164
|
return state.symbol && state.enabled ? state : null;
|
|
184
165
|
}, 'Custom token metadata did not resolve to an enabled import.', Math.min(timeoutMs, 15_000));
|
|
185
166
|
} catch (error) {
|
|
186
|
-
if (expectedDecimals === null) throw error;
|
|
167
|
+
if (expectedDecimals === null || !(await visible(page, SYMBOL)) || !(await visible(page, DECIMALS))) throw error;
|
|
187
168
|
metadataSource = 'manual';
|
|
188
169
|
await page.setInput(SYMBOL, expectedSymbol);
|
|
189
170
|
await page.setInput(DECIMALS, expectedDecimals);
|
|
@@ -216,6 +197,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
216
197
|
|
|
217
198
|
return {
|
|
218
199
|
action: input.action,
|
|
200
|
+
changed: true,
|
|
219
201
|
network: selectedNetwork.label || network,
|
|
220
202
|
chainId,
|
|
221
203
|
address,
|