@farmslot/adapter-web 0.5.0 → 0.6.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 +5 -0
- package/README.md +24 -1
- package/dist/web-dapp/cleanup.d.mts +15 -0
- package/dist/web-dapp/index.d.mts +123 -0
- package/dist/web-dapp/inject.d.mts +12 -0
- package/dist/web-dapp/launch.d.mts +56 -0
- package/dist/web-dapp/lib/blocked-hosts.d.mts +3 -0
- package/dist/web-dapp/lib/cdp-client.d.mts +8 -0
- package/dist/web-dapp/lib/display.d.mts +26 -0
- package/dist/web-dapp/lib/hud.d.mts +14 -0
- package/dist/web-dapp/lib/paths.d.mts +11 -0
- package/dist/web-dapp/lib/policy-fence.d.mts +18 -0
- package/dist/web-dapp/lib/policy.d.mts +4 -0
- package/dist/web-dapp/lib/processes.d.mts +57 -0
- package/dist/web-dapp/lib/readiness.d.mts +85 -0
- package/dist/web-dapp/lib/runtime.d.mts +29 -0
- package/dist/web-dapp/lib/signers.d.mts +13 -0
- package/dist/web-dapp/lib/viem.d.mts +1 -0
- package/dist/web-dapp/stop.d.mts +2 -0
- package/dist/web-dapp/verify.d.mts +2 -0
- package/dist/web-dapp/wallet-host.d.mts +2 -0
- package/package.json +17 -10
- package/src/web-dapp/cleanup.mjs +74 -0
- package/src/web-dapp/index.mjs +590 -0
- package/src/web-dapp/inject.mjs +100 -0
- package/src/web-dapp/launch.mjs +806 -0
- package/src/web-dapp/lib/blocked-hosts.mjs +24 -0
- package/src/web-dapp/lib/cdp-client.mjs +86 -0
- package/src/web-dapp/lib/display.mjs +169 -0
- package/src/web-dapp/lib/hud.mjs +156 -0
- package/src/web-dapp/lib/paths.mjs +55 -0
- package/src/web-dapp/lib/policy-fence.mjs +128 -0
- package/src/web-dapp/lib/policy.mjs +108 -0
- package/src/web-dapp/lib/processes.mjs +215 -0
- package/src/web-dapp/lib/readiness.mjs +438 -0
- package/src/web-dapp/lib/runtime.mjs +195 -0
- package/src/web-dapp/lib/signers.mjs +105 -0
- package/src/web-dapp/lib/viem.mjs +26 -0
- package/src/web-dapp/stop.mjs +44 -0
- package/src/web-dapp/verify.mjs +68 -0
- package/src/web-dapp/wallet-host.mjs +728 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cleanup.mjs — `mm-harness cleanup --adapter <web-dapp adapter>`: stop the slot
|
|
3
|
+
// browser and wallet host, then remove the slot's runtime state and the installed
|
|
4
|
+
// wallet fixture. Browser profiles are kept unless --reset-profile.
|
|
5
|
+
//
|
|
6
|
+
// Inputs: --target <checkout> [--cdp-port <port>] [--reset-profile]
|
|
7
|
+
// Outputs: JSON summary on stdout. Exit 0 cleaned; 1 the CDP port stayed busy.
|
|
8
|
+
// Never touches: product source files, the Next.js dev server.
|
|
9
|
+
|
|
10
|
+
import { existsSync, readdirSync, rmSync } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
import { recipeHarnessPath, walletFixturePath } from './lib/paths.mjs';
|
|
15
|
+
import { stopConsoleCollector, stopWebDappBrowser } from './lib/processes.mjs';
|
|
16
|
+
import { webDappPolicy, webDappRuntimeDir } from './lib/runtime.mjs';
|
|
17
|
+
|
|
18
|
+
const usage = 'Usage: cleanup.mjs --target <checkout> [--cdp-port <port>] [--reset-profile]';
|
|
19
|
+
|
|
20
|
+
export async function cleanupWebDappRuntime({ target, cdpPort, resetProfile = false }) {
|
|
21
|
+
const root = path.resolve(target);
|
|
22
|
+
const stop = await stopWebDappBrowser(root, { cdpPort });
|
|
23
|
+
await stopConsoleCollector(root);
|
|
24
|
+
const runtime = webDappRuntimeDir(root);
|
|
25
|
+
const removed = [];
|
|
26
|
+
if (existsSync(runtime)) {
|
|
27
|
+
for (const entry of readdirSync(runtime)) {
|
|
28
|
+
if (entry.startsWith('profile-') && !resetProfile) continue;
|
|
29
|
+
rmSync(path.join(runtime, entry), { recursive: true, force: true });
|
|
30
|
+
removed.push(entry);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const overlay = recipeHarnessPath(root, webDappPolicy().adapterId);
|
|
34
|
+
if (existsSync(overlay)) {
|
|
35
|
+
rmSync(overlay, { recursive: true, force: true });
|
|
36
|
+
removed.push(path.relative(root, overlay));
|
|
37
|
+
}
|
|
38
|
+
const fixture = walletFixturePath(root);
|
|
39
|
+
if (existsSync(fixture)) {
|
|
40
|
+
rmSync(fixture, { force: true });
|
|
41
|
+
removed.push(path.relative(root, fixture));
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
status: 'pass',
|
|
45
|
+
target: root,
|
|
46
|
+
stopped: stop.stopped,
|
|
47
|
+
cdpPortFree: stop.cdpPortFree,
|
|
48
|
+
removed,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
53
|
+
const argv = process.argv.slice(2);
|
|
54
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
55
|
+
process.stdout.write(`${usage}\n`);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
const value = (flag) => {
|
|
59
|
+
const index = argv.indexOf(flag);
|
|
60
|
+
return index >= 0 ? argv[index + 1] : undefined;
|
|
61
|
+
};
|
|
62
|
+
const rawPort = value('--cdp-port') ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
|
|
63
|
+
try {
|
|
64
|
+
const result = await cleanupWebDappRuntime({
|
|
65
|
+
target: value('--target') ?? process.cwd(),
|
|
66
|
+
cdpPort: rawPort ? Number(rawPort) : undefined,
|
|
67
|
+
resetProfile: argv.includes('--reset-profile'),
|
|
68
|
+
});
|
|
69
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
process.stdout.write(`${JSON.stringify({ status: 'fail', error: error.message })}\n`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// The generic web-dapp lifecycle as an adapter-sdk PlatformAdapter: a web app
|
|
3
|
+
// under test whose dev server the slot owns; the adapter owns the slot browser
|
|
4
|
+
// (an extension signer or the injected strict wallet) and its wallet host. What
|
|
5
|
+
// it knows about the app's venue comes from the venue policy of the adapter that
|
|
6
|
+
// extends it, bound into RECIPE_WEB_DAPP_POLICY by `bindWebDappPolicy`. Selected
|
|
7
|
+
// on its own, web-dapp has no policy: launch, verify and runtime status refuse,
|
|
8
|
+
// doctor reports it.
|
|
9
|
+
//
|
|
10
|
+
// The leaf scripts (launch, wallet-host, inject, verify, stop, cleanup) are CLIs
|
|
11
|
+
// that run as their own node processes; `webDappLeafPath` names them.
|
|
12
|
+
|
|
13
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
import { recipeRuntimePath, shellQuote } from './lib/paths.mjs';
|
|
19
|
+
import { POLICY_ENV, webDappAdapterId, webDappPolicy } from './lib/policy.mjs';
|
|
20
|
+
import { check, webDappReadiness } from './lib/readiness.mjs';
|
|
21
|
+
import { defaultSigner, loadSigners, SIGNER_MODULE_ENV } from './lib/signers.mjs';
|
|
22
|
+
import { resolveBrowser } from './launch.mjs';
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
loadSigners,
|
|
26
|
+
POLICY_ENV,
|
|
27
|
+
check as readinessCheck,
|
|
28
|
+
SIGNER_MODULE_ENV,
|
|
29
|
+
webDappAdapterId,
|
|
30
|
+
webDappPolicy,
|
|
31
|
+
};
|
|
32
|
+
export { cleanupWebDappRuntime } from './cleanup.mjs';
|
|
33
|
+
export { installWebDappRuntime } from './inject.mjs';
|
|
34
|
+
export {
|
|
35
|
+
backgroundWindowParams,
|
|
36
|
+
checkFocusAfterLaunch,
|
|
37
|
+
chromeForTestingCandidates,
|
|
38
|
+
LAUNCH_SERVICES,
|
|
39
|
+
LAUNCH_TIMEOUTS,
|
|
40
|
+
launchMethodFor,
|
|
41
|
+
launchWebDappBrowser,
|
|
42
|
+
macApplicationForExecutable,
|
|
43
|
+
parseLaunchArgs,
|
|
44
|
+
resolveBrowser,
|
|
45
|
+
SPAWN,
|
|
46
|
+
spawnDetached,
|
|
47
|
+
} from './launch.mjs';
|
|
48
|
+
export { hostOf, hostResolverRules, isBlockedUrl } from './lib/blocked-hosts.mjs';
|
|
49
|
+
export { browserWebSocketUrl, CdpClient } from './lib/cdp-client.mjs';
|
|
50
|
+
export {
|
|
51
|
+
recipeHarnessPath,
|
|
52
|
+
recipeRuntimeDir,
|
|
53
|
+
recipeRuntimePath,
|
|
54
|
+
shellQuote,
|
|
55
|
+
walletFixturePath,
|
|
56
|
+
} from './lib/paths.mjs';
|
|
57
|
+
export {
|
|
58
|
+
assertPolicyDigest,
|
|
59
|
+
fencePolicy,
|
|
60
|
+
POLICY_DIGEST_ENV,
|
|
61
|
+
PolicyFenceError,
|
|
62
|
+
} from './lib/policy-fence.mjs';
|
|
63
|
+
export {
|
|
64
|
+
cdpListenerPids,
|
|
65
|
+
ownedBrowserPids,
|
|
66
|
+
OWNER_FLAG,
|
|
67
|
+
OWNER_KINDS,
|
|
68
|
+
ownerMarker,
|
|
69
|
+
ownsProcess,
|
|
70
|
+
pidAlive,
|
|
71
|
+
portFree,
|
|
72
|
+
stopConsoleCollector,
|
|
73
|
+
stopWebDappBrowser,
|
|
74
|
+
terminate,
|
|
75
|
+
} from './lib/processes.mjs';
|
|
76
|
+
export {
|
|
77
|
+
assertLaunchNetwork,
|
|
78
|
+
DEV_SERVER_PID_FILE,
|
|
79
|
+
devServerTestnetDiagnostic,
|
|
80
|
+
pythonFor,
|
|
81
|
+
webDappReadiness,
|
|
82
|
+
} from './lib/readiness.mjs';
|
|
83
|
+
export {
|
|
84
|
+
accountName,
|
|
85
|
+
appOrigin,
|
|
86
|
+
appPort,
|
|
87
|
+
canonicalPath,
|
|
88
|
+
cdpPort,
|
|
89
|
+
DEFAULT_ACCOUNT,
|
|
90
|
+
fixtureAccount,
|
|
91
|
+
fixtureEntry,
|
|
92
|
+
fixturePath,
|
|
93
|
+
loadWalletFixture,
|
|
94
|
+
readBrowserState,
|
|
95
|
+
readJsonFile,
|
|
96
|
+
resolveAccountAddress,
|
|
97
|
+
resolveSigner,
|
|
98
|
+
SIGNER_MODES,
|
|
99
|
+
viemAccountFromEntry,
|
|
100
|
+
webDappRuntimeDir,
|
|
101
|
+
webDappRuntimePath,
|
|
102
|
+
writePrivateJson,
|
|
103
|
+
} from './lib/runtime.mjs';
|
|
104
|
+
|
|
105
|
+
const LEAVES = Object.freeze(['launch', 'wallet-host', 'inject', 'verify', 'stop', 'cleanup']);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The absolute path of a leaf script (`launch`, `wallet-host`, `inject`,
|
|
109
|
+
* `verify`, `stop` or `cleanup`).
|
|
110
|
+
* @param {'launch' | 'wallet-host' | 'inject' | 'verify' | 'stop' | 'cleanup'} name
|
|
111
|
+
*/
|
|
112
|
+
export function webDappLeafPath(name) {
|
|
113
|
+
if (!LEAVES.includes(name))
|
|
114
|
+
throw new Error(`unknown web-dapp leaf ${JSON.stringify(name)}; one of ${LEAVES.join(', ')}.`);
|
|
115
|
+
return fileURLToPath(new URL(`./${name}.mjs`, import.meta.url));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Console files, next to the slot browser's other runtime files:
|
|
119
|
+
// app-console.log the app page(s): console calls and uncaught exceptions
|
|
120
|
+
// extension-console.log the wallet extension's service worker and UI pages (signer=extension)
|
|
121
|
+
export const WEB_DAPP_CONSOLE_FILES = Object.freeze({
|
|
122
|
+
app: 'app-console.log',
|
|
123
|
+
extension: 'extension-console.log',
|
|
124
|
+
collectorLog: 'console-tail.log',
|
|
125
|
+
pidFile: 'console-tail.pid',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A file in the slot's runtime directory (the realpath of the target, as the
|
|
130
|
+
* launcher uses): <checkout>/<runtime>/<adapter id>/<name>.
|
|
131
|
+
* @param {string} target
|
|
132
|
+
* @param {string} name
|
|
133
|
+
*/
|
|
134
|
+
export function webDappRuntimeFile(target, name) {
|
|
135
|
+
let root = path.resolve(target);
|
|
136
|
+
try {
|
|
137
|
+
root = fs.realpathSync(root);
|
|
138
|
+
} catch {
|
|
139
|
+
// A missing checkout keeps the resolved path; callers report it.
|
|
140
|
+
}
|
|
141
|
+
return recipeRuntimePath(root, webDappAdapterId(), name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Flags only the web-dapp launcher reads; other adapters reject them instead
|
|
145
|
+
// of silently ignoring them.
|
|
146
|
+
export const WEB_DAPP_ONLY_LAUNCH_OPTIONS = Object.freeze({
|
|
147
|
+
signer: '--signer',
|
|
148
|
+
account: '--account',
|
|
149
|
+
freshProfile: '--fresh-profile',
|
|
150
|
+
headless: '--headless',
|
|
151
|
+
headful: '--headful',
|
|
152
|
+
slowMo: '--slow-mo',
|
|
153
|
+
network: '--network',
|
|
154
|
+
mainnetConfirmation: '--mainnet-confirmation',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/** @param {Record<string, string | boolean>} options */
|
|
158
|
+
export function webDappOnlyLaunchFlags(options) {
|
|
159
|
+
return Object.entries(WEB_DAPP_ONLY_LAUNCH_OPTIONS)
|
|
160
|
+
.filter(([key]) => options[key] !== undefined && options[key] !== false)
|
|
161
|
+
.map(([, name]) => name);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The leaf prints one JSON document (compact with --json, indented without).
|
|
166
|
+
* Parse it whole; fall back to the last line for any log noise before it.
|
|
167
|
+
* @param {string} output
|
|
168
|
+
* @returns {Record<string, unknown>}
|
|
169
|
+
*/
|
|
170
|
+
export function parseWebDappLauncherOutput(output) {
|
|
171
|
+
const text = output.trim();
|
|
172
|
+
if (!text)
|
|
173
|
+
return { status: 'fail', error: `the ${webDappAdapterId()} launcher printed no summary` };
|
|
174
|
+
try {
|
|
175
|
+
return JSON.parse(text);
|
|
176
|
+
} catch {
|
|
177
|
+
const last =
|
|
178
|
+
text
|
|
179
|
+
.split('\n')
|
|
180
|
+
.filter((line) => line.trim())
|
|
181
|
+
.pop() ?? '';
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(last);
|
|
184
|
+
} catch {
|
|
185
|
+
return {
|
|
186
|
+
status: 'fail',
|
|
187
|
+
error: `the ${webDappAdapterId()} launcher printed no JSON summary: ${last.slice(0, 200)}`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @param {...string} names */
|
|
194
|
+
function portFromEnv(...names) {
|
|
195
|
+
for (const name of names) {
|
|
196
|
+
const raw = process.env[name];
|
|
197
|
+
if (raw && /^\d+$/u.test(raw)) return Number(raw);
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function webDappAppPort() {
|
|
203
|
+
return portFromEnv('TERMINAL_APP_PORT', 'RECIPE_WATCHER_PORT', 'WATCHER_PORT');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function webDappCdpPort() {
|
|
207
|
+
return portFromEnv('RECIPE_CDP_PORT', 'CDP_PORT');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// The running browser's signer, else the requested one, else the default.
|
|
211
|
+
/**
|
|
212
|
+
* @param {string} target
|
|
213
|
+
* @param {string} fallback
|
|
214
|
+
*/
|
|
215
|
+
function webDappSignerFor(target, fallback) {
|
|
216
|
+
try {
|
|
217
|
+
const state = JSON.parse(fs.readFileSync(webDappRuntimeFile(target, 'browser.json'), 'utf8'));
|
|
218
|
+
if (typeof state.signer === 'string') return state.signer;
|
|
219
|
+
} catch {
|
|
220
|
+
// No running slot browser.
|
|
221
|
+
}
|
|
222
|
+
return process.env.TERMINAL_SIGNER || fallback;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Bind the venue policy of an adapter that extends web-dapp: its module path
|
|
227
|
+
* goes to RECIPE_WEB_DAPP_POLICY, which web-dapp's members, leaf processes and
|
|
228
|
+
* live actions read for this command. Called when the host adopts the adapter.
|
|
229
|
+
* @template {{ extends?: string, policy?: { module?: unknown } }} T
|
|
230
|
+
* @param {T} adapter
|
|
231
|
+
* @returns {T}
|
|
232
|
+
*/
|
|
233
|
+
export function bindWebDappPolicy(adapter) {
|
|
234
|
+
if (adapter.extends === 'web-dapp' && typeof adapter.policy?.module === 'string') {
|
|
235
|
+
process.env[POLICY_ENV] = adapter.policy.module;
|
|
236
|
+
}
|
|
237
|
+
return adapter;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Mainnet attempts the slot browser blocked or the wallet refused, and wallet
|
|
241
|
+
// log entries that could not be attributed, during the run (whatever a
|
|
242
|
+
// read_signatures cursor later skips). The launch's own probes are excluded.
|
|
243
|
+
/**
|
|
244
|
+
* @param {readonly string[]} lines
|
|
245
|
+
* @returns {import('@farmslot/adapter-sdk').AdapterLogFinding[]}
|
|
246
|
+
*/
|
|
247
|
+
export function walletRequestFindings(lines) {
|
|
248
|
+
/** @type {import('@farmslot/adapter-sdk').AdapterLogFinding[]} */
|
|
249
|
+
const findings = [];
|
|
250
|
+
for (const line of lines) {
|
|
251
|
+
let entry;
|
|
252
|
+
try {
|
|
253
|
+
entry = JSON.parse(line);
|
|
254
|
+
} catch {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) continue;
|
|
258
|
+
let text = null;
|
|
259
|
+
if (entry.kind === 'blocked-mainnet' && entry.probe !== true)
|
|
260
|
+
text = `blocked-mainnet: ${String(entry.transport ?? 'request')} to ${String(entry.url ?? '?')}`;
|
|
261
|
+
else if (entry.kind === 'refused-mainnet')
|
|
262
|
+
text = `refused-mainnet: ${String(entry.method ?? '?')} (${String(entry.reason ?? '?')})`;
|
|
263
|
+
else if (entry.kind === 'unattributed')
|
|
264
|
+
text = `unattributed wallet ${entry.binding === 'request' ? 'request' : 'log entry'}${typeof entry.method === 'string' ? ` (${entry.method})` : ''}`;
|
|
265
|
+
if (text) findings.push({ level: 'error', source: 'wallet', text });
|
|
266
|
+
}
|
|
267
|
+
return findings;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @param {Record<string, string | boolean>} options
|
|
272
|
+
* @param {string} key
|
|
273
|
+
*/
|
|
274
|
+
const str = (options, key) => (typeof options[key] === 'string' ? options[key] : undefined);
|
|
275
|
+
/**
|
|
276
|
+
* @param {Record<string, string | boolean>} options
|
|
277
|
+
* @param {string} key
|
|
278
|
+
*/
|
|
279
|
+
const flag = (options, key) => options[key] === true;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @typedef {Partial<Omit<import('@farmslot/adapter-sdk').PlatformAdapter, 'id' | 'sdkVersion' | 'harness'>> & {
|
|
283
|
+
* harness?: Partial<import('@farmslot/adapter-sdk').AdapterHarness>,
|
|
284
|
+
* }} WebDappAdapterHooks
|
|
285
|
+
*/
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @typedef {object} WebDappAdapterOptions
|
|
289
|
+
* @property {string} [id] The adapter id; web-dapp by default.
|
|
290
|
+
* @property {string} [signerModule] Absolute path of the ESM module exporting
|
|
291
|
+
* `signers` (see lib/signers.mjs). The one source of the signer hooks: readiness
|
|
292
|
+
* loads it in this process and launch, verify and the wallet host load it in theirs.
|
|
293
|
+
* @property {string} [cli] The host's bin name, for `Next:` lines. farmslot-recipe by default.
|
|
294
|
+
* @property {WebDappAdapterHooks} [hooks]
|
|
295
|
+
* Anything of the PlatformAdapter the host supplies or replaces. `actions` is
|
|
296
|
+
* required in practice (web-dapp ships no action manifest); `diagnostics`,
|
|
297
|
+
* `readiness` and `harness` merge over the generic members, the rest replace.
|
|
298
|
+
*/
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* @param {WebDappAdapterOptions} [factoryOptions]
|
|
302
|
+
* @returns {import('@farmslot/adapter-sdk').PlatformAdapter}
|
|
303
|
+
*/
|
|
304
|
+
export function createWebDappAdapter(factoryOptions = {}) {
|
|
305
|
+
if ('signers' in factoryOptions) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
'createWebDappAdapter no longer takes `signers`: pass `signerModule`, the path of the module exporting them, so readiness, launch, verify and the wallet host load the same hooks.',
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
const { id = 'web-dapp', signerModule, cli = 'farmslot-recipe', hooks = {} } = factoryOptions;
|
|
311
|
+
const { actions, diagnostics, readiness, harness, ...rest } = hooks;
|
|
312
|
+
const resolveSigners = async () =>
|
|
313
|
+
loadSigners(signerModule ? { ...process.env, [SIGNER_MODULE_ENV]: signerModule } : process.env);
|
|
314
|
+
const venueHint = `pass --adapter <the adapter that extends ${id}>, e.g. --adapter terminal, with its recipe library on RECIPE_LIBRARY_PATH`;
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
id,
|
|
318
|
+
sdkVersion: 1,
|
|
319
|
+
headless: false,
|
|
320
|
+
|
|
321
|
+
resolveSlotPorts() {
|
|
322
|
+
// Ports come from the slot (Farmslot {{port}} / {{cdp_port}}) through
|
|
323
|
+
// --watcher-port / --cdp-port or the runtime context; nothing is claimed here.
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
async runtimeStatus(target) {
|
|
327
|
+
if (!process.env[POLICY_ENV]) {
|
|
328
|
+
return {
|
|
329
|
+
decision: 'blocked',
|
|
330
|
+
reasonCode: 'web-dapp-venue-policy',
|
|
331
|
+
reasons: [`${id} needs a venue policy: select an adapter that extends it`],
|
|
332
|
+
nextAction: venueHint,
|
|
333
|
+
deps: 'missing',
|
|
334
|
+
devServer: { label: 'next', status: 'down' },
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const policy = webDappPolicy();
|
|
338
|
+
const report = await webDappReadiness({
|
|
339
|
+
policy,
|
|
340
|
+
signers: await resolveSigners(),
|
|
341
|
+
target: path.resolve(target),
|
|
342
|
+
appPort: webDappAppPort(),
|
|
343
|
+
cdpPort: webDappCdpPort(),
|
|
344
|
+
signer: process.env.TERMINAL_SIGNER || defaultSigner(signerModule),
|
|
345
|
+
});
|
|
346
|
+
const blocking = report.checks.filter((item) => item.required && item.status === 'fail');
|
|
347
|
+
const devServer = report.checks.find((item) => item.id === 'app-dev-server');
|
|
348
|
+
return {
|
|
349
|
+
decision: blocking.length === 0 ? 'ready' : 'blocked',
|
|
350
|
+
reasonCode: blocking[0]?.id,
|
|
351
|
+
reasons: (blocking.length ? blocking : report.checks).map(
|
|
352
|
+
(item) => `${item.id}: ${item.detail}`,
|
|
353
|
+
),
|
|
354
|
+
nextAction:
|
|
355
|
+
blocking.length === 0
|
|
356
|
+
? undefined
|
|
357
|
+
: blocking[0].id === 'app-dev-server' || blocking[0].id === 'testnet-forced'
|
|
358
|
+
? 'start the slot dev server with testnet forced (Farmslot preflight), then retry'
|
|
359
|
+
: `${cli} doctor --adapter ${policy.adapterId} --target ${shellQuote(path.resolve(target))} --json`,
|
|
360
|
+
deps:
|
|
361
|
+
report.checks.find((item) => item.id === 'dependencies')?.status === 'pass'
|
|
362
|
+
? 'current'
|
|
363
|
+
: 'missing',
|
|
364
|
+
devServer: { label: 'next', status: devServer?.status === 'pass' ? 'up' : 'down' },
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
devServer: {
|
|
369
|
+
label: 'browser',
|
|
370
|
+
describe: () =>
|
|
371
|
+
`${webDappAdapterId()} slot browser and wallet host (the Next.js dev server belongs to the slot)`,
|
|
372
|
+
stop(target) {
|
|
373
|
+
const args = [webDappLeafPath('stop'), '--target', path.resolve(target)];
|
|
374
|
+
const cdpPort = process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
|
|
375
|
+
if (cdpPort) args.push('--cdp-port', cdpPort);
|
|
376
|
+
const result = spawnSync(process.execPath, args, { encoding: 'utf8' });
|
|
377
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
|
|
378
|
+
const status = result.status ?? 1;
|
|
379
|
+
return {
|
|
380
|
+
kind: 'stopped',
|
|
381
|
+
status,
|
|
382
|
+
summary:
|
|
383
|
+
status === 0
|
|
384
|
+
? `${webDappAdapterId()} browser and wallet host stopped`
|
|
385
|
+
: `${webDappAdapterId()} browser stop failed`,
|
|
386
|
+
output,
|
|
387
|
+
};
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
logSources(target) {
|
|
392
|
+
return [
|
|
393
|
+
{ label: 'app-console', path: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.app) },
|
|
394
|
+
...(webDappSignerFor(target, defaultSigner(signerModule)) === 'extension'
|
|
395
|
+
? [
|
|
396
|
+
{
|
|
397
|
+
label: 'extension-console',
|
|
398
|
+
path: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.extension),
|
|
399
|
+
},
|
|
400
|
+
]
|
|
401
|
+
: []),
|
|
402
|
+
{ label: 'wallet-requests', path: webDappRuntimeFile(target, 'wallet-requests.jsonl') },
|
|
403
|
+
{ label: 'wallet-host', path: webDappRuntimeFile(target, 'wallet-host.log') },
|
|
404
|
+
{ label: 'browser', path: webDappRuntimeFile(target, 'browser.log') },
|
|
405
|
+
{ label: 'next', path: path.join(target, 'temp/farmslot/next-dev.log') },
|
|
406
|
+
];
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
// The app page's console and uncaught exceptions: errors make a run REVIEW.
|
|
410
|
+
appLogSource(target) {
|
|
411
|
+
return { label: 'app-console', path: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.app) };
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
flags: { launch: ['freshProfile', 'headless', 'headful'] },
|
|
415
|
+
// Bare web-dapp has no venue: every hint names the adapter that extends it.
|
|
416
|
+
hints: {
|
|
417
|
+
launch: venueHint,
|
|
418
|
+
relaunch: `pass --adapter <the adapter that extends ${id}>, e.g. --adapter terminal, with --fresh-profile`,
|
|
419
|
+
runtimeProbeRecovery: () => venueHint,
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
// Start (or reuse) the slot browser with the requested signer. The app's dev
|
|
423
|
+
// server belongs to the slot and is never started here.
|
|
424
|
+
async launch({ options, target, jsonOutput, stream, usage }) {
|
|
425
|
+
const adapterId = webDappAdapterId();
|
|
426
|
+
const cdpPort =
|
|
427
|
+
str(options, 'cdpPort') ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
|
|
428
|
+
// Same precedence as the web-dapp surface and live actions.
|
|
429
|
+
const appPort =
|
|
430
|
+
str(options, 'watcherPort') ??
|
|
431
|
+
process.env.TERMINAL_APP_PORT ??
|
|
432
|
+
process.env.RECIPE_WATCHER_PORT ??
|
|
433
|
+
process.env.WATCHER_PORT;
|
|
434
|
+
if (!cdpPort || !appPort) {
|
|
435
|
+
return usage(
|
|
436
|
+
`${adapterId} launch needs the slot browser CDP port and the slot dev-server port.`,
|
|
437
|
+
`${cli} launch --adapter ${adapterId} --cdp-port <port> --watcher-port <app port>`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
const args = [
|
|
441
|
+
webDappLeafPath('launch'),
|
|
442
|
+
'--target',
|
|
443
|
+
target,
|
|
444
|
+
'--cdp-port',
|
|
445
|
+
cdpPort,
|
|
446
|
+
'--app-port',
|
|
447
|
+
appPort,
|
|
448
|
+
'--signer',
|
|
449
|
+
str(options, 'signer') ?? process.env.TERMINAL_SIGNER ?? defaultSigner(signerModule),
|
|
450
|
+
'--account',
|
|
451
|
+
str(options, 'account') ?? process.env.TERMINAL_ACCOUNT ?? 'dev1',
|
|
452
|
+
'--json',
|
|
453
|
+
];
|
|
454
|
+
if (signerModule) args.push('--signer-module', signerModule);
|
|
455
|
+
if (flag(options, 'freshProfile')) args.push('--fresh-profile');
|
|
456
|
+
if (flag(options, 'headless')) args.push('--headless');
|
|
457
|
+
if (flag(options, 'headful')) args.push('--headful');
|
|
458
|
+
const slowMo = str(options, 'slowMo');
|
|
459
|
+
if (slowMo) args.push('--slow-mo', slowMo);
|
|
460
|
+
const network = str(options, 'network');
|
|
461
|
+
if (network) args.push('--network', network);
|
|
462
|
+
const mainnetConfirmation = str(options, 'mainnetConfirmation');
|
|
463
|
+
if (mainnetConfirmation) args.push('--mainnet-confirmation', mainnetConfirmation);
|
|
464
|
+
stream.phase('launch', { adapter: adapterId, cdpPort, appPort });
|
|
465
|
+
let output = '';
|
|
466
|
+
let status = 0;
|
|
467
|
+
try {
|
|
468
|
+
output = execFileSync(process.execPath, args, {
|
|
469
|
+
encoding: 'utf8',
|
|
470
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
471
|
+
});
|
|
472
|
+
} catch (error) {
|
|
473
|
+
status = 1;
|
|
474
|
+
output = String(/** @type {{ stdout?: string }} */ (error)?.stdout ?? '');
|
|
475
|
+
}
|
|
476
|
+
const result =
|
|
477
|
+
/** @type {{ status?: string, reused?: boolean, cdpPort?: number, signer?: string, account?: { name?: string }, error?: string }} */ (
|
|
478
|
+
parseWebDappLauncherOutput(output)
|
|
479
|
+
);
|
|
480
|
+
if (jsonOutput)
|
|
481
|
+
console.log(
|
|
482
|
+
JSON.stringify(
|
|
483
|
+
{ schemaVersion: 1, command: 'launch', adapter: adapterId, target, ...result },
|
|
484
|
+
null,
|
|
485
|
+
2,
|
|
486
|
+
),
|
|
487
|
+
);
|
|
488
|
+
else if (result.status === 'pass')
|
|
489
|
+
console.log(
|
|
490
|
+
`✓ ${adapterId} browser ${result.reused ? 'reused' : 'started'} on CDP ${result.cdpPort} (${result.signer}, ${result.account?.name})`,
|
|
491
|
+
);
|
|
492
|
+
else console.error(`✗ ${adapterId} launch failed: ${result.error ?? 'unknown error'}`);
|
|
493
|
+
stream.complete(result.status === 'pass' ? 'pass' : 'fail', status);
|
|
494
|
+
return result.status === 'pass' ? 0 : 1;
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
// The web-dapp ships no action manifest: the host supplies its action set
|
|
498
|
+
// (`hooks.actions`).
|
|
499
|
+
actions: actions ?? {
|
|
500
|
+
manifestPath() {
|
|
501
|
+
throw new Error(
|
|
502
|
+
`${id} ships no action manifest: pass hooks.actions to createWebDappAdapter.`,
|
|
503
|
+
);
|
|
504
|
+
},
|
|
505
|
+
semantic: [],
|
|
506
|
+
cdpTarget: { transport: 'chrome-cdp', probePath: 'json/version' },
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
// The overlay and slot: install/verify/cleanup leaves.
|
|
510
|
+
harness: {
|
|
511
|
+
install: {
|
|
512
|
+
entry: webDappLeafPath('inject'),
|
|
513
|
+
fallback: webDappLeafPath('inject'),
|
|
514
|
+
node: true,
|
|
515
|
+
},
|
|
516
|
+
cleanup: {
|
|
517
|
+
entry: webDappLeafPath('cleanup'),
|
|
518
|
+
fallback: webDappLeafPath('cleanup'),
|
|
519
|
+
node: true,
|
|
520
|
+
},
|
|
521
|
+
verify: () => ({
|
|
522
|
+
command: process.execPath,
|
|
523
|
+
prefixArgs: [
|
|
524
|
+
webDappLeafPath('verify'),
|
|
525
|
+
...(signerModule ? ['--signer-module', signerModule] : []),
|
|
526
|
+
],
|
|
527
|
+
}),
|
|
528
|
+
restart: `${cli} launch`,
|
|
529
|
+
...harness,
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
// An agentic-runtime.json whose ports all belong to the slot (never claimed here).
|
|
533
|
+
runtimeContext: {
|
|
534
|
+
forbiddenFields: ['metroPort', 'simulator', 'simulatorUdid', 'adbSerial', 'extensionId'],
|
|
535
|
+
resources: ({ existing, defaults, envPort }) => ({
|
|
536
|
+
cdpPort: envPort('RECIPE_CDP_PORT', 'CDP_PORT') ?? existing.cdpPort ?? defaults.cdpPort,
|
|
537
|
+
}),
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
readiness: {
|
|
541
|
+
// The slot's readiness (dev server, testnet, fixture, browser, the signer's
|
|
542
|
+
// own checks, CDP port) as doctor checks; the browser itself is optional
|
|
543
|
+
// because recipes start it with their launch node. Without a venue policy
|
|
544
|
+
// (web-dapp selected on its own) doctor reports that one required check.
|
|
545
|
+
async liveChecks(target) {
|
|
546
|
+
if (!process.env[POLICY_ENV]) {
|
|
547
|
+
return [
|
|
548
|
+
{
|
|
549
|
+
id: 'web-dapp-venue-policy',
|
|
550
|
+
status: 'fail',
|
|
551
|
+
required: true,
|
|
552
|
+
message: `${id} needs a venue policy: select an adapter that extends it (e.g. --adapter terminal with its recipe library on RECIPE_LIBRARY_PATH).`,
|
|
553
|
+
},
|
|
554
|
+
];
|
|
555
|
+
}
|
|
556
|
+
const policy = webDappPolicy();
|
|
557
|
+
const report = await webDappReadiness({
|
|
558
|
+
policy,
|
|
559
|
+
signers: await resolveSigners(),
|
|
560
|
+
target,
|
|
561
|
+
appPort: webDappAppPort(),
|
|
562
|
+
cdpPort: webDappCdpPort(),
|
|
563
|
+
signer: process.env.TERMINAL_SIGNER || defaultSigner(signerModule),
|
|
564
|
+
account: process.env.TERMINAL_ACCOUNT || 'dev1',
|
|
565
|
+
probeBrowser: resolveBrowser,
|
|
566
|
+
});
|
|
567
|
+
return report.checks.map((item) => ({
|
|
568
|
+
id: `${policy.adapterId}-${item.id}`,
|
|
569
|
+
status: item.status === 'pass' ? 'pass' : 'fail',
|
|
570
|
+
required: item.required,
|
|
571
|
+
message: item.detail,
|
|
572
|
+
}));
|
|
573
|
+
},
|
|
574
|
+
// Screenshots come from the slot browser over CDP.
|
|
575
|
+
captureProviders: ['cdp'],
|
|
576
|
+
...readiness,
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
// The wallet request log; the console capture is the host's (`hooks.diagnostics.console`).
|
|
580
|
+
diagnostics: {
|
|
581
|
+
requestLog: {
|
|
582
|
+
path: (projectRoot) => webDappRuntimeFile(projectRoot, 'wallet-requests.jsonl'),
|
|
583
|
+
findings: (lines) => walletRequestFindings(lines),
|
|
584
|
+
},
|
|
585
|
+
...diagnostics,
|
|
586
|
+
},
|
|
587
|
+
|
|
588
|
+
...rest,
|
|
589
|
+
};
|
|
590
|
+
}
|