ethagent 1.1.1 → 2.0.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 (271) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +127 -29
  3. package/package.json +16 -9
  4. package/src/app/FirstRun.tsx +192 -146
  5. package/src/app/FirstRunTimeline.tsx +47 -0
  6. package/src/app/input/AppInputProvider.tsx +1 -1
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -1
  8. package/src/chat/ChatBottomPane.tsx +0 -1
  9. package/src/chat/ChatInput.tsx +6 -6
  10. package/src/chat/ChatScreen.tsx +43 -18
  11. package/src/chat/ContextLimitView.tsx +4 -4
  12. package/src/chat/ContinuityEditReviewView.tsx +11 -17
  13. package/src/chat/ConversationStack.tsx +3 -0
  14. package/src/chat/CopyPicker.tsx +0 -1
  15. package/src/chat/MessageList.tsx +62 -45
  16. package/src/chat/PermissionPrompt.tsx +13 -9
  17. package/src/chat/PlanApprovalView.tsx +3 -3
  18. package/src/chat/ResumeView.tsx +1 -4
  19. package/src/chat/RewindView.tsx +2 -2
  20. package/src/chat/TranscriptView.tsx +6 -0
  21. package/src/chat/chatInputState.ts +1 -1
  22. package/src/chat/chatScreenUtils.ts +22 -11
  23. package/src/chat/chatSessionState.ts +2 -2
  24. package/src/chat/chatTurnOrchestrator.ts +16 -81
  25. package/src/chat/commands.ts +1 -1
  26. package/src/chat/textCursor.ts +1 -1
  27. package/src/chat/transcriptViewport.ts +2 -7
  28. package/src/cli/ResetConfirmView.tsx +1 -1
  29. package/src/cli/main.tsx +9 -3
  30. package/src/cli/preview.tsx +0 -5
  31. package/src/cli/updateNotice.ts +5 -3
  32. package/src/identity/continuity/editor.ts +7 -107
  33. package/src/identity/continuity/envelope.ts +1048 -40
  34. package/src/identity/continuity/history.ts +4 -4
  35. package/src/identity/continuity/localBackup.ts +249 -0
  36. package/src/identity/continuity/privateEdit/apply.ts +170 -0
  37. package/src/identity/continuity/privateEdit/diff.ts +82 -0
  38. package/src/identity/continuity/privateEdit/files.ts +23 -0
  39. package/src/identity/continuity/privateEdit/types.ts +28 -0
  40. package/src/identity/continuity/privateEdit.ts +10 -298
  41. package/src/identity/continuity/publicSkills.ts +8 -9
  42. package/src/identity/continuity/snapshots.ts +17 -6
  43. package/src/identity/continuity/storage/defaults.ts +111 -0
  44. package/src/identity/continuity/storage/files.ts +72 -0
  45. package/src/identity/continuity/storage/markdown.ts +81 -0
  46. package/src/identity/continuity/storage/paths.ts +24 -0
  47. package/src/identity/continuity/storage/scaffold.ts +124 -0
  48. package/src/identity/continuity/storage/status.ts +86 -0
  49. package/src/identity/continuity/storage/types.ts +27 -0
  50. package/src/identity/continuity/storage.ts +32 -507
  51. package/src/identity/continuity/zipWriter.ts +95 -0
  52. package/src/identity/crypto/backupEnvelope.ts +14 -247
  53. package/src/identity/crypto/eth.ts +7 -7
  54. package/src/identity/ens/agentRecords.ts +96 -0
  55. package/src/identity/ens/ensAutomation/contracts.ts +38 -0
  56. package/src/identity/ens/ensAutomation/delete.ts +80 -0
  57. package/src/identity/ens/ensAutomation/names.ts +14 -0
  58. package/src/identity/ens/ensAutomation/operators.ts +29 -0
  59. package/src/identity/ens/ensAutomation/read.ts +114 -0
  60. package/src/identity/ens/ensAutomation/root.ts +63 -0
  61. package/src/identity/ens/ensAutomation/setup.ts +284 -0
  62. package/src/identity/ens/ensAutomation/transactions.ts +107 -0
  63. package/src/identity/ens/ensAutomation/types.ts +126 -0
  64. package/src/identity/ens/ensAutomation.ts +29 -0
  65. package/src/identity/ens/ensLookup/client.ts +43 -0
  66. package/src/identity/ens/ensLookup/constants.ts +26 -0
  67. package/src/identity/ens/ensLookup/discovery.ts +70 -0
  68. package/src/identity/ens/ensLookup/names.ts +34 -0
  69. package/src/identity/ens/ensLookup/records.ts +45 -0
  70. package/src/identity/ens/ensLookup/resolve.ts +75 -0
  71. package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
  72. package/src/identity/ens/ensLookup/types.ts +38 -0
  73. package/src/identity/ens/ensLookup/validation.ts +72 -0
  74. package/src/identity/ens/ensLookup.ts +19 -0
  75. package/src/identity/ens/ensRegistration.ts +199 -0
  76. package/src/identity/ens/resolverDelegation.ts +48 -0
  77. package/src/identity/hub/IdentityHub.tsx +13 -815
  78. package/src/identity/hub/OperationalRoutes.tsx +370 -0
  79. package/src/identity/hub/Routes.tsx +361 -0
  80. package/src/identity/hub/advancedEnsValidation.ts +45 -0
  81. package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
  82. package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
  83. package/src/identity/hub/components/FlowTimeline.tsx +27 -0
  84. package/src/identity/hub/components/IdentitySummary.tsx +190 -0
  85. package/src/identity/hub/components/MenuScreen.tsx +237 -0
  86. package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
  87. package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
  88. package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
  89. package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
  90. package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
  91. package/src/identity/hub/effects/create.ts +310 -0
  92. package/src/identity/hub/effects/ens/flows.ts +218 -0
  93. package/src/identity/hub/effects/ens/index.ts +11 -0
  94. package/src/identity/hub/effects/ens/transactions.ts +239 -0
  95. package/src/identity/hub/effects/index.ts +74 -0
  96. package/src/identity/hub/effects/profile/profileState.ts +173 -0
  97. package/src/identity/hub/effects/publicProfile/index.ts +5 -0
  98. package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
  99. package/src/identity/hub/effects/rebackup/index.ts +7 -0
  100. package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
  101. package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
  102. package/src/identity/hub/effects/receipts.ts +46 -0
  103. package/src/identity/hub/effects/restore/apply.ts +112 -0
  104. package/src/identity/hub/effects/restore/auth.ts +159 -0
  105. package/src/identity/hub/effects/restore/discover.ts +86 -0
  106. package/src/identity/hub/effects/restore/envelopes.ts +21 -0
  107. package/src/identity/hub/effects/restore/fetch.ts +25 -0
  108. package/src/identity/hub/effects/restore/index.ts +22 -0
  109. package/src/identity/hub/effects/restore/recovery.ts +135 -0
  110. package/src/identity/hub/effects/restore/resolve.ts +102 -0
  111. package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
  112. package/src/identity/hub/effects/restore/shared.ts +91 -0
  113. package/src/identity/hub/effects/restoreAdmin.ts +93 -0
  114. package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
  115. package/src/identity/hub/effects/shared/snapshot.ts +336 -0
  116. package/src/identity/hub/effects/shared/sync.ts +190 -0
  117. package/src/identity/hub/effects/token-transfer/index.ts +6 -0
  118. package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
  119. package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
  120. package/src/identity/hub/effects/types.ts +53 -0
  121. package/src/identity/hub/effects/vault/preflight.ts +50 -0
  122. package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
  123. package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
  124. package/src/identity/hub/flows/continuity/RecoveryConfirmScreen.tsx +104 -0
  125. package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
  126. package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
  127. package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
  128. package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
  129. package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
  130. package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
  131. package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
  132. package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
  133. package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
  134. package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
  135. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
  136. package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
  137. package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
  138. package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
  139. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
  140. package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
  141. package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
  142. package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
  143. package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
  144. package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
  145. package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
  146. package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
  147. package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
  148. package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
  149. package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +25 -43
  150. package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
  151. package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
  152. package/src/identity/hub/identityHubReducer.ts +166 -101
  153. package/src/identity/hub/model/continuity.ts +94 -0
  154. package/src/identity/hub/model/copy.ts +35 -0
  155. package/src/identity/hub/model/custody.ts +54 -0
  156. package/src/identity/hub/model/ens.ts +49 -0
  157. package/src/identity/hub/model/errors.ts +140 -0
  158. package/src/identity/hub/model/format.ts +15 -0
  159. package/src/identity/hub/model/identity.ts +94 -0
  160. package/src/identity/hub/model/network.ts +32 -0
  161. package/src/identity/hub/model/transfer.ts +57 -0
  162. package/src/identity/hub/operatorWallets.ts +131 -0
  163. package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
  164. package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
  165. package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
  166. package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
  167. package/src/identity/hub/reconciliation/index.ts +21 -0
  168. package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
  169. package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
  170. package/src/identity/hub/txGuard.ts +51 -0
  171. package/src/identity/hub/types.ts +17 -0
  172. package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
  173. package/src/identity/hub/useIdentityHubController.ts +396 -0
  174. package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
  175. package/src/identity/hub/utils.ts +79 -0
  176. package/src/identity/identityCompat.ts +34 -0
  177. package/src/identity/profile/agentIcon.ts +61 -0
  178. package/src/identity/profile/imagePicker.ts +12 -12
  179. package/src/identity/registry/erc8004/abi.ts +14 -0
  180. package/src/identity/registry/erc8004/chains.ts +150 -0
  181. package/src/identity/registry/erc8004/client.ts +11 -0
  182. package/src/identity/registry/erc8004/discovery.ts +511 -0
  183. package/src/identity/registry/erc8004/metadata.ts +335 -0
  184. package/src/identity/registry/erc8004/ownership.ts +121 -0
  185. package/src/identity/registry/erc8004/preflight.ts +123 -0
  186. package/src/identity/registry/erc8004/transactions.ts +77 -0
  187. package/src/identity/registry/erc8004/types.ts +88 -0
  188. package/src/identity/registry/erc8004/uri.ts +59 -0
  189. package/src/identity/registry/erc8004/utils.ts +58 -0
  190. package/src/identity/registry/erc8004.ts +53 -1106
  191. package/src/identity/registry/fieldParsers.ts +28 -0
  192. package/src/identity/registry/operatorVault/bytecode.ts +98 -0
  193. package/src/identity/registry/operatorVault/constants.ts +38 -0
  194. package/src/identity/registry/operatorVault/read.ts +246 -0
  195. package/src/identity/registry/operatorVault/transactions.ts +81 -0
  196. package/src/identity/registry/operatorVault.ts +44 -0
  197. package/src/identity/storage/ipfs.ts +26 -24
  198. package/src/identity/wallet/browserWallet/gas.ts +41 -0
  199. package/src/identity/wallet/browserWallet/html.ts +106 -0
  200. package/src/identity/wallet/browserWallet/http.ts +28 -0
  201. package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
  202. package/src/identity/wallet/browserWallet/requests.ts +191 -0
  203. package/src/identity/wallet/browserWallet/session.ts +325 -0
  204. package/src/identity/wallet/browserWallet/types.ts +192 -0
  205. package/src/identity/wallet/browserWallet/validation.ts +74 -0
  206. package/src/identity/wallet/browserWallet.ts +30 -393
  207. package/src/identity/wallet/page/constants.ts +5 -0
  208. package/src/identity/wallet/page/controller.ts +251 -0
  209. package/src/identity/wallet/page/copy.ts +340 -0
  210. package/src/identity/wallet/page/grainient.ts +278 -0
  211. package/src/identity/wallet/page/html.ts +28 -0
  212. package/src/identity/wallet/page/markup.ts +50 -0
  213. package/src/identity/wallet/page/state.ts +9 -0
  214. package/src/identity/wallet/page/styles/base.ts +259 -0
  215. package/src/identity/wallet/page/styles/components.ts +262 -0
  216. package/src/identity/wallet/page/styles/index.ts +5 -0
  217. package/src/identity/wallet/page/styles/responsive.ts +247 -0
  218. package/src/identity/wallet/page/types.ts +47 -0
  219. package/src/identity/wallet/page/view.ts +535 -0
  220. package/src/identity/wallet/page/walletProvider.ts +70 -0
  221. package/src/identity/wallet/page.tsx +38 -0
  222. package/src/identity/wallet/walletPurposeCompat.ts +27 -0
  223. package/src/mcp/manager.ts +0 -1
  224. package/src/models/ModelPicker.tsx +36 -30
  225. package/src/models/catalog.ts +5 -2
  226. package/src/models/huggingface.ts +9 -9
  227. package/src/models/llamacpp.ts +13 -13
  228. package/src/models/modelDisplay.ts +75 -0
  229. package/src/models/modelPickerOptions.ts +16 -3
  230. package/src/models/modelRecommendation.ts +0 -1
  231. package/src/providers/errors.ts +16 -0
  232. package/src/providers/gemini.ts +252 -39
  233. package/src/providers/registry.ts +2 -2
  234. package/src/providers/retry.ts +1 -1
  235. package/src/runtime/sessionMode.ts +1 -1
  236. package/src/runtime/systemPrompt.ts +2 -0
  237. package/src/runtime/toolExecution.ts +18 -22
  238. package/src/runtime/toolIntent.ts +0 -20
  239. package/src/runtime/turn.ts +0 -92
  240. package/src/storage/atomicWrite.ts +4 -1
  241. package/src/storage/config.ts +181 -5
  242. package/src/storage/identity.ts +9 -3
  243. package/src/storage/secrets.ts +2 -2
  244. package/src/tools/bashSafety.ts +8 -0
  245. package/src/tools/changeDirectoryTool.ts +1 -1
  246. package/src/tools/deleteFileTool.ts +4 -4
  247. package/src/tools/editTool.ts +4 -4
  248. package/src/tools/editUtils.ts +5 -5
  249. package/src/tools/privateContinuityEditTool.ts +4 -5
  250. package/src/tools/privateContinuityReadTool.ts +1 -2
  251. package/src/tools/registry.ts +30 -0
  252. package/src/tools/writeFileTool.ts +5 -5
  253. package/src/ui/BrandSplash.tsx +20 -85
  254. package/src/ui/ProgressBar.tsx +3 -5
  255. package/src/ui/Select.tsx +21 -9
  256. package/src/ui/Spinner.tsx +38 -3
  257. package/src/ui/Surface.tsx +3 -3
  258. package/src/ui/TextInput.tsx +191 -29
  259. package/src/ui/theme.ts +7 -34
  260. package/src/utils/openExternal.ts +21 -0
  261. package/src/utils/withRetry.ts +47 -3
  262. package/src/identity/hub/identityHubEffects.ts +0 -937
  263. package/src/identity/hub/identityHubModel.ts +0 -291
  264. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -144
  265. package/src/identity/hub/screens/EditProfileFlow.tsx +0 -145
  266. package/src/identity/hub/screens/IdentitySummary.tsx +0 -90
  267. package/src/identity/hub/screens/MenuScreen.tsx +0 -117
  268. package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +0 -87
  269. package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
  270. package/src/identity/wallet/wallet-page/wallet.html +0 -1202
  271. /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
