@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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { runAdapter } from '../platform/bridge.mjs';
|
|
2
|
+
import {
|
|
3
|
+
closeNativeSession,
|
|
4
|
+
createNativeSession,
|
|
5
|
+
nativeLabel,
|
|
6
|
+
nativeNode,
|
|
7
|
+
} from '../platform/native-session.mjs';
|
|
8
|
+
|
|
9
|
+
function visibleLabels(snapshot) {
|
|
10
|
+
return snapshot.nodes
|
|
11
|
+
.filter((node) => node.visibleToUser !== false)
|
|
12
|
+
.map(nativeLabel)
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function quote(snapshot) {
|
|
17
|
+
const source = nativeNode(snapshot, 'source-token-area');
|
|
18
|
+
const destination = nativeNode(snapshot, 'dest-token-area');
|
|
19
|
+
const sourceAmount = nativeLabel(nativeNode(snapshot, 'source-token-area-input'));
|
|
20
|
+
const destinationAmount = nativeLabel(nativeNode(snapshot, 'dest-token-area-input'));
|
|
21
|
+
const button = nativeNode(snapshot, 'bridge-confirm-button');
|
|
22
|
+
const labels = visibleLabels(snapshot);
|
|
23
|
+
return {
|
|
24
|
+
source: nativeLabel(source),
|
|
25
|
+
destination: nativeLabel(destination),
|
|
26
|
+
sourceAmount,
|
|
27
|
+
destinationAmount,
|
|
28
|
+
buttonEnabled: button?.enabled === true,
|
|
29
|
+
warning: labels.find((label) => /insufficient|no quotes|unavailable|may fail|might fail/iu.test(label)) ?? null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function waitForQuote(session, expected, timeoutMs) {
|
|
34
|
+
const deadline = Date.now() + Math.min(timeoutMs, 60_000);
|
|
35
|
+
let stable = null;
|
|
36
|
+
let stableSince = 0;
|
|
37
|
+
while (Date.now() <= deadline) {
|
|
38
|
+
const snapshot = await session.snapshot();
|
|
39
|
+
const current = quote(snapshot);
|
|
40
|
+
const matches = current.source.toUpperCase().includes(expected.sourceSymbol) &&
|
|
41
|
+
current.destination.toUpperCase().includes(expected.destinationSymbol) &&
|
|
42
|
+
Number(current.sourceAmount) === expected.amount &&
|
|
43
|
+
Number(current.destinationAmount) > 0 &&
|
|
44
|
+
current.buttonEnabled &&
|
|
45
|
+
!current.warning;
|
|
46
|
+
const identity = JSON.stringify(current);
|
|
47
|
+
if (!matches) {
|
|
48
|
+
stable = null;
|
|
49
|
+
stableSince = 0;
|
|
50
|
+
} else if (stable !== identity) {
|
|
51
|
+
stable = identity;
|
|
52
|
+
stableSince = Date.now();
|
|
53
|
+
} else if (Date.now() - stableSince >= 1500) {
|
|
54
|
+
return current;
|
|
55
|
+
}
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
57
|
+
}
|
|
58
|
+
throw new Error('The Mobile Swap quote did not remain submittable for the requested pair and amount.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function pressIfVisible(session, testId, timeoutMs) {
|
|
62
|
+
if (!nativeNode(await session.snapshot(), testId)) return false;
|
|
63
|
+
await session.execute('ui.press', { test_id: testId, timeout_ms: timeoutMs, settle: false });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function waitForTerminalStatus(session, timeoutMs) {
|
|
68
|
+
const deadline = Date.now() + timeoutMs;
|
|
69
|
+
while (Date.now() <= deadline) {
|
|
70
|
+
const snapshot = await session.snapshot();
|
|
71
|
+
const labels = visibleLabels(snapshot);
|
|
72
|
+
const failed = labels.find((label) => /^(?:Swap|Bridge) failed$/iu.test(label));
|
|
73
|
+
if (failed) throw new Error(`Mobile ${failed}.`);
|
|
74
|
+
const success = labels.find((label) => /^(?:Swap|Bridge) complete$/iu.test(label));
|
|
75
|
+
if (success) return { snapshot, status: success };
|
|
76
|
+
if (nativeNode(snapshot, 'price-impact-footer-confirm')) {
|
|
77
|
+
await session.execute('ui.press', {
|
|
78
|
+
test_id: 'price-impact-footer-confirm',
|
|
79
|
+
timeout_ms: Math.max(1, deadline - Date.now()),
|
|
80
|
+
settle: false,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
84
|
+
}
|
|
85
|
+
throw new Error('Mobile Swap did not reach a terminal post-trade status.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
runAdapter(async (input) => {
|
|
89
|
+
const sourceSymbol = String(input.node?.source_symbol ?? '').trim().toUpperCase();
|
|
90
|
+
const destinationSymbol = String(input.node?.destination_symbol ?? '').trim().toUpperCase();
|
|
91
|
+
const amount = Number(input.node?.amount);
|
|
92
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 300_000);
|
|
93
|
+
if (!sourceSymbol || !destinationSymbol || !(amount > 0)) {
|
|
94
|
+
throw new Error('metamask.swap_bridge.submit_transaction requires source_symbol, destination_symbol, and positive amount.');
|
|
95
|
+
}
|
|
96
|
+
if (input.node?.confirm_live !== true) {
|
|
97
|
+
throw new Error('metamask.swap_bridge.submit_transaction requires confirm_live=true.');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const session = createNativeSession(input, 'Swap', `submit-${process.pid}`);
|
|
101
|
+
try {
|
|
102
|
+
await session.open();
|
|
103
|
+
const before = await waitForQuote(session, { sourceSymbol, destinationSymbol, amount }, timeoutMs);
|
|
104
|
+
await session.execute('ui.press', {
|
|
105
|
+
test_id: 'bridge-confirm-button',
|
|
106
|
+
timeout_ms: Math.min(timeoutMs, 60_000),
|
|
107
|
+
settle: false,
|
|
108
|
+
});
|
|
109
|
+
const terminal = await waitForTerminalStatus(session, timeoutMs);
|
|
110
|
+
await pressIfVisible(session, 'post-trade-bottom-sheet-view-activity-button', timeoutMs);
|
|
111
|
+
let activity = null;
|
|
112
|
+
const activityDeadline = Date.now() + Math.min(timeoutMs, 60_000);
|
|
113
|
+
while (Date.now() <= activityDeadline) {
|
|
114
|
+
const snapshot = await session.snapshot();
|
|
115
|
+
if (nativeNode(snapshot, 'activity-screen-safe-area-view')) {
|
|
116
|
+
const labels = visibleLabels(snapshot);
|
|
117
|
+
activity = labels.find((label) => {
|
|
118
|
+
const upper = label.toUpperCase();
|
|
119
|
+
return upper.includes(sourceSymbol) && upper.includes(destinationSymbol);
|
|
120
|
+
}) ?? labels.find((label) => /swap|bridge/iu.test(label)) ?? null;
|
|
121
|
+
if (activity) break;
|
|
122
|
+
}
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
124
|
+
}
|
|
125
|
+
if (!activity) throw new Error('Confirmed Mobile Swap did not appear in visible Activity.');
|
|
126
|
+
return {
|
|
127
|
+
action: input.action,
|
|
128
|
+
sourceSymbol,
|
|
129
|
+
destinationSymbol,
|
|
130
|
+
amount,
|
|
131
|
+
destinationAmount: before.destinationAmount,
|
|
132
|
+
submitted: true,
|
|
133
|
+
confirmed: true,
|
|
134
|
+
status: terminal.status,
|
|
135
|
+
activity,
|
|
136
|
+
proofPath: 'native-accessibility-swap-post-trade-activity',
|
|
137
|
+
};
|
|
138
|
+
} finally {
|
|
139
|
+
await closeNativeSession(session);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createAgentDeviceUiTransport } from '@farmslot/expo-recipe';
|
|
2
2
|
import { createAgentDeviceClient } from 'agent-device';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
nativeAgentDeviceStateDir,
|
|
5
|
+
nativeSessionName,
|
|
6
|
+
} from '../platform/native-session-name.mjs';
|
|
4
7
|
|
|
5
8
|
const IDS = {
|
|
6
9
|
wallet: ['wallet-safe-area', 'homepage-container'],
|
|
@@ -9,12 +12,18 @@ const IDS = {
|
|
|
9
12
|
perpsEntry: 'homepage-balance-breakdown-row-perps',
|
|
10
13
|
perpsTradeEntry: 'wallet-perps-action',
|
|
11
14
|
swapTradeEntry: 'wallet-actions-bottom-sheet-swap-button',
|
|
15
|
+
swapBack: 'button-icon',
|
|
12
16
|
swapSourceInput: 'source-token-area-input',
|
|
13
17
|
swapSourceToken: 'source-token-area',
|
|
14
18
|
swapDestinationToken: 'dest-token-area',
|
|
15
19
|
perpsHome: 'perps-home',
|
|
16
20
|
perpsHomeBack: 'perps-home-back-button',
|
|
21
|
+
perpsSearch: 'perps-home-search-toggle',
|
|
17
22
|
tutorialSkip: 'perps-tutorial-skip-button',
|
|
23
|
+
modeLite: 'perps-mode-selection-lite-option',
|
|
24
|
+
modePro: 'perps-mode-selection-pro-option',
|
|
25
|
+
modeToggleLite: 'perps-mode-toggle-lite',
|
|
26
|
+
modeTogglePro: 'perps-mode-toggle-pro',
|
|
18
27
|
market: 'perps-market-details-view',
|
|
19
28
|
marketBack: 'perps-market-header-back-button',
|
|
20
29
|
marketSubtitle: 'perps-market-header-subtitle',
|
|
@@ -23,6 +32,11 @@ const IDS = {
|
|
|
23
32
|
proMarketList: 'perps-pro-market-header-market-list-button',
|
|
24
33
|
proMarketSubtitle: 'perps-pro-market-header-subtitle',
|
|
25
34
|
marketList: 'perps-market-list',
|
|
35
|
+
marketListClose: 'perps-market-list-close-button-back-button',
|
|
36
|
+
tokenList: 'token-list',
|
|
37
|
+
tokenListBack: 'back-button',
|
|
38
|
+
tokenDetails: 'token-asset-overview',
|
|
39
|
+
tokenDetailsBack: 'back-arrow-button',
|
|
26
40
|
};
|
|
27
41
|
|
|
28
42
|
export async function navigateOpaqueMobile(input, destination) {
|
|
@@ -33,8 +47,10 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
33
47
|
}
|
|
34
48
|
const app = String(env.ANDROID_PACKAGE_ID ?? 'io.metamask');
|
|
35
49
|
const session = nativeSessionName(env, device, app);
|
|
50
|
+
const stateDir = nativeAgentDeviceStateDir(env, input.context.projectRoot, device);
|
|
36
51
|
const client = createAgentDeviceClient({
|
|
37
52
|
session,
|
|
53
|
+
stateDir,
|
|
38
54
|
lockPolicy: 'reject',
|
|
39
55
|
lockPlatform: 'android',
|
|
40
56
|
});
|
|
@@ -44,6 +60,7 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
44
60
|
device,
|
|
45
61
|
app,
|
|
46
62
|
session,
|
|
63
|
+
stateDir,
|
|
47
64
|
client,
|
|
48
65
|
});
|
|
49
66
|
const context = {
|
|
@@ -52,9 +69,11 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
52
69
|
artifactsDir: input.context.artifactsDir,
|
|
53
70
|
env,
|
|
54
71
|
};
|
|
72
|
+
const perpsMode = destination.params?.mode === 'pro' ? 'pro' : 'lite';
|
|
55
73
|
const press = (testId) => transport.execute('ui.press', {
|
|
56
74
|
test_id: testId,
|
|
57
75
|
timeout_ms: Number(input.node?.timeout_ms ?? 30_000),
|
|
76
|
+
settle: false,
|
|
58
77
|
}, context);
|
|
59
78
|
const wait = (testId) => transport.execute('ui.wait_for', {
|
|
60
79
|
test_id: testId,
|
|
@@ -80,7 +99,9 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
80
99
|
forceFull: true,
|
|
81
100
|
});
|
|
82
101
|
const identifiers = async () => new Set(
|
|
83
|
-
(await snapshot()).nodes
|
|
102
|
+
(await snapshot()).nodes
|
|
103
|
+
.filter((node) => node.visibleToUser !== false)
|
|
104
|
+
.map((node) => String(node.identifier ?? '')),
|
|
84
105
|
);
|
|
85
106
|
|
|
86
107
|
try {
|
|
@@ -90,26 +111,28 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
90
111
|
observedScreen = 'wallet-home';
|
|
91
112
|
} else if (destination.page === 'swap') {
|
|
92
113
|
observedScreen = await navigateSwap({ identifiers, press, wait, back });
|
|
114
|
+
} else if (destination.page === 'perps') {
|
|
115
|
+
observedScreen = await navigatePerpsHome({ identifiers, press, wait, back, timeoutMs: Number(input.node?.timeout_ms ?? 30_000), perpsMode });
|
|
93
116
|
} else if (destination.page === 'perps-market-list') {
|
|
94
|
-
observedScreen = await navigatePerpsMarketList({ identifiers, press, wait, back, timeoutMs: Number(input.node?.timeout_ms ?? 30_000) });
|
|
117
|
+
observedScreen = await navigatePerpsMarketList({ identifiers, press, wait, back, timeoutMs: Number(input.node?.timeout_ms ?? 30_000), perpsMode });
|
|
95
118
|
} else {
|
|
96
119
|
const timeoutMs = Number(input.node?.timeout_ms ?? 30_000);
|
|
97
120
|
const initialIds = await identifiers();
|
|
98
121
|
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 });
|
|
122
|
+
? await selectMarket({ destination, identifiers, press, snapshot, timeoutMs, perpsMode })
|
|
123
|
+
: await navigatePerps({ identifiers, press, wait, back, timeoutMs, perpsMode });
|
|
101
124
|
if (destination.page === 'perps-market') {
|
|
102
125
|
const market = String(destination.params.market.symbol).toUpperCase();
|
|
103
126
|
const ids = await identifiers();
|
|
104
127
|
if (ids.has(IDS.proMarket) && !await marketMatches(snapshot, market)) {
|
|
105
128
|
await press(IDS.proMarketList);
|
|
106
|
-
observedScreen = await selectMarket({ destination, identifiers, press, snapshot, timeoutMs });
|
|
129
|
+
observedScreen = await selectMarket({ destination, identifiers, press, snapshot, timeoutMs, perpsMode });
|
|
107
130
|
} else if (!ids.has(IDS.proMarket) && (!ids.has(IDS.market) || !await marketMatches(snapshot, market))) {
|
|
108
131
|
if (ids.has(IDS.market)) await press(IDS.marketBack);
|
|
109
132
|
await press(`perps-market-row-item-${market}`);
|
|
110
133
|
observedScreen = IDS.market;
|
|
111
134
|
}
|
|
112
|
-
await waitForPerpsDestination(identifiers, timeoutMs);
|
|
135
|
+
await waitForPerpsDestination(identifiers, timeoutMs, press, perpsMode);
|
|
113
136
|
if (!await marketMatches(snapshot, market)) {
|
|
114
137
|
throw new Error(`Opaque Mobile navigation opened a different Perps market than ${market}.`);
|
|
115
138
|
}
|
|
@@ -147,10 +170,10 @@ async function navigateSwap({ identifiers, press, wait, back }) {
|
|
|
147
170
|
return IDS.swapSourceInput;
|
|
148
171
|
}
|
|
149
172
|
|
|
150
|
-
async function selectMarket({ destination, identifiers, press, snapshot, timeoutMs }) {
|
|
173
|
+
async function selectMarket({ destination, identifiers, press, snapshot, timeoutMs, perpsMode }) {
|
|
151
174
|
const market = String(destination.params.market.symbol).toUpperCase();
|
|
152
175
|
await press(`perps-market-row-item-${market}`);
|
|
153
|
-
const observed = await waitForPerpsDestination(identifiers, timeoutMs);
|
|
176
|
+
const observed = await waitForPerpsDestination(identifiers, timeoutMs, press, perpsMode);
|
|
154
177
|
if (!await marketMatches(snapshot, market)) {
|
|
155
178
|
throw new Error(`Opaque Mobile navigation opened a different Perps market than ${market}.`);
|
|
156
179
|
}
|
|
@@ -162,32 +185,56 @@ async function navigateHome({ identifiers, press, wait, back }) {
|
|
|
162
185
|
const ids = await identifiers();
|
|
163
186
|
if (IDS.wallet.some((id) => ids.has(id))) return;
|
|
164
187
|
if (ids.has(IDS.walletTab)) {
|
|
165
|
-
await press
|
|
166
|
-
|
|
167
|
-
|
|
188
|
+
if (await pressCurrent(press, IDS.walletTab)) {
|
|
189
|
+
await wait(IDS.wallet[0]);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
168
193
|
}
|
|
169
194
|
if (ids.has(IDS.marketBack)) {
|
|
170
|
-
await press
|
|
195
|
+
await pressCurrent(press, IDS.marketBack);
|
|
171
196
|
continue;
|
|
172
197
|
}
|
|
173
198
|
if (ids.has(IDS.proMarketBack)) {
|
|
174
|
-
await press
|
|
199
|
+
await pressCurrent(press, IDS.proMarketBack);
|
|
175
200
|
continue;
|
|
176
201
|
}
|
|
177
202
|
if (ids.has(IDS.perpsHomeBack)) {
|
|
178
|
-
await press
|
|
203
|
+
await pressCurrent(press, IDS.perpsHomeBack);
|
|
179
204
|
continue;
|
|
180
205
|
}
|
|
181
|
-
if (ids.has(IDS.
|
|
182
|
-
await
|
|
206
|
+
if (ids.has(IDS.marketListClose)) {
|
|
207
|
+
await pressCurrent(press, IDS.marketListClose);
|
|
183
208
|
continue;
|
|
184
209
|
}
|
|
185
|
-
|
|
210
|
+
if (ids.has(IDS.tokenList) && ids.has(IDS.tokenListBack)) {
|
|
211
|
+
await pressCurrent(press, IDS.tokenListBack);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (ids.has(IDS.tokenDetails) && ids.has(IDS.tokenDetailsBack)) {
|
|
215
|
+
await pressCurrent(press, IDS.tokenDetailsBack);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (ids.has(IDS.swapSourceInput) && ids.has(IDS.swapBack)) {
|
|
219
|
+
await pressCurrent(press, IDS.swapBack);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
await back();
|
|
186
223
|
}
|
|
187
224
|
throw new Error('Opaque Mobile navigation did not reach wallet home.');
|
|
188
225
|
}
|
|
189
226
|
|
|
190
|
-
async function
|
|
227
|
+
async function pressCurrent(press, testId) {
|
|
228
|
+
try {
|
|
229
|
+
await press(testId);
|
|
230
|
+
return true;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (!/Selector did not match/iu.test(String(error?.message ?? error))) throw error;
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function navigatePerps({ identifiers, press, wait, back, timeoutMs, perpsMode }) {
|
|
191
238
|
let ids = await identifiers();
|
|
192
239
|
if (ids.has(IDS.proMarket)) return IDS.proMarket;
|
|
193
240
|
if (ids.has(IDS.market)) {
|
|
@@ -219,15 +266,61 @@ async function navigatePerps({ identifiers, press, wait, back, timeoutMs }) {
|
|
|
219
266
|
ids = await identifiers();
|
|
220
267
|
}
|
|
221
268
|
if (ids.has(IDS.tutorialSkip)) await press(IDS.tutorialSkip);
|
|
222
|
-
return waitForPerpsDestination(identifiers, timeoutMs);
|
|
269
|
+
return waitForPerpsDestination(identifiers, timeoutMs, press, perpsMode);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function navigatePerpsHome({ identifiers, press, wait, back, timeoutMs, perpsMode }) {
|
|
273
|
+
const deadline = Date.now() + timeoutMs;
|
|
274
|
+
let observed = await navigatePerps({ identifiers, press, wait, back, timeoutMs, perpsMode });
|
|
275
|
+
while (Date.now() <= deadline) {
|
|
276
|
+
let ids = await identifiers();
|
|
277
|
+
if (ids.has(IDS.perpsHome)) return IDS.perpsHome;
|
|
278
|
+
if (perpsMode === 'lite' && ids.has(IDS.proMarket)) {
|
|
279
|
+
if (!ids.has(IDS.modeTogglePro)) {
|
|
280
|
+
throw new Error('Opaque Mobile navigation reached Pro market details without the visible Pro mode control.');
|
|
281
|
+
}
|
|
282
|
+
await press(IDS.modeTogglePro);
|
|
283
|
+
observed = await waitForPerpsMode(identifiers, press, deadline, 'lite');
|
|
284
|
+
ids = await identifiers();
|
|
285
|
+
}
|
|
286
|
+
if (ids.has(IDS.market)) {
|
|
287
|
+
await press(IDS.marketBack);
|
|
288
|
+
} else if (ids.has(IDS.marketList)) {
|
|
289
|
+
await press(IDS.marketListClose);
|
|
290
|
+
} else {
|
|
291
|
+
observed = await navigatePerps({ identifiers, press, wait, back, timeoutMs: Math.max(1, deadline - Date.now()), perpsMode });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`Opaque Mobile navigation reached ${observed}, but did not converge to Perps Home.`);
|
|
223
295
|
}
|
|
224
296
|
|
|
225
|
-
async function
|
|
297
|
+
async function waitForPerpsMode(identifiers, press, deadline, perpsMode) {
|
|
298
|
+
let selected = false;
|
|
299
|
+
while (Date.now() <= deadline) {
|
|
300
|
+
const ids = await identifiers();
|
|
301
|
+
const expected = perpsMode === 'pro' ? IDS.proMarket : IDS.market;
|
|
302
|
+
if (ids.has(expected)) return expected;
|
|
303
|
+
const option = perpsMode === 'pro' ? IDS.modePro : IDS.modeLite;
|
|
304
|
+
if (!selected && ids.has(option)) {
|
|
305
|
+
await press(option);
|
|
306
|
+
selected = true;
|
|
307
|
+
}
|
|
308
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
309
|
+
}
|
|
310
|
+
throw new Error(`Opaque Mobile navigation did not visibly switch Perps to ${perpsMode} mode.`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function navigatePerpsMarketList({ identifiers, press, wait, back, timeoutMs, perpsMode }) {
|
|
226
314
|
let ids = await identifiers();
|
|
227
315
|
if (ids.has(IDS.marketList)) return IDS.marketList;
|
|
228
|
-
const observed = await navigatePerps({ identifiers, press, wait, back, timeoutMs });
|
|
316
|
+
const observed = await navigatePerps({ identifiers, press, wait, back, timeoutMs, perpsMode });
|
|
229
317
|
ids = await identifiers();
|
|
230
318
|
if (ids.has(IDS.marketList)) return IDS.marketList;
|
|
319
|
+
if (ids.has(IDS.perpsSearch)) {
|
|
320
|
+
await press(IDS.perpsSearch);
|
|
321
|
+
await wait(IDS.marketList);
|
|
322
|
+
return IDS.marketList;
|
|
323
|
+
}
|
|
231
324
|
if (ids.has(IDS.proMarketList)) {
|
|
232
325
|
await press(IDS.proMarketList);
|
|
233
326
|
await wait(IDS.marketList);
|
|
@@ -236,10 +329,14 @@ async function navigatePerpsMarketList({ identifiers, press, wait, back, timeout
|
|
|
236
329
|
throw new Error(`Opaque Mobile navigation reached ${observed}, but no visible market-list control was available.`);
|
|
237
330
|
}
|
|
238
331
|
|
|
239
|
-
async function waitForPerpsDestination(identifiers, timeoutMs) {
|
|
332
|
+
async function waitForPerpsDestination(identifiers, timeoutMs, press, perpsMode = 'lite') {
|
|
240
333
|
const deadline = Date.now() + timeoutMs;
|
|
241
334
|
while (Date.now() <= deadline) {
|
|
242
335
|
const ids = await identifiers();
|
|
336
|
+
if (ids.has(IDS.modeLite) || ids.has(IDS.modePro)) {
|
|
337
|
+
await press(perpsMode === 'pro' ? IDS.modePro : IDS.modeLite);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
243
340
|
for (const id of [IDS.perpsHome, IDS.market, IDS.proMarket]) {
|
|
244
341
|
if (ids.has(id)) return id;
|
|
245
342
|
}
|
|
@@ -15,11 +15,22 @@ function text(value) {
|
|
|
15
15
|
function pageRoute(node) {
|
|
16
16
|
const page = text(node?.page);
|
|
17
17
|
if (!page) return undefined;
|
|
18
|
-
|
|
18
|
+
const mode = text(node?.mode)?.toLowerCase() ?? 'lite';
|
|
19
|
+
if (!['lite', 'pro'].includes(mode)) {
|
|
20
|
+
throw new Error('mobile ui.navigate mode must be lite or pro.');
|
|
21
|
+
}
|
|
22
|
+
if (PAGE_ROUTES[page]) {
|
|
23
|
+
const route = PAGE_ROUTES[page];
|
|
24
|
+
return {
|
|
25
|
+
page,
|
|
26
|
+
...route,
|
|
27
|
+
params: page.startsWith('perps') ? { ...route.params, mode } : route.params,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
19
30
|
if (page === 'perps-market') {
|
|
20
31
|
const market = text(node.market) ?? text(node.symbol) ?? text(node.params?.market?.symbol);
|
|
21
32
|
if (!market) throw new Error('mobile ui.navigate page=perps-market requires market or symbol.');
|
|
22
|
-
return { page, route: 'PerpsMarketDetails', params: { market: { symbol: market } } };
|
|
33
|
+
return { page, route: 'PerpsMarketDetails', params: { market: { symbol: market }, mode } };
|
|
23
34
|
}
|
|
24
35
|
throw new Error('mobile ui.navigate supported page intents: home, perps, perps-market-list, perps-market, swap.');
|
|
25
36
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -45,6 +46,18 @@ function selectedAddress(status, input) {
|
|
|
45
46
|
return String(selectedEntry(status, input)?.account?.address ?? '').toLowerCase();
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
export function redactedSelectedIdentity(expectedAddress, actualAddress) {
|
|
50
|
+
const expected = String(expectedAddress ?? '').toLowerCase();
|
|
51
|
+
const actual = String(actualAddress ?? '').toLowerCase();
|
|
52
|
+
if (!expected || actual !== expected) {
|
|
53
|
+
throw new Error('Mobile wallet import did not visibly select the expected account.');
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
selectedIdentitySha256: createHash('sha256').update(expected).digest('hex'),
|
|
57
|
+
selectedAddressMatched: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
function walletReady(observed, input) {
|
|
49
62
|
const route = String(selectedEntry(observed, input)?.route?.name ?? '');
|
|
50
63
|
return Boolean(
|
|
@@ -199,9 +212,7 @@ async function importThroughUi(input, timeoutMs, options) {
|
|
|
199
212
|
observed = await status(input);
|
|
200
213
|
}
|
|
201
214
|
const actualAddress = selectedAddress(observed, input);
|
|
202
|
-
|
|
203
|
-
throw new Error(`Wallet import selected ${actualAddress || 'no address'}, expected ${credentials.expectedAddress}.`);
|
|
204
|
-
}
|
|
215
|
+
redactedSelectedIdentity(credentials.expectedAddress, actualAddress);
|
|
205
216
|
const { consent } = await setMobileConsent(
|
|
206
217
|
input,
|
|
207
218
|
metametricsEnabled,
|
|
@@ -255,7 +266,7 @@ runAdapter(async (input) => {
|
|
|
255
266
|
method: proof.alreadyReady ? 'native-profile' : 'native-ui-import',
|
|
256
267
|
credentialSource: credentials.source,
|
|
257
268
|
credentialSourceName: credentials.sourceName,
|
|
258
|
-
|
|
269
|
+
...redactedSelectedIdentity(credentials.expectedAddress, proof.selectedAddress),
|
|
259
270
|
metametricsRequested: metametricsEnabled,
|
|
260
271
|
biometricsRequested: biometricsEnabled,
|
|
261
272
|
biometricPromptDismissed: proof.biometricPromptDismissed ?? false,
|
|
@@ -294,7 +305,7 @@ runAdapter(async (input) => {
|
|
|
294
305
|
method: 'ui-import',
|
|
295
306
|
credentialSource: imported.credentials.source,
|
|
296
307
|
credentialSourceName: imported.credentials.sourceName,
|
|
297
|
-
|
|
308
|
+
...redactedSelectedIdentity(imported.credentials.expectedAddress, importedEntry.account.address),
|
|
298
309
|
route: importedEntry.route ?? null,
|
|
299
310
|
metametricsEnabled: imported.metametricsEnabled,
|
|
300
311
|
interests: imported.interests,
|
|
@@ -311,16 +322,12 @@ runAdapter(async (input) => {
|
|
|
311
322
|
...(route === 'Login' ? { password: credentials.password } : {}),
|
|
312
323
|
},
|
|
313
324
|
});
|
|
314
|
-
const actualAddress = String(unlocked.account?.address ?? address).toLowerCase();
|
|
315
|
-
if (actualAddress !== credentials.expectedAddress) {
|
|
316
|
-
throw new Error(`Wallet profile selected ${actualAddress || 'no address'}, expected ${credentials.expectedAddress}.`);
|
|
317
|
-
}
|
|
318
325
|
return {
|
|
319
326
|
action: input.action,
|
|
320
327
|
method: 'profile',
|
|
321
328
|
credentialSource: credentials.source,
|
|
322
329
|
credentialSourceName: credentials.sourceName,
|
|
323
|
-
|
|
330
|
+
...redactedSelectedIdentity(credentials.expectedAddress, unlocked.account?.address ?? address),
|
|
324
331
|
route: unlocked.route ?? entry.route ?? null,
|
|
325
332
|
redacted: true,
|
|
326
333
|
proofPath: 'mobile-fixture-profile',
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { runAdapter } from '../platform/bridge.mjs';
|
|
2
|
+
import { isOpaqueMobileRuntime, lockWalletThroughNativeUi } from './native-ui.mjs';
|
|
3
|
+
|
|
4
|
+
runAdapter(async (input) => {
|
|
5
|
+
if (!isOpaqueMobileRuntime(input)) {
|
|
6
|
+
throw new Error('metamask.wallet.lock requires an opaque Mobile runtime.');
|
|
7
|
+
}
|
|
8
|
+
return {
|
|
9
|
+
action: input.action,
|
|
10
|
+
...await lockWalletThroughNativeUi(input),
|
|
11
|
+
locked: true,
|
|
12
|
+
proofPath: 'mobile-visible-wallet-lock-native',
|
|
13
|
+
};
|
|
14
|
+
});
|