@deeeed/metamask-harness 0.42.0 → 0.44.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 +42 -0
- package/README.md +5 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +363 -55
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +23 -10
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +2 -0
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +296 -25
- package/adapters/mobile/bridge-runtime/lib/config.cjs +14 -4
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +4 -2
- package/adapters/mobile/reload-app.mjs +99 -1
- package/adapters/mobile/start-console-forwarder.sh +5 -1
- package/dist/adapters/extension/browser-cdp.js +174 -0
- package/dist/adapters/extension/network-observer.js +19 -110
- package/dist/adapters/extension/performance-observer.js +75 -0
- package/dist/adapters/mobile/frame-metrics.js +45 -0
- package/dist/adapters/mobile/performance-observer.js +43 -0
- package/dist/adapters/mobile/prepare.js +12 -0
- package/dist/adapters/performance/cdp-trace.js +342 -0
- package/dist/adapters/performance/js-task-metrics.js +35 -0
- package/dist/adapters.js +39 -5
- package/dist/artifact-files.js +92 -0
- package/dist/async.js +19 -0
- package/dist/commands/call.js +63 -4
- package/dist/commands/run-engine.js +265 -139
- package/dist/commands/run-report.js +68 -0
- package/dist/commands/run.js +67 -3
- package/dist/execution-provenance.js +342 -0
- package/dist/network-observation.js +59 -47
- package/dist/performance-observation.js +465 -0
- package/dist/run-diagnostics.js +36 -11
- package/dist/runner.js +44 -13
- package/docs/PERFORMANCE-CAPTURE.md +33 -0
- package/docs/RECIPES.md +7 -0
- package/library/actions/mobile/perps/performance-capture.mjs +570 -189
- package/library/actions/mobile/perps/perps.mjs +122 -0
- package/library/actions/mobile/platform/bridge.mjs +43 -8
- package/library/actions/mobile/platform/native-session.mjs +1 -1
- package/library/actions/mobile/wallet/lock.mjs +1 -4
- package/library/actions/mobile/wallet/select_account.mjs +129 -17
- package/library/manifests/extension.action-manifest.json +85 -0
- package/library/manifests/mobile.action-manifest.json +125 -3
- package/library/recipes/mobile/perps/performance.recipe.json +73 -47
- package/package.json +1 -1
- package/scripts/site-contrast.mjs +43 -27
|
@@ -2,6 +2,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
2
2
|
import {
|
|
3
3
|
bridgeCommand,
|
|
4
4
|
evalAsync,
|
|
5
|
+
isTargetTransition,
|
|
5
6
|
navigate,
|
|
6
7
|
runAdapter,
|
|
7
8
|
} from '../platform/bridge.mjs';
|
|
@@ -69,6 +70,126 @@ export async function measureHomepageVisible(input) {
|
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
const MODE_TEST_IDS = {
|
|
74
|
+
lite: 'perps-mode-toggle-lite',
|
|
75
|
+
pro: 'perps-mode-toggle-pro',
|
|
76
|
+
};
|
|
77
|
+
const MODE_ROOT_TEST_IDS = {
|
|
78
|
+
lite: 'perps-market-details-view',
|
|
79
|
+
pro: 'perps-pro-market-scroll-view',
|
|
80
|
+
};
|
|
81
|
+
const READ_ATTEMPT_TIMEOUT_MS = 10_000;
|
|
82
|
+
|
|
83
|
+
function withRemainingDeadline(input, deadline, attemptTimeoutMs) {
|
|
84
|
+
const remainingMs = Math.max(1, deadline - Date.now());
|
|
85
|
+
const boundedTimeout = (configured) =>
|
|
86
|
+
Math.min(
|
|
87
|
+
remainingMs,
|
|
88
|
+
Number(configured ?? remainingMs),
|
|
89
|
+
attemptTimeoutMs ?? remainingMs,
|
|
90
|
+
);
|
|
91
|
+
return {
|
|
92
|
+
...input,
|
|
93
|
+
node: {
|
|
94
|
+
...input.node,
|
|
95
|
+
_action_deadline_epoch_ms: deadline,
|
|
96
|
+
bridge_timeout_ms: boundedTimeout(input.node?.bridge_timeout_ms),
|
|
97
|
+
cdp_timeout_ms: boundedTimeout(input.node?.cdp_timeout_ms),
|
|
98
|
+
controller_ready_timeout_ms: boundedTimeout(
|
|
99
|
+
input.node?.controller_ready_timeout_ms,
|
|
100
|
+
),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function isModePresent(input, mode, deadline) {
|
|
106
|
+
const rootTarget = JSON.stringify({
|
|
107
|
+
testId: MODE_ROOT_TEST_IDS[mode],
|
|
108
|
+
visibility: 'viewport',
|
|
109
|
+
});
|
|
110
|
+
const controlTarget = JSON.stringify({
|
|
111
|
+
testId: MODE_TEST_IDS[mode],
|
|
112
|
+
visibility: 'tree',
|
|
113
|
+
});
|
|
114
|
+
const result = await evalAsync(
|
|
115
|
+
withRemainingDeadline(input, deadline, READ_ATTEMPT_TIMEOUT_MS),
|
|
116
|
+
`(function(){
|
|
117
|
+
const api = globalThis.__AGENTIC__;
|
|
118
|
+
if (!api || typeof api.queryUiTarget !== 'function') {
|
|
119
|
+
return Promise.resolve(JSON.stringify({ unsupported: true }));
|
|
120
|
+
}
|
|
121
|
+
return Promise.all([
|
|
122
|
+
api.queryUiTarget(${rootTarget}),
|
|
123
|
+
api.queryUiTarget(${controlTarget})
|
|
124
|
+
]).then(function(values){
|
|
125
|
+
return JSON.stringify({ root: values[0], control: values[1] });
|
|
126
|
+
});
|
|
127
|
+
})()`,
|
|
128
|
+
);
|
|
129
|
+
if (result?.unsupported) {
|
|
130
|
+
throw new Error('metamask.perps.ensure_mode requires UI target queries.');
|
|
131
|
+
}
|
|
132
|
+
return result?.root?.visible === true && result?.control?.present === true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function readModePresence(input, mode, deadline) {
|
|
136
|
+
try {
|
|
137
|
+
return await isModePresent(input, mode, deadline);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (!isTargetTransition(error)) throw error;
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function ensureMode(input) {
|
|
145
|
+
const requestedMode = String(input.node?.mode ?? '');
|
|
146
|
+
if (!(requestedMode in MODE_TEST_IDS)) {
|
|
147
|
+
throw new Error('metamask.perps.ensure_mode requires mode=lite|pro.');
|
|
148
|
+
}
|
|
149
|
+
const otherMode = requestedMode === 'lite' ? 'pro' : 'lite';
|
|
150
|
+
const deadline = Date.now() + Number(input.node?.timeout_ms ?? 30_000);
|
|
151
|
+
let switched = false;
|
|
152
|
+
|
|
153
|
+
while (Date.now() < deadline) {
|
|
154
|
+
const requestedModePresent = await readModePresence(
|
|
155
|
+
input,
|
|
156
|
+
requestedMode,
|
|
157
|
+
deadline,
|
|
158
|
+
);
|
|
159
|
+
if (requestedModePresent) {
|
|
160
|
+
return {
|
|
161
|
+
action: input.action,
|
|
162
|
+
mode: requestedMode,
|
|
163
|
+
alreadySelected: !switched,
|
|
164
|
+
proofPath: 'visible-market-detail-root-and-active-mode-control',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (Date.now() >= deadline) break;
|
|
168
|
+
if (requestedModePresent !== null && !switched) {
|
|
169
|
+
const otherModePresent = await readModePresence(
|
|
170
|
+
input,
|
|
171
|
+
otherMode,
|
|
172
|
+
deadline,
|
|
173
|
+
);
|
|
174
|
+
if (otherModePresent && Date.now() < deadline) {
|
|
175
|
+
// The visible control names the mode it will switch to, not the mode
|
|
176
|
+
// currently selected.
|
|
177
|
+
const pressed = await bridgeCommand(withRemainingDeadline(input, deadline), [
|
|
178
|
+
'press-test-id',
|
|
179
|
+
MODE_TEST_IDS[otherMode],
|
|
180
|
+
]);
|
|
181
|
+
if (pressed?.ok === false) {
|
|
182
|
+
throw new Error(String(pressed.error || 'Perps mode control press failed.'));
|
|
183
|
+
}
|
|
184
|
+
switched = true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
await sleep(Math.min(250, Math.max(0, deadline - Date.now())));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
throw new Error(`Perps did not reach ${requestedMode} mode before timeout.`);
|
|
191
|
+
}
|
|
192
|
+
|
|
72
193
|
export async function retryPerpsClientRead(
|
|
73
194
|
read,
|
|
74
195
|
{ timeoutMs = 20000, intervalMs = 500, sleep: wait = sleep } = {},
|
|
@@ -1074,6 +1195,7 @@ const DIRECT_ACTIONS = new Map([
|
|
|
1074
1195
|
['metamask.perps.ensure_positions', ensurePositions],
|
|
1075
1196
|
['metamask.perps.ensure_orders', ensureOrders],
|
|
1076
1197
|
['metamask.perps.clear_performance_caches', clearPerformanceCaches],
|
|
1198
|
+
['metamask.perps.ensure_mode', ensureMode],
|
|
1077
1199
|
['metamask.perps.measure_homepage_visible', measureHomepageVisible],
|
|
1078
1200
|
['metamask.perps.start_state', startState],
|
|
1079
1201
|
['metamask.perps.teardown_state', teardownState],
|
|
@@ -8,6 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import bridgeErrors from '../../../../adapters/mobile/bridge-runtime/lib/bridge-errors.cjs';
|
|
10
10
|
import brokerModule from '../../../../adapters/mobile/bridge-runtime/lib/cdp-broker.cjs';
|
|
11
|
+
import configModule from '../../../../adapters/mobile/bridge-runtime/lib/config.cjs';
|
|
11
12
|
import { resolveMobileToolPath } from './tool-paths.mjs';
|
|
12
13
|
|
|
13
14
|
const {
|
|
@@ -18,12 +19,14 @@ const {
|
|
|
18
19
|
parseErrorMarker,
|
|
19
20
|
} = bridgeErrors;
|
|
20
21
|
const { brokerSocketPath } = brokerModule;
|
|
22
|
+
const { resolvePort } = configModule;
|
|
21
23
|
|
|
22
24
|
// Re-exported so the TS adapter classifies on the same code constants without a
|
|
23
25
|
// second import path into the cjs bridge-runtime.
|
|
24
26
|
export const MOBILE_BRIDGE_ERROR_CODES = BRIDGE_ERROR_CODES;
|
|
25
27
|
|
|
26
28
|
const execFileAsync = promisify(execFile);
|
|
29
|
+
const ACTION_DEADLINE_FIELD = '_action_deadline_epoch_ms';
|
|
27
30
|
|
|
28
31
|
let adapterActionActive = false;
|
|
29
32
|
let actionBridgeLockPath = null;
|
|
@@ -115,14 +118,17 @@ function runtimeDir() {
|
|
|
115
118
|
* Returns the trimmed ro.product.model value, or null when adb is unavailable
|
|
116
119
|
* or the serial does not respond. Uses execFile (never shell interpolation).
|
|
117
120
|
*/
|
|
118
|
-
async function resolveAndroidModel(adbSerial) {
|
|
121
|
+
async function resolveAndroidModel(adbSerial, timeoutMs = 5_000) {
|
|
119
122
|
const adbPath = resolveMobileToolPath('adb');
|
|
120
|
-
if (!adbPath) return null;
|
|
123
|
+
if (!adbPath || timeoutMs <= 0) return null;
|
|
121
124
|
try {
|
|
122
125
|
const { stdout } = await execFileAsync(
|
|
123
126
|
adbPath,
|
|
124
127
|
['-s', adbSerial, 'shell', 'getprop', 'ro.product.model'],
|
|
125
|
-
{
|
|
128
|
+
{
|
|
129
|
+
timeout: Math.max(1, Math.min(5_000, Math.floor(timeoutMs))),
|
|
130
|
+
encoding: 'utf8',
|
|
131
|
+
},
|
|
126
132
|
);
|
|
127
133
|
const model = stdout.trim();
|
|
128
134
|
return model || null;
|
|
@@ -184,7 +190,11 @@ export async function bridgeEnv(input) {
|
|
|
184
190
|
androidTargetDeviceName,
|
|
185
191
|
)
|
|
186
192
|
) {
|
|
187
|
-
const
|
|
193
|
+
const deadline = Number(input.node?.[ACTION_DEADLINE_FIELD]);
|
|
194
|
+
const modelTimeoutMs = Number.isFinite(deadline)
|
|
195
|
+
? deadline - Date.now()
|
|
196
|
+
: 5_000;
|
|
197
|
+
const model = await resolveAndroidModel(serialStr, modelTimeoutMs);
|
|
188
198
|
if (model) {
|
|
189
199
|
env.ANDROID_TARGET_DEVICE_NAME = model;
|
|
190
200
|
}
|
|
@@ -430,16 +440,41 @@ const TRANSIENT_NULL_COMMANDS = new Set(['get-route']);
|
|
|
430
440
|
|
|
431
441
|
export async function bridgeCommand(input, args) {
|
|
432
442
|
const script = bridgeScript(input);
|
|
433
|
-
// bridgeEnv is async: it may call `adb getprop` to resolve the Metro device name.
|
|
434
443
|
const env = await bridgeEnv(input);
|
|
435
444
|
const brokerSocket = brokerSocketPath(
|
|
436
445
|
path.dirname(resolveBridgeLockPath(input, env)),
|
|
446
|
+
resolvePort(env, input.context.projectRoot),
|
|
437
447
|
);
|
|
438
448
|
if (adapterActionActive && !existsSync(brokerSocket)) {
|
|
439
449
|
env.CDP_BRIDGE_LOCK_OWNER_PID = await acquireActionBridgeLock(input, env);
|
|
440
450
|
}
|
|
451
|
+
const actionDeadline = Number(input.node?.[ACTION_DEADLINE_FIELD]);
|
|
452
|
+
const configuredTimeoutValue = Number(
|
|
453
|
+
input.node?.bridge_timeout_ms ??
|
|
454
|
+
input.node?.cdp_timeout_ms ??
|
|
455
|
+
process.env.CDP_TIMEOUT ??
|
|
456
|
+
30000,
|
|
457
|
+
);
|
|
458
|
+
const configuredTimeoutMs =
|
|
459
|
+
Number.isFinite(configuredTimeoutValue) && configuredTimeoutValue > 0
|
|
460
|
+
? configuredTimeoutValue
|
|
461
|
+
: 30_000;
|
|
462
|
+
const remainingActionMs = Number.isFinite(actionDeadline)
|
|
463
|
+
? actionDeadline - Date.now()
|
|
464
|
+
: configuredTimeoutMs;
|
|
465
|
+
if (remainingActionMs <= 0) {
|
|
466
|
+
throw coded(
|
|
467
|
+
new Error(
|
|
468
|
+
`Mobile CDP bridge command did not start before its action deadline: ${String(args[0] ?? 'unknown')}`,
|
|
469
|
+
),
|
|
470
|
+
BRIDGE_ERROR_CODES.CDP_TIMEOUT,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
const timeoutMs = Math.max(
|
|
474
|
+
1,
|
|
475
|
+
Math.min(configuredTimeoutMs, remainingActionMs),
|
|
476
|
+
);
|
|
441
477
|
const result = await new Promise((resolve, reject) => {
|
|
442
|
-
const timeoutMs = Number(input.node?.bridge_timeout_ms ?? input.node?.cdp_timeout_ms ?? process.env.CDP_TIMEOUT ?? 30000);
|
|
443
478
|
const child = spawn(process.execPath, [script, ...args], {
|
|
444
479
|
cwd: input.context.projectRoot,
|
|
445
480
|
env: { ...env, APP_ROOT: input.context.projectRoot },
|
|
@@ -564,7 +599,7 @@ export async function evalAsync(input, expression) {
|
|
|
564
599
|
} catch (error) {
|
|
565
600
|
lastError = error;
|
|
566
601
|
if (!String(error?.message ?? error).includes('CLIENT_NOT_INITIALIZED')) throw error;
|
|
567
|
-
await sleep(500);
|
|
602
|
+
await sleep(Math.min(500, Math.max(0, deadline - Date.now())));
|
|
568
603
|
}
|
|
569
604
|
}
|
|
570
605
|
throw lastError ?? new Error('Timed out waiting for Mobile async evaluation.');
|
|
@@ -580,7 +615,7 @@ const TARGET_TRANSITION_CODES = new Set([
|
|
|
580
615
|
BRIDGE_ERROR_CODES.WS_CLOSED,
|
|
581
616
|
]);
|
|
582
617
|
|
|
583
|
-
function isTargetTransition(error) {
|
|
618
|
+
export function isTargetTransition(error) {
|
|
584
619
|
return TARGET_TRANSITION_CODES.has(error?.code);
|
|
585
620
|
}
|
|
586
621
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
export function createNativeSession(input, domain, sessionSuffix = '') {
|
|
10
10
|
const env = { ...process.env, ...(input.context?.env ?? {}) };
|
|
11
11
|
const device = String(env.ADB_SERIAL ?? env.ANDROID_SERIAL ?? '').trim();
|
|
12
|
-
if (!device) throw new Error(`
|
|
12
|
+
if (!device) throw new Error(`Mobile ${domain} native actions require one explicit Android device serial.`);
|
|
13
13
|
const app = String(env.ANDROID_PACKAGE_ID ?? 'io.metamask');
|
|
14
14
|
const baseSession = nativeSessionName(env, device, app);
|
|
15
15
|
const session = sessionSuffix
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { runAdapter } from '../platform/bridge.mjs';
|
|
2
|
-
import {
|
|
2
|
+
import { lockWalletThroughNativeUi } from './native-ui.mjs';
|
|
3
3
|
|
|
4
4
|
runAdapter(async (input) => {
|
|
5
|
-
if (!isOpaqueMobileRuntime(input)) {
|
|
6
|
-
throw new Error('metamask.wallet.lock requires an opaque Mobile runtime.');
|
|
7
|
-
}
|
|
8
5
|
return {
|
|
9
6
|
action: input.action,
|
|
10
7
|
...await lockWalletThroughNativeUi(input),
|
|
@@ -1,7 +1,87 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
bridgeCommand,
|
|
4
|
+
MOBILE_BRIDGE_ERROR_CODES,
|
|
5
|
+
runAdapter,
|
|
6
|
+
} from '../platform/bridge.mjs';
|
|
3
7
|
|
|
4
8
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
const TRANSIENT_READ_CODES = new Set([
|
|
10
|
+
MOBILE_BRIDGE_ERROR_CODES.NO_TARGET,
|
|
11
|
+
MOBILE_BRIDGE_ERROR_CODES.CDP_TIMEOUT,
|
|
12
|
+
MOBILE_BRIDGE_ERROR_CODES.WS_CLOSED,
|
|
13
|
+
]);
|
|
14
|
+
const READ_ATTEMPT_TIMEOUT_MS = 10_000;
|
|
15
|
+
|
|
16
|
+
function withRemainingDeadline(input, deadline, attemptTimeoutMs) {
|
|
17
|
+
const remainingMs = Math.max(1, deadline - Date.now());
|
|
18
|
+
const boundedTimeout = (configured) =>
|
|
19
|
+
Math.min(
|
|
20
|
+
remainingMs,
|
|
21
|
+
Number(configured ?? remainingMs),
|
|
22
|
+
attemptTimeoutMs ?? remainingMs,
|
|
23
|
+
);
|
|
24
|
+
return {
|
|
25
|
+
...input,
|
|
26
|
+
node: {
|
|
27
|
+
...input.node,
|
|
28
|
+
_action_deadline_epoch_ms: deadline,
|
|
29
|
+
bridge_timeout_ms: boundedTimeout(input.node?.bridge_timeout_ms),
|
|
30
|
+
cdp_timeout_ms: boundedTimeout(input.node?.cdp_timeout_ms),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readSelectedAccount(input, deadline) {
|
|
36
|
+
let lastError = null;
|
|
37
|
+
while (Date.now() < deadline) {
|
|
38
|
+
try {
|
|
39
|
+
return await bridgeCommand(
|
|
40
|
+
withRemainingDeadline(input, deadline, READ_ATTEMPT_TIMEOUT_MS),
|
|
41
|
+
['get-selected-account'],
|
|
42
|
+
);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (!TRANSIENT_READ_CODES.has(error?.code)) throw error;
|
|
45
|
+
lastError = error;
|
|
46
|
+
await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw lastError ?? new Error('Timed out reading the selected Mobile account.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readAccounts(input, deadline) {
|
|
53
|
+
let lastError = null;
|
|
54
|
+
while (Date.now() < deadline) {
|
|
55
|
+
try {
|
|
56
|
+
return await bridgeCommand(
|
|
57
|
+
withRemainingDeadline(input, deadline, READ_ATTEMPT_TIMEOUT_MS),
|
|
58
|
+
['list-accounts'],
|
|
59
|
+
);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (!TRANSIENT_READ_CODES.has(error?.code)) throw error;
|
|
62
|
+
lastError = error;
|
|
63
|
+
await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw lastError ?? new Error('Timed out reading Mobile accounts.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ambiguousWriteFailure(writeError, match, finalSelected, readError) {
|
|
70
|
+
const failure = new Error(
|
|
71
|
+
`Mobile account switch to ${match.address} returned an ambiguous ${String(writeError.code)} error and did not converge before the deadline; ` +
|
|
72
|
+
`writeError=${String(writeError.message ?? writeError)}; finalSelectedAccount=${JSON.stringify(finalSelected)}${
|
|
73
|
+
readError
|
|
74
|
+
? `; finalReadError=${String(readError.message ?? readError)}`
|
|
75
|
+
: ''
|
|
76
|
+
}.`,
|
|
77
|
+
{ cause: writeError },
|
|
78
|
+
);
|
|
79
|
+
failure.code = writeError.code;
|
|
80
|
+
failure.writeError = writeError;
|
|
81
|
+
failure.finalSelectedAccount = finalSelected;
|
|
82
|
+
if (readError) failure.finalReadError = readError;
|
|
83
|
+
return failure;
|
|
84
|
+
}
|
|
5
85
|
|
|
6
86
|
export async function selectAccount(input) {
|
|
7
87
|
const requestedAddress = input.node?.address ? String(input.node.address).toLowerCase() : null;
|
|
@@ -10,7 +90,9 @@ export async function selectAccount(input) {
|
|
|
10
90
|
if (!requestedAddress && !requestedId && !requestedName) {
|
|
11
91
|
throw new Error('metamask.wallet.select_account mobile live adapter requires node.address, node.id, or node.name.');
|
|
12
92
|
}
|
|
13
|
-
const
|
|
93
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
|
|
94
|
+
const deadline = Date.now() + timeoutMs;
|
|
95
|
+
const accounts = await readAccounts(input, deadline);
|
|
14
96
|
if (!Array.isArray(accounts)) {
|
|
15
97
|
throw new Error(`metamask.wallet.select_account expected list-accounts to return an array, got ${typeof accounts}.`);
|
|
16
98
|
}
|
|
@@ -25,7 +107,7 @@ export async function selectAccount(input) {
|
|
|
25
107
|
if (!match?.address) {
|
|
26
108
|
throw new Error(`Requested mobile account was not found in list-accounts: ${JSON.stringify({ address: requestedAddress, id: requestedId, name: requestedName })}`);
|
|
27
109
|
}
|
|
28
|
-
const current = await
|
|
110
|
+
const current = await readSelectedAccount(input, deadline);
|
|
29
111
|
const matchAddress = String(match.address).toLowerCase();
|
|
30
112
|
if (String(current?.address ?? current ?? '').toLowerCase() === matchAddress) {
|
|
31
113
|
return {
|
|
@@ -39,29 +121,59 @@ export async function selectAccount(input) {
|
|
|
39
121
|
proofPath: 'agentic-account-selection',
|
|
40
122
|
};
|
|
41
123
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
124
|
+
if (Date.now() >= deadline) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Mobile account selection did not leave enough time to switch to ${match.address} within ${timeoutMs}ms.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
let result = null;
|
|
130
|
+
let ambiguousWriteError = null;
|
|
131
|
+
try {
|
|
132
|
+
result = await bridgeCommand(withRemainingDeadline(input, deadline), [
|
|
133
|
+
'switch-account',
|
|
134
|
+
String(match.address),
|
|
135
|
+
]);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (!TRANSIENT_READ_CODES.has(error?.code)) throw error;
|
|
138
|
+
ambiguousWriteError = error;
|
|
139
|
+
}
|
|
140
|
+
let selected = current;
|
|
141
|
+
let finalReadError = null;
|
|
142
|
+
try {
|
|
143
|
+
do {
|
|
144
|
+
selected = await readSelectedAccount(input, deadline);
|
|
145
|
+
if (String(selected?.address ?? selected ?? '').toLowerCase() === String(match.address).toLowerCase()) {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
149
|
+
} while (Date.now() < deadline);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (!ambiguousWriteError) throw error;
|
|
152
|
+
finalReadError = error;
|
|
153
|
+
}
|
|
53
154
|
|
|
54
155
|
if (String(selected?.address ?? selected ?? '').toLowerCase() !== String(match.address).toLowerCase()) {
|
|
156
|
+
if (ambiguousWriteError) {
|
|
157
|
+
throw ambiguousWriteFailure(
|
|
158
|
+
ambiguousWriteError,
|
|
159
|
+
match,
|
|
160
|
+
selected,
|
|
161
|
+
finalReadError,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
55
164
|
throw new Error(`Mobile account selection did not converge to ${match.address} after ${timeoutMs}ms.`);
|
|
56
165
|
}
|
|
57
166
|
|
|
58
167
|
return {
|
|
59
168
|
action: input.action,
|
|
60
169
|
selected: {
|
|
61
|
-
address: selected?.address ?? result.address,
|
|
62
|
-
id: selected?.id ?? result.id,
|
|
63
|
-
name: selected?.name ?? result.name,
|
|
170
|
+
address: selected?.address ?? result?.address ?? match.address,
|
|
171
|
+
id: selected?.id ?? result?.id ?? match.id,
|
|
172
|
+
name: selected?.name ?? result?.name ?? match.name,
|
|
64
173
|
},
|
|
174
|
+
...(ambiguousWriteError
|
|
175
|
+
? { recoveredAfterAmbiguousWrite: true }
|
|
176
|
+
: {}),
|
|
65
177
|
proofPath: 'agentic-account-selection',
|
|
66
178
|
};
|
|
67
179
|
}
|
|
@@ -1207,6 +1207,91 @@
|
|
|
1207
1207
|
},
|
|
1208
1208
|
"execution_capabilities": []
|
|
1209
1209
|
},
|
|
1210
|
+
"app.performance_capture": {
|
|
1211
|
+
"description": "Capture bounded CDP renderer frame metrics and separately labelled JavaScript work across Recipe Protocol v1 node boundaries.",
|
|
1212
|
+
"examples": [
|
|
1213
|
+
{
|
|
1214
|
+
"action": "app.performance_capture",
|
|
1215
|
+
"phase": "start",
|
|
1216
|
+
"id": "homepage-scroll",
|
|
1217
|
+
"max_duration_ms": 300000,
|
|
1218
|
+
"intent": "Start the bounded UI smoothness window",
|
|
1219
|
+
"next": "exercise-flow"
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
"action": "app.performance_capture",
|
|
1223
|
+
"phase": "end",
|
|
1224
|
+
"id": "homepage-scroll",
|
|
1225
|
+
"artifact_path": "performance/homepage-scroll.json",
|
|
1226
|
+
"html_path": "performance/homepage-scroll.html",
|
|
1227
|
+
"intent": "End and index the UI smoothness window",
|
|
1228
|
+
"next": "assert-performance"
|
|
1229
|
+
}
|
|
1230
|
+
],
|
|
1231
|
+
"schema": {
|
|
1232
|
+
"type": "object",
|
|
1233
|
+
"properties": {
|
|
1234
|
+
"phase": {
|
|
1235
|
+
"type": "string",
|
|
1236
|
+
"enum": ["start", "end"]
|
|
1237
|
+
},
|
|
1238
|
+
"id": { "type": "string" },
|
|
1239
|
+
"max_duration_ms": {
|
|
1240
|
+
"type": "integer",
|
|
1241
|
+
"minimum": 1,
|
|
1242
|
+
"description": "Maximum accepted at runtime: 3600000 ms."
|
|
1243
|
+
},
|
|
1244
|
+
"artifact_path": { "type": "string" },
|
|
1245
|
+
"html_path": { "type": "string" }
|
|
1246
|
+
},
|
|
1247
|
+
"required": ["phase", "id"],
|
|
1248
|
+
"additionalProperties": false
|
|
1249
|
+
},
|
|
1250
|
+
"execution_capabilities": ["host-read-export"]
|
|
1251
|
+
},
|
|
1252
|
+
"app.performance_assert": {
|
|
1253
|
+
"description": "Assert a previously indexed app.performance_capture summary without hiding unavailable or partial source coverage.",
|
|
1254
|
+
"examples": [
|
|
1255
|
+
{
|
|
1256
|
+
"action": "app.performance_assert",
|
|
1257
|
+
"id": "homepage-scroll",
|
|
1258
|
+
"artifact_path": "performance/homepage-scroll.json",
|
|
1259
|
+
"required_status": ["complete", "partial"],
|
|
1260
|
+
"minimum_frame_count": 1,
|
|
1261
|
+
"require_native_ui_when_supported": true,
|
|
1262
|
+
"required_node_ids": ["scroll-homepage"],
|
|
1263
|
+
"intent": "Assert the indexed UI smoothness summary",
|
|
1264
|
+
"next": "done"
|
|
1265
|
+
}
|
|
1266
|
+
],
|
|
1267
|
+
"schema": {
|
|
1268
|
+
"type": "object",
|
|
1269
|
+
"properties": {
|
|
1270
|
+
"id": { "type": "string" },
|
|
1271
|
+
"artifact_path": { "type": "string" },
|
|
1272
|
+
"required_status": {
|
|
1273
|
+
"type": "array",
|
|
1274
|
+
"items": {
|
|
1275
|
+
"type": "string",
|
|
1276
|
+
"enum": ["complete", "partial", "unavailable"]
|
|
1277
|
+
}
|
|
1278
|
+
},
|
|
1279
|
+
"minimum_frame_count": {
|
|
1280
|
+
"type": "integer",
|
|
1281
|
+
"minimum": 0,
|
|
1282
|
+
"description": "Minimum MetaMask renderer frame count. JavaScript task count cannot satisfy this assertion."
|
|
1283
|
+
},
|
|
1284
|
+
"require_native_ui_when_supported": { "type": "boolean" },
|
|
1285
|
+
"required_node_ids": {
|
|
1286
|
+
"type": "array",
|
|
1287
|
+
"items": { "type": "string" }
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
"required": ["id"],
|
|
1291
|
+
"additionalProperties": false
|
|
1292
|
+
},
|
|
1293
|
+
"execution_capabilities": ["host-read-export"]
|
|
1294
|
+
},
|
|
1210
1295
|
"app.status": {
|
|
1211
1296
|
"description": "Report the adapter's static status — platform, project root, resolved checkout shape, and headless compatibility mode (no live route or account).",
|
|
1212
1297
|
"examples": [
|