@deeeed/metamask-harness 0.35.0 → 0.36.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +9 -1
  3. package/adapters/extension/check-infura-readiness.cjs +102 -0
  4. package/adapters/extension/inject.mjs +1 -0
  5. package/adapters/extension/live.sh +25 -10
  6. package/adapters/extension/start-watch.sh +15 -0
  7. package/adapters/extension/wallet-fixture-state.cjs +3 -1
  8. package/adapters/manifest.json +16 -0
  9. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -42
  10. package/adapters/mobile/reset-app-data.sh +154 -0
  11. package/dist/adapters/extension/product-config.js +29 -1
  12. package/dist/adapters.js +18 -0
  13. package/dist/cli-commands.js +1 -1
  14. package/dist/cli.js +2 -2
  15. package/dist/command-contract.js +1 -1
  16. package/dist/commands/device-target.js +5 -0
  17. package/dist/commands/fixtures.js +106 -31
  18. package/dist/commands/launch/extension.js +39 -3
  19. package/dist/commands/launch/index.js +13 -0
  20. package/dist/mm-harness-cli.js +6 -3
  21. package/dist/recipe-security.js +3 -0
  22. package/docs/RECIPES.md +29 -0
  23. package/library/actions/extension/wallet/import.mjs +234 -0
  24. package/library/actions/extension/wallet/reset.mjs +98 -0
  25. package/library/actions/extension/wallet/state.mjs +1 -0
  26. package/library/actions/mobile/analytics/consent-settings.mjs +112 -0
  27. package/library/actions/mobile/analytics/set_consent.mjs +4 -112
  28. package/library/actions/mobile/platform/bridge.mjs +8 -0
  29. package/library/actions/mobile/wallet/import.mjs +259 -0
  30. package/library/actions/mobile/wallet/reset-helper.mjs +99 -0
  31. package/library/actions/mobile/wallet/reset.mjs +7 -0
  32. package/library/actions/shared/wallet/import-source.mjs +101 -0
  33. package/library/manifests/extension.action-manifest.json +112 -0
  34. package/library/manifests/mobile.action-manifest.json +108 -0
  35. package/library/recipes/wallet/import.recipe.json +83 -0
  36. package/library/recipes/wallet/reset-import.recipe.json +88 -0
  37. package/package.json +1 -1
  38. package/scripts/completions.sh +2 -2
