@deeeed/metamask-harness 0.83.0 → 0.85.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 +62 -0
- package/README.md +8 -1
- package/adapters/extension/console-tail.mjs +11 -7
- package/adapters/extension/ensure-browser.sh +17 -18
- package/adapters/manifest.json +14 -110
- package/adapters/mobile/start-console-forwarder.sh +8 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +48 -6
- package/adapters/shared/session-lock.mjs +43 -0
- package/adapters/web-dapp/metamask-signer.mjs +212 -0
- package/dist/adapters/extension/actions.js +5 -0
- package/dist/adapters/extension/console-capture.js +18 -0
- package/dist/adapters/extension/fixtures.js +2 -0
- package/dist/adapters/extension/overlay.js +5 -4
- package/dist/adapters/extension/run.js +4 -2
- package/dist/adapters/extension/runtime.js +73 -18
- package/dist/adapters/mobile/devices.js +5 -1
- package/dist/adapters/mobile/launch.js +19 -2
- package/dist/adapters/mobile/provision.js +4 -2
- package/dist/adapters/mobile/run.js +3 -2
- package/dist/adapters/mobile/surface.js +1 -2
- package/dist/adapters/mobile/verify.js +15 -7
- package/dist/adapters/plugin-surface.js +14 -42
- package/dist/adapters/session-lock.js +23 -0
- package/dist/adapters/web-dapp/actions.js +7 -5
- package/dist/adapters/web-dapp/console-capture.js +2 -20
- package/dist/adapters/web-dapp/run.js +25 -24
- package/dist/adapters/web-dapp/surface.js +32 -108
- package/dist/adapters.js +5 -3
- package/dist/command-contract.js +1351 -643
- package/dist/commands/check.js +5 -7
- package/dist/commands/debug.js +2 -2
- package/dist/commands/doctor.js +3 -4
- package/dist/commands/domain.js +4 -3
- package/dist/commands/fixtures.js +3 -3
- package/dist/commands/help.js +6 -5
- package/dist/commands/launch/device.js +1 -1
- package/dist/commands/launch/extension.js +9 -7
- package/dist/commands/logs.js +2 -2
- package/dist/commands/prepare.js +4 -4
- package/dist/commands/provision.js +2 -2
- package/dist/commands/review.js +8 -10
- package/dist/commands/runtime-launch.js +2 -0
- package/dist/commands/status.js +4 -2
- package/dist/commands/task-init.js +2 -3
- package/dist/console-classifier.js +3 -2
- package/dist/doctor.js +11 -4
- package/dist/mm-harness-cli.js +241 -1210
- package/dist/paths.js +9 -4
- package/dist/recipe-security.js +1 -1
- package/dist/review/knowledge.js +10 -2
- package/dist/runner.js +18 -3
- package/docs/CONTRIBUTING.md +14 -4
- package/docs/RECIPES.md +14 -1
- package/docs/cli-contract.md +8 -7
- package/docs/feature-guard.md +1 -1
- package/library/actions/extension/app/assert_no_console_errors.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +26 -0
- package/library/actions/mobile/app/assert_no_console_errors.mjs +129 -0
- package/library/actions/shared/console-assert.mjs +174 -0
- package/library/actions/shared/console-collector.mjs +11 -4
- package/library/actions/shared/console-findings.mjs +108 -7
- package/library/actions/web-dapp/app/assert_no_console_errors.mjs +10 -54
- package/library/actions/web-dapp/app/launch.mjs +1 -1
- package/library/actions/web-dapp/platform/adapter-web-lib.mjs +18 -0
- package/library/actions/web-dapp/platform/hud.mjs +3 -10
- package/library/actions/web-dapp/platform/page.mjs +63 -18
- package/library/actions/web-dapp/platform/runtime.mjs +30 -179
- package/library/diagnostics/console-allowlist.json +13 -0
- package/library/manifests/extension.action-manifest.json +36 -0
- package/library/manifests/mobile.action-manifest.json +36 -0
- package/library/manifests/web-dapp.action-manifest.json +1 -1
- package/library/recipes/core/health/check.recipe.json +40 -0
- package/library/recipes/extension/health/check.recipe.json +71 -0
- package/library/recipes/mobile/health/check.recipe.json +71 -0
- package/package.json +9 -9
- package/scripts/completions.sh +1 -1
- package/site/assets/help-recipes.json +2 -1
- package/adapters/web-dapp/cleanup.mjs +0 -69
- package/adapters/web-dapp/inject.mjs +0 -75
- package/adapters/web-dapp/launch.mjs +0 -631
- package/adapters/web-dapp/lib/blocked-hosts.mjs +0 -24
- package/adapters/web-dapp/lib/cdp-client.mjs +0 -84
- package/adapters/web-dapp/lib/display.mjs +0 -115
- package/adapters/web-dapp/lib/hud.mjs +0 -148
- package/adapters/web-dapp/lib/policy-fence.mjs +0 -119
- package/adapters/web-dapp/lib/policy.mjs +0 -84
- package/adapters/web-dapp/lib/processes.mjs +0 -187
- package/adapters/web-dapp/lib/readiness.mjs +0 -272
- package/adapters/web-dapp/stop.mjs +0 -38
- package/adapters/web-dapp/verify.mjs +0 -51
- package/adapters/web-dapp/wallet-host.mjs +0 -520
- package/dist/adapters/web-dapp/doctor-checks.js +0 -33
- package/dist/adapters/web-dapp/launch.js +0 -87
- package/dist/adapters/web-dapp/overlay.js +0 -21
- package/dist/adapters/web-dapp/readiness.js +0 -10
- package/dist/adapters/web-dapp/stop.js +0 -20
- package/dist/cli.js +0 -202
|
@@ -7,11 +7,10 @@
|
|
|
7
7
|
// so it stays the window's active tab and keeps painting without it.
|
|
8
8
|
|
|
9
9
|
import { isAppUrl } from '@farmslot/adapter-web/origin';
|
|
10
|
+
import { cdpListenerPids } from '@farmslot/adapter-web/web-dapp';
|
|
10
11
|
|
|
11
|
-
import { resolveSlowMo } from '
|
|
12
|
-
import {
|
|
13
|
-
import { cdpListenerPids } from '../../../../adapters/web-dapp/lib/processes.mjs';
|
|
14
|
-
import { appOrigin, cdpPort, readBrowserState } from './runtime.mjs';
|
|
12
|
+
import { readHudState, resolveSlowMo } from './adapter-web-lib.mjs';
|
|
13
|
+
import { appOrigin, cdpPort, readBrowserState, webDappRuntimeDir } from './runtime.mjs';
|
|
15
14
|
import { importRecipeHarnessRuntimeCdp } from '../../harness-exports.mjs';
|
|
16
15
|
|
|
17
16
|
const { CdpSession, CdpWebPage, listCdpTargets, sleep } = await importRecipeHarnessRuntimeCdp();
|
|
@@ -73,32 +72,78 @@ export async function selectAppTarget(input) {
|
|
|
73
72
|
// MetaMask pages run under LavaMoat scuttling, so main-world helpers (timers in
|
|
74
73
|
// wait loops) are unavailable there; evaluate in an isolated world that shares
|
|
75
74
|
// the DOM instead.
|
|
75
|
+
//
|
|
76
|
+
// A wallet screenshot carries the running recipe's HUD too: hud.json's current
|
|
77
|
+
// step is drawn on the MetaMask page for the capture only, then removed. A
|
|
78
|
+
// finished or absent state (--hud hide) draws nothing.
|
|
76
79
|
class WalletSurfacePage extends CdpWebPage {
|
|
77
80
|
evaluate(expression) {
|
|
78
81
|
return this.evaluateInIsolatedWorld(expression, 'web-dapp-wallet-surface');
|
|
79
82
|
}
|
|
80
|
-
}
|
|
81
83
|
|
|
82
|
-
// Number inputs ignore select(), so the generic setInput would append to a
|
|
83
|
-
// prefilled value (the order form prefills the mid price). Clear the field with
|
|
84
|
-
// the editor's selectAll command, as a user's Cmd+A would, type over the
|
|
85
|
-
// selection and require the field to read back exactly that value.
|
|
86
|
-
// Screenshots are evidence of the app, so the recipe HUD is hidden while one
|
|
87
|
-
// is captured and shown again afterwards.
|
|
88
|
-
export function hudVisibilityExpression(visible) {
|
|
89
|
-
return `(() => { const host = document.documentElement.querySelector(':scope > ${HUD_ELEMENT}'); if (!host) return false; host.style.visibility = ${JSON.stringify(visible ? '' : 'hidden')}; return true; })()`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
class AppPage extends CdpWebPage {
|
|
93
84
|
async screenshot(context, relPath, metadata = {}) {
|
|
94
|
-
const
|
|
85
|
+
const state = context?.projectRoot ? readHudState(webDappRuntimeDir(context.projectRoot)) : null;
|
|
86
|
+
const drawn = state?.current && !state.finished
|
|
87
|
+
? await this.evaluate(walletHudExpression(state)).then((result) => result?.hud === true, () => false)
|
|
88
|
+
: false;
|
|
95
89
|
try {
|
|
96
90
|
return await super.screenshot(context, relPath, metadata);
|
|
97
91
|
} finally {
|
|
98
|
-
if (
|
|
92
|
+
if (drawn) await this.evaluate(walletHudExpression(null)).catch(() => {});
|
|
99
93
|
}
|
|
100
94
|
}
|
|
95
|
+
}
|
|
101
96
|
|
|
97
|
+
// The wallet-page HUD: a bar docked to the bottom edge that reserves its own
|
|
98
|
+
// height (a spacer after the content and the body's bottom padding), as the
|
|
99
|
+
// Extension's HUD does on MetaMask pages, so the request and its footer
|
|
100
|
+
// buttons move up instead of being covered. Inert, aria-hidden, pointer-events
|
|
101
|
+
// none. null removes it and restores the body padding.
|
|
102
|
+
export const WALLET_HUD_ID = 'mm-harness-wallet-hud';
|
|
103
|
+
|
|
104
|
+
export function walletHudExpression(state) {
|
|
105
|
+
const progress = state?.current?.progress;
|
|
106
|
+
const view = state ? {
|
|
107
|
+
badge: ['RUN', progress ? `${progress.current}/${progress.total}` : ''].filter(Boolean).join(' '),
|
|
108
|
+
intent: String(state.current?.intent ?? ''),
|
|
109
|
+
} : null;
|
|
110
|
+
return `(() => {
|
|
111
|
+
const view = ${JSON.stringify(view)};
|
|
112
|
+
const id = ${JSON.stringify(WALLET_HUD_ID)};
|
|
113
|
+
const old = document.getElementById(id);
|
|
114
|
+
if (old) {
|
|
115
|
+
document.body.style.paddingBottom = old.dataset.previousPadding || '';
|
|
116
|
+
old.remove();
|
|
117
|
+
document.getElementById(id + '-space')?.remove();
|
|
118
|
+
}
|
|
119
|
+
if (!view) return { hud: false, cleared: Boolean(old) };
|
|
120
|
+
const bar = document.createElement('div');
|
|
121
|
+
bar.id = id;
|
|
122
|
+
bar.setAttribute('aria-hidden', 'true');
|
|
123
|
+
bar.inert = true;
|
|
124
|
+
bar.dataset.previousPadding = document.body.style.paddingBottom;
|
|
125
|
+
Object.assign(bar.style, { position: 'fixed', left: '0', right: '0', bottom: '0', zIndex: '2147483647', pointerEvents: 'none', boxSizing: 'border-box', padding: '5px 8px', background: 'rgba(8, 10, 14, 0.9)', color: '#fff', font: '600 11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', borderTop: '1px solid rgba(255,255,255,0.14)' });
|
|
126
|
+
const badge = document.createElement('span');
|
|
127
|
+
Object.assign(badge.style, { color: '#8ab4ff', fontSize: '9px', fontWeight: '800', marginRight: '6px' });
|
|
128
|
+
badge.textContent = view.badge;
|
|
129
|
+
bar.append(badge, view.intent);
|
|
130
|
+
document.body.append(bar);
|
|
131
|
+
const height = Math.ceil(bar.getBoundingClientRect().height);
|
|
132
|
+
const space = document.createElement('div');
|
|
133
|
+
space.id = id + '-space';
|
|
134
|
+
space.setAttribute('aria-hidden', 'true');
|
|
135
|
+
Object.assign(space.style, { display: 'block', height: height + 'px', pointerEvents: 'none' });
|
|
136
|
+
document.body.insertBefore(space, bar);
|
|
137
|
+
document.body.style.paddingBottom = height + 'px';
|
|
138
|
+
return { hud: true, docked: true, height };
|
|
139
|
+
})()`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Number inputs ignore select(), so the generic setInput would append to a
|
|
143
|
+
// prefilled value (the order form prefills the mid price). Clear the field with
|
|
144
|
+
// the editor's selectAll command, as a user's Cmd+A would, type over the
|
|
145
|
+
// selection and require the field to read back exactly that value.
|
|
146
|
+
class AppPage extends CdpWebPage {
|
|
102
147
|
// A tab whose first load is still pending (a slow dev-server compile) has no
|
|
103
148
|
// committed history entry yet; navigating away from it is still fine.
|
|
104
149
|
async currentNavigationUrl() {
|
|
@@ -1,186 +1,37 @@
|
|
|
1
1
|
// Resolve the web-dapp slot runtime: the venue policy, ports, runtime paths,
|
|
2
|
-
// browser state and wallet-fixture accounts
|
|
3
|
-
//
|
|
2
|
+
// browser state and wallet-fixture accounts, from @farmslot/adapter-web; plus
|
|
3
|
+
// the live-action plumbing (input, direct run, output). Key material is only
|
|
4
|
+
// ever returned as a viem account object; callers must never log or persist it.
|
|
4
5
|
|
|
5
|
-
import { realpathSync } from 'node:fs';
|
|
6
6
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
7
|
-
import path from 'node:path';
|
|
8
7
|
import { pathToFileURL } from 'node:url';
|
|
9
8
|
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function webDappRuntimePath(projectRoot, ...segments) {
|
|
38
|
-
return path.join(webDappRuntimeDir(projectRoot), ...segments);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function portFrom(raw, label) {
|
|
42
|
-
if (raw === undefined || raw === null || raw === '') return undefined;
|
|
43
|
-
const port = Number(raw);
|
|
44
|
-
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
45
|
-
throw new Error(`${label} must be a TCP port, got ${JSON.stringify(raw)}.`);
|
|
46
|
-
}
|
|
47
|
-
return port;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// The slot's Next.js dev server. Farmslot passes it as --watcher-port {{port}};
|
|
51
|
-
// TERMINAL_APP_PORT is the explicit override.
|
|
52
|
-
export function appPort(input = {}, env = process.env) {
|
|
53
|
-
const port =
|
|
54
|
-
portFrom(input.node?.app_port, 'app_port') ??
|
|
55
|
-
portFrom(env.TERMINAL_APP_PORT, 'TERMINAL_APP_PORT') ??
|
|
56
|
-
portFrom(env.RECIPE_WATCHER_PORT, 'RECIPE_WATCHER_PORT') ??
|
|
57
|
-
portFrom(env.WATCHER_PORT, 'WATCHER_PORT');
|
|
58
|
-
if (!port) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
`${webDappPolicy().adapterId} adapter requires the slot dev-server port.\n` +
|
|
61
|
-
'Next: pass --watcher-port <port> (Farmslot: {{port}}) or set TERMINAL_APP_PORT.',
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
return port;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function appOrigin(input = {}, env = process.env) {
|
|
68
|
-
return `http://localhost:${appPort(input, env)}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function cdpPort(input = {}, env = process.env) {
|
|
72
|
-
const port =
|
|
73
|
-
portFrom(input.node?.cdp_port, 'cdp_port') ??
|
|
74
|
-
portFrom(env.RECIPE_CDP_PORT, 'RECIPE_CDP_PORT') ??
|
|
75
|
-
portFrom(env.CDP_PORT, 'CDP_PORT');
|
|
76
|
-
if (!port) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
`${webDappPolicy().adapterId} adapter requires a per-slot browser CDP port.\n` +
|
|
79
|
-
'Next: pass --cdp-port <port> (Farmslot: {{cdp_port}}).',
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
return port;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function readJsonFile(file) {
|
|
86
|
-
try {
|
|
87
|
-
return JSON.parse(await readFile(file, 'utf8'));
|
|
88
|
-
} catch (error) {
|
|
89
|
-
if (error?.code === 'ENOENT') return null;
|
|
90
|
-
throw error;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export async function writePrivateJson(file, value) {
|
|
95
|
-
await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function readBrowserState(projectRoot) {
|
|
99
|
-
return readJsonFile(webDappRuntimePath(projectRoot, 'browser.json'));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function resolveSigner(raw) {
|
|
103
|
-
const signer = raw == null || raw === '' ? 'extension' : String(raw);
|
|
104
|
-
if (!SIGNER_MODES.includes(signer)) {
|
|
105
|
-
throw new Error(`signer must be one of ${SIGNER_MODES.join(' | ')}, got ${JSON.stringify(raw)}.`);
|
|
106
|
-
}
|
|
107
|
-
return signer;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Account name precedence: node.account_name → node.account (when not an
|
|
111
|
-
// address) → the running browser's account → dev1.
|
|
112
|
-
export function accountName(input = {}, browserState = null) {
|
|
113
|
-
const explicit = input.node?.account_name ?? input.node?.account;
|
|
114
|
-
if (typeof explicit === 'string' && explicit.trim() && !/^0x[0-9a-fA-F]{40}$/u.test(explicit.trim())) {
|
|
115
|
-
return explicit.trim();
|
|
116
|
-
}
|
|
117
|
-
return browserState?.account?.name ?? DEFAULT_ACCOUNT;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function fixturePath(projectRoot, env = process.env) {
|
|
121
|
-
return env.RECIPE_WALLET_FIXTURE ? path.resolve(env.RECIPE_WALLET_FIXTURE) : walletFixturePath(projectRoot);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export async function loadWalletFixture(projectRoot, env = process.env) {
|
|
125
|
-
const file = fixturePath(projectRoot, env);
|
|
126
|
-
const fixture = await readJsonFile(file);
|
|
127
|
-
if (!fixture) {
|
|
128
|
-
throw new Error(
|
|
129
|
-
`wallet fixture not found at ${file}.\n` +
|
|
130
|
-
`Next: mm-harness install --adapter ${webDappPolicy().adapterId} --target <checkout> with RECIPE_WALLET_FIXTURE set, or copy the fixture there (0600).`,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
if (!Array.isArray(fixture.accounts) || fixture.accounts.length === 0) {
|
|
134
|
-
throw new Error(`wallet fixture at ${file} has no accounts.`);
|
|
135
|
-
}
|
|
136
|
-
return { file, fixture };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function fixtureEntry(fixture, name) {
|
|
140
|
-
const entry = fixture.accounts.find((account) => account?.name === name);
|
|
141
|
-
if (!entry) {
|
|
142
|
-
const names = fixture.accounts.map((account) => account?.name).filter(Boolean).join(', ');
|
|
143
|
-
throw new Error(`wallet fixture has no account named "${name}". Available: ${names}.`);
|
|
144
|
-
}
|
|
145
|
-
if (entry.type !== 'mnemonic' && entry.type !== 'privateKey') {
|
|
146
|
-
throw new Error(`wallet fixture account "${name}" must be mnemonic or privateKey, got "${entry.type}".`);
|
|
147
|
-
}
|
|
148
|
-
if (typeof entry.value !== 'string' || !entry.value.trim()) {
|
|
149
|
-
throw new Error(`wallet fixture account "${name}" has no value.`);
|
|
150
|
-
}
|
|
151
|
-
return entry;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Mnemonics use BIP-44 index 0, the MetaMask default and the derivation the
|
|
155
|
-
// Extension fixture flow imports.
|
|
156
|
-
export function viemAccountFromEntry(entry) {
|
|
157
|
-
if (entry.type === 'mnemonic') return mnemonicToAccount(entry.value.trim(), { addressIndex: 0 });
|
|
158
|
-
const raw = entry.value.trim();
|
|
159
|
-
const key = raw.startsWith('0x') ? raw : `0x${raw}`;
|
|
160
|
-
if (!/^0x[0-9a-fA-F]{64}$/u.test(key)) {
|
|
161
|
-
throw new Error(`wallet fixture privateKey account "${entry.name}" is not a 32-byte hex key.`);
|
|
162
|
-
}
|
|
163
|
-
return privateKeyToAccount(key);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export async function fixtureAccount(projectRoot, name, env = process.env) {
|
|
167
|
-
const { fixture } = await loadWalletFixture(projectRoot, env);
|
|
168
|
-
return viemAccountFromEntry(fixtureEntry(fixture, name));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Address-only view of the account a node targets: an explicit address wins
|
|
172
|
-
// (read-only use), otherwise the fixture account is derived.
|
|
173
|
-
export async function resolveAccountAddress(input, env = process.env) {
|
|
174
|
-
const raw = input.node?.account;
|
|
175
|
-
if (typeof raw === 'string' && /^0x[0-9a-fA-F]{40}$/u.test(raw.trim())) {
|
|
176
|
-
return { name: null, address: raw.trim() };
|
|
177
|
-
}
|
|
178
|
-
const projectRoot = input.context?.projectRoot;
|
|
179
|
-
const browserState = projectRoot ? await readBrowserState(projectRoot) : null;
|
|
180
|
-
const name = accountName(input, browserState);
|
|
181
|
-
const account = await fixtureAccount(projectRoot, name, env);
|
|
182
|
-
return { name, address: account.address };
|
|
183
|
-
}
|
|
9
|
+
import { webDappAdapterId } from '@farmslot/adapter-web/web-dapp';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
accountName,
|
|
13
|
+
appOrigin,
|
|
14
|
+
appPort,
|
|
15
|
+
canonicalPath,
|
|
16
|
+
cdpPort,
|
|
17
|
+
DEFAULT_ACCOUNT,
|
|
18
|
+
fixtureAccount,
|
|
19
|
+
fixtureEntry,
|
|
20
|
+
fixturePath,
|
|
21
|
+
loadWalletFixture,
|
|
22
|
+
POLICY_ENV,
|
|
23
|
+
readBrowserState,
|
|
24
|
+
readJsonFile,
|
|
25
|
+
resolveAccountAddress,
|
|
26
|
+
resolveSigner,
|
|
27
|
+
SIGNER_MODES,
|
|
28
|
+
viemAccountFromEntry,
|
|
29
|
+
webDappAdapterId,
|
|
30
|
+
webDappPolicy,
|
|
31
|
+
webDappRuntimeDir,
|
|
32
|
+
webDappRuntimePath,
|
|
33
|
+
writePrivateJson,
|
|
34
|
+
} from '@farmslot/adapter-web/web-dapp';
|
|
184
35
|
|
|
185
36
|
export async function loadInput() {
|
|
186
37
|
const inputPath = process.argv[2] || process.env.METAMASK_RECIPE_ADAPTER_INPUT;
|
|
@@ -199,7 +50,7 @@ export async function runAdapter(callback) {
|
|
|
199
50
|
const output = await callback(input);
|
|
200
51
|
await writeFile(input.outputPath, `${JSON.stringify(output, null, 2)}\n`);
|
|
201
52
|
} catch (error) {
|
|
202
|
-
process.stderr.write(`[${webDappAdapterId()}] ${input.action} failed: ${error?.stack ?? error}\n`);
|
|
53
|
+
process.stderr.write(`[${webDappAdapterId()}] ${input.action} failed: ${error?.verdict ? error.message : error?.stack ?? error}\n`);
|
|
203
54
|
process.exitCode = 1;
|
|
204
55
|
}
|
|
205
56
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 3,
|
|
3
|
+
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"signature": "Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://reactjs.org/link/unsafe-component-lifecycles for details.",
|
|
6
|
+
"continuation": "* Move code with side effects to componentDidMount, and set initial state in the constructor.\nPlease update the following components: %s UnlockPageBase",
|
|
7
|
+
"first_frame": "chrome-extension://<extension>/vendor-node_modules_ra.js",
|
|
8
|
+
"source": "extension",
|
|
9
|
+
"reason": "React warns once per load that the Extension unlock page (UnlockPageBase) still uses UNSAFE_componentWillMount. Reproduced on unchanged main (metamask-extension a6765b12, macpro-mme-5, same wallet fixture and selected account as the feature slot).",
|
|
10
|
+
"link": "https://github.com/MetaMask/metamask-extension/blob/main/ui/pages/unlock-page/unlock-page.component.tsx"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -1237,6 +1237,42 @@
|
|
|
1237
1237
|
},
|
|
1238
1238
|
"execution_capabilities": []
|
|
1239
1239
|
},
|
|
1240
|
+
"metamask.app.assert_no_console_errors": {
|
|
1241
|
+
"description": "Fail when MetaMask (service worker or UI pages) logged an error or uncaught exception that no console allowlist (the bundled library's, then each recipe library's diagnostics/console-allowlist.json) marks as known noise, judging the events since the launch began, by their own timestamps. Reads extension-console.log from the offset mm-harness launch records when it starts, and leaves out records the collector stamps with an event time before the launch (a replaced collector's replay of a browser the launch kept); with no recorded launch it fails unless since is start-of-log. The same classification feeds run diagnostics. Fails as unavailable unless this runtime's console collector runs on the run's CDP port and a control line logged in MetaMask's service worker or page reaches extension-console.log. Allowlist entries are exact signatures (schemaVersion 3).",
|
|
1242
|
+
"execution_capabilities": ["host-read-export"],
|
|
1243
|
+
"schema": {
|
|
1244
|
+
"type": "object",
|
|
1245
|
+
"properties": {
|
|
1246
|
+
"levels": {
|
|
1247
|
+
"type": "array",
|
|
1248
|
+
"items": { "type": "string", "enum": ["error", "exception", "warning"] },
|
|
1249
|
+
"description": "Default [\"error\",\"exception\"]."
|
|
1250
|
+
},
|
|
1251
|
+
"since": {
|
|
1252
|
+
"type": "string",
|
|
1253
|
+
"enum": ["launch", "start-of-log"],
|
|
1254
|
+
"default": "launch"
|
|
1255
|
+
},
|
|
1256
|
+
"allowlist_root": {
|
|
1257
|
+
"type": "string",
|
|
1258
|
+
"description": "An extra directory holding diagnostics/console-allowlist.json."
|
|
1259
|
+
},
|
|
1260
|
+
"timeout_ms": {
|
|
1261
|
+
"type": "number",
|
|
1262
|
+
"minimum": 1,
|
|
1263
|
+
"description": "Maximum time in milliseconds to wait for the capture control line (default 8000)."
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
"additionalProperties": false
|
|
1267
|
+
},
|
|
1268
|
+
"examples": [
|
|
1269
|
+
{
|
|
1270
|
+
"action": "metamask.app.assert_no_console_errors",
|
|
1271
|
+
"intent": "Prove MetaMask logged no unexpected error since launch",
|
|
1272
|
+
"next": "done"
|
|
1273
|
+
}
|
|
1274
|
+
]
|
|
1275
|
+
},
|
|
1240
1276
|
"app.network_assert": {
|
|
1241
1277
|
"description": "Assert a previously indexed app.network_capture summary without hiding its diagnostics on failure.",
|
|
1242
1278
|
"examples": [
|
|
@@ -1260,6 +1260,42 @@
|
|
|
1260
1260
|
},
|
|
1261
1261
|
"execution_capabilities": []
|
|
1262
1262
|
},
|
|
1263
|
+
"metamask.app.assert_no_console_errors": {
|
|
1264
|
+
"description": "Fail when the app logged an error or uncaught exception that no console allowlist (the bundled library's, then each recipe library's diagnostics/console-allowlist.json) marks as known noise, judging the events since the launch began, by their own timestamps. Reads app-console.log from the adapter-rn console forwarder, which stamps each line with the event's own time: mm-harness launch records when it starts, and lines a reused app runtime replays from before then are left out; with no recorded launch it fails unless since is start-of-log. Only the device the bridge selects for the run (the one the control line comes back from) is judged; when that line names no device, every device is, reported as devices: \"all\". The same classification feeds run diagnostics. Fails as unavailable unless this checkout's forwarder is running and a control line logged inside the app through the debug bridge reaches app-console.log; the opaque release runtime has no bridge and always fails as unavailable. Allowlist entries are exact signatures (schemaVersion 3).",
|
|
1265
|
+
"execution_capabilities": ["host-read-export"],
|
|
1266
|
+
"schema": {
|
|
1267
|
+
"type": "object",
|
|
1268
|
+
"properties": {
|
|
1269
|
+
"levels": {
|
|
1270
|
+
"type": "array",
|
|
1271
|
+
"items": { "type": "string", "enum": ["error", "exception", "warning"] },
|
|
1272
|
+
"description": "Default [\"error\",\"exception\"]."
|
|
1273
|
+
},
|
|
1274
|
+
"since": {
|
|
1275
|
+
"type": "string",
|
|
1276
|
+
"enum": ["launch", "start-of-log"],
|
|
1277
|
+
"default": "launch"
|
|
1278
|
+
},
|
|
1279
|
+
"allowlist_root": {
|
|
1280
|
+
"type": "string",
|
|
1281
|
+
"description": "An extra directory holding diagnostics/console-allowlist.json."
|
|
1282
|
+
},
|
|
1283
|
+
"timeout_ms": {
|
|
1284
|
+
"type": "number",
|
|
1285
|
+
"minimum": 1,
|
|
1286
|
+
"description": "Maximum time in milliseconds to wait for the capture control line (default 8000)."
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
"additionalProperties": false
|
|
1290
|
+
},
|
|
1291
|
+
"examples": [
|
|
1292
|
+
{
|
|
1293
|
+
"action": "metamask.app.assert_no_console_errors",
|
|
1294
|
+
"intent": "Prove the app logged no unexpected error since launch",
|
|
1295
|
+
"next": "done"
|
|
1296
|
+
}
|
|
1297
|
+
]
|
|
1298
|
+
},
|
|
1263
1299
|
"app.network_assert": {
|
|
1264
1300
|
"description": "Assert a previously indexed app.network_capture summary without hiding its diagnostics on failure.",
|
|
1265
1301
|
"examples": [
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"app.hud": {
|
|
20
|
-
"description": "Display recipe progress in an overlay on the app tab: recipe, step N/M, the current node intent and the last pass/fail.
|
|
20
|
+
"description": "Display recipe progress in an overlay on the app tab: recipe, step N/M, the current node intent and the last pass/fail. Drawn on a MetaMask wallet page only for the length of a surface=wallet ui.screenshot; ignored by clicks and page text; kept in every ui.screenshot. Drawn in a headless or headful browser unless --hud hide, which also clears the HUD an earlier run left.",
|
|
21
21
|
"examples": [
|
|
22
22
|
{
|
|
23
23
|
"action": "app.hud",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
3
|
+
"title": "MetaMask Core health check",
|
|
4
|
+
"description": "Read-only check to run before ticket work: the headless Perps controller imports and answers one bounded HyperLiquid testnet account read. A failed precondition stops at the node named after its error; that node's intent gives the recovery. Places no orders.",
|
|
5
|
+
"paramsSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"timeout_ms": { "type": "integer", "minimum": 1000, "default": 60000 }
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"workflow": {
|
|
13
|
+
"entry": "status",
|
|
14
|
+
"nodes": {
|
|
15
|
+
"status": {
|
|
16
|
+
"action": "app.status",
|
|
17
|
+
"next": "CONTROLLER_UNAVAILABLE",
|
|
18
|
+
"intent": "Record the resolved Core checkout"
|
|
19
|
+
},
|
|
20
|
+
"CONTROLLER_UNAVAILABLE": {
|
|
21
|
+
"action": "metamask.perps.read_account",
|
|
22
|
+
"network": "testnet",
|
|
23
|
+
"timeout_ms": "{{params.timeout_ms}}",
|
|
24
|
+
"next": "ACCOUNT_READ_FAILED",
|
|
25
|
+
"intent": "CONTROLLER_UNAVAILABLE unless the Perps controller imports and reads the testnet account in time. Recovery: mm-harness doctor --fix"
|
|
26
|
+
},
|
|
27
|
+
"ACCOUNT_READ_FAILED": {
|
|
28
|
+
"action": "assert_output",
|
|
29
|
+
"source": "CONTROLLER_UNAVAILABLE",
|
|
30
|
+
"assert": { "path": "$.accountState.totalBalance", "operator": "matches", "value": "^-?[0-9]+(\\.[0-9]+)?$" },
|
|
31
|
+
"next": "done",
|
|
32
|
+
"intent": "ACCOUNT_READ_FAILED unless the read returned a numeric balance; the controller reports an unreachable venue as a placeholder. Recovery: check network access to HyperLiquid testnet, then rerun"
|
|
33
|
+
},
|
|
34
|
+
"done": {
|
|
35
|
+
"action": "end",
|
|
36
|
+
"status": "pass"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
3
|
+
"title": "MetaMask Extension health check",
|
|
4
|
+
"description": "Read-only check to run before ticket work: the extension answers over CDP, onboarding is done, and the wallet is unlocked on a selected account at wallet home. A failed precondition stops at the node named after its error; that node's intent gives the recovery. Changes no app state.",
|
|
5
|
+
"paramsSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"capture_path": { "type": "string", "default": "screenshots/health-check.png" }
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"workflow": {
|
|
13
|
+
"entry": "APP_NOT_RUNNING",
|
|
14
|
+
"nodes": {
|
|
15
|
+
"APP_NOT_RUNNING": {
|
|
16
|
+
"action": "cdp.target",
|
|
17
|
+
"require_reachable": true,
|
|
18
|
+
"next": "state",
|
|
19
|
+
"intent": "APP_NOT_RUNNING unless the extension answers over CDP. Recovery: mm-harness launch --verify"
|
|
20
|
+
},
|
|
21
|
+
"state": {
|
|
22
|
+
"action": "metamask.wallet.read_state",
|
|
23
|
+
"next": "ONBOARDING",
|
|
24
|
+
"intent": "Read the selected account, lock state and page without changing them"
|
|
25
|
+
},
|
|
26
|
+
"ONBOARDING": {
|
|
27
|
+
"action": "assert_output",
|
|
28
|
+
"source": "state",
|
|
29
|
+
"assert": { "path": "$.state.completedOnboarding", "operator": "eq", "value": true },
|
|
30
|
+
"next": "WALLET_LOCKED",
|
|
31
|
+
"intent": "ONBOARDING if onboarding is not complete. Recovery: mm-harness run wallet.import"
|
|
32
|
+
},
|
|
33
|
+
"WALLET_LOCKED": {
|
|
34
|
+
"action": "assert_output",
|
|
35
|
+
"source": "state",
|
|
36
|
+
"assert": { "path": "$.state.passwordInput", "operator": "falsy" },
|
|
37
|
+
"next": "NO_SELECTED_ACCOUNT",
|
|
38
|
+
"intent": "WALLET_LOCKED if the unlock page is showing. Recovery: mm-harness run wallet.smoke"
|
|
39
|
+
},
|
|
40
|
+
"NO_SELECTED_ACCOUNT": {
|
|
41
|
+
"action": "assert_output",
|
|
42
|
+
"source": "state",
|
|
43
|
+
"assert": { "path": "$.state.selectedAccount.address", "operator": "not_null" },
|
|
44
|
+
"next": "NOT_ON_HOME",
|
|
45
|
+
"intent": "NO_SELECTED_ACCOUNT if the wallet reports no selected account. Recovery: mm-harness run wallet.import"
|
|
46
|
+
},
|
|
47
|
+
"NOT_ON_HOME": {
|
|
48
|
+
"action": "assert_output",
|
|
49
|
+
"source": "state",
|
|
50
|
+
"assert": { "path": "$.state.href", "operator": "matches", "value": "/(home|sidepanel)\\.html#/?$" },
|
|
51
|
+
"next": "CONSOLE_ERRORS",
|
|
52
|
+
"intent": "NOT_ON_HOME unless wallet home is showing. Recovery: a ui.navigate node with page home, then rerun"
|
|
53
|
+
},
|
|
54
|
+
"CONSOLE_ERRORS": {
|
|
55
|
+
"action": "metamask.app.assert_no_console_errors",
|
|
56
|
+
"next": "screenshot",
|
|
57
|
+
"intent": "CONSOLE_ERRORS if the app logged an error outside the console allowlist since launch. Recovery: fix the error, or allowlist known noise with a reason in a library's diagnostics/console-allowlist.json"
|
|
58
|
+
},
|
|
59
|
+
"screenshot": {
|
|
60
|
+
"action": "ui.screenshot",
|
|
61
|
+
"path": "{{params.capture_path}}",
|
|
62
|
+
"next": "done",
|
|
63
|
+
"intent": "Record the ready wallet home as evidence"
|
|
64
|
+
},
|
|
65
|
+
"done": {
|
|
66
|
+
"action": "end",
|
|
67
|
+
"status": "pass"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
3
|
+
"title": "MetaMask Mobile health check",
|
|
4
|
+
"description": "Read-only check to run before ticket work: the app answers, onboarding is done, and the wallet is unlocked on a selected account at wallet home. A failed precondition stops at the node named after its error; that node's intent gives the recovery. Changes no app state.",
|
|
5
|
+
"paramsSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"capture_path": { "type": "string", "default": "screenshots/health-check.png" }
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"workflow": {
|
|
13
|
+
"entry": "APP_NOT_RUNNING",
|
|
14
|
+
"nodes": {
|
|
15
|
+
"APP_NOT_RUNNING": {
|
|
16
|
+
"action": "cdp.target",
|
|
17
|
+
"require_reachable": true,
|
|
18
|
+
"next": "state",
|
|
19
|
+
"intent": "APP_NOT_RUNNING unless the app's debug bridge answers. Recovery: mm-harness launch ios|android --verify"
|
|
20
|
+
},
|
|
21
|
+
"state": {
|
|
22
|
+
"action": "metamask.wallet.read_state",
|
|
23
|
+
"next": "ONBOARDING",
|
|
24
|
+
"intent": "Read the selected account and route without changing them"
|
|
25
|
+
},
|
|
26
|
+
"ONBOARDING": {
|
|
27
|
+
"action": "assert_output",
|
|
28
|
+
"source": "state",
|
|
29
|
+
"assert": { "path": "$.route.name", "operator": "matches", "value": "^(?!Onboarding|OptinMetrics)" },
|
|
30
|
+
"next": "WALLET_LOCKED",
|
|
31
|
+
"intent": "ONBOARDING if the app is still in onboarding. Recovery: mm-harness run wallet.import"
|
|
32
|
+
},
|
|
33
|
+
"WALLET_LOCKED": {
|
|
34
|
+
"action": "assert_output",
|
|
35
|
+
"source": "state",
|
|
36
|
+
"assert": { "path": "$.route.name", "operator": "ne", "value": "Login" },
|
|
37
|
+
"next": "NO_SELECTED_ACCOUNT",
|
|
38
|
+
"intent": "WALLET_LOCKED if the unlock screen is showing. Recovery: mm-harness run wallet.smoke"
|
|
39
|
+
},
|
|
40
|
+
"NO_SELECTED_ACCOUNT": {
|
|
41
|
+
"action": "assert_output",
|
|
42
|
+
"source": "state",
|
|
43
|
+
"assert": { "path": "$.account.address", "operator": "not_null" },
|
|
44
|
+
"next": "NOT_ON_HOME",
|
|
45
|
+
"intent": "NO_SELECTED_ACCOUNT if the wallet reports no selected account. Recovery: mm-harness run wallet.import"
|
|
46
|
+
},
|
|
47
|
+
"NOT_ON_HOME": {
|
|
48
|
+
"action": "assert_output",
|
|
49
|
+
"source": "state",
|
|
50
|
+
"assert": { "path": "$.route.name", "operator": "eq", "value": "WalletView" },
|
|
51
|
+
"next": "CONSOLE_ERRORS",
|
|
52
|
+
"intent": "NOT_ON_HOME unless wallet home is showing. Recovery: a ui.navigate node with page home, then rerun"
|
|
53
|
+
},
|
|
54
|
+
"CONSOLE_ERRORS": {
|
|
55
|
+
"action": "metamask.app.assert_no_console_errors",
|
|
56
|
+
"next": "screenshot",
|
|
57
|
+
"intent": "CONSOLE_ERRORS if the app logged an error outside the console allowlist since launch. Recovery: fix the error, or allowlist known noise with a reason in a library's diagnostics/console-allowlist.json"
|
|
58
|
+
},
|
|
59
|
+
"screenshot": {
|
|
60
|
+
"action": "ui.screenshot",
|
|
61
|
+
"path": "{{params.capture_path}}",
|
|
62
|
+
"next": "done",
|
|
63
|
+
"intent": "Record the ready wallet home as evidence"
|
|
64
|
+
},
|
|
65
|
+
"done": {
|
|
66
|
+
"action": "end",
|
|
67
|
+
"status": "pass"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|