@deeeed/metamask-harness 0.35.0 → 0.37.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 (73) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +9 -1
  3. package/adapters/extension/artifact-runtime-state.cjs +128 -0
  4. package/adapters/extension/check-infura-readiness.cjs +102 -0
  5. package/adapters/extension/inject.mjs +2 -0
  6. package/adapters/extension/launch-browser.cjs +22 -0
  7. package/adapters/extension/live.sh +103 -18
  8. package/adapters/extension/readiness.mjs +77 -36
  9. package/adapters/extension/snapshot-dist.sh +88 -3
  10. package/adapters/extension/start-watch.sh +15 -0
  11. package/adapters/extension/verify.sh +6 -2
  12. package/adapters/extension/wallet-fixture-state.cjs +3 -1
  13. package/adapters/manifest.json +25 -1
  14. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -42
  15. package/adapters/mobile/reset-app-data.sh +154 -0
  16. package/adapters/shared/log-tui.mjs +1 -1
  17. package/dist/adapters/extension/artifact-integrity.js +38 -0
  18. package/dist/adapters/extension/extension-id.js +23 -4
  19. package/dist/adapters/extension/product-config.js +29 -1
  20. package/dist/adapters/extension/release-artifact.js +386 -0
  21. package/dist/adapters/extension/runtime-decision.js +161 -20
  22. package/dist/adapters/extension/runtime.js +127 -0
  23. package/dist/adapters/mobile/release-artifact-state.js +124 -0
  24. package/dist/adapters/mobile/release-artifact.js +295 -0
  25. package/dist/adapters.js +29 -4
  26. package/dist/cli-commands.js +1 -1
  27. package/dist/cli.js +2 -2
  28. package/dist/command-contract.js +17 -1
  29. package/dist/commands/call.js +2 -1
  30. package/dist/commands/device-target.js +5 -0
  31. package/dist/commands/fixtures.js +106 -31
  32. package/dist/commands/launch/extension.js +92 -7
  33. package/dist/commands/launch/index.js +13 -0
  34. package/dist/commands/launch/mobile.js +2 -0
  35. package/dist/commands/provision.js +2 -0
  36. package/dist/commands/run-engine.js +89 -5
  37. package/dist/commands/run.js +3 -1
  38. package/dist/commands/runtime-launch.js +178 -10
  39. package/dist/heal-bounds.js +1 -1
  40. package/dist/live-adapter-contract.js +3 -1
  41. package/dist/metamask-action-validation.js +47 -1
  42. package/dist/mm-harness-cli.js +37 -4
  43. package/dist/recipe-security.js +3 -0
  44. package/dist/run-diagnostics.js +1 -1
  45. package/docs/RECIPES.md +29 -0
  46. package/docs/RELEASE-QA-CAPABILITY-MAP.md +150 -0
  47. package/library/actions/extension/perps/perps.mjs +2 -0
  48. package/library/actions/extension/perps/read_snapshot.mjs +470 -0
  49. package/library/actions/extension/platform/cdp.mjs +6 -3
  50. package/library/actions/extension/wallet/import.mjs +201 -0
  51. package/library/actions/extension/wallet/reset.mjs +98 -0
  52. package/library/actions/extension/wallet/secret-input.mjs +98 -0
  53. package/library/actions/extension/wallet/state.mjs +1 -0
  54. package/library/actions/mobile/analytics/consent-settings.mjs +112 -0
  55. package/library/actions/mobile/analytics/set_consent.mjs +4 -112
  56. package/library/actions/mobile/platform/bridge.mjs +8 -0
  57. package/library/actions/mobile/platform/observe-ui.mjs +84 -2
  58. package/library/actions/mobile/ui/native-navigation.mjs +225 -0
  59. package/library/actions/mobile/ui/navigate.mjs +7 -0
  60. package/library/actions/mobile/wallet/import.mjs +328 -0
  61. package/library/actions/mobile/wallet/native-ui.mjs +493 -0
  62. package/library/actions/mobile/wallet/read_state.mjs +16 -0
  63. package/library/actions/mobile/wallet/reset-helper.mjs +99 -0
  64. package/library/actions/mobile/wallet/reset.mjs +20 -0
  65. package/library/actions/shared/ui/locators.mjs +7 -0
  66. package/library/actions/shared/wallet/import-source.mjs +101 -0
  67. package/library/manifests/extension.action-manifest.json +228 -0
  68. package/library/manifests/mobile.action-manifest.json +124 -0
  69. package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
  70. package/library/recipes/wallet/import.recipe.json +102 -0
  71. package/library/recipes/wallet/reset-import.recipe.json +107 -0
  72. package/package.json +1 -1
  73. package/scripts/completions.sh +2 -2
