@deeeed/metamask-harness 0.43.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 +25 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +304 -34
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +20 -8
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +102 -0
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +4 -2
- package/adapters/mobile/reload-app.mjs +99 -1
- package/dist/adapters/mobile/prepare.js +12 -0
- package/dist/adapters.js +22 -3
- package/dist/commands/call.js +40 -2
- package/dist/commands/run-engine.js +247 -139
- package/dist/commands/run-report.js +68 -0
- package/dist/commands/run.js +47 -2
- package/dist/execution-provenance.js +342 -0
- package/dist/run-diagnostics.js +36 -11
- package/dist/runner.js +44 -13
- 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 +40 -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/mobile.action-manifest.json +28 -3
- package/library/recipes/mobile/perps/performance.recipe.json +73 -47
- package/package.json +1 -1
|
@@ -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],
|
|
@@ -26,6 +26,7 @@ const { resolvePort } = configModule;
|
|
|
26
26
|
export const MOBILE_BRIDGE_ERROR_CODES = BRIDGE_ERROR_CODES;
|
|
27
27
|
|
|
28
28
|
const execFileAsync = promisify(execFile);
|
|
29
|
+
const ACTION_DEADLINE_FIELD = '_action_deadline_epoch_ms';
|
|
29
30
|
|
|
30
31
|
let adapterActionActive = false;
|
|
31
32
|
let actionBridgeLockPath = null;
|
|
@@ -117,14 +118,17 @@ function runtimeDir() {
|
|
|
117
118
|
* Returns the trimmed ro.product.model value, or null when adb is unavailable
|
|
118
119
|
* or the serial does not respond. Uses execFile (never shell interpolation).
|
|
119
120
|
*/
|
|
120
|
-
async function resolveAndroidModel(adbSerial) {
|
|
121
|
+
async function resolveAndroidModel(adbSerial, timeoutMs = 5_000) {
|
|
121
122
|
const adbPath = resolveMobileToolPath('adb');
|
|
122
|
-
if (!adbPath) return null;
|
|
123
|
+
if (!adbPath || timeoutMs <= 0) return null;
|
|
123
124
|
try {
|
|
124
125
|
const { stdout } = await execFileAsync(
|
|
125
126
|
adbPath,
|
|
126
127
|
['-s', adbSerial, 'shell', 'getprop', 'ro.product.model'],
|
|
127
|
-
{
|
|
128
|
+
{
|
|
129
|
+
timeout: Math.max(1, Math.min(5_000, Math.floor(timeoutMs))),
|
|
130
|
+
encoding: 'utf8',
|
|
131
|
+
},
|
|
128
132
|
);
|
|
129
133
|
const model = stdout.trim();
|
|
130
134
|
return model || null;
|
|
@@ -186,7 +190,11 @@ export async function bridgeEnv(input) {
|
|
|
186
190
|
androidTargetDeviceName,
|
|
187
191
|
)
|
|
188
192
|
) {
|
|
189
|
-
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);
|
|
190
198
|
if (model) {
|
|
191
199
|
env.ANDROID_TARGET_DEVICE_NAME = model;
|
|
192
200
|
}
|
|
@@ -432,7 +440,6 @@ const TRANSIENT_NULL_COMMANDS = new Set(['get-route']);
|
|
|
432
440
|
|
|
433
441
|
export async function bridgeCommand(input, args) {
|
|
434
442
|
const script = bridgeScript(input);
|
|
435
|
-
// bridgeEnv is async: it may call `adb getprop` to resolve the Metro device name.
|
|
436
443
|
const env = await bridgeEnv(input);
|
|
437
444
|
const brokerSocket = brokerSocketPath(
|
|
438
445
|
path.dirname(resolveBridgeLockPath(input, env)),
|
|
@@ -441,8 +448,33 @@ export async function bridgeCommand(input, args) {
|
|
|
441
448
|
if (adapterActionActive && !existsSync(brokerSocket)) {
|
|
442
449
|
env.CDP_BRIDGE_LOCK_OWNER_PID = await acquireActionBridgeLock(input, env);
|
|
443
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
|
+
);
|
|
444
477
|
const result = await new Promise((resolve, reject) => {
|
|
445
|
-
const timeoutMs = Number(input.node?.bridge_timeout_ms ?? input.node?.cdp_timeout_ms ?? process.env.CDP_TIMEOUT ?? 30000);
|
|
446
478
|
const child = spawn(process.execPath, [script, ...args], {
|
|
447
479
|
cwd: input.context.projectRoot,
|
|
448
480
|
env: { ...env, APP_ROOT: input.context.projectRoot },
|
|
@@ -567,7 +599,7 @@ export async function evalAsync(input, expression) {
|
|
|
567
599
|
} catch (error) {
|
|
568
600
|
lastError = error;
|
|
569
601
|
if (!String(error?.message ?? error).includes('CLIENT_NOT_INITIALIZED')) throw error;
|
|
570
|
-
await sleep(500);
|
|
602
|
+
await sleep(Math.min(500, Math.max(0, deadline - Date.now())));
|
|
571
603
|
}
|
|
572
604
|
}
|
|
573
605
|
throw lastError ?? new Error('Timed out waiting for Mobile async evaluation.');
|
|
@@ -583,7 +615,7 @@ const TARGET_TRANSITION_CODES = new Set([
|
|
|
583
615
|
BRIDGE_ERROR_CODES.WS_CLOSED,
|
|
584
616
|
]);
|
|
585
617
|
|
|
586
|
-
function isTargetTransition(error) {
|
|
618
|
+
export function isTargetTransition(error) {
|
|
587
619
|
return TARGET_TRANSITION_CODES.has(error?.code);
|
|
588
620
|
}
|
|
589
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
|
}
|
|
@@ -1618,7 +1618,7 @@
|
|
|
1618
1618
|
"execution_capabilities": ["app-mutation"]
|
|
1619
1619
|
},
|
|
1620
1620
|
"metamask.wallet.lock": {
|
|
1621
|
-
"description": "Lock the Mobile wallet through the visible menu and confirm the login screen.",
|
|
1621
|
+
"description": "Lock the Android Mobile wallet through the visible menu and confirm the login screen on development or opaque clients.",
|
|
1622
1622
|
"schema": {
|
|
1623
1623
|
"type": "object",
|
|
1624
1624
|
"properties": {
|
|
@@ -3115,6 +3115,9 @@
|
|
|
3115
3115
|
"wait_for_lifecycle": {
|
|
3116
3116
|
"type": "string"
|
|
3117
3117
|
},
|
|
3118
|
+
"wait_for_content_variant": {
|
|
3119
|
+
"type": "string"
|
|
3120
|
+
},
|
|
3118
3121
|
"wait_timeout_ms": {
|
|
3119
3122
|
"type": "number",
|
|
3120
3123
|
"minimum": 1
|
|
@@ -3144,7 +3147,7 @@
|
|
|
3144
3147
|
},
|
|
3145
3148
|
"required_live_streams": {
|
|
3146
3149
|
"type": "array",
|
|
3147
|
-
"description": "Require values_ready records
|
|
3150
|
+
"description": "Require app-emitted [PerpsLoadProof] values_ready records from fresh_socket with one complete session, lifecycle, account, context, and connection-generation identity.",
|
|
3148
3151
|
"items": {
|
|
3149
3152
|
"type": "string",
|
|
3150
3153
|
"enum": ["prices", "positions", "orders", "account"]
|
|
@@ -3213,7 +3216,7 @@
|
|
|
3213
3216
|
{
|
|
3214
3217
|
"action": "metamask.perps.capture_performance",
|
|
3215
3218
|
"phase": "end",
|
|
3216
|
-
"require_records":
|
|
3219
|
+
"require_records": true,
|
|
3217
3220
|
"required_live_streams": ["positions", "orders", "account"],
|
|
3218
3221
|
"intent": "Preserve production Perps loading and live-stream evidence emitted during this run",
|
|
3219
3222
|
"next": "done"
|
|
@@ -3290,6 +3293,28 @@
|
|
|
3290
3293
|
],
|
|
3291
3294
|
"execution_capabilities": ["app-mutation"]
|
|
3292
3295
|
},
|
|
3296
|
+
"metamask.perps.ensure_mode": {
|
|
3297
|
+
"description": "Ensure the viewport-visible Perps market-detail root is Lite or Pro. The action presses the active mode control at most once and fails closed if that press opens another chooser or does not converge directly.",
|
|
3298
|
+
"schema": {
|
|
3299
|
+
"type": "object",
|
|
3300
|
+
"properties": {
|
|
3301
|
+
"mode": {
|
|
3302
|
+
"type": "string",
|
|
3303
|
+
"enum": ["lite", "pro"]
|
|
3304
|
+
},
|
|
3305
|
+
"timeout_ms": { "type": "integer", "minimum": 1000, "default": 30000 }
|
|
3306
|
+
},
|
|
3307
|
+
"required": ["mode"],
|
|
3308
|
+
"additionalProperties": false
|
|
3309
|
+
},
|
|
3310
|
+
"examples": [{
|
|
3311
|
+
"action": "metamask.perps.ensure_mode",
|
|
3312
|
+
"mode": "pro",
|
|
3313
|
+
"intent": "Reach Pro through the visible market-detail mode control",
|
|
3314
|
+
"next": "done"
|
|
3315
|
+
}],
|
|
3316
|
+
"execution_capabilities": ["app-mutation"]
|
|
3317
|
+
},
|
|
3293
3318
|
"metamask.perps.start_state": {
|
|
3294
3319
|
"description": "mobile Default to testnet for Perps mutations; mainnet is read-only unless explicitly requested. Recommended configurable Perps start state that first restores the fixture-backed unlocked wallet, then composes reusable domain operations before the proof window.",
|
|
3295
3320
|
"schema": {
|