@deeeed/metamask-harness 0.2.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.
Files changed (158) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +140 -0
  3. package/bin/mm-harness +99 -0
  4. package/docs/CHEATSHEET.md +61 -0
  5. package/docs/CLI-SPEC.md +915 -0
  6. package/docs/MENTAL-MODEL.md +295 -0
  7. package/docs/architecture.md +367 -0
  8. package/docs/extension-runtime-commands.md +60 -0
  9. package/docs/harness-cli.md +43 -0
  10. package/docs/live-adapter-contract.md +188 -0
  11. package/docs/package-boundaries.md +47 -0
  12. package/docs/perps-flow-catalog.md +235 -0
  13. package/docs/recipe-libraries.md +95 -0
  14. package/docs/runtime-file-conventions.md +36 -0
  15. package/library/actions/core/perps/_controller.mjs +727 -0
  16. package/library/actions/core/perps/assert_orders.mjs +53 -0
  17. package/library/actions/core/perps/assert_positions.mjs +52 -0
  18. package/library/actions/core/perps/close_orders.mjs +97 -0
  19. package/library/actions/core/perps/close_positions.mjs +118 -0
  20. package/library/actions/core/perps/ensure_orders.mjs +40 -0
  21. package/library/actions/core/perps/ensure_positions.mjs +37 -0
  22. package/library/actions/core/perps/place_order.mjs +201 -0
  23. package/library/actions/core/perps/read_account.mjs +30 -0
  24. package/library/actions/core/perps/read_orders.mjs +27 -0
  25. package/library/actions/core/perps/read_positions.mjs +27 -0
  26. package/library/actions/core/perps/start_state.mjs +92 -0
  27. package/library/actions/core/perps/teardown_state.mjs +86 -0
  28. package/library/actions/extension/perps/assert_orders.mjs +11 -0
  29. package/library/actions/extension/perps/assert_positions.mjs +11 -0
  30. package/library/actions/extension/perps/close_orders.mjs +8 -0
  31. package/library/actions/extension/perps/close_positions.mjs +8 -0
  32. package/library/actions/extension/perps/ensure_orders.mjs +4 -0
  33. package/library/actions/extension/perps/ensure_positions.mjs +4 -0
  34. package/library/actions/extension/perps/perps.mjs +730 -0
  35. package/library/actions/extension/perps/place_order.mjs +7 -0
  36. package/library/actions/extension/perps/read_orders.mjs +4 -0
  37. package/library/actions/extension/perps/read_positions.mjs +3 -0
  38. package/library/actions/extension/platform/cdp.mjs +541 -0
  39. package/library/actions/extension/ui/navigate.mjs +44 -0
  40. package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
  41. package/library/actions/extension/wallet/read_state.mjs +27 -0
  42. package/library/actions/extension/wallet/select_account.mjs +48 -0
  43. package/library/actions/extension/wallet/setup.mjs +35 -0
  44. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  45. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  46. package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
  47. package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
  48. package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
  49. package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
  50. package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
  51. package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
  52. package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
  53. package/library/actions/mobile/perps/assert_orders.mjs +11 -0
  54. package/library/actions/mobile/perps/assert_positions.mjs +11 -0
  55. package/library/actions/mobile/perps/close_orders.mjs +8 -0
  56. package/library/actions/mobile/perps/close_positions.mjs +8 -0
  57. package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
  58. package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
  59. package/library/actions/mobile/perps/perps.mjs +709 -0
  60. package/library/actions/mobile/perps/place_order.mjs +7 -0
  61. package/library/actions/mobile/perps/read_orders.mjs +4 -0
  62. package/library/actions/mobile/perps/read_positions.mjs +3 -0
  63. package/library/actions/mobile/platform/bridge.mjs +283 -0
  64. package/library/actions/mobile/ui/navigate.mjs +38 -0
  65. package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
  66. package/library/actions/mobile/wallet/home.mjs +35 -0
  67. package/library/actions/mobile/wallet/read_state.mjs +40 -0
  68. package/library/actions/mobile/wallet/select_account.mjs +48 -0
  69. package/library/actions/mobile/wallet/setup.mjs +220 -0
  70. package/library/flows/perps.flows.json +64 -0
  71. package/library/library.json +7 -0
  72. package/library/manifests/core.action-manifest.json +1282 -0
  73. package/library/manifests/extension.action-manifest.json +1749 -0
  74. package/library/manifests/mobile.action-manifest.json +1753 -0
  75. package/library/recipes/action-validation.extension.recipe.json +417 -0
  76. package/library/recipes/action-validation.mobile.recipe.json +422 -0
  77. package/library/recipes/order-lifecycle.core.recipe.json +78 -0
  78. package/library/recipes/perps-lifecycle.recipe.json +194 -0
  79. package/library/recipes/read-markets.core.recipe.json +38 -0
  80. package/library/recipes/smoke.extension.recipe.json +31 -0
  81. package/library/recipes/smoke.mobile.recipe.json +31 -0
  82. package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
  83. package/orchestration/compat-overlays/README.md +19 -0
  84. package/orchestration/compat-overlays/mobile/README.md +13 -0
  85. package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
  86. package/orchestration/core/cleanup.sh +37 -0
  87. package/orchestration/core/inject.sh +154 -0
  88. package/orchestration/doctor.mjs +72 -0
  89. package/orchestration/extension/cleanup.mjs +60 -0
  90. package/orchestration/extension/console-tail.mjs +228 -0
  91. package/orchestration/extension/ensure-browser.sh +416 -0
  92. package/orchestration/extension/ensure-ready.ts +185 -0
  93. package/orchestration/extension/extension-id.ts +107 -0
  94. package/orchestration/extension/inject.mjs +266 -0
  95. package/orchestration/extension/launch-browser.cjs +216 -0
  96. package/orchestration/extension/launch.sh +175 -0
  97. package/orchestration/extension/live.sh +320 -0
  98. package/orchestration/extension/pin-remote-flags.cjs +45 -0
  99. package/orchestration/extension/readiness.mjs +414 -0
  100. package/orchestration/extension/refresh-build.sh +190 -0
  101. package/orchestration/extension/runtime-decision.ts +445 -0
  102. package/orchestration/extension/runtime.ts +407 -0
  103. package/orchestration/extension/seed-fixture.sh +177 -0
  104. package/orchestration/extension/sidepanel-toggle.sh +291 -0
  105. package/orchestration/extension/snapshot-dist.sh +84 -0
  106. package/orchestration/extension/start-watch.sh +339 -0
  107. package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
  108. package/orchestration/lib/activate-repo-node.sh +144 -0
  109. package/orchestration/lib/cli-color.mjs +84 -0
  110. package/orchestration/lib/cli-commands.mjs +243 -0
  111. package/orchestration/lib/cli-home.mjs +354 -0
  112. package/orchestration/lib/cli-ux.sh +252 -0
  113. package/orchestration/lib/cli-version.mjs +123 -0
  114. package/orchestration/lib/ensure-runner-deps.sh +56 -0
  115. package/orchestration/lib/harness-path.sh +55 -0
  116. package/orchestration/lib/hash-helpers.sh +44 -0
  117. package/orchestration/lib/json-field.sh +23 -0
  118. package/orchestration/lib/log-tui.mjs +304 -0
  119. package/orchestration/lib/open-debug.mjs +317 -0
  120. package/orchestration/lib/path-defaults.json +4 -0
  121. package/orchestration/lib/progress.mjs +107 -0
  122. package/orchestration/lib/recipe-paths.mjs +26 -0
  123. package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
  124. package/orchestration/manifest.json +358 -0
  125. package/orchestration/mobile/cleanup.sh +192 -0
  126. package/orchestration/mobile/deps-markers.ts +21 -0
  127. package/orchestration/mobile/inject.sh +681 -0
  128. package/orchestration/mobile/launch.sh +137 -0
  129. package/orchestration/mobile/live.sh +125 -0
  130. package/orchestration/mobile/runtime-decision.ts +292 -0
  131. package/orchestration/porcelain/metamask-recipe +99 -0
  132. package/orchestration/porcelain/mm-recipe +1591 -0
  133. package/orchestration/porcelain/mme-recipe +1181 -0
  134. package/package.json +59 -0
  135. package/runner/extension/verify.sh +511 -0
  136. package/runner/mobile/verify.sh +501 -0
  137. package/runner/src/adapters.ts +601 -0
  138. package/runner/src/cli.ts +1820 -0
  139. package/runner/src/commands/debug.ts +44 -0
  140. package/runner/src/commands/fixtures.ts +99 -0
  141. package/runner/src/commands/launch.ts +397 -0
  142. package/runner/src/commands/logs.ts +60 -0
  143. package/runner/src/commands/shared.ts +138 -0
  144. package/runner/src/completions-cache.ts +86 -0
  145. package/runner/src/doctor.ts +203 -0
  146. package/runner/src/harness.ts +516 -0
  147. package/runner/src/heal-bounds.ts +179 -0
  148. package/runner/src/index.ts +6 -0
  149. package/runner/src/live-adapter-contract.ts +274 -0
  150. package/runner/src/manifest.ts +47 -0
  151. package/runner/src/mm-harness-cli.ts +488 -0
  152. package/runner/src/paths.ts +198 -0
  153. package/runner/src/recording-target.ts +147 -0
  154. package/runner/src/run-recording.ts +329 -0
  155. package/runner/src/runner.ts +108 -0
  156. package/runner/src/types.ts +57 -0
  157. package/scripts/completions.sh +125 -0
  158. package/scripts/install-completions.sh +62 -0
