ethagent 1.1.2 → 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 (268) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +126 -30
  3. package/package.json +7 -2
  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,347 @@
1
+ import React from 'react'
2
+ import type { Address } from 'viem'
3
+ import { Box, Text } from 'ink'
4
+ import { Surface } from '../../../../ui/Surface.js'
5
+ import { Select } from '../../../../ui/Select.js'
6
+ import { theme } from '../../../../ui/theme.js'
7
+ import type { ProfileUpdates, Step } from '../../identityHubReducer.js'
8
+ import {
9
+ displayCustodyMode,
10
+ identityOwnerAddress,
11
+ readCustodyMode,
12
+ readIdentityStateString,
13
+ } from '../../model/custody.js'
14
+ import { ensValidationReasonText, selectEnsStatus } from '../../model/ens.js'
15
+ import { shortAddress } from '../../model/format.js'
16
+ import { lastBackupLabel } from '../../model/identity.js'
17
+ import {
18
+ describeFixPlanItem,
19
+ fixPlanRequiresOwnerWallet,
20
+ reconcileWalletSetup,
21
+ type AgentReconciliation,
22
+ type RecordsFixPlan,
23
+ } from '../../reconciliation/index.js'
24
+
25
+ const footerHint = (hint: string) => <Text color={theme.dim}>{hint}</Text>
26
+
27
+ type CustodyStep = Extract<Step, { kind: 'custody-model' | 'custody-advanced-confirm' | 'custody-simple-confirm' }>
28
+
29
+ interface CustodyEditFlowProps {
30
+ step: CustodyStep
31
+ reconciliation?: AgentReconciliation
32
+ vaultAddress?: Address
33
+ onSetStep: (step: Step) => void
34
+ onSwitchToAdvanced: (returnTo: Step, profileUpdates: ProfileUpdates) => void
35
+ onSwitchToSimple: (returnTo: Step, profileUpdates: ProfileUpdates) => void
36
+ onWithdrawToken: (returnTo: Step) => void
37
+ onReturnToVault: (returnTo: Step, vaultAddress: Address) => void
38
+ onResumeAdvanced: (returnTo: Step) => void
39
+ onManageOperatorWallets: () => void
40
+ onFixRecords: (plan: RecordsFixPlan) => void
41
+ onPrepareTransfer: () => void
42
+ onBack: () => void
43
+ }
44
+
45
+ export function isCustodyEditStep(step: Step): step is CustodyStep {
46
+ return step.kind === 'custody-model'
47
+ || step.kind === 'custody-advanced-confirm'
48
+ || step.kind === 'custody-simple-confirm'
49
+ }
50
+
51
+ export const CustodyEditFlow: React.FC<CustodyEditFlowProps> = ({
52
+ step,
53
+ reconciliation,
54
+ vaultAddress,
55
+ onSetStep,
56
+ onSwitchToAdvanced,
57
+ onSwitchToSimple,
58
+ onWithdrawToken,
59
+ onReturnToVault,
60
+ onResumeAdvanced,
61
+ onManageOperatorWallets,
62
+ onFixRecords,
63
+ onPrepareTransfer,
64
+ onBack,
65
+ }) => {
66
+ const identity = step.identity
67
+ const registry = step.registry
68
+ const returnTo = step.returnTo
69
+ const state = (identity.state ?? {}) as Record<string, unknown>
70
+ const custodyMode = readCustodyMode(state)
71
+ const ownerAddress = identityOwnerAddress(identity, reconciliation?.onChainOwner)
72
+ const activeOperator = readIdentityStateString(state, 'activeOperatorAddress')
73
+ const approvedOperatorCount = Array.isArray(state.approvedOperatorWallets)
74
+ ? (state.approvedOperatorWallets as unknown[]).length
75
+ : 0
76
+ const agentName = readIdentityStateString(state, 'name')
77
+ const tokenLabel = identity.agentId ? `Token #${identity.agentId}` : 'Token #unknown'
78
+ const tokenOwner = identity.ownerAddress ?? identity.address
79
+
80
+ const [fixPlan, setFixPlan] = React.useState<RecordsFixPlan | null>(null)
81
+ React.useEffect(() => {
82
+ if (step.kind !== 'custody-model') return
83
+ if (custodyMode !== 'advanced') return
84
+ let cancelled = false
85
+ reconcileWalletSetup({ identity, registry })
86
+ .then(plan => { if (!cancelled) setFixPlan(plan) })
87
+ .catch(() => { if (!cancelled) setFixPlan(null) })
88
+ return () => { cancelled = true }
89
+ }, [identity, registry, step.kind, custodyMode])
90
+
91
+ if (step.kind === 'custody-model') {
92
+ type Action = 'switch-advanced' | 'switch-simple' | 'resume-advanced' | 'cancel-advanced' | 'withdraw-token' | 'return-to-vault' | 'manage-operator-wallets' | 'fix-records' | 'back'
93
+ const onChainCustody = reconciliation?.custody
94
+ const midFlow = onChainCustody === 'mid-flow-uri-pending'
95
+ const isAdvanced = onChainCustody === 'advanced' || midFlow || custodyMode === 'advanced'
96
+ const vaultHolds = onChainCustody === 'advanced' || midFlow
97
+ const subtitle = midFlow
98
+ ? 'Advanced setup pending. This agent vault holds your token. Finish by publishing the first onchain update.'
99
+ : isAdvanced
100
+ ? 'Advanced is active. Authorized operator wallets publish updates for this agent without an owner signature each time.'
101
+ : 'Simple is active. One wallet owns the token and signs every update.'
102
+ const modeLabel = midFlow ? 'Advanced (setup pending)' : displayCustodyMode(isAdvanced ? 'advanced' : 'simple')
103
+ const options: Array<{ value: Action; role?: 'section' | 'utility'; label: string; hint?: string }> = []
104
+ if (midFlow) {
105
+ options.push({ value: 'resume-advanced', role: 'section', label: 'Resume Setup' })
106
+ options.push({
107
+ value: 'resume-advanced',
108
+ label: 'Resume Advanced Setup',
109
+ hint: 'Sign once to publish onchain and finish the agent-vault switch.',
110
+ })
111
+ options.push({
112
+ value: 'cancel-advanced',
113
+ label: 'Cancel Advanced Setup',
114
+ hint: 'Unwrap the token back to the owner wallet and revert to simple.',
115
+ })
116
+ }
117
+ options.push({ value: 'switch-advanced', role: 'section', label: 'Custody' })
118
+ if (!isAdvanced) {
119
+ options.push({
120
+ value: 'switch-advanced',
121
+ label: 'Switch to Advanced',
122
+ hint: 'Deposit this token into its own OperatorVault so operator wallets can publish updates onchain.',
123
+ })
124
+ } else {
125
+ if (!midFlow) {
126
+ options.push({
127
+ value: 'switch-simple',
128
+ label: 'Switch to Simple',
129
+ hint: 'Unwrap the token and revoke operator delegations.',
130
+ })
131
+ }
132
+ if (vaultHolds) {
133
+ options.push({
134
+ value: 'withdraw-token',
135
+ label: 'Withdraw Token',
136
+ hint: 'Unwrap this token to the owner wallet. Vault setup stays for easy redeposit.',
137
+ })
138
+ } else if (vaultAddress) {
139
+ options.push({
140
+ value: 'return-to-vault',
141
+ label: 'Return Token to Vault',
142
+ hint: 'Redeposit this token to its agent vault. No redeploy, no operator re-add.',
143
+ })
144
+ }
145
+ options.push({ value: 'manage-operator-wallets', role: 'section', label: 'Operators' })
146
+ options.push({
147
+ value: 'manage-operator-wallets',
148
+ label: 'Manage Operators',
149
+ hint: 'Add or revoke wallets that can publish updates onchain.',
150
+ })
151
+ }
152
+ const hasFixablePlan = fixPlan !== null && fixPlanRequiresOwnerWallet(fixPlan)
153
+ if (hasFixablePlan) {
154
+ options.push({ value: 'fix-records', role: 'section', label: 'Records Out Of Sync' })
155
+ options.push({
156
+ value: 'fix-records',
157
+ label: 'Fix Records (Owner Wallet)',
158
+ hint: 'Sync ENS resolver approvals with the operator wallet list.',
159
+ })
160
+ }
161
+ options.push({ value: 'back', role: 'section', label: 'Navigation' })
162
+ options.push({ value: 'back', label: 'Back', hint: 'Return to Identity Hub', role: 'utility' })
163
+ const notice = step.kind === 'custody-model' ? step.notice : undefined
164
+ return (
165
+ <Surface title="Custody Mode" subtitle={subtitle} footer={footerHint('enter select · esc back')}>
166
+ {notice ? (
167
+ <Box marginBottom={1}>
168
+ <Text color={theme.accentPeriwinkle}>{notice}</Text>
169
+ </Box>
170
+ ) : null}
171
+ <Box flexDirection="column">
172
+ {(() => {
173
+ const ensStatus = selectEnsStatus(identity)
174
+ return (
175
+ <Text>
176
+ <Text color={theme.dim}>{'ENS'.padEnd(14)}</Text>
177
+ {ensStatus.kind === 'linked'
178
+ ? <Text color={theme.accentPeriwinkle}>{ensStatus.name}</Text>
179
+ : ensStatus.kind === 'issue'
180
+ ? <Text color={theme.accentError}>{ensStatus.name} ({ensValidationReasonText(ensStatus.reason)})</Text>
181
+ : <Text color={theme.dim}>Not Linked</Text>}
182
+ </Text>
183
+ )
184
+ })()}
185
+ <Row label="Custody" value={modeLabel} />
186
+ <Row label="Owner" value={shortAddress(ownerAddress || tokenOwner)} />
187
+ {isAdvanced && vaultAddress ? <Row label="Agent Vault" value={shortAddress(vaultAddress)} /> : null}
188
+ {isAdvanced ? (
189
+ <Row
190
+ label="Operators"
191
+ value={approvedOperatorCount > 1
192
+ ? `${approvedOperatorCount} authorized${activeOperator ? ` (active ${shortAddress(activeOperator)})` : ''}`
193
+ : activeOperator
194
+ ? shortAddress(activeOperator)
195
+ : 'None Authorized'}
196
+ muted={!activeOperator && approvedOperatorCount === 0}
197
+ />
198
+ ) : null}
199
+ {(() => {
200
+ const lastBackup = lastBackupLabel(identity)
201
+ return <Row label="Last Saved" value={lastBackup} muted={lastBackup === 'never'} />
202
+ })()}
203
+ </Box>
204
+ {fixPlan && fixPlan.items.length > 0 ? (
205
+ <Box marginTop={1} flexDirection="column">
206
+ <Text color={theme.accentPeriwinkle} bold>Records out of sync:</Text>
207
+ {fixPlan.items.map((item, idx) => (
208
+ <Text key={idx} color={theme.dim}>· {describeFixPlanItem(item)}</Text>
209
+ ))}
210
+ </Box>
211
+ ) : null}
212
+ <Box marginTop={1}>
213
+ <Select<Action>
214
+ options={options}
215
+ hintLayout="inline"
216
+ onSubmit={choice => {
217
+ if (choice === 'back') return onBack()
218
+ if (choice === 'manage-operator-wallets') return onManageOperatorWallets()
219
+ if (choice === 'withdraw-token') return onWithdrawToken(returnTo ?? { kind: 'menu' })
220
+ if (choice === 'return-to-vault') {
221
+ if (!vaultAddress) return
222
+ return onReturnToVault(returnTo ?? { kind: 'menu' }, vaultAddress)
223
+ }
224
+ if (choice === 'resume-advanced') return onResumeAdvanced(returnTo ?? { kind: 'menu' })
225
+ if (choice === 'cancel-advanced') {
226
+ onSetStep({ kind: 'custody-simple-confirm', identity, registry, returnTo })
227
+ return
228
+ }
229
+ if (choice === 'fix-records') {
230
+ if (fixPlan) onFixRecords(fixPlan)
231
+ return
232
+ }
233
+ if (choice === 'switch-advanced') {
234
+ onSetStep({ kind: 'custody-advanced-confirm', identity, registry, returnTo })
235
+ return
236
+ }
237
+ if (choice === 'switch-simple') {
238
+ onSetStep({ kind: 'custody-simple-confirm', identity, registry, returnTo })
239
+ return
240
+ }
241
+ }}
242
+ onCancel={onBack}
243
+ />
244
+ </Box>
245
+ </Surface>
246
+ )
247
+ }
248
+
249
+ if (step.kind === 'custody-advanced-confirm') {
250
+ type Action = 'confirm' | 'transfer' | 'back'
251
+ return (
252
+ <Surface
253
+ title="Switch to Advanced"
254
+ subtitle="Move this token into its own OperatorVault so authorized operator wallets can update this agent onchain without your signature each time."
255
+ footer={footerHint('enter confirm, esc back')}
256
+ >
257
+ <Box flexDirection="column">
258
+ <Row label="Token" value={tokenLabel} />
259
+ {agentName ? <Row label="Name" value={agentName} /> : null}
260
+ <Row label="Owner Wallet" value={shortAddress(ownerAddress || tokenOwner)} />
261
+ <Text color={theme.textSubtle}>You sign once now to deposit token #{identity.agentId ?? 'unknown'} into a dedicated OperatorVault.</Text>
262
+ <Text color={theme.textSubtle}>This vault can hold only this ERC-8004 token.</Text>
263
+ <Text color={theme.textSubtle}>Other agent tokens use their own vaults.</Text>
264
+ <Text color={theme.textSubtle}>After that, operator wallets you authorize can publish updates for this agent.</Text>
265
+ <Box marginTop={1} flexDirection="column">
266
+ <Text color={theme.accentBlue}>Want a different wallet to be the owner?</Text>
267
+ <Text color={theme.textSubtle}>Move the token there first via Prepare Token Transfer; your continuity files come along.</Text>
268
+ </Box>
269
+ </Box>
270
+ <Box marginTop={1}>
271
+ <Select<Action>
272
+ options={[
273
+ { value: 'confirm', role: 'section', label: 'Confirm' },
274
+ { value: 'confirm', label: 'Yes, Switch to Advanced', hint: `Sign with ${shortAddress(ownerAddress || tokenOwner)} to deposit this token into its agent vault` },
275
+ { value: 'transfer', role: 'section', label: 'Move Token First' },
276
+ { value: 'transfer', label: 'Prepare Token Transfer', hint: 'Move the token to a different wallet first, with snapshot handoff' },
277
+ { value: 'back', role: 'section', label: 'Cancel' },
278
+ { value: 'back', label: 'No, Go Back', hint: 'Return without changing custody', role: 'utility' },
279
+ ]}
280
+ hintLayout="inline"
281
+ onSubmit={choice => {
282
+ if (choice === 'back') return onBack()
283
+ if (choice === 'transfer') return onPrepareTransfer()
284
+ const updates: ProfileUpdates = {
285
+ custodyMode: 'advanced',
286
+ ownerAddress: ownerAddress || tokenOwner,
287
+ bumpRestoreAccessEpoch: true,
288
+ custodyPhase: 'switch-advanced',
289
+ }
290
+ onSwitchToAdvanced(returnTo ?? { kind: 'menu' }, updates)
291
+ }}
292
+ onCancel={onBack}
293
+ />
294
+ </Box>
295
+ </Surface>
296
+ )
297
+ }
298
+
299
+ type Action = 'confirm' | 'back'
300
+ return (
301
+ <Surface
302
+ title="Switch to Simple"
303
+ subtitle="Unwraps this ERC-8004 token from its agent vault and returns it directly to the owner wallet."
304
+ footer={footerHint('enter confirm · esc back')}
305
+ >
306
+ <Box flexDirection="column">
307
+ <Row label="Token" value={tokenLabel} />
308
+ {agentName ? <Row label="Name" value={agentName} /> : null}
309
+ <Text> </Text>
310
+ <Text color={theme.accentBlue}>Operators lose decrypt access on future snapshots immediately.</Text>
311
+ <Text color={theme.textSubtle}>Operator approvals are cleared from local state for future snapshots. Revoke onchain via Manage Operators first if needed.</Text>
312
+ <Text color={theme.textSubtle}>This switch calls the agent vault's unwrap function for this token, so the owner wallet must sign the transaction.</Text>
313
+ </Box>
314
+ <Box marginTop={1}>
315
+ <Select<Action>
316
+ options={[
317
+ { value: 'confirm', role: 'section', label: 'Confirm' },
318
+ { value: 'confirm', label: 'Yes, Switch to Simple', hint: `Sign with the owner wallet to unwrap ${tokenLabel} from its agent vault` },
319
+ { value: 'back', role: 'section', label: 'Cancel' },
320
+ { value: 'back', label: 'No, Go Back', hint: 'Return without changing custody', role: 'utility' },
321
+ ]}
322
+ hintLayout="inline"
323
+ onSubmit={choice => {
324
+ if (choice === 'back') return onBack()
325
+ const updates: ProfileUpdates = {
326
+ custodyMode: 'simple',
327
+ bumpRestoreAccessEpoch: true,
328
+ custodyPhase: 'switch-simple',
329
+ approvedOperatorWallets: [],
330
+ activeOperatorAddress: '',
331
+ operatorVaultAddress: '',
332
+ }
333
+ onSwitchToSimple(returnTo ?? { kind: 'menu' }, updates)
334
+ }}
335
+ onCancel={onBack}
336
+ />
337
+ </Box>
338
+ </Surface>
339
+ )
340
+ }
341
+
342
+ const Row: React.FC<{ label: string; value: string; muted?: boolean }> = ({ label, value, muted }) => (
343
+ <Text>
344
+ <Text color={theme.dim}>{label.padEnd(14)}</Text>
345
+ <Text color={muted ? theme.dim : theme.text}>{value}</Text>
346
+ </Text>
347
+ )
@@ -0,0 +1,321 @@
1
+ import { encodeDeployData, getAddress, type Address, type Hex, type PublicClient } from 'viem'
2
+ import {
3
+ confirmAgentWithdrawnFromVault,
4
+ encodeDepositAgent,
5
+ encodeUnwrapAgent,
6
+ isAgentInVault,
7
+ resolveConfiguredOperatorVaultAddress,
8
+ OPERATOR_VAULT_ABI,
9
+ OPERATOR_VAULT_DEPLOY_BYTECODE,
10
+ assertVaultBytecode,
11
+ } from '../../../registry/operatorVault.js'
12
+ import {
13
+ createErc8004PublicClient,
14
+ type Erc8004RegistryConfig,
15
+ } from '../../../registry/erc8004.js'
16
+ import type { EthagentIdentity } from '../../../../storage/config.js'
17
+ import { readOperatorVaultAddressField, readOwnerAddressField } from '../../../identityCompat.js'
18
+ import { prepareTransactionGasFee, sendBrowserWalletTransaction } from '../../../wallet/browserWallet.js'
19
+ import { acquireTxGuard, releaseTxGuard, type TxGuardKind } from '../../txGuard.js'
20
+ import { awaitConfirmedReceipt } from '../../effects/receipts.js'
21
+ import type { EffectCallbacks } from '../../effects/types.js'
22
+ import { readCustodyMode } from '../../model/custody.js'
23
+
24
+ export function resolveOperatorVaultAddress(
25
+ identity: EthagentIdentity,
26
+ operatorVaults?: Readonly<Record<string, string>>,
27
+ ): Address | undefined {
28
+ const identityVault = readOperatorVaultAddressField(identity.state as Record<string, unknown> | undefined)
29
+ if (identityVault) return getAddress(identityVault)
30
+ if (readCustodyMode(identity.state as Record<string, unknown> | undefined) !== 'advanced') return undefined
31
+ if (!identity.chainId) return undefined
32
+ return resolveConfiguredOperatorVaultAddress(operatorVaults, identity.chainId)
33
+ }
34
+
35
+ async function withTxGuard<T>(kind: TxGuardKind, fn: () => Promise<T>): Promise<T> {
36
+ acquireTxGuard(kind)
37
+ try {
38
+ return await fn()
39
+ } finally {
40
+ releaseTxGuard(kind)
41
+ }
42
+ }
43
+
44
+ export async function runVaultDeployTransaction(args: {
45
+ registry: Erc8004RegistryConfig
46
+ walletAddress: Address
47
+ agentId: bigint
48
+ callbacks: EffectCallbacks
49
+ publicClient?: Pick<PublicClient, 'waitForTransactionReceipt' | 'getBytecode'>
50
+ flowId?: string
51
+ }): Promise<{ txHash: Hex; vaultAddress: Address }> {
52
+ return withTxGuard('vault-deploy', () => runVaultDeployTransactionInner(args))
53
+ }
54
+
55
+ async function runVaultDeployTransactionInner(args: {
56
+ registry: Erc8004RegistryConfig
57
+ walletAddress: Address
58
+ agentId: bigint
59
+ callbacks: EffectCallbacks
60
+ publicClient?: Pick<PublicClient, 'waitForTransactionReceipt' | 'getBytecode'>
61
+ flowId?: string
62
+ }): Promise<{ txHash: Hex; vaultAddress: Address }> {
63
+ const walletAddress = getAddress(args.walletAddress)
64
+ const registryAddress = getAddress(args.registry.identityRegistryAddress)
65
+ const deployData = encodeDeployData({
66
+ abi: OPERATOR_VAULT_ABI,
67
+ bytecode: OPERATOR_VAULT_DEPLOY_BYTECODE,
68
+ args: [registryAddress, args.agentId],
69
+ })
70
+ const gasFeeClient = createErc8004PublicClient(args.registry)
71
+ const gasFee = await prepareTransactionGasFee({
72
+ client: gasFeeClient,
73
+ account: walletAddress,
74
+ data: deployData,
75
+ })
76
+ const result = await sendBrowserWalletTransaction({
77
+ chainId: args.registry.chainId,
78
+ expectedAccount: walletAddress,
79
+ data: deployData,
80
+ gas: gasFee.gas,
81
+ maxFeePerGas: gasFee.maxFeePerGas,
82
+ maxPriorityFeePerGas: gasFee.maxPriorityFeePerGas,
83
+ purpose: 'deploy-agent-vault',
84
+ onReady: args.callbacks.onWalletReady,
85
+ ...(args.flowId ? { flowId: args.flowId } : {}),
86
+ })
87
+ args.callbacks.onWalletReady(null)
88
+ const client = args.publicClient ?? createErc8004PublicClient(args.registry)
89
+ const receipt = await awaitConfirmedReceipt(client, result.txHash, 'Operator delegation vault deploy', { kind: 'vault-deploy', chainId: args.registry.chainId })
90
+ if (!receipt.contractAddress) {
91
+ throw new Error('Operator delegation vault deploy receipt is missing contractAddress; the transaction was not a contract creation')
92
+ }
93
+ const vaultAddress = getAddress(receipt.contractAddress)
94
+ await assertVaultBytecode(client, vaultAddress, result.txHash)
95
+ return { txHash: result.txHash, vaultAddress }
96
+ }
97
+
98
+ export async function runVaultDepositTransaction(args: {
99
+ identity: EthagentIdentity
100
+ registry: Erc8004RegistryConfig
101
+ vaultAddress: Address
102
+ callbacks: EffectCallbacks
103
+ flowId?: string
104
+ }): Promise<{ txHash: string }> {
105
+ return withTxGuard('vault-deposit', () => runVaultDepositTransactionInner(args))
106
+ }
107
+
108
+ async function runVaultDepositTransactionInner(args: {
109
+ identity: EthagentIdentity
110
+ registry: Erc8004RegistryConfig
111
+ vaultAddress: Address
112
+ callbacks: EffectCallbacks
113
+ flowId?: string
114
+ }): Promise<{ txHash: string }> {
115
+ const { identity, registry, vaultAddress } = args
116
+ if (!identity.agentId) {
117
+ throw new Error('Cannot deposit token to operator delegation vault: agent token ID is missing')
118
+ }
119
+ const tokenOwner = getAddress(identity.ownerAddress ?? identity.address)
120
+ await assertVaultCanAcceptAgent({
121
+ registry,
122
+ vaultAddress,
123
+ agentId: BigInt(identity.agentId),
124
+ })
125
+ const encoded = encodeDepositAgent({
126
+ registry: getAddress(registry.identityRegistryAddress),
127
+ agentId: BigInt(identity.agentId),
128
+ walletAddress: tokenOwner,
129
+ vaultAddress,
130
+ })
131
+ const gasFeeClient = createErc8004PublicClient(registry)
132
+ const gasFee = await prepareTransactionGasFee({
133
+ client: gasFeeClient,
134
+ account: tokenOwner,
135
+ to: encoded.to,
136
+ data: encoded.data,
137
+ })
138
+ const result = await sendBrowserWalletTransaction({
139
+ chainId: registry.chainId,
140
+ expectedAccount: tokenOwner,
141
+ to: encoded.to,
142
+ data: encoded.data,
143
+ gas: gasFee.gas,
144
+ maxFeePerGas: gasFee.maxFeePerGas,
145
+ maxPriorityFeePerGas: gasFee.maxPriorityFeePerGas,
146
+ purpose: 'deposit-agent-vault',
147
+ onReady: args.callbacks.onWalletReady,
148
+ ...(args.flowId ? { flowId: args.flowId } : {}),
149
+ })
150
+ args.callbacks.onWalletReady(null)
151
+ const depositClient = createErc8004PublicClient(registry)
152
+ await awaitConfirmedReceipt(
153
+ depositClient,
154
+ result.txHash as Hex,
155
+ 'Operator delegation vault deposit',
156
+ { kind: 'vault-deposit', chainId: registry.chainId },
157
+ )
158
+ return { txHash: result.txHash }
159
+ }
160
+
161
+ async function assertVaultCanAcceptAgent(args: {
162
+ registry: Erc8004RegistryConfig
163
+ vaultAddress: Address
164
+ agentId: bigint
165
+ }): Promise<void> {
166
+ const client = createErc8004PublicClient(args.registry)
167
+ let held: readonly [Address, bigint, Address]
168
+ try {
169
+ held = await client.readContract({
170
+ address: getAddress(args.vaultAddress),
171
+ abi: OPERATOR_VAULT_ABI,
172
+ functionName: 'heldAgent',
173
+ }) as readonly [Address, bigint, Address]
174
+ } catch {
175
+ return
176
+ }
177
+ const [heldRegistry, heldAgentId, heldOwner] = held
178
+ if (!heldOwner || heldOwner.toLowerCase() === '0x0000000000000000000000000000000000000000') return
179
+ const expectedRegistry = getAddress(args.registry.identityRegistryAddress)
180
+ const sameAgent = heldRegistry.toLowerCase() === expectedRegistry.toLowerCase() && heldAgentId === args.agentId
181
+ if (sameAgent) {
182
+ throw new Error(`Agent vault ${getAddress(args.vaultAddress)} already holds ERC-8004 token #${args.agentId.toString()}. Publish the pending update instead of depositing again.`)
183
+ }
184
+ throw new Error(`Agent vault ${getAddress(args.vaultAddress)} already holds ERC-8004 token #${heldAgentId.toString()} for registry ${getAddress(heldRegistry)}. Deploy a fresh vault for this agent.`)
185
+ }
186
+
187
+ export async function runVaultUnwrapTransaction(args: {
188
+ identity: EthagentIdentity
189
+ registry: Erc8004RegistryConfig
190
+ vaultAddress: Address
191
+ callbacks: EffectCallbacks
192
+ flowId?: string
193
+ agentId?: bigint
194
+ }): Promise<{ txHash: string } | null> {
195
+ return withTxGuard('vault-unwrap', () => runVaultUnwrapTransactionInner(args))
196
+ }
197
+
198
+ async function runVaultUnwrapTransactionInner(args: {
199
+ identity: EthagentIdentity
200
+ registry: Erc8004RegistryConfig
201
+ vaultAddress: Address
202
+ callbacks: EffectCallbacks
203
+ flowId?: string
204
+ agentId?: bigint
205
+ }): Promise<{ txHash: string } | null> {
206
+ const { identity, registry, vaultAddress } = args
207
+ const targetAgentId = args.agentId ?? (identity.agentId ? BigInt(identity.agentId) : undefined)
208
+ if (targetAgentId === undefined) {
209
+ throw new Error('Cannot unwrap token from operator delegation vault: agent token ID is missing')
210
+ }
211
+ const baseState = (identity.state ?? {}) as Record<string, unknown>
212
+ const ownerAddressRaw = readOwnerAddressField(baseState)
213
+ const ownerAddress = ownerAddressRaw
214
+ ? getAddress(ownerAddressRaw)
215
+ : getAddress(identity.ownerAddress ?? identity.address)
216
+ const publicClient = createErc8004PublicClient(registry)
217
+ const status = await isAgentInVault({
218
+ client: publicClient,
219
+ vaultAddress,
220
+ registry: getAddress(registry.identityRegistryAddress),
221
+ agentId: targetAgentId,
222
+ })
223
+ if (!status.inVault) return null
224
+ const encoded = encodeUnwrapAgent({
225
+ registry: getAddress(registry.identityRegistryAddress),
226
+ agentId: targetAgentId,
227
+ recipient: ownerAddress,
228
+ vaultAddress,
229
+ })
230
+ const result = await sendBrowserWalletTransaction({
231
+ chainId: registry.chainId,
232
+ expectedAccount: ownerAddress,
233
+ to: encoded.to,
234
+ data: encoded.data,
235
+ purpose: 'unwrap-agent-vault',
236
+ onReady: args.callbacks.onWalletReady,
237
+ ...(args.flowId ? { flowId: args.flowId } : {}),
238
+ })
239
+ args.callbacks.onWalletReady(null)
240
+ await awaitConfirmedReceipt(
241
+ publicClient,
242
+ result.txHash as Hex,
243
+ 'Operator delegation vault unwrap',
244
+ { kind: 'vault-unwrap', chainId: registry.chainId },
245
+ )
246
+ await confirmAgentWithdrawnFromVault({
247
+ client: publicClient,
248
+ vaultAddress,
249
+ registry: getAddress(registry.identityRegistryAddress),
250
+ agentId: targetAgentId,
251
+ recipient: ownerAddress,
252
+ })
253
+ return { txHash: result.txHash }
254
+ }
255
+
256
+ export async function runVaultWithdrawTransaction(args: {
257
+ identity: EthagentIdentity
258
+ registry: Erc8004RegistryConfig
259
+ vaultAddress: Address
260
+ callbacks: EffectCallbacks
261
+ agentId?: bigint
262
+ }): Promise<{ txHash: string; recipient: Address }> {
263
+ return withTxGuard('vault-withdraw', () => runVaultWithdrawTransactionInner(args))
264
+ }
265
+
266
+ async function runVaultWithdrawTransactionInner(args: {
267
+ identity: EthagentIdentity
268
+ registry: Erc8004RegistryConfig
269
+ vaultAddress: Address
270
+ callbacks: EffectCallbacks
271
+ agentId?: bigint
272
+ }): Promise<{ txHash: string; recipient: Address }> {
273
+ const { identity, registry, vaultAddress } = args
274
+ const targetAgentId = args.agentId ?? (identity.agentId ? BigInt(identity.agentId) : undefined)
275
+ if (targetAgentId === undefined) {
276
+ throw new Error('Cannot withdraw token: agent token ID is missing')
277
+ }
278
+ const publicClient = createErc8004PublicClient(registry)
279
+ const status = await isAgentInVault({
280
+ client: publicClient,
281
+ vaultAddress,
282
+ registry: getAddress(registry.identityRegistryAddress),
283
+ agentId: targetAgentId,
284
+ })
285
+ if (!status.inVault) {
286
+ throw new Error('Token is not currently held by the vault, nothing to unwrap')
287
+ }
288
+ if (!status.ownerAddress) {
289
+ throw new Error('Vault has no recorded depositor for this token; cannot determine recipient')
290
+ }
291
+ const recipient = getAddress(status.ownerAddress)
292
+ const encoded = encodeUnwrapAgent({
293
+ registry: getAddress(registry.identityRegistryAddress),
294
+ agentId: targetAgentId,
295
+ recipient,
296
+ vaultAddress,
297
+ })
298
+ const result = await sendBrowserWalletTransaction({
299
+ chainId: registry.chainId,
300
+ expectedAccount: recipient,
301
+ to: encoded.to,
302
+ data: encoded.data,
303
+ purpose: 'withdraw-vault',
304
+ onReady: args.callbacks.onWalletReady,
305
+ })
306
+ args.callbacks.onWalletReady(null)
307
+ await awaitConfirmedReceipt(
308
+ publicClient,
309
+ result.txHash as Hex,
310
+ 'Operator delegation vault withdraw',
311
+ { kind: 'vault-withdraw', chainId: registry.chainId },
312
+ )
313
+ await confirmAgentWithdrawnFromVault({
314
+ client: publicClient,
315
+ vaultAddress,
316
+ registry: getAddress(registry.identityRegistryAddress),
317
+ agentId: targetAgentId,
318
+ recipient,
319
+ })
320
+ return { txHash: result.txHash, recipient }
321
+ }