@@ -0,0 +1,470 @@
1
+ import { pathToFileURL } from 'node:url';
2
+
3
+ import { normalizeMarketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
4
+
5
+ const ALL_SECTIONS = ['account', 'markets', 'positions', 'orders', 'history', 'watchlist'];
6
+ const DEFAULT_HISTORY_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
7
+ const MAX_HISTORY_WINDOW_MS = 365 * 24 * 60 * 60 * 1000;
8
+
9
+ function boundedInteger(value, fallback, minimum, maximum, name) {
10
+ const resolved = value == null ? fallback : Number(value);
11
+ if (!Number.isInteger(resolved) || resolved < minimum || resolved > maximum) {
12
+ throw new Error(`metamask.perps.read_snapshot ${name} must be an integer from ${minimum} to ${maximum}.`);
13
+ }
14
+ return resolved;
15
+ }
16
+
17
+ function requestedMarkets(node) {
18
+ if (node.market != null && node.markets != null) {
19
+ throw new Error('metamask.perps.read_snapshot accepts market or markets, not both.');
20
+ }
21
+ const values = node.markets ?? (node.market == null ? [] : [node.market]);
22
+ if (!Array.isArray(values)) {
23
+ throw new Error('metamask.perps.read_snapshot markets must be an array of market symbols.');
24
+ }
25
+ const markets = Array.from(new Set(
26
+ values.map((value) => typeof value === 'string' ? normalizeMarketSymbol(value) : '').filter(Boolean),
27
+ ));
28
+ if (markets.length > 100) {
29
+ throw new Error('metamask.perps.read_snapshot accepts at most 100 market symbols.');
30
+ }
31
+ return markets;
32
+ }
33
+
34
+ export function resolveSnapshotOptions(input, now = Date.now()) {
35
+ const node = input.node ?? {};
36
+ const include = node.include ?? ALL_SECTIONS;
37
+ if (!Array.isArray(include) || include.length === 0) {
38
+ throw new Error('metamask.perps.read_snapshot include must be a non-empty array.');
39
+ }
40
+ const sections = Array.from(new Set(include.map((section) => String(section).toLowerCase())));
41
+ const unsupported = sections.filter((section) => !ALL_SECTIONS.includes(section));
42
+ if (unsupported.length > 0) {
43
+ throw new Error(`metamask.perps.read_snapshot received unsupported sections: ${unsupported.join(', ')}.`);
44
+ }
45
+
46
+ const history = node.history && typeof node.history === 'object' && !Array.isArray(node.history)
47
+ ? node.history
48
+ : {};
49
+ const endTime = boundedInteger(
50
+ history.end_time_ms,
51
+ now,
52
+ 0,
53
+ Number.MAX_SAFE_INTEGER,
54
+ 'history.end_time_ms',
55
+ );
56
+ const startTime = boundedInteger(
57
+ history.start_time_ms,
58
+ Math.max(0, endTime - DEFAULT_HISTORY_WINDOW_MS),
59
+ 0,
60
+ Number.MAX_SAFE_INTEGER,
61
+ 'history.start_time_ms',
62
+ );
63
+ if (startTime > endTime) {
64
+ throw new Error('metamask.perps.read_snapshot history.start_time_ms must not exceed history.end_time_ms.');
65
+ }
66
+ if (endTime - startTime > MAX_HISTORY_WINDOW_MS) {
67
+ throw new Error('metamask.perps.read_snapshot history range must not exceed 365 days.');
68
+ }
69
+
70
+ return {
71
+ include: sections,
72
+ markets: requestedMarkets(node),
73
+ timeoutMs: boundedInteger(node.timeout_ms, 20000, 1000, 60000, 'timeout_ms'),
74
+ maxItems: boundedInteger(node.max_items_per_section, 100, 1, 500, 'max_items_per_section'),
75
+ history: {
76
+ startTime,
77
+ endTime,
78
+ accountId: history.account_id == null || history.account_id === ''
79
+ ? undefined
80
+ : String(history.account_id),
81
+ limit: boundedInteger(history.limit, 100, 1, 500, 'history.limit'),
82
+ },
83
+ };
84
+ }
85
+
86
+ export function snapshotRequestExpression(options) {
87
+ const methods = {
88
+ account: ['perpsGetAccountState', []],
89
+ markets: ['perpsGetMarketDataWithPrices', []],
90
+ positions: ['perpsGetPositions', []],
91
+ orders: ['perpsGetOpenOrders', []],
92
+ history: ['perpsGetUserHistory', null],
93
+ watchlist: ['perpsGetWatchlistMarkets', []],
94
+ };
95
+ return `(async () => {
96
+ const request = globalThis.stateHooks?.submitRequestToBackground;
97
+ if (typeof request !== 'function') {
98
+ throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot read live Perps snapshot.');
99
+ }
100
+ const timeoutMs = ${JSON.stringify(options.timeoutMs)};
101
+ const historyParams = {
102
+ startTime: ${JSON.stringify(options.history.startTime)},
103
+ endTime: ${JSON.stringify(options.history.endTime)},
104
+ accountId: ${options.history.accountId === undefined ? 'undefined' : JSON.stringify(options.history.accountId)},
105
+ };
106
+ const methods = ${JSON.stringify(methods)};
107
+ const markets = ${JSON.stringify(options.markets)};
108
+ const maxItems = ${JSON.stringify(options.maxItems)};
109
+ const historyLimit = ${JSON.stringify(Math.min(options.history.limit, options.maxItems))};
110
+ function field(record, names) {
111
+ for (const name of names) {
112
+ const value = record?.[name];
113
+ if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) return value;
114
+ }
115
+ return null;
116
+ }
117
+ function marketFor(item) {
118
+ const value = field(item, ['symbol', 'coin', 'market', 'asset']);
119
+ return typeof value === 'string' ? value.replace(/-PERP$/iu, '').toUpperCase() : '';
120
+ }
121
+ function sideFor(item) {
122
+ const side = String(field(item, ['side', 'direction']) ?? '').toLowerCase();
123
+ if (['buy', 'b', 'long'].includes(side)) return 'long';
124
+ if (['sell', 'a', 's', 'short'].includes(side)) return 'short';
125
+ return side || null;
126
+ }
127
+ function leverageFor(item) {
128
+ const leverage = item?.leverage;
129
+ if (leverage === null || ['string', 'number'].includes(typeof leverage)) return leverage;
130
+ if (!leverage || typeof leverage !== 'object' || Array.isArray(leverage)) return null;
131
+ return { type: field(leverage, ['type']), value: field(leverage, ['value']) };
132
+ }
133
+ function normalize(section, item) {
134
+ if (section === 'markets') return {
135
+ market: marketFor(item) || null,
136
+ name: field(item, ['name']),
137
+ price: field(item, ['price', 'currentPrice']),
138
+ markPrice: field(item, ['markPrice', 'markPx']),
139
+ oraclePrice: field(item, ['oraclePrice', 'oraclePx']),
140
+ change24hPercent: field(item, ['change24hPercent', 'priceChange24h']),
141
+ volume24h: field(item, ['volume24h', 'dayNtlVlm']),
142
+ openInterest: field(item, ['openInterest']),
143
+ fundingRate: field(item, ['fundingRate', 'funding']),
144
+ maxLeverage: field(item, ['maxLeverage']),
145
+ };
146
+ if (section === 'positions') return {
147
+ market: marketFor(item) || null,
148
+ side: sideFor(item),
149
+ size: field(item, ['size', 'szi']),
150
+ entryPrice: field(item, ['entryPrice', 'entryPx']),
151
+ positionValue: field(item, ['positionValue']),
152
+ leverage: leverageFor(item),
153
+ liquidationPrice: field(item, ['liquidationPrice', 'liquidationPx']),
154
+ marginUsed: field(item, ['marginUsed']),
155
+ unrealizedPnl: field(item, ['unrealizedPnl']),
156
+ returnOnEquity: field(item, ['returnOnEquity']),
157
+ takeProfitPrice: field(item, ['takeProfitPrice']),
158
+ stopLossPrice: field(item, ['stopLossPrice']),
159
+ };
160
+ if (section === 'orders') return {
161
+ orderId: field(item, ['orderId', 'id', 'oid']),
162
+ market: marketFor(item) || null,
163
+ side: sideFor(item),
164
+ size: field(item, ['size', 'sz', 'szi']),
165
+ price: field(item, ['price', 'limitPx', 'px']),
166
+ type: field(item, ['orderType', 'type']),
167
+ status: field(item, ['status']),
168
+ timestamp: field(item, ['timestamp', 'time']),
169
+ reduceOnly: field(item, ['reduceOnly']),
170
+ isTrigger: field(item, ['isTrigger']),
171
+ triggerPrice: field(item, ['triggerPrice', 'triggerPx']),
172
+ triggerOrderType: field(item, ['triggerOrderType']),
173
+ takeProfitPrice: field(item, ['takeProfitPrice']),
174
+ stopLossPrice: field(item, ['stopLossPrice']),
175
+ };
176
+ return {
177
+ type: field(item, ['type', 'activityType']),
178
+ status: field(item, ['status']),
179
+ asset: field(item, ['asset', 'token', 'currency']),
180
+ market: marketFor(item) || null,
181
+ amount: field(item, ['amount', 'value']),
182
+ fee: field(item, ['fee']),
183
+ timestamp: field(item, ['timestamp', 'time']),
184
+ transactionHash: field(item, ['transactionHash', 'txHash', 'hash']),
185
+ };
186
+ }
187
+ function transport(section, value) {
188
+ if (section === 'account') {
189
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return { value };
190
+ const allowed = ${JSON.stringify([
191
+ 'accountValue', 'totalBalance', 'balance', 'spendableBalance',
192
+ 'withdrawableBalance', 'availableBalance', 'availableToTradeBalance',
193
+ 'marginUsed', 'maintenanceMargin', 'unrealizedPnl', 'returnOnEquity',
194
+ ])};
195
+ return { value: Object.fromEntries(allowed.map((name) => [name, field(value, [name])]).filter(([, item]) => item !== null)) };
196
+ }
197
+ if (!Array.isArray(value)) return { value };
198
+ if (section === 'watchlist') {
199
+ const items = value.map((item) => typeof item === 'string' ? item : marketFor(item)).filter(Boolean);
200
+ const selected = markets.length === 0 ? items : items.filter((item) => markets.includes(String(item).replace(/-PERP$/iu, '').toUpperCase()));
201
+ const bounded = selected.slice(0, maxItems).map((item) => String(item).replace(/-PERP$/iu, '').toUpperCase());
202
+ return { value: bounded, totalCount: value.length, matchingCount: selected.length, truncated: bounded.length < selected.length, transportBound: true };
203
+ }
204
+ const selected = markets.length === 0 ? value : value.filter((item) => markets.includes(marketFor(item)));
205
+ const limit = section === 'history' ? historyLimit : maxItems;
206
+ const bounded = selected.slice(0, limit).map((item) => normalize(section, item));
207
+ return { value: bounded, totalCount: value.length, matchingCount: selected.length, truncated: bounded.length < selected.length, transportBound: true };
208
+ }
209
+ async function read(section) {
210
+ const [method, configuredArgs] = methods[section];
211
+ const args = section === 'history' ? [historyParams] : configuredArgs;
212
+ let timeoutId;
213
+ try {
214
+ const value = await Promise.race([
215
+ request(method, args),
216
+ new Promise((_, reject) => {
217
+ timeoutId = setTimeout(
218
+ () => reject(new Error(method + ' timed out after ' + timeoutMs + 'ms')),
219
+ timeoutMs,
220
+ );
221
+ }),
222
+ ]);
223
+ return [section, { ok: true, method, ...transport(section, value) }];
224
+ } catch (error) {
225
+ return [section, {
226
+ ok: false,
227
+ method,
228
+ error: error instanceof Error ? error.message : String(error),
229
+ }];
230
+ } finally {
231
+ clearTimeout(timeoutId);
232
+ }
233
+ }
234
+ return Object.fromEntries(await Promise.all(${JSON.stringify(options.include)}.map(read)));
235
+ })()`;
236
+ }
237
+
238
+ function field(record, names) {
239
+ for (const name of names) {
240
+ const value = record?.[name];
241
+ if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) return value;
242
+ }
243
+ return null;
244
+ }
245
+
246
+ function normalizedSide(item) {
247
+ const side = String(field(item, ['side', 'direction']) ?? '').toLowerCase();
248
+ if (side === 'buy' || side === 'b' || side === 'long') return 'long';
249
+ if (side === 'sell' || side === 'a' || side === 's' || side === 'short') return 'short';
250
+ return side || null;
251
+ }
252
+
253
+ function normalizedLeverage(item) {
254
+ const leverage = item?.leverage;
255
+ if (leverage === null || ['string', 'number'].includes(typeof leverage)) return leverage;
256
+ if (!leverage || typeof leverage !== 'object' || Array.isArray(leverage)) return null;
257
+ return {
258
+ type: field(leverage, ['type']),
259
+ value: field(leverage, ['value']),
260
+ };
261
+ }
262
+
263
+ function marketForItem(item) {
264
+ const value = field(item, ['symbol', 'coin', 'market', 'asset']);
265
+ return typeof value === 'string' && value.length > 0 ? normalizeMarketSymbol(value) : '';
266
+ }
267
+
268
+ function normalizeAccount(account) {
269
+ const allowed = [
270
+ 'accountValue',
271
+ 'totalBalance',
272
+ 'balance',
273
+ 'spendableBalance',
274
+ 'withdrawableBalance',
275
+ 'availableBalance',
276
+ 'availableToTradeBalance',
277
+ 'marginUsed',
278
+ 'maintenanceMargin',
279
+ 'unrealizedPnl',
280
+ 'returnOnEquity',
281
+ ];
282
+ return Object.fromEntries(
283
+ allowed
284
+ .map((name) => [name, field(account, [name])])
285
+ .filter(([, value]) => value !== null),
286
+ );
287
+ }
288
+
289
+ function normalizeMarket(item) {
290
+ return {
291
+ market: marketForItem(item) || null,
292
+ name: field(item, ['name']),
293
+ price: field(item, ['price', 'currentPrice']),
294
+ markPrice: field(item, ['markPrice', 'markPx']),
295
+ oraclePrice: field(item, ['oraclePrice', 'oraclePx']),
296
+ change24hPercent: field(item, ['change24hPercent', 'priceChange24h']),
297
+ volume24h: field(item, ['volume24h', 'dayNtlVlm']),
298
+ openInterest: field(item, ['openInterest']),
299
+ fundingRate: field(item, ['fundingRate', 'funding']),
300
+ maxLeverage: field(item, ['maxLeverage']),
301
+ };
302
+ }
303
+
304
+ function normalizePosition(item) {
305
+ return {
306
+ market: marketForItem(item) || null,
307
+ side: normalizedSide(item),
308
+ size: field(item, ['size', 'szi']),
309
+ entryPrice: field(item, ['entryPrice', 'entryPx']),
310
+ positionValue: field(item, ['positionValue']),
311
+ leverage: normalizedLeverage(item),
312
+ liquidationPrice: field(item, ['liquidationPrice', 'liquidationPx']),
313
+ marginUsed: field(item, ['marginUsed']),
314
+ unrealizedPnl: field(item, ['unrealizedPnl']),
315
+ returnOnEquity: field(item, ['returnOnEquity']),
316
+ takeProfitPrice: field(item, ['takeProfitPrice']),
317
+ stopLossPrice: field(item, ['stopLossPrice']),
318
+ };
319
+ }
320
+
321
+ function normalizeOrder(item) {
322
+ return {
323
+ orderId: field(item, ['orderId', 'id', 'oid']),
324
+ market: marketForItem(item) || null,
325
+ side: normalizedSide(item),
326
+ size: field(item, ['size', 'sz', 'szi']),
327
+ price: field(item, ['price', 'limitPx', 'px']),
328
+ type: field(item, ['orderType', 'type']),
329
+ status: field(item, ['status']),
330
+ timestamp: field(item, ['timestamp', 'time']),
331
+ reduceOnly: field(item, ['reduceOnly']),
332
+ isTrigger: field(item, ['isTrigger']),
333
+ triggerPrice: field(item, ['triggerPrice', 'triggerPx']),
334
+ triggerOrderType: field(item, ['triggerOrderType']),
335
+ takeProfitPrice: field(item, ['takeProfitPrice']),
336
+ stopLossPrice: field(item, ['stopLossPrice']),
337
+ };
338
+ }
339
+
340
+ function normalizeHistoryItem(item) {
341
+ return {
342
+ type: field(item, ['type', 'activityType']),
343
+ status: field(item, ['status']),
344
+ asset: field(item, ['asset', 'token', 'currency']),
345
+ market: marketForItem(item) || null,
346
+ amount: field(item, ['amount', 'value']),
347
+ fee: field(item, ['fee']),
348
+ timestamp: field(item, ['timestamp', 'time']),
349
+ transactionHash: field(item, ['transactionHash', 'txHash', 'hash']),
350
+ };
351
+ }
352
+
353
+ function selectAndBound(items, options, normalize, limit = options.maxItems, result = {}) {
354
+ const selected = result.transportBound === true || options.markets.length === 0
355
+ ? items
356
+ : items.filter((item) => options.markets.includes(marketForItem(item)));
357
+ const bounded = selected.slice(0, Math.min(limit, options.maxItems));
358
+ return {
359
+ totalCount: result.transportBound === true ? result.totalCount : items.length,
360
+ matchingCount: result.transportBound === true ? result.matchingCount : selected.length,
361
+ returnedCount: bounded.length,
362
+ truncated: result.transportBound === true ? result.truncated === true : bounded.length < selected.length,
363
+ items: bounded.map(normalize),
364
+ };
365
+ }
366
+
367
+ function requireObjectSection(section, result) {
368
+ if (!result?.ok) {
369
+ throw new Error(`Requested Perps snapshot section ${section} is unavailable: ${result?.error ?? 'unknown error'}.`);
370
+ }
371
+ if (!result.value || typeof result.value !== 'object' || Array.isArray(result.value)) {
372
+ throw new Error(`Requested Perps snapshot section ${section} is unavailable: ${result.method} returned a non-object result.`);
373
+ }
374
+ return result.value;
375
+ }
376
+
377
+ function requireArraySection(section, result) {
378
+ if (!result?.ok) {
379
+ throw new Error(`Requested Perps snapshot section ${section} is unavailable: ${result?.error ?? 'unknown error'}.`);
380
+ }
381
+ if (!Array.isArray(result.value)) {
382
+ throw new Error(`Requested Perps snapshot section ${section} is unavailable: ${result.method} returned a non-array result.`);
383
+ }
384
+ return result.value;
385
+ }
386
+
387
+ export function normalizeSnapshotResult(raw, options) {
388
+ const sections = {};
389
+ for (const section of options.include) {
390
+ const result = raw?.[section];
391
+ if (section === 'account') {
392
+ sections.account = {
393
+ source: result?.method ?? 'perpsGetAccountState',
394
+ state: normalizeAccount(requireObjectSection(section, result)),
395
+ };
396
+ continue;
397
+ }
398
+ if (section === 'watchlist') {
399
+ const values = requireArraySection(section, result);
400
+ const items = values
401
+ .map((item) => typeof item === 'string' ? item : marketForItem(item))
402
+ .filter((item) => typeof item === 'string' && item.length > 0);
403
+ const selected = result.transportBound === true || options.markets.length === 0
404
+ ? items
405
+ : items.filter((item) => options.markets.includes(normalizeMarketSymbol(item)));
406
+ const bounded = selected
407
+ .slice(0, options.maxItems)
408
+ .map((item) => typeof item === 'string' && item.length > 0 ? normalizeMarketSymbol(item) : '')
409
+ .filter(Boolean);
410
+ sections.watchlist = {
411
+ source: result.method,
412
+ totalCount: result.transportBound === true ? result.totalCount : values.length,
413
+ matchingCount: result.transportBound === true ? result.matchingCount : selected.length,
414
+ returnedCount: bounded.length,
415
+ truncated: result.transportBound === true ? result.truncated === true : bounded.length < selected.length,
416
+ markets: bounded,
417
+ };
418
+ continue;
419
+ }
420
+ const values = requireArraySection(section, result);
421
+ const normalizer = {
422
+ markets: normalizeMarket,
423
+ positions: normalizePosition,
424
+ orders: normalizeOrder,
425
+ history: normalizeHistoryItem,
426
+ }[section];
427
+ sections[section] = {
428
+ source: result.method,
429
+ ...selectAndBound(
430
+ values,
431
+ options,
432
+ normalizer,
433
+ section === 'history' ? options.history.limit : options.maxItems,
434
+ result,
435
+ ),
436
+ };
437
+ }
438
+ return sections;
439
+ }
440
+
441
+ export async function readSnapshot(input) {
442
+ const options = resolveSnapshotOptions(input);
443
+ return withExtensionPage(input, async (page) => {
444
+ const raw = await page.evaluate(snapshotRequestExpression(options));
445
+ return {
446
+ action: input.action,
447
+ source: 'extension-stateHooks-background',
448
+ include: options.include,
449
+ markets: options.markets,
450
+ historyQuery: options.include.includes('history')
451
+ ? {
452
+ startTime: options.history.startTime,
453
+ endTime: options.history.endTime,
454
+ accountScoped: options.history.accountId !== undefined,
455
+ }
456
+ : null,
457
+ sections: normalizeSnapshotResult(raw, options),
458
+ redacted: true,
459
+ proofPath: 'background-perps-snapshot',
460
+ };
461
+ });
462
+ }
463
+
464
+ function isDirectRun() {
465
+ return process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
466
+ }
467
+
468
+ if (isDirectRun()) {
469
+ runAdapter(readSnapshot);
470
+ }
@@ -67,9 +67,12 @@ export function extensionTarget(targets, extensionId = null) {
67
67
  if (extensionId && extensionIdFromTarget(target) !== extensionId) return false;
68
68
  return Boolean(target.webSocketDebuggerUrl);
69
69
  });
