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,236 @@
1
+ import type { Address } from 'viem'
2
+ import { createErc8004PublicClient } from '../../../registry/erc8004.js'
3
+ import { discoverPriorVaultFromTokenOwner, isAgentInVault } from '../../../registry/operatorVault.js'
4
+ import type { ProfileUpdates, Step } from '../../identityHubReducer.js'
5
+ import { isCustodyEditStep } from './CustodyEditFlow.js'
6
+ import { resolveOperatorVaultAddress } from './custodyEffects.js'
7
+ import type { CustodyFlowDeps } from './custodyFlowTypes.js'
8
+ import { humanOwnerAddress } from './custodyFlowHelpers.js'
9
+
10
+ export function createCustodyFlowActions({
11
+ config,
12
+ setStep,
13
+ handleStepError,
14
+ guardOwnership,
15
+ triggerRebackup,
16
+ }: CustodyFlowDeps): {
17
+ beginVaultDeposit: (currentStep: Step, returnTo: Step, profileUpdates: ProfileUpdates) => void
18
+ beginVaultUnwrap: (currentStep: Step, returnTo: Step, profileUpdates: ProfileUpdates) => void
19
+ beginWithdrawToken: (currentStep: Step, returnTo: Step) => void
20
+ beginReturnToVault: (currentStep: Step, returnTo: Step, vaultAddress: Address) => void
21
+ } {
22
+ const beginVaultDeposit = (currentStep: Step, returnTo: Step, profileUpdates: ProfileUpdates): void => {
23
+ if (!isCustodyEditStep(currentStep) || currentStep.kind !== 'custody-advanced-confirm') return
24
+ const vaultAddress = resolveOperatorVaultAddress(currentStep.identity, config?.erc8004?.operatorVaults)
25
+ const expectedOwnerForDiscovery = humanOwnerAddress(currentStep.identity)
26
+ if (!vaultAddress) {
27
+ const registry = currentStep.registry
28
+ ;(async () => {
29
+ try {
30
+ const client = createErc8004PublicClient(registry)
31
+ const probe = await discoverPriorVaultFromTokenOwner({
32
+ client,
33
+ registry: registry.identityRegistryAddress,
34
+ agentId: BigInt(currentStep.identity.agentId ?? '0'),
35
+ expectedOwner: expectedOwnerForDiscovery,
36
+ })
37
+ if (probe.found) {
38
+ const recoveredVault = probe.vaultAddress
39
+ const recoveredProfileUpdates = withVaultProfileUpdates(profileUpdates, recoveredVault)
40
+ const status = await isAgentInVault({
41
+ client,
42
+ vaultAddress: recoveredVault,
43
+ registry: registry.identityRegistryAddress,
44
+ agentId: BigInt(currentStep.identity.agentId ?? '0'),
45
+ })
46
+ if (status.inVault && status.ownerAddress?.toLowerCase() === expectedOwnerForDiscovery.toLowerCase()) {
47
+ triggerRebackup(returnTo, recoveredProfileUpdates, { vaultAddress: recoveredVault })
48
+ return
49
+ }
50
+ if (status.inVault) {
51
+ handleStepError(
52
+ new Error(`Recovered operator delegation vault ${recoveredVault} holds the token, but the vault-level depositor is ${status.ownerAddress ?? 'unknown'}, not your wallet ${expectedOwnerForDiscovery}. Mid-flow recovery requires the original depositor's wallet to call vault.unwrap.`),
53
+ { kind: 'custody-model', identity: currentStep.identity, registry, returnTo },
54
+ )
55
+ return
56
+ }
57
+ setStep({
58
+ kind: 'custody-vault-deposit-tx',
59
+ identity: currentStep.identity,
60
+ registry,
61
+ vaultAddress: recoveredVault,
62
+ profileUpdates: recoveredProfileUpdates,
63
+ returnTo,
64
+ })
65
+ return
66
+ }
67
+ } catch {
68
+ void 0
69
+ }
70
+ setStep({
71
+ kind: 'custody-vault-deploy-tx',
72
+ identity: currentStep.identity,
73
+ registry,
74
+ profileUpdates,
75
+ returnTo,
76
+ })
77
+ })()
78
+ return
79
+ }
80
+ const expectedOwner = humanOwnerAddress(currentStep.identity)
81
+ const registry = currentStep.registry
82
+ ;(async () => {
83
+ try {
84
+ const client = createErc8004PublicClient(registry)
85
+ const vaultProfileUpdates = withVaultProfileUpdates(profileUpdates, vaultAddress)
86
+ const status = await isAgentInVault({
87
+ client,
88
+ vaultAddress,
89
+ registry: registry.identityRegistryAddress,
90
+ agentId: BigInt(currentStep.identity.agentId ?? '0'),
91
+ })
92
+ if (status.inVault && status.ownerAddress?.toLowerCase() === expectedOwner.toLowerCase()) {
93
+ triggerRebackup(returnTo, vaultProfileUpdates, { vaultAddress })
94
+ return
95
+ }
96
+ if (status.inVault) {
97
+ handleStepError(
98
+ new Error(`Token is held by the operator delegation vault, but the vault-level owner is ${status.ownerAddress ?? 'unknown'}, not your wallet ${expectedOwner}. Recovery requires that wallet to call vault.unwrap.`),
99
+ { kind: 'custody-model', identity: currentStep.identity, registry, returnTo },
100
+ )
101
+ return
102
+ }
103
+ setStep({
104
+ kind: 'custody-vault-deposit-tx',
105
+ identity: currentStep.identity,
106
+ registry,
107
+ vaultAddress,
108
+ profileUpdates: vaultProfileUpdates,
109
+ returnTo,
110
+ })
111
+ } catch (err: unknown) {
112
+ setStep({
113
+ kind: 'custody-vault-deposit-tx',
114
+ identity: currentStep.identity,
115
+ registry,
116
+ vaultAddress,
117
+ profileUpdates: withVaultProfileUpdates(profileUpdates, vaultAddress),
118
+ returnTo,
119
+ })
120
+ void err
121
+ }
122
+ })()
123
+ }
124
+
125
+ const beginWithdrawToken = (currentStep: Step, returnTo: Step): void => {
126
+ if (!isCustodyEditStep(currentStep)) return
127
+ const vaultAddress = resolveOperatorVaultAddress(currentStep.identity, config?.erc8004?.operatorVaults)
128
+ if (!vaultAddress) {
129
+ handleStepError(
130
+ new Error('No agent vault is recorded for this identity. There is nothing to withdraw.'),
131
+ { kind: 'custody-model', identity: currentStep.identity, registry: currentStep.registry, returnTo },
132
+ )
133
+ return
134
+ }
135
+ const depositor = humanOwnerAddress(currentStep.identity)
136
+ const activeAgentId = currentStep.identity.agentId
137
+ if (!activeAgentId) {
138
+ handleStepError(
139
+ new Error('This identity does not have an ERC-8004 token ID. There is nothing to withdraw.'),
140
+ { kind: 'custody-model', identity: currentStep.identity, registry: currentStep.registry, returnTo },
141
+ )
142
+ return
143
+ }
144
+ setStep({
145
+ kind: 'custody-vault-withdraw-discovering',
146
+ identity: currentStep.identity,
147
+ registry: currentStep.registry,
148
+ vaultAddress,
149
+ returnTo,
150
+ })
151
+ ;(async () => {
152
+ const client = createErc8004PublicClient(currentStep.registry)
153
+ try {
154
+ const status = await isAgentInVault({
155
+ client,
156
+ vaultAddress,
157
+ registry: currentStep.registry.identityRegistryAddress,
158
+ agentId: BigInt(activeAgentId),
159
+ })
160
+ if (status.inVault) {
161
+ if (status.ownerAddress && status.ownerAddress.toLowerCase() !== depositor.toLowerCase()) {
162
+ handleStepError(
163
+ new Error(`Agent vault holds token #${activeAgentId} but recorded the depositor as ${status.ownerAddress}, not your wallet ${depositor}. Only the original depositor can withdraw.`),
164
+ { kind: 'custody-model', identity: currentStep.identity, registry: currentStep.registry, returnTo },
165
+ )
166
+ return
167
+ }
168
+ setStep({
169
+ kind: 'custody-vault-withdraw-tx',
170
+ identity: currentStep.identity,
171
+ registry: currentStep.registry,
172
+ vaultAddress,
173
+ agentId: activeAgentId,
174
+ returnTo,
175
+ })
176
+ return
177
+ }
178
+ handleStepError(
179
+ new Error(`Token #${activeAgentId} is not currently in the agent vault. There is nothing to withdraw; the token is already with the owner wallet.`),
180
+ { kind: 'custody-model', identity: currentStep.identity, registry: currentStep.registry, returnTo },
181
+ )
182
+ } catch (err: unknown) {
183
+ handleStepError(err, { kind: 'custody-model', identity: currentStep.identity, registry: currentStep.registry, returnTo })
184
+ }
185
+ })()
186
+ }
187
+
188
+ const beginVaultUnwrap = (currentStep: Step, returnTo: Step, profileUpdates: ProfileUpdates): void => {
189
+ if (!isCustodyEditStep(currentStep) || currentStep.kind !== 'custody-simple-confirm') return
190
+ const vaultAddress = resolveOperatorVaultAddress(currentStep.identity, config?.erc8004?.operatorVaults)
191
+ if (!vaultAddress) {
192
+ triggerRebackup(returnTo, profileUpdates, { useVault: false })
193
+ return
194
+ }
195
+ ;(async () => {
196
+ const allowed = await guardOwnership(currentStep.identity, currentStep.registry, 'vault-level-owner', returnTo)
197
+ if (!allowed) return
198
+ const agentIds = currentStep.identity.agentId ? [currentStep.identity.agentId] : []
199
+ setStep({
200
+ kind: 'custody-vault-unwrap-tx',
201
+ identity: currentStep.identity,
202
+ registry: currentStep.registry,
203
+ vaultAddress,
204
+ profileUpdates,
205
+ returnTo,
206
+ agentIds,
207
+ })
208
+ })()
209
+ }
210
+
211
+ const beginReturnToVault = (currentStep: Step, returnTo: Step, vaultAddress: Address): void => {
212
+ if (!isCustodyEditStep(currentStep)) return
213
+ setStep({
214
+ kind: 'custody-vault-deposit-tx',
215
+ identity: currentStep.identity,
216
+ registry: currentStep.registry,
217
+ vaultAddress,
218
+ profileUpdates: withVaultProfileUpdates({}, vaultAddress),
219
+ returnTo,
220
+ })
221
+ }
222
+
223
+ return {
224
+ beginVaultDeposit,
225
+ beginVaultUnwrap,
226
+ beginWithdrawToken,
227
+ beginReturnToVault,
228
+ }
229
+ }
230
+
231
+ function withVaultProfileUpdates(profileUpdates: ProfileUpdates, vaultAddress: Address): ProfileUpdates {
232
+ return {
233
+ ...profileUpdates,
234
+ operatorVaultAddress: vaultAddress,
235
+ }
236
+ }
@@ -0,0 +1,163 @@
1
+ import { useEffect } from 'react'
2
+ import { createErc8004PublicClient } from '../../../registry/erc8004.js'
3
+ import { confirmAgentInVault } from '../../../registry/operatorVault.js'
4
+ import { invalidateOwnershipCache } from '../../reconciliation/index.js'
5
+ import type { ProfileUpdates } from '../../identityHubReducer.js'
6
+ import type { CustodyFlowDeps } from './custodyFlowTypes.js'
7
+ import { humanOwnerAddress } from './custodyFlowHelpers.js'
8
+ import {
9
+ runVaultDeployTransaction,
10
+ runVaultDepositTransaction,
11
+ runVaultUnwrapTransaction,
12
+ runVaultWithdrawTransaction,
13
+ } from './custodyEffects.js'
14
+
15
+ export function useCustodyTransactionEffects({
16
+ step,
17
+ setStep,
18
+ callbacks,
19
+ handleStepError,
20
+ triggerRebackup,
21
+ refreshReconciliation,
22
+ }: CustodyFlowDeps): void {
23
+ useEffect(() => {
24
+ if (step.kind !== 'custody-vault-deploy-tx') return
25
+ let cancelled = false
26
+ if (!step.identity.agentId) {
27
+ handleStepError(
28
+ new Error('Cannot deploy agent vault: agent token ID is missing'),
29
+ { kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo },
30
+ )
31
+ return () => { cancelled = true }
32
+ }
33
+ runVaultDeployTransaction({
34
+ registry: step.registry,
35
+ walletAddress: humanOwnerAddress(step.identity),
36
+ agentId: BigInt(step.identity.agentId),
37
+ callbacks,
38
+ flowId: 'advanced-custody',
39
+ })
40
+ .then(async ({ vaultAddress }) => {
41
+ if (cancelled) return
42
+ setStep({
43
+ kind: 'custody-vault-deposit-tx',
44
+ identity: step.identity,
45
+ registry: step.registry,
46
+ vaultAddress,
47
+ profileUpdates: withVaultProfileUpdates(step.profileUpdates, vaultAddress),
48
+ returnTo: step.returnTo,
49
+ })
50
+ })
51
+ .catch((err: unknown) => {
52
+ if (cancelled) return
53
+ handleStepError(err, { kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })
54
+ })
55
+ return () => { cancelled = true }
56
+ }, [step])
57
+
58
+ useEffect(() => {
59
+ if (step.kind !== 'custody-vault-deposit-tx') return
60
+ let cancelled = false
61
+ runVaultDepositTransaction({
62
+ identity: step.identity,
63
+ registry: step.registry,
64
+ vaultAddress: step.vaultAddress,
65
+ callbacks,
66
+ flowId: 'advanced-custody',
67
+ })
68
+ .then(async () => {
69
+ if (cancelled) return
70
+ invalidateOwnershipCache()
71
+ refreshReconciliation()
72
+ const probeClient = createErc8004PublicClient(step.registry)
73
+ const expectedOwner = humanOwnerAddress(step.identity)
74
+ const status = await confirmAgentInVault({
75
+ client: probeClient,
76
+ vaultAddress: step.vaultAddress,
77
+ registry: step.registry.identityRegistryAddress,
78
+ agentId: BigInt(step.identity.agentId ?? '0'),
79
+ })
80
+ if (cancelled) return
81
+ if (status.ownerAddress.toLowerCase() !== expectedOwner.toLowerCase()) {
82
+ throw new Error(
83
+ `Vault holds the token but recorded the depositor as ${status.ownerAddress} instead of ${expectedOwner}. Use Withdraw Token to recover.`,
84
+ )
85
+ }
86
+ triggerRebackup(step.returnTo ?? { kind: 'menu' }, step.profileUpdates, { vaultAddress: step.vaultAddress })
87
+ })
88
+ .catch((err: unknown) => {
89
+ if (cancelled) return
90
+ handleStepError(err, { kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })
91
+ })
92
+ return () => { cancelled = true }
93
+ }, [step])
94
+
95
+ useEffect(() => {
96
+ if (step.kind !== 'custody-vault-withdraw-tx') return
97
+ let cancelled = false
98
+ runVaultWithdrawTransaction({
99
+ identity: step.identity,
100
+ registry: step.registry,
101
+ vaultAddress: step.vaultAddress,
102
+ callbacks,
103
+ ...(step.agentId ? { agentId: BigInt(step.agentId) } : {}),
104
+ })
105
+ .then(({ recipient }) => {
106
+ if (cancelled) return
107
+ invalidateOwnershipCache()
108
+ refreshReconciliation()
109
+ setStep({
110
+ kind: 'custody-vault-withdraw-done',
111
+ identity: step.identity,
112
+ registry: step.registry,
113
+ vaultAddress: step.vaultAddress,
114
+ recipient,
115
+ returnTo: step.returnTo,
116
+ ...(step.returnContext ? { returnContext: step.returnContext } : {}),
117
+ })
118
+ })
119
+ .catch((err: unknown) => {
120
+ if (cancelled) return
121
+ handleStepError(err, { kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })
122
+ })
123
+ return () => { cancelled = true }
124
+ }, [step])
125
+
126
+ useEffect(() => {
127
+ if (step.kind !== 'custody-vault-unwrap-tx') return
128
+ let cancelled = false
129
+ const agentIds = step.agentIds && step.agentIds.length > 0
130
+ ? step.agentIds
131
+ : (step.identity.agentId ? [step.identity.agentId] : [])
132
+ ;(async () => {
133
+ try {
134
+ for (const agentIdStr of agentIds) {
135
+ if (cancelled) return
136
+ await runVaultUnwrapTransaction({
137
+ identity: step.identity,
138
+ registry: step.registry,
139
+ vaultAddress: step.vaultAddress,
140
+ callbacks,
141
+ flowId: 'simple-custody',
142
+ agentId: BigInt(agentIdStr),
143
+ })
144
+ }
145
+ if (cancelled) return
146
+ invalidateOwnershipCache()
147
+ refreshReconciliation()
148
+ triggerRebackup(step.returnTo ?? { kind: 'menu' }, step.profileUpdates, { useVault: false })
149
+ } catch (err: unknown) {
150
+ if (cancelled) return
151
+ handleStepError(err, { kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })
152
+ }
153
+ })()
154
+ return () => { cancelled = true }
155
+ }, [step])
156
+ }
157
+
158
+ function withVaultProfileUpdates(profileUpdates: ProfileUpdates, vaultAddress: `0x${string}`): ProfileUpdates {
159
+ return {
160
+ ...profileUpdates,
161
+ operatorVaultAddress: vaultAddress,
162
+ }
163
+ }
@@ -0,0 +1,25 @@
1
+ import type { EthagentConfig, EthagentIdentity } from '../../../../storage/config.js'
2
+ import { buildSeedConfigForIdentity } from '../../../../storage/config.js'
3
+ import { readOwnerAddressField } from '../../../identityCompat.js'
4
+ import { supportedErc8004ChainForId, type Erc8004RegistryConfig } from '../../../registry/erc8004.js'
5
+
6
+ export function chainLabel(chainId: number): string {
7
+ return supportedErc8004ChainForId(chainId)?.name ?? `chain ${chainId}`
8
+ }
9
+
10
+ export function humanOwnerAddress(identity: EthagentIdentity): `0x${string}` {
11
+ const stateOwnerAddress = readOwnerAddressField(identity.state as Record<string, unknown> | undefined)
12
+ if (stateOwnerAddress && /^0x[a-fA-F0-9]{40}$/.test(stateOwnerAddress)) {
13
+ return stateOwnerAddress as `0x${string}`
14
+ }
15
+ return (identity.ownerAddress ?? identity.address) as `0x${string}`
16
+ }
17
+
18
+ export function buildSeedConfigFromStep(identity: EthagentIdentity, registry: Erc8004RegistryConfig): EthagentConfig {
19
+ return buildSeedConfigForIdentity({
20
+ identity,
21
+ chainId: registry.chainId,
22
+ rpcUrl: registry.rpcUrl,
23
+ identityRegistryAddress: registry.identityRegistryAddress,
24
+ })
25
+ }
@@ -0,0 +1,239 @@
1
+ import React from 'react'
2
+ import { Box, Text } from 'ink'
3
+ import { Surface } from '../../../../ui/Surface.js'
4
+ import { Select } from '../../../../ui/Select.js'
5
+ import { theme } from '../../../../ui/theme.js'
6
+ import { WalletApprovalScreen } from '../../components/WalletApprovalScreen.js'
7
+ import { shortAddress } from '../../model/format.js'
8
+ import type { Step } from '../../identityHubReducer.js'
9
+ import type { CustodyFlowDeps } from './custodyFlowTypes.js'
10
+ import { chainLabel, humanOwnerAddress } from './custodyFlowHelpers.js'
11
+
12
+ const Row: React.FC<{ label: string; value: string }> = ({ label, value }) => (
13
+ <Text>
14
+ <Text color={theme.dim}>{label.padEnd(18)}</Text>
15
+ <Text color={theme.text}>{value}</Text>
16
+ </Text>
17
+ )
18
+
19
+ export function renderCustodyStep({
20
+ step,
21
+ setStep,
22
+ walletSession,
23
+ }: CustodyFlowDeps): React.ReactElement | null {
24
+ if (step.kind === 'custody-vault-deploy-tx') {
25
+ return (
26
+ <WalletApprovalScreen
27
+ title="Deploy Operator Delegation Vault"
28
+ subtitle={`Deploying a dedicated OperatorVault for this ERC-8004 token on ${chainLabel(step.registry.chainId)}.`}
29
+ walletSession={walletSession}
30
+ label="waiting for owner wallet transaction..."
31
+ onCancel={() => setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
32
+ />
33
+ )
34
+ }
35
+ if (step.kind === 'custody-vault-deposit-tx') {
36
+ return (
37
+ <WalletApprovalScreen
38
+ title="Deposit Token Into Operator Delegation Vault"
39
+ subtitle={`Sign one ${chainLabel(step.registry.chainId)} transaction. Sends ERC-8004 token #${step.identity.agentId ?? ''} to its agent vault.`}
40
+ walletSession={walletSession}
41
+ label="waiting for token-owner wallet transaction..."
42
+ onCancel={() => setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
43
+ />
44
+ )
45
+ }
46
+ if (step.kind === 'custody-vault-withdraw-discovering') {
47
+ const targetAgentId = step.identity.agentId ?? ''
48
+ return (
49
+ <Surface
50
+ title="Checking Vault"
51
+ subtitle={targetAgentId
52
+ ? `Confirming the agent vault holds ERC-8004 token #${targetAgentId} on ${chainLabel(step.registry.chainId)}.`
53
+ : `Checking this identity's recorded agent vault on ${chainLabel(step.registry.chainId)}.`}
54
+ footer={<Text color={theme.dim}>esc cancel</Text>}
55
+ >
56
+ <Box marginTop={1}>
57
+ <Text color={theme.textSubtle}>Reading vault state from chain...</Text>
58
+ </Box>
59
+ </Surface>
60
+ )
61
+ }
62
+ if (step.kind === 'custody-vault-withdraw-tx') {
63
+ const targetAgentId = step.agentId ?? step.identity.agentId ?? ''
64
+ return (
65
+ <WalletApprovalScreen
66
+ title="Withdraw Token"
67
+ subtitle={`Unwraps ERC-8004 token #${targetAgentId} from its agent vault to your owner wallet on ${chainLabel(step.registry.chainId)}.`}
68
+ walletSession={walletSession}
69
+ label="waiting for owner wallet transaction..."
70
+ onCancel={() => setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
71
+ />
72
+ )
73
+ }
74
+ if (step.kind === 'custody-vault-withdraw-pick-token') {
75
+ const activeId = step.identity.agentId
76
+ const options = step.tokens.map(t => ({
77
+ value: t.agentId,
78
+ label: `Token #${t.agentId}${activeId && t.agentId === activeId ? ' (active)' : ''}`,
79
+ hint: 'Withdraw this token to your owner wallet',
80
+ }))
81
+ return (
82
+ <Surface
83
+ title="Pick a Vaulted Token"
84
+ subtitle={`Your wallet has ${step.tokens.length} vaulted tokens on ${chainLabel(step.registry.chainId)}. Pick one to withdraw.`}
85
+ footer={<Text color={theme.dim}>enter select · esc back</Text>}
86
+ >
87
+ <Box marginTop={1}>
88
+ <Select<string>
89
+ options={[
90
+ { value: 'header', role: 'section', label: 'Vaulted Tokens' },
91
+ ...options,
92
+ { value: 'cancel', role: 'section', label: 'Navigation' },
93
+ { value: 'cancel', label: 'Back', hint: 'Return to Custody Mode', role: 'utility' },
94
+ ]}
95
+ hintLayout="inline"
96
+ onSubmit={choice => {
97
+ if (choice === 'cancel') {
98
+ setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })
99
+ return
100
+ }
101
+ setStep({
102
+ kind: 'custody-vault-withdraw-tx',
103
+ identity: step.identity,
104
+ registry: step.registry,
105
+ vaultAddress: step.vaultAddress,
106
+ agentId: choice,
107
+ returnTo: step.returnTo,
108
+ ...(step.returnContext ? { returnContext: step.returnContext } : {}),
109
+ })
110
+ }}
111
+ onCancel={() => setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
112
+ />
113
+ </Box>
114
+ </Surface>
115
+ )
116
+ }
117
+ if (step.kind === 'custody-vault-withdraw-done') {
118
+ const onReturnToVault = () => {
119
+ setStep({
120
+ kind: 'custody-vault-deposit-tx',
121
+ identity: step.identity,
122
+ registry: step.registry,
123
+ vaultAddress: step.vaultAddress,
124
+ profileUpdates: { operatorVaultAddress: step.vaultAddress },
125
+ returnTo: step.returnTo,
126
+ })
127
+ }
128
+ const onKeepOut = () => {
129
+ if (step.returnContext === 'ens' && step.returnTo) {
130
+ setStep(step.returnTo)
131
+ } else {
132
+ setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })
133
+ }
134
+ }
135
+ return (
136
+ <Surface
137
+ title="Token Returned to Owner Wallet"
138
+ subtitle={`Token returned to ${shortAddress(step.recipient)} on ${chainLabel(step.registry.chainId)}. The agent vault can be reused for this token.`}
139
+ footer={<Text color={theme.dim}>enter select · esc back</Text>}
140
+ >
141
+ <Box flexDirection="column">
142
+ <Text color={theme.textSubtle}>
143
+ {step.returnContext === 'ens'
144
+ ? 'Use the token with your owner wallet to set ENS records, then return it to the vault to resume Advanced custody.'
145
+ : 'Use the token with your owner wallet for whatever you need, then return it to the vault when finished.'}
146
+ </Text>
147
+ </Box>
148
+ <Box marginTop={1}>
149
+ <Select<'return-to-vault' | 'keep-out'>
150
+ options={[
151
+ { value: 'return-to-vault', role: 'section', label: 'Resume Advanced Custody' },
152
+ { value: 'return-to-vault', label: 'Return Token to Vault', hint: 'Redeposit to this agent vault. No redeploy, no operator re-add' },
153
+ { value: 'keep-out', role: 'section', label: 'Later' },
154
+ { value: 'keep-out', label: 'Keep Out For Now', hint: 'Token stays with the owner wallet; redeposit any time from Custody Mode', role: 'utility' },
155
+ ]}
156
+ hintLayout="inline"
157
+ onSubmit={choice => {
158
+ if (choice === 'return-to-vault') onReturnToVault()
159
+ else onKeepOut()
160
+ }}
161
+ onCancel={onKeepOut}
162
+ />
163
+ </Box>
164
+ </Surface>
165
+ )
166
+ }
167
+ if (step.kind === 'custody-vault-unwrap-tx') {
168
+ return (
169
+ <WalletApprovalScreen
170
+ title="Unwrap Token From Agent Vault"
171
+ subtitle={`Sign one ${chainLabel(step.registry.chainId)} transaction. Calls the agent vault's unwrap function to return ERC-8004 token #${step.identity.agentId ?? ''} to the owner wallet.`}
172
+ walletSession={walletSession}
173
+ label="waiting for owner wallet transaction..."
174
+ onCancel={() => setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
175
+ />
176
+ )
177
+ }
178
+ if (step.kind === 'custody-advanced-done') {
179
+ const state = (step.identity.state ?? {}) as Record<string, unknown>
180
+ const ownerWallet = humanOwnerAddress(step.identity) as string
181
+ const operatorCount = Array.isArray(state.approvedOperatorWallets) ? state.approvedOperatorWallets.length : 0
182
+ return (
183
+ <Surface
184
+ title="Advanced Custody Active"
185
+ subtitle="Your token is held in its own OperatorVault. Authorized operator wallets can rotate the agent URI onchain without owner signatures."
186
+ footer={<Text color={theme.dim}>enter continues</Text>}
187
+ >
188
+ <Box flexDirection="column">
189
+ {step.vaultAddress ? <Row label="Agent Vault" value={shortAddress(step.vaultAddress)} /> : null}
190
+ <Row label="Owner Wallet" value={shortAddress(ownerWallet)} />
191
+ <Row label="Operator Wallets" value={operatorCount === 1 ? '1 approved' : `${operatorCount} approved`} />
192
+ <Box marginTop={1}>
193
+ <Text color={theme.textSubtle}>Use Manage Operators to add or revoke operator wallets. Use Withdraw Token to pull this token out of its vault temporarily without dismantling advanced custody.</Text>
194
+ </Box>
195
+ </Box>
196
+ <Box marginTop={1}>
197
+ <Select<'continue'>
198
+ options={[{ value: 'continue', label: 'Return to Custody Mode' }]}
199
+ onSubmit={() => setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
200
+ onCancel={() => setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
201
+ />
202
+ </Box>
203
+ </Surface>
204
+ )
205
+ }
206
+ if (step.kind === 'custody-simple-done') {
207
+ const ownerWallet = humanOwnerAddress(step.identity) as string
208
+ return (
209
+ <Surface
210
+ title="Simple Custody Active"
211
+ subtitle={`The token is back with the owner wallet. Operator slots are cleared.`}
212
+ footer={<Text color={theme.dim}>enter continues</Text>}
213
+ >
214
+ <Box flexDirection="column">
215
+ <Row label="Owner Wallet" value={shortAddress(ownerWallet)} />
216
+ <Box marginTop={1}>
217
+ <Text color={theme.textSubtle}>Future URI rotations require an owner wallet signature per edit. Switch back to Advanced from Custody Mode at any time.</Text>
218
+ </Box>
219
+ </Box>
220
+ <Box marginTop={1}>
221
+ <Select<'continue'>
222
+ options={[{ value: 'continue', label: 'Return to Custody Mode' }]}
223
+ onSubmit={() => setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
224
+ onCancel={() => setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
225
+ />
226
+ </Box>
227
+ </Surface>
228
+ )
229
+ }
230
+ return null
231
+ }
232
+
233
+ export function renderRebackupSubtitle(
234
+ defaultSubtitle: React.ReactNode,
235
+ vaultRouted: boolean,
236
+ ): React.ReactNode {
237
+ if (!vaultRouted) return defaultSubtitle
238
+ return <Text color={theme.textSubtle}>{defaultSubtitle} Routed through this token's agent vault.</Text>
239
+ }