@deeeed/metamask-harness 0.28.0 → 0.29.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 (52) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +41 -0
  3. package/adapters/extension/build-lavamoat.sh +2 -1
  4. package/adapters/extension/ensure-browser.sh +82 -9
  5. package/adapters/extension/inject.mjs +1 -0
  6. package/adapters/extension/launch-browser.cjs +83 -1
  7. package/adapters/extension/lib/chrome-args.cjs +325 -1
  8. package/adapters/extension/lib/playwright-cdp.cjs +34 -0
  9. package/adapters/extension/lib/slot-title.cjs +2 -4
  10. package/adapters/extension/lib/validation-launch-supervisor.cjs +292 -0
  11. package/adapters/extension/lib/validation-process-ownership.cjs +69 -0
  12. package/adapters/extension/reattach.sh +2 -1
  13. package/adapters/extension/sidepanel-toggle.sh +14 -96
  14. package/adapters/extension/wallet-fixture-state.cjs +8 -31
  15. package/adapters/manifest.json +16 -0
  16. package/adapters/shared/private-atomic-write.cjs +47 -0
  17. package/adapters/shared/setup-base.sh +864 -0
  18. package/dist/adapters/extension/runtime.js +367 -24
  19. package/dist/adapters/extension/validation-process-ownership.js +10 -0
  20. package/dist/cli-commands.js +1 -0
  21. package/dist/command-contract.js +12 -0
  22. package/dist/commands/launch/extension.js +130 -19
  23. package/dist/commands/setup-base.js +24 -0
  24. package/dist/mm-harness-cli.js +28 -2
  25. package/library/actions/extension/analytics/consent.mjs +203 -0
  26. package/library/actions/extension/analytics/set_consent.mjs +19 -143
  27. package/library/actions/extension/perps/perps.mjs +2 -16
  28. package/library/actions/extension/perps/state.mjs +20 -0
  29. package/library/actions/extension/wallet/list_accounts.mjs +3 -25
  30. package/library/actions/extension/wallet/read_state.mjs +3 -23
  31. package/library/actions/extension/wallet/select_account.mjs +6 -33
  32. package/library/actions/extension/wallet/setup.mjs +2 -20
  33. package/library/actions/extension/wallet/state.mjs +111 -0
  34. package/library/recipes/runner/action-validation.extension.recipe.json +1 -1
  35. package/library/recipes/runner/action-validation.mobile.recipe.json +1 -1
  36. package/package.json +7 -4
  37. package/scripts/site-contrast.mjs +538 -0
  38. package/site/architecture.html +415 -0
  39. package/site/assets/progress.mjs +272 -0
  40. package/site/assets/style.css +808 -0
  41. package/site/cheatsheet.html +305 -0
  42. package/site/index.html +643 -0
  43. package/site/recipes.html +396 -0
  44. package/site/reviewers.html +374 -0
  45. package/site/tutorials/index.html +180 -0
  46. package/site/tutorials/v1.html +211 -0
  47. package/site/tutorials/v2.html +207 -0
  48. package/site/tutorials/v3.html +214 -0
  49. package/site/tutorials/v4.html +195 -0
  50. package/site/tutorials/v5.html +163 -0
  51. package/site/tutorials/v6.html +165 -0
  52. package/site/tutorials/v7.html +184 -0
@@ -1,5 +1,6 @@
1
1
  import { pathToFileURL } from 'node:url';