@@ -0,0 +1,259 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { resolveWalletImportCredentials, validateWalletImportOptions, walletImportMethod } from '../../shared/wallet/import-source.mjs';
6
+ import {
7
+ bridgeCommand,
8
+ runAdapter,
9
+ selectBridgeStatusEntry,
10
+ } from '../platform/bridge.mjs';
11
+ import { setMobileConsent } from '../analytics/consent-settings.mjs';
12
+ import { ensureUnlocked } from './ensure_unlocked.mjs';
13
+
14
+ const TEST_IDS = {
15
+ existingWallet: 'wallet-setup-screen-have-an-existing-wallet-button-id',
16
+ importSeed: 'onboarding-sheet-import-seed-button-id',
17
+ srp: 'phrase-input-id',
18
+ continue: 'import-from-seed-screen-continue-button-id',
19
+ password: 'create-password-first-input-field',
20
+ confirmPassword: 'create-password-second-input-field',
21
+ passwordTerms: 'password-understand-box',
22
+ createPassword: 'submit-button',
23
+ metricsCheckbox: 'optin-metrics-metrics-checkbox',
24
+ metricsContinue: 'optin-metrics-continue-button-id',
25
+ interestSkip: 'onboarding-interest-skip-button',
26
+ interestContinue: 'onboarding-interest-continue-button',
27
+ successDone: 'onboarding-success-done-button',
28
+ };
29
+
30
+ const INTEREST_IDS = new Set([
31
+ 'swap_tokens',
32
+ 'trade_perpetuals',
33
+ 'prediction_markets',
34
+ 'send_receive_crypto',
35
+ 'earn_and_spend',
36
+ 'use_other_crypto_apps',
37
+ ]);
38
+
39
+ function selectedEntry(status, input) {
40
+ return selectBridgeStatusEntry(status, input) ?? {};
41
+ }
42
+
43
+ function selectedAddress(status, input) {
44
+ return String(selectedEntry(status, input)?.account?.address ?? '').toLowerCase();
45
+ }
46
+
47
+ function walletReady(observed, input) {
48
+ const route = String(selectedEntry(observed, input)?.route?.name ?? '');
49
+ return Boolean(
50
+ selectedAddress(observed, input) &&
51
+ route &&
52
+ route !== 'Login' &&
53
+ route !== 'Onboarding' &&
54
+ route !== 'OptinMetrics' &&
55
+ route !== 'OnboardingInterestQuestionnaire'
56
+ && route !== 'OnboardingSuccess'
57
+ );
58
+ }
59
+
60
+ async function waitForPostPasswordScreen(input, timeoutMs) {
61
+ const deadline = Date.now() + timeoutMs;
62
+ let observed;
63
+ while (Date.now() <= deadline) {
64
+ observed = await status(input);
65
+ const route = String(selectedEntry(observed, input)?.route?.name ?? '');
66
+ if (
67
+ route === 'OptinMetrics' ||
68
+ route === 'OnboardingInterestQuestionnaire' ||
69
+ route === 'OnboardingSuccess' ||
70
+ walletReady(observed, input)
71
+ ) return { route, observed };
72
+ await new Promise((resolve) => setTimeout(resolve, 250));
73
+ }
74
+ const route = String(selectedEntry(observed, input)?.route?.name ?? 'unknown');
75
+ throw new Error(`Mobile onboarding did not advance after password creation from ${route}.`);
76
+ }
77
+
78
+ async function status(input) {
79
+ return bridgeCommand(input, ['status-selected']);
80
+ }
81
+
82
+ async function waitForCommand(input, args, timeoutMs, label) {
83
+ const deadline = Date.now() + timeoutMs;
84
+ let lastError;
85
+ while (Date.now() <= deadline) {
86
+ try {
87
+ const result = await bridgeCommand(input, args);
88
+ if (result?.ok !== false) return result;
89
+ lastError = result?.error;
90
+ } catch (error) {
91
+ lastError = error;
92
+ }
93
+ await new Promise((resolve) => setTimeout(resolve, 250));
94
+ }
95
+ throw new Error(`${label} was not available before the wallet import timeout: ${String(lastError?.message ?? lastError ?? 'unknown')}`);
96
+ }
97
+
98
+ async function press(input, testId, timeoutMs) {
99
+ return waitForCommand(input, ['press-test-id', testId], timeoutMs, testId);
100
+ }
101
+
102
+ async function setSecretInput(input, testId, value, timeoutMs) {
103
+ const directory = await mkdtemp(path.join(os.tmpdir(), 'mm-harness-wallet-import-'));
104
+ const file = path.join(directory, 'value');
105
+ try {
106
+ await writeFile(file, value, { mode: 0o600 });
107
+ return await waitForCommand(
108
+ input,
109
+ ['set-input-file', testId, file],
110
+ timeoutMs,
111
+ testId,
112
+ );
113
+ } finally {
114
+ await rm(directory, { recursive: true, force: true });
115
+ }
116
+ }
117
+
118
+ async function waitForInterestDecision(input, timeoutMs) {
119
+ const deadline = Date.now() + Math.min(timeoutMs, 10000);
120
+ let observed;
121
+ while (Date.now() <= deadline) {
122
+ observed = await status(input);
123
+ const route = String(selectedEntry(observed, input)?.route?.name ?? '');
124
+ if (route === 'OnboardingInterestQuestionnaire') return { available: true, observed };
125
+ if (route === 'OnboardingSuccess') return { available: false, observed };
126
+ if (walletReady(observed, input)) return { available: false, observed };
127
+ await new Promise((resolve) => setTimeout(resolve, 250));
128
+ }
129
+ const route = String(selectedEntry(observed, input)?.route?.name ?? 'unknown');
130
+ throw new Error(`Mobile onboarding did not reach the interests screen or wallet home from ${route}.`);
131
+ }
132
+
133
+ async function importThroughUi(input, timeoutMs) {
134
+ const credentials = await resolveWalletImportCredentials(input);
135
+ const metametricsEnabled = input.node?.metametrics === true || input.node?.metametrics === 'true';
136
+ const interests = Array.isArray(input.node?.interests)
137
+ ? input.node.interests.map((value) => String(value))
138
+ : [];
139
+ const invalidInterest = interests.find((value) => !INTEREST_IDS.has(value));
140
+ if (invalidInterest) throw new Error(`Unsupported Mobile onboarding interest: ${invalidInterest}.`);
141
+ await press(input, TEST_IDS.existingWallet, timeoutMs);
142
+ try {
143
+ await setSecretInput(input, TEST_IDS.srp, credentials.mnemonic, 3000);
144
+ } catch {
145
+ await press(input, TEST_IDS.importSeed, timeoutMs);
146
+ await setSecretInput(input, TEST_IDS.srp, credentials.mnemonic, timeoutMs);
147
+ }
148
+ await press(input, TEST_IDS.continue, timeoutMs);
149
+ await setSecretInput(input, TEST_IDS.password, credentials.password, timeoutMs);
150
+ await setSecretInput(input, TEST_IDS.confirmPassword, credentials.password, timeoutMs);
151
+ await press(input, TEST_IDS.passwordTerms, timeoutMs);
152
+ await press(input, TEST_IDS.createPassword, timeoutMs);
153
+
154
+ const postPassword = await waitForPostPasswordScreen(input, timeoutMs);
155
+ if (postPassword.route === 'OptinMetrics') {
156
+ if (!metametricsEnabled) await press(input, TEST_IDS.metricsCheckbox, timeoutMs);
157
+ await press(input, TEST_IDS.metricsContinue, timeoutMs);
158
+ }
159
+
160
+ const interestDecision = await waitForInterestDecision(input, timeoutMs);
161
+ if (!interestDecision.available && interests.length > 0) {
162
+ throw new Error('Mobile onboarding interests were requested, but the questionnaire is not enabled for this build.');
163
+ }
164
+ if (interestDecision.available && interests.length > 0) {
165
+ for (const interest of interests) {
166
+ await press(input, `onboarding-interest-option-${interest}`, timeoutMs);
167
+ }
168
+ await press(input, TEST_IDS.interestContinue, timeoutMs);
169
+ } else if (interestDecision.available) {
170
+ await press(input, TEST_IDS.interestSkip, timeoutMs);
171
+ }
172
+
173
+ const afterInterests = await status(input);
174
+ if (String(selectedEntry(afterInterests, input)?.route?.name ?? '') === 'OnboardingSuccess') {
175
+ await press(input, TEST_IDS.successDone, timeoutMs);
176
+ }
177
+
178
+ const deadline = Date.now() + timeoutMs;
179
+ let observed = await status(input);
180
+ while (Date.now() <= deadline) {
181
+ if (walletReady(observed, input)) break;
182
+ await new Promise((resolve) => setTimeout(resolve, 250));
183
+ observed = await status(input);
184
+ }
185
+ const actualAddress = selectedAddress(observed, input);
186
+ if (actualAddress !== credentials.expectedAddress) {
187
+ throw new Error(`Wallet import selected ${actualAddress || 'no address'}, expected ${credentials.expectedAddress}.`);
188
+ }
189
+ const { consent } = await setMobileConsent(
190
+ input,
191
+ metametricsEnabled,
192
+ false,
193
+ Math.min(timeoutMs, 30000),
194
+ );
195
+ const actualMetametricsEnabled = consent.optedIn === true;
196
+ await bridgeCommand(input, ['navigate', 'WalletView']);
197
+ observed = await status(input);
198
+ if (!walletReady(observed, input)) {
199
+ throw new Error('Mobile wallet import could not return to wallet home after applying MetaMetrics consent.');
200
+ }
201
+ return { credentials, observed, metametricsEnabled: actualMetametricsEnabled, interests };
202
+ }
203
+
204
+ runAdapter(async (input) => {
205
+ validateWalletImportOptions('mobile', input.node);
206
+ const method = walletImportMethod(input);
207
+ const timeoutMs = Number(input.node?.timeout_ms ?? 60000);
208
+ const before = await status(input);
209
+ const entry = selectedEntry(before, input);
210
+ const address = selectedAddress(before, input);
211
+ const route = String(entry?.route?.name ?? '');
212
+ const onboarding = route === 'Onboarding' || (!address && route !== 'Login');
213
+
214
+ if (method === 'ui' && !onboarding) {
215
+ throw new Error('metamask.wallet.import method=ui requires a fresh Mobile onboarding state.');
216
+ }
217
+ if (onboarding) {
218
+ if (method === 'profile') {
219
+ throw new Error('metamask.wallet.import method=profile found a fresh Mobile onboarding state.');
220
+ }
221
+ const imported = await importThroughUi(input, timeoutMs);
222
+ const importedEntry = selectedEntry(imported.observed, input);
223
+ return {
224
+ action: input.action,
225
+ method: 'ui-import',
226
+ credentialSource: imported.credentials.source,
227
+ credentialSourceName: imported.credentials.sourceName,
228
+ selectedAddress: importedEntry.account.address,
229
+ route: importedEntry.route ?? null,
230
+ metametricsEnabled: imported.metametricsEnabled,
231
+ interests: imported.interests,
232
+ redacted: true,
233
+ proofPath: 'mobile-visible-onboarding-import',
234
+ };
235
+ }
236
+
237
+ const credentials = await resolveWalletImportCredentials(input);
238
+ const unlocked = await ensureUnlocked({
239
+ ...input,
240
+ node: {
241
+ ...input.node,
242
+ ...(route === 'Login' ? { password: credentials.password } : {}),
243
+ },
244
+ });
245
+ const actualAddress = String(unlocked.account?.address ?? address).toLowerCase();
246
+ if (actualAddress !== credentials.expectedAddress) {
247
+ throw new Error(`Wallet profile selected ${actualAddress || 'no address'}, expected ${credentials.expectedAddress}.`);
248
+ }
249
+ return {
250
+ action: input.action,
251
+ method: 'profile',
252
+ credentialSource: credentials.source,
253
+ credentialSourceName: credentials.sourceName,
254
+ selectedAddress: unlocked.account?.address ?? address,
255
+ route: unlocked.route ?? entry.route ?? null,
256
+ redacted: true,
257
+ proofPath: 'mobile-fixture-profile',
258
+ };
259
+ });
@@ -0,0 +1,99 @@
1
+ import {
2
+ bridgeCommand,
3
+ selectBridgeStatusEntry,
4
+ } from '../platform/bridge.mjs';
5
+
6
+ const TEST_IDS = {
7
+ existingWallet: 'wallet-setup-screen-have-an-existing-wallet-button-id',
8
+ forgotPassword: 'reset-wallet-button',
9
+ resetWallet: 'forgot-password-modal-reset-wallet-button',
10
+ confirmReset: 'forgot-password-modal-yes-reset-wallet-button',
11
+ };
12
+
13
+ function selectedEntry(status, input) {
14
+ return selectBridgeStatusEntry(status, input) ?? {};
15
+ }
16
+
17
+ function walletState(status, input) {
18
+ const entry = selectedEntry(status, input);
19
+ return {
20
+ address: String(entry?.account?.address ?? '').toLowerCase(),
21
+ route: String(entry?.route?.name ?? ''),
22
+ };
23
+ }
24
+
25
+ async function isFreshWallet(status, input) {
26
+ const state = walletState(status, input);
27
+ if (state.route !== 'Onboarding') return false;
28
+ return bridgeCommand(input, ['eval', `(() => {
29
+ const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
30
+ const rootsFor = hook?.getFiberRoots;
31
+ if (!hook?.renderers || typeof rootsFor !== 'function') return false;
32
+ const walk = (fiber) => {
33
+ if (!fiber) return false;
34
+ if (fiber.memoizedProps?.testID === ${JSON.stringify(TEST_IDS.existingWallet)}) return true;
35
+ return walk(fiber.child) || walk(fiber.sibling);
36
+ };
37
+ for (const [id] of hook.renderers) {
38
+ for (const root of rootsFor(id) ?? []) {
39
+ if (walk(root.current)) return true;
40
+ }
41
+ }
42
+ return false;
43
+ })()`]);
44
+ }
45
+
46
+ async function waitForCommand(input, args, timeoutMs, label) {
47
+ const deadline = Date.now() + timeoutMs;
48
+ let lastError;
49
+ while (Date.now() <= deadline) {
50
+ try {
51
+ const result = await bridgeCommand(input, args);
52
+ if (result?.ok !== false) return result;
53
+ lastError = result?.error;
54
+ } catch (error) {
55
+ lastError = error;
56
+ }
57
+ await new Promise((resolve) => setTimeout(resolve, 250));
58
+ }
59
+ throw new Error(`${label} was not available before the wallet reset timeout: ${String(lastError?.message ?? lastError ?? 'unknown')}`);
60
+ }
61
+
62
+ async function waitForFreshWallet(input, timeoutMs) {
63
+ const deadline = Date.now() + timeoutMs;
64
+ let observed;
65
+ while (Date.now() <= deadline) {
66
+ observed = await bridgeCommand(input, ['status-selected']);
67
+ if (await isFreshWallet(observed, input)) return observed;
68
+ await new Promise((resolve) => setTimeout(resolve, 250));
69
+ }
70
+ const state = walletState(observed, input);
71
+ throw new Error(`Mobile wallet reset did not reach fresh onboarding; route=${state.route || 'unknown'} account=${state.address || 'none'}.`);
72
+ }
73
+
74
+ export async function resetMobileWalletThroughUi(input) {
75
+ const timeoutMs = Number(input.node?.timeout_ms ?? 60000);
76
+ const before = await bridgeCommand(input, ['status-selected']);
77
+ if (await isFreshWallet(before, input)) {
78
+ return {
79
+ alreadyFresh: true,
80
+ route: walletState(before, input).route,
81
+ redacted: true,
82
+ proofPath: 'mobile-visible-wallet-reset',
83
+ };
84
+ }
85
+
86
+ if (walletState(before, input).route !== 'Login') {
87
+ await bridgeCommand(input, ['navigate', 'Login']);
88
+ }
89
+ await waitForCommand(input, ['press-test-id', TEST_IDS.forgotPassword], timeoutMs, TEST_IDS.forgotPassword);
90
+ await waitForCommand(input, ['press-test-id', TEST_IDS.resetWallet], timeoutMs, TEST_IDS.resetWallet);
91
+ await waitForCommand(input, ['press-test-id', TEST_IDS.confirmReset], timeoutMs, TEST_IDS.confirmReset);
92
+ const after = await waitForFreshWallet(input, timeoutMs);
93
+ return {
94
+ alreadyFresh: false,
95
+ route: walletState(after, input).route,
96
+ redacted: true,
97
+ proofPath: 'mobile-visible-wallet-reset',
98
+ };
99
+ }
@@ -0,0 +1,7 @@
1
+ import { runAdapter } from '../platform/bridge.mjs';
2
+ import { resetMobileWalletThroughUi } from './reset-helper.mjs';
3
+
4
+ runAdapter(async (input) => ({
5
+ action: input.action,
6
+ ...(await resetMobileWalletThroughUi(input)),
7
+ }));
@@ -0,0 +1,101 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { mnemonicToAccount } from 'viem/accounts';
5
+
6
+ import { walletFixturePath } from '../../harness-exports.mjs';
7
+
8
+ const ENV_NAME = /^[A-Z_][A-Z0-9_]*$/u;
9
+
10
+ function environmentValue(input, name) {
11
+ if (!ENV_NAME.test(name)) {
12
+ throw new Error(`Wallet import environment name is invalid: ${name}`);
13
+ }
14
+ return input.context?.env?.[name] ?? process.env[name];
15
+ }
16
+
17
+ async function optionalFixture(projectRoot) {
18
+ const absolutePath = walletFixturePath(projectRoot);
19
+ try {
20
+ return {
21
+ absolutePath,
22
+ relativePath: path.relative(projectRoot, absolutePath),
23
+ value: JSON.parse(await readFile(absolutePath, 'utf8')),
24
+ };
25
+ } catch (error) {
26
+ if (error?.code === 'ENOENT') return null;
27
+ throw error;
28
+ }
29
+ }
30
+
31
+ export async function resolveWalletImportCredentials(input) {
32
+ const source = String(input.node?.credential_source ?? 'auto');
33
+ if (!['auto', 'environment', 'fixture'].includes(source)) {
34
+ throw new Error('metamask.wallet.import credential_source must be auto, environment, or fixture.');
35
+ }
36
+
37
+ const srpEnv = String(input.node?.srp_env ?? 'MM_HARNESS_WALLET_SRP');
38
+ const passwordEnv = String(input.node?.password_env ?? 'MM_HARNESS_WALLET_PASSWORD');
39
+ const environmentMnemonic = source === 'fixture'
40
+ ? undefined
41
+ : environmentValue(input, srpEnv)?.trim();
42
+ const environmentPassword = source === 'fixture'
43
+ ? undefined
44
+ : environmentValue(input, passwordEnv);
45
+ const fixture = source === 'environment' || (environmentMnemonic && environmentPassword)
46
+ ? null
47
+ : await optionalFixture(input.context.projectRoot);
48
+ const fixtureMnemonic = source === 'environment'
49
+ ? undefined
50
+ : fixture?.value?.accounts?.find((account) => account?.type === 'mnemonic')?.value?.trim();
51
+ const mnemonic = environmentMnemonic || fixtureMnemonic;
52
+ const password = environmentPassword ||
53
+ (source === 'environment' ? undefined : fixture?.value?.password);
54
+
55
+ if (!mnemonic) {
56
+ throw new Error(
57
+ `Wallet import requires ${srpEnv} or a mnemonic account in temp/recipe/runtime/wallet-fixture.json.\n` +
58
+ ` Next: export ${srpEnv}=<secret-recovery-phrase>`,
59
+ );
60
+ }
61
+ if (typeof password !== 'string' || password.length === 0) {
62
+ throw new Error(
63
+ `Wallet import requires ${passwordEnv} or password in temp/recipe/runtime/wallet-fixture.json.\n` +
64
+ ` Next: export ${passwordEnv}=<wallet-password>`,
65
+ );
66
+ }
67
+
68
+ let expectedAddress;
69
+ try {
70
+ expectedAddress = mnemonicToAccount(mnemonic).address.toLowerCase();
71
+ } catch {
72
+ throw new Error('Wallet import Secret Recovery Phrase is invalid.');
73
+ }
74
+
75
+ return {
76
+ mnemonic,
77
+ password,
78
+ expectedAddress,
79
+ source: environmentMnemonic ? 'environment' : 'fixture',
80
+ sourceName: environmentMnemonic ? srpEnv : fixture?.relativePath,
81
+ };
82
+ }
83
+
84
+ export function walletImportMethod(input) {
85
+ const method = String(input.node?.method ?? 'auto');
86
+ if (!['auto', 'profile', 'ui'].includes(method)) {
87
+ throw new Error('metamask.wallet.import method must be auto, profile, or ui.');
88
+ }
89
+ return method;
90
+ }
91
+
92
+ export function validateWalletImportOptions(platform, node) {
93
+ const interests = Array.isArray(node?.interests) ? node.interests : [];
94
+ const metametrics = node?.metametrics === true || node?.metametrics === 'true';
95
+ if (platform === 'extension' && interests.length > 0) {
96
+ throw new Error('Wallet import interests are supported only on Mobile.');
97
+ }
98
+ if (platform === 'mobile' && interests.length > 0 && !metametrics) {
99
+ throw new Error('Mobile onboarding interests require metametrics=true.');
100
+ }
101
+ }
@@ -1099,6 +1099,45 @@
1099
1099
  ],