@@ -0,0 +1,324 @@
1
+ import React from 'react'
2
+ import { Text } from 'ink'
3
+ import { Surface } from '../../../../ui/Surface.js'
4
+ import { Select } from '../../../../ui/Select.js'
5
+ import { TextInput } from '../../../../ui/TextInput.js'
6
+ import { theme } from '../../../../ui/theme.js'
7
+ import { normalizeErc8004RegistryConfig } from '../../../registry/erc8004.js'
8
+ import {
9
+ isCurrentAgentCandidate,
10
+ tokenCandidateHint,
11
+ tokenCandidateSelectLabel,
12
+ } from '../../model/identity.js'
13
+ import { networkLabel } from '../../model/network.js'
14
+ import { shortAddress } from '../../model/format.js'
15
+ import { registryConfigFromConfig } from '../../../registry/registryConfig.js'
16
+ import type { Step } from '../../identityHubReducer.js'
17
+ import { WalletApprovalScreen } from '../../components/WalletApprovalScreen.js'
18
+ import { BusyScreen } from '../../components/BusyScreen.js'
19
+ import type { BrowserWalletReady } from '../../../wallet/browserWallet.js'
20
+ import type { EthagentConfig } from '../../../../storage/config.js'
21
+ import { restoreSignatureRequestForStep } from '../../effects/restore/index.js'
22
+ import type { RestoreProgress } from '../../effects/types.js'
23
+
24
+ type RestoreStep = Exclude<Extract<Step, { kind: `restore-${string}` }>, { kind: 'restore-wallet' | 'restore-network' }>
25
+
26
+ type RestoreFlowProps = {
27
+ step: RestoreStep
28
+ config?: EthagentConfig
29
+ walletSession: BrowserWalletReady | null
30
+ restoreProgress: RestoreProgress | null
31
+ onRestoreRegistrySubmit: (value: string) => void
32
+ onRetryDiscovery: () => void
33
+ onTokenSelect: (tokenId: string) => void
34
+ onEnsSubmit: (value: string) => void
35
+ onTokenIdSubmit: (value: string) => void
36
+ onPickRecoveryMethod: (choice: 'ens' | 'token-id') => void
37
+ onBack: () => void
38
+ }
39
+
40
+ const footerHint = (hint: string) => <Text color={theme.dim}>{hint}</Text>
41
+
42
+ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
43
+ step,
44
+ config,
45
+ walletSession,
46
+ restoreProgress,
47
+ onRestoreRegistrySubmit,
48
+ onRetryDiscovery,
49
+ onTokenSelect,
50
+ onEnsSubmit,
51
+ onTokenIdSubmit,
52
+ onPickRecoveryMethod,
53
+ onBack,
54
+ }) => {
55
+ const purpose = 'purpose' in step ? step.purpose ?? 'restore' : 'restore'
56
+ const isSwitch = purpose === 'switch'
57
+
58
+ if (step.kind === 'restore-registry') {
59
+ const resolution = registryConfigFromConfig(config)
60
+ return (
61
+ <Surface
62
+ title={`${resolution.network ? networkLabel(resolution.network).charAt(0).toUpperCase() + networkLabel(resolution.network).slice(1) : ''} Agent Registry`}
63
+ subtitle={step.error ? `Lookup failed: ${step.error}` : 'Paste the agent registry address for this network.'}
64
+ footer={footerHint('enter discover · esc back')}
65
+ >
66
+ <Text color={theme.dim}>RPC defaults to {resolution.defaultRpcUrl}</Text>
67
+ <TextInput
68
+ initialValue={config?.erc8004?.identityRegistryAddress ?? ''}
69
+ placeholder="0x registry address"
70
+ validate={value => {
71
+ try {
72
+ normalizeErc8004RegistryConfig({
73
+ chainId: resolution.chainId,
74
+ rpcUrl: resolution.config?.rpcUrl ?? resolution.defaultRpcUrl,
75
+ identityRegistryAddress: value.trim(),
76
+ })
77
+ return null
78
+ } catch (err: unknown) {
79
+ return (err as Error).message
80
+ }
81
+ }}
82
+ onSubmit={onRestoreRegistrySubmit}
83
+ onCancel={onBack}
84
+ />
85
+ </Surface>
86
+ )
87
+ }
88
+
89
+ if (step.kind === 'restore-discovering') {
90
+ return (
91
+ <BusyScreen
92
+ title={isSwitch ? 'Finding Agents' : 'Finding Agents'}
93
+ subtitle={step.ownerHandle}
94
+ label="checking tokens this wallet directly owns..."
95
+ onCancel={onBack}
96
+ />
97
+ )
98
+ }
99
+
100
+ if (step.kind === 'restore-recovery-input') {
101
+ return (
102
+ <Surface
103
+ title={isSwitch ? 'Load Agent' : 'Restore Agent'}
104
+ subtitle="The connected wallet doesn't directly own an agent token on this network."
105
+ footer={footerHint('enter select · esc back')}
106
+ >
107
+ <Text color={theme.dim}>If this wallet is an approved operator wallet, choose how to find the agent token.</Text>
108
+ <Select<'ens' | 'token-id' | 'back'>
109
+ options={[
110
+ { value: 'ens', role: 'section', label: 'Recovery Key' },
111
+ { value: 'ens', label: 'Enter ENS Name', hint: 'Resolve the agent via its ENS subdomain (e.g. agent.example.eth)' },
112
+ { value: 'token-id', label: 'Enter Token ID', hint: 'Look up the agent directly by ERC-8004 token ID (e.g. 45744)' },
113
+ { value: 'back', role: 'section', label: 'Navigation' },
114
+ { value: 'back', label: 'Back', hint: 'Pick a different network', role: 'utility' },
115
+ ]}
116
+ hintLayout="inline"
117
+ onSubmit={value => value === 'back' ? onBack() : onPickRecoveryMethod(value)}
118
+ onCancel={onBack}
119
+ />
120
+ </Surface>
121
+ )
122
+ }
123
+
124
+ if (step.kind === 'restore-ens-input') {
125
+ return (
126
+ <Surface
127
+ title={isSwitch ? 'Load Agent' : 'Restore Agent'}
128
+ subtitle="Enter the agent's ENS name to decrypt with an authorized operator wallet."
129
+ footer={footerHint(step.busy ? 'Looking up...' : 'enter continue · esc back')}
130
+ >
131
+ <Text color={theme.dim}>The full agent subdomain, e.g. agent.example.eth.</Text>
132
+ <TextInput
133
+ placeholder="agent.example.eth"
134
+ onSubmit={value => onEnsSubmit(value.trim())}
135
+ onCancel={onBack}
136
+ />
137
+ {step.error ? <Text color={theme.accentError}>{step.error}</Text> : null}
138
+ </Surface>
139
+ )
140
+ }
141
+
142
+ if (step.kind === 'restore-token-id-input') {
143
+ return (
144
+ <Surface
145
+ title={isSwitch ? 'Load Agent' : 'Restore Agent'}
146
+ subtitle={`Enter the ERC-8004 token ID on ${networkLabelForRegistry(step.registry)}.`}
147
+ footer={footerHint(step.busy ? 'Looking up...' : 'enter continue · esc back')}
148
+ >
149
+ <Text color={theme.dim}>The integer token ID assigned at mint (for example, 45744). Store it alongside your wallet seed so the agent stays recoverable even if its ENS record is cleared.</Text>
150
+ <TextInput
151
+ placeholder="45744"
152
+ onSubmit={value => onTokenIdSubmit(value.trim())}
153
+ onCancel={onBack}
154
+ />
155
+ {step.error ? <Text color={theme.accentError}>{step.error}</Text> : null}
156
+ </Surface>
157
+ )
158
+ }
159
+
160
+ if (step.kind === 'restore-not-found') {
161
+ const view = restoreNotFoundView(step)
162
+ return (
163
+ <Surface
164
+ title={view.title}
165
+ subtitle={view.subtitle}
166
+ footer={footerHint('enter continue · esc back')}
167
+ >
168
+ <Text color={theme.dim}>{view.detail}</Text>
169
+ <Select<'retry' | 'network'>
170
+ options={[
171
+ { value: 'retry', role: 'section', label: 'Next' },
172
+ { value: 'retry', label: 'Retry Search', hint: 'Search token ownership and ERC-8004 restore access again' },
173
+ { value: 'network', label: 'Choose Network', hint: 'Search a different ERC-8004 network' },
174
+ ]}
175
+ hintLayout="inline"
176
+ onSubmit={choice => {
177
+ if (choice === 'retry') onRetryDiscovery()
178
+ else onBack()
179
+ }}
180
+ onCancel={onBack}
181
+ />
182
+ </Surface>
183
+ )
184
+ }
185
+
186
+ if (step.kind === 'restore-select-token') {
187
+ return (
188
+ <Surface
189
+ title={isSwitch ? 'Load an Agent' : 'Choose Your Agent'}
190
+ subtitle={step.ownerHandle}
191
+ footer={footerHint('enter select · esc back')}
192
+ >
193
+ <Select<string>
194
+ options={[
195
+ { value: 'section:available-agents', role: 'section', label: 'Available Agents' },
196
+ ...step.candidates.map(candidate => {
197
+ const current = isSwitch && isCurrentAgentCandidate(config?.identity, candidate)
198
+ return {
199
+ value: candidate.agentId.toString(),
200
+ label: tokenCandidateSelectLabel(candidate, current),
201
+ hint: tokenCandidateHint(candidate),
202
+ }
203
+ }),
204
+ { value: '__ens__', role: 'section', label: 'Recovery Key' },
205
+ { value: '__ens__', label: 'Enter ENS Name', hint: 'Resolve the agent via its ENS subdomain (e.g. agent.example.eth)' },
206
+ { value: '__token-id__', label: 'Enter Token ID', hint: 'Look up the agent directly by ERC-8004 token ID (e.g. 45744)' },
207
+ { value: '__back__', role: 'section', label: 'Navigation' },
208
+ { value: '__back__', label: 'Back', hint: 'Return to the previous step', role: 'utility' },
209
+ ]}
210
+ hintLayout="inline"
211
+ onSubmit={value => {
212
+ if (value === '__ens__') onPickRecoveryMethod('ens')
213
+ else if (value === '__token-id__') onPickRecoveryMethod('token-id')
214
+ else if (value === '__back__') onBack()
215
+ else onTokenSelect(value)
216
+ }}
217
+ onCancel={onBack}
218
+ />
219
+ </Surface>
220
+ )
221
+ }
222
+
223
+ if (step.kind === 'restore-fetching') {
224
+ return (
225
+ <BusyScreen
226
+ title={isSwitch ? 'Loading Agent' : 'Restoring Your Agent'}
227
+ subtitle="IPFS"
228
+ label="opening encrypted state from IPFS..."
229
+ onCancel={onBack}
230
+ />
231
+ )
232
+ }
233
+
234
+ if (step.kind === 'restore-authorizing') {
235
+ const view = restoreAuthorizationView(step, isSwitch)
236
+ if (restoreProgress) {
237
+ return (
238
+ <BusyScreen
239
+ title={view.progressTitle}
240
+ subtitle="Wallet Signature Received"
241
+ label={restoreProgress.label}
242
+ />
243
+ )
244
+ }
245
+ return (
246
+ <WalletApprovalScreen
247
+ title={view.title}
248
+ subtitle={view.subtitle}
249
+ walletSession={walletSession}
250
+ label={view.label}
251
+ onCancel={onBack}
252
+ />
253
+ )
254
+ }
255
+
256
+ return null
257
+ }
258
+
259
+ function networkLabelForRegistry(registry: { chainId: number }): string {
260
+ const network = registry.chainId === 1 ? 'mainnet'
261
+ : registry.chainId === 8453 ? 'base'
262
+ : undefined
263
+ return network ? networkLabel(network) : `chain ${registry.chainId}`
264
+ }
265
+
266
+ function restoreNotFoundView(
267
+ step: Extract<RestoreStep, { kind: 'restore-not-found' }>,
268
+ ): { title: string; subtitle: string; detail: string } {
269
+ const network = networkLabelForRegistry(step.registry)
270
+ const address = step.requesterAddress ?? step.ownerHandle
271
+ if (step.reason === 'cancelled') {
272
+ return {
273
+ title: 'Search Cancelled',
274
+ subtitle: `Stopped scanning ${network} for ${shortAddress(address)}.`,
275
+ detail: 'No restore target was selected. Retry to keep searching, or choose a different network.',
276
+ }
277
+ }
278
+ if (step.reason === 'no-owner-or-operator') {
279
+ return {
280
+ title: 'No Agent Access Found',
281
+ subtitle: `${shortAddress(address)} has no ERC-8004 agent access on ${network}.`,
282
+ detail: step.requesterAddress
283
+ ? 'Checked token ownership and indexed ERC-8004 restore metadata. This wallet owns no agent token and is not listed as an operator wallet.'
284
+ : 'Checked token ownership for this name. Operator-wallet metadata lookup requires a wallet address.',
285
+ }
286
+ }
287
+ return {
288
+ title: 'Agent Search Incomplete',
289
+ subtitle: `ERC-8004 ownership or restore metadata could not be checked completely on ${network}.`,
290
+ detail: 'No restore target was selected. Retry the search or choose another network.',
291
+ }
292
+ }
293
+
294
+ function restoreAuthorizationView(
295
+ step: Extract<RestoreStep, { kind: 'restore-authorizing' }>,
296
+ isSwitch: boolean,
297
+ ): { title: string; subtitle: string; label: string; progressTitle: string } {
298
+ const owner = step.candidate.ownerAddress
299
+ let requester: string | undefined = step.requesterAddress
300
+ let role: 'owner-wallet' | 'operator-wallet' = 'owner-wallet'
301
+ try {
302
+ const request = restoreSignatureRequestForStep(step)
303
+ requester = request.expectedAccount
304
+ role = request.role
305
+ } catch {
306
+ requester = step.requesterAddress
307
+ }
308
+
309
+ if (role === 'operator-wallet' && requester) {
310
+ return {
311
+ title: 'Operator Wallet Required',
312
+ subtitle: `Sign with the operator wallet ${shortAddress(requester)} to decrypt this snapshot.`,
313
+ label: 'waiting for operator wallet signature...',
314
+ progressTitle: isSwitch ? 'Loading Agent' : 'Restoring Your Agent',
315
+ }
316
+ }
317
+
318
+ return {
319
+ title: 'Owner Wallet Required',
320
+ subtitle: `This encrypted snapshot requires the owner wallet ${shortAddress(owner)}.`,
321
+ label: 'waiting for owner wallet signature...',
322
+ progressTitle: isSwitch ? 'Loading Agent' : 'Restoring Your Agent',
323
+ }
324
+ }
@@ -0,0 +1,77 @@
1
+ import { useEffect } from 'react'
2
+ import type { EthagentConfig } from '../../../../storage/config.js'
3
+ import {
4
+ runRestoreAuthorize,
5
+ runRestoreConnectWallet,
6
+ runRestoreDiscover,
7
+ runRestoreFetch,
8
+ } from '../../effects/restore/index.js'
9
+ import type { EffectCallbacks } from '../../effects/types.js'
10
+ import type { Step } from '../../identityHubReducer.js'
11
+
12
+ const MIN_BUSY_ERROR_MS = 2000
13
+
14
+ function waitForMinimumBusyTime(startedAt: number): Promise<void> {
15
+ const elapsed = Date.now() - startedAt
16
+ if (elapsed >= MIN_BUSY_ERROR_MS) return Promise.resolve()
17
+ return new Promise(resolve => setTimeout(resolve, MIN_BUSY_ERROR_MS - elapsed))
18
+ }
19
+
20
+ type RestoreFlowEffectsArgs = {
21
+ step: Step
22
+ config: EthagentConfig | undefined
23
+ callbacks: EffectCallbacks
24
+ handleStepError: (err: unknown, backStep: Step, softCancel?: Step) => void
25
+ }
26
+
27
+ export function useRestoreFlowEffects(args: RestoreFlowEffectsArgs): void {
28
+ const { step, config, callbacks, handleStepError } = args
29
+
30
+ useEffect(() => {
31
+ if (step.kind !== 'restore-discovering') return
32
+ let cancelled = false
33
+ const startedAt = Date.now()
34
+ const abortController = new AbortController()
35
+ const stepWithSignal = { ...step, abortSignal: abortController.signal }
36
+ runRestoreDiscover(stepWithSignal, config, callbacks)
37
+ .catch(async (err: unknown) => {
38
+ await waitForMinimumBusyTime(startedAt)
39
+ if (cancelled) return
40
+ handleStepError(err, { kind: 'restore-network', ownerHandle: step.ownerHandle, purpose: step.purpose })
41
+ })
42
+ return () => {
43
+ cancelled = true
44
+ abortController.abort()
45
+ }
46
+ }, [step])
47
+
48
+ useEffect(() => {
49
+ if (step.kind !== 'restore-wallet') return
50
+ let cancelled = false
51
+ runRestoreConnectWallet(step, callbacks)
52
+ .catch((err: unknown) => { if (!cancelled) handleStepError(err, { kind: 'menu' }) })
53
+ return () => { cancelled = true }
54
+ }, [step])
55
+
56
+ useEffect(() => {
57
+ if (step.kind !== 'restore-fetching') return
58
+ let cancelled = false
59
+ const startedAt = Date.now()
60
+ runRestoreFetch(step, callbacks)
61
+ .catch(async (err: unknown) => {
62
+ await waitForMinimumBusyTime(startedAt)
63
+ if (!cancelled) handleStepError(err, { kind: 'restore-network', ownerHandle: step.requesterAddress ?? step.candidate.ownerAddress, purpose: step.purpose })
64
+ })
65
+ return () => { cancelled = true }
66
+ }, [step])
67
+
68
+ useEffect(() => {
69
+ if (step.kind !== 'restore-authorizing') return
70
+ let cancelled = false
71
+ runRestoreAuthorize(step, callbacks)
72
+ .catch((err: unknown) => {
73
+ if (!cancelled) handleStepError(err, { kind: 'restore-network', ownerHandle: step.requesterAddress ?? step.candidate.ownerAddress, purpose: step.purpose })
74
+ })
75
+ return () => { cancelled = true }
76
+ }, [step])
77
+ }
@@ -1,20 +1,17 @@
1
1
  import React from 'react'
