ethagent 1.1.2 → 2.0.1

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 (268) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +124 -32
  3. package/package.json +8 -3
  4. package/src/app/FirstRun.tsx +190 -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 +35 -15
  11. package/src/chat/ContextLimitView.tsx +4 -4
  12. package/src/chat/ContinuityEditReviewView.tsx +10 -22
  13. package/src/chat/CopyPicker.tsx +0 -1
  14. package/src/chat/MessageList.tsx +62 -45
  15. package/src/chat/PermissionPrompt.tsx +13 -9
  16. package/src/chat/PlanApprovalView.tsx +3 -3
  17. package/src/chat/ResumeView.tsx +1 -4
  18. package/src/chat/RewindView.tsx +2 -2
  19. package/src/chat/chatInputState.ts +1 -1
  20. package/src/chat/chatScreenUtils.ts +22 -11
  21. package/src/chat/chatSessionState.ts +2 -2
  22. package/src/chat/chatTurnOrchestrator.ts +16 -81
  23. package/src/chat/commands.ts +1 -1
  24. package/src/chat/textCursor.ts +1 -1
  25. package/src/chat/transcriptViewport.ts +2 -7
  26. package/src/cli/ResetConfirmView.tsx +1 -1
  27. package/src/cli/main.tsx +9 -3
  28. package/src/cli/preview.tsx +0 -5
  29. package/src/cli/updateNotice.ts +4 -2
  30. package/src/identity/continuity/editor.ts +7 -107
  31. package/src/identity/continuity/envelope.ts +1048 -40
  32. package/src/identity/continuity/history.ts +4 -4
  33. package/src/identity/continuity/localBackup.ts +249 -0
  34. package/src/identity/continuity/privateEdit/apply.ts +170 -0
  35. package/src/identity/continuity/privateEdit/diff.ts +82 -0
  36. package/src/identity/continuity/privateEdit/files.ts +23 -0
  37. package/src/identity/continuity/privateEdit/types.ts +28 -0
  38. package/src/identity/continuity/privateEdit.ts +10 -298
  39. package/src/identity/continuity/publicSkills.ts +8 -9
  40. package/src/identity/continuity/snapshots.ts +17 -6
  41. package/src/identity/continuity/storage/defaults.ts +111 -0
  42. package/src/identity/continuity/storage/files.ts +72 -0
  43. package/src/identity/continuity/storage/markdown.ts +81 -0
  44. package/src/identity/continuity/storage/paths.ts +24 -0
  45. package/src/identity/continuity/storage/scaffold.ts +124 -0
  46. package/src/identity/continuity/storage/status.ts +86 -0
  47. package/src/identity/continuity/storage/types.ts +27 -0
  48. package/src/identity/continuity/storage.ts +32 -507
  49. package/src/identity/continuity/zipWriter.ts +95 -0
  50. package/src/identity/crypto/backupEnvelope.ts +14 -247
  51. package/src/identity/crypto/eth.ts +7 -7
  52. package/src/identity/ens/agentRecords.ts +96 -0
  53. package/src/identity/ens/ensAutomation/contracts.ts +38 -0
  54. package/src/identity/ens/ensAutomation/delete.ts +80 -0
  55. package/src/identity/ens/ensAutomation/names.ts +14 -0
  56. package/src/identity/ens/ensAutomation/operators.ts +29 -0
  57. package/src/identity/ens/ensAutomation/read.ts +114 -0
  58. package/src/identity/ens/ensAutomation/root.ts +63 -0
  59. package/src/identity/ens/ensAutomation/setup.ts +284 -0
  60. package/src/identity/ens/ensAutomation/transactions.ts +107 -0
  61. package/src/identity/ens/ensAutomation/types.ts +126 -0
  62. package/src/identity/ens/ensAutomation.ts +29 -0
  63. package/src/identity/ens/ensLookup/client.ts +43 -0
  64. package/src/identity/ens/ensLookup/constants.ts +26 -0
  65. package/src/identity/ens/ensLookup/discovery.ts +70 -0
  66. package/src/identity/ens/ensLookup/names.ts +34 -0
  67. package/src/identity/ens/ensLookup/records.ts +45 -0
  68. package/src/identity/ens/ensLookup/resolve.ts +75 -0
  69. package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
  70. package/src/identity/ens/ensLookup/types.ts +38 -0
  71. package/src/identity/ens/ensLookup/validation.ts +72 -0
  72. package/src/identity/ens/ensLookup.ts +19 -0
  73. package/src/identity/ens/ensRegistration.ts +199 -0
  74. package/src/identity/ens/resolverDelegation.ts +48 -0
  75. package/src/identity/hub/IdentityHub.tsx +13 -817
  76. package/src/identity/hub/OperationalRoutes.tsx +370 -0
  77. package/src/identity/hub/Routes.tsx +361 -0
  78. package/src/identity/hub/advancedEnsValidation.ts +45 -0
  79. package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
  80. package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
  81. package/src/identity/hub/components/FlowTimeline.tsx +27 -0
  82. package/src/identity/hub/components/IdentitySummary.tsx +190 -0
  83. package/src/identity/hub/components/MenuScreen.tsx +237 -0
  84. package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
  85. package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
  86. package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
  87. package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
  88. package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
  89. package/src/identity/hub/effects/create.ts +310 -0
  90. package/src/identity/hub/effects/ens/flows.ts +218 -0
  91. package/src/identity/hub/effects/ens/index.ts +11 -0
  92. package/src/identity/hub/effects/ens/transactions.ts +239 -0
  93. package/src/identity/hub/effects/index.ts +74 -0
  94. package/src/identity/hub/effects/profile/profileState.ts +173 -0
  95. package/src/identity/hub/effects/publicProfile/index.ts +5 -0
  96. package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
  97. package/src/identity/hub/effects/rebackup/index.ts +7 -0
  98. package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
  99. package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
  100. package/src/identity/hub/effects/receipts.ts +46 -0
  101. package/src/identity/hub/effects/restore/apply.ts +112 -0
  102. package/src/identity/hub/effects/restore/auth.ts +159 -0
  103. package/src/identity/hub/effects/restore/discover.ts +86 -0
  104. package/src/identity/hub/effects/restore/envelopes.ts +21 -0
  105. package/src/identity/hub/effects/restore/fetch.ts +25 -0
  106. package/src/identity/hub/effects/restore/index.ts +22 -0
  107. package/src/identity/hub/effects/restore/recovery.ts +135 -0
  108. package/src/identity/hub/effects/restore/resolve.ts +102 -0
  109. package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
  110. package/src/identity/hub/effects/restore/shared.ts +91 -0
  111. package/src/identity/hub/effects/restoreAdmin.ts +93 -0
  112. package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
  113. package/src/identity/hub/effects/shared/snapshot.ts +336 -0
  114. package/src/identity/hub/effects/shared/sync.ts +190 -0
  115. package/src/identity/hub/effects/token-transfer/index.ts +6 -0
  116. package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
  117. package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
  118. package/src/identity/hub/effects/types.ts +53 -0
  119. package/src/identity/hub/effects/vault/preflight.ts +50 -0
  120. package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
  121. package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
  122. package/src/identity/hub/{screens → flows/continuity}/RecoveryConfirmScreen.tsx +28 -19
  123. package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
  124. package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
  125. package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
  126. package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
  127. package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
  128. package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
  129. package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
  130. package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
  131. package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
  132. package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
  133. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
  134. package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
  135. package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
  136. package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
  137. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
  138. package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
  139. package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
  140. package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
  141. package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
  142. package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
  143. package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
  144. package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
  145. package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
  146. package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
  147. package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +23 -44
  148. package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
  149. package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
  150. package/src/identity/hub/identityHubReducer.ts +164 -99
  151. package/src/identity/hub/model/continuity.ts +94 -0
  152. package/src/identity/hub/model/copy.ts +35 -0
  153. package/src/identity/hub/model/custody.ts +54 -0
  154. package/src/identity/hub/model/ens.ts +49 -0
  155. package/src/identity/hub/model/errors.ts +140 -0
  156. package/src/identity/hub/model/format.ts +15 -0
  157. package/src/identity/hub/model/identity.ts +94 -0
  158. package/src/identity/hub/model/network.ts +32 -0
  159. package/src/identity/hub/model/transfer.ts +57 -0
  160. package/src/identity/hub/operatorWallets.ts +131 -0
  161. package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
  162. package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
  163. package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
  164. package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
  165. package/src/identity/hub/reconciliation/index.ts +21 -0
  166. package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
  167. package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
  168. package/src/identity/hub/txGuard.ts +51 -0
  169. package/src/identity/hub/types.ts +17 -0
  170. package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
  171. package/src/identity/hub/useIdentityHubController.ts +396 -0
  172. package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
  173. package/src/identity/hub/utils.ts +79 -0
  174. package/src/identity/identityCompat.ts +34 -0
  175. package/src/identity/profile/agentIcon.ts +61 -0
  176. package/src/identity/profile/imagePicker.ts +12 -12
  177. package/src/identity/registry/erc8004/abi.ts +14 -0
  178. package/src/identity/registry/erc8004/chains.ts +150 -0
  179. package/src/identity/registry/erc8004/client.ts +11 -0
  180. package/src/identity/registry/erc8004/discovery.ts +511 -0
  181. package/src/identity/registry/erc8004/metadata.ts +335 -0
  182. package/src/identity/registry/erc8004/ownership.ts +121 -0
  183. package/src/identity/registry/erc8004/preflight.ts +123 -0
  184. package/src/identity/registry/erc8004/transactions.ts +77 -0
  185. package/src/identity/registry/erc8004/types.ts +88 -0
  186. package/src/identity/registry/erc8004/uri.ts +59 -0
  187. package/src/identity/registry/erc8004/utils.ts +58 -0
  188. package/src/identity/registry/erc8004.ts +53 -1106
  189. package/src/identity/registry/fieldParsers.ts +28 -0
  190. package/src/identity/registry/operatorVault/bytecode.ts +98 -0
  191. package/src/identity/registry/operatorVault/constants.ts +38 -0
  192. package/src/identity/registry/operatorVault/read.ts +246 -0
  193. package/src/identity/registry/operatorVault/transactions.ts +81 -0
  194. package/src/identity/registry/operatorVault.ts +44 -0
  195. package/src/identity/storage/ipfs.ts +26 -24
  196. package/src/identity/wallet/browserWallet/gas.ts +41 -0
  197. package/src/identity/wallet/browserWallet/html.ts +106 -0
  198. package/src/identity/wallet/browserWallet/http.ts +28 -0
  199. package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
  200. package/src/identity/wallet/browserWallet/requests.ts +191 -0
  201. package/src/identity/wallet/browserWallet/session.ts +325 -0
  202. package/src/identity/wallet/browserWallet/types.ts +192 -0
  203. package/src/identity/wallet/browserWallet/validation.ts +74 -0
  204. package/src/identity/wallet/browserWallet.ts +30 -393
  205. package/src/identity/wallet/page/constants.ts +5 -0
  206. package/src/identity/wallet/page/controller.ts +251 -0
  207. package/src/identity/wallet/page/copy.ts +340 -0
  208. package/src/identity/wallet/page/grainient.ts +278 -0
  209. package/src/identity/wallet/page/html.ts +28 -0
  210. package/src/identity/wallet/page/markup.ts +50 -0
  211. package/src/identity/wallet/page/state.ts +9 -0
  212. package/src/identity/wallet/page/styles/base.ts +259 -0
  213. package/src/identity/wallet/page/styles/components.ts +262 -0
  214. package/src/identity/wallet/page/styles/index.ts +5 -0
  215. package/src/identity/wallet/page/styles/responsive.ts +247 -0
  216. package/src/identity/wallet/page/types.ts +47 -0
  217. package/src/identity/wallet/page/view.ts +535 -0
  218. package/src/identity/wallet/page/walletProvider.ts +70 -0
  219. package/src/identity/wallet/page.tsx +38 -0
  220. package/src/identity/wallet/walletPurposeCompat.ts +27 -0
  221. package/src/mcp/manager.ts +0 -1
  222. package/src/models/ModelPicker.tsx +36 -30
  223. package/src/models/catalog.ts +5 -2
  224. package/src/models/huggingface.ts +9 -9
  225. package/src/models/llamacpp.ts +13 -13
  226. package/src/models/modelDisplay.ts +75 -0
  227. package/src/models/modelPickerOptions.ts +16 -3
  228. package/src/models/modelRecommendation.ts +0 -1
  229. package/src/providers/errors.ts +16 -0
  230. package/src/providers/gemini.ts +252 -39
  231. package/src/providers/registry.ts +2 -2
  232. package/src/providers/retry.ts +1 -1
  233. package/src/runtime/sessionMode.ts +1 -1
  234. package/src/runtime/systemPrompt.ts +2 -0
  235. package/src/runtime/toolExecution.ts +18 -22
  236. package/src/runtime/toolIntent.ts +0 -20
  237. package/src/runtime/turn.ts +0 -92
  238. package/src/storage/atomicWrite.ts +4 -1
  239. package/src/storage/config.ts +181 -5
  240. package/src/storage/identity.ts +9 -3
  241. package/src/storage/secrets.ts +2 -2
  242. package/src/tools/bashSafety.ts +8 -0
  243. package/src/tools/changeDirectoryTool.ts +1 -1
  244. package/src/tools/deleteFileTool.ts +4 -4
  245. package/src/tools/editTool.ts +4 -4
  246. package/src/tools/editUtils.ts +5 -5
  247. package/src/tools/privateContinuityEditTool.ts +4 -5
  248. package/src/tools/privateContinuityReadTool.ts +1 -2
  249. package/src/tools/registry.ts +30 -0
  250. package/src/tools/writeFileTool.ts +5 -5
  251. package/src/ui/BrandSplash.tsx +20 -85
  252. package/src/ui/ProgressBar.tsx +3 -5
  253. package/src/ui/Select.tsx +20 -8
  254. package/src/ui/Spinner.tsx +38 -3
  255. package/src/ui/Surface.tsx +2 -2
  256. package/src/ui/TextInput.tsx +63 -20
  257. package/src/ui/theme.ts +7 -34
  258. package/src/utils/openExternal.ts +21 -0
  259. package/src/utils/withRetry.ts +47 -3
  260. package/src/identity/hub/identityHubEffects.ts +0 -937
  261. package/src/identity/hub/identityHubModel.ts +0 -371
  262. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -156
  263. package/src/identity/hub/screens/EditProfileFlow.tsx +0 -146
  264. package/src/identity/hub/screens/IdentitySummary.tsx +0 -106
  265. package/src/identity/hub/screens/MenuScreen.tsx +0 -117
  266. package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
  267. package/src/identity/wallet/wallet-page/wallet.html +0 -1202
  268. /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