2
2
  import { dataTestId, marketSymbol, normalizeMarketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
3
+ import { readPerpsRuntimeStateExpression } from './state.mjs';
3
4
 
4
5
  function sleep(ms) {
5
6
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -704,22 +705,7 @@ async function applyStateNavigation(input, config) {
704
705
 
705
706
 
706
707
  async function readPerpsRuntimeState(page) {
707
- return page.evaluate(`(async () => {
708
- const hooks = globalThis.stateHooks || {};
709
- const request = hooks.submitRequestToBackground;
710
- const reduxState = hooks.store?.getState?.() || {};
711
- const cleanState = (await hooks.getCleanAppState?.()) || hooks.getState?.() || {};
712
- const metamask = reduxState.metamask || cleanState.metamask || {};
713
- const nestedPerps = metamask.PerpsController || cleanState.PerpsController || cleanState.perps || {};
714
- return {
715
- available: typeof request === 'function',
716
- activeProvider: metamask.activeProvider || nestedPerps.activeProvider || 'hyperliquid',
717
- networkKnown: typeof nestedPerps.isTestnet === 'boolean',
718
- isTestnet: Boolean(nestedPerps.isTestnet),
719
- initializationState: metamask.initializationState || nestedPerps.initializationState || null,
720
- isEligible: Boolean(metamask.isEligible ?? nestedPerps.isEligible),
721
- };
722
- })()`);
708
+ return page.evaluate(readPerpsRuntimeStateExpression());
723
709
  }
724
710
 
725
711
  async function ensureProvider(input, config) {
@@ -0,0 +1,20 @@
1
+ export function readPerpsRuntimeStateExpression() {
2
+ return `(async () => {
3
+ const hooks = globalThis.stateHooks || {};
4
+ const request = hooks.submitRequestToBackground;
5
+ const reduxState = hooks.store?.getState?.() || {};
6
+ const cleanState = (await hooks.getCleanAppState?.()) || hooks.getState?.() || {};
7
+ const reduxMetamask = reduxState.metamask || {};
8
+ const cleanMetamask = cleanState.metamask || {};
9
+ const metamask = { ...cleanMetamask, ...reduxMetamask };
10
+ const nestedPerps = reduxMetamask.PerpsController || cleanMetamask.PerpsController || cleanState.PerpsController || cleanState.perps || {};
11
+ return {
12
+ available: typeof request === 'function',
13
+ activeProvider: metamask.activeProvider || nestedPerps.activeProvider || 'hyperliquid',
14
+ networkKnown: typeof metamask.isTestnet === 'boolean' || typeof nestedPerps.isTestnet === 'boolean',
15
+ isTestnet: Boolean(metamask.isTestnet ?? nestedPerps.isTestnet),
16
+ initializationState: metamask.initializationState || nestedPerps.initializationState || null,
17
+ isEligible: Boolean(metamask.isEligible ?? nestedPerps.isEligible),
18
+ };
19
+ })()`;
20
+ }
@@ -1,37 +1,15 @@
1
1
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+ import { listWalletAccountsExpression } from './state.mjs';
2
3
 
3
4
  runAdapter((input) => withExtensionPage(input, async (page) => {
4
5
  const scope = input.node?.scope == null ? 'named' : String(input.node.scope);
5
6
  if (scope !== 'selected' && scope !== 'named' && scope !== 'evm' && scope !== 'all') {
6
7
  throw new Error('metamask.wallet.list_accounts scope must be selected, named, evm, or all.');
7
8
  }
8
- const result = await page.evaluate(`(() => {
9
- const store = globalThis.stateHooks?.store;
10
- if (!store || typeof store.getState !== 'function') {
11
- throw new Error('stateHooks.store.getState is unavailable.');
12
- }
13
- const internal = store.getState()?.metamask?.internalAccounts || {};
14
- const selectedId = internal.selectedAccount || null;
15
- const scope = ${JSON.stringify(scope)};
16
- const accounts = Object.entries(internal.accounts || {})
17
- .map(([id, account]) => ({
18
- id,
19
- address: account?.address || null,
20
- name: account?.metadata?.name || null,
21
- type: account?.type || null,
22
- selected: id === selectedId
23
- }))
24
- .filter((account) => {
25
- if (scope === 'selected') return account.selected;
26
- if (scope === 'all') return true;
27
- if (scope === 'evm') return /^0x[0-9a-f]{40}$/iu.test(account.address || '');
28
- return account.selected || Boolean(String(account.name || '').trim());
29
- });
30
- return { accounts };
31
- })()`);
9
+ const result = await page.evaluate(listWalletAccountsExpression(scope), { awaitPromise: true });
32
10
  return {
33
11
  action: input.action,
34
- source: 'extension-stateHooks-store',
12
+ source: 'extension-stateHooks-clean-state',
35
13
  scope,
36
14
  ...result,
37
15
  count: result.accounts.length,
@@ -1,27 +1,7 @@
1
1
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+ import { readWalletStateExpression } from './state.mjs';
2
3
 
3
4
  runAdapter((input) => withExtensionPage(input, async (page) => {
4
- const state = await page.evaluate(`(() => {
5
- const hooks = globalThis.stateHooks;
6
- const store = hooks && hooks.store;
7
- const root = store && typeof store.getState === 'function' ? store.getState() : {};
8
- const metamask = root.metamask || {};
9
- const internal = metamask.internalAccounts || {};
10
- const accounts = internal.accounts || {};
11
- const selectedId = internal.selectedAccount || null;
12
- const selected = selectedId && accounts[selectedId] ? accounts[selectedId] : null;
13
- const metadata = selected && selected.metadata ? selected.metadata : {};
14
- return {
15
- href: String(globalThis.location && globalThis.location.href || ''),
16
- selectedAccount: selected ? {
17
- id: selectedId,
18
- address: selected.address || null,
19
- name: metadata.name || null,
20
- type: selected.type || null
21
- } : null,
22
- completedOnboarding: Boolean(metamask.completedOnboarding),
23
- selectedNetworkClientId: metamask.selectedNetworkClientId || null
24
- };
25
- })()`);
26
- return { action: input.action, source: 'extension-stateHooks-store', state, redacted: true, proofPath: 'extension-wallet-state' };
5
+ const state = await page.evaluate(readWalletStateExpression(), { awaitPromise: true });
6
+ return { action: input.action, source: 'extension-stateHooks-clean-state', state, redacted: true, proofPath: 'extension-wallet-state' };
27
7
  }));
@@ -1,4 +1,5 @@
1
1
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+ import { selectWalletAccountExpression } from './state.mjs';
2
3
 
3
4
  function normalize(value) {
4
5
  return String(value ?? '').toLowerCase();
@@ -11,38 +12,10 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
11
12
  if (!requestedAddress && !requestedId && !requestedName) {
12
13
  throw new Error('metamask.wallet.select_account extension live adapter requires node.address, node.id, or node.name.');
13
14
  }
14
- const selected = await page.evaluate(`(async () => {
15
- const hooks = globalThis.stateHooks;
16
- const store = hooks && hooks.store;
17
- const request = hooks && hooks.submitRequestToBackground;
18
- if (!store || typeof store.getState !== 'function') throw new Error('stateHooks.store.getState is unavailable.');
19
- if (typeof request !== 'function') throw new Error('stateHooks.submitRequestToBackground is unavailable.');
20
- const state = store.getState() || {};
21
- const internal = state.metamask?.internalAccounts || {};
22
- const accounts = internal.accounts || {};
23
- const requestedAddress = ${JSON.stringify(requestedAddress)};
24
- const requestedId = ${JSON.stringify(requestedId)};
25
- const requestedName = ${JSON.stringify(requestedName)};
26
- const entries = Object.entries(accounts);
27
- const match = entries.find(([id, account]) => {
28
- const metadata = account?.metadata || {};
29
- return (requestedId && id === requestedId) ||
30
- (requestedAddress && String(account?.address || '').toLowerCase() === requestedAddress) ||
31
- (requestedName && metadata.name === requestedName);
32
- });
33
- if (!match) throw new Error('Requested account was not found in internalAccounts.');
34
- const [id, account] = match;
35
- await request('setSelectedInternalAccount', [id]);
36
- const after = store.getState()?.metamask?.internalAccounts || {};
37
- const selectedId = after.selectedAccount;
38
- const selected = after.accounts?.[selectedId];
39
- if (selectedId !== id) throw new Error('Expected selected account ' + id + ', got ' + selectedId);
40
- return {
41
- id: selectedId,
42
- address: selected?.address || null,
43
- name: selected?.metadata?.name || null,
44
- type: selected?.type || null
45
- };
46
- })()`, { awaitPromise: true });
15
+ const selected = await page.evaluate(selectWalletAccountExpression({
16
+ address: requestedAddress,
17
+ id: requestedId,
18
+ name: requestedName,
19
+ }), { awaitPromise: true });
47
20
  return { action: input.action, selected, proofPath: 'extension-account-selection' };
48
21
  }));
@@ -1,26 +1,8 @@
1
1
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+ import { readWalletStateExpression } from './state.mjs';
2
3
 
3
4
  runAdapter((input) => withExtensionPage(input, async (page) => {
4
- const state = await page.evaluate(`(() => {
5
- const passwordInput = Boolean(document.querySelector('input[type="password"]'));
6
- const hooks = globalThis.stateHooks;
7
- const store = hooks && hooks.store;
8
- const root = store && typeof store.getState === 'function' ? store.getState() : {};
9
- const metamask = root.metamask || {};
10
- const internal = metamask.internalAccounts || {};
11
- const accounts = internal.accounts || {};
12
- const selectedId = internal.selectedAccount || null;
13
- const selected = selectedId && accounts[selectedId] ? accounts[selectedId] : null;
14
- return {
15
- href: String(globalThis.location && globalThis.location.href || ''),
16
- passwordInput,
17
- completedOnboarding: Boolean(metamask.completedOnboarding),
18
- selectedAccount: selected ? {
19
- id: selectedId,
20
- address: selected.address || null,
21
- } : null,
22
- };
23
- })()`);
5
+ const state = await page.evaluate(readWalletStateExpression(), { awaitPromise: true });
24
6
  const seededProfilePresent = Boolean(state?.passwordInput || (state?.completedOnboarding && state?.selectedAccount?.address));
25
7
  if (!seededProfilePresent) {
26
8
  throw new Error(`Extension fixture profile is not ready; expected a locked password screen or completed onboarding with a selected account, got ${JSON.stringify(state)}`);
@@ -0,0 +1,111 @@
1
+ const walletStatePreamble = `
2
+ const readWalletState = async () => {
3
+ const hooks = globalThis.stateHooks || {};
4
+ let root = null;
5
+ if (typeof hooks.getCleanAppState === 'function') {
6
+ root = await hooks.getCleanAppState();
7
+ }
8
+ if ((!root || typeof root !== 'object') && typeof hooks.store?.getState === 'function') {
9
+ root = hooks.store.getState();
10
+ }
11
+ if (!root || typeof root !== 'object') {
12
+ throw new Error('stateHooks.getCleanAppState and stateHooks.store.getState are unavailable.');
13
+ }
14
+ const metamask = root.metamask || root;
15
+ return {
16
+ hooks,
17
+ metamask,
18
+ internal: metamask.internalAccounts || {},
19
+ };
20
+ };
21
+ `;
22
+
23
+ export function readWalletStateExpression() {
24
+ return `(async () => {
25
+ ${walletStatePreamble}
26
+ const { metamask, internal } = await readWalletState();
27
+ const accounts = internal.accounts || {};
28
+ const selectedId = internal.selectedAccount || null;
29
+ const selected = selectedId && accounts[selectedId] ? accounts[selectedId] : null;
30
+ const metadata = selected?.metadata || {};
31
+ return {
32
+ href: String(globalThis.location?.href || ''),
33
+ passwordInput: Boolean(globalThis.document?.querySelector?.('input[type="password"]')),
34
+ selectedAccount: selected ? {
35
+ id: selectedId,
36
+ address: selected.address || null,
37
+ name: metadata.name || null,
38
+ type: selected.type || null,
39
+ } : null,
40
+ completedOnboarding: Boolean(metamask.completedOnboarding),
41
+ selectedNetworkClientId: metamask.selectedNetworkClientId || null,
42
+ };
43
+ })()`;
44
+ }
45
+
46
+ export function listWalletAccountsExpression(scope) {
47
+ return `(async () => {
48
+ ${walletStatePreamble}
49
+ const { internal } = await readWalletState();
50
+ const selectedId = internal.selectedAccount || null;
51
+ const scope = ${JSON.stringify(scope)};
52
+ const accounts = Object.entries(internal.accounts || {})
53
+ .map(([id, account]) => ({
54
+ id,
55
+ address: account?.address || null,
56
+ name: account?.metadata?.name || null,
57
+ type: account?.type || null,
58
+ selected: id === selectedId,
59
+ }))
60
+ .filter((account) => {
61
+ if (scope === 'selected') return account.selected;
62
+ if (scope === 'all') return true;
63
+ if (scope === 'evm') return /^0x[0-9a-f]{40}$/iu.test(account.address || '');
64
+ return account.selected || Boolean(String(account.name || '').trim());
65
+ });
66
+ return { accounts };
67
+ })()`;
68
+ }
69
+
70
+ export function selectWalletAccountExpression(criteria, timeoutMs = 5000) {
71
+ return `(async () => {
72
+ ${walletStatePreamble}
73
+ const before = await readWalletState();
74
+ const accounts = before.internal.accounts || {};
75
+ const requestedAddress = ${JSON.stringify(criteria.address)};
76
+ const requestedId = ${JSON.stringify(criteria.id)};
77
+ const requestedName = ${JSON.stringify(criteria.name)};
78
+ const match = Object.entries(accounts).find(([id, account]) => {
79
+ const metadata = account?.metadata || {};
80
+ return (requestedId && id === requestedId) ||
81
+ (requestedAddress && String(account?.address || '').toLowerCase() === requestedAddress) ||
82
+ (requestedName && metadata.name === requestedName);
83
+ });
84
+ if (!match) throw new Error('Requested account was not found in internalAccounts.');
85
+ if (typeof before.hooks.submitRequestToBackground !== 'function') {
86
+ throw new Error('stateHooks.submitRequestToBackground is unavailable.');
87
+ }
88
+ const [id] = match;
89
+ await before.hooks.submitRequestToBackground('setSelectedInternalAccount', [id]);
90
+ const deadline = Date.now() + ${Number(timeoutMs)};
91
+ let observedId = before.internal.selectedAccount || null;
92
+ while (Date.now() <= deadline) {
93
+ const after = await readWalletState();
94
+ observedId = after.internal.selectedAccount || null;
95
+ if (observedId === id) {
96
+ const selected = after.internal.accounts?.[observedId];
97
+ return {
98
+ id: observedId,
99
+ address: selected?.address || null,
100
+ name: selected?.metadata?.name || null,
101
+ type: selected?.type || null,
102
+ };
103
+ }
104
+ await new Promise((resolve) => setTimeout(resolve, 100));
105
+ }
106
+ throw new Error(
107
+ 'Expected selected account ' + id + ', got ' + String(observedId) +
108
+ ' after ${Number(timeoutMs)}ms.',
109
+ );
110
+ })()`;
111
+ }
@@ -7,7 +7,7 @@
7
7
  "nodes": {
8
8
  "prepare-files": {
9
9
  "action": "command",
10
- "cmd": "mkdir -p temp/recipe-runner.action-validation && printf 'E2E_ACTION_VALIDATION extension\\n' > temp/recipe-runner.action-validation/probe.txt && printf '{\"platform\":\"extension\",\"ok\":true}\n' > temp/recipe-runner.action-validation/probe.json && printf 'extension log E2E_ACTION_VALIDATION\\n' > temp/recipe-runner.action-validation/validation.log && echo E2E_ACTION_VALIDATION extension",
10
+ "cmd": "mkdir -p temp/recipe-runner.action-validation && printf 'E2E_ACTION_VALIDATION extension\\n' > temp/recipe-runner.action-validation/probe.txt && printf '{\"platform\":\"extension\",\"ok\":true}\n' > temp/recipe-runner.action-validation/probe.json && printf 'extension log E2E_ACTION_VALIDATION\\n' >> temp/recipe-runner.action-validation/validation.log && echo E2E_ACTION_VALIDATION extension",
11
11
  "next": "assert-command-exit",
12
12
  "intent": "Prepare runner.action-validation probe files"
13
13
  },
@@ -7,7 +7,7 @@
7
7
  "nodes": {
8
8
  "prepare-files": {
9
9
  "action": "command",
10
- "cmd": "mkdir -p temp/recipe-runner.action-validation && printf 'E2E_ACTION_VALIDATION mobile\\n' > temp/recipe-runner.action-validation/probe.txt && printf '{\"platform\":\"mobile\",\"ok\":true}\n' > temp/recipe-runner.action-validation/probe.json && printf 'mobile log E2E_ACTION_VALIDATION\\n' > temp/recipe-runner.action-validation/validation.log && echo E2E_ACTION_VALIDATION mobile",
10
+ "cmd": "mkdir -p temp/recipe-runner.action-validation && printf 'E2E_ACTION_VALIDATION mobile\\n' > temp/recipe-runner.action-validation/probe.txt && printf '{\"platform\":\"mobile\",\"ok\":true}\n' > temp/recipe-runner.action-validation/probe.json && printf 'mobile log E2E_ACTION_VALIDATION\\n' >> temp/recipe-runner.action-validation/validation.log && echo E2E_ACTION_VALIDATION mobile",
11
11
  "next": "assert-command-exit",
12
12
  "intent": "Prepare runner.action-validation probe files"
13
13
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -13,16 +13,17 @@
13
13
  "test:unit": "vitest run --config scripts/vitest.config.mjs",
14
14
  "test:coverage": "vitest run --coverage --config scripts/vitest.config.mjs",
15
15
  "qa:human": "node scripts/validate-human-outcomes.mjs",
16
+ "site:contrast": "node scripts/site-contrast.mjs",
16
17
  "self-test": "bin/mm-harness self-test",
17
18
  "manifest:mobile": "bin/mm-harness actions --raw --adapter mobile --json",
18
19
  "manifest:extension": "bin/mm-harness actions --raw --adapter extension --json",
19
20
  "check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
20
21
  },
21
22
  "dependencies": {
22
- "@farmslot/agent-runtime": "^0.4.0",
23
+ "@farmslot/agent-runtime": "^0.5.0",
23
24
  "@farmslot/handoff": "^0.3.1",
24
- "@farmslot/protocol": "^0.14.0",
25
- "@farmslot/recipe-harness": "^0.10.4",
25
+ "@farmslot/protocol": "^0.15.0",
26
+ "@farmslot/recipe-harness": "^0.11.0",
26
27
  "commander": "^12.0.0",
27
28
  "es-module-lexer": "2.3.1",
28
29
  "esbuild": "0.28.1",
@@ -61,8 +62,10 @@
61
62
  "dist",
62
63
  "adapters",
63
64
  "library",
65
+ "site",
64
66
  "scripts/completions.sh",
65
67
  "scripts/install-completions.sh",
68
+ "scripts/site-contrast.mjs",
66
69
  "scripts/validate-human-outcomes.mjs",
67
70
  "!scripts/README.md",
68
71
  "docs",