@deeeed/metamask-harness 0.34.1 → 0.34.3
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 +24 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +222 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +22 -1
- package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +18 -0
- package/adapters/mobile/open-device.sh +6 -4
- package/dist/adapters/mobile/prepare.js +1 -4
- package/dist/adapters.js +139 -8
- package/dist/commands/launch/mobile.js +16 -2
- package/dist/live-adapter-contract.js +6 -6
- package/dist/recipe-security.js +3 -0
- package/dist/runner.js +53 -8
- package/dist/runtime-context.js +1 -0
- package/library/actions/core/perps/_controller.mjs +75 -23
- package/library/actions/core/perps/read_account.mjs +6 -12
- package/library/actions/core/perps/read_snapshot.mjs +66 -0
- package/library/actions/extension/performance/_navigation-memory.mjs +372 -0
- package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +18 -0
- package/library/actions/extension/performance/measure_detached_dom.mjs +18 -0
- package/library/actions/extension/performance/measure_navigation_memory.mjs +18 -0
- package/library/actions/mobile/app/network-control.mjs +24 -12
- package/library/actions/mobile/perps/measure_homepage_visible.mjs +4 -0
- package/library/actions/mobile/perps/performance-capture.mjs +506 -120
- package/library/actions/mobile/perps/perps.mjs +221 -38
- package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +58 -0
- package/library/actions/mobile/platform/bridge.mjs +38 -5
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +5 -1
- package/library/manifests/core.action-manifest.json +55 -0
- package/library/manifests/extension.action-manifest.json +100 -0
- package/library/manifests/mobile.action-manifest.json +226 -512
- package/library/recipes/core/perps/snapshot.recipe.json +25 -0
- package/library/recipes/extension/performance/navigation-memory.recipe.json +175 -0
- package/library/recipes/mobile/perps/performance.homepage.cold-position-sample.recipe.json +120 -0
- package/library/recipes/mobile/perps/performance.homepage.ios-background-reconnect.recipe.json +7 -6
- package/library/recipes/mobile/perps/performance.homepage.ios-cold-no-cache.recipe.json +5 -5
- package/package.json +1 -1
- package/scripts/site-contrast.mjs +39 -3
- package/site/architecture.html +22 -16
- package/site/assets/progress.mjs +1 -1
- package/site/assets/style.css +113 -1
- package/site/cheatsheet.html +14 -12
- package/site/how-it-works.html +693 -0
- package/site/index.html +70 -640
- package/site/perps.html +7 -6
- package/site/recipes.html +23 -17
- package/site/reviewers.html +7 -6
- package/site/tutorials/index.html +7 -6
- package/site/tutorials/v1.html +12 -11
- package/site/tutorials/v2.html +7 -6
- package/site/tutorials/v3.html +15 -11
- package/site/tutorials/v4.html +6 -5
- package/site/tutorials/v5.html +6 -5
- package/site/tutorials/v6.html +6 -5
- package/site/tutorials/v7.html +6 -5
|
@@ -352,15 +352,15 @@ function liveAdapterProcessTimeoutMs(node) {
|
|
|
352
352
|
}
|
|
353
353
|
const settleMs = Number(node.settle_ms);
|
|
354
354
|
const settleAllowance = Number.isFinite(settleMs) && settleMs > 0 ? settleMs : 0;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
355
|
+
const actionTimeouts = [
|
|
356
|
+
node.target_timeout_ms,
|
|
357
|
+
node.unlock_timeout_ms,
|
|
358
|
+
node.timeout_ms
|
|
359
|
+
].map(Number).filter((value) => Number.isFinite(value) && value > 0);
|
|
360
360
|
if (actionTimeouts.length > 0) {
|
|
361
361
|
return Math.max(
|
|
362
362
|
6e4,
|
|
363
|
-
actionTimeouts.reduce((total, value) => total + value, 0) + 5e3
|
|
363
|
+
actionTimeouts.reduce((total, value) => total + value, 0) + settleAllowance + 5e3
|
|
364
364
|
);
|
|
365
365
|
}
|
|
366
366
|
return 6e4 + settleAllowance;
|
package/dist/recipe-security.js
CHANGED
|
@@ -9,6 +9,7 @@ const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
9
9
|
"metamask.perps.read_positions",
|
|
10
10
|
"metamask.perps.read_orders",
|
|
11
11
|
"metamask.perps.read_account",
|
|
12
|
+
"metamask.perps.read_snapshot",
|
|
12
13
|
"metamask.perps.assert_positions",
|
|
13
14
|
"metamask.perps.assert_orders",
|
|
14
15
|
// Collector reads are host-only; starting capture remains arbitrary-code
|
|
@@ -29,6 +30,8 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
29
30
|
"metamask.perps.ensure_positions",
|
|
30
31
|
"metamask.perps.ensure_orders",
|
|
31
32
|
"metamask.perps.clear_performance_caches",
|
|
33
|
+
"metamask.perps.measure_homepage_visible",
|
|
34
|
+
"metamask.perps.prepare_local_snapshot_endpoint",
|
|
32
35
|
"metamask.perps.start_state",
|
|
33
36
|
"metamask.perps.teardown_state",
|
|
34
37
|
// Consent changes wallet state and is therefore an app mutation.
|
package/dist/runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
1
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
2
2
|
import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
|
|
3
3
|
import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
|
|
4
4
|
import { bridgeCommand } from "../library/actions/mobile/platform/bridge.mjs";
|
|
@@ -239,6 +239,8 @@ function isAutomaticHudProgress(action, node, nodeId) {
|
|
|
239
239
|
function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe = probeMobileLifecycleRuntime, sourceFreshness = {
|
|
240
240
|
fingerprint: mobileSourceFingerprint,
|
|
241
241
|
record: recordMobileSourceBaseline
|
|
242
|
+
}, processIdentity = {
|
|
243
|
+
readIosPid: readIosAppPid
|
|
242
244
|
}) {
|
|
243
245
|
if (adapter !== "mobile") return lifecycle;
|
|
244
246
|
return lifecycle.map((entry) => ({
|
|
@@ -246,6 +248,8 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe =
|
|
|
246
248
|
async execute(node, context) {
|
|
247
249
|
const command = node.command ?? node.event ?? node.state;
|
|
248
250
|
const reloadsSource = command === "launch" || command === "foreground" || command === "restart";
|
|
251
|
+
const checksIosContinuity = command === "foreground" && !isAndroidLifecycle(node, context.env ?? {});
|
|
252
|
+
const processBefore = checksIosContinuity ? processIdentity.readIosPid(node, context) : void 0;
|
|
249
253
|
const fingerprint = reloadsSource ? sourceFreshness.fingerprint(context.projectRoot) : void 0;
|
|
250
254
|
const androidRestart = command === "restart" && isAndroidLifecycle(node, context.env ?? {});
|
|
251
255
|
const initialNode = androidRestart ? { ...node, settle_ms: 0 } : node;
|
|
@@ -261,6 +265,25 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe =
|
|
|
261
265
|
if (reloadsSource) {
|
|
262
266
|
await readinessProbe(node, context);
|
|
263
267
|
}
|
|
268
|
+
if (checksIosContinuity) {
|
|
269
|
+
const processAfter = processIdentity.readIosPid(node, context);
|
|
270
|
+
const actualTransition = processBefore !== null && processBefore === processAfter ? "foreground_resume" : processAfter !== null ? "cold_relaunch" : "unknown";
|
|
271
|
+
const resultRecord = asRecord(result);
|
|
272
|
+
result = {
|
|
273
|
+
...resultRecord,
|
|
274
|
+
output: {
|
|
275
|
+
...asRecord(resultRecord.output),
|
|
276
|
+
actualTransition,
|
|
277
|
+
processBefore,
|
|
278
|
+
processAfter
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
if (node.require_process_continuity === true && actualTransition !== "foreground_resume") {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`iOS foreground did not preserve the app process (actualTransition=${actualTransition}, before=${String(processBefore)}, after=${String(processAfter)}); this lifecycle sample is a cold relaunch, not a reconnect.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
264
287
|
if (fingerprint !== void 0 && !sourceFreshness.record(context.projectRoot, fingerprint)) {
|
|
265
288
|
throw new Error(
|
|
266
289
|
"Mobile source changed while the app was reloading; the loaded-source baseline was not recorded. Next: retry app.lifecycle restart."
|
|
@@ -270,6 +293,26 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe =
|
|
|
270
293
|
}
|
|
271
294
|
}));
|
|
272
295
|
}
|
|
296
|
+
function readIosAppPid(node, context) {
|
|
297
|
+
const env = context.env ?? {};
|
|
298
|
+
const device = node.simulator ?? node.ios_simulator ?? env.SIM_UDID ?? env.IOS_SIMULATOR ?? process.env.SIM_UDID ?? process.env.IOS_SIMULATOR;
|
|
299
|
+
const bundleId = node.bundle_id ?? node.bundleId ?? env.IOS_BUNDLE_ID ?? process.env.IOS_BUNDLE_ID ?? "io.metamask.MetaMask";
|
|
300
|
+
if (typeof device !== "string" || typeof bundleId !== "string") return null;
|
|
301
|
+
try {
|
|
302
|
+
const output = execFileSync(
|
|
303
|
+
"xcrun",
|
|
304
|
+
["simctl", "spawn", device, "launchctl", "list"],
|
|
305
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
306
|
+
);
|
|
307
|
+
const labelPrefix = `UIKitApplication:${bundleId}[`;
|
|
308
|
+
const line = output.split("\n").find((candidate) => candidate.includes(labelPrefix));
|
|
309
|
+
if (!line) return null;
|
|
310
|
+
const pid = Number.parseInt(line.trim().split(/\s+/u)[0] ?? "", 10);
|
|
311
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
273
316
|
function isAndroidLifecycle(node, env) {
|
|
274
317
|
const platform = node.platform ?? env.PLATFORM ?? process.env.PLATFORM;
|
|
275
318
|
if (platform === "android") return true;
|
|
@@ -299,10 +342,9 @@ function mergeAndroidRestartResults(initialResult, foregroundResult) {
|
|
|
299
342
|
}
|
|
300
343
|
async function probeMobileLifecycleRuntime(node, context) {
|
|
301
344
|
const command = node.command ?? node.event ?? node.state;
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
node.runtime_ready_timeout_ms
|
|
305
|
-
defaultTimeoutMs
|
|
345
|
+
const timeoutMs = resolveMobileLifecycleReadinessTimeout(
|
|
346
|
+
command,
|
|
347
|
+
node.runtime_ready_timeout_ms
|
|
306
348
|
);
|
|
307
349
|
const deadline = Date.now() + timeoutMs;
|
|
308
350
|
let lastError;
|
|
@@ -328,8 +370,10 @@ async function probeMobileLifecycleRuntime(node, context) {
|
|
|
328
370
|
`Mobile lifecycle ${String(command)} did not expose a usable pinned __AGENTIC__ runtime within ${timeoutMs}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`
|
|
329
371
|
);
|
|
330
372
|
}
|
|
331
|
-
function
|
|
332
|
-
if (value === void 0)
|
|
373
|
+
function resolveMobileLifecycleReadinessTimeout(command, value) {
|
|
374
|
+
if (value === void 0) {
|
|
375
|
+
return command === "launch" || command === "restart" || command === "foreground" ? 6e4 : 3e4;
|
|
376
|
+
}
|
|
333
377
|
const timeout = Number(value);
|
|
334
378
|
if (!Number.isInteger(timeout) || timeout < 1e3 || timeout > 12e4) {
|
|
335
379
|
throw new Error(
|
|
@@ -364,5 +408,6 @@ export {
|
|
|
364
408
|
createMetaMaskMobileRunner,
|
|
365
409
|
createMetaMaskRunner,
|
|
366
410
|
isAutomaticHudProgress,
|
|
367
|
-
mobileSourceAwareLifecycleAdapters
|
|
411
|
+
mobileSourceAwareLifecycleAdapters,
|
|
412
|
+
resolveMobileLifecycleReadinessTimeout
|
|
368
413
|
};
|
package/dist/runtime-context.js
CHANGED
|
@@ -244,6 +244,7 @@ function persistExplicitDevicePin(target, device) {
|
|
|
244
244
|
return;
|
|
245
245
|
}
|
|
246
246
|
const next = { ...existing };
|
|
247
|
+
next.platform = device.platform;
|
|
247
248
|
if (device.platform === "ios") {
|
|
248
249
|
const simulatorName = device.name ? device.name.replace(/_/gu, " ").trim() : device.id;
|
|
249
250
|
next.simulator = simulatorName;
|
|
@@ -39,8 +39,9 @@ function messengerEntry(projectRoot) {
|
|
|
39
39
|
// a persistent auto-reconnecting HyperLiquid WebSocket (HyperLiquidClientService
|
|
40
40
|
// .initialize → wsTransport.ready()); these logs proved the order returns over
|
|
41
41
|
// HTTP while that open socket kept the event loop alive — see disconnectAndExit.
|
|
42
|
-
function buildInfrastructure(stubbed) {
|
|
42
|
+
function buildInfrastructure(stubbed, input) {
|
|
43
43
|
const noop = () => undefined;
|
|
44
|
+
const terminalApi = resolveTerminalApi(input);
|
|
44
45
|
return {
|
|
45
46
|
logger: {
|
|
46
47
|
error: (error, meta) =>
|
|
@@ -79,6 +80,7 @@ function buildInfrastructure(stubbed) {
|
|
|
79
80
|
setItem: async () => undefined,
|
|
80
81
|
removeItem: async () => undefined,
|
|
81
82
|
},
|
|
83
|
+
...(terminalApi ? { terminalApi } : {}),
|
|
82
84
|
rewards: { getPerpsDiscountForAccount: async () => null },
|
|
83
85
|
};
|
|
84
86
|
}
|
|
@@ -246,6 +248,25 @@ export function resolveNetwork(input) {
|
|
|
246
248
|
return raw;
|
|
247
249
|
}
|
|
248
250
|
|
|
251
|
+
export function resolveTerminalApi(input) {
|
|
252
|
+
const marketDataUrl = String(
|
|
253
|
+
input.node?.terminal_market_data_url ??
|
|
254
|
+
process.env.CORE_PERPS_TERMINAL_MARKET_DATA_URL ??
|
|
255
|
+
'',
|
|
256
|
+
).trim();
|
|
257
|
+
const globalSnapshotUrl = String(
|
|
258
|
+
input.node?.terminal_global_snapshot_url ??
|
|
259
|
+
process.env.CORE_PERPS_TERMINAL_GLOBAL_SNAPSHOT_URL ??
|
|
260
|
+
'',
|
|
261
|
+
).trim();
|
|
262
|
+
|
|
263
|
+
if (!marketDataUrl && !globalSnapshotUrl) return undefined;
|
|
264
|
+
return {
|
|
265
|
+
...(marketDataUrl ? { marketDataUrl } : {}),
|
|
266
|
+
...(globalSnapshotUrl ? { globalSnapshotUrl } : {}),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
249
270
|
export function requireRuntimeExport(moduleNamespace, exportName, sourceLabel) {
|
|
250
271
|
const value = moduleNamespace?.[exportName] ?? moduleNamespace?.default?.[exportName];
|
|
251
272
|
if (value === undefined) {
|
|
@@ -264,8 +285,16 @@ export async function getCoreController(input) {
|
|
|
264
285
|
if (!projectRoot) throw new Error('core adapter requires context.projectRoot.');
|
|
265
286
|
const accountAddress = await requireAccountAddress(input);
|
|
266
287
|
const network = resolveNetwork(input);
|
|
288
|
+
const terminalApi = resolveTerminalApi(input);
|
|
289
|
+
const terminalApiKey = JSON.stringify(terminalApi ?? null);
|
|
267
290
|
|
|
268
|
-
if (
|
|
291
|
+
if (
|
|
292
|
+
cached &&
|
|
293
|
+
cached.projectRoot === projectRoot &&
|
|
294
|
+
cached.network === network &&
|
|
295
|
+
cached.accountAddress.toLowerCase() === accountAddress.toLowerCase() &&
|
|
296
|
+
cached.terminalApiKey === terminalApiKey
|
|
297
|
+
) {
|
|
269
298
|
return { ...cached, accountAddress };
|
|
270
299
|
}
|
|
271
300
|
|
|
@@ -290,7 +319,7 @@ export async function getCoreController(input) {
|
|
|
290
319
|
);
|
|
291
320
|
|
|
292
321
|
const stubbed = new Set();
|
|
293
|
-
const infrastructure = buildInfrastructure(stubbed);
|
|
322
|
+
const infrastructure = buildInfrastructure(stubbed, input);
|
|
294
323
|
|
|
295
324
|
// Root + child messenger pair, mirroring the real app wiring (and the core
|
|
296
325
|
// repo's own test harness in
|
|
@@ -305,6 +334,7 @@ export async function getCoreController(input) {
|
|
|
305
334
|
namespace: 'PerpsController',
|
|
306
335
|
parent: rootMessenger,
|
|
307
336
|
});
|
|
337
|
+
registerSelectedAccountHandler(rootMessenger, messenger, accountAddress);
|
|
308
338
|
|
|
309
339
|
// Default testnet. Mainnet only on explicit node.network: "mainnet" — mainnet
|
|
310
340
|
// reads are safe; mainnet mutations use real funds (gated in getCoreControllerWithSigner).
|
|
@@ -339,7 +369,15 @@ export async function getCoreController(input) {
|
|
|
339
369
|
);
|
|
340
370
|
}
|
|
341
371
|
}
|
|
342
|
-
cached = {
|
|
372
|
+
cached = {
|
|
373
|
+
controller,
|
|
374
|
+
messenger,
|
|
375
|
+
rootMessenger,
|
|
376
|
+
projectRoot,
|
|
377
|
+
network,
|
|
378
|
+
accountAddress,
|
|
379
|
+
terminalApiKey,
|
|
380
|
+
};
|
|
343
381
|
return { ...cached, accountAddress };
|
|
344
382
|
}
|
|
345
383
|
|
|
@@ -366,11 +404,30 @@ export async function getCoreController(input) {
|
|
|
366
404
|
// getSelectedEvmAccountFromMessenger). These get registered on the root and
|
|
367
405
|
// delegated into the PerpsController child messenger.
|
|
368
406
|
const SIGNER_ACTIONS = [
|
|
369
|
-
'AccountsController:getSelectedAccount',
|
|
370
407
|
'KeyringController:getState',
|
|
371
408
|
'KeyringController:signTypedMessage',
|
|
372
409
|
];
|
|
373
410
|
|
|
411
|
+
function registerSelectedAccountHandler(
|
|
412
|
+
rootMessenger,
|
|
413
|
+
childMessenger,
|
|
414
|
+
address,
|
|
415
|
+
) {
|
|
416
|
+
rootMessenger.registerActionHandler(
|
|
417
|
+
'AccountsController:getSelectedAccount',
|
|
418
|
+
() => ({
|
|
419
|
+
id: 'core-headless-account',
|
|
420
|
+
address,
|
|
421
|
+
type: 'eip155:eoa',
|
|
422
|
+
metadata: { keyring: { type: 'HD Key Tree' } },
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
425
|
+
rootMessenger.delegate({
|
|
426
|
+
actions: ['AccountsController:getSelectedAccount'],
|
|
427
|
+
messenger: childMessenger,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
374
431
|
/**
|
|
375
432
|
* Register the signer-backed external handlers on the root messenger and
|
|
376
433
|
* delegate them into the PerpsController child, mirroring the real app's
|
|
@@ -379,26 +436,10 @@ const SIGNER_ACTIONS = [
|
|
|
379
436
|
* @param rootMessenger - The permissive (MOCK_ANY_NAMESPACE) root messenger.
|
|
380
437
|
* @param childMessenger - The PerpsController-namespaced messenger.
|
|
381
438
|
* @param account - The viem signer account.
|
|
382
|
-
* @param address - The selected EVM account address (0x).
|
|
383
439
|
*/
|
|
384
|
-
function registerSignerHandlers(rootMessenger, childMessenger, account
|
|
440
|
+
function registerSignerHandlers(rootMessenger, childMessenger, account) {
|
|
385
441
|
if (rootMessenger.__coreSignerRegistered) return;
|
|
386
442
|
|
|
387
|
-
// AccountsController:getSelectedAccount — getSelectedEvmAccountFromMessenger()
|
|
388
|
-
// calls this first and uses it when the returned object looks like an account
|
|
389
|
-
// with an EVM `type`. Minimal InternalAccount shape: address + EVM type, plus
|
|
390
|
-
// metadata.keyring.type so isSelectedHardwareWallet() sees a software (non-
|
|
391
|
-
// hardware) keyring and allows user signing.
|
|
392
|
-
rootMessenger.registerActionHandler(
|
|
393
|
-
'AccountsController:getSelectedAccount',
|
|
394
|
-
() => ({
|
|
395
|
-
id: 'core-headless-signer',
|
|
396
|
-
address,
|
|
397
|
-
type: 'eip155:eoa',
|
|
398
|
-
metadata: { keyring: { type: 'HD Key Tree' } },
|
|
399
|
-
}),
|
|
400
|
-
);
|
|
401
|
-
|
|
402
443
|
// KeyringController:getState — isKeyringUnlocked() reads `.isUnlocked`. The
|
|
403
444
|
// headless keyring is always unlocked (we hold the private key).
|
|
404
445
|
rootMessenger.registerActionHandler('KeyringController:getState', () => ({
|
|
@@ -479,7 +520,7 @@ export async function getCoreControllerWithSigner(input) {
|
|
|
479
520
|
}
|
|
480
521
|
|
|
481
522
|
const { account, address: signerAddress } = await resolveSignerFromFixture(input);
|
|
482
|
-
registerSignerHandlers(rootMessenger, childMessenger, account
|
|
523
|
+
registerSignerHandlers(rootMessenger, childMessenger, account);
|
|
483
524
|
|
|
484
525
|
// Bring up the active provider. placeOrder/closePosition call
|
|
485
526
|
// getActiveProvider(), which throws CLIENT_NOT_INITIALIZED until init()
|
|
@@ -618,6 +659,17 @@ export function redactPosition(position) {
|
|
|
618
659
|
};
|
|
619
660
|
}
|
|
620
661
|
|
|
662
|
+
export function redactAccount(account) {
|
|
663
|
+
return {
|
|
664
|
+
totalBalance: account?.totalBalance ?? null,
|
|
665
|
+
spendableBalance: account?.spendableBalance ?? null,
|
|
666
|
+
withdrawableBalance: account?.withdrawableBalance ?? null,
|
|
667
|
+
marginUsed: account?.marginUsed ?? null,
|
|
668
|
+
unrealizedPnl: account?.unrealizedPnl ?? null,
|
|
669
|
+
returnOnEquity: account?.returnOnEquity ?? null,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
621
673
|
/**
|
|
622
674
|
* Wrap a controller rejection so its typed code survives as data.
|
|
623
675
|
*
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
withdrawableBalance: account?.withdrawableBalance ?? null,
|
|
8
|
-
marginUsed: account?.marginUsed ?? null,
|
|
9
|
-
unrealizedPnl: account?.unrealizedPnl ?? null,
|
|
10
|
-
returnOnEquity: account?.returnOnEquity ?? null,
|
|
11
|
-
};
|
|
12
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
getCoreController,
|
|
3
|
+
isDirectRun,
|
|
4
|
+
redactAccount,
|
|
5
|
+
runAdapter,
|
|
6
|
+
} from './_controller.mjs';
|
|
13
7
|
|
|
14
8
|
export async function readAccount(input) {
|
|
15
9
|
const { controller, accountAddress, network } = await getCoreController(input);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCoreController,
|
|
3
|
+
isDirectRun,
|
|
4
|
+
redactAccount,
|
|
5
|
+
redactOrder,
|
|
6
|
+
redactPosition,
|
|
7
|
+
runAdapter,
|
|
8
|
+
} from './_controller.mjs';
|
|
9
|
+
|
|
10
|
+
export async function readSnapshotFromController(controller, scope) {
|
|
11
|
+
const output = {};
|
|
12
|
+
|
|
13
|
+
if (scope === 'markets' || scope === 'all') {
|
|
14
|
+
const markets = await controller.getMarketDataWithPrices({
|
|
15
|
+
standalone: true,
|
|
16
|
+
});
|
|
17
|
+
output.markets = {
|
|
18
|
+
count: markets.length,
|
|
19
|
+
dataSources: Array.from(
|
|
20
|
+
new Set(markets.map((market) => market.dataSource ?? 'provider')),
|
|
21
|
+
),
|
|
22
|
+
marketsWithTrend: markets.filter(
|
|
23
|
+
(market) => Array.isArray(market.trend) && market.trend.length > 0,
|
|
24
|
+
).length,
|
|
25
|
+
sampleSymbols: markets.slice(0, 10).map((market) => market.symbol),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (scope === 'account' || scope === 'all') {
|
|
30
|
+
const snapshot = await controller.getUserDataSnapshot();
|
|
31
|
+
output.user = {
|
|
32
|
+
identity: snapshot.identity,
|
|
33
|
+
positions: snapshot.positions.map(redactPosition),
|
|
34
|
+
orders: snapshot.orders.map(redactOrder),
|
|
35
|
+
accountState: redactAccount(snapshot.accountState),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return output;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function readSnapshot(input) {
|
|
43
|
+
const scope = String(input.node?.scope ?? 'all');
|
|
44
|
+
if (!['markets', 'account', 'all'].includes(scope)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`metamask.perps.read_snapshot scope must be markets, account, or all; got ${scope}.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { controller, accountAddress, network } =
|
|
51
|
+
await getCoreController(input);
|
|
52
|
+
const snapshot = await readSnapshotFromController(controller, scope);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
action: input.action,
|
|
56
|
+
source: 'perps-controller-public-api',
|
|
57
|
+
network,
|
|
58
|
+
account: accountAddress,
|
|
59
|
+
scope,
|
|
60
|
+
...snapshot,
|
|
61
|
+
proofPath:
|
|
62
|
+
'PerpsController.getMarketDataWithPrices/getUserDataSnapshot',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isDirectRun(import.meta.url)) runAdapter(readSnapshot);
|