@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +222 -0
  3. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +22 -1
  4. package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +18 -0
  5. package/adapters/mobile/open-device.sh +6 -4
  6. package/dist/adapters/mobile/prepare.js +1 -4
  7. package/dist/adapters.js +139 -8
  8. package/dist/commands/launch/mobile.js +16 -2
  9. package/dist/live-adapter-contract.js +6 -6
  10. package/dist/recipe-security.js +3 -0
  11. package/dist/runner.js +53 -8
  12. package/dist/runtime-context.js +1 -0
  13. package/library/actions/core/perps/_controller.mjs +75 -23
  14. package/library/actions/core/perps/read_account.mjs +6 -12
  15. package/library/actions/core/perps/read_snapshot.mjs +66 -0
  16. package/library/actions/extension/performance/_navigation-memory.mjs +372 -0
  17. package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +18 -0
  18. package/library/actions/extension/performance/measure_detached_dom.mjs +18 -0
  19. package/library/actions/extension/performance/measure_navigation_memory.mjs +18 -0
  20. package/library/actions/mobile/app/network-control.mjs +24 -12
  21. package/library/actions/mobile/perps/measure_homepage_visible.mjs +4 -0
  22. package/library/actions/mobile/perps/performance-capture.mjs +506 -120
  23. package/library/actions/mobile/perps/perps.mjs +221 -38
  24. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +58 -0
  25. package/library/actions/mobile/platform/bridge.mjs +38 -5
  26. package/library/actions/mobile/wallet/ensure_unlocked.mjs +5 -1
  27. package/library/manifests/core.action-manifest.json +55 -0
  28. package/library/manifests/extension.action-manifest.json +100 -0
  29. package/library/manifests/mobile.action-manifest.json +226 -512
  30. package/library/recipes/core/perps/snapshot.recipe.json +25 -0
  31. package/library/recipes/extension/performance/navigation-memory.recipe.json +175 -0
  32. package/library/recipes/mobile/perps/performance.homepage.cold-position-sample.recipe.json +120 -0
  33. package/library/recipes/mobile/perps/performance.homepage.ios-background-reconnect.recipe.json +7 -6
  34. package/library/recipes/mobile/perps/performance.homepage.ios-cold-no-cache.recipe.json +5 -5
  35. package/package.json +1 -1
  36. package/scripts/site-contrast.mjs +39 -3
  37. package/site/architecture.html +22 -16
  38. package/site/assets/progress.mjs +1 -1
  39. package/site/assets/style.css +113 -1
  40. package/site/cheatsheet.html +14 -12
  41. package/site/how-it-works.html +693 -0
  42. package/site/index.html +70 -640
  43. package/site/perps.html +7 -6
  44. package/site/recipes.html +23 -17
  45. package/site/reviewers.html +7 -6
  46. package/site/tutorials/index.html +7 -6
  47. package/site/tutorials/v1.html +12 -11
  48. package/site/tutorials/v2.html +7 -6
  49. package/site/tutorials/v3.html +15 -11
  50. package/site/tutorials/v4.html +6 -5
  51. package/site/tutorials/v5.html +6 -5
  52. package/site/tutorials/v6.html +6 -5
  53. 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
- if (node.timeout_ms != null) {
356
- const timeoutMs = Number(node.timeout_ms);
357
- return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs + settleAllowance + 5e3 : 6e4;
358
- }
359
- const actionTimeouts = [node.target_timeout_ms, node.unlock_timeout_ms].map(Number).filter((value) => Number.isFinite(value) && value > 0);
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;
@@ -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 defaultTimeoutMs = command === "restart" || command === "launch" ? 6e4 : 3e4;
303
- const timeoutMs = lifecycleReadinessTimeout(
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 lifecycleReadinessTimeout(value, fallback) {
332
- if (value === void 0) return fallback;
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
  };
@@ -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 (cached && cached.projectRoot === projectRoot && cached.network === network) {
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 = { controller, messenger, rootMessenger, projectRoot, network };
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, address) {
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, signerAddress);
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 { getCoreController, isDirectRun, runAdapter } from './_controller.mjs';
2
-
3
- function redactAccount(account) {
4
- return {
5
- totalBalance: account?.totalBalance ?? null,
6
- spendableBalance: account?.spendableBalance ?? null,
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);