1100
1100
  "execution_capabilities": []
1101
1101
  },
1102
+ "metamask.wallet.validate_import": {
1103
+ "description": "Validate wallet recovery credentials and derive the expected primary address without launching or mutating the app.",
1104
+ "schema": {
1105
+ "type": "object",
1106
+ "properties": {
1107
+ "credential_source": {
1108
+ "type": "string",
1109
+ "enum": ["auto", "environment", "fixture"]
1110
+ },
1111
+ "srp_env": {
1112
+ "type": "string"
1113
+ },
1114
+ "password_env": {
1115
+ "type": "string"
1116
+ },
1117
+ "metametrics": {
1118
+ "type": "boolean"
1119
+ },
1120
+ "interests": {
1121
+ "type": "array",
1122
+ "items": {
1123
+ "type": "string"
1124
+ }
1125
+ }
1126
+ },
1127
+ "additionalProperties": false
1128
+ },
1129
+ "examples": [
1130
+ {
1131
+ "action": "metamask.wallet.validate_import",
1132
+ "credential_source": "auto",
1133
+ "intent": "Validate recovery credentials before resetting a wallet",
1134
+ "next": "done"
1135
+ }
1136
+ ],
1137
+ "execution_capabilities": [
1138
+ "host-read-export"
1139
+ ]
1140
+ },
1102
1141
  "metamask.wallet.setup": {
1103
1142
  "description": "Import the wallet fixture accounts and password so the app starts from a known signed-in state.",
1104
1143
  "schema": {
@@ -1117,6 +1156,79 @@
1117
1156
  "app-mutation"
1118
1157
  ]
1119
1158
  },