70
- return candidates.find((target) => String(target.url).includes('/home.html')) ??
71
- candidates.find((target) => String(target.url).includes('/sidepanel.html')) ??
72
- null;
70
+ const home = candidates.find((target) => String(target.url).includes('/home.html'));
71
+ const sidepanel = candidates.find((target) => String(target.url).includes('/sidepanel.html'));
72
+ if (home && String(home.url).includes('/onboarding/completion') && sidepanel) {
73
+ return sidepanel;
74
+ }
75
+ return home ?? sidepanel ?? null;
73
76
  }
74
77
 
75
78
 
@@ -0,0 +1,201 @@
1
+ import { resolveWalletImportCredentials, validateWalletImportOptions, walletImportMethod } from '../../shared/wallet/import-source.mjs';
2
+ import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
3
+ import { pasteSecretRecoveryPhrase, setSecretInput } from './secret-input.mjs';
4
+ import { readWalletStateExpression } from './state.mjs';
5
+
6
+ const SELECTORS = {
7
+ importWallet: '[data-testid="onboarding-import-wallet"]',
8
+ importWithSrp: '[data-testid="onboarding-import-with-srp-button"]',
9
+ srp: '[data-testid="srp-input-import__srp-note"]',
10
+ confirmSrp: '[data-testid="import-srp-confirm"]',
11
+ password: '[data-testid="create-password-new-input"]',
12
+ confirmPassword: '[data-testid="create-password-confirm-input"]',
13
+ passwordTerms: '[data-testid="create-password-terms"]',
14
+ createPassword: '[data-testid="create-password-submit"]',
15
+ skipPasskey: '[data-testid="passkey-maybe-later-button"]',
16
+ metricsCheckbox: '[data-testid="metametrics-checkbox"]',
17
+ metricsContinue: '[data-testid="metametrics-i-agree"]',
18
+ complete: '[data-testid="onboarding-complete-done"]',
19
+ };
20
+
21
+ async function hasSelector(page, selector) {
22
+ try {
23
+ return await page.evaluate(`Boolean(document.querySelector(${JSON.stringify(selector)}))`);
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ async function waitForSelector(page, selector, timeoutMs) {
30
+ const deadline = Date.now() + timeoutMs;
31
+ while (Date.now() <= deadline) {
32
+ if (await hasSelector(page, selector)) return;
33
+ await new Promise((resolve) => setTimeout(resolve, 100));
34
+ }
35
+ throw new Error(`Wallet import timed out waiting for ${selector}.`);
36
+ }
37
+
38
+ async function waitForSelectorToDisappear(page, selector, timeoutMs) {
39
+ const deadline = Date.now() + timeoutMs;
40
+ while (Date.now() <= deadline) {
41
+ if (!(await hasSelector(page, selector))) return;
42
+ await new Promise((resolve) => setTimeout(resolve, 100));
43
+ }
44
+ throw new Error(`Wallet import timed out waiting for ${selector} to disappear.`);
45
+ }
46
+
47
+ async function waitForFirstSelector(page, selectors, timeoutMs) {
48
+ const deadline = Date.now() + timeoutMs;
49
+ while (Date.now() <= deadline) {
50
+ for (const selector of selectors) {
51
+ if (await hasSelector(page, selector)) return selector;
52
+ }
53
+ await new Promise((resolve) => setTimeout(resolve, 100));
54
+ }
55
+ throw new Error('Wallet import timed out waiting for the next onboarding screen.');
56
+ }
57
+
58
+ async function waitForEnabled(page, selector, timeoutMs) {
59
+ const deadline = Date.now() + timeoutMs;
60
+ while (Date.now() <= deadline) {
61
+ const enabled = await page.evaluate(`(() => {
62
+ const element = document.querySelector(${JSON.stringify(selector)});
63
+ return Boolean(element && !element.disabled && element.getAttribute('aria-disabled') !== 'true');
64
+ })()`).catch(() => false);
65
+ if (enabled) return;
66
+ await new Promise((resolve) => setTimeout(resolve, 100));
67
+ }
68
+ throw new Error(`Wallet import timed out waiting for ${selector} to become enabled.`);
69
+ }
70
+
71
+ async function readState(page) {
72
+ return page.evaluate(readWalletStateExpression(), { awaitPromise: true });
73
+ }
74
+
75
+ async function unlockProfile(page, credentials, timeoutMs) {
76
+ await setSecretInput(page, 'input[type="password"]', credentials.password);
77
+ await page.click('button[type="submit"]');
78
+ await waitForSelectorToDisappear(page, 'input[type="password"]', timeoutMs);
79
+ }
80
+
81
+ async function importThroughUi(page, input, timeoutMs) {
82
+ const credentials = await resolveWalletImportCredentials(input);
83
+ const metametricsEnabled = input.node?.metametrics === true || input.node?.metametrics === 'true';
84
+ const interests = Array.isArray(input.node?.interests) ? input.node.interests : [];
85
+ if (interests.length > 0) {
86
+ throw new Error('metamask.wallet.import interests are supported only on Mobile.');
87
+ }
88
+ if (!(await hasSelector(page, SELECTORS.srp))) {
89
+ await page.click(SELECTORS.importWallet);
90
+ const importChoice = await waitForFirstSelector(
91
+ page,
92
+ [SELECTORS.importWithSrp, SELECTORS.srp],
93
+ timeoutMs,
94
+ );
95
+ if (importChoice === SELECTORS.importWithSrp) {
96
+ await page.click(SELECTORS.importWithSrp);
97
+ }
98
+ await waitForSelector(page, SELECTORS.srp, timeoutMs);
99
+ }
100
+ await pasteSecretRecoveryPhrase(page, SELECTORS.srp, credentials.mnemonic, timeoutMs);
101
+ await waitForEnabled(page, SELECTORS.confirmSrp, timeoutMs);
102
+ await page.click(SELECTORS.confirmSrp);
103
+ await waitForSelector(page, SELECTORS.password, timeoutMs);
104
+ await setSecretInput(page, SELECTORS.password, credentials.password);
105
+ await setSecretInput(page, SELECTORS.confirmPassword, credentials.password);
106
+ await page.click(SELECTORS.passwordTerms);
107
+ await waitForEnabled(page, SELECTORS.createPassword, timeoutMs);
108
+ await page.click(SELECTORS.createPassword);
109
+
110
+ await waitForFirstSelector(
111
+ page,
112
+ [SELECTORS.skipPasskey, SELECTORS.metricsContinue],
113
+ timeoutMs,
114
+ );
115
+ if (await hasSelector(page, SELECTORS.skipPasskey)) {
116
+ await page.click(SELECTORS.skipPasskey);
117
+ await waitForSelector(page, SELECTORS.metricsContinue, timeoutMs);
118
+ }
119
+ const metricsChecked = await page.evaluate(`(() => {
120
+ const checkbox = document.querySelector(${JSON.stringify(SELECTORS.metricsCheckbox)});
121
+ return checkbox?.checked === true ||
122
+ checkbox?.getAttribute('data-checked') === 'true' ||
123
+ checkbox?.getAttribute('aria-checked') === 'true';
124
+ })()`);
125
+ if (metricsChecked !== metametricsEnabled) await page.click(SELECTORS.metricsCheckbox);
126
+ await page.click(SELECTORS.metricsContinue);
127
+ await waitForSelector(page, SELECTORS.complete, timeoutMs);
128
+ await page.click(SELECTORS.complete);
129
+
130
+ const deadline = Date.now() + timeoutMs;
131
+ let state = await readState(page);
132
+ while (
133
+ Date.now() <= deadline &&
134
+ (!state.completedOnboarding || !state.selectedAccount?.address)
135
+ ) {
136
+ await new Promise((resolve) => setTimeout(resolve, 100));
137
+ state = await readState(page);
138
+ }
139
+ if (!state.completedOnboarding || !state.selectedAccount?.address) {
140
+ throw new Error('Extension wallet import did not complete onboarding with a selected account.');
141
+ }
142
+ if (state.metametricsOptedIn !== metametricsEnabled) {
143
+ throw new Error(`Extension MetaMetrics preference was ${String(state.metametricsOptedIn)}, expected ${String(metametricsEnabled)}.`);
144
+ }
145
+ const actualAddress = String(state.selectedAccount?.address ?? '').toLowerCase();
146
+ if (actualAddress !== credentials.expectedAddress) {
147
+ throw new Error(`Wallet import selected ${actualAddress || 'no address'}, expected ${credentials.expectedAddress}.`);
148
+ }
149
+ return { credentials, state, metametricsEnabled };
150
+ }
151
+
152
+ runAdapter((input) => withExtensionPage(input, async (page) => {
153
+ validateWalletImportOptions('extension', input.node);
154
+ const method = walletImportMethod(input);
155
+ const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
156
+ const before = await readState(page);
157
+ const onboarding = String(before.href ?? '').includes('onboarding');
158
+
159
+ if (method === 'ui' && !onboarding) {
160
+ throw new Error('metamask.wallet.import method=ui requires a fresh Extension onboarding profile.');
161
+ }
162
+ if (onboarding) {
163
+ if (method === 'profile') {
164
+ throw new Error('metamask.wallet.import method=profile found a fresh Extension onboarding profile.');
165
+ }
166
+ const imported = await importThroughUi(page, input, timeoutMs);
167
+ return {
168
+ action: input.action,
169
+ method: 'ui-import',
170
+ credentialSource: imported.credentials.source,
171
+ credentialSourceName: imported.credentials.sourceName,
172
+ selectedAddress: imported.state.selectedAccount.address,
173
+ completedOnboarding: imported.state.completedOnboarding,
174
+ metametricsEnabled: imported.metametricsEnabled,
175
+ interests: [],
176
+ redacted: true,
177
+ proofPath: 'extension-visible-onboarding-import',
178
+ };
179
+ }
180
+
181
+ const credentials = await resolveWalletImportCredentials(input);
182
+ if (before.passwordInput) await unlockProfile(page, credentials, timeoutMs);
183
+ const after = await readState(page);
184
+ if (!after.completedOnboarding || !after.selectedAccount?.address || after.passwordInput) {
185
+ throw new Error('Extension fixture-backed wallet is not ready after profile verification.');
186
+ }
187
+ const actualAddress = String(after.selectedAccount.address).toLowerCase();
188
+ if (actualAddress !== credentials.expectedAddress) {
189
+ throw new Error(`Wallet profile selected ${actualAddress}, expected ${credentials.expectedAddress}.`);
190
+ }
191
+ return {
192
+ action: input.action,
193
+ method: 'profile',
194
+ credentialSource: credentials.source,
195
+ credentialSourceName: credentials.sourceName,
196
+ selectedAddress: after.selectedAccount.address,
197
+ completedOnboarding: after.completedOnboarding,
198
+ redacted: true,
199
+ proofPath: 'extension-fixture-profile',
200
+ };
201
+ }));