@farmslot/adapter-web 0.17.7 → 0.19.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 +8 -0
- package/README.md +6 -0
- package/dist/launch-browser.d.cts +3 -3
- package/dist/plain-web.d.mts +17 -0
- package/dist/web-dapp/index.d.mts +1 -1
- package/dist/web-dapp/lib/runtime.d.mts +23 -1
- package/package.json +5 -1
- package/src/launch-browser.cjs +19 -11
- package/src/plain-web.mjs +219 -0
- package/src/web-dapp/index.mjs +3 -0
- package/src/web-dapp/launch.mjs +19 -11
- package/src/web-dapp/lib/readiness.mjs +2 -3
- package/src/web-dapp/lib/runtime.mjs +125 -7
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ All notable changes to `@farmslot/adapter-web` are tracked here.
|
|
|
6
6
|
|
|
7
7
|
- Active-development baseline; add user-facing changes here before release or package publication.
|
|
8
8
|
|
|
9
|
+
## 0.19.0 - 2026-11-06
|
|
10
|
+
|
|
11
|
+
- Web-dapp wallet accounts: a mnemonic entry yields every account its `count` names (`names[]`, else dev1, dev2, ...), and `--account` selects one by name or address. A host can add named derivations of a fixture secret at a higher HD index through `RECIPE_WALLET_DERIVED_ACCOUNTS`; a declared address that does not match is refused. `browser.json` records the account's fixture secret name and index.
|
|
12
|
+
|
|
13
|
+
## 0.18.0 - 2026-11-05
|
|
14
|
+
|
|
15
|
+
- Run ordinary websites in an owned extension-free browser with shared CDP actions, screenshots and whole-run recording. Existing extension launches retain their lifecycle.
|
|
16
|
+
|
|
9
17
|
## 0.17.7 - 2026-11-04
|
|
10
18
|
|
|
11
19
|
- Check host-resolver blocking in an owned blank document so strict application CSP needs no harness-only source. Keep application HTTP and WebSocket blocking checks unchanged.
|
package/README.md
CHANGED
|
@@ -108,6 +108,12 @@ const binding = createWalletRequestBinding({
|
|
|
108
108
|
});
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
## Plain websites
|
|
112
|
+
|
|
113
|
+
`@farmslot/adapter-web/plain-web` exports `createPlainWebAdapter({ manifestPath, url, serverLabel })`. The project supplies its action catalog and a URL function using its selected ports. The runtime owns an isolated browser and reuses shared CDP actions, screenshots and whole-run recording. The project owns its application server.
|
|
114
|
+
|
|
115
|
+
Browser stop is scoped to the selected checkout's profile and retains the application server. This path needs no extension, signer or wallet policy and makes no assumptions about the application framework or page selectors.
|
|
116
|
+
|
|
111
117
|
## Web-dapp adapter
|
|
112
118
|
|
|
113
119
|
`@farmslot/adapter-web/web-dapp` (ESM) is the lifecycle of a web app under test whose dev server the slot owns. `createWebDappAdapter({ id, signerModule, cli, hooks })` returns an `@farmslot/adapter-sdk` `ProjectRuntime`: launch, stop, runtime status, doctor checks, logs and the wallet request-log findings. It runs one slot browser per slot, and a wallet host attached to it over CDP that records the wallet requests (and, with `signer=injected`, answers them with the strict wallet from `dapp`).
|
|
@@ -3,9 +3,9 @@ export type LaunchBrowserOptions = {
|
|
|
3
3
|
chromeBin: string;
|
|
4
4
|
profile: string;
|
|
5
5
|
/**
|
|
6
|
-
* Unpacked extension to load.
|
|
6
|
+
* Unpacked extension to load; omitted for a plain web page.
|
|
7
7
|
*/
|
|
8
|
-
extensionDir
|
|
8
|
+
extensionDir?: string | undefined;
|
|
9
9
|
/**
|
|
10
10
|
* Holds browser-resolution.json and the runtime identity.
|
|
11
11
|
*/
|
|
@@ -72,7 +72,7 @@ export type LaunchBrowserOptions = {
|
|
|
72
72
|
* @property {number} cdpPort
|
|
73
73
|
* @property {string} chromeBin
|
|
74
74
|
* @property {string} profile
|
|
75
|
-
* @property {string} extensionDir Unpacked extension to load.
|
|
75
|
+
* @property {string} [extensionDir] Unpacked extension to load; omitted for a plain web page.
|
|
76
76
|
* @property {string} runtimeDir Holds browser-resolution.json and the runtime identity.
|
|
77
77
|
* @property {string} chromeLog
|
|
78
78
|
* @property {string} chromePid
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Runtime files and the browser profile always belong to the selected checkout. */
|
|
2
|
+
export function plainWebRuntimeDir(target: any): string;
|
|
3
|
+
/**
|
|
4
|
+
* Use existing browser ownership, CDP actions and whole-run recording for a plain website.
|
|
5
|
+
* @param {PlainWebAdapterConfig} config
|
|
6
|
+
* @returns {import('@farmslot/adapter-sdk').ProjectRuntime}
|
|
7
|
+
*/
|
|
8
|
+
export function createPlainWebAdapter(config: PlainWebAdapterConfig): import("@farmslot/adapter-sdk").ProjectRuntime;
|
|
9
|
+
/** Releases only the selected checkout's owned profile. The application server is retained. */
|
|
10
|
+
export function stopPlainWebBrowser(target: any): {
|
|
11
|
+
stopped: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type PlainWebAdapterConfig = {
|
|
14
|
+
manifestPath: string;
|
|
15
|
+
url: (target: string) => string;
|
|
16
|
+
serverLabel?: string;
|
|
17
|
+
};
|
|
@@ -125,5 +125,5 @@ export { recipeHarnessPath, recipeRuntimeDir, recipeRuntimePath, shellQuote, wal
|
|
|
125
125
|
export { assertPolicyDigest, fencePolicy, POLICY_DIGEST_ENV, PolicyFenceError } from "./lib/policy-fence.mjs";
|
|
126
126
|
export { cdpListenerPids, ownedBrowserPids, OWNER_FLAG, OWNER_KINDS, ownerMarker, ownsProcess, pidAlive, portFree, stopConsoleCollector, stopWebDappBrowser, terminate } from "./lib/processes.mjs";
|
|
127
127
|
export { assertLaunchNetwork, DEV_SERVER_PID_FILE, devServerTestnetDiagnostic, pythonFor, webDappReadiness } from "./lib/readiness.mjs";
|
|
128
|
-
export { accountName, appOrigin, appPort, canonicalPath, cdpPort, DEFAULT_ACCOUNT, fixtureAccount, fixtureEntry, fixturePath, loadWalletFixture, readBrowserState, readJsonFile, resolveAccountAddress, resolveSigner, SIGNER_MODES, viemAccountFromEntry, webDappRuntimeDir, webDappRuntimePath, writePrivateJson } from "./lib/runtime.mjs";
|
|
128
|
+
export { accountName, appOrigin, appPort, canonicalPath, cdpPort, DEFAULT_ACCOUNT, DERIVED_ACCOUNTS_ENV, fixtureAccount, fixtureEntry, fixturePath, loadWalletFixture, readBrowserState, readJsonFile, resolveAccountAddress, resolveSigner, SIGNER_MODES, viemAccountFromEntry, walletAccount, walletAccounts, webDappRuntimeDir, webDappRuntimePath, writePrivateJson } from "./lib/runtime.mjs";
|
|
129
129
|
export { macosSessionLocked, parseSessionLocked, SESSION_LOCKED_MESSAGE, SESSION_LOCKED_USER_ACTION } from "./lib/session-lock.mjs";
|
|
@@ -15,7 +15,28 @@ export function loadWalletFixture(projectRoot: any, env?: NodeJS.ProcessEnv): Pr
|
|
|
15
15
|
fixture: any;
|
|
16
16
|
}>;
|
|
17
17
|
export function fixtureEntry(fixture: any, name: any): any;
|
|
18
|
-
export function viemAccountFromEntry(entry: any, projectRoot: any): any;
|
|
18
|
+
export function viemAccountFromEntry(entry: any, projectRoot: any, addressIndex?: number): any;
|
|
19
|
+
export function derivedAccounts(env?: NodeJS.ProcessEnv): any[];
|
|
20
|
+
/**
|
|
21
|
+
* Every account the fixture offers: each mnemonic entry expanded to its count,
|
|
22
|
+
* each private key, then the host's named derivations (DERIVED_ACCOUNTS_ENV)
|
|
23
|
+
* of the secrets this fixture holds. `account` is the viem account holding the
|
|
24
|
+
* key: sign with it, never log or persist it.
|
|
25
|
+
*/
|
|
26
|
+
export function walletAccounts(fixture: any, projectRoot: any, env?: NodeJS.ProcessEnv): {
|
|
27
|
+
name: any;
|
|
28
|
+
address: any;
|
|
29
|
+
secret: any;
|
|
30
|
+
index: any;
|
|
31
|
+
account: any;
|
|
32
|
+
}[];
|
|
33
|
+
export function walletAccount(fixture: any, selector: any, projectRoot: any, env?: NodeJS.ProcessEnv): {
|
|
34
|
+
name: any;
|
|
35
|
+
address: any;
|
|
36
|
+
secret: any;
|
|
37
|
+
index: any;
|
|
38
|
+
account: any;
|
|
39
|
+
};
|
|
19
40
|
export function fixtureAccount(projectRoot: any, name: any, env?: NodeJS.ProcessEnv): Promise<any>;
|
|
20
41
|
export function resolveAccountAddress(input: any, env?: NodeJS.ProcessEnv): Promise<{
|
|
21
42
|
name: any;
|
|
@@ -23,6 +44,7 @@ export function resolveAccountAddress(input: any, env?: NodeJS.ProcessEnv): Prom
|
|
|
23
44
|
}>;
|
|
24
45
|
export const SIGNER_MODES: readonly string[];
|
|
25
46
|
export const DEFAULT_ACCOUNT: "dev1";
|
|
47
|
+
export const DERIVED_ACCOUNTS_ENV: "RECIPE_WALLET_DERIVED_ACCOUNTS";
|
|
26
48
|
import { POLICY_ENV } from './policy.mjs';
|
|
27
49
|
import { webDappAdapterId } from './policy.mjs';
|
|
28
50
|
import { webDappPolicy } from './policy.mjs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farmslot/adapter-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Web platform adapter for Farmslot recipes: resolve and launch an isolated Chromium, own its CDP port, load unpacked extensions, and keep macOS focus.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"exports": {
|
|
@@ -68,6 +68,10 @@
|
|
|
68
68
|
"./web-dapp": {
|
|
69
69
|
"types": "./dist/web-dapp/index.d.mts",
|
|
70
70
|
"default": "./src/web-dapp/index.mjs"
|
|
71
|
+
},
|
|
72
|
+
"./plain-web": {
|
|
73
|
+
"types": "./dist/plain-web.d.mts",
|
|
74
|
+
"default": "./src/plain-web.mjs"
|
|
71
75
|
}
|
|
72
76
|
},
|
|
73
77
|
"scripts": {
|
package/src/launch-browser.cjs
CHANGED
|
@@ -67,7 +67,7 @@ const BROWSER_RESOLVER_CLI = require.resolve('./browser-resolver.cjs');
|
|
|
67
67
|
* @property {number} cdpPort
|
|
68
68
|
* @property {string} chromeBin
|
|
69
69
|
* @property {string} profile
|
|
70
|
-
* @property {string} extensionDir Unpacked extension to load.
|
|
70
|
+
* @property {string} [extensionDir] Unpacked extension to load; omitted for a plain web page.
|
|
71
71
|
* @property {string} runtimeDir Holds browser-resolution.json and the runtime identity.
|
|
72
72
|
* @property {string} chromeLog
|
|
73
73
|
* @property {string} chromePid
|
|
@@ -114,7 +114,6 @@ function normalizeOptions(options) {
|
|
|
114
114
|
const flags = {
|
|
115
115
|
chromeBin: 'chrome-bin',
|
|
116
116
|
profile: 'profile',
|
|
117
|
-
extensionDir: 'extension-dir',
|
|
118
117
|
runtimeDir: 'runtime-dir',
|
|
119
118
|
chromeLog: 'chrome-log',
|
|
120
119
|
chromePid: 'chrome-pid',
|
|
@@ -122,6 +121,10 @@ function normalizeOptions(options) {
|
|
|
122
121
|
for (const [key, flag] of Object.entries(flags)) {
|
|
123
122
|
if (!opts[key]) throw new Error(`Missing --${flag}`);
|
|
124
123
|
}
|
|
124
|
+
if (!opts.extensionDir && !opts.stopOnly) {
|
|
125
|
+
if (!opts.startUrl || !/^https?:$/u.test(new URL(opts.startUrl).protocol))
|
|
126
|
+
throw new Error('Plain web launch requires an explicit HTTP(S) --start-url');
|
|
127
|
+
}
|
|
125
128
|
opts.log = opts.log ?? ((line) => process.stderr.write(`${line}\n`));
|
|
126
129
|
opts.progress = opts.progress ?? (() => {});
|
|
127
130
|
return opts;
|
|
@@ -135,7 +138,7 @@ function runLaunch(opts, acquireLock) {
|
|
|
135
138
|
if (!opts.stopOnly) {
|
|
136
139
|
if (!fs.existsSync(opts.chromeBin))
|
|
137
140
|
throw new Error(`Chrome binary not found: ${opts.chromeBin}`);
|
|
138
|
-
if (!fs.existsSync(path.join(extensionDir, 'manifest.json'))) {
|
|
141
|
+
if (extensionDir && !fs.existsSync(path.join(extensionDir, 'manifest.json'))) {
|
|
139
142
|
throw new Error(
|
|
140
143
|
`Extension dist manifest not found: ${path.join(extensionDir, 'manifest.json')}`,
|
|
141
144
|
);
|
|
@@ -189,7 +192,7 @@ function runLaunch(opts, acquireLock) {
|
|
|
189
192
|
// run's Chrome would hold the lock and the write fails.
|
|
190
193
|
if (opts.stopOnly) return { stopped: true };
|
|
191
194
|
try {
|
|
192
|
-
removeExtensionWorkerRegistrations(profile);
|
|
195
|
+
if (extensionDir) removeExtensionWorkerRegistrations(profile);
|
|
193
196
|
} catch (error) {
|
|
194
197
|
const detail = error instanceof Error ? error.message : String(error);
|
|
195
198
|
throw new Error(
|
|
@@ -221,7 +224,7 @@ function runLaunch(opts, acquireLock) {
|
|
|
221
224
|
const initialUrl =
|
|
222
225
|
opts.startUrl || extensionPageUrl(extensionDir, opts.homePage) || 'chrome://extensions/';
|
|
223
226
|
const browser = launchBrowserResolution(opts.browserResolution, opts.chromeBin);
|
|
224
|
-
const loadsOverCdp = browser.extensionLoading === CDP_LOAD_UNPACKED;
|
|
227
|
+
const loadsOverCdp = Boolean(extensionDir) && browser.extensionLoading === CDP_LOAD_UNPACKED;
|
|
225
228
|
let cdpLoad = null;
|
|
226
229
|
let owner = null;
|
|
227
230
|
const runtimeNonce = createRuntimeIdentityNonce();
|
|
@@ -238,10 +241,14 @@ function runLaunch(opts, acquireLock) {
|
|
|
238
241
|
...isolatedProfileArgs(),
|
|
239
242
|
'--disable-default-apps',
|
|
240
243
|
'--disable-popup-blocking',
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
244
|
+
...(extensionDir
|
|
245
|
+
? [
|
|
246
|
+
'--disable-extensions-file-access-check',
|
|
247
|
+
'--disable-extensions-content-verification',
|
|
248
|
+
'--disable-features=ExtensionContentVerification,DisableLoadExtensionCommandLineSwitch',
|
|
249
|
+
...extensionLaunchArgs(extensionDir, browser.extensionLoading),
|
|
250
|
+
]
|
|
251
|
+
: []),
|
|
245
252
|
]);
|
|
246
253
|
// Initial tab: the caller's URL when provided, else the extension home. The
|
|
247
254
|
// extension home avoids leaving disposable chrome://newtab/ or extensions
|
|
@@ -333,7 +340,7 @@ function runLaunch(opts, acquireLock) {
|
|
|
333
340
|
cdpLoad = loadExtensionOverCdp(opts, browserPid, initialUrl, clearLaunchMarkers);
|
|
334
341
|
else if (application) openBackgroundWindow(opts, browserPid, initialUrl, clearLaunchMarkers);
|
|
335
342
|
owner = cdpLoad?.owner ?? ownerIdentity(cdpPort, profile, browserPid, log);
|
|
336
|
-
for (const problem of pruneExtraHomeTabs(cdpPort, extensionDir, opts)) {
|
|
343
|
+
for (const problem of extensionDir ? pruneExtraHomeTabs(cdpPort, extensionDir, opts) : []) {
|
|
337
344
|
log(`[launch] home-tab pruning: ${problem}`);
|
|
338
345
|
}
|
|
339
346
|
} finally {
|
|
@@ -658,7 +665,8 @@ function processIsOwnedBrowser(pid, profile, extensionDir, extensionOwnerRoot) {
|
|
|
658
665
|
const command = processCommand(pid);
|
|
659
666
|
if (!command) return false;
|
|
660
667
|
if (command.includes(`--user-data-dir=${profile}`)) return true;
|
|
661
|
-
const ownerRoot =
|
|
668
|
+
const ownerRoot =
|
|
669
|
+
extensionDir && extensionOwnerRoot ? extensionOwnerRoot(path.resolve(extensionDir)) : null;
|
|
662
670
|
return Boolean(ownerRoot && command.includes(`--load-extension=${ownerRoot}`));
|
|
663
671
|
}
|
|
664
672
|
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// A regular website in a slot-owned browser. The project owns its application server.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
|
|
6
|
+
import { CdpWebPage, listCdpTargets } from '@farmslot/recipe-runner/runtime/cdp';
|
|
7
|
+
|
|
8
|
+
import { assertSameOwner, cdpOwner } from './browser-cdp.cjs';
|
|
9
|
+
import { resolveBrowser } from './browser-resolver.cjs';
|
|
10
|
+
import { removeRuntimeIdentity } from './chrome-args.cjs';
|
|
11
|
+
import { launchBrowser } from './launch-browser.cjs';
|
|
12
|
+
import { stopProfileProcessesSync } from './validation-process-ownership.cjs';
|
|
13
|
+
|
|
14
|
+
/** @typedef {{
|
|
15
|
+
* manifestPath: string,
|
|
16
|
+
* url: (target: string) => string,
|
|
17
|
+
* serverLabel?: string
|
|
18
|
+
* }} PlainWebAdapterConfig
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function port(env = process.env) {
|
|
22
|
+
const value = Number(env.RECIPE_CDP_PORT ?? env.CDP_PORT);
|
|
23
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535)
|
|
24
|
+
throw new Error('The plain web runtime requires the assigned --cdp-port.');
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Runtime files and the browser profile always belong to the selected checkout. */
|
|
29
|
+
export function plainWebRuntimeDir(target) {
|
|
30
|
+
return path.join(fs.realpathSync(target), '.agent', 'web');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function browserOptions(target, env = process.env) {
|
|
34
|
+
const runtimeDir = plainWebRuntimeDir(target);
|
|
35
|
+
return {
|
|
36
|
+
cdpPort: port(env),
|
|
37
|
+
runtimeDir,
|
|
38
|
+
profile: path.join(runtimeDir, 'profile'),
|
|
39
|
+
chromeLog: path.join(runtimeDir, 'browser.log'),
|
|
40
|
+
chromePid: path.join(runtimeDir, 'browser.pid'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function owned(target, env = process.env) {
|
|
45
|
+
const options = browserOptions(target, env);
|
|
46
|
+
return cdpOwner(options.cdpPort, options.profile);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Use existing browser ownership, CDP actions and whole-run recording for a plain website.
|
|
51
|
+
* @param {PlainWebAdapterConfig} config
|
|
52
|
+
* @returns {import('@farmslot/adapter-sdk').ProjectRuntime}
|
|
53
|
+
*/
|
|
54
|
+
export function createPlainWebAdapter(config) {
|
|
55
|
+
const hint = 'Prepare the selected slot, then launch its plain web browser.';
|
|
56
|
+
async function page(target, env = process.env) {
|
|
57
|
+
const owner = owned(target, env);
|
|
58
|
+
const file = path.join(plainWebRuntimeDir(target), 'page.json');
|
|
59
|
+
const state = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
60
|
+
const selected = (await listCdpTargets('127.0.0.1', port(env))).find(
|
|
61
|
+
(entry) => entry.id === state.pageId && entry.type === 'page',
|
|
62
|
+
);
|
|
63
|
+
assertSameOwner(owner);
|
|
64
|
+
if (!selected) throw new Error(`The owned application page is unavailable. ${hint}`);
|
|
65
|
+
return selected;
|
|
66
|
+
}
|
|
67
|
+
/** @type {import('@farmslot/adapter-sdk').ProjectRuntime} */
|
|
68
|
+
const runtime = {
|
|
69
|
+
id: 'web',
|
|
70
|
+
sdkVersion: 1,
|
|
71
|
+
headless: false,
|
|
72
|
+
targets: ['web', 'cli'],
|
|
73
|
+
resolveSlotPorts() {},
|
|
74
|
+
async runtimeStatus(target) {
|
|
75
|
+
try {
|
|
76
|
+
await page(target);
|
|
77
|
+
const response = await fetch(config.url(target), { signal: AbortSignal.timeout(5000) });
|
|
78
|
+
await response.body?.cancel();
|
|
79
|
+
if (!response.ok) throw new Error(`Application returned HTTP ${response.status}.`);
|
|
80
|
+
return {
|
|
81
|
+
decision: 'ready',
|
|
82
|
+
reasons: [],
|
|
83
|
+
devServer: { label: config.serverLabel ?? 'website', status: 'up' },
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
decision: 'blocked',
|
|
88
|
+
reasons: [error instanceof Error ? error.message : String(error)],
|
|
89
|
+
nextAction: hint,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
devServer: {
|
|
94
|
+
label: 'browser',
|
|
95
|
+
describe: () => 'The owned website browser; the application server belongs to the project',
|
|
96
|
+
stop(target) {
|
|
97
|
+
stopPlainWebBrowser(target);
|
|
98
|
+
return {
|
|
99
|
+
kind: 'stopped',
|
|
100
|
+
status: 0,
|
|
101
|
+
summary: 'Website browser stopped; application server retained.',
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
logSources: (target) => [
|
|
106
|
+
{ label: 'Browser', path: path.join(plainWebRuntimeDir(target), 'browser.log') },
|
|
107
|
+
],
|
|
108
|
+
appLogSource: () => null,
|
|
109
|
+
hints: { launch: hint, relaunch: hint, runtimeProbeRecovery: () => hint },
|
|
110
|
+
harness: {
|
|
111
|
+
kind: 'built-in',
|
|
112
|
+
verify(target) {
|
|
113
|
+
return {
|
|
114
|
+
inProcess: async () => async (_args, io) => {
|
|
115
|
+
const status = await runtime.runtimeStatus(target);
|
|
116
|
+
if (status.decision !== 'ready') {
|
|
117
|
+
io.err(status.reasons.join('; '));
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
io.out('ready');
|
|
121
|
+
return 0;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
runtimeContext: { forbiddenFields: [] },
|
|
127
|
+
readiness: { captureProviders: ['cdp'], readyIndicator: () => 'ready' },
|
|
128
|
+
recording: {
|
|
129
|
+
async target(context) {
|
|
130
|
+
const owner = owned(context.projectRoot, context.env);
|
|
131
|
+
await page(context.projectRoot, context.env);
|
|
132
|
+
assertSameOwner(owner);
|
|
133
|
+
return { kind: 'pid', pid: owner.pid };
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
async launch({ target, jsonOutput }) {
|
|
137
|
+
const url = config.url(target);
|
|
138
|
+
const parsed = new URL(url);
|
|
139
|
+
if (!['http:', 'https:'].includes(parsed.protocol))
|
|
140
|
+
throw new Error('Expected an HTTP(S) application URL.');
|
|
141
|
+
const options = browserOptions(target);
|
|
142
|
+
const resolution = await resolveBrowser({
|
|
143
|
+
env: {
|
|
144
|
+
...process.env,
|
|
145
|
+
RECIPE_HARNESS_BROWSER: process.env.RECIPE_HARNESS_BROWSER ?? 'chrome',
|
|
146
|
+
},
|
|
147
|
+
cft: () => ({
|
|
148
|
+
error:
|
|
149
|
+
'Select installed Chrome or set RECIPE_HARNESS_CHROME_BIN to an installed Chromium executable.',
|
|
150
|
+
}),
|
|
151
|
+
cachePath: path.join(options.runtimeDir, 'browser-probe.json'),
|
|
152
|
+
launchMethod: process.platform === 'darwin' ? 'launch-services' : 'spawn',
|
|
153
|
+
display: 'headful',
|
|
154
|
+
});
|
|
155
|
+
launchBrowser({ ...options, chromeBin: resolution.bin, startUrl: url });
|
|
156
|
+
try {
|
|
157
|
+
const owner = owned(target);
|
|
158
|
+
const deadline = Date.now() + 15_000;
|
|
159
|
+
let candidates = [];
|
|
160
|
+
do {
|
|
161
|
+
candidates = (await listCdpTargets('127.0.0.1', port())).filter(
|
|
162
|
+
(entry) => entry.type === 'page' && entry.url.startsWith(`${parsed.origin}/`),
|
|
163
|
+
);
|
|
164
|
+
if (candidates.length) break;
|
|
165
|
+
await delay(100);
|
|
166
|
+
} while (Date.now() < deadline);
|
|
167
|
+
if (candidates.length !== 1)
|
|
168
|
+
throw new Error('Expected one application page in the owned browser.');
|
|
169
|
+
assertSameOwner(owner);
|
|
170
|
+
fs.writeFileSync(
|
|
171
|
+
path.join(options.runtimeDir, 'page.json'),
|
|
172
|
+
JSON.stringify({ pageId: candidates[0].id }),
|
|
173
|
+
{ mode: 0o600 },
|
|
174
|
+
);
|
|
175
|
+
const status = await runtime.runtimeStatus(target);
|
|
176
|
+
if (status.decision !== 'ready') throw new Error(status.reasons.join('; '));
|
|
177
|
+
} catch (error) {
|
|
178
|
+
stopPlainWebBrowser(target);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
if (jsonOutput)
|
|
182
|
+
console.log(
|
|
183
|
+
JSON.stringify({
|
|
184
|
+
schemaVersion: 1,
|
|
185
|
+
command: 'launch',
|
|
186
|
+
status: 'pass',
|
|
187
|
+
adapter: 'web',
|
|
188
|
+
target,
|
|
189
|
+
url,
|
|
190
|
+
cdpPort: options.cdpPort,
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
return 0;
|
|
194
|
+
},
|
|
195
|
+
actions: {
|
|
196
|
+
manifestPath: () => config.manifestPath,
|
|
197
|
+
semantic: [],
|
|
198
|
+
cdpTarget: { transport: 'cdp', probePath: '/json/version' },
|
|
199
|
+
ui: ({ createCdpWebUiTransport }) => ({
|
|
200
|
+
base: createCdpWebUiTransport({
|
|
201
|
+
getPage: async ({ context }) =>
|
|
202
|
+
CdpWebPage.connectToTarget(await page(context.projectRoot, context.env)),
|
|
203
|
+
}),
|
|
204
|
+
}),
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
return runtime;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Releases only the selected checkout's owned profile. The application server is retained. */
|
|
211
|
+
export function stopPlainWebBrowser(target) {
|
|
212
|
+
const runtimeDir = plainWebRuntimeDir(target);
|
|
213
|
+
// Stop also works without active port options, including a repeated closeout.
|
|
214
|
+
stopProfileProcessesSync(path.join(runtimeDir, 'profile'));
|
|
215
|
+
removeRuntimeIdentity(runtimeDir);
|
|
216
|
+
fs.rmSync(path.join(runtimeDir, 'browser.pid'), { force: true });
|
|
217
|
+
fs.rmSync(path.join(runtimeDir, 'page.json'), { force: true });
|
|
218
|
+
return { stopped: true };
|
|
219
|
+
}
|
package/src/web-dapp/index.mjs
CHANGED
|
@@ -90,6 +90,7 @@ export {
|
|
|
90
90
|
canonicalPath,
|
|
91
91
|
cdpPort,
|
|
92
92
|
DEFAULT_ACCOUNT,
|
|
93
|
+
DERIVED_ACCOUNTS_ENV,
|
|
93
94
|
fixtureAccount,
|
|
94
95
|
fixtureEntry,
|
|
95
96
|
fixturePath,
|
|
@@ -100,6 +101,8 @@ export {
|
|
|
100
101
|
resolveSigner,
|
|
101
102
|
SIGNER_MODES,
|
|
102
103
|
viemAccountFromEntry,
|
|
104
|
+
walletAccount,
|
|
105
|
+
walletAccounts,
|
|
103
106
|
webDappRuntimeDir,
|
|
104
107
|
webDappRuntimePath,
|
|
105
108
|
writePrivateJson,
|
package/src/web-dapp/launch.mjs
CHANGED
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
// wallet requests.
|
|
12
12
|
//
|
|
13
13
|
// Inputs (flags): --target <checkout> --cdp-port <port> --app-port <port>
|
|
14
|
-
// [--signer extension|injected] [--signer-module <path>] [--account <
|
|
14
|
+
// [--signer extension|injected] [--signer-module <path>] [--account <name|address>]
|
|
15
15
|
// [--fresh-profile] [--headless | --headful] [--slow-mo <ms>] [--start-chain <id>]
|
|
16
16
|
// [--json] [--network mainnet --mainnet-confirmation REAL_FUNDS] (testnet
|
|
17
17
|
// otherwise, enforced in the browser from the venue policy)
|
|
18
18
|
// env: TERMINAL_CHROME_BIN, RECIPE_WEB_DAPP_SIGNER_MODULE, RECIPE_WALLET_FIXTURE,
|
|
19
|
+
// RECIPE_WALLET_DERIVED_ACCOUNTS,
|
|
19
20
|
// TERMINAL_HEADLESS, TERMINAL_SLOW_MO, TERMINAL_WINDOW, TERMINAL_SCREEN
|
|
20
21
|
// (plus whatever the signer module reads)
|
|
21
22
|
// The browser is headful by default, in a window placed per slot (see
|
|
@@ -68,10 +69,9 @@ import {
|
|
|
68
69
|
} from './lib/processes.mjs';
|
|
69
70
|
import { assertLaunchNetwork, devServerTestnetDiagnostic } from './lib/readiness.mjs';
|
|
70
71
|
import {
|
|
71
|
-
fixtureEntry,
|
|
72
72
|
loadWalletFixture,
|
|
73
73
|
resolveSigner,
|
|
74
|
-
|
|
74
|
+
walletAccount,
|
|
75
75
|
webDappPolicy,
|
|
76
76
|
webDappRuntimeDir,
|
|
77
77
|
} from './lib/runtime.mjs';
|
|
@@ -86,7 +86,7 @@ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
86
86
|
const macosFocus = createRequire(import.meta.url)('../macos-focus.cjs');
|
|
87
87
|
|
|
88
88
|
function usage() {
|
|
89
|
-
return 'Usage: launch.mjs --target <checkout> --cdp-port <port> --app-port <port> [--signer extension|injected] [--signer-module <path>] [--account <name>] [--fresh-profile] [--headless | --headful] [--slow-mo <ms>] [--network testnet|mainnet --mainnet-confirmation REAL_FUNDS] [--start-chain <id>] [--json]';
|
|
89
|
+
return 'Usage: launch.mjs --target <checkout> --cdp-port <port> --app-port <port> [--signer extension|injected] [--signer-module <path>] [--account <name|address>] [--fresh-profile] [--headless | --headful] [--slow-mo <ms>] [--network testnet|mainnet --mainnet-confirmation REAL_FUNDS] [--start-chain <id>] [--json]';
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export function parseLaunchArgs(argv, env = process.env) {
|
|
@@ -490,7 +490,14 @@ export async function launchWebDappBrowser(
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
const { fixture } = await loadWalletFixture(target, env);
|
|
493
|
-
|
|
493
|
+
// --account is an account name or address; the browser runs under its name.
|
|
494
|
+
const selected = walletAccount(fixture, args.account, target, env);
|
|
495
|
+
const account = {
|
|
496
|
+
name: selected.name,
|
|
497
|
+
address: selected.address,
|
|
498
|
+
secret: selected.secret,
|
|
499
|
+
index: selected.index,
|
|
500
|
+
};
|
|
494
501
|
|
|
495
502
|
const previous = readStateFile(stateFile);
|
|
496
503
|
const reusable =
|
|
@@ -499,7 +506,8 @@ export async function launchWebDappBrowser(
|
|
|
499
506
|
previous.signer === signer &&
|
|
500
507
|
previous.cdpPort === cdpPort &&
|
|
501
508
|
previous.appOrigin === appOrigin &&
|
|
502
|
-
previous.account?.name ===
|
|
509
|
+
previous.account?.name === account.name &&
|
|
510
|
+
previous.account?.address?.toLowerCase() === account.address.toLowerCase() &&
|
|
503
511
|
previous.browser?.headless === headless &&
|
|
504
512
|
(previous.network ?? 'testnet') === network &&
|
|
505
513
|
previous.networkEnforcement?.fingerprint === enforcementFingerprint &&
|
|
@@ -507,7 +515,7 @@ export async function launchWebDappBrowser(
|
|
|
507
515
|
ownsProcess('wallet-host', previous.hostPid, runtime);
|
|
508
516
|
if (reusable) {
|
|
509
517
|
await browserWebSocketUrl(cdpPort, 5000);
|
|
510
|
-
log(`reusing browser pid ${previous.browserPid} (${signer}, ${
|
|
518
|
+
log(`reusing browser pid ${previous.browserPid} (${signer}, ${account.name})`);
|
|
511
519
|
const state = { ...previous, slowMoMs };
|
|
512
520
|
if (previous.slowMoMs !== slowMoMs)
|
|
513
521
|
writePrivateFile(stateFile, `${JSON.stringify(state, null, 2)}\n`);
|
|
@@ -518,7 +526,7 @@ export async function launchWebDappBrowser(
|
|
|
518
526
|
const browser = await resolveBrowser(env);
|
|
519
527
|
const profile = path.join(
|
|
520
528
|
runtime,
|
|
521
|
-
`profile-${signer}-${
|
|
529
|
+
`profile-${signer}-${account.name.replace(/[^A-Za-z0-9_-]/gu, '_')}`,
|
|
522
530
|
);
|
|
523
531
|
if (args['fresh-profile'] && existsSync(profile)) {
|
|
524
532
|
const retired = `${profile}.retired-${Date.now()}`;
|
|
@@ -571,7 +579,7 @@ export async function launchWebDappBrowser(
|
|
|
571
579
|
target,
|
|
572
580
|
runtime,
|
|
573
581
|
profile,
|
|
574
|
-
account:
|
|
582
|
+
account: account.name,
|
|
575
583
|
fixture,
|
|
576
584
|
env,
|
|
577
585
|
freshProfile,
|
|
@@ -683,7 +691,7 @@ export async function launchWebDappBrowser(
|
|
|
683
691
|
'--target',
|
|
684
692
|
target,
|
|
685
693
|
'--account',
|
|
686
|
-
|
|
694
|
+
account.name,
|
|
687
695
|
'--start-chain',
|
|
688
696
|
String(args['start-chain'] ?? policy.startChain),
|
|
689
697
|
'--network',
|
|
@@ -758,7 +766,7 @@ export async function launchWebDappBrowser(
|
|
|
758
766
|
appListeners: cdpListenerPids(appPort),
|
|
759
767
|
cdpPort,
|
|
760
768
|
appOrigin,
|
|
761
|
-
account
|
|
769
|
+
account,
|
|
762
770
|
browserPid,
|
|
763
771
|
hostPid,
|
|
764
772
|
browser: {
|
|
@@ -11,10 +11,9 @@ import path from 'node:path';
|
|
|
11
11
|
import { ownsProcess, pidAlive, portFree } from './processes.mjs';
|
|
12
12
|
import {
|
|
13
13
|
canonicalPath,
|
|
14
|
-
fixtureEntry,
|
|
15
14
|
loadWalletFixture,
|
|
16
15
|
readBrowserState,
|
|
17
|
-
|
|
16
|
+
walletAccount,
|
|
18
17
|
webDappPolicy,
|
|
19
18
|
webDappRuntimeDir,
|
|
20
19
|
} from './runtime.mjs';
|
|
@@ -359,7 +358,7 @@ export async function webDappReadiness({
|
|
|
359
358
|
|
|
360
359
|
try {
|
|
361
360
|
const { file, fixture } = await loadWalletFixture(target, env);
|
|
362
|
-
const address =
|
|
361
|
+
const { address } = walletAccount(fixture, account, target, env);
|
|
363
362
|
checks.push(
|
|
364
363
|
check('wallet-fixture', true, `${path.basename(file)}: account ${account} -> ${address}`),
|
|
365
364
|
);
|
|
@@ -151,22 +151,26 @@ export function fixtureEntry(fixture, name) {
|
|
|
151
151
|
.join(', ');
|
|
152
152
|
throw new Error(`wallet fixture has no account named "${name}". Available: ${names}.`);
|
|
153
153
|
}
|
|
154
|
+
return checkedEntry(entry);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function checkedEntry(entry) {
|
|
154
158
|
if (entry.type !== 'mnemonic' && entry.type !== 'privateKey') {
|
|
155
159
|
throw new Error(
|
|
156
|
-
`wallet fixture account "${name}" must be mnemonic or privateKey, got "${entry.type}".`,
|
|
160
|
+
`wallet fixture account "${entry.name}" must be mnemonic or privateKey, got "${entry.type}".`,
|
|
157
161
|
);
|
|
158
162
|
}
|
|
159
163
|
if (typeof entry.value !== 'string' || !entry.value.trim()) {
|
|
160
|
-
throw new Error(`wallet fixture account "${name}" has no value.`);
|
|
164
|
+
throw new Error(`wallet fixture account "${entry.name}" has no value.`);
|
|
161
165
|
}
|
|
162
166
|
return entry;
|
|
163
167
|
}
|
|
164
168
|
|
|
165
|
-
// Mnemonics use BIP-44
|
|
166
|
-
// Extension fixture flow imports.
|
|
167
|
-
export function viemAccountFromEntry(entry, projectRoot) {
|
|
169
|
+
// Mnemonics use BIP-44 m/44'/60'/0'/0/<addressIndex>, the MetaMask derivation
|
|
170
|
+
// the Extension fixture flow imports; index 0 is the wallet's first account.
|
|
171
|
+
export function viemAccountFromEntry(entry, projectRoot, addressIndex = 0) {
|
|
168
172
|
const { mnemonicToAccount, privateKeyToAccount } = loadViemAccounts(projectRoot);
|
|
169
|
-
if (entry.type === 'mnemonic') return mnemonicToAccount(entry.value.trim(), { addressIndex
|
|
173
|
+
if (entry.type === 'mnemonic') return mnemonicToAccount(entry.value.trim(), { addressIndex });
|
|
170
174
|
const raw = entry.value.trim();
|
|
171
175
|
const key = raw.startsWith('0x') ? raw : `0x${raw}`;
|
|
172
176
|
if (!/^0x[0-9a-fA-F]{64}$/u.test(key)) {
|
|
@@ -175,9 +179,123 @@ export function viemAccountFromEntry(entry, projectRoot) {
|
|
|
175
179
|
return privateKeyToAccount(key);
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
// Named HD derivations of the fixture's mnemonic entries that the host adds,
|
|
183
|
+
// e.g. a team's shared test account at a higher index: JSON
|
|
184
|
+
// [{ "name", "secret": <mnemonic entry name>, "index", "address"? }]. Names and
|
|
185
|
+
// indexes only; the key stays in the fixture.
|
|
186
|
+
export const DERIVED_ACCOUNTS_ENV = 'RECIPE_WALLET_DERIVED_ACCOUNTS';
|
|
187
|
+
|
|
188
|
+
export function derivedAccounts(env = process.env) {
|
|
189
|
+
const raw = env[DERIVED_ACCOUNTS_ENV];
|
|
190
|
+
if (!raw) return [];
|
|
191
|
+
let value;
|
|
192
|
+
try {
|
|
193
|
+
value = JSON.parse(raw);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
throw new Error(`${DERIVED_ACCOUNTS_ENV} is not JSON: ${error.message}`);
|
|
196
|
+
}
|
|
197
|
+
const valid = (item) =>
|
|
198
|
+
item &&
|
|
199
|
+
typeof item.name === 'string' &&
|
|
200
|
+
item.name.trim() &&
|
|
201
|
+
typeof item.secret === 'string' &&
|
|
202
|
+
item.secret.trim() &&
|
|
203
|
+
Number.isInteger(item.index) &&
|
|
204
|
+
item.index >= 0 &&
|
|
205
|
+
(item.address === undefined || /^0x[0-9a-fA-F]{40}$/u.test(item.address));
|
|
206
|
+
if (!Array.isArray(value) || !value.every(valid)) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`${DERIVED_ACCOUNTS_ENV} must be an array of { name, secret, index, address? } (index a non-negative integer).`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function mnemonicCount(entry) {
|
|
215
|
+
const count = Number(entry.count ?? entry.numberOfAccounts ?? 1);
|
|
216
|
+
if (!Number.isInteger(count) || count < 1 || count > 100) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`wallet fixture account "${entry.name}" count must be an integer from 1 through 100.`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return count;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// names[index] when the entry gives one; a single account keeps the entry's
|
|
225
|
+
// name; otherwise dev1 with count 3 is dev1, dev2, dev3 (a name without a
|
|
226
|
+
// trailing number gets one appended).
|
|
227
|
+
function derivedName(entry, index, count) {
|
|
228
|
+
const explicit = Array.isArray(entry.names) ? entry.names[index] : undefined;
|
|
229
|
+
if (typeof explicit === 'string' && explicit.trim()) return explicit.trim();
|
|
230
|
+
if (count === 1) return entry.name;
|
|
231
|
+
const match = /^(.*?)(\d+)$/u.exec(entry.name);
|
|
232
|
+
return match ? `${match[1]}${Number(match[2]) + index}` : `${entry.name}${index + 1}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Every account the fixture offers: each mnemonic entry expanded to its count,
|
|
237
|
+
* each private key, then the host's named derivations (DERIVED_ACCOUNTS_ENV)
|
|
238
|
+
* of the secrets this fixture holds. `account` is the viem account holding the
|
|
239
|
+
* key: sign with it, never log or persist it.
|
|
240
|
+
*/
|
|
241
|
+
export function walletAccounts(fixture, projectRoot, env = process.env) {
|
|
242
|
+
const accounts = [];
|
|
243
|
+
for (const entry of fixture.accounts) {
|
|
244
|
+
if (entry?.type !== 'mnemonic' && entry?.type !== 'privateKey') continue;
|
|
245
|
+
checkedEntry(entry);
|
|
246
|
+
const count = entry.type === 'mnemonic' ? mnemonicCount(entry) : 1;
|
|
247
|
+
for (let index = 0; index < count; index += 1) {
|
|
248
|
+
const account = viemAccountFromEntry(entry, projectRoot, index);
|
|
249
|
+
accounts.push({
|
|
250
|
+
name: derivedName(entry, index, count),
|
|
251
|
+
address: account.address,
|
|
252
|
+
secret: entry.name,
|
|
253
|
+
index,
|
|
254
|
+
account,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const item of derivedAccounts(env)) {
|
|
259
|
+
const entry = fixture.accounts.find(
|
|
260
|
+
(candidate) => candidate?.type === 'mnemonic' && candidate.name === item.secret,
|
|
261
|
+
);
|
|
262
|
+
if (!entry) continue;
|
|
263
|
+
const account = viemAccountFromEntry(checkedEntry(entry), projectRoot, item.index);
|
|
264
|
+
if (item.address && item.address.toLowerCase() !== account.address.toLowerCase()) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`wallet account "${item.name}" derives to ${account.address} from "${item.secret}" index ${item.index}, not the declared ${item.address}.`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
accounts.push({
|
|
270
|
+
name: item.name,
|
|
271
|
+
address: account.address,
|
|
272
|
+
secret: item.secret,
|
|
273
|
+
index: item.index,
|
|
274
|
+
account,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return accounts;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// The fixture account a selector names: an account name, else an address.
|
|
281
|
+
export function walletAccount(fixture, selector, projectRoot, env = process.env) {
|
|
282
|
+
const accounts = walletAccounts(fixture, projectRoot, env);
|
|
283
|
+
const wanted = String(selector ?? '').trim();
|
|
284
|
+
const found =
|
|
285
|
+
accounts.find((account) => account.name === wanted) ??
|
|
286
|
+
(/^0x[0-9a-fA-F]{40}$/u.test(wanted)
|
|
287
|
+
? accounts.find((account) => account.address.toLowerCase() === wanted.toLowerCase())
|
|
288
|
+
: undefined);
|
|
289
|
+
if (!found) {
|
|
290
|
+
const names = accounts.map((account) => account.name).join(', ');
|
|
291
|
+
throw new Error(`wallet fixture has no account named "${wanted}". Available: ${names}.`);
|
|
292
|
+
}
|
|
293
|
+
return found;
|
|
294
|
+
}
|
|
295
|
+
|
|
178
296
|
export async function fixtureAccount(projectRoot, name, env = process.env) {
|
|
179
297
|
const { fixture } = await loadWalletFixture(projectRoot, env);
|
|
180
|
-
return
|
|
298
|
+
return walletAccount(fixture, name, projectRoot, env).account;
|
|
181
299
|
}
|
|
182
300
|
|
|
183
301
|
// Address-only view of the account a node targets: an explicit address wins
|