1159
+ "metamask.wallet.import": {
1160
+ "description": "Make the wallet ready by reusing a fixture-backed profile or importing the primary mnemonic through visible onboarding UI.",
1161
+ "schema": {
1162
+ "type": "object",
1163
+ "properties": {
1164
+ "method": {
1165
+ "type": "string",
1166
+ "enum": ["auto", "profile", "ui"]
1167
+ },
1168
+ "credential_source": {
1169
+ "type": "string",
1170
+ "enum": ["auto", "environment", "fixture"]
1171
+ },
1172
+ "srp_env": {
1173
+ "type": "string"
1174
+ },
1175
+ "password_env": {
1176
+ "type": "string"
1177
+ },
1178
+ "metametrics": {
1179
+ "type": "boolean",
1180
+ "default": false,
1181
+ "description": "Enable MetaMetrics during visible onboarding."
1182
+ },
1183
+ "interests": {
1184
+ "type": "array",
1185
+ "items": {
1186
+ "type": "string",
1187
+ "enum": ["swap_tokens", "trade_perpetuals", "prediction_markets", "send_receive_crypto", "earn_and_spend", "use_other_crypto_apps"]
1188
+ },
1189
+ "default": [],
1190
+ "description": "Mobile-only onboarding interests; ignored when empty on Extension."
1191
+ },
1192
+ "timeout_ms": {
1193
+ "type": "number"
1194
+ }
1195
+ },
1196
+ "additionalProperties": false
1197
+ },
1198
+ "examples": [
1199
+ {
1200
+ "action": "metamask.wallet.import",
1201
+ "method": "auto",
1202
+ "intent": "Use an existing fixture profile or import through onboarding",
1203
+ "next": "done"
1204
+ }
1205
+ ],
1206
+ "execution_capabilities": [
1207
+ "app-mutation"
1208
+ ]
1209
+ },
1210
+ "metamask.wallet.reset": {
1211
+ "description": "Delete the current wallet through the visible client reset flow and reach fresh onboarding.",
1212
+ "schema": {
1213
+ "type": "object",
1214
+ "properties": {
1215
+ "timeout_ms": {
1216
+ "type": "number"
1217
+ }
1218
+ },
1219
+ "additionalProperties": false
1220
+ },
1221
+ "examples": [
1222
+ {
1223
+ "action": "metamask.wallet.reset",
1224
+ "intent": "Reset the wallet through visible UI before importing it again",
1225
+ "next": "done"
1226
+ }
1227
+ ],
1228
+ "execution_capabilities": [
1229
+ "app-mutation"
1230
+ ]
1231
+ },
1120
1232
  "metamask.wallet.ensure_unlocked": {
1121
1233
  "description": "Unlock the wallet with the fixture password when it is currently locked.",
1122
1234
  "schema": {