@@ -0,0 +1,1662 @@
1
+ /**
2
+ * AgenticService — __DEV__-only bridge for Recipe v1 runners.
3
+ *
4
+ * This file is not bundled in production builds. It exposes a stable control
5
+ * surface for deterministic local recipes while keeping user-visible proof
6
+ * flows on the real app path.
7
+ */
8
+ import {
9
+ NavigationContainerRef,
10
+ ParamListBase,
11
+ } from '@react-navigation/native';
12
+ import { Dimensions, Platform } from 'react-native';
13
+ import Logger from '../../util/Logger';
14
+ import ReduxService from '../../core/redux';
15
+ import { persistor } from '../../store';
16
+ import Engine from '../../core/Engine';
17
+ import { Engine as EngineClass } from '../../core/Engine/Engine';
18
+ import {
19
+ passwordSet,
20
+ setExistingUser,
21
+ logIn,
22
+ seedphraseBackedUp,
23
+ setMultichainAccountsIntroModalSeen,
24
+ } from '../../actions/user';
25
+ import { setCompletedOnboarding } from '../../actions/onboarding';
26
+ import { mnemonicPhraseToBytes } from '@metamask/key-tree';
27
+ import { AccountImportStrategy } from '@metamask/keyring-controller';
28
+ import type { AccountGroupId } from '@metamask/account-api';
29
+ import StorageWrapper from '../../store/storage-wrapper';
30
+ import {
31
+ OPTIN_META_METRICS_UI_SEEN,
32
+ PERPS_GTM_MODAL_SHOWN,
33
+ PREDICT_GTM_MODAL_SHOWN,
34
+ REWARDS_GTM_MODAL_SHOWN,
35
+ } from '../../constants/storage';
36
+ import { analytics } from '../../util/analytics/analytics';
37
+ import {
38
+ setDataCollectionForMarketing,
39
+ setOsAuthEnabled,
40
+ } from '../../actions/security';
41
+ import { setLockTime } from '../../actions/settings';
42
+ import AccountTreeInitService from '../../multichain-accounts/AccountTreeInitService';
43
+ import Routes from '../../constants/navigation/Routes';
44
+ import SecureKeychain from '../../core/SecureKeychain';
45
+ import AUTHENTICATION_TYPE from '../../constants/userProperties';
46
+ import DevLogger from '../../core/SDKConnect/utils/DevLogger';
47
+ import { importNewSecretRecoveryPhrase } from '../../actions/multiSrp';
48
+ import { bufferToHex, privateToAddress } from 'ethereumjs-util';
49
+ import Authentication from '../../core/Authentication';
50
+ import { emitStepHud } from './AgentStepHud';
51
+ import { Wallet as EthersWallet } from 'ethers';
52
+ import PerpsConnectionManager from '../../components/UI/Perps/services/PerpsConnectionManager';
53
+ import { getStreamManagerInstance } from '../../components/UI/Perps/providers/PerpsStreamManager';
54
+
55
+ // ─── Fiber tree types ──────────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Minimal React fiber node shape used to walk the component tree
59
+ * via __REACT_DEVTOOLS_GLOBAL_HOOK__.
60
+ */
61
+ interface FiberNode {
62
+ child: FiberNode | null;
63
+ sibling: FiberNode | null;
64
+ return: FiberNode | null;
65
+ memoizedProps: {
66
+ testID?: string;
67
+ onPress?: (...args: unknown[]) => unknown;
68
+ onChangeText?: (text: string) => void;
69
+ [key: string]: unknown;
70
+ } | null;
71
+ stateNode: {
72
+ scrollTo?: (opts: { y: number; animated: boolean }) => void;
73
+ scrollToOffset?: (opts: { offset: number; animated: boolean }) => void;
74
+ measure?: (
75
+ callback: (
76
+ x: number,
77
+ y: number,
78
+ width: number,
79
+ height: number,
80
+ pageX: number,
81
+ pageY: number,
82
+ ) => void,
83
+ ) => void;
84
+ measureInWindow?: (
85
+ callback: (x: number, y: number, width: number, height: number) => void,
86
+ ) => void;
87
+ [key: string]: unknown;
88
+ } | null;
89
+ }
90
+
91
+ interface FiberRoot {
92
+ current: FiberNode | null;
93
+ }
94
+
95
+ interface ReactDevToolsHook {
96
+ renderers: Map<number, unknown>;
97
+ getFiberRoots?: (id: number) => Set<FiberRoot>;
98
+ }
99
+
100
+ /** Shape of the __DEV__-only agentic bridge on globalThis. */
101
+ interface AgenticHudStep {
102
+ id: string;
103
+ status?: string;
104
+ intent: string;
105
+ progress?: { current?: number; total?: number };
106
+ detail?: string;
107
+ error?: string;
108
+ nodeId?: string;
109
+ debug?: { nodeId?: string; proofTarget?: unknown };
110
+ }
111
+
112
+ interface AgenticBridge {
113
+ platform: string;
114
+ replayHarnessPatch?: string;
115
+ navigate: (name: string, params?: object) => void;
116
+ getRoute: () => unknown;
117
+ getState: () => unknown;
118
+ canGoBack: () => boolean;
119
+ goBack: () => void;
120
+ listAccounts: () => { id: string; address: string; name: string }[];
121
+ getSelectedAccount: () => { id: string; address: string; name: string };
122
+ pressTestId: (testId: string) => {
123
+ ok: boolean;
124
+ testId?: string;
125
+ error?: string;
126
+ };
127
+ pressText: (
128
+ text: string,
129
+ options?: { requiredTexts?: string[]; maxTexts?: number },
130
+ ) => { ok: boolean; text?: string; error?: string };
131
+ scrollView: (options?: {
132
+ testId?: string;
133
+ offset?: number;
134
+ animated?: boolean;
135
+ }) => {
136
+ ok: boolean;
137
+ error?: string;
138
+ testId?: string;
139
+ offset?: number;
140
+ animated?: boolean;
141
+ };
142
+ setInput: (
143
+ testId: string,
144
+ value: string,
145
+ ) => {
146
+ ok: boolean;
147
+ testId?: string;
148
+ value?: string;
149
+ error?: string;
150
+ };
151
+ getTextByTestId: (
152
+ testId: string,
153
+ options?: { all?: boolean },
154
+ ) => string | string[] | null;
155
+ getAncestorTextsByTestId: (
156
+ testId: string,
157
+ options?: { requiredLabels?: string[]; maxTexts?: number },
158
+ ) => string[] | null;
159
+ getRowValue: (
160
+ label: string,
161
+ pattern: string,
162
+ options?: {
163
+ anchorTestId?: string;
164
+ requiredLabels?: string[];
165
+ maxTexts?: number;
166
+ },
167
+ ) => string | null;
168
+ switchAccount: (address: string) => {
169
+ switched: boolean;
170
+ id: string;
171
+ address: string;
172
+ name: string;
173
+ };
174
+ setupWallet: (fixture: {
175
+ password: string;
176
+ accounts: {
177
+ type: 'mnemonic' | 'privateKey';
178
+ value: string;
179
+ name?: string;
180
+ count?: number;
181
+ numberOfAccounts?: number;
182
+ names?: string[];
183
+ }[];
184
+ settings?: {
185
+ metametrics?: boolean;
186
+ skipGtmModals?: boolean;
187
+ skipPerpsTutorial?: boolean;
188
+ autoLockNever?: boolean;
189
+ deviceAuthEnabled?: boolean;
190
+ };
191
+ }) => Promise<{
192
+ ok: boolean;
193
+ error?: string;
194
+ step?: string;
195
+ accounts?: { address: string; name: string }[];
196
+ }>;
197
+ applyWalletFixture: (
198
+ fixture: Parameters<AgenticBridge['setupWallet']>[0],
199
+ ) => Promise<{
200
+ ok: boolean;
201
+ error?: string;
202
+ accounts?: { address: string; name: string }[];
203
+ }>;
204
+ showStep: (step: AgenticHudStep) => void;
205
+ hideStep: () => void;
206
+ refreshPerpsStreams: () => Promise<{ ok: boolean; positions: number }>;
207
+ findFiberByTestId: (testId: string) => boolean;
208
+ queryUiTarget: (options: {
209
+ testId?: string;
210
+ textContains?: string;
211
+ visibility?: 'tree' | 'viewport';
212
+ }) => Promise<{
213
+ present: boolean;
214
+ visible: boolean;
215
+ visibility: 'tree' | 'viewport';
216
+ testId?: string;
217
+ textContains?: string;
218
+ textMatched?: boolean;
219
+ rect?: { x: number; y: number; width: number; height: number };
220
+ viewport?: { width: number; height: number };
221
+ error?: string;
222
+ }>;
223
+ }
224
+
225
+ type WalletFixture = Parameters<AgenticBridge['setupWallet']>[0];
226
+
227
+ interface FixtureKeyringController {
228
+ isUnlocked: () => boolean;
229
+ submitPassword: (password: string) => Promise<void>;
230
+ }
231
+
232
+ function logFixtureStep(step: string, detail?: Record<string, unknown>) {
233
+ DevLogger.log(`[AgenticService] fixture ${step}`, detail);
234
+ }
235
+
236
+ async function withFixtureTimeout<T>(
237
+ label: string,
238
+ work: Promise<T>,
239
+ timeoutMs = 30_000,
240
+ ): Promise<T> {
241
+ let timer: ReturnType<typeof setTimeout> | undefined;
242
+ try {
243
+ return await Promise.race([
244
+ work,
245
+ new Promise<never>((_resolve, reject) => {
246
+ timer = setTimeout(
247
+ () => reject(new Error(`Fixture step timed out: ${label}`)),
248
+ timeoutMs,
249
+ );
250
+ }),
251
+ ]);
252
+ } finally {
253
+ if (timer) {
254
+ clearTimeout(timer);
255
+ }
256
+ }
257
+ }
258
+
259
+ const sleep = (ms: number): Promise<void> =>
260
+ new Promise((resolve) => setTimeout(resolve, ms));
261
+
262
+ async function waitForFixtureCondition(
263
+ label: string,
264
+ condition: () => boolean,
265
+ timeoutMs = 90_000,
266
+ intervalMs = 500,
267
+ ): Promise<void> {
268
+ const deadline = Date.now() + timeoutMs;
269
+ while (Date.now() < deadline) {
270
+ if (condition()) {
271
+ return;
272
+ }
273
+ await sleep(intervalMs);
274
+ }
275
+ throw new Error(`Fixture step timed out: ${label}`);
276
+ }
277
+
278
+ async function createFixtureHdAccount(
279
+ label: string,
280
+ createAccount: () => Promise<unknown>,
281
+ accountExists: () => boolean,
282
+ ): Promise<void> {
283
+ await withFixtureTimeout(label, createAccount(), 90_000);
284
+ await waitForFixtureCondition(label, accountExists, 30_000);
285
+ }
286
+
287
+ declare global {
288
+ // eslint-disable-next-line no-var
289
+ var __AGENTIC__: AgenticBridge | undefined;
290
+ // eslint-disable-next-line no-var
291
+ var __REACT_DEVTOOLS_GLOBAL_HOOK__: ReactDevToolsHook | undefined;
292
+ }
293
+
294
+ // ─── Fiber tree helpers ─────────────────────────────────────────────────────
295
+
296
+ /**
297
+ * Walk a fiber sub-tree depth-first, calling `visitor` on each node.
298
+ * Returns true as soon as visitor returns true (short-circuit).
299
+ * Sibling traversal is iterative to avoid stack overflow on wide trees.
300
+ */
301
+ function walkFiber(
302
+ fiber: FiberNode | null,
303
+ visitor: (f: FiberNode) => boolean,
304
+ ): boolean {
305
+ let current: FiberNode | null = fiber;
306
+ while (current) {
307
+ if (visitor(current)) return true;
308
+ if (walkFiber(current.child, visitor)) return true;
309
+ current = current.sibling;
310
+ }
311
+ return false;
312
+ }
313
+
314
+ /**
315
+ * Find the first fiber node whose `testID` prop matches.
316
+ */
317
+ function findFiberByTestId(
318
+ fiber: FiberNode | null,
319
+ testId: string,
320
+ ): FiberNode | null {
321
+ let result: FiberNode | null = null;
322
+ walkFiber(fiber, (f) => {
323
+ if (f.memoizedProps?.testID === testId) {
324
+ result = f;
325
+ return true;
326
+ }
327
+ return false;
328
+ });
329
+ return result;
330
+ }
331
+
332
+ /**
333
+ * Iterate all React renderer roots and call `visitor` on each root fiber.
334
+ * Returns true if any visitor call returns true.
335
+ */
336
+ function walkFiberRoots(visitor: (rootFiber: FiberNode) => boolean): boolean {
337
+ const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
338
+ if (!hook?.renderers) return false;
339
+
340
+ for (const [id] of hook.renderers) {
341
+ const fiberRoots = hook.getFiberRoots?.(id);
342
+ if (!fiberRoots) continue;
343
+ let found = false;
344
+ fiberRoots.forEach((root) => {
345
+ if (!found && root.current) {
346
+ found = visitor(root.current);
347
+ }
348
+ });
349
+ if (found) return true;
350
+ }
351
+ return false;
352
+ }
353
+
354
+ // ─── Shared helpers ─────────────────────────────────────────────────────────
355
+
356
+ /** Map an internal account to the slim shape exposed by the bridge. */
357
+ function toAccountSummary(a: {
358
+ id: string;
359
+ address: string;
360
+ metadata: { name: string };
361
+ }): { id: string; address: string; name: string } {
362
+ return { id: a.id, address: a.address, name: a.metadata.name };
363
+ }
364
+
365
+ export function getFixtureMnemonicCount(account?: {
366
+ count?: number;
367
+ numberOfAccounts?: number;
368
+ }): number {
369
+ const raw = account?.count ?? account?.numberOfAccounts ?? 1;
370
+ const count = Number(raw);
371
+ if (!Number.isInteger(count) || count < 1 || count > 100) {
372
+ throw new Error(`Invalid mnemonic account count: ${raw}`);
373
+ }
374
+ return count;
375
+ }
376
+
377
+ export function getFixtureAccountNames(
378
+ account: { name?: string; names?: string[] } | undefined,
379
+ count: number,
380
+ ): string[] {
381
+ return Array.from({ length: count }, (_unused, index) => {
382
+ const explicitName = account?.names?.[index];
383
+ if (typeof explicitName === 'string' && explicitName.trim()) {
384
+ return explicitName.trim();
385
+ }
386
+ if (
387
+ index === 0 &&
388
+ typeof account?.name === 'string' &&
389
+ account.name.trim()
390
+ ) {
391
+ return account.name.trim();
392
+ }
393
+ return `Account ${index + 1}`;
394
+ });
395
+ }
396
+
397
+ interface FixtureEvmAccount {
398
+ id: string;
399
+ address: string;
400
+ metadata: { name: string; keyring?: { type?: string } };
401
+ }
402
+
403
+ function findEvmAccounts(accounts: Record<string, unknown>) {
404
+ return (Object.values(accounts) as FixtureEvmAccount[]).filter((account) =>
405
+ account.address?.startsWith('0x'),
406
+ );
407
+ }
408
+
409
+ function isHdFixtureAccount(account: FixtureEvmAccount) {
410
+ return account.metadata?.keyring?.type === 'HD Key Tree';
411
+ }
412
+
413
+ function normalizePrivateKey(value: string) {
414
+ return value.startsWith('0x') ? value.slice(2) : value;
415
+ }
416
+
417
+ function getPrivateKeyAddress(value: string) {
418
+ return bufferToHex(
419
+ privateToAddress(Buffer.from(normalizePrivateKey(value), 'hex')),
420
+ ).toLowerCase();
421
+ }
422
+
423
+ function isExpectedLegacyAccountTreeInitError(error: unknown) {
424
+ const message = String((error as Error).message || error);
425
+ return (
426
+ message.includes('Money Keyring') ||
427
+ message.includes('No keyringBuilder found')
428
+ );
429
+ }
430
+
431
+ async function initializeFixtureAccountTree(
432
+ options: {
433
+ allowLegacyAccountTreeInitFailure?: boolean;
434
+ } = {},
435
+ ) {
436
+ try {
437
+ await AccountTreeInitService.initializeAccountTree();
438
+ } catch (error) {
439
+ if (
440
+ !options.allowLegacyAccountTreeInitFailure ||
441
+ !isExpectedLegacyAccountTreeInitError(error)
442
+ ) {
443
+ throw error;
444
+ }
445
+ // Historical replay vaults can lack the multichain keyring builder. In that
446
+ // mode AccountsController remains the source of truth for fixture validation
447
+ // and account labels; group renames are skipped when no account-tree group
448
+ // exists.
449
+ Logger.log(
450
+ '[AgenticService] Skipping fixture account-tree refresh for historical replay fixture setup',
451
+ );
452
+ }
453
+ }
454
+
455
+ async function markFixtureMetricsOptInSeenIfNeeded(fixture: WalletFixture) {
456
+ if (fixture.settings?.metametrics !== undefined) {
457
+ await StorageWrapper.setItem(OPTIN_META_METRICS_UI_SEEN, 'true');
458
+ }
459
+ }
460
+
461
+ function resetNavigationToWalletHome(
462
+ navRef: NavigationContainerRef<ParamListBase>,
463
+ ) {
464
+ navRef.reset({
465
+ routes: [{ name: Routes.ONBOARDING.HOME_NAV }],
466
+ });
467
+ }
468
+
469
+ async function unlockFixtureVault(
470
+ fixture: WalletFixture,
471
+ keyringController: FixtureKeyringController,
472
+ ) {
473
+ if (keyringController.isUnlocked()) {
474
+ return;
475
+ }
476
+
477
+ logFixtureStep('unlock-via-authentication');
478
+ await Authentication.unlockWallet({ password: fixture.password });
479
+ if (keyringController.isUnlocked()) {
480
+ return;
481
+ }
482
+
483
+ // A previous interrupted fixture setup can leave a vault present while Redux
484
+ // still marks the user as new. Authentication.unlockWallet then navigates to
485
+ // onboarding without submitting the password. Recover that dev-only partial
486
+ // state before materializing accounts.
487
+ logFixtureStep('unlock-via-keyring-fallback');
488
+ await keyringController.submitPassword(fixture.password);
489
+ if (!keyringController.isUnlocked()) {
490
+ throw new Error('Fixture vault remained locked after password submit');
491
+ }
492
+
493
+ const store = ReduxService.store;
494
+ store.dispatch(passwordSet());
495
+ store.dispatch(seedphraseBackedUp());
496
+ store.dispatch(setCompletedOnboarding(true));
497
+ store.dispatch(setExistingUser(true));
498
+ store.dispatch(logIn());
499
+ store.dispatch(setMultichainAccountsIntroModalSeen(true));
500
+ }
501
+
502
+ function getMnemonicFirstAddress(value: string) {
503
+ return EthersWallet.fromMnemonic(
504
+ value,
505
+ "m/44'/60'/0'/0/0",
506
+ ).address.toLowerCase();
507
+ }
508
+
509
+ interface FixtureHdWallet {
510
+ keyringId?: string;
511
+ accounts: FixtureEvmAccount[];
512
+ }
513
+
514
+ function getHdFixtureWallets(
515
+ accountsController: {
516
+ state: { internalAccounts: { accounts: Record<string, unknown> } };
517
+ },
518
+ accountTreeController: {
519
+ state: { accountTree?: { wallets?: Record<string, unknown> } };
520
+ },
521
+ ): FixtureHdWallet[] {
522
+ const evmById = new Map(
523
+ findEvmAccounts(accountsController.state.internalAccounts.accounts).map(
524
+ (account) => [account.id, account],
525
+ ),
526
+ );
527
+ const wallets = accountTreeController.state.accountTree?.wallets ?? {};
528
+ const result: FixtureHdWallet[] = [];
529
+ for (const wallet of Object.values(wallets) as {
530
+ metadata?: { entropy?: { id?: string } };
531
+ groups?: Record<
532
+ string,
533
+ { accounts?: string[]; metadata?: { entropy?: { groupIndex?: number } } }
534
+ >;
535
+ }[]) {
536
+ const entropyId = wallet.metadata?.entropy?.id;
537
+ if (!entropyId) continue;
538
+ const accounts = Object.values(wallet.groups ?? {})
539
+ .map((group) => ({
540
+ index: group.metadata?.entropy?.groupIndex ?? Number.MAX_SAFE_INTEGER,
541
+ account: group.accounts?.[0]
542
+ ? evmById.get(group.accounts[0])
543
+ : undefined,
544
+ }))
545
+ .filter((entry): entry is { index: number; account: FixtureEvmAccount } =>
546
+ Boolean(entry.account),
547
+ )
548
+ .sort((left, right) => left.index - right.index)
549
+ .map((entry) => entry.account);
550
+ if (accounts.length > 0) {
551
+ result.push({ keyringId: entropyId, accounts });
552
+ }
553
+ }
554
+ if (result.length > 0) {
555
+ return result;
556
+ }
557
+ const legacyHdAccounts = findEvmAccounts(
558
+ accountsController.state.internalAccounts.accounts,
559
+ ).filter(isHdFixtureAccount);
560
+ return legacyHdAccounts.length > 0 ? [{ accounts: legacyHdAccounts }] : [];
561
+ }
562
+
563
+ async function ensureFixtureMnemonicAccounts(
564
+ mnemonicAccount: WalletFixture['accounts'][number],
565
+ mnemonicIndex: number,
566
+ controllers: {
567
+ AccountsController: {
568
+ state: { internalAccounts: { accounts: Record<string, unknown> } };
569
+ };
570
+ AccountTreeController: {
571
+ state: { accountTree?: { wallets?: Record<string, unknown> } };
572
+ setAccountGroupName: (
573
+ accountGroupId: AccountGroupId,
574
+ accountGroupName: string,
575
+ ) => void;
576
+ };
577
+ },
578
+ options: { allowLegacyAccountTreeInitFailure?: boolean } = {},
579
+ ) {
580
+ const { AccountsController, AccountTreeController } = controllers;
581
+ const count = getFixtureMnemonicCount(mnemonicAccount);
582
+ const names = getFixtureAccountNames(mnemonicAccount, count);
583
+ const firstAddress = getMnemonicFirstAddress(mnemonicAccount.value);
584
+ const findWallet = () =>
585
+ getHdFixtureWallets(AccountsController, AccountTreeController).find(
586
+ (hdWallet) =>
587
+ hdWallet.accounts[0]?.address.toLowerCase() === firstAddress,
588
+ );
589
+
590
+ let wallet = findWallet();
591
+ if (!wallet) {
592
+ logFixtureStep('import-mnemonic', { mnemonicIndex });
593
+ await importNewSecretRecoveryPhrase(mnemonicAccount.value, {
594
+ shouldSelectAccount: false,
595
+ });
596
+ await initializeFixtureAccountTree(options);
597
+ wallet = findWallet();
598
+ }
599
+
600
+ if (!wallet) {
601
+ throw new Error(
602
+ `No HD wallet found for fixture mnemonic ${mnemonicIndex + 1}`,
603
+ );
604
+ }
605
+
606
+ if (wallet.accounts.length < count && !wallet.keyringId) {
607
+ throw new Error(
608
+ `Cannot add fixture accounts to legacy vault (no entropy source); fixture expects ${count} accounts but vault has ${wallet.accounts.length}`,
609
+ );
610
+ }
611
+ const { MultichainAccountService } = Engine.context;
612
+ if (wallet.accounts.length < count) {
613
+ const fromGroupIndex = wallet.accounts.length;
614
+ const toGroupIndex = count - 1;
615
+ const entropySource = wallet.keyringId as string;
616
+ logFixtureStep('create-hd-accounts', {
617
+ mnemonicIndex,
618
+ fromGroupIndex,
619
+ toGroupIndex,
620
+ count,
621
+ });
622
+ await createFixtureHdAccount(
623
+ `create HD accounts ${fromGroupIndex + 1}-${count} for mnemonic ${
624
+ mnemonicIndex + 1
625
+ }`,
626
+ () =>
627
+ MultichainAccountService.createMultichainAccountGroups({
628
+ fromGroupIndex,
629
+ toGroupIndex,
630
+ entropySource,
631
+ }),
632
+ () => (findWallet()?.accounts.length ?? 0) >= count,
633
+ );
634
+ wallet = findWallet();
635
+ if (!wallet) {
636
+ throw new Error(
637
+ `No HD wallet found after adding fixture accounts ${fromGroupIndex + 1}-${count}`,
638
+ );
639
+ }
640
+ }
641
+
642
+ wallet.accounts.slice(0, count).forEach((account, index) => {
643
+ setFixtureAccountName(AccountTreeController, account, names[index]);
644
+ });
645
+ }
646
+
647
+ function findAccountGroupIdByAccountId(
648
+ accountTreeController: {
649
+ state: { accountTree?: { wallets?: Record<string, unknown> } };
650
+ },
651
+ accountId: string,
652
+ ): string | undefined {
653
+ const wallets = accountTreeController.state.accountTree?.wallets ?? {};
654
+ for (const wallet of Object.values(wallets) as {
655
+ groups?: Record<string, { accounts?: string[] }>;
656
+ }[]) {
657
+ for (const [groupId, group] of Object.entries(wallet.groups ?? {})) {
658
+ if (group.accounts?.includes(accountId)) {
659
+ return groupId;
660
+ }
661
+ }
662
+ }
663
+ return undefined;
664
+ }
665
+
666
+ function setFixtureAccountName(
667
+ accountTreeController: {
668
+ state: { accountTree?: { wallets?: Record<string, unknown> } };
669
+ setAccountGroupName: (
670
+ accountGroupId: AccountGroupId,
671
+ accountGroupName: string,
672
+ ) => void;
673
+ },
674
+ account: { id: string; address: string },
675
+ name: string,
676
+ ) {
677
+ Engine.setAccountLabel(account.address, name);
678
+ const groupId = findAccountGroupIdByAccountId(
679
+ accountTreeController,
680
+ account.id,
681
+ );
682
+ // Legacy vault fallback skips account-tree init, so no group exists yet.
683
+ // The account label set above is sufficient; skip the group rename instead
684
+ // of throwing and crashing fixture setup.
685
+ if (!groupId) {
686
+ DevLogger.log(
687
+ `[AgenticService] No account group for fixture account ${account.address}; skipped group rename`,
688
+ );
689
+ return;
690
+ }
691
+ accountTreeController.setAccountGroupName(groupId as AccountGroupId, name);
692
+ }
693
+
694
+ async function materializeFixtureAccounts(
695
+ fixture: WalletFixture,
696
+ controllers: {
697
+ KeyringController: {
698
+ importAccountWithStrategy: (
699
+ strategy: AccountImportStrategy,
700
+ args: string[],
701
+ ) => Promise<unknown>;
702
+ };
703
+ AccountsController: {
704
+ state: { internalAccounts: { accounts: Record<string, unknown> } };
705
+ };
706
+ AccountTreeController: {
707
+ state: { accountTree?: { wallets?: Record<string, unknown> } };
708
+ setAccountGroupName: (
709
+ accountGroupId: AccountGroupId,
710
+ accountGroupName: string,
711
+ ) => void;
712
+ };
713
+ },
714
+ options: { allowLegacyAccountTreeInitFailure?: boolean } = {},
715
+ ) {
716
+ const { KeyringController, AccountsController, AccountTreeController } =
717
+ controllers;
718
+ const mnemonicAccounts = fixture.accounts.filter(
719
+ (account) => account.type === 'mnemonic',
720
+ );
721
+
722
+ for (const [mnemonicIndex, mnemonicAccount] of mnemonicAccounts.entries()) {
723
+ await ensureFixtureMnemonicAccounts(
724
+ mnemonicAccount,
725
+ mnemonicIndex,
726
+ {
727
+ AccountsController,
728
+ AccountTreeController,
729
+ },
730
+ options,
731
+ );
732
+ }
733
+
734
+ for (const account of fixture.accounts) {
735
+ if (account.type !== 'privateKey') continue;
736
+ const address = getPrivateKeyAddress(account.value);
737
+ let imported = findEvmAccounts(
738
+ AccountsController.state.internalAccounts.accounts,
739
+ ).find((evmAccount) => evmAccount.address.toLowerCase() === address);
740
+
741
+ if (!imported) {
742
+ logFixtureStep('import-private-key', { address });
743
+ await withFixtureTimeout(
744
+ `import private key ${address}`,
745
+ KeyringController.importAccountWithStrategy(
746
+ AccountImportStrategy.privateKey,
747
+ [`0x${normalizePrivateKey(account.value)}`],
748
+ ),
749
+ );
750
+ imported = findEvmAccounts(
751
+ AccountsController.state.internalAccounts.accounts,
752
+ ).find((evmAccount) => evmAccount.address.toLowerCase() === address);
753
+ }
754
+
755
+ if (!imported) {
756
+ throw new Error(
757
+ `Fixture private key import did not create account ${address}`,
758
+ );
759
+ }
760
+ if (account.name) {
761
+ setFixtureAccountName(AccountTreeController, imported, account.name);
762
+ }
763
+ }
764
+ }
765
+
766
+ /**
767
+ * Walk a fiber sub-tree looking for a scrollable stateNode (scrollTo or
768
+ * scrollToOffset). When `walkSiblings` is false only the child axis is
769
+ * traversed — useful when starting from a testId anchor whose siblings
770
+ * are unrelated components.
771
+ */
772
+ function tryScroll(
773
+ start: FiberNode | null,
774
+ offset: number,
775
+ animated: boolean,
776
+ walkSiblings = true,
777
+ ): boolean {
778
+ let current: FiberNode | null = start;
779
+ while (current) {
780
+ const sn = current.stateNode;
781
+ if (sn) {
782
+ if (typeof sn.scrollTo === 'function') {
783
+ sn.scrollTo({ y: offset, animated });
784
+ return true;
785
+ }
786
+ if (typeof sn.scrollToOffset === 'function') {
787
+ sn.scrollToOffset({ offset, animated });
788
+ return true;
789
+ }
790
+ }
791
+ if (tryScroll(current.child, offset, animated)) return true;
792
+ current = walkSiblings ? current.sibling : null;
793
+ }
794
+ return false;
795
+ }
796
+
797
+ function targetMatches(
798
+ fiber: FiberNode,
799
+ options: { testId?: string; textContains?: string },
800
+ ): boolean {
801
+ if (options.testId && fiber.memoizedProps?.testID !== options.testId) {
802
+ return false;
803
+ }
804
+ if (options.textContains) {
805
+ const needle = options.textContains.toLowerCase();
806
+ const texts = options.testId
807
+ ? collectFiberTexts(fiber)
808
+ : collectOwnFiberTexts(fiber);
809
+ return texts.some((text) => text.toLowerCase().includes(needle));
810
+ }
811
+ return Boolean(options.testId);
812
+ }
813
+
814
+ function findUiTargetFiber(
815
+ rootFiber: FiberNode,
816
+ options: { testId?: string; textContains?: string },
817
+ ): FiberNode | null {
818
+ let result: FiberNode | null = null;
819
+ walkFiber(rootFiber, (fiber) => {
820
+ if (targetMatches(fiber, options)) {
821
+ result = fiber;
822
+ return true;
823
+ }
824
+ return false;
825
+ });
826
+ return result;
827
+ }
828
+
829
+ function findMeasurableStateNode(
830
+ fiber: FiberNode | null,
831
+ ): FiberNode['stateNode'] | null {
832
+ let result: FiberNode['stateNode'] | null = null;
833
+ walkFiber(fiber, (node) => {
834
+ const sn = node.stateNode;
835
+ if (
836
+ sn &&
837
+ (typeof sn.measureInWindow === 'function' ||
838
+ typeof sn.measure === 'function')
839
+ ) {
840
+ result = sn;
841
+ return true;
842
+ }
843
+ return false;
844
+ });
845
+ return result;
846
+ }
847
+
848
+ function measureStateNode(
849
+ stateNode: FiberNode['stateNode'],
850
+ ): Promise<{ x: number; y: number; width: number; height: number } | null> {
851
+ return new Promise((resolve) => {
852
+ if (!stateNode) {
853
+ resolve(null);
854
+ return;
855
+ }
856
+ let settled = false;
857
+ const settle = (
858
+ value: { x: number; y: number; width: number; height: number } | null,
859
+ ) => {
860
+ if (settled) return;
861
+ settled = true;
862
+ resolve(value);
863
+ };
864
+ // Native measure callbacks never fire for detached/off-screen views;
865
+ // fall back to null after a short delay so callers don't hang forever.
866
+ const timeout = setTimeout(() => settle(null), 2500);
867
+ const finish = (x: number, y: number, width: number, height: number) => {
868
+ clearTimeout(timeout);
869
+ settle({ x, y, width, height });
870
+ };
871
+ try {
872
+ if (typeof stateNode.measureInWindow === 'function') {
873
+ stateNode.measureInWindow(finish);
874
+ return;
875
+ }
876
+ if (typeof stateNode.measure === 'function') {
877
+ stateNode.measure((_x, _y, width, height, pageX, pageY) =>
878
+ finish(pageX, pageY, width, height),
879
+ );
880
+ return;
881
+ }
882
+ } catch (e) {
883
+ Logger.log(String(e), 'AgenticService.measureStateNode');
884
+ }
885
+ clearTimeout(timeout);
886
+ settle(null);
887
+ });
888
+ }
889
+
890
+ async function queryUiTarget(options: {
891
+ testId?: string;
892
+ textContains?: string;
893
+ visibility?: 'tree' | 'viewport';
894
+ }): Promise<{
895
+ present: boolean;
896
+ visible: boolean;
897
+ visibility: 'tree' | 'viewport';
898
+ testId?: string;
899
+ textContains?: string;
900
+ textMatched?: boolean;
901
+ rect?: { x: number; y: number; width: number; height: number };
902
+ viewport?: { width: number; height: number };
903
+ error?: string;
904
+ }> {
905
+ const visibility: 'tree' | 'viewport' =
906
+ options.visibility === 'viewport' ? 'viewport' : 'tree';
907
+ let target: FiberNode | null = null;
908
+
909
+ walkFiberRoots((rootFiber) => {
910
+ target = findUiTargetFiber(rootFiber, options);
911
+ return Boolean(target);
912
+ });
913
+
914
+ const base = {
915
+ present: Boolean(target),
916
+ visible: Boolean(target) && visibility === 'tree',
917
+ visibility,
918
+ testId: options.testId,
919
+ textContains: options.textContains,
920
+ textMatched: options.textContains
921
+ ? Boolean(
922
+ target &&
923
+ collectFiberTexts(target).some((text) =>
924
+ text
925
+ .toLowerCase()
926
+ .includes(String(options.textContains).toLowerCase()),
927
+ ),
928
+ )
929
+ : undefined,
930
+ };
931
+
932
+ if (!target || visibility === 'tree') {
933
+ return base;
934
+ }
935
+
936
+ const stateNode = findMeasurableStateNode(target);
937
+ if (!stateNode) {
938
+ return {
939
+ ...base,
940
+ visible: false,
941
+ error:
942
+ 'Target exists in fiber tree but no measurable native node was found',
943
+ };
944
+ }
945
+
946
+ const rect = await measureStateNode(stateNode);
947
+ const viewport = Dimensions.get('window');
948
+ if (!rect) {
949
+ return {
950
+ ...base,
951
+ visible: false,
952
+ viewport: { width: viewport.width, height: viewport.height },
953
+ error: 'Target exists in fiber tree but measurement returned no frame',
954
+ };
955
+ }
956
+
957
+ const visible =
958
+ rect.width > 0 &&
959
+ rect.height > 0 &&
960
+ rect.x < viewport.width &&
961
+ rect.y < viewport.height &&
962
+ rect.x + rect.width > 0 &&
963
+ rect.y + rect.height > 0;
964
+
965
+ return {
966
+ ...base,
967
+ visible,
968
+ rect,
969
+ viewport: { width: viewport.width, height: viewport.height },
970
+ };
971
+ }
972
+
973
+ function appendTextContent(value: unknown, out: string[]) {
974
+ if (value === null || value === undefined) {
975
+ return;
976
+ }
977
+ if (typeof value === 'string' || typeof value === 'number') {
978
+ const text = String(value).trim();
979
+ if (text.length > 0) {
980
+ out.push(text);
981
+ }
982
+ return;
983
+ }
984
+ if (Array.isArray(value)) {
985
+ value.forEach((entry) => appendTextContent(entry, out));
986
+ return;
987
+ }
988
+ if (typeof value === 'object' && value && 'props' in value) {
989
+ const maybeProps = (value as { props?: { children?: unknown } }).props;
990
+ if (maybeProps?.children !== undefined) {
991
+ appendTextContent(maybeProps.children, out);
992
+ }
993
+ }
994
+ }
995
+
996
+ function dedupeTexts(texts: string[]): string[] {
997
+ return texts.filter((text, index) => texts.indexOf(text) === index);
998
+ }
999
+
1000
+ function collectFiberTexts(fiber: FiberNode | null): string[] {
1001
+ const texts: string[] = [];
1002
+ walkFiber(fiber, (node) => {
1003
+ if (node.memoizedProps?.children !== undefined) {
1004
+ appendTextContent(node.memoizedProps.children, texts);
1005
+ }
1006
+ return false;
1007
+ });
1008
+ return dedupeTexts(texts);
1009
+ }
1010
+
1011
+ function collectOwnFiberTexts(fiber: FiberNode | null): string[] {
1012
+ const texts: string[] = [];
1013
+ if (fiber?.memoizedProps?.children !== undefined) {
1014
+ appendTextContent(fiber.memoizedProps.children, texts);
1015
+ }
1016
+ return dedupeTexts(texts);
1017
+ }
1018
+
1019
+ function findAncestorTexts(
1020
+ fiber: FiberNode | null,
1021
+ predicate: (texts: string[]) => boolean,
1022
+ maxTexts = 14,
1023
+ ): string[] | null {
1024
+ let current = fiber;
1025
+ while (current) {
1026
+ const texts = collectFiberTexts(current);
1027
+ if (texts.length > 0 && texts.length <= maxTexts && predicate(texts)) {
1028
+ return texts;
1029
+ }
1030
+ current = current.return;
1031
+ }
1032
+ return null;
1033
+ }
1034
+
1035
+ function findRowTexts(
1036
+ label: string,
1037
+ options: {
1038
+ anchorTestId?: string;
1039
+ requiredLabels?: string[];
1040
+ maxTexts?: number;
1041
+ } = {},
1042
+ ): string[] | null {
1043
+ const { anchorTestId, requiredLabels = [], maxTexts = 14 } = options;
1044
+ const matchesRow = (texts: string[]) =>
1045
+ texts.includes(label) &&
1046
+ requiredLabels.every((requiredLabel) => texts.includes(requiredLabel));
1047
+
1048
+ if (anchorTestId) {
1049
+ let anchoredMatch: string[] | null = null;
1050
+ walkFiberRoots((rootFiber) => {
1051
+ const anchor = findFiberByTestId(rootFiber, anchorTestId);
1052
+ if (!anchor) {
1053
+ return false;
1054
+ }
1055
+ anchoredMatch = findAncestorTexts(anchor, matchesRow, maxTexts);
1056
+ return Boolean(anchoredMatch);
1057
+ });
1058
+ if (anchoredMatch) {
1059
+ return anchoredMatch;
1060
+ }
1061
+ }
1062
+
1063
+ let fallbackMatch: string[] | null = null;
1064
+ walkFiberRoots((rootFiber) =>
1065
+ walkFiber(rootFiber, (fiber) => {
1066
+ const texts = collectFiberTexts(fiber);
1067
+ if (texts.length === 0 || texts.length > maxTexts) {
1068
+ return false;
1069
+ }
1070
+ if (!matchesRow(texts)) {
1071
+ return false;
1072
+ }
1073
+ fallbackMatch = texts;
1074
+ return true;
1075
+ }),
1076
+ );
1077
+
1078
+ return fallbackMatch;
1079
+ }
1080
+
1081
+ function getRowValue(
1082
+ label: string,
1083
+ pattern: string,
1084
+ options: {
1085
+ anchorTestId?: string;
1086
+ requiredLabels?: string[];
1087
+ maxTexts?: number;
1088
+ } = {},
1089
+ ): string | null {
1090
+ const rowTexts = findRowTexts(label, options);
1091
+ if (!rowTexts) {
1092
+ return null;
1093
+ }
1094
+ const matcher = new RegExp(pattern);
1095
+ return rowTexts.find((text) => text !== label && matcher.test(text)) ?? null;
1096
+ }
1097
+
1098
+ // ─── AgenticService ─────────────────────────────────────────────────────────
1099
+
1100
+ /**
1101
+ * __DEV__-only service that installs the `globalThis.__AGENTIC__` bridge.
1102
+ *
1103
+ * The bridge exposes navigation primitives, account helpers, and UI
1104
+ * interaction methods (pressTestId, scrollView) so that AI coding agents
1105
+ * can inspect and drive the app remotely via Metro's Hermes CDP WebSocket.
1106
+ *
1107
+ * Consumed by the runner-owned Mobile bridge runtime.
1108
+ * See docs/perps/perps-agentic-feedback-loop.md for the full workflow.
1109
+ */
1110
+ const AgenticService = {
1111
+ /**
1112
+ * Install the agentic bridge on globalThis.
1113
+ *
1114
+ * @param navRef - Raw (unwrapped) navigation container ref
1115
+ * @param deferredNav - The requestAnimationFrame-deferred proxy
1116
+ */
1117
+ install(
1118
+ navRef: NavigationContainerRef<ParamListBase>,
1119
+ deferredNav: NavigationContainerRef<ParamListBase>,
1120
+ ) {
1121
+ // Defensive guard against accidental production imports/calls; the bridge
1122
+ // must never assign globals outside development builds.
1123
+ if (!__DEV__) return;
1124
+
1125
+ Logger.log('[AgenticService] __AGENTIC__ bridge installed');
1126
+
1127
+ globalThis.__AGENTIC__ = {
1128
+ platform: Platform.OS,
1129
+ replayHarnessPatch: 'legacy-wallet-fixture-r2',
1130
+ navigate: (name: string, params?: object) =>
1131
+ (
1132
+ deferredNav as unknown as {
1133
+ navigate: (name: string, params?: object) => void;
1134
+ }
1135
+ ).navigate(name, params),
1136
+ getRoute: () => navRef.getCurrentRoute(),
1137
+ getState: () => navRef.getState(),
1138
+ canGoBack: () => navRef.canGoBack(),
1139
+ goBack: () => deferredNav.goBack(),
1140
+ listAccounts: () =>
1141
+ Engine.context.AccountsController.listAccounts().map(toAccountSummary),
1142
+ getSelectedAccount: () =>
1143
+ toAccountSummary(
1144
+ Engine.context.AccountsController.getSelectedAccount(),
1145
+ ),
1146
+ pressTestId: (testId: string) => {
1147
+ try {
1148
+ const found = walkFiberRoots((rootFiber) =>
1149
+ walkFiber(rootFiber, (f) => {
1150
+ if (
1151
+ f.memoizedProps?.testID === testId &&
1152
+ typeof f.memoizedProps?.onPress === 'function'
1153
+ ) {
1154
+ f.memoizedProps.onPress();
1155
+ return true;
1156
+ }
1157
+ return false;
1158
+ }),
1159
+ );
1160
+ if (found) return { ok: true, testId };
1161
+ return {
1162
+ ok: false,
1163
+ error: `No component with testID="${testId}" found or no onPress prop`,
1164
+ };
1165
+ } catch (e) {
1166
+ return { ok: false, error: String(e) };
1167
+ }
1168
+ },
1169
+ pressText: (
1170
+ text: string,
1171
+ options: { requiredTexts?: string[]; maxTexts?: number } = {},
1172
+ ) => {
1173
+ const { requiredTexts = [], maxTexts = 14 } = options;
1174
+ try {
1175
+ let bestMatch: FiberNode | null = null;
1176
+ let bestTextCount = Number.POSITIVE_INFINITY;
1177
+ walkFiberRoots((rootFiber) =>
1178
+ walkFiber(rootFiber, (fiber) => {
1179
+ if (typeof fiber.memoizedProps?.onPress !== 'function') {
1180
+ return false;
1181
+ }
1182
+ const texts = collectFiberTexts(fiber);
1183
+ if (texts.length === 0 || texts.length > maxTexts) {
1184
+ return false;
1185
+ }
1186
+ if (!texts.includes(text)) {
1187
+ return false;
1188
+ }
1189
+ if (requiredTexts.some((required) => !texts.includes(required))) {
1190
+ return false;
1191
+ }
1192
+ if (texts.length < bestTextCount) {
1193
+ bestMatch = fiber;
1194
+ bestTextCount = texts.length;
1195
+ }
1196
+ return false;
1197
+ }),
1198
+ );
1199
+ const matched = bestMatch as FiberNode | null;
1200
+ if (matched && typeof matched.memoizedProps?.onPress === 'function') {
1201
+ matched.memoizedProps.onPress();
1202
+ return { ok: true, text };
1203
+ }
1204
+ return {
1205
+ ok: false,
1206
+ error: `No pressable found for text="${text}"`,
1207
+ };
1208
+ } catch (e) {
1209
+ return { ok: false, error: String(e) };
1210
+ }
1211
+ },
1212
+ scrollView: (
1213
+ options: {
1214
+ testId?: string;
1215
+ offset?: number;
1216
+ animated?: boolean;
1217
+ } = {},
1218
+ ) => {
1219
+ const {
1220
+ testId: scrollTestId,
1221
+ offset = 300,
1222
+ animated = false,
1223
+ } = options;
1224
+ try {
1225
+ const found = walkFiberRoots((rootFiber) => {
1226
+ if (scrollTestId) {
1227
+ const anchor = findFiberByTestId(rootFiber, scrollTestId);
1228
+ if (!anchor) return false;
1229
+ return tryScroll(anchor, offset, animated, false);
1230
+ }
1231
+ return tryScroll(rootFiber, offset, animated);
1232
+ });
1233
+ if (found)
1234
+ return { ok: true, testId: scrollTestId, offset, animated };
1235
+ return {
1236
+ ok: false,
1237
+ error: scrollTestId
1238
+ ? `No scrollable found near testID="${scrollTestId}"`
1239
+ : 'No scrollable found in fiber tree',
1240
+ };
1241
+ } catch (e) {
1242
+ return { ok: false, error: String(e) };
1243
+ }
1244
+ },
1245
+ setInput: (testId: string, value: string) => {
1246
+ try {
1247
+ const result: {
1248
+ ok: boolean;
1249
+ testId?: string;
1250
+ value?: string;
1251
+ error?: string;
1252
+ } = {
1253
+ ok: false,
1254
+ error: `No component with testID="${testId}" found`,
1255
+ };
1256
+ walkFiberRoots((rootFiber) => {
1257
+ const target = findFiberByTestId(rootFiber, testId);
1258
+ if (!target) return false;
1259
+ // Walk the found fiber and its parents looking for onChangeText
1260
+ let current: FiberNode | null = target;
1261
+ while (current) {
1262
+ if (typeof current.memoizedProps?.onChangeText === 'function') {
1263
+ current.memoizedProps.onChangeText(value);
1264
+ result.ok = true;
1265
+ result.testId = testId;
1266
+ result.value = value;
1267
+ result.error = undefined;
1268
+ return true;
1269
+ }
1270
+ current = current.return;
1271
+ }
1272
+ result.error = `Component with testID="${testId}" has no onChangeText prop`;
1273
+ return true;
1274
+ });
1275
+ return result;
1276
+ } catch (e) {
1277
+ return { ok: false, error: String(e) };
1278
+ }
1279
+ },
1280
+ getTextByTestId: (testId: string, options: { all?: boolean } = {}) => {
1281
+ let texts: string[] | null = null;
1282
+ walkFiberRoots((rootFiber) => {
1283
+ const target = findFiberByTestId(rootFiber, testId);
1284
+ if (!target) {
1285
+ return false;
1286
+ }
1287
+ texts = collectFiberTexts(target);
1288
+ return true;
1289
+ });
1290
+ const collected = texts as string[] | null;
1291
+ if (!collected || collected.length === 0) {
1292
+ return null;
1293
+ }
1294
+ return options.all ? collected : collected[0];
1295
+ },
1296
+ getAncestorTextsByTestId: (
1297
+ testId: string,
1298
+ options: { requiredLabels?: string[]; maxTexts?: number } = {},
1299
+ ) => {
1300
+ const { requiredLabels = [], maxTexts = 14 } = options;
1301
+ let texts: string[] | null = null;
1302
+ walkFiberRoots((rootFiber) => {
1303
+ const target = findFiberByTestId(rootFiber, testId);
1304
+ if (!target) {
1305
+ return false;
1306
+ }
1307
+ texts = findAncestorTexts(
1308
+ target,
1309
+ (candidateTexts) =>
1310
+ requiredLabels.every((label) => candidateTexts.includes(label)),
1311
+ maxTexts,
1312
+ );
1313
+ return Boolean(texts);
1314
+ });
1315
+ return texts;
1316
+ },
1317
+ getRowValue: (
1318
+ label: string,
1319
+ pattern: string,
1320
+ options: {
1321
+ anchorTestId?: string;
1322
+ requiredLabels?: string[];
1323
+ maxTexts?: number;
1324
+ } = {},
1325
+ ) => getRowValue(label, pattern, options),
1326
+ switchAccount: (address: string) => {
1327
+ const accounts = Engine.context.AccountsController.listAccounts();
1328
+ const target = accounts.find(
1329
+ (a: { address: string }) =>
1330
+ a.address.toLowerCase() === address.toLowerCase(),
1331
+ );
1332
+ if (!target) {
1333
+ throw new Error(`No account found for address ${address}`);
1334
+ }
1335
+ Engine.setSelectedAddress(target.address);
1336
+ return { switched: true, ...toAccountSummary(target) };
1337
+ },
1338
+ showStep: (step: AgenticHudStep) => {
1339
+ emitStepHud(step);
1340
+ },
1341
+ hideStep: () => {
1342
+ emitStepHud(null);
1343
+ },
1344
+ refreshPerpsStreams: async () => {
1345
+ await PerpsConnectionManager.ensureConnected({
1346
+ source: 'agentic_refresh_perps_streams',
1347
+ suppressError: true,
1348
+ });
1349
+ const streamManager = getStreamManagerInstance();
1350
+ streamManager.clearAllChannels();
1351
+ const positions = await Engine.context.PerpsController.getPositions();
1352
+ return {
1353
+ ok: true,
1354
+ positions: Array.isArray(positions) ? positions.length : 0,
1355
+ };
1356
+ },
1357
+ findFiberByTestId: (testId: string): boolean => {
1358
+ let found = false;
1359
+ walkFiberRoots((rootFiber) => {
1360
+ if (findFiberByTestId(rootFiber, testId)) {
1361
+ found = true;
1362
+ return true;
1363
+ }
1364
+ return false;
1365
+ });
1366
+ return found;
1367
+ },
1368
+ queryUiTarget,
1369
+ applyWalletFixture: async (fixture) => {
1370
+ let setupStep = 'start';
1371
+ try {
1372
+ const {
1373
+ MultichainAccountService,
1374
+ KeyringController,
1375
+ AccountsController,
1376
+ AccountTreeController,
1377
+ } = Engine.context;
1378
+ // The backup subscriber reads the Engine class static, so set it on
1379
+ // the class (not the facade) before importing/renaming accounts to
1380
+ // avoid racing native keychain export during fixture apply.
1381
+ EngineClass.disableAutomaticVaultBackup = true;
1382
+ // Authentication.unlockWallet can navigate to the metrics opt-in
1383
+ // screen when the seen flag is absent. Persist the fixture preference
1384
+ // before unlock so existing-vault fixture replay stays on the wallet
1385
+ // setup path instead of being interrupted by onboarding UI.
1386
+ setupStep = 'persist-metrics-setting';
1387
+ await markFixtureMetricsOptInSeenIfNeeded(fixture);
1388
+
1389
+ // Unlock via the real auth flow (loginVaultCreation + dispatchLogin +
1390
+ // post-login) rather than a bare KeyringController.submitPassword, so
1391
+ // multichain services and Redux/auth state are consistent before we
1392
+ // mutate accounts.
1393
+ setupStep = 'unlock-fixture-vault';
1394
+ await unlockFixtureVault(fixture, KeyringController);
1395
+ // Existing replay vaults can have the same historical multichain
1396
+ // account-tree init gap as fresh legacy setup. Only the known legacy
1397
+ // init errors are tolerated by this option; unexpected errors still
1398
+ // throw from initializeFixtureAccountTree().
1399
+ const legacyAccountTreeInitOptions = {
1400
+ allowLegacyAccountTreeInitFailure: true,
1401
+ };
1402
+ await initializeFixtureAccountTree(legacyAccountTreeInitOptions);
1403
+ try {
1404
+ await withFixtureTimeout(
1405
+ 'initialize multichain account service',
1406
+ MultichainAccountService.init(),
1407
+ 60_000,
1408
+ );
1409
+ } catch (error) {
1410
+ if (!isExpectedLegacyAccountTreeInitError(error)) {
1411
+ throw error;
1412
+ }
1413
+ Logger.log(
1414
+ '[AgenticService] Skipping multichain account service initialization for historical replay fixture apply',
1415
+ );
1416
+ }
1417
+ await materializeFixtureAccounts(
1418
+ fixture,
1419
+ {
1420
+ KeyringController,
1421
+ AccountsController,
1422
+ AccountTreeController,
1423
+ },
1424
+ legacyAccountTreeInitOptions,
1425
+ );
1426
+ const ethAccs = findEvmAccounts(
1427
+ AccountsController.state.internalAccounts.accounts,
1428
+ ).map(toAccountSummary);
1429
+ return { ok: true, accounts: ethAccs };
1430
+ } catch (e) {
1431
+ return {
1432
+ ok: false,
1433
+ step: setupStep,
1434
+ error: String((e as Error).message || e),
1435
+ };
1436
+ }
1437
+ },
1438
+ setupWallet: async (fixture) => {
1439
+ let setupStep = 'start';
1440
+ try {
1441
+ setupStep = 'read-engine';
1442
+ const {
1443
+ MultichainAccountService,
1444
+ KeyringController,
1445
+ AccountsController,
1446
+ AccountTreeController,
1447
+ } = Engine.context;
1448
+ const store = ReduxService.store;
1449
+ const settings = fixture.settings ?? {};
1450
+ // Deliberately one-way for the dev harness process: fixture setup
1451
+ // rewrites vault/account state, so automatic backup must stay disabled
1452
+ // for the rest of this simulator session to avoid native keychain
1453
+ // export paths racing the synthetic setup flow.
1454
+ EngineClass.disableAutomaticVaultBackup = true;
1455
+
1456
+ // 1. Create wallet from the first mnemonic (same path as onboarding UI)
1457
+ setupStep = 'create-wallet';
1458
+ const mnemonicAccount = fixture.accounts.find(
1459
+ (a) => a.type === 'mnemonic',
1460
+ );
1461
+ let usedLegacyVaultSetup = false;
1462
+ if (mnemonicAccount) {
1463
+ const mnemonic = mnemonicPhraseToBytes(mnemonicAccount.value);
1464
+ try {
1465
+ logFixtureStep('create-wallet-restore');
1466
+ await withFixtureTimeout(
1467
+ 'create multichain wallet from fixture mnemonic',
1468
+ MultichainAccountService.createMultichainAccountWallet({
1469
+ type: 'restore',
1470
+ password: fixture.password,
1471
+ mnemonic,
1472
+ }),
1473
+ 60_000,
1474
+ );
1475
+ } catch (error) {
1476
+ if (!isExpectedLegacyAccountTreeInitError(error)) {
1477
+ throw error;
1478
+ }
1479
+ Logger.log(
1480
+ '[AgenticService] Falling back to legacy vault restore for historical replay fixture setup',
1481
+ );
1482
+ await KeyringController.createNewVaultAndRestore(
1483
+ fixture.password,
1484
+ mnemonic,
1485
+ );
1486
+ usedLegacyVaultSetup = true;
1487
+ }
1488
+ } else {
1489
+ try {
1490
+ logFixtureStep('create-wallet-empty');
1491
+ await withFixtureTimeout(
1492
+ 'create multichain wallet',
1493
+ MultichainAccountService.createMultichainAccountWallet({
1494
+ type: 'create',
1495
+ password: fixture.password,
1496
+ }),
1497
+ 60_000,
1498
+ );
1499
+ } catch (error) {
1500
+ if (!isExpectedLegacyAccountTreeInitError(error)) {
1501
+ throw error;
1502
+ }
1503
+ Logger.log(
1504
+ '[AgenticService] Falling back to legacy vault creation for historical replay fixture setup',
1505
+ );
1506
+ await KeyringController.createNewVaultAndKeychain(
1507
+ fixture.password,
1508
+ );
1509
+ usedLegacyVaultSetup = true;
1510
+ }
1511
+ }
1512
+
1513
+ // 2. Initialize services (same as Authentication.dispatchLogin)
1514
+ setupStep = 'initialize-services';
1515
+ if (!usedLegacyVaultSetup) {
1516
+ try {
1517
+ await AccountTreeInitService.initializeAccountTree();
1518
+ await MultichainAccountService.init();
1519
+ } catch (error) {
1520
+ if (!isExpectedLegacyAccountTreeInitError(error)) {
1521
+ throw error;
1522
+ }
1523
+ Logger.log(
1524
+ '[AgenticService] Skipping multichain account-tree initialization for historical replay fixture setup',
1525
+ );
1526
+ }
1527
+ }
1528
+
1529
+ // 3. Materialize requested fixture accounts. A mnemonic account can
1530
+ // declare count/numberOfAccounts plus names[]; this is generic
1531
+ // fixture semantics, not a dev-account special case.
1532
+ setupStep = 'materialize-srp-accounts';
1533
+ await materializeFixtureAccounts(
1534
+ fixture,
1535
+ {
1536
+ KeyringController,
1537
+ AccountsController,
1538
+ AccountTreeController,
1539
+ },
1540
+ { allowLegacyAccountTreeInitFailure: usedLegacyVaultSetup },
1541
+ );
1542
+
1543
+ // 4. Dispatch all onboarding/auth flags
1544
+ setupStep = 'dispatch-onboarding-flags';
1545
+ store.dispatch(passwordSet());
1546
+ store.dispatch(seedphraseBackedUp());
1547
+ store.dispatch(setCompletedOnboarding(true));
1548
+ store.dispatch(setExistingUser(true));
1549
+ store.dispatch(logIn());
1550
+
1551
+ // 5. Suppress post-onboarding modals if explicitly requested
1552
+ setupStep = 'persist-modal-settings';
1553
+ if (settings.skipGtmModals === true) {
1554
+ await Promise.all([
1555
+ StorageWrapper.setItem(PERPS_GTM_MODAL_SHOWN, 'true'),
1556
+ StorageWrapper.setItem(PREDICT_GTM_MODAL_SHOWN, 'true'),
1557
+ StorageWrapper.setItem(REWARDS_GTM_MODAL_SHOWN, 'true'),
1558
+ ]);
1559
+ // Suppress ExperienceEnhancer (marketing consent) modal
1560
+ store.dispatch(setDataCollectionForMarketing(false));
1561
+ }
1562
+
1563
+ // 5b. Set metrics UI as seen (prevents Authentication.unlockWallet
1564
+ // from navigating to OptinMetrics after setupWallet resets to Wallet)
1565
+ setupStep = 'persist-metrics-setting';
1566
+ await markFixtureMetricsOptInSeenIfNeeded(fixture);
1567
+
1568
+ // 5c. Mark multichain accounts intro modal as seen
1569
+ setupStep = 'dispatch-multichain-intro-seen';
1570
+ store.dispatch(setMultichainAccountsIntroModalSeen(true));
1571
+
1572
+ // 6. Skip perps tutorial onboarding if requested
1573
+ setupStep = 'persist-perps-tutorial-setting';
1574
+ if (settings.skipPerpsTutorial === true) {
1575
+ Engine.context.PerpsController?.markTutorialCompleted();
1576
+ }
1577
+
1578
+ // 7. Set auto-lock to "Never" (-1) for agentic workflows
1579
+ setupStep = 'dispatch-auto-lock';
1580
+ if (settings.autoLockNever === true) {
1581
+ ReduxService.store.dispatch(setLockTime(-1));
1582
+ }
1583
+
1584
+ // 8. Enable device authentication only on Android fixture runs.
1585
+ // On iOS simulator, toggling OS auth during synthetic setup can drive
1586
+ // react-native-keychain/quick-crypto secret export on the JS runtime
1587
+ // and crash the app after setupWallet returns. Android is the only
1588
+ // harness path that stores the fixture password for auto-unlock here.
1589
+ setupStep = 'dispatch-device-auth';
1590
+ if (
1591
+ settings.deviceAuthEnabled === true &&
1592
+ Platform.OS === 'android'
1593
+ ) {
1594
+ ReduxService.store.dispatch(setOsAuthEnabled(true));
1595
+ }
1596
+
1597
+ // 8b. Store password in SecureKeychain for device-auth auto-unlock on reload (Android only)
1598
+ if (
1599
+ settings.deviceAuthEnabled === true &&
1600
+ Platform.OS === 'android'
1601
+ ) {
1602
+ DevLogger.log('[AUTO-UNLOCK] Storing password in SecureKeychain', {
1603
+ authType: AUTHENTICATION_TYPE.DEVICE_AUTHENTICATION,
1604
+ });
1605
+ await SecureKeychain.setGenericPassword(
1606
+ fixture.password,
1607
+ AUTHENTICATION_TYPE.DEVICE_AUTHENTICATION,
1608
+ );
1609
+ DevLogger.log('[AUTO-UNLOCK] SecureKeychain password stored');
1610
+ }
1611
+
1612
+ // 9. Configure MetaMetrics if specified
1613
+ setupStep = 'configure-metametrics';
1614
+ if (settings.metametrics === false) {
1615
+ await analytics.optOut();
1616
+ } else if (settings.metametrics === true) {
1617
+ await analytics.optIn();
1618
+ }
1619
+
1620
+ // 10. Navigate to wallet (same as Authentication.unlockWallet)
1621
+ setupStep = 'navigate-wallet';
1622
+ resetNavigationToWalletHome(navRef);
1623
+
1624
+ // 11. Collect all ETH accounts for the summary
1625
+ setupStep = 'collect-accounts';
1626
+ const ethAccs = findEvmAccounts(
1627
+ AccountsController.state.internalAccounts.accounts,
1628
+ ).map(toAccountSummary);
1629
+
1630
+ return { ok: true, accounts: ethAccs };
1631
+ } catch (e) {
1632
+ return {
1633
+ ok: false,
1634
+ step: setupStep,
1635
+ error: String((e as Error).message || e),
1636
+ };
1637
+ }
1638
+ },
1639
+ };
1640
+
1641
+ try {
1642
+ (globalThis as { store?: unknown }).store = ReduxService.store;
1643
+ (globalThis as { persistor?: unknown }).persistor = persistor;
1644
+ } catch (error) {
1645
+ Logger.log(
1646
+ String((error as Error).message || error),
1647
+ 'AgenticService.install.exposeReduxGlobals',
1648
+ );
1649
+ }
1650
+ (globalThis as { Engine?: unknown }).Engine = Engine;
1651
+ },
1652
+ };
1653
+
1654
+ export default AgenticService;
1655
+ export {
1656
+ walkFiber,
1657
+ findFiberByTestId,
1658
+ walkFiberRoots,
1659
+ tryScroll,
1660
+ toAccountSummary,
1661
+ };
1662
+ export type { FiberNode, FiberRoot, ReactDevToolsHook, AgenticBridge };