@deeeed/metamask-harness 0.36.0 → 0.38.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 +51 -0
- package/adapters/extension/artifact-runtime-state.cjs +138 -0
- package/adapters/extension/inject.mjs +2 -0
- package/adapters/extension/launch-browser.cjs +24 -0
- package/adapters/extension/live.sh +82 -12
- package/adapters/extension/readiness.mjs +77 -36
- package/adapters/extension/snapshot-dist.sh +88 -3
- package/adapters/extension/verify.sh +6 -2
- package/adapters/manifest.json +17 -1
- package/adapters/shared/log-tui.mjs +1 -1
- package/dist/adapters/extension/artifact-integrity.js +38 -0
- package/dist/adapters/extension/extension-id.js +23 -4
- package/dist/adapters/extension/release-artifact.js +386 -0
- package/dist/adapters/extension/runtime-decision.js +164 -20
- package/dist/adapters/extension/runtime.js +169 -3
- package/dist/adapters/mobile/provision.js +38 -3
- package/dist/adapters/mobile/release-artifact-state.js +124 -0
- package/dist/adapters/mobile/release-artifact.js +295 -0
- package/dist/adapters.js +78 -6
- package/dist/app-lifecycle.js +4 -3
- package/dist/command-contract.js +16 -0
- package/dist/commands/call.js +6 -2
- package/dist/commands/launch/extension.js +74 -9
- package/dist/commands/launch/index.js +3 -0
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/provision.js +2 -0
- package/dist/commands/run-engine.js +109 -14
- package/dist/commands/run.js +4 -2
- package/dist/commands/runtime-launch.js +178 -10
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +3 -1
- package/dist/metamask-action-validation.js +47 -1
- package/dist/mm-harness-cli.js +31 -1
- package/dist/recipe-security.js +40 -0
- package/dist/run-diagnostics.js +1 -1
- package/dist/runner.js +3 -0
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +125 -0
- package/library/actions/extension/assets/finish_send.mjs +128 -0
- package/library/actions/extension/assets/import_custom_token.mjs +227 -0
- package/library/actions/extension/assets/prepare_send.mjs +53 -0
- package/library/actions/extension/assets/read_details.mjs +57 -0
- package/library/actions/extension/assets/read_visible_state.mjs +50 -0
- package/library/actions/extension/assets/review_send.mjs +23 -0
- package/library/actions/extension/assets/select_network_scope.mjs +30 -0
- package/library/actions/extension/assets/send.mjs +104 -0
- package/library/actions/extension/assets/set_custom_gas.mjs +56 -0
- package/library/actions/extension/assets/set_token_visibility.mjs +129 -0
- package/library/actions/extension/assets/verify_sorting.mjs +89 -0
- package/library/actions/extension/networks/add_chainlist.mjs +94 -0
- package/library/actions/extension/networks/add_custom.mjs +123 -0
- package/library/actions/extension/networks/custom_network.mjs +103 -0
- package/library/actions/extension/networks/read_visible_state.mjs +50 -0
- package/library/actions/extension/networks/remove_custom.mjs +68 -0
- package/library/actions/extension/perps/close_orders.mjs +1 -0
- package/library/actions/extension/perps/close_visible_position.mjs +133 -0
- package/library/actions/extension/perps/compare_provider_market.mjs +109 -0
- package/library/actions/extension/perps/edit_margin.mjs +82 -0
- package/library/actions/extension/perps/open_balance_action.mjs +58 -0
- package/library/actions/extension/perps/open_position_action.mjs +80 -0
- package/library/actions/extension/perps/perps.mjs +196 -2
- package/library/actions/extension/perps/place_order.mjs +5 -2
- package/library/actions/extension/perps/read_eligibility.mjs +34 -0
- package/library/actions/extension/perps/read_funds_confirmation.mjs +69 -0
- package/library/actions/extension/perps/read_snapshot.mjs +470 -0
- package/library/actions/extension/perps/read_visible_state.mjs +98 -0
- package/library/actions/extension/perps/select_activity_filter.mjs +44 -0
- package/library/actions/extension/perps/select_market_filter.mjs +71 -0
- package/library/actions/extension/perps/set_market_favorite.mjs +47 -0
- package/library/actions/extension/perps/update_position_tpsl.mjs +106 -0
- package/library/actions/extension/platform/cdp.mjs +109 -3
- package/library/actions/extension/settings/set_basic_functionality.mjs +58 -0
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +71 -0
- package/library/actions/extension/swap_bridge/select_assets.mjs +103 -0
- package/library/actions/extension/swap_bridge/set_amount.mjs +36 -0
- package/library/actions/extension/swap_bridge/set_max_amount.mjs +58 -0
- package/library/actions/extension/swap_bridge/set_slippage.mjs +121 -0
- package/library/actions/extension/swap_bridge/submit_transaction.mjs +363 -0
- package/library/actions/extension/ui/activate_and_observe.mjs +96 -0
- package/library/actions/extension/ui/navigate.mjs +264 -8
- package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -6
- package/library/actions/extension/wallet/import.mjs +13 -46
- package/library/actions/extension/wallet/lock.mjs +35 -0
- package/library/actions/extension/wallet/secret-input.mjs +98 -0
- package/library/actions/extension/wallet/select_account.mjs +71 -7
- package/library/actions/mobile/assets/read_visible_state.mjs +37 -0
- package/library/actions/mobile/perps/read_visible_state.mjs +31 -0
- package/library/actions/mobile/platform/native-session-name.mjs +5 -0
- package/library/actions/mobile/platform/observe-ui.mjs +86 -2
- package/library/actions/mobile/swap_bridge/native-session.mjs +45 -0
- package/library/actions/mobile/swap_bridge/read_visible_state.mjs +31 -0
- package/library/actions/mobile/swap_bridge/select_assets.mjs +94 -0
- package/library/actions/mobile/swap_bridge/set_amount.mjs +73 -0
- package/library/actions/mobile/ui/native-navigation.mjs +265 -0
- package/library/actions/mobile/ui/navigate.mjs +10 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +17 -0
- package/library/actions/mobile/wallet/import.mjs +71 -2
- package/library/actions/mobile/wallet/native-ui.mjs +512 -0
- package/library/actions/mobile/wallet/read_state.mjs +16 -0
- package/library/actions/mobile/wallet/reset.mjs +17 -4
- package/library/actions/shared/assets/visible-state.mjs +176 -0
- package/library/actions/shared/networks/visible-state.mjs +70 -0
- package/library/actions/shared/perps/visible-state.mjs +354 -0
- package/library/actions/shared/swap-bridge/transaction.mjs +54 -0
- package/library/actions/shared/swap-bridge/visible-state.mjs +97 -0
- package/library/actions/shared/ui/locators.mjs +7 -0
- package/library/manifests/extension.action-manifest.json +1366 -184
- package/library/manifests/mobile.action-manifest.json +235 -1
- package/library/recipes/assets/release-custom-gas-send.recipe.json +88 -0
- package/library/recipes/assets/release-custom-network-token-persistence.recipe.json +106 -0
- package/library/recipes/assets/release-native-max-cancel.recipe.json +70 -0
- package/library/recipes/assets/release-native-max-send.recipe.json +78 -0
- package/library/recipes/assets/release-own-account-send.recipe.json +80 -0
- package/library/recipes/assets/release-token-address-import.recipe.json +47 -0
- package/library/recipes/assets/release-token-details.recipe.json +48 -0
- package/library/recipes/assets/release-token-hide-readd.recipe.json +55 -0
- package/library/recipes/assets/release-token-list.recipe.json +75 -0
- package/library/recipes/assets/release-token-persistence.recipe.json +73 -0
- package/library/recipes/assets/release-token-search-import.recipe.json +40 -0
- package/library/recipes/assets/release-token-sorting.recipe.json +29 -0
- package/library/recipes/extension/networks/release-balance-stability.recipe.json +51 -0
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +104 -0
- package/library/recipes/extension/perps/release-order-entry.recipe.json +155 -0
- package/library/recipes/extension/perps/release-order-validation.recipe.json +220 -0
- package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
- package/library/recipes/perps/release-activity-filters.recipe.json +51 -0
- package/library/recipes/perps/release-funds-flows.recipe.json +76 -0
- package/library/recipes/perps/release-live-market-order.recipe.json +122 -0
- package/library/recipes/perps/release-market-details.recipe.json +83 -0
- package/library/recipes/perps/release-market-search.recipe.json +79 -0
- package/library/recipes/perps/release-watchlist.recipe.json +99 -0
- package/library/recipes/swap-bridge/release-custom-slippage.recipe.json +90 -0
- package/library/recipes/swap-bridge/release-live-max-swap.recipe.json +99 -0
- package/library/recipes/swap-bridge/release-live-swap.recipe.json +101 -0
- package/library/recipes/swap-bridge/release-max-quote.recipe.json +72 -0
- package/library/recipes/swap-bridge/release-quote.recipe.json +72 -0
- package/library/recipes/swap-bridge/release-surface.recipe.json +43 -0
- package/library/recipes/wallet/import.recipe.json +20 -1
- package/library/recipes/wallet/reset-import.recipe.json +20 -1
- package/package.json +6 -5
- package/scripts/validate-release-report.mjs +319 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { createAgentDeviceUiTransport } from '@farmslot/expo-recipe';
|
|
2
|
+
import { createAgentDeviceClient } from 'agent-device';
|
|
3
|
+
import { nativeSessionName } from '../platform/native-session-name.mjs';
|
|
4
|
+
|
|
5
|
+
const IDS = {
|
|
6
|
+
wallet: ['wallet-safe-area', 'homepage-container'],
|
|
7
|
+
walletTab: 'tab-bar-item-Wallet',
|
|
8
|
+
tradeTab: 'tab-bar-item-Trade',
|
|
9
|
+
perpsEntry: 'homepage-balance-breakdown-row-perps',
|
|
10
|
+
perpsTradeEntry: 'wallet-perps-action',
|
|
11
|
+
swapTradeEntry: 'wallet-actions-bottom-sheet-swap-button',
|
|
12
|
+
swapSourceInput: 'source-token-area-input',
|
|
13
|
+
swapSourceToken: 'source-token-area',
|
|
14
|
+
swapDestinationToken: 'dest-token-area',
|
|
15
|
+
perpsHome: 'perps-home',
|
|
16
|
+
perpsHomeBack: 'perps-home-back-button',
|
|
17
|
+
tutorialSkip: 'perps-tutorial-skip-button',
|
|
18
|
+
market: 'perps-market-details-view',
|
|
19
|
+
marketBack: 'perps-market-header-back-button',
|
|
20
|
+
marketSubtitle: 'perps-market-header-subtitle',
|
|
21
|
+
proMarket: 'perps-pro-market-scroll-view',
|
|
22
|
+
proMarketBack: 'perps-pro-market-header-back-button',
|
|
23
|
+
proMarketList: 'perps-pro-market-header-market-list-button',
|
|
24
|
+
proMarketSubtitle: 'perps-pro-market-header-subtitle',
|
|
25
|
+
marketList: 'perps-market-list',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export async function navigateOpaqueMobile(input, destination) {
|
|
29
|
+
const env = { ...process.env, ...(input.context?.env ?? {}) };
|
|
30
|
+
const device = String(env.ADB_SERIAL ?? env.ANDROID_SERIAL ?? '').trim();
|
|
31
|
+
if (!device) {
|
|
32
|
+
throw new Error('Opaque Mobile navigation requires one explicit Android device serial.');
|
|
33
|
+
}
|
|
34
|
+
const app = String(env.ANDROID_PACKAGE_ID ?? 'io.metamask');
|
|
35
|
+
const session = nativeSessionName(env, device, app);
|
|
36
|
+
const client = createAgentDeviceClient({
|
|
37
|
+
session,
|
|
38
|
+
lockPolicy: 'reject',
|
|
39
|
+
lockPlatform: 'android',
|
|
40
|
+
});
|
|
41
|
+
const selection = { platform: 'android', target: 'mobile', serial: device, session };
|
|
42
|
+
const transport = createAgentDeviceUiTransport({
|
|
43
|
+
platform: 'android',
|
|
44
|
+
device,
|
|
45
|
+
app,
|
|
46
|
+
session,
|
|
47
|
+
client,
|
|
48
|
+
});
|
|
49
|
+
const context = {
|
|
50
|
+
nodeId: String(input.context?.nodeId ?? 'navigate'),
|
|
51
|
+
projectRoot: input.context.projectRoot,
|
|
52
|
+
artifactsDir: input.context.artifactsDir,
|
|
53
|
+
env,
|
|
54
|
+
};
|
|
55
|
+
const press = (testId) => transport.execute('ui.press', {
|
|
56
|
+
test_id: testId,
|
|
57
|
+
timeout_ms: Number(input.node?.timeout_ms ?? 30_000),
|
|
58
|
+
}, context);
|
|
59
|
+
const wait = (testId) => transport.execute('ui.wait_for', {
|
|
60
|
+
test_id: testId,
|
|
61
|
+
expected: 'visible',
|
|
62
|
+
timeout_ms: Number(input.node?.timeout_ms ?? 30_000),
|
|
63
|
+
settle: false,
|
|
64
|
+
}, context);
|
|
65
|
+
const back = () => client.command.back({
|
|
66
|
+
platform: 'android',
|
|
67
|
+
target: 'mobile',
|
|
68
|
+
serial: device,
|
|
69
|
+
session,
|
|
70
|
+
mode: 'system',
|
|
71
|
+
responseLevel: 'digest',
|
|
72
|
+
});
|
|
73
|
+
const snapshot = () => client.capture.snapshot({
|
|
74
|
+
platform: 'android',
|
|
75
|
+
target: 'mobile',
|
|
76
|
+
serial: device,
|
|
77
|
+
session,
|
|
78
|
+
app,
|
|
79
|
+
interactiveOnly: false,
|
|
80
|
+
forceFull: true,
|
|
81
|
+
});
|
|
82
|
+
const identifiers = async () => new Set(
|
|
83
|
+
(await snapshot()).nodes.map((node) => String(node.identifier ?? '')),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
let observedScreen;
|
|
88
|
+
if (destination.page === 'home') {
|
|
89
|
+
await navigateHome({ identifiers, press, wait, back });
|
|
90
|
+
observedScreen = 'wallet-home';
|
|
91
|
+
} else if (destination.page === 'swap') {
|
|
92
|
+
observedScreen = await navigateSwap({ identifiers, press, wait, back });
|
|
93
|
+
} else if (destination.page === 'perps-market-list') {
|
|
94
|
+
observedScreen = await navigatePerpsMarketList({ identifiers, press, wait, back, timeoutMs: Number(input.node?.timeout_ms ?? 30_000) });
|
|
95
|
+
} else {
|
|
96
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 30_000);
|
|
97
|
+
const initialIds = await identifiers();
|
|
98
|
+
observedScreen = destination.page === 'perps-market' && initialIds.has(IDS.marketList)
|
|
99
|
+
? await selectMarket({ destination, identifiers, press, snapshot, timeoutMs })
|
|
100
|
+
: await navigatePerps({ identifiers, press, wait, back, timeoutMs });
|
|
101
|
+
if (destination.page === 'perps-market') {
|
|
102
|
+
const market = String(destination.params.market.symbol).toUpperCase();
|
|
103
|
+
const ids = await identifiers();
|
|
104
|
+
if (ids.has(IDS.proMarket) && !await marketMatches(snapshot, market)) {
|
|
105
|
+
await press(IDS.proMarketList);
|
|
106
|
+
observedScreen = await selectMarket({ destination, identifiers, press, snapshot, timeoutMs });
|
|
107
|
+
} else if (!ids.has(IDS.proMarket) && (!ids.has(IDS.market) || !await marketMatches(snapshot, market))) {
|
|
108
|
+
if (ids.has(IDS.market)) await press(IDS.marketBack);
|
|
109
|
+
await press(`perps-market-row-item-${market}`);
|
|
110
|
+
observedScreen = IDS.market;
|
|
111
|
+
}
|
|
112
|
+
await waitForPerpsDestination(identifiers, timeoutMs);
|
|
113
|
+
if (!await marketMatches(snapshot, market)) {
|
|
114
|
+
throw new Error(`Opaque Mobile navigation opened a different Perps market than ${market}.`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
page: destination.page,
|
|
120
|
+
route: destination.route,
|
|
121
|
+
params: destination.params,
|
|
122
|
+
provider: 'android-native-accessibility',
|
|
123
|
+
proofPath: 'visible-native-navigation',
|
|
124
|
+
observedScreen,
|
|
125
|
+
};
|
|
126
|
+
} finally {
|
|
127
|
+
await closeNativeSession(client, selection);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function navigateSwap({ identifiers, press, wait, back }) {
|
|
132
|
+
let ids = await identifiers();
|
|
133
|
+
if (ids.has(IDS.swapSourceInput)) return IDS.swapSourceInput;
|
|
134
|
+
if (!ids.has(IDS.swapTradeEntry)) {
|
|
135
|
+
await navigateHome({ identifiers, press, wait, back });
|
|
136
|
+
await press(IDS.tradeTab);
|
|
137
|
+
await wait(IDS.swapTradeEntry);
|
|
138
|
+
ids = await identifiers();
|
|
139
|
+
}
|
|
140
|
+
if (!ids.has(IDS.swapTradeEntry)) {
|
|
141
|
+
throw new Error('Opaque Mobile navigation could not find the visible Swap entry.');
|
|
142
|
+
}
|
|
143
|
+
await press(IDS.swapTradeEntry);
|
|
144
|
+
await wait(IDS.swapSourceInput);
|
|
145
|
+
await wait(IDS.swapSourceToken);
|
|
146
|
+
await wait(IDS.swapDestinationToken);
|
|
147
|
+
return IDS.swapSourceInput;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function selectMarket({ destination, identifiers, press, snapshot, timeoutMs }) {
|
|
151
|
+
const market = String(destination.params.market.symbol).toUpperCase();
|
|
152
|
+
await press(`perps-market-row-item-${market}`);
|
|
153
|
+
const observed = await waitForPerpsDestination(identifiers, timeoutMs);
|
|
154
|
+
if (!await marketMatches(snapshot, market)) {
|
|
155
|
+
throw new Error(`Opaque Mobile navigation opened a different Perps market than ${market}.`);
|
|
156
|
+
}
|
|
157
|
+
return observed;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function navigateHome({ identifiers, press, wait, back }) {
|
|
161
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
162
|
+
const ids = await identifiers();
|
|
163
|
+
if (IDS.wallet.some((id) => ids.has(id))) return;
|
|
164
|
+
if (ids.has(IDS.walletTab)) {
|
|
165
|
+
await press(IDS.walletTab);
|
|
166
|
+
await wait(IDS.wallet[0]);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (ids.has(IDS.marketBack)) {
|
|
170
|
+
await press(IDS.marketBack);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (ids.has(IDS.proMarketBack)) {
|
|
174
|
+
await press(IDS.proMarketBack);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (ids.has(IDS.perpsHomeBack)) {
|
|
178
|
+
await press(IDS.perpsHomeBack);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (ids.has(IDS.perpsTradeEntry)) {
|
|
182
|
+
await back();
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
throw new Error('Opaque Mobile navigation could not find a visible route back to wallet home.');
|
|
186
|
+
}
|
|
187
|
+
throw new Error('Opaque Mobile navigation did not reach wallet home.');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function navigatePerps({ identifiers, press, wait, back, timeoutMs }) {
|
|
191
|
+
let ids = await identifiers();
|
|
192
|
+
if (ids.has(IDS.proMarket)) return IDS.proMarket;
|
|
193
|
+
if (ids.has(IDS.market)) {
|
|
194
|
+
await press(IDS.marketBack);
|
|
195
|
+
ids = await identifiers();
|
|
196
|
+
}
|
|
197
|
+
if (!ids.has(IDS.perpsHome)) {
|
|
198
|
+
if (ids.has(IDS.perpsTradeEntry)) {
|
|
199
|
+
await press(IDS.perpsTradeEntry);
|
|
200
|
+
} else if (ids.has(IDS.perpsEntry)) {
|
|
201
|
+
await press(IDS.perpsEntry);
|
|
202
|
+
} else {
|
|
203
|
+
if (!ids.has(IDS.tradeTab)) {
|
|
204
|
+
await navigateHome({ identifiers, press, wait, back });
|
|
205
|
+
ids = await identifiers();
|
|
206
|
+
}
|
|
207
|
+
if (ids.has(IDS.tradeTab)) {
|
|
208
|
+
await press(IDS.tradeTab);
|
|
209
|
+
ids = await identifiers();
|
|
210
|
+
}
|
|
211
|
+
if (ids.has(IDS.perpsTradeEntry)) {
|
|
212
|
+
await press(IDS.perpsTradeEntry);
|
|
213
|
+
} else if (ids.has(IDS.perpsEntry)) {
|
|
214
|
+
await press(IDS.perpsEntry);
|
|
215
|
+
} else {
|
|
216
|
+
throw new Error('Opaque Mobile navigation could not find the visible Perps entry.');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
ids = await identifiers();
|
|
220
|
+
}
|
|
221
|
+
if (ids.has(IDS.tutorialSkip)) await press(IDS.tutorialSkip);
|
|
222
|
+
return waitForPerpsDestination(identifiers, timeoutMs);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function navigatePerpsMarketList({ identifiers, press, wait, back, timeoutMs }) {
|
|
226
|
+
let ids = await identifiers();
|
|
227
|
+
if (ids.has(IDS.marketList)) return IDS.marketList;
|
|
228
|
+
const observed = await navigatePerps({ identifiers, press, wait, back, timeoutMs });
|
|
229
|
+
ids = await identifiers();
|
|
230
|
+
if (ids.has(IDS.marketList)) return IDS.marketList;
|
|
231
|
+
if (ids.has(IDS.proMarketList)) {
|
|
232
|
+
await press(IDS.proMarketList);
|
|
233
|
+
await wait(IDS.marketList);
|
|
234
|
+
return IDS.marketList;
|
|
235
|
+
}
|
|
236
|
+
throw new Error(`Opaque Mobile navigation reached ${observed}, but no visible market-list control was available.`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function waitForPerpsDestination(identifiers, timeoutMs) {
|
|
240
|
+
const deadline = Date.now() + timeoutMs;
|
|
241
|
+
while (Date.now() <= deadline) {
|
|
242
|
+
const ids = await identifiers();
|
|
243
|
+
for (const id of [IDS.perpsHome, IDS.market, IDS.proMarket]) {
|
|
244
|
+
if (ids.has(id)) return id;
|
|
245
|
+
}
|
|
246
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
247
|
+
}
|
|
248
|
+
throw new Error('Opaque Mobile navigation did not reach a visible Perps destination.');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function closeNativeSession(client, selection) {
|
|
252
|
+
try {
|
|
253
|
+
await client.sessions.close({ ...selection, responseLevel: 'digest' });
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (!/No active session|SESSION_NOT_FOUND/iu.test(String(error?.message ?? error))) throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function marketMatches(snapshot, market) {
|
|
260
|
+
const nodes = (await snapshot()).nodes;
|
|
261
|
+
return nodes.some((node) =>
|
|
262
|
+
(node.identifier === IDS.marketSubtitle || node.identifier === IDS.proMarketSubtitle) &&
|
|
263
|
+
String(node.label ?? node.value ?? '').toUpperCase().includes(`${market}-USD`)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { navigate, runAdapter } from '../platform/bridge.mjs';
|
|
2
|
+
import { navigateOpaqueMobile } from './native-navigation.mjs';
|
|
2
3
|
|
|
3
4
|
const PAGE_ROUTES = {
|
|
4
5
|
home: { route: 'WalletView', params: {} },
|
|
5
6
|
perps: { route: 'PerpsMarketListView', params: {} },
|
|
7
|
+
'perps-market-list': { route: 'PerpsMarketListView', params: {} },
|
|
8
|
+
swap: { route: 'BridgeView', params: {} },
|
|
6
9
|
};
|
|
7
10
|
|
|
8
11
|
function text(value) {
|
|
@@ -18,12 +21,18 @@ function pageRoute(node) {
|
|
|
18
21
|
if (!market) throw new Error('mobile ui.navigate page=perps-market requires market or symbol.');
|
|
19
22
|
return { page, route: 'PerpsMarketDetails', params: { market: { symbol: market } } };
|
|
20
23
|
}
|
|
21
|
-
throw new Error('mobile ui.navigate supported page intents: home, perps, perps-market.');
|
|
24
|
+
throw new Error('mobile ui.navigate supported page intents: home, perps, perps-market-list, perps-market, swap.');
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
runAdapter(async (input) => {
|
|
25
28
|
const alias = pageRoute(input.node);
|
|
26
29
|
if (alias) {
|
|
30
|
+
if (
|
|
31
|
+
input.context?.env?.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === '1' ||
|
|
32
|
+
process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === '1'
|
|
33
|
+
) {
|
|
34
|
+
return navigateOpaqueMobile(input, alias);
|
|
35
|
+
}
|
|
27
36
|
const navigation = await navigate(input, alias.route, alias.params);
|
|
28
37
|
return { action: input.action, ...alias, navigation, proofPath: 'agentic-navigation' };
|
|
29
38
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
selectBridgeStatusEntry,
|
|
7
7
|
} from '../platform/bridge.mjs';
|
|
8
8
|
import { walletFixturePath } from '../../harness-exports.mjs';
|
|
9
|
+
import { isOpaqueMobileRuntime, unlockWalletThroughNativeUi } from './native-ui.mjs';
|
|
9
10
|
|
|
10
11
|
async function fixturePassword(projectRoot) {
|
|
11
12
|
const candidates = [
|
|
@@ -156,6 +157,22 @@ async function waitForStableUnlocked(input, initialStatus, stableMs = 750) {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
export async function ensureUnlocked(input) {
|
|
160
|
+
if (isOpaqueMobileRuntime(input)) {
|
|
161
|
+
const timeoutMs = Number(input.node?.unlock_timeout_ms ?? 30_000);
|
|
162
|
+
const password = input.node?.password ?? await fixturePassword(input.context.projectRoot);
|
|
163
|
+
const proof = await unlockWalletThroughNativeUi(input, String(password), timeoutMs);
|
|
164
|
+
return {
|
|
165
|
+
action: input.action,
|
|
166
|
+
unlocked: true,
|
|
167
|
+
alreadyUnlocked: proof.alreadyReady,
|
|
168
|
+
account: proof.selectedAddress || proof.selectedAddressObserved
|
|
169
|
+
? { address: proof.selectedAddress ?? proof.selectedAddressObserved }
|
|
170
|
+
: null,
|
|
171
|
+
redacted: true,
|
|
172
|
+
provider: proof.provider,
|
|
173
|
+
proofPath: proof.proofPath,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
159
176
|
const targetTimeoutMs = Number(input.node?.target_timeout_ms ?? 20000);
|
|
160
177
|
const targetDeadline = Date.now() + targetTimeoutMs;
|
|
161
178
|
const targetStatus = await waitForTargetStatus(input, targetTimeoutMs);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '../platform/bridge.mjs';
|
|
11
11
|
import { setMobileConsent } from '../analytics/consent-settings.mjs';
|
|
12
12
|
import { ensureUnlocked } from './ensure_unlocked.mjs';
|
|
13
|
+
import { importWalletThroughNativeUi, isOpaqueMobileRuntime } from './native-ui.mjs';
|
|
13
14
|
|
|
14
15
|
const TEST_IDS = {
|
|
15
16
|
existingWallet: 'wallet-setup-screen-have-an-existing-wallet-button-id',
|
|
@@ -99,6 +100,15 @@ async function press(input, testId, timeoutMs) {
|
|
|
99
100
|
return waitForCommand(input, ['press-test-id', testId], timeoutMs, testId);
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
async function pressIfPresent(input, testId, timeoutMs = 2000) {
|
|
104
|
+
try {
|
|
105
|
+
await press(input, testId, Math.min(timeoutMs, 2000));
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
102
112
|
async function setSecretInput(input, testId, value, timeoutMs) {
|
|
103
113
|
const directory = await mkdtemp(path.join(os.tmpdir(), 'mm-harness-wallet-import-'));
|
|
104
114
|
const file = path.join(directory, 'value');
|
|
@@ -130,7 +140,7 @@ async function waitForInterestDecision(input, timeoutMs) {
|
|
|
130
140
|
throw new Error(`Mobile onboarding did not reach the interests screen or wallet home from ${route}.`);
|
|
131
141
|
}
|
|
132
142
|
|
|
133
|
-
async function importThroughUi(input, timeoutMs) {
|
|
143
|
+
async function importThroughUi(input, timeoutMs, options) {
|
|
134
144
|
const credentials = await resolveWalletImportCredentials(input);
|
|
135
145
|
const metametricsEnabled = input.node?.metametrics === true || input.node?.metametrics === 'true';
|
|
136
146
|
const interests = Array.isArray(input.node?.interests)
|
|
@@ -174,6 +184,12 @@ async function importThroughUi(input, timeoutMs) {
|
|
|
174
184
|
if (String(selectedEntry(afterInterests, input)?.route?.name ?? '') === 'OnboardingSuccess') {
|
|
175
185
|
await press(input, TEST_IDS.successDone, timeoutMs);
|
|
176
186
|
}
|
|
187
|
+
if (!options.notificationsEnabled) {
|
|
188
|
+
await pressIfPresent(input, 'push-notification-new-user-sheet-button-not-now');
|
|
189
|
+
}
|
|
190
|
+
if (options.skipWalletTour) {
|
|
191
|
+
await pressIfPresent(input, 'wallet-home-onboarding-steps-skip');
|
|
192
|
+
}
|
|
177
193
|
|
|
178
194
|
const deadline = Date.now() + timeoutMs;
|
|
179
195
|
let observed = await status(input);
|
|
@@ -205,6 +221,56 @@ runAdapter(async (input) => {
|
|
|
205
221
|
validateWalletImportOptions('mobile', input.node);
|
|
206
222
|
const method = walletImportMethod(input);
|
|
207
223
|
const timeoutMs = Number(input.node?.timeout_ms ?? 60000);
|
|
224
|
+
const biometricsEnabled = input.node?.biometrics === true || input.node?.biometrics === 'true';
|
|
225
|
+
const notificationsEnabled = input.node?.notifications === true || input.node?.notifications === 'true';
|
|
226
|
+
const skipWalletTour = input.node?.skip_wallet_tour !== false && input.node?.skip_wallet_tour !== 'false';
|
|
227
|
+
if (biometricsEnabled) {
|
|
228
|
+
throw new Error('Mobile biometrics=true requires manual device authentication and is not supported by an unattended recipe.');
|
|
229
|
+
}
|
|
230
|
+
if (isOpaqueMobileRuntime(input)) {
|
|
231
|
+
if (method === 'profile') {
|
|
232
|
+
throw new Error('metamask.wallet.import method=profile requires the Mobile development bridge; use method=auto or ui for a release artifact.');
|
|
233
|
+
}
|
|
234
|
+
const credentials = await resolveWalletImportCredentials(input);
|
|
235
|
+
const metametricsEnabled = input.node?.metametrics === true || input.node?.metametrics === 'true';
|
|
236
|
+
const interests = Array.isArray(input.node?.interests)
|
|
237
|
+
? input.node.interests.map((value) => String(value))
|
|
238
|
+
: [];
|
|
239
|
+
const invalidInterest = interests.find((value) => !INTEREST_IDS.has(value));
|
|
240
|
+
if (invalidInterest) throw new Error(`Unsupported Mobile onboarding interest: ${invalidInterest}.`);
|
|
241
|
+
const proof = await importWalletThroughNativeUi(input, credentials, {
|
|
242
|
+
timeoutMs,
|
|
243
|
+
metametricsEnabled,
|
|
244
|
+
biometricsEnabled,
|
|
245
|
+
notificationsEnabled,
|
|
246
|
+
skipWalletTour,
|
|
247
|
+
interests,
|
|
248
|
+
requireFresh: method === 'ui',
|
|
249
|
+
});
|
|
250
|
+
if (!proof.addressVisible) {
|
|
251
|
+
throw new Error('Visible Mobile onboarding reached wallet home, but the imported account address was not observable through native accessibility.');
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
action: input.action,
|
|
255
|
+
method: proof.alreadyReady ? 'native-profile' : 'native-ui-import',
|
|
256
|
+
credentialSource: credentials.source,
|
|
257
|
+
credentialSourceName: credentials.sourceName,
|
|
258
|
+
selectedAddress: proof.selectedAddress,
|
|
259
|
+
metametricsRequested: metametricsEnabled,
|
|
260
|
+
biometricsRequested: biometricsEnabled,
|
|
261
|
+
biometricPromptDismissed: proof.biometricPromptDismissed ?? false,
|
|
262
|
+
notificationsRequested: notificationsEnabled,
|
|
263
|
+
notificationPromptHandled: proof.notificationPromptHandled ?? false,
|
|
264
|
+
walletTourSkipped: proof.walletTourSkipped ?? false,
|
|
265
|
+
interests,
|
|
266
|
+
provider: proof.provider,
|
|
267
|
+
redacted: true,
|
|
268
|
+
proofPath: proof.proofPath,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (notificationsEnabled) {
|
|
272
|
+
throw new Error('Mobile notifications=true requires native system-permission handling and is supported only for an Android release artifact.');
|
|
273
|
+
}
|
|
208
274
|
const before = await status(input);
|
|
209
275
|
const entry = selectedEntry(before, input);
|
|
210
276
|
const address = selectedAddress(before, input);
|
|
@@ -218,7 +284,10 @@ runAdapter(async (input) => {
|
|
|
218
284
|
if (method === 'profile') {
|
|
219
285
|
throw new Error('metamask.wallet.import method=profile found a fresh Mobile onboarding state.');
|
|
220
286
|
}
|
|
221
|
-
const imported = await importThroughUi(input, timeoutMs
|
|
287
|
+
const imported = await importThroughUi(input, timeoutMs, {
|
|
288
|
+
notificationsEnabled,
|
|
289
|
+
skipWalletTour,
|
|
290
|
+
});
|
|
222
291
|
const importedEntry = selectedEntry(imported.observed, input);
|
|
223
292
|
return {
|
|
224
293
|
action: input.action,
|