@@ -0,0 +1,299 @@
1
+ import React from 'react'
2
+ import { openImageFilePicker } from '../../../profile/imagePicker.js'
3
+ import { readOwnerAddressField } from '../../../identityCompat.js'
4
+ import type { BrowserWalletReady } from '../../../wallet/browserWallet.js'
5
+ import type { ProfileUpdates, Step } from '../../identityHubReducer.js'
6
+ import { readCustodyMode } from '../../model/custody.js'
7
+ import { OperatorWalletsScreen } from './OperatorWalletsScreen.js'
8
+ import { EDIT_PROFILE_STEPS, EditProfileFlow } from '../profile/EditProfileFlow.js'
9
+ import { FlowTimeline } from '../../components/FlowTimeline.js'
10
+ import { WalletApprovalScreen } from '../../components/WalletApprovalScreen.js'
11
+
12
+ type StepOf<K extends Step['kind']> = Extract<Step, { kind: K }>
13
+
14
+ type IdentityHubEnsStep = StepOf<
15
+ | 'manage-ens-operators'
16
+ | 'edit-profile-name'
17
+ | 'edit-profile-description'
18
+ | 'edit-profile-image'
19
+ | 'edit-profile-review'
20
+ | 'edit-profile-ens'
21
+ | 'ens-records-tx'
22
+ | 'ens-setup-registry-tx'
23
+ | 'ens-setup-records-tx'
24
+ | 'public-profile-signing'
25
+ >
26
+
27
+ type IdentityHubEnsFlowProps = {
28
+ step: IdentityHubEnsStep
29
+ walletSession: BrowserWalletReady | null
30
+ onSetStep: (step: Step) => void
31
+ onBack: () => void
32
+ onWalletReady: (session: BrowserWalletReady | null) => void
33
+ onTriggerRebackup: (backStep: Step, profileUpdates?: ProfileUpdates) => void
34
+ onTriggerPublicProfileSave: (backStep: Step, profileUpdates: ProfileUpdates) => void
35
+ }
36
+
37
+ export function isIdentityHubEnsStep(step: Step): step is IdentityHubEnsStep {
38
+ return step.kind === 'manage-ens-operators'
39
+ || step.kind === 'edit-profile-name'
40
+ || step.kind === 'edit-profile-description'
41
+ || step.kind === 'edit-profile-image'
42
+ || step.kind === 'edit-profile-review'
43
+ || step.kind === 'edit-profile-ens'
44
+ || step.kind === 'ens-records-tx'
45
+ || step.kind === 'ens-setup-registry-tx'
46
+ || step.kind === 'ens-setup-records-tx'
47
+ || step.kind === 'public-profile-signing'
48
+ }
49
+
50
+ export const IdentityHubEnsFlow: React.FC<IdentityHubEnsFlowProps> = ({
51
+ step,
52
+ walletSession,
53
+ onSetStep,
54
+ onBack,
55
+ onWalletReady,
56
+ onTriggerRebackup,
57
+ onTriggerPublicProfileSave,
58
+ }) => {
59
+ if (step.kind === 'manage-ens-operators') {
60
+ return (
61
+ <OperatorWalletsScreen
62
+ identity={step.identity}
63
+ registry={step.registry}
64
+ walletSession={walletSession}
65
+ notice={step.notice}
66
+ error={step.error}
67
+ onSave={updates => onTriggerRebackup(step.returnTo ?? { kind: 'menu' }, updates)}
68
+ onWalletReady={onWalletReady}
69
+ onBack={onBack}
70
+ />
71
+ )
72
+ }
73
+
74
+ if (isEditProfileStep(step)) {
75
+ return (
76
+ <EditProfileFlow
77
+ step={step}
78
+ onNameSubmit={name => {
79
+ if (step.kind !== 'edit-profile-name') return
80
+ onSetStep({
81
+ kind: 'edit-profile-description',
82
+ identity: step.identity,
83
+ registry: step.registry,
84
+ name,
85
+ description: step.description,
86
+ imagePath: step.imagePath,
87
+ returnTo: step.returnTo,
88
+ })
89
+ }}
90
+ onDescriptionSubmit={description => {
91
+ if (step.kind !== 'edit-profile-description') return
92
+ onSetStep({ kind: 'edit-profile-image', identity: step.identity, registry: step.registry, name: step.name, description, imagePath: step.imagePath, returnTo: step.returnTo })
93
+ }}
94
+ onIconSubmit={iconPath => {
95
+ if (step.kind !== 'edit-profile-image') return
96
+ onSetStep({ kind: 'edit-profile-review', identity: step.identity, registry: step.registry, name: step.name, description: step.description, ...(iconPath !== undefined ? { imagePath: iconPath } : {}), returnTo: step.returnTo })
97
+ }}
98
+ onIconPick={() => {
99
+ if (step.kind !== 'edit-profile-image') return
100
+ const iconStep = step
101
+ void openImageFilePicker()
102
+ .then(result => {
103
+ if (!result.ok) {
104
+ onSetStep({ ...iconStep, error: result.cancelled ? 'icon selection cancelled.' : `${result.error}` })
105
+ return
106
+ }
107
+ onSetStep({
108
+ kind: 'edit-profile-review',
109
+ identity: iconStep.identity,
110
+ registry: iconStep.registry,
111
+ name: iconStep.name,
112
+ description: iconStep.description,
113
+ imagePath: result.file,
114
+ returnTo: iconStep.returnTo,
115
+ })
116
+ })
117
+ .catch((err: unknown) => {
118
+ onSetStep({ ...iconStep, error: `${(err as Error).message}` })
119
+ })
120
+ }}
121
+ onReviewSave={() => {
122
+ if (step.kind !== 'edit-profile-review') return
123
+ const updates: ProfileUpdates = {
124
+ name: step.name,
125
+ description: step.description,
126
+ ...(step.imagePath !== undefined ? { imagePath: step.imagePath } : {}),
127
+ }
128
+ onTriggerPublicProfileSave(step.returnTo ?? { kind: 'continuity-public' }, updates)
129
+ }}
130
+ onEnsLink={(fullName, options) => {
131
+ if (step.kind !== 'edit-profile-ens') return
132
+ const state = (step.identity.state ?? {}) as Record<string, unknown>
133
+ const savedOwnerAddress = readOwnerAddressField(state) ?? ''
134
+ const updates: ProfileUpdates = {
135
+ ensName: fullName,
136
+ ...(options.mode === 'advanced' && options.ownerAddress && !savedOwnerAddress ? { ownerAddress: options.ownerAddress } : {}),
137
+ ...(options.mode === 'advanced' && options.operatorWallet ? {
138
+ approvedOperatorWallets: [options.operatorWallet],
139
+ activeOperatorAddress: options.operatorWallet,
140
+ } : {}),
141
+ }
142
+ onTriggerRebackup(step.returnTo ?? { kind: 'menu' }, updates)
143
+ }}
144
+ onEnsUnlink={() => {
145
+ if (step.kind !== 'edit-profile-ens') return
146
+ onTriggerRebackup(step.returnTo ?? { kind: 'menu' }, { ensName: '' })
147
+ }}
148
+ onEnsRecordsUpdate={(fullName, records, options, clearRecords, currentRecords) => {
149
+ if (step.kind !== 'edit-profile-ens') return
150
+ onSetStep({
151
+ kind: 'ens-records-tx',
152
+ identity: step.identity,
153
+ registry: step.registry,
154
+ fullName,
155
+ records,
156
+ ...(currentRecords ? { currentRecords } : {}),
157
+ ...(clearRecords ? { clearRecords: true } : {}),
158
+ ...(options.mode === 'advanced' && options.ownerAddress ? { ownerAddress: options.ownerAddress } : {}),
159
+ returnTo: step.returnTo ?? { kind: 'menu' },
160
+ })
161
+ }}
162
+ onEnsSetup={setup => {
163
+ if (step.kind !== 'edit-profile-ens') return
164
+ if (setup.registryAction === 'none') {
165
+ onSetStep({
166
+ kind: 'ens-setup-records-tx',
167
+ identity: step.identity,
168
+ registry: step.registry,
169
+ setup,
170
+ returnTo: step.returnTo ?? { kind: 'menu' },
171
+ })
172
+ return
173
+ }
174
+ onSetStep({
175
+ kind: 'ens-setup-registry-tx',
176
+ identity: step.identity,
177
+ registry: step.registry,
178
+ setup,
179
+ returnTo: step.returnTo ?? { kind: 'menu' },
180
+ })
181
+ }}
182
+ onManageOperatorWalletAccess={() => {
183
+ if (step.kind !== 'edit-profile-ens') return
184
+ onSetStep({
185
+ kind: 'manage-ens-operators',
186
+ identity: step.identity,
187
+ registry: step.registry,
188
+ returnTo: { kind: 'edit-profile-ens', identity: step.identity, registry: step.registry, returnTo: step.returnTo, initialView: 'advanced' },
189
+ })
190
+ }}
191
+ onBack={onBack}
192
+ onMenu={() => onSetStep(step.returnTo ?? { kind: 'continuity-public' })}
193
+ />
194
+ )
195
+ }
196
+
197
+ if (step.kind === 'ens-records-tx') {
198
+ return (
199
+ <WalletApprovalScreen
200
+ title={step.clearRecords ? 'Unlink ENS' : 'Update ENS Records'}
201
+ subtitle={step.clearRecords
202
+ ? `Ethereum Mainnet: sign one transaction to clear ethagent record values on ${step.fullName}. Requires gas.`
203
+ : `Ethereum Mainnet: sign one transaction to set ENS records on ${step.fullName}. Requires gas.`}
204
+ walletSession={walletSession}
205
+ label="waiting for wallet transaction..."
206
+ onCancel={() => onSetStep({ kind: 'edit-profile-ens', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
207
+ />
208
+ )
209
+ }
210
+
211
+ if (step.kind === 'ens-setup-registry-tx') {
212
+ const signer = step.setup.mode === 'simple' ? 'Connected wallet' : 'Owner wallet'
213
+ return (
214
+ <WalletApprovalScreen
215
+ title={step.setup.mode === 'simple' ? 'Use Connected Wallet' : 'Use Owner Wallet'}
216
+ subtitle={`${signer} signs one Ethereum Mainnet ENS registry transaction for ${step.setup.fullName}.`}
217
+ walletSession={walletSession}
218
+ label={step.setup.mode === 'simple' ? 'waiting for connected wallet transaction...' : 'waiting for owner wallet transaction...'}
219
+ onCancel={() => onSetStep({
220
+ kind: 'edit-profile-ens',
221
+ identity: step.identity,
222
+ registry: step.registry,
223
+ returnTo: step.returnTo,
224
+ ...(step.setup.mode === 'advanced' ? { initialView: 'advanced' as const } : {}),
225
+ })}
226
+ />
227
+ )
228
+ }
229
+
230
+ if (step.kind === 'ens-setup-records-tx') {
231
+ const signer = step.setup.mode === 'simple' ? 'Connected wallet' : 'Owner wallet'
232
+ return (
233
+ <WalletApprovalScreen
234
+ title={step.setup.mode === 'simple' ? 'Use Connected Wallet' : 'Use Owner Wallet'}
235
+ subtitle={`${signer} signs one Ethereum Mainnet resolver transaction for ${step.setup.fullName}.`}
236
+ walletSession={walletSession}
237
+ label={step.setup.mode === 'simple' ? 'waiting for connected wallet transaction...' : 'waiting for owner wallet transaction...'}
238
+ onCancel={() => onSetStep({
239
+ kind: 'edit-profile-ens',
240
+ identity: step.identity,
241
+ registry: step.registry,
242
+ returnTo: step.returnTo,
243
+ ...(step.setup.mode === 'advanced' ? { initialView: 'advanced' as const } : {}),
244
+ })}
245
+ />
246
+ )
247
+ }
248
+
249
+ const approval = publicProfileWalletApprovalView(step)
250
+ return (
251
+ <WalletApprovalScreen
252
+ title={approval.title}
253
+ subtitle={approval.subtitle}
254
+ walletSession={walletSession}
255
+ label={approval.label}
256
+ onCancel={() => onSetStep(step.returnTo ?? { kind: 'continuity-public' })}
257
+ />
258
+ )
259
+ }
260
+
261
+ function publicProfileWalletApprovalView(step: StepOf<'public-profile-signing'>): {
262
+ title: string
263
+ subtitle: React.ReactNode
264
+ label: string
265
+ } {
266
+ if (usesAdvancedSetup(step)) {
267
+ return {
268
+ title: 'Use Wallet',
269
+ subtitle: 'Sign the public profile and ERC-8004 token URI update.',
270
+ label: 'waiting for wallet signature...',
271
+ }
272
+ }
273
+ return {
274
+ title: 'Use Wallet',
275
+ subtitle: <FlowTimeline steps={EDIT_PROFILE_STEPS} current={5} />,
276
+ label: 'waiting for wallet signature...',
277
+ }
278
+ }
279
+
280
+ function usesAdvancedSetup(step: StepOf<'public-profile-signing'>): boolean {
281
+ const state = (step.identity.state ?? {}) as Record<string, unknown>
282
+ const custodyMode = step.profileUpdates?.custodyMode ?? readCustodyMode(state)
283
+ const ownerAddress = step.profileUpdates?.ownerAddress ?? readOwnerAddressField(state)
284
+ return custodyMode === 'advanced' && typeof ownerAddress === 'string' && ownerAddress.trim().length > 0
285
+ }
286
+
287
+ function isEditProfileStep(step: IdentityHubEnsStep): step is StepOf<
288
+ | 'edit-profile-name'
289
+ | 'edit-profile-description'
290
+ | 'edit-profile-image'
291
+ | 'edit-profile-review'
292
+ | 'edit-profile-ens'
293
+ > {
294
+ return step.kind === 'edit-profile-name'
295
+ || step.kind === 'edit-profile-description'
296
+ || step.kind === 'edit-profile-image'
297
+ || step.kind === 'edit-profile-review'
298
+ || step.kind === 'edit-profile-ens'
299
+ }
@@ -0,0 +1,398 @@
1
+ import React from 'react'
2
+ import { Box, Text } from 'ink'
3
+ import { getAddress, isAddress, type Address } from 'viem'
4
+ import { Surface } from '../../../../ui/Surface.js'
5
+ import { Select, type SelectOption } from '../../../../ui/Select.js'
6
+ import { Spinner } from '../../../../ui/Spinner.js'
7
+ import { theme } from '../../../../ui/theme.js'
8
+ import { useAppInput } from '../../../../app/input/AppInputProvider.js'
9
+ import { openExternalUrl } from '../../../../utils/openExternal.js'
10
+ import type { EthagentIdentity } from '../../../../storage/config.js'
11
+ import { readOwnerAddressField } from '../../../identityCompat.js'
12
+ import type { Erc8004RegistryConfig } from '../../../registry/erc8004.js'
13
+ import {
14
+ createWalletRestoreAccessChallenge,
15
+ createWalletRestoreAccessKey,
16
+ } from '../../../continuity/envelope.js'
17
+ import { requestBrowserWalletSignature, type BrowserWalletReady } from '../../../wallet/browserWallet.js'
18
+ import { FlowTimeline } from '../../components/FlowTimeline.js'
19
+ import { OPEN_BROWSER_HINT } from '../../components/WalletApprovalScreen.js'
20
+ import { readCustodyMode } from '../../model/custody.js'
21
+ import { shortAddress } from '../../model/format.js'
22
+ import type { ProfileUpdates } from '../../identityHubReducer.js'
23
+ import {
24
+ normalizeApprovedOperatorWallets,
25
+ removeApprovedOperatorWallet,
26
+ upsertApprovedOperatorWallet,
27
+ type ApprovedOperatorWalletRecord,
28
+ } from '../../operatorWallets.js'
29
+ import {
30
+ reconcileWalletSetup,
31
+ type RecordsFixPlan,
32
+ } from '../../reconciliation/index.js'
33
+
34
+ type OperatorPhase =
35
+ | { kind: 'main'; notice?: string; error?: string }
36
+ | { kind: 'signing' }
37
+
38
+ type OperatorAction =
39
+ | 'add-browser'
40
+ | 'back'
41
+ | 'remove-all'
42
+ | `remove:${string}`
43
+ | `activate:${string}`
44
+
45
+ type OperatorWalletsScreenProps = {
46
+ identity: EthagentIdentity
47
+ registry: Erc8004RegistryConfig
48
+ walletSession: BrowserWalletReady | null
49
+ notice?: string
50
+ error?: string
51
+ onSave: (updates: ProfileUpdates) => void
52
+ onWalletReady: (session: BrowserWalletReady | null) => void
53
+ onBack: () => void
54
+ }
55
+
56
+ export const OperatorWalletsScreen: React.FC<OperatorWalletsScreenProps> = ({
57
+ identity,
58
+ registry,
59
+ walletSession,
60
+ notice,
61
+ error,
62
+ onSave,
63
+ onWalletReady,
64
+ onBack,
65
+ }) => {
66
+ const state = (identity.state ?? {}) as Record<string, unknown>
67
+ const custodyMode = readCustodyMode(state)
68
+ const ownerAddressRaw = readOwnerAddressField(state)
69
+ const ownerAddress = ownerAddressRaw && isAddress(ownerAddressRaw, { strict: false }) ? getAddress(ownerAddressRaw) : undefined
70
+ const activeOperatorAddress = readStateAddress(state, 'activeOperatorAddress')
71
+ const restoreAccessEpoch = readStateNumber(state, 'restoreAccessEpoch') ?? 0
72
+ const records = normalizeApprovedOperatorWallets(state.approvedOperatorWallets)
73
+ const [phase, setPhase] = React.useState<OperatorPhase>({ kind: 'main', notice, error })
74
+ const [fixPlan, setFixPlan] = React.useState<RecordsFixPlan | null>(null)
75
+
76
+ React.useEffect(() => {
77
+ setPhase(current => current.kind === 'main' ? { kind: 'main', notice, error } : current)
78
+ }, [notice, error])
79
+
80
+ React.useEffect(() => {
81
+ if (custodyMode !== 'advanced' || records.length === 0) {
82
+ setFixPlan(null)
83
+ return
84
+ }
85
+ let cancelled = false
86
+ reconcileWalletSetup({ identity, registry })
87
+ .then(plan => { if (!cancelled) setFixPlan(plan) })
88
+ .catch(() => { if (!cancelled) setFixPlan(null) })
89
+ return () => { cancelled = true }
90
+ }, [identity, registry, custodyMode, records.length])
91
+
92
+ const driftItems = fixPlan?.items.filter(item =>
93
+ item.kind === 'missing-approval' || item.kind === 'stale-approval',
94
+ ) ?? []
95
+
96
+ const saveOperators = React.useCallback((
97
+ approvedOperatorWallets: ApprovedOperatorWalletRecord[],
98
+ activeOperator: Address | '' | undefined,
99
+ ) => {
100
+ const updates: ProfileUpdates = {
101
+ custodyMode: 'advanced',
102
+ ...(ownerAddress ? { ownerAddress } : {}),
103
+ approvedOperatorWallets,
104
+ restoreAccessEpoch: restoreAccessEpoch + 1,
105
+ }
106
+ if (activeOperator !== undefined) updates.activeOperatorAddress = activeOperator
107
+ onSave(updates)
108
+ }, [ownerAddress, onSave, restoreAccessEpoch])
109
+
110
+ const addRecord = React.useCallback((record: ApprovedOperatorWalletRecord) => {
111
+ if (!ownerAddress) {
112
+ setPhase({ kind: 'main', error: 'advanced custody needs an owner wallet before managing operator wallets' })
113
+ return
114
+ }
115
+ if (record.address.toLowerCase() === ownerAddress.toLowerCase()) {
116
+ setPhase({ kind: 'main', error: 'operator wallet must differ from the owner wallet' })
117
+ return
118
+ }
119
+ const next = upsertApprovedOperatorWallet(records, record)
120
+ const active = activeOperatorAddress ?? record.address
121
+ saveOperators(next, active)
122
+ }, [activeOperatorAddress, ownerAddress, records, saveOperators])
123
+
124
+ const startBrowserSignature = React.useCallback(() => {
125
+ if (!ownerAddress) {
126
+ setPhase({ kind: 'main', error: 'advanced custody needs an owner wallet before managing operator wallets' })
127
+ return
128
+ }
129
+ if (!identity.agentId) {
130
+ setPhase({ kind: 'main', error: 'agent token ID is required before authorizing a wallet' })
131
+ return
132
+ }
133
+ const token = restoreAccessToken(registry, identity.agentId)
134
+ const nextEpoch = restoreAccessEpoch + 1
135
+ setPhase({ kind: 'signing' })
136
+ requestBrowserWalletSignature({
137
+ chainId: registry.chainId,
138
+ purpose: 'operator-proof',
139
+ messageForAccount: account => createWalletRestoreAccessChallenge({
140
+ token,
141
+ ownerAddress: ownerAddress,
142
+ walletAddress: account,
143
+ accessEpoch: nextEpoch,
144
+ purpose: 'restore-operator',
145
+ }),
146
+ onReady: onWalletReady,
147
+ }).then(wallet => {
148
+ onWalletReady(null)
149
+ const restoreAccessKey = createWalletRestoreAccessKey({
150
+ token,
151
+ ownerAddress: ownerAddress,
152
+ walletAddress: wallet.account,
153
+ walletSignature: wallet.signature,
154
+ accessEpoch: nextEpoch,
155
+ createdAt: new Date().toISOString(),
156
+ purpose: 'restore-operator',
157
+ })
158
+ addRecord({
159
+ address: wallet.account,
160
+ challenge: wallet.message,
161
+ verifiedAt: restoreAccessKey.createdAt,
162
+ restoreAccessKey,
163
+ })
164
+ }).catch((err: unknown) => {
165
+ onWalletReady(null)
166
+ setPhase({ kind: 'main', error: err instanceof Error ? err.message : String(err) })
167
+ })
168
+ }, [addRecord, ownerAddress, identity.agentId, onWalletReady, registry, restoreAccessEpoch])
169
+
170
+ if (custodyMode !== 'advanced' || !ownerAddress) {
171
+ return (
172
+ <Surface
173
+ title="Operator Wallets"
174
+ subtitle="Advanced custody must be set up before operator wallets can be managed."
175
+ footer={footerHint('enter select · esc back')}
176
+ >
177
+ <Box flexDirection="column">
178
+ <Text color={theme.dim}>Switch to Advanced custody from the Custody Mode menu to capture an owner wallet.</Text>
179
+ <Text color={theme.dim}>Operator wallets can be changed later and never receive token control.</Text>
180
+ {phase.kind === 'main' && phase.error ? <Text color={theme.accentError}>{phase.error}</Text> : null}
181
+ </Box>
182
+ <Box marginTop={1}>
183
+ <Select<'back'>
184
+ options={[
185
+ { value: 'back', role: 'section', label: 'Navigation' },
186
+ { value: 'back', label: 'Back', hint: 'Return to the previous screen', role: 'utility' },
187
+ ]}
188
+ hintLayout="inline"
189
+ onSubmit={() => onBack()}
190
+ onCancel={onBack}
191
+ />
192
+ </Box>
193
+ </Surface>
194
+ )
195
+ }
196
+
197
+ if (phase.kind === 'signing') {
198
+ return (
199
+ <WalletWaitSurface
200
+ title="Authorize Wallet"
201
+ subtitle={
202
+ <Box flexDirection="column">
203
+ <FlowTimeline steps={['Verify Operator', 'Sign Backup', 'Publish Snapshot', 'Approve ENS', 'Approve Vault']} current={1} />
204
+ <Text color={theme.dim}>Operator wallet signs restore access. Owner wallet then re-attests the backup, publishes the new snapshot, and authorizes the operator onchain (ENS + vault).</Text>
205
+ </Box>
206
+ }
207
+ walletSession={walletSession}
208
+ onCancel={() => {
209
+ onWalletReady(null)
210
+ setPhase({ kind: 'main' })
211
+ }}
212
+ />
213
+ )
214
+ }
215
+
216
+ const options = operatorOptions({
217
+ records,
218
+ activeOperatorAddress,
219
+ })
220
+ const phaseNotice = phase.kind === 'main' ? phase.notice : undefined
221
+ const phaseError = phase.kind === 'main' ? phase.error : undefined
222
+
223
+ return (
224
+ <Surface
225
+ title="Operator Wallets"
226
+ subtitle="Owner wallet controls this list. The active operator wallet signs frequent updates; others stay authorized."
227
+ footer={footerHint('enter select · esc back')}
228
+ >
229
+ <Box flexDirection="column">
230
+ <Text>
231
+ <Text color={theme.dim}>{'Owner Wallet'.padEnd(18)}</Text>
232
+ <Text color={theme.text}>{shortAddress(ownerAddress)}</Text>
233
+ </Text>
234
+ <Box marginTop={1} flexDirection="column">
235
+ <Text color={theme.dim}>Operator Wallets</Text>
236
+ {records.length > 0
237
+ ? records.map(record => {
238
+ const isActive = record.address.toLowerCase() === activeOperatorAddress?.toLowerCase()
239
+ const approved = record.verifiedAt ? `approved ${record.verifiedAt.slice(0, 10)}` : null
240
+ const meta = [isActive ? 'active' : null, approved].filter(Boolean).join(' · ')
241
+ return (
242
+ <Text key={record.address}>
243
+ <Text color={isActive ? theme.accentPeriwinkle : theme.text}>
244
+ {shortAddress(record.address)}
245
+ </Text>
246
+ {meta ? <Text color={theme.dim}>{` ${meta}`}</Text> : null}
247
+ </Text>
248
+ )
249
+ })
250
+ : <Text color={theme.dim}>No operator wallets saved.</Text>}
251
+ </Box>
252
+ <Box marginTop={1} flexDirection="column">
253
+ <Text color={theme.dim}>Add as many operator wallets as needed; unlink any saved wallet here.</Text>
254
+ <Text color={theme.dim}>No approve(), setApprovalForAll(), transferFrom(), or token approval is requested.</Text>
255
+ </Box>
256
+ {driftItems.length > 0
257
+ ? (
258
+ <Box marginTop={1} flexDirection="column">
259
+ <Text color="#e8a070">
260
+ {`! ENS resolver drift: ${driftItems.length} operator wallet${driftItems.length === 1 ? '' : 's'} missing onchain approval.`}
261
+ </Text>
262
+ <Text color={theme.dim}>Run "Fix Records" from Custody Mode to re-sync; onchain ENS writes will fail until then.</Text>
263
+ </Box>
264
+ )
265
+ : null}
266
+ {phaseNotice ? <Text color={theme.accentPeriwinkle}>{phaseNotice}</Text> : null}
267
+ {phaseError ? <Text color={theme.accentError}>{phaseError}</Text> : null}
268
+ </Box>
269
+ <Box marginTop={1}>
270
+ <Select<OperatorAction>
271
+ options={options}
272
+ hintLayout="inline"
273
+ maxVisible={10}
274
+ onSubmit={choice => {
275
+ if (choice === 'add-browser') return startBrowserSignature()
276
+ if (choice === 'back') return onBack()
277
+ if (choice === 'remove-all') {
278
+ try {
279
+ saveOperators([], '')
280
+ } catch (err: unknown) {
281
+ setPhase({ kind: 'main', error: err instanceof Error ? err.message : String(err) })
282
+ }
283
+ return
284
+ }
285
+ if (choice.startsWith('remove:')) {
286
+ try {
287
+ const address = getAddress(choice.slice('remove:'.length))
288
+ const next = removeApprovedOperatorWallet(records, address)
289
+ const removedActive = activeOperatorAddress?.toLowerCase() === address.toLowerCase()
290
+ const nextActive = removedActive ? '' : activeOperatorAddress ?? ''
291
+ saveOperators(next, nextActive)
292
+ } catch (err: unknown) {
293
+ setPhase({ kind: 'main', error: err instanceof Error ? err.message : String(err) })
294
+ }
295
+ }
296
+ if (choice.startsWith('activate:')) {
297
+ try {
298
+ const address = getAddress(choice.slice('activate:'.length))
299
+ saveOperators(records, address)
300
+ } catch (err: unknown) {
301
+ setPhase({ kind: 'main', error: err instanceof Error ? err.message : String(err) })
302
+ }
303
+ }
304
+ }}
305
+ onCancel={onBack}
306
+ />
307
+ </Box>
308
+ </Surface>
309
+ )
310
+ }
311
+
312
+ function operatorOptions(args: {
313
+ records: ApprovedOperatorWalletRecord[]
314
+ activeOperatorAddress: Address | undefined
315
+ }): Array<SelectOption<OperatorAction>> {
316
+ const options: Array<SelectOption<OperatorAction>> = [
317
+ { value: 'add-browser', role: 'section', label: 'Operator Wallets' },
318
+ { value: 'add-browser', label: 'Add Wallet', hint: 'Connect wallet to sign restore access' },
319
+ ]
320
+ if (args.records[0]) options.push({ value: `remove:${args.records[0].address}`, role: 'section', label: 'Operator Wallets' })
321
+ for (const record of args.records) {
322
+ const active = args.activeOperatorAddress?.toLowerCase() === record.address.toLowerCase()
323
+ if (!active) {
324
+ options.push({
325
+ value: `activate:${record.address}`,
326
+ label: `Set Active: ${shortAddress(record.address)}`,
327
+ hint: 'Make this the active operator wallet (owner wallet signs)',
328
+ })
329
+ }
330
+ options.push({
331
+ value: `remove:${record.address}`,
332
+ label: `Unlink ${shortAddress(record.address)}${active ? ' (active)' : ''}`,
333
+ hint: active
334
+ ? 'Active operator wallet. Unlinking clears the active slot; pick a new active from the list.'
335
+ : 'Remove this wallet from the approved list (owner wallet signs)',
336
+ })
337
+ }
338
+ if (args.records.length > 1) {
339
+ options.push({
340
+ value: 'remove-all',
341
+ label: 'Unlink All Operator Wallets',
342
+ hint: 'Remove every approved operator wallet (owner wallet signs)',
343
+ })
344
+ }
345
+ options.push(
346
+ { value: 'back', role: 'section', label: 'Navigation' },
347
+ { value: 'back', label: 'Back', hint: 'Return to the previous screen', role: 'utility' },
348
+ )
349
+ return options
350
+ }
351
+
352
+ const WalletWaitSurface: React.FC<{
353
+ title: string
354
+ subtitle: React.ReactNode
355
+ walletSession: BrowserWalletReady | null
356
+ onCancel: () => void
357
+ }> = ({ title, subtitle, walletSession, onCancel }) => {
358
+ useAppInput((_input, key) => {
359
+ if (key.escape) onCancel()
360
+ if (key.return && walletSession?.url) {
361
+ openExternalUrl(walletSession.url)
362
+ }
363
+ })
364
+ return (
365
+ <Surface title={title} subtitle={subtitle} footer={footerHint('esc cancels')}>
366
+ <Box marginTop={1}>
367
+ <Spinner label={walletSession ? 'waiting for wallet signature...' : 'opening wallet request...'} />
368
+ </Box>
369
+ {walletSession ? (
370
+ <Box flexDirection="column">
371
+ <Text color={theme.accentPeriwinkle} underline>{walletSession.url}</Text>
372
+ <Text color={theme.dim}>{OPEN_BROWSER_HINT}</Text>
373
+ </Box>
374
+ ) : null}
375
+ </Surface>
376
+ )
377
+ }
378
+
379
+ const footerHint = (hint: string) => <Text color={theme.dim}>{hint}</Text>
380
+
381
+ function readStateNumber(state: Record<string, unknown>, key: string): number | undefined {
382
+ const value = state[key]
383
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined
384
+ }
385
+
386
+ function restoreAccessToken(registry: Erc8004RegistryConfig, agentId: string) {
387
+ return {
388
+ chainId: registry.chainId,
389
+ identityRegistryAddress: registry.identityRegistryAddress,
390
+ agentId,
391
+ }
392
+ }
393
+
394
+ function readStateAddress(state: Record<string, unknown>, key: string): Address | undefined {
395
+ const value = state[key]
396
+ if (typeof value !== 'string' || !isAddress(value, { strict: false })) return undefined
397
+ return getAddress(value)
398
+ }