@deeeed/metamask-harness 0.34.4 → 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.
- package/CHANGELOG.md +25 -0
- package/README.md +9 -1
- package/adapters/core/inject.sh +3 -3
- package/adapters/extension/check-infura-readiness.cjs +102 -0
- package/adapters/extension/inject.mjs +4 -4
- package/adapters/extension/live.sh +25 -10
- package/adapters/extension/start-watch.sh +15 -0
- package/adapters/extension/wallet-fixture-state.cjs +3 -1
- package/adapters/manifest.json +16 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -42
- package/adapters/mobile/inject.sh +4 -3
- package/adapters/mobile/reset-app-data.sh +154 -0
- package/adapters/mobile/verify.sh +37 -0
- package/dist/adapters/extension/product-config.js +29 -1
- package/dist/adapters/extension/runtime.js +60 -15
- package/dist/adapters/extension/surface.js +6 -1
- package/dist/adapters/mobile/surface.js +6 -1
- package/dist/adapters.js +18 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/command-contract.js +1 -1
- package/dist/commands/device-target.js +5 -0
- package/dist/commands/fixtures.js +106 -31
- package/dist/commands/launch/extension.js +39 -3
- package/dist/commands/launch/index.js +13 -0
- package/dist/doctor.js +30 -1
- package/dist/mm-harness-cli.js +6 -3
- package/dist/recipe-security.js +3 -0
- package/docs/RECIPES.md +29 -0
- package/library/actions/extension/wallet/import.mjs +234 -0
- package/library/actions/extension/wallet/reset.mjs +98 -0
- package/library/actions/extension/wallet/state.mjs +1 -0
- package/library/actions/mobile/analytics/consent-settings.mjs +112 -0
- package/library/actions/mobile/analytics/set_consent.mjs +4 -112
- package/library/actions/mobile/platform/bridge.mjs +8 -0
- package/library/actions/mobile/wallet/import.mjs +259 -0
- package/library/actions/mobile/wallet/reset-helper.mjs +99 -0
- package/library/actions/mobile/wallet/reset.mjs +7 -0
- package/library/actions/shared/wallet/import-source.mjs +101 -0
- package/library/manifests/extension.action-manifest.json +112 -0
- package/library/manifests/mobile.action-manifest.json +108 -0
- package/library/recipes/wallet/import.recipe.json +83 -0
- package/library/recipes/wallet/reset-import.recipe.json +88 -0
- package/package.json +1 -1
- package/scripts/completions.sh +2 -2
- package/scripts/site-contrast.mjs +7 -0
- package/site/architecture.html +14 -7
- package/site/assets/metamask-fox.svg +24 -0
- package/site/assets/progress.mjs +6 -0
- package/site/assets/style.css +88 -29
- package/site/cheatsheet.html +5 -6
- package/site/ecosystem.html +162 -0
- package/site/how-it-works.html +8 -9
- package/site/index.html +17 -12
- package/site/perps.html +103 -45
- package/site/recipes.html +5 -6
- package/site/reviewers.html +5 -6
- package/site/tutorials/index.html +5 -6
- package/site/tutorials/v1.html +5 -6
- package/site/tutorials/v2.html +6 -8
- package/site/tutorials/v3.html +5 -6
- package/site/tutorials/v4.html +5 -6
- package/site/tutorials/v5.html +5 -6
- package/site/tutorials/v6.html +5 -6
- package/site/tutorials/v7.html +5 -6
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { resolveWalletImportCredentials, validateWalletImportOptions, walletImportMethod } from '../../shared/wallet/import-source.mjs';
|
|
2
|
+
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
3
|
+
import { readWalletStateExpression } from './state.mjs';
|
|
4
|
+
|
|
5
|
+
const SELECTORS = {
|
|
6
|
+
importWallet: '[data-testid="onboarding-import-wallet"]',
|
|
7
|
+
importWithSrp: '[data-testid="onboarding-import-with-srp-button"]',
|
|
8
|
+
srp: '[data-testid="srp-input-import__srp-note"]',
|
|
9
|
+
confirmSrp: '[data-testid="import-srp-confirm"]',
|
|
10
|
+
password: '[data-testid="create-password-new-input"]',
|
|
11
|
+
confirmPassword: '[data-testid="create-password-confirm-input"]',
|
|
12
|
+
passwordTerms: '[data-testid="create-password-terms"]',
|
|
13
|
+
createPassword: '[data-testid="create-password-submit"]',
|
|
14
|
+
skipPasskey: '[data-testid="passkey-maybe-later-button"]',
|
|
15
|
+
metricsCheckbox: '[data-testid="metametrics-checkbox"]',
|
|
16
|
+
metricsContinue: '[data-testid="metametrics-i-agree"]',
|
|
17
|
+
complete: '[data-testid="onboarding-complete-done"]',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async function hasSelector(page, selector) {
|
|
21
|
+
try {
|
|
22
|
+
return await page.evaluate(`Boolean(document.querySelector(${JSON.stringify(selector)}))`);
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function waitForSelector(page, selector, timeoutMs) {
|
|
29
|
+
const deadline = Date.now() + timeoutMs;
|
|
30
|
+
while (Date.now() <= deadline) {
|
|
31
|
+
if (await hasSelector(page, selector)) return;
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Wallet import timed out waiting for ${selector}.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function waitForSelectorToDisappear(page, selector, timeoutMs) {
|
|
38
|
+
const deadline = Date.now() + timeoutMs;
|
|
39
|
+
while (Date.now() <= deadline) {
|
|
40
|
+
if (!(await hasSelector(page, selector))) return;
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Wallet import timed out waiting for ${selector} to disappear.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function waitForFirstSelector(page, selectors, timeoutMs) {
|
|
47
|
+
const deadline = Date.now() + timeoutMs;
|
|
48
|
+
while (Date.now() <= deadline) {
|
|
49
|
+
for (const selector of selectors) {
|
|
50
|
+
if (await hasSelector(page, selector)) return selector;
|
|
51
|
+
}
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
53
|
+
}
|
|
54
|
+
throw new Error('Wallet import timed out waiting for the next onboarding screen.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function setSecretInput(page, selector, value) {
|
|
58
|
+
await page.evaluate(`(() => {
|
|
59
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
60
|
+
if (!element) throw new Error('Wallet import secret input was not found.');
|
|
61
|
+
element.focus();
|
|
62
|
+
if (typeof element.select === 'function') element.select();
|
|
63
|
+
})()`);
|
|
64
|
+
await page.session.call('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace' });
|
|
65
|
+
await page.session.call('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace' });
|
|
66
|
+
await page.session.call('Input.insertText', { text: value });
|
|
67
|
+
const matches = await page.evaluate(`(() => {
|
|
68
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
69
|
+
if (!element) return false;
|
|
70
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
71
|
+
return element.value === ${JSON.stringify(value)};
|
|
72
|
+
})()`);
|
|
73
|
+
if (!matches) throw new Error('Wallet import secret input did not retain the supplied value.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function pasteSecretInput(page, selector, value) {
|
|
77
|
+
const pasted = await page.evaluate(`(() => {
|
|
78
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
79
|
+
if (!element) return false;
|
|
80
|
+
const clipboardData = new DataTransfer();
|
|
81
|
+
clipboardData.setData('text', ${JSON.stringify(value)});
|
|
82
|
+
element.focus();
|
|
83
|
+
element.dispatchEvent(new ClipboardEvent('paste', {
|
|
84
|
+
bubbles: true,
|
|
85
|
+
cancelable: true,
|
|
86
|
+
clipboardData,
|
|
87
|
+
}));
|
|
88
|
+
return true;
|
|
89
|
+
})()`);
|
|
90
|
+
if (!pasted) throw new Error('Wallet import Secret Recovery Phrase input was not found.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function waitForEnabled(page, selector, timeoutMs) {
|
|
94
|
+
const deadline = Date.now() + timeoutMs;
|
|
95
|
+
while (Date.now() <= deadline) {
|
|
96
|
+
const enabled = await page.evaluate(`(() => {
|
|
97
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
98
|
+
return Boolean(element && !element.disabled && element.getAttribute('aria-disabled') !== 'true');
|
|
99
|
+
})()`).catch(() => false);
|
|
100
|
+
if (enabled) return;
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`Wallet import timed out waiting for ${selector} to become enabled.`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function readState(page) {
|
|
107
|
+
return page.evaluate(readWalletStateExpression(), { awaitPromise: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function unlockProfile(page, credentials, timeoutMs) {
|
|
111
|
+
await setSecretInput(page, 'input[type="password"]', credentials.password);
|
|
112
|
+
await page.click('button[type="submit"]');
|
|
113
|
+
await waitForSelectorToDisappear(page, 'input[type="password"]', timeoutMs);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function importThroughUi(page, input, timeoutMs) {
|
|
117
|
+
const credentials = await resolveWalletImportCredentials(input);
|
|
118
|
+
const metametricsEnabled = input.node?.metametrics === true || input.node?.metametrics === 'true';
|
|
119
|
+
const interests = Array.isArray(input.node?.interests) ? input.node.interests : [];
|
|
120
|
+
if (interests.length > 0) {
|
|
121
|
+
throw new Error('metamask.wallet.import interests are supported only on Mobile.');
|
|
122
|
+
}
|
|
123
|
+
await page.click(SELECTORS.importWallet);
|
|
124
|
+
const importChoice = await waitForFirstSelector(
|
|
125
|
+
page,
|
|
126
|
+
[SELECTORS.importWithSrp, SELECTORS.srp],
|
|
127
|
+
timeoutMs,
|
|
128
|
+
);
|
|
129
|
+
if (importChoice === SELECTORS.importWithSrp) {
|
|
130
|
+
await page.click(SELECTORS.importWithSrp);
|
|
131
|
+
}
|
|
132
|
+
await waitForSelector(page, SELECTORS.srp, timeoutMs);
|
|
133
|
+
await pasteSecretInput(page, SELECTORS.srp, credentials.mnemonic);
|
|
134
|
+
await waitForEnabled(page, SELECTORS.confirmSrp, timeoutMs);
|
|
135
|
+
await page.click(SELECTORS.confirmSrp);
|
|
136
|
+
await waitForSelector(page, SELECTORS.password, timeoutMs);
|
|
137
|
+
await setSecretInput(page, SELECTORS.password, credentials.password);
|
|
138
|
+
await setSecretInput(page, SELECTORS.confirmPassword, credentials.password);
|
|
139
|
+
await page.click(SELECTORS.passwordTerms);
|
|
140
|
+
await waitForEnabled(page, SELECTORS.createPassword, timeoutMs);
|
|
141
|
+
await page.click(SELECTORS.createPassword);
|
|
142
|
+
|
|
143
|
+
await waitForFirstSelector(
|
|
144
|
+
page,
|
|
145
|
+
[SELECTORS.skipPasskey, SELECTORS.metricsContinue],
|
|
146
|
+
timeoutMs,
|
|
147
|
+
);
|
|
148
|
+
if (await hasSelector(page, SELECTORS.skipPasskey)) {
|
|
149
|
+
await page.click(SELECTORS.skipPasskey);
|
|
150
|
+
await waitForSelector(page, SELECTORS.metricsContinue, timeoutMs);
|
|
151
|
+
}
|
|
152
|
+
const metricsChecked = await page.evaluate(`(() => {
|
|
153
|
+
const checkbox = document.querySelector(${JSON.stringify(SELECTORS.metricsCheckbox)});
|
|
154
|
+
return checkbox?.checked === true ||
|
|
155
|
+
checkbox?.getAttribute('data-checked') === 'true' ||
|
|
156
|
+
checkbox?.getAttribute('aria-checked') === 'true';
|
|
157
|
+
})()`);
|
|
158
|
+
if (metricsChecked !== metametricsEnabled) await page.click(SELECTORS.metricsCheckbox);
|
|
159
|
+
await page.click(SELECTORS.metricsContinue);
|
|
160
|
+
await waitForSelector(page, SELECTORS.complete, timeoutMs);
|
|
161
|
+
await page.click(SELECTORS.complete);
|
|
162
|
+
|
|
163
|
+
const deadline = Date.now() + timeoutMs;
|
|
164
|
+
let state = await readState(page);
|
|
165
|
+
while (
|
|
166
|
+
Date.now() <= deadline &&
|
|
167
|
+
(!state.completedOnboarding || !state.selectedAccount?.address)
|
|
168
|
+
) {
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
170
|
+
state = await readState(page);
|
|
171
|
+
}
|
|
172
|
+
if (!state.completedOnboarding || !state.selectedAccount?.address) {
|
|
173
|
+
throw new Error('Extension wallet import did not complete onboarding with a selected account.');
|
|
174
|
+
}
|
|
175
|
+
if (state.metametricsOptedIn !== metametricsEnabled) {
|
|
176
|
+
throw new Error(`Extension MetaMetrics preference was ${String(state.metametricsOptedIn)}, expected ${String(metametricsEnabled)}.`);
|
|
177
|
+
}
|
|
178
|
+
const actualAddress = String(state.selectedAccount?.address ?? '').toLowerCase();
|
|
179
|
+
if (actualAddress !== credentials.expectedAddress) {
|
|
180
|
+
throw new Error(`Wallet import selected ${actualAddress || 'no address'}, expected ${credentials.expectedAddress}.`);
|
|
181
|
+
}
|
|
182
|
+
return { credentials, state, metametricsEnabled };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
186
|
+
validateWalletImportOptions('extension', input.node);
|
|
187
|
+
const method = walletImportMethod(input);
|
|
188
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
|
|
189
|
+
const before = await readState(page);
|
|
190
|
+
const onboarding = String(before.href ?? '').includes('onboarding');
|
|
191
|
+
|
|
192
|
+
if (method === 'ui' && !onboarding) {
|
|
193
|
+
throw new Error('metamask.wallet.import method=ui requires a fresh Extension onboarding profile.');
|
|
194
|
+
}
|
|
195
|
+
if (onboarding) {
|
|
196
|
+
if (method === 'profile') {
|
|
197
|
+
throw new Error('metamask.wallet.import method=profile found a fresh Extension onboarding profile.');
|
|
198
|
+
}
|
|
199
|
+
const imported = await importThroughUi(page, input, timeoutMs);
|
|
200
|
+
return {
|
|
201
|
+
action: input.action,
|
|
202
|
+
method: 'ui-import',
|
|
203
|
+
credentialSource: imported.credentials.source,
|
|
204
|
+
credentialSourceName: imported.credentials.sourceName,
|
|
205
|
+
selectedAddress: imported.state.selectedAccount.address,
|
|
206
|
+
completedOnboarding: imported.state.completedOnboarding,
|
|
207
|
+
metametricsEnabled: imported.metametricsEnabled,
|
|
208
|
+
interests: [],
|
|
209
|
+
redacted: true,
|
|
210
|
+
proofPath: 'extension-visible-onboarding-import',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const credentials = await resolveWalletImportCredentials(input);
|
|
215
|
+
if (before.passwordInput) await unlockProfile(page, credentials, timeoutMs);
|
|
216
|
+
const after = await readState(page);
|
|
217
|
+
if (!after.completedOnboarding || !after.selectedAccount?.address || after.passwordInput) {
|
|
218
|
+
throw new Error('Extension fixture-backed wallet is not ready after profile verification.');
|
|
219
|
+
}
|
|
220
|
+
const actualAddress = String(after.selectedAccount.address).toLowerCase();
|
|
221
|
+
if (actualAddress !== credentials.expectedAddress) {
|
|
222
|
+
throw new Error(`Wallet profile selected ${actualAddress}, expected ${credentials.expectedAddress}.`);
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
action: input.action,
|
|
226
|
+
method: 'profile',
|
|
227
|
+
credentialSource: credentials.source,
|
|
228
|
+
credentialSourceName: credentials.sourceName,
|
|
229
|
+
selectedAddress: after.selectedAccount.address,
|
|
230
|
+
completedOnboarding: after.completedOnboarding,
|
|
231
|
+
redacted: true,
|
|
232
|
+
proofPath: 'extension-fixture-profile',
|
|
233
|
+
};
|
|
234
|
+
}));
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import { readWalletStateExpression } from './state.mjs';
|
|
3
|
+
|
|
4
|
+
const SELECTORS = {
|
|
5
|
+
accountMenu: '[data-testid="account-options-menu-button"]',
|
|
6
|
+
lock: '[data-testid="global-menu-lock"]',
|
|
7
|
+
forgotPassword: '[data-testid="unlock-forgot-password-button"]',
|
|
8
|
+
resetChoice: '[data-testid="reset-password-modal-button-link"]',
|
|
9
|
+
confirmReset: '[data-testid="reset-password-modal-button"]',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async function hasSelector(page, selector) {
|
|
13
|
+
return page.evaluate(`Boolean(document.querySelector(${JSON.stringify(selector)}))`).catch(() => false);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function waitForSelector(page, selector, timeoutMs) {
|
|
17
|
+
const deadline = Date.now() + timeoutMs;
|
|
18
|
+
while (Date.now() <= deadline) {
|
|
19
|
+
if (await hasSelector(page, selector)) return;
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Wallet reset timed out waiting for ${selector}.`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function clickWhenReady(page, selector, timeoutMs) {
|
|
26
|
+
const deadline = Date.now() + timeoutMs;
|
|
27
|
+
let lastError;
|
|
28
|
+
while (Date.now() <= deadline) {
|
|
29
|
+
if (await hasSelector(page, selector)) {
|
|
30
|
+
try {
|
|
31
|
+
await page.click(selector);
|
|
32
|
+
return;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const message = String(error?.message ?? error);
|
|
35
|
+
if (!message.includes('Target is obscured') && !message.includes('Target is not visible')) {
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
lastError = error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
42
|
+
}
|
|
43
|
+
throw lastError ?? new Error(`Wallet reset timed out waiting for ${selector}.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readState(page) {
|
|
47
|
+
return page.evaluate(readWalletStateExpression(), { awaitPromise: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isFreshWallet(state) {
|
|
51
|
+
return !state.completedOnboarding &&
|
|
52
|
+
String(state.href ?? '').includes('/onboarding') &&
|
|
53
|
+
!state.selectedAccount;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function waitForFreshWallet(page, timeoutMs) {
|
|
57
|
+
const deadline = Date.now() + timeoutMs;
|
|
58
|
+
let observed;
|
|
59
|
+
while (Date.now() <= deadline) {
|
|
60
|
+
observed = await readState(page);
|
|
61
|
+
if (isFreshWallet(observed)) return observed;
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
63
|
+
}
|
|
64
|
+
throw new Error('Extension wallet reset did not clear onboarding and the selected account.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
68
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
|
|
69
|
+
const before = await readState(page);
|
|
70
|
+
if (isFreshWallet(before)) {
|
|
71
|
+
return {
|
|
72
|
+
action: input.action,
|
|
73
|
+
alreadyFresh: true,
|
|
74
|
+
completedOnboarding: false,
|
|
75
|
+
redacted: true,
|
|
76
|
+
proofPath: 'extension-visible-wallet-reset',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!before.passwordInput) {
|
|
81
|
+
await page.navigateHash('#/');
|
|
82
|
+
await waitForSelector(page, SELECTORS.accountMenu, timeoutMs);
|
|
83
|
+
await clickWhenReady(page, SELECTORS.accountMenu, timeoutMs);
|
|
84
|
+
await clickWhenReady(page, SELECTORS.lock, timeoutMs);
|
|
85
|
+
await waitForSelector(page, SELECTORS.forgotPassword, timeoutMs);
|
|
86
|
+
}
|
|
87
|
+
await clickWhenReady(page, SELECTORS.forgotPassword, timeoutMs);
|
|
88
|
+
await clickWhenReady(page, SELECTORS.resetChoice, timeoutMs);
|
|
89
|
+
await clickWhenReady(page, SELECTORS.confirmReset, timeoutMs);
|
|
90
|
+
const after = await waitForFreshWallet(page, timeoutMs);
|
|
91
|
+
return {
|
|
92
|
+
action: input.action,
|
|
93
|
+
alreadyFresh: false,
|
|
94
|
+
completedOnboarding: after.completedOnboarding,
|
|
95
|
+
redacted: true,
|
|
96
|
+
proofPath: 'extension-visible-wallet-reset',
|
|
97
|
+
};
|
|
98
|
+
}));
|
|
@@ -38,6 +38,7 @@ export function readWalletStateExpression() {
|
|
|
38
38
|
type: selected.type || null,
|
|
39
39
|
} : null,
|
|
40
40
|
completedOnboarding: Boolean(metamask.completedOnboarding),
|
|
41
|
+
metametricsOptedIn: typeof metamask.optedIn === 'boolean' ? metamask.optedIn : null,
|
|
41
42
|
selectedNetworkClientId: metamask.selectedNetworkClientId || null,
|
|
42
43
|
};
|
|
43
44
|
})()`;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { evalAsync, navigate } from '../platform/bridge.mjs';
|
|
2
|
+
|
|
3
|
+
const SWITCH_IDS = {
|
|
4
|
+
participate: 'metametrics-switch',
|
|
5
|
+
marketing: 'data-collection-switch',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function toggleExpression(testId, expected, invoke) {
|
|
9
|
+
return `(() => {
|
|
10
|
+
const find = () => {
|
|
11
|
+
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
12
|
+
const rootsFor = hook?.getFiberRoots;
|
|
13
|
+
if (!hook?.renderers || typeof rootsFor !== 'function') return null;
|
|
14
|
+
const walk = (fiber) => {
|
|
15
|
+
if (!fiber) return null;
|
|
16
|
+
if (fiber.memoizedProps?.testID === ${JSON.stringify(testId)}) return fiber;
|
|
17
|
+
return walk(fiber.child) || walk(fiber.sibling);
|
|
18
|
+
};
|
|
19
|
+
for (const [id] of hook.renderers) {
|
|
20
|
+
for (const root of rootsFor(id) ?? []) {
|
|
21
|
+
const match = walk(root.current);
|
|
22
|
+
if (match) return match;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
const target = find();
|
|
28
|
+
const props = target?.memoizedProps;
|
|
29
|
+
if (!props) return { matched: false, found: false, invoked: false };
|
|
30
|
+
if (Boolean(props.value) === ${JSON.stringify(expected)}) {
|
|
31
|
+
return { matched: true, found: true, invoked: false };
|
|
32
|
+
}
|
|
33
|
+
if (!${JSON.stringify(invoke)}) {
|
|
34
|
+
return { matched: false, found: true, invoked: false };
|
|
35
|
+
}
|
|
36
|
+
if (typeof props.onValueChange !== 'function') {
|
|
37
|
+
throw new Error('Consent switch has no onValueChange handler: ${testId}');
|
|
38
|
+
}
|
|
39
|
+
return Promise.resolve(
|
|
40
|
+
props.onValueChange(${JSON.stringify(expected)})
|
|
41
|
+
).then(() => ({
|
|
42
|
+
matched: false,
|
|
43
|
+
found: true,
|
|
44
|
+
invoked: true
|
|
45
|
+
}));
|
|
46
|
+
})()`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function stateExpression(participate, marketing) {
|
|
50
|
+
return `(() => {
|
|
51
|
+
const state = globalThis.store?.getState?.();
|
|
52
|
+
const analytics = state?.engine?.backgroundState?.AnalyticsController ?? {};
|
|
53
|
+
const consent = {
|
|
54
|
+
optedIn: Boolean(analytics.optedIn),
|
|
55
|
+
dataCollectionForMarketing: Boolean(state?.security?.dataCollectionForMarketing),
|
|
56
|
+
analyticsId: analytics.analyticsId ? 'set' : null
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
ready:
|
|
60
|
+
consent.optedIn === ${JSON.stringify(participate)} &&
|
|
61
|
+
consent.dataCollectionForMarketing === ${JSON.stringify(marketing)} &&
|
|
62
|
+
(!${JSON.stringify(participate)} || consent.analyticsId === 'set'),
|
|
63
|
+
consent
|
|
64
|
+
};
|
|
65
|
+
})()`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function delay(ms) {
|
|
69
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function setSwitch(input, testId, expected, timeoutMs) {
|
|
73
|
+
const deadline = Date.now() + timeoutMs;
|
|
74
|
+
let invoked = false;
|
|
75
|
+
while (Date.now() < deadline) {
|
|
76
|
+
const result = await evalAsync(
|
|
77
|
+
input,
|
|
78
|
+
toggleExpression(testId, expected, !invoked),
|
|
79
|
+
);
|
|
80
|
+
invoked ||= Boolean(result?.invoked);
|
|
81
|
+
if (result?.matched) {
|
|
82
|
+
return { ...result, changed: invoked };
|
|
83
|
+
}
|
|
84
|
+
await delay(100);
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`Timed out setting consent switch ${testId} to ${expected}.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function readConsent(input, participate, marketing, timeoutMs) {
|
|
90
|
+
const deadline = Date.now() + timeoutMs;
|
|
91
|
+
while (Date.now() < deadline) {
|
|
92
|
+
const result = await evalAsync(
|
|
93
|
+
input,
|
|
94
|
+
stateExpression(participate, marketing),
|
|
95
|
+
);
|
|
96
|
+
if (result?.ready) {
|
|
97
|
+
return result.consent;
|
|
98
|
+
}
|
|
99
|
+
await delay(100);
|
|
100
|
+
}
|
|
101
|
+
throw new Error('Timed out reading back Mobile analytics consent.');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function setMobileConsent(input, participate, marketing, timeoutMs) {
|
|
105
|
+
const navigation = await navigate(input, 'SettingsView', {
|
|
106
|
+
screen: 'SecuritySettings',
|
|
107
|
+
}, 'SecuritySettings');
|
|
108
|
+
await setSwitch(input, SWITCH_IDS.participate, participate, timeoutMs);
|
|
109
|
+
await setSwitch(input, SWITCH_IDS.marketing, marketing, timeoutMs);
|
|
110
|
+
const consent = await readConsent(input, participate, marketing, timeoutMs);
|
|
111
|
+
return { consent, navigation };
|
|
112
|
+
}
|
|
@@ -1,120 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runAdapter } from '../platform/bridge.mjs';
|
|
2
2
|
import { consentParams } from '../../shared/analytics/consent.mjs';
|
|
3
|
-
|
|
4
|
-
const SWITCH_IDS = {
|
|
5
|
-
participate: 'metametrics-switch',
|
|
6
|
-
marketing: 'data-collection-switch',
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
function toggleExpression(testId, expected, invoke) {
|
|
10
|
-
return `(() => {
|
|
11
|
-
const find = () => {
|
|
12
|
-
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
13
|
-
const rootsFor = hook?.getFiberRoots;
|
|
14
|
-
if (!hook?.renderers || typeof rootsFor !== 'function') return null;
|
|
15
|
-
const walk = (fiber) => {
|
|
16
|
-
if (!fiber) return null;
|
|
17
|
-
if (fiber.memoizedProps?.testID === ${JSON.stringify(testId)}) return fiber;
|
|
18
|
-
return walk(fiber.child) || walk(fiber.sibling);
|
|
19
|
-
};
|
|
20
|
-
for (const [id] of hook.renderers) {
|
|
21
|
-
for (const root of rootsFor(id) ?? []) {
|
|
22
|
-
const match = walk(root.current);
|
|
23
|
-
if (match) return match;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return null;
|
|
27
|
-
};
|
|
28
|
-
const target = find();
|
|
29
|
-
const props = target?.memoizedProps;
|
|
30
|
-
if (!props) return { matched: false, found: false, invoked: false };
|
|
31
|
-
if (Boolean(props.value) === ${JSON.stringify(expected)}) {
|
|
32
|
-
return { matched: true, found: true, invoked: false };
|
|
33
|
-
}
|
|
34
|
-
if (!${JSON.stringify(invoke)}) {
|
|
35
|
-
return { matched: false, found: true, invoked: false };
|
|
36
|
-
}
|
|
37
|
-
if (typeof props.onValueChange !== 'function') {
|
|
38
|
-
throw new Error('Consent switch has no onValueChange handler: ${testId}');
|
|
39
|
-
}
|
|
40
|
-
return Promise.resolve(
|
|
41
|
-
props.onValueChange(${JSON.stringify(expected)})
|
|
42
|
-
).then(() => ({
|
|
43
|
-
matched: false,
|
|
44
|
-
found: true,
|
|
45
|
-
invoked: true
|
|
46
|
-
}));
|
|
47
|
-
})()`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function stateExpression(participate, marketing) {
|
|
51
|
-
return `(() => {
|
|
52
|
-
const state = globalThis.store?.getState?.();
|
|
53
|
-
const analytics = state?.engine?.backgroundState?.AnalyticsController ?? {};
|
|
54
|
-
const consent = {
|
|
55
|
-
optedIn: Boolean(analytics.optedIn),
|
|
56
|
-
dataCollectionForMarketing: Boolean(state?.security?.dataCollectionForMarketing),
|
|
57
|
-
analyticsId: analytics.analyticsId ? 'set' : null
|
|
58
|
-
};
|
|
59
|
-
return {
|
|
60
|
-
ready:
|
|
61
|
-
consent.optedIn === ${JSON.stringify(participate)} &&
|
|
62
|
-
consent.dataCollectionForMarketing === ${JSON.stringify(marketing)} &&
|
|
63
|
-
(!${JSON.stringify(participate)} || consent.analyticsId === 'set'),
|
|
64
|
-
consent
|
|
65
|
-
};
|
|
66
|
-
})()`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function delay(ms) {
|
|
70
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function setSwitch(input, testId, expected, timeoutMs) {
|
|
74
|
-
const deadline = Date.now() + timeoutMs;
|
|
75
|
-
let invoked = false;
|
|
76
|
-
while (Date.now() < deadline) {
|
|
77
|
-
const result = await evalAsync(
|
|
78
|
-
input,
|
|
79
|
-
toggleExpression(testId, expected, !invoked),
|
|
80
|
-
);
|
|
81
|
-
invoked ||= Boolean(result?.invoked);
|
|
82
|
-
if (result?.matched) {
|
|
83
|
-
return { ...result, changed: invoked };
|
|
84
|
-
}
|
|
85
|
-
await delay(100);
|
|
86
|
-
}
|
|
87
|
-
throw new Error(`Timed out setting consent switch ${testId} to ${expected}.`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function readConsent(input, participate, marketing, timeoutMs) {
|
|
91
|
-
const deadline = Date.now() + timeoutMs;
|
|
92
|
-
while (Date.now() < deadline) {
|
|
93
|
-
const result = await evalAsync(
|
|
94
|
-
input,
|
|
95
|
-
stateExpression(participate, marketing),
|
|
96
|
-
);
|
|
97
|
-
if (result?.ready) {
|
|
98
|
-
return result.consent;
|
|
99
|
-
}
|
|
100
|
-
await delay(100);
|
|
101
|
-
}
|
|
102
|
-
throw new Error('Timed out reading back Mobile analytics consent.');
|
|
103
|
-
}
|
|
3
|
+
import { setMobileConsent } from './consent-settings.mjs';
|
|
104
4
|
|
|
105
5
|
runAdapter(async (input) => {
|
|
106
6
|
const { participate, marketing, timeoutMs } = consentParams(input.node);
|
|
107
7
|
|
|
108
|
-
const navigation = await
|
|
109
|
-
|
|
110
|
-
}, 'SecuritySettings');
|
|
111
|
-
await setSwitch(input, SWITCH_IDS.participate, participate, timeoutMs);
|
|
112
|
-
await setSwitch(input, SWITCH_IDS.marketing, marketing, timeoutMs);
|
|
113
|
-
const consent = await readConsent(
|
|
114
|
-
input,
|
|
115
|
-
participate,
|
|
116
|
-
marketing,
|
|
117
|
-
timeoutMs,
|
|
8
|
+
const { consent, navigation } = await setMobileConsent(
|
|
9
|
+
input, participate, marketing, timeoutMs,
|
|
118
10
|
);
|
|
119
11
|
|
|
120
12
|
return {
|
|
@@ -454,6 +454,9 @@ export async function bridgeCommand(input, args) {
|
|
|
454
454
|
function redactBridgeArgs(args) {
|
|
455
455
|
const command = String(args[0] ?? '');
|
|
456
456
|
if (command === 'unlock' && args.length > 1) return [command, '<redacted-password>', ...args.slice(2)];
|
|
457
|
+
if (command === 'set-input' && isSensitiveInputId(args[1])) {
|
|
458
|
+
return [command, String(args[1]), '<redacted>'];
|
|
459
|
+
}
|
|
457
460
|
return args.map((arg) => String(arg));
|
|
458
461
|
}
|
|
459
462
|
|
|
@@ -470,9 +473,14 @@ function redactBridgeOutput(output, args) {
|
|
|
470
473
|
function sensitiveBridgeArgs(args) {
|
|
471
474
|
const command = String(args[0] ?? '');
|
|
472
475
|
if (command === 'unlock' && args.length > 1) return [args[1]];
|
|
476
|
+
if (command === 'set-input' && isSensitiveInputId(args[1])) return args.slice(2);
|
|
473
477
|
return [];
|
|
474
478
|
}
|
|
475
479
|
|
|
480
|
+
function isSensitiveInputId(value) {
|
|
481
|
+
return /(?:confirm-password|password|phrase|seed|srp)/iu.test(String(value ?? ''));
|
|
482
|
+
}
|
|
483
|
+
|
|
476
484
|
function parseMaybeJson(value) {
|
|
477
485
|
if (typeof value !== 'string') return value;
|
|
478
486
|
try {
|