2
2
  import { Box, Text } from 'ink'
3
- import { Surface } from '../../../ui/Surface.js'
4
- import { Select } from '../../../ui/Select.js'
5
- import { TextInput } from '../../../ui/TextInput.js'
6
- import { theme } from '../../../ui/theme.js'
7
- import { extractPinataJwt } from '../../storage/ipfs.js'
8
- import type { Step } from '../identityHubReducer.js'
9
-
10
- const PINATA_API_KEYS_URL = 'https://app.pinata.cloud/developers/api-keys'
3
+ import { Surface } from '../../../../ui/Surface.js'
4
+ import { Select } from '../../../../ui/Select.js'
5
+ import { theme } from '../../../../ui/theme.js'
6
+ import { PinataJwtInput } from '../../components/PinataJwtInput.js'
7
+ import type { Step } from '../../identityHubReducer.js'
11
8
 
12
9
  type StorageCredentialAction = 'edit' | 'forget' | 'back'
13
10
 
14
11
  export const STORAGE_CREDENTIAL_FORGET_COPY = [
15
12
  'removes the saved IPFS storage token from this machine.',
16
13
  'existing pinned IPFS backups are not deleted.',
17
- 'ethagent cannot pin new encrypted state with that account until you save a token again.',
14
+ 'new encrypted snapshots cannot be pinned with that account until you save a token again.',
18
15
  'agent identity and sessions stay on this machine.',
19
16
  ] as const
20
17
 
@@ -41,39 +38,21 @@ export const StorageCredentialScreen: React.FC<StorageCredentialScreenProps> = (
41
38
  }) => {
42
39
  if (step.kind === 'storage-credential-input') {
43
40
  return (
44
- <Surface
45
- title="IPFS Storage Credential"
46
- subtitle={step.error ?? 'Save the token ethagent uses to pin encrypted state.'}
41
+ <PinataJwtInput
42
+ inputKey="storage-credential-input"
43
+ title="IPFS Storage"
44
+ subtitle={step.error ?? 'Save the Pinata JWT used to pin encrypted snapshots.'}
47
45
  footer={footer}
48
- >
49
- <Text>
50
- <Text color={theme.dim}>Paste your Pinata JWT. Get one at </Text>
51
- <Text color={theme.accentPrimary} underline>{PINATA_API_KEYS_URL}</Text>
52
- </Text>
53
- <Text color={theme.dim}>Stored encrypted on this device. Used only for IPFS pinning.</Text>
54
- <TextInput
55
- key="storage-credential-input"
56
- isSecret
57
- placeholder="Pinata JWT"
58
- validate={v => {
59
- try {
60
- extractPinataJwt(v)
61
- return null
62
- } catch (err: unknown) {
63
- return (err as Error).message
64
- }
65
- }}
66
- onSubmit={onSubmit}
67
- onCancel={onCancel}
68
- />
69
- </Surface>
46
+ onSubmit={onSubmit}
47
+ onCancel={onCancel}
48
+ />
70
49
  )
71
50
  }
72
51
 
73
52
  if (step.kind === 'storage-credential-forget-confirm') {
74
53
  return (
75
54
  <Surface
76
- title="Forget IPFS Storage Credential?"
55
+ title="Forget IPFS Storage?"
77
56
  subtitle="This only removes the local token used for pinning."
78
57
  footer={footer}
79
58
  >
@@ -82,12 +61,15 @@ export const StorageCredentialScreen: React.FC<StorageCredentialScreenProps> = (
82
61
  <Text key={line} color={theme.dim}>- {line}</Text>
83
62
  ))}
84
63
  </Box>
64
+ <Box marginTop={1}>
65
+ <Text color={theme.accentPeriwinkle}>Remove the token from this machine?</Text>
66
+ </Box>
85
67
  <Box marginTop={1}>
86
68
  <Select<StorageCredentialAction>
87
69
  options={[
88
- { value: 'forget', role: 'section', prefix: '--', label: 'Credential' },
70
+ { value: 'forget', role: 'section', label: 'Credential' },
89
71
  { value: 'forget', label: 'Forget Credential', hint: 'Remove local IPFS pinning token' },
90
- { value: 'back', role: 'section', prefix: '--', label: 'Navigation' },
72
+ { value: 'back', role: 'section', label: 'Navigation' },
91
73
  { value: 'back', label: 'Keep Credential', hint: 'Return without changing storage access', role: 'utility' },
92
74
  ]}
93
75
  hintLayout="inline"
@@ -101,18 +83,18 @@ export const StorageCredentialScreen: React.FC<StorageCredentialScreenProps> = (
101
83
 
102
84
  return (
103
85
  <Surface
104
- title="IPFS Storage Credential"
105
- subtitle="Controls whether ethagent can pin encrypted state from this machine."
86
+ title="IPFS Storage"
87
+ subtitle="Manage the credential used to pin encrypted snapshots from this machine."
106
88
  footer={footer}
107
89
  >
108
90
  <Box marginTop={1}>
109
91
  <Select<StorageCredentialAction>
110
92
  options={[
111
- { value: 'edit', role: 'section', prefix: '--', label: 'Credential' },
93
+ { value: 'edit', role: 'section', label: 'Credential' },
112
94
  { value: 'edit', label: hasCredential ? 'Replace Credential' : 'Save Credential', hint: 'Store Pinata JWT for IPFS pinning' },
113
- { value: 'forget', label: 'Forget Credential', hint: 'Remove the local pinning token; existing pins remain', disabled: !hasCredential },
114
- { value: 'back', role: 'section', prefix: '--', label: 'Navigation' },
115
- { value: 'back', label: 'Back To Identity Hub', hint: 'Return without changing storage access', role: 'utility' },
95
+ { value: 'forget', label: 'Forget Credential', hint: 'Remove the local pinning token. Existing pins remain', disabled: !hasCredential },
96
+ { value: 'back', role: 'section', label: 'Navigation' },
97
+ { value: 'back', label: 'Back', hint: 'Return to Identity Hub menu', role: 'utility' },
116
98
  ]}
117
99
  hintLayout="inline"
118
100
  onSubmit={choice => {
@@ -0,0 +1,162 @@
1
+ import React from 'react'
2
+ import type { BrowserWalletReady } from '../../../wallet/browserWallet.js'
3
+ import { supportedErc8004ChainForId } from '../../../registry/erc8004.js'
4
+ import {
5
+ runTokenTransferStorageSubmit,
6
+ runTokenTransferTargetSubmit,
7
+ } from '../../effects/token-transfer/runTokenTransfer.js'
8
+ import type {
9
+ EffectCallbacks,
10
+ TokenTransferProgress,
11
+ } from '../../effects/types.js'
12
+ import type { Step } from '../../identityHubReducer.js'
13
+ import { BusyScreen } from '../../components/BusyScreen.js'
14
+ import { RebackupStorageScreen } from '../continuity/RebackupStorageScreen.js'
15
+ import {
16
+ TokenTransferReadyScreen,
17
+ TokenTransferSigningScreen,
18
+ TokenTransferTargetScreen,
19
+ } from './TokenTransferScreens.js'
20
+
21
+ type TokenTransferStep = Extract<Step, {
22
+ kind:
23
+ | 'token-transfer-target'
24
+ | 'token-transfer-resolving'
25
+ | 'token-transfer-storage'
26
+ | 'token-transfer-signing'
27
+ | 'token-transfer-ready'
28
+ }>
29
+
30
+ type IdentityHubTokenTransferFlowProps = {
31
+ step: TokenTransferStep
32
+ callbacks: EffectCallbacks
33
+ footer: React.ReactNode
34
+ progress: TokenTransferProgress | null
35
+ walletSession: BrowserWalletReady | null
36
+ onSetStep: (step: Step) => void
37
+ onBack: () => void
38
+ }
39
+
40
+ export function isTokenTransferStep(step: Step): step is TokenTransferStep {
41
+ return step.kind === 'token-transfer-target'
42
+ || step.kind === 'token-transfer-resolving'
43
+ || step.kind === 'token-transfer-storage'
44
+ || step.kind === 'token-transfer-signing'
45
+ || step.kind === 'token-transfer-ready'
46
+ }
47
+
48
+ export const IdentityHubTokenTransferFlow: React.FC<IdentityHubTokenTransferFlowProps> = ({
49
+ step,
50
+ callbacks,
51
+ footer,
52
+ progress,
53
+ walletSession,
54
+ onSetStep,
55
+ onBack,
56
+ }) => {
57
+ const resolveAbortRef = React.useRef<AbortController | null>(null)
58
+ const tokenNetworkLabel = networkLabelForChainId(step.registry.chainId)
59
+ const readyBackHint = tokenTransferBackHint(step.returnTo)
60
+
61
+ if (step.kind === 'token-transfer-target') {
62
+ return (
63
+ <TokenTransferTargetScreen
64
+ identity={step.identity}
65
+ tokenNetworkLabel={tokenNetworkLabel}
66
+ error={step.error}
67
+ initialValue={step.previousTarget}
68
+ onSubmit={async value => {
69
+ resolveAbortRef.current?.abort()
70
+ const controller = new AbortController()
71
+ resolveAbortRef.current = controller
72
+ try {
73
+ await runTokenTransferTargetSubmit(value, step, callbacks, { signal: controller.signal })
74
+ } catch (err: unknown) {
75
+ if (controller.signal.aborted) return
76
+ onSetStep({ ...step, error: (err as Error).message })
77
+ } finally {
78
+ if (resolveAbortRef.current === controller) resolveAbortRef.current = null
79
+ }
80
+ }}
81
+ onBack={onBack}
82
+ />
83
+ )
84
+ }
85
+
86
+ if (step.kind === 'token-transfer-resolving') {
87
+ return (
88
+ <BusyScreen
89
+ title="Resolve Receiver Wallet"
90
+ subtitle={`Resolving ${step.targetHandle} before preparing the transfer snapshot.`}
91
+ label="checking ens or address..."
92
+ onCancel={() => {
93
+ resolveAbortRef.current?.abort()
94
+ resolveAbortRef.current = null
95
+ onBack()
96
+ }}
97
+ />
98
+ )
99
+ }
100
+
101
+ if (step.kind === 'token-transfer-storage') {
102
+ return (
103
+ <RebackupStorageScreen
104
+ step={{
105
+ kind: 'rebackup-storage',
106
+ identity: step.identity,
107
+ registry: step.registry,
108
+ error: step.error,
109
+ pinataJwt: step.pinataJwt,
110
+ }}
111
+ footer={footer}
112
+ title="Connect IPFS Storage"
113
+ subtitle="Save a Pinata JWT so ethagent can pin the transfer snapshot to IPFS."
114
+ onSubmit={async input => {
115
+ try {
116
+ await runTokenTransferStorageSubmit(input, step, callbacks)
117
+ } catch (err: unknown) {
118
+ onSetStep({ ...step, error: (err as Error).message })
119
+ }
120
+ }}
121
+ onCancel={onBack}
122
+ />
123
+ )
124
+ }
125
+
126
+ if (step.kind === 'token-transfer-signing') {
127
+ return (
128
+ <TokenTransferSigningScreen
129
+ identity={step.identity}
130
+ tokenNetworkLabel={tokenNetworkLabel}
131
+ targetHandle={step.targetHandle}
132
+ targetAddress={step.targetAddress}
133
+ progress={progress}
134
+ walletSession={walletSession}
135
+ onCancel={onBack}
136
+ />
137
+ )
138
+ }
139
+
140
+ return (
141
+ <TokenTransferReadyScreen
142
+ identity={step.identity}
143
+ tokenNetworkLabel={tokenNetworkLabel}
144
+ targetHandle={step.targetHandle}
145
+ targetAddress={step.targetAddress}
146
+ snapshotCid={step.snapshotCid}
147
+ txHash={step.txHash}
148
+ footer={footer}
149
+ backHint={readyBackHint}
150
+ onBack={onBack}
151
+ />
152
+ )
153
+ }
154
+
155
+ function networkLabelForChainId(chainId: number): string {
156
+ return supportedErc8004ChainForId(chainId)?.name ?? `Chain ${chainId}`
157
+ }
158
+
159
+ function tokenTransferBackHint(returnTo: Step | undefined): string {
160
+ if (returnTo?.kind === 'edit-profile-ens') return 'Return to ENS setup'
161
+ return 'Return to Identity Hub menu'
162
+ }