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,190 @@
1
+ import { getAddress, type Address } from 'viem'
2
+ import type { EthagentIdentity } from '../../../../storage/config.js'
3
+ import {
4
+ createErc8004PublicClient,
5
+ type Erc8004RegistryConfig,
6
+ } from '../../../registry/erc8004.js'
7
+ import {
8
+ OPERATOR_VAULT_ABI,
9
+ encodeSetMetadataOperator,
10
+ readMetadataOperators,
11
+ } from '../../../registry/operatorVault.js'
12
+ import {
13
+ sendBrowserWalletTransaction,
14
+ type WalletPurpose,
15
+ } from '../../../wallet/browserWallet.js'
16
+ import {
17
+ computeApprovalDiff,
18
+ encodeResolverApprovalChanges,
19
+ verifyResolverApprovalsLanded,
20
+ type ApprovalDiff,
21
+ } from '../../reconciliation/index.js'
22
+ import { normalizeApprovedOperatorWallets } from '../../operatorWallets.js'
23
+ import { readOwnerAddressField } from '../../../identityCompat.js'
24
+ import { localContinuitySnapshotContentHashes } from '../../../continuity/storage.js'
25
+ import { updatePublishedContinuitySnapshotContentHashes } from '../../../continuity/snapshots.js'
26
+ import type { EffectCallbacks } from '../types.js'
27
+ import { awaitConfirmedReceipt, awaitOptionalReceipt } from '../receipts.js'
28
+ import { createMainnetEnsPublicClient } from '../ens/transactions.js'
29
+
30
+ export function resolverSyncWarningMessage(err: unknown): string {
31
+ return err instanceof Error ? err.message : String(err)
32
+ }
33
+
34
+ export function appendResolverSyncWarning(message: string, warning: string | null): string {
35
+ if (!warning) return message
36
+ return `${message}\n\nWarning: ${warning}`
37
+ }
38
+
39
+ export async function syncResolverApprovalsAfterOwnerSave(args: {
40
+ beforeIdentity: EthagentIdentity
41
+ afterIdentity: EthagentIdentity
42
+ registry: Erc8004RegistryConfig
43
+ vaultAddress?: Address
44
+ callbacks: EffectCallbacks
45
+ }): Promise<void> {
46
+ const beforeState = (args.beforeIdentity.state ?? {}) as Record<string, unknown>
47
+ const afterState = (args.afterIdentity.state ?? {}) as Record<string, unknown>
48
+ const ensName = typeof afterState.ensName === 'string' ? afterState.ensName.trim() : ''
49
+ const before = normalizeApprovedOperatorWallets(beforeState.approvedOperatorWallets)
50
+ const after = normalizeApprovedOperatorWallets(afterState.approvedOperatorWallets)
51
+ const diff = computeApprovalDiff(before, after)
52
+ if (diff.added.length === 0 && diff.removed.length === 0) return
53
+
54
+ const ownerAddressRaw = readOwnerAddressField(afterState) ?? args.afterIdentity.ownerAddress ?? args.afterIdentity.address
55
+ const ownerAddress = getAddress(ownerAddressRaw)
56
+
57
+ if (ensName) {
58
+ let encoded
59
+ try {
60
+ encoded = await encodeResolverApprovalChanges({ ensName, diff })
61
+ } catch {
62
+ encoded = null
63
+ }
64
+ if (encoded) {
65
+ const purpose: WalletPurpose = diff.removed.length > 0 && diff.added.length === 0
66
+ ? 'revoke-operator-wallet-resolver'
67
+ : 'authorize-operator-wallet-resolver'
68
+
69
+ const tx = await sendBrowserWalletTransaction({
70
+ chainId: 1,
71
+ expectedAccount: ownerAddress,
72
+ to: encoded.resolverAddress,
73
+ data: encoded.data,
74
+ onReady: args.callbacks.onWalletReady,
75
+ purpose,
76
+ })
77
+ args.callbacks.onWalletReady(null)
78
+ const client = createMainnetEnsPublicClient()
79
+ await awaitOptionalReceipt(client, tx.txHash, 'Resolver delegation sync')
80
+ await verifyResolverApprovalsLanded({
81
+ ensName,
82
+ ownerAddress: ownerAddress,
83
+ resolverAddress: encoded.resolverAddress,
84
+ added: encoded.added,
85
+ removed: encoded.removed,
86
+ client,
87
+ })
88
+ }
89
+ }
90
+
91
+ await syncVaultMetadataOperatorsAfterOwnerSave({
92
+ afterIdentity: args.afterIdentity,
93
+ registry: args.registry,
94
+ vaultAddress: args.vaultAddress,
95
+ diff,
96
+ ownerAddress,
97
+ callbacks: args.callbacks,
98
+ })
99
+ }
100
+
101
+ export async function syncVaultMetadataOperatorsAfterOwnerSave(args: {
102
+ afterIdentity: EthagentIdentity
103
+ registry: Erc8004RegistryConfig
104
+ vaultAddress: Address | undefined
105
+ diff: ApprovalDiff
106
+ ownerAddress: Address
107
+ callbacks: EffectCallbacks
108
+ }): Promise<void> {
109
+ if (!args.vaultAddress) return
110
+ const agentIdRaw = args.afterIdentity.agentId
111
+ if (!agentIdRaw) return
112
+ const agentId = BigInt(agentIdRaw)
113
+ const registryAddress = getAddress(args.registry.identityRegistryAddress)
114
+ const vaultAddress = getAddress(args.vaultAddress)
115
+ const probeClient = createErc8004PublicClient(args.registry)
116
+ let depositor: Address | undefined
117
+ try {
118
+ depositor = await probeClient.readContract({
119
+ address: vaultAddress,
120
+ abi: OPERATOR_VAULT_ABI,
121
+ functionName: 'agentOwner',
122
+ args: [registryAddress, agentId],
123
+ }) as Address
124
+ } catch {
125
+ depositor = undefined
126
+ }
127
+ if (!depositor || depositor.toLowerCase() !== args.ownerAddress.toLowerCase()) return
128
+
129
+ const operations: Array<{ operator: Address; approved: boolean }> = []
130
+ for (const operator of args.diff.added) operations.push({ operator: getAddress(operator), approved: true })
131
+ for (const operator of args.diff.removed) operations.push({ operator: getAddress(operator), approved: false })
132
+ if (operations.length === 0) return
133
+
134
+ for (const op of operations) {
135
+ const encoded = encodeSetMetadataOperator({
136
+ registry: registryAddress,
137
+ agentId,
138
+ operator: op.operator,
139
+ approved: op.approved,
140
+ vaultAddress,
141
+ })
142
+ const tx = await sendBrowserWalletTransaction({
143
+ chainId: args.registry.chainId,
144
+ expectedAccount: args.ownerAddress,
145
+ to: encoded.to,
146
+ data: encoded.data,
147
+ onReady: args.callbacks.onWalletReady,
148
+ purpose: 'sync-operator-vault',
149
+ })
150
+ args.callbacks.onWalletReady(null)
151
+ await awaitConfirmedReceipt(probeClient, tx.txHash, 'Vault operator sync')
152
+ }
153
+
154
+ const VERIFY_MAX_ATTEMPTS = 5
155
+ const VERIFY_DELAY_MS = 1500
156
+ let lastMismatch: { op: typeof operations[number]; observed: boolean } | undefined
157
+ for (let attempt = 0; attempt < VERIFY_MAX_ATTEMPTS; attempt++) {
158
+ if (attempt > 0) await new Promise(resolve => setTimeout(resolve, VERIFY_DELAY_MS))
159
+ const final = await readMetadataOperators({
160
+ client: probeClient,
161
+ vaultAddress,
162
+ registry: registryAddress,
163
+ agentId,
164
+ candidates: operations.map(o => o.operator),
165
+ })
166
+ lastMismatch = undefined
167
+ for (const op of operations) {
168
+ const observed = Boolean(final[op.operator])
169
+ if (observed !== op.approved) {
170
+ lastMismatch = { op, observed }
171
+ break
172
+ }
173
+ }
174
+ if (!lastMismatch) return
175
+ }
176
+ if (lastMismatch) {
177
+ throw new Error(
178
+ lastMismatch.op.approved
179
+ ? `Vault operator authorization didn't land for ${lastMismatch.op.operator}. Your wallet may have rejected the inner transaction; retry the save to apply it.`
180
+ : `Vault operator revocation didn't land for ${lastMismatch.op.operator}. Your wallet may have rejected the inner transaction; retry the save to apply it.`,
181
+ )
182
+ }
183
+ }
184
+
185
+ export async function markCurrentContinuityFilesPublished(identity: EthagentIdentity): Promise<void> {
186
+ const cid = identity.backup?.cid
187
+ if (!cid) return
188
+ const contentHashes = await localContinuitySnapshotContentHashes(identity)
189
+ await updatePublishedContinuitySnapshotContentHashes(identity, cid, contentHashes).catch(() => null)
190
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ runTokenTransferSigning,
3
+ runTokenTransferStorageSubmit,
4
+ runTokenTransferTargetSubmit,
5
+ tokenTransferProgressForPhase,
6
+ } from './runTokenTransfer.js'
@@ -0,0 +1,59 @@
1
+ import { getAddress } from 'viem'
2
+ import type { TokenTransferProgress } from '../types.js'
3
+
4
+ export function tokenTransferProgressForPhase(
5
+ phase: TokenTransferProgress['phase'],
6
+ ownerAddress: string,
7
+ targetAddress: string,
8
+ ): TokenTransferProgress {
9
+ const sender = getAddress(ownerAddress)
10
+ const receiver = getAddress(targetAddress)
11
+ switch (phase) {
12
+ case 'sender-sign':
13
+ return {
14
+ phase,
15
+ walletRole: 'sender',
16
+ expectedAddress: sender,
17
+ title: 'Use Sender Wallet',
18
+ detail: 'Sign to save a transfer snapshot.',
19
+ walletAction: 'Sign Snapshot',
20
+ label: 'Sender Wallet: sign to save the transfer snapshot.',
21
+ }
22
+ case 'target-sign':
23
+ return {
24
+ phase,
25
+ walletRole: 'receiver',
26
+ expectedAddress: receiver,
27
+ title: 'Use Receiver Wallet',
28
+ detail: 'Sign so this wallet can restore after it receives the token.',
29
+ walletAction: 'Sign Restore Access',
30
+ label: 'Receiver Wallet: sign once to authorize future restore after the token transfer.',
31
+ }
32
+ case 'sender-transaction':
33
+ return {
34
+ phase,
35
+ walletRole: 'sender',
36
+ expectedAddress: sender,
37
+ title: 'Use Sender Wallet Again',
38
+ detail: 'Publish the transfer snapshot to the ERC-8004 token URI.',
39
+ walletAction: 'Update Token URI',
40
+ label: 'Sender Wallet: sign one ERC-8004 token URI update that points the token at the transfer snapshot.',
41
+ }
42
+ case 'pinning':
43
+ return {
44
+ phase,
45
+ walletRole: 'none',
46
+ title: 'Publishing Snapshot',
47
+ detail: 'Encrypting and pinning the dual-wallet transfer snapshot.',
48
+ label: 'Encrypting and pinning the dual-wallet transfer snapshot.',
49
+ }
50
+ case 'confirming':
51
+ return {
52
+ phase,
53
+ walletRole: 'none',
54
+ title: 'Confirming Token URI Update',
55
+ detail: 'Waiting for the ERC-8004 token URI transaction.',
56
+ label: 'Confirming the ERC-8004 token URI update.',
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,299 @@
1
+ import { getAddress, isAddress, type Address, type Hex } from 'viem'
2
+ import type { EthagentIdentity } from '../../../../storage/config.js'
3
+ import {
4
+ createTransferContinuitySnapshotChallenge,
5
+ createTransferContinuitySnapshotEnvelope,
6
+ serializeContinuitySnapshotEnvelope,
7
+ transferSnapshotMetadataFromEnvelope,
8
+ } from '../../../continuity/envelope.js'
9
+ import {
10
+ continuityAgentSnapshot,
11
+ continuityVaultStatus,
12
+ readContinuityFiles,
13
+ readPublicSkillsFile,
14
+ writePublicSkillsFile,
15
+ } from '../../../continuity/storage.js'
16
+ import {
17
+ createAgentCard,
18
+ defaultPublicSkillsProfile,
19
+ serializeAgentCard,
20
+ } from '../../../continuity/publicSkills.js'
21
+ import { recordPublishedContinuitySnapshot } from '../../../continuity/snapshots.js'
22
+ import { addToIpfs, DEFAULT_IPFS_API_URL, isPinataUploadUrl } from '../../../storage/ipfs.js'
23
+ import {
24
+ createErc8004PublicClient,
25
+ encodeSetAgentUri,
26
+ preflightSetAgentUri,
27
+ withEthagentPointers,
28
+ } from '../../../registry/erc8004.js'
29
+ import { resolveValidatedPinataJwt, savePinataJwt } from '../../../storage/pinataJwt.js'
30
+ import { openBrowserWalletSession } from '../../../wallet/browserWallet.js'
31
+ import { resolveEnsAddress } from '../../../ens/ensLookup.js'
32
+ import type { Step } from '../../identityHubReducer.js'
33
+ import type { EffectCallbacks } from '../types.js'
34
+ import { awaitConfirmedReceipt } from '../receipts.js'
35
+ import {
36
+ assertVerifiedPin,
37
+ deriveAgentName,
38
+ } from '../shared/profilePrep.js'
39
+ import { operatorsPointerFromState } from '../shared/snapshot.js'
40
+ import { tokenTransferProgressForPhase } from './progress.js'
41
+ import { assertTokenNotInVault } from '../vault/preflight.js'
42
+
43
+ type BackupMetadata = NonNullable<EthagentIdentity['backup']>
44
+ type PublicSkillsMetadata = NonNullable<EthagentIdentity['publicSkills']>
45
+
46
+ type TokenTransferResult = {
47
+ identity: EthagentIdentity
48
+ snapshotCid: string
49
+ txHash: Hex
50
+ }
51
+
52
+ export async function runTokenTransferTargetSubmit(
53
+ value: string,
54
+ step: Extract<Step, { kind: 'token-transfer-target' }>,
55
+ callbacks: EffectCallbacks,
56
+ options: { signal?: AbortSignal } = {},
57
+ ): Promise<void> {
58
+ if (!step.identity.agentId) throw new Error('Cannot prepare token transfer: identity is missing an agent token ID')
59
+ const targetHandle = value.trim()
60
+ if (!targetHandle) throw new Error('Enter the receiver wallet ENS name or 0x address')
61
+ throwIfTransferAborted(options.signal)
62
+ await assertTokenNotInVault({ identity: step.identity, registry: step.registry })
63
+ throwIfTransferAborted(options.signal)
64
+ callbacks.onStep({ kind: 'token-transfer-resolving', identity: step.identity, registry: step.registry, targetHandle, returnTo: step.returnTo })
65
+ const targetAddress = await resolveTransferTargetAddress(targetHandle, { signal: options.signal })
66
+ throwIfTransferAborted(options.signal)
67
+ const ownerAddress = getAddress(step.identity.ownerAddress ?? step.identity.address)
68
+ if (targetAddress.toLowerCase() === ownerAddress.toLowerCase()) {
69
+ throw new Error('Receiver wallet must be different from the sender wallet')
70
+ }
71
+ const status = await continuityVaultStatus(step.identity)
72
+ throwIfTransferAborted(options.signal)
73
+ if (!status.ready) {
74
+ throw new Error('Restore local continuity files before preparing a transfer snapshot')
75
+ }
76
+ const apiUrl = DEFAULT_IPFS_API_URL
77
+ let jwt: string | undefined
78
+ try {
79
+ jwt = isPinataUploadUrl(apiUrl) ? await resolveValidatedPinataJwt() : undefined
80
+ throwIfTransferAborted(options.signal)
81
+ } catch (err: unknown) {
82
+ throwIfTransferAborted(options.signal)
83
+ callbacks.onStep({
84
+ kind: 'token-transfer-storage',
85
+ identity: step.identity,
86
+ registry: step.registry,
87
+ targetHandle,
88
+ targetAddress,
89
+ error: (err as Error).message,
90
+ returnTo: step.returnTo,
91
+ })
92
+ return
93
+ }
94
+ if (isPinataUploadUrl(apiUrl) && !jwt) {
95
+ throwIfTransferAborted(options.signal)
96
+ callbacks.onStep({ kind: 'token-transfer-storage', identity: step.identity, registry: step.registry, targetHandle, targetAddress, returnTo: step.returnTo })
97
+ return
98
+ }
99
+ throwIfTransferAborted(options.signal)
100
+ callbacks.onStep({ kind: 'token-transfer-signing', identity: step.identity, registry: step.registry, targetHandle, targetAddress, pinataJwt: jwt, returnTo: step.returnTo })
101
+ }
102
+
103
+ export async function runTokenTransferStorageSubmit(
104
+ input: string,
105
+ step: Extract<Step, { kind: 'token-transfer-storage' }>,
106
+ callbacks: EffectCallbacks,
107
+ ): Promise<void> {
108
+ const { jwt: pinataJwt } = await savePinataJwt(input)
109
+ callbacks.onStep({
110
+ kind: 'token-transfer-signing',
111
+ identity: step.identity,
112
+ registry: step.registry,
113
+ targetHandle: step.targetHandle,
114
+ targetAddress: step.targetAddress,
115
+ pinataJwt,
116
+ returnTo: step.returnTo,
117
+ })
118
+ }
119
+
120
+ export async function runTokenTransferSigning(
121
+ step: Extract<Step, { kind: 'token-transfer-signing' }>,
122
+ callbacks: EffectCallbacks,
123
+ ): Promise<TokenTransferResult> {
124
+ if (!step.identity.agentId) throw new Error('Cannot prepare token transfer: identity is missing an agent token ID')
125
+ const transferAgentId = step.identity.agentId
126
+ const ownerAddress = getAddress(step.identity.ownerAddress ?? step.identity.address)
127
+ const targetAddress = getAddress(step.targetAddress)
128
+ const token = {
129
+ chainId: step.registry.chainId,
130
+ identityRegistryAddress: step.registry.identityRegistryAddress,
131
+ agentId: transferAgentId,
132
+ }
133
+ const senderChallenge = createTransferContinuitySnapshotChallenge({ token, ownerAddress, targetAddress, role: 'sender' })
134
+ const targetChallenge = createTransferContinuitySnapshotChallenge({ token, ownerAddress, targetAddress, role: 'receiver' })
135
+
136
+ const session = await openBrowserWalletSession({ onReady: callbacks.onWalletReady })
137
+ try {
138
+
139
+ callbacks.onTokenTransferProgress?.(tokenTransferProgressForPhase('sender-sign', ownerAddress, targetAddress))
140
+ const senderSignature = await session.requestSignature({
141
+ chainId: step.registry.chainId,
142
+ expectedAccount: ownerAddress,
143
+ message: senderChallenge,
144
+ purpose: 'prepare-transfer-sender',
145
+ })
146
+
147
+ callbacks.onTokenTransferProgress?.(tokenTransferProgressForPhase('target-sign', ownerAddress, targetAddress))
148
+ const targetSignature = await session.requestSignature({
149
+ chainId: step.registry.chainId,
150
+ expectedAccount: targetAddress,
151
+ message: targetChallenge,
152
+ purpose: 'prepare-transfer-target',
153
+ })
154
+
155
+ callbacks.onTokenTransferProgress?.(tokenTransferProgressForPhase('pinning', ownerAddress, targetAddress))
156
+ const baseState = (step.identity.state ?? {}) as Record<string, unknown>
157
+ const state: Record<string, unknown> = {
158
+ ...baseState,
159
+ ownerAddress,
160
+ lastBackedUpAt: new Date().toISOString(),
161
+ }
162
+ const nextIdentityForFiles: EthagentIdentity = { ...step.identity, state }
163
+ const continuityFiles = await readContinuityFiles(nextIdentityForFiles)
164
+ const publicSkillsJson = await readPublicSkillsFile(nextIdentityForFiles)
165
+ const publicSkillsPin = await addToIpfs(DEFAULT_IPFS_API_URL, publicSkillsJson, fetch, { pinataJwt: step.pinataJwt })
166
+ assertVerifiedPin(publicSkillsPin)
167
+ const agentCardPin = await addToIpfs(
168
+ DEFAULT_IPFS_API_URL,
169
+ serializeAgentCard(createAgentCard(defaultPublicSkillsProfile(nextIdentityForFiles))),
170
+ fetch,
171
+ { pinataJwt: step.pinataJwt },
172
+ )
173
+ assertVerifiedPin(agentCardPin)
174
+ const envelope = createTransferContinuitySnapshotEnvelope({
175
+ ownerAddress,
176
+ ownerWalletSignature: senderSignature.signature,
177
+ targetAddress,
178
+ targetWalletSignature: targetSignature.signature,
179
+ targetHandle: step.targetHandle,
180
+ token,
181
+ payload: {
182
+ agent: continuityAgentSnapshot(nextIdentityForFiles),
183
+ files: continuityFiles,
184
+ transcript: [],
185
+ state,
186
+ },
187
+ })
188
+ const transferSnapshot = transferSnapshotMetadataFromEnvelope(envelope)
189
+ const statePin = await addToIpfs(DEFAULT_IPFS_API_URL, serializeContinuitySnapshotEnvelope(envelope), fetch, { pinataJwt: step.pinataJwt })
190
+ assertVerifiedPin(statePin)
191
+ const snapshotCid = statePin.cid
192
+ const backup: BackupMetadata = {
193
+ cid: snapshotCid,
194
+ createdAt: envelope.createdAt,
195
+ envelopeVersion: envelope.envelopeVersion,
196
+ ipfsApiUrl: DEFAULT_IPFS_API_URL,
197
+ status: 'pinned',
198
+ ownerAddress,
199
+ chainId: step.registry.chainId,
200
+ rpcUrl: step.registry.rpcUrl,
201
+ identityRegistryAddress: step.registry.identityRegistryAddress,
202
+ agentId: transferAgentId,
203
+ ...(transferSnapshot ? { transferSnapshot } : {}),
204
+ }
205
+ const publicSkills: PublicSkillsMetadata = {
206
+ cid: publicSkillsPin.cid,
207
+ agentCardCid: agentCardPin.cid,
208
+ updatedAt: envelope.createdAt,
209
+ status: 'pinned',
210
+ }
211
+ const nextName = typeof state.name === 'string' && state.name.trim() ? state.name.trim() : deriveAgentName(step.identity)
212
+ const nextDescription = typeof state.description === 'string' && state.description.trim() ? state.description.trim() : ''
213
+ const uploadedImageUri = typeof state.imageUrl === 'string' && state.imageUrl.trim() ? state.imageUrl.trim() : undefined
214
+ const nextEnsName = typeof state.ensName === 'string' && state.ensName.trim() ? state.ensName.trim() : undefined
215
+ const registration = withEthagentPointers({
216
+ type: 'https://eips.ethereum.org/EIPS/eip-8004#registration-v1',
217
+ name: nextName,
218
+ ...(nextDescription ? { description: nextDescription } : {}),
219
+ ...(uploadedImageUri ? { image: uploadedImageUri } : {}),
220
+ }, {
221
+ backup: { cid: snapshotCid, envelopeVersion: envelope.envelopeVersion, createdAt: envelope.createdAt, ...(transferSnapshot ? { transferSnapshot } : {}) },
222
+ publicDiscovery: { skillsCid: publicSkills.cid, agentCardCid: publicSkills.agentCardCid, updatedAt: publicSkills.updatedAt },
223
+ registration: { chainId: step.registry.chainId, identityRegistryAddress: step.registry.identityRegistryAddress, agentId: transferAgentId },
224
+ ensName: nextEnsName,
225
+ operators: operatorsPointerFromState(state, nextEnsName),
226
+ ownerAddress,
227
+ })
228
+ const metadataPin = await addToIpfs(DEFAULT_IPFS_API_URL, JSON.stringify(registration, null, 2), fetch, { pinataJwt: step.pinataJwt })
229
+ assertVerifiedPin(metadataPin)
230
+ const metadataCid = metadataPin.cid
231
+ const agentUri = `ipfs://${metadataCid}`
232
+ const agentId = BigInt(transferAgentId)
233
+ await preflightSetAgentUri({
234
+ ...step.registry,
235
+ account: ownerAddress,
236
+ agentId,
237
+ newUri: agentUri,
238
+ })
239
+
240
+ callbacks.onTokenTransferProgress?.(tokenTransferProgressForPhase('sender-transaction', ownerAddress, targetAddress))
241
+ const tx = await session.sendTransaction({
242
+ chainId: step.registry.chainId,
243
+ expectedAccount: ownerAddress,
244
+ to: step.registry.identityRegistryAddress,
245
+ data: encodeSetAgentUri({ agentId, newUri: agentUri }),
246
+ purpose: 'publish-transfer-snapshot',
247
+ })
248
+
249
+ callbacks.onTokenTransferProgress?.(tokenTransferProgressForPhase('confirming', ownerAddress, targetAddress))
250
+ const client = createErc8004PublicClient(step.registry)
251
+ await awaitConfirmedReceipt(client, tx.txHash, 'Token transfer URI publish', { kind: 'token-transfer', chainId: step.registry.chainId })
252
+ const nextIdentity: EthagentIdentity = {
253
+ ...step.identity,
254
+ source: 'erc8004',
255
+ address: ownerAddress,
256
+ ownerAddress,
257
+ chainId: step.registry.chainId,
258
+ rpcUrl: step.registry.rpcUrl,
259
+ identityRegistryAddress: step.registry.identityRegistryAddress,
260
+ agentUri,
261
+ metadataCid,
262
+ backup: { ...backup, metadataCid, agentUri, txHash: tx.txHash },
263
+ publicSkills,
264
+ state,
265
+ }
266
+ await writePublicSkillsFile(nextIdentity, publicSkillsJson)
267
+ await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'published transfer snapshot' }).catch(() => null)
268
+ callbacks.onTokenTransferProgress?.(null)
269
+ return {
270
+ identity: nextIdentity,
271
+ snapshotCid,
272
+ txHash: tx.txHash,
273
+ }
274
+
275
+ } finally {
276
+ await session.close().catch(() => {})
277
+ callbacks.onWalletReady(null)
278
+ }
279
+ }
280
+
281
+ async function resolveTransferTargetAddress(value: string, options: { signal?: AbortSignal } = {}): Promise<Address> {
282
+ const trimmed = value.trim()
283
+ throwIfTransferAborted(options.signal)
284
+ if (isAddress(trimmed, { strict: false })) return getAddress(trimmed)
285
+ if (!trimmed.includes('.')) throw new Error('Enter a receiver Ethereum address or ENS name')
286
+ const resolved = await resolveEnsAddress(trimmed, { signal: options.signal })
287
+ throwIfTransferAborted(options.signal)
288
+ if (!resolved) throw new Error(`ENS name did not resolve: ${trimmed}`)
289
+ return getAddress(resolved)
290
+ }
291
+
292
+ function throwIfTransferAborted(signal: AbortSignal | undefined): void {
293
+ if (!signal?.aborted) return
294
+ const err = new Error('token transfer preparation cancelled')
295
+ err.name = 'AbortError'
296
+ throw err
297
+ }
298
+
299
+ export { tokenTransferProgressForPhase } from './progress.js'
@@ -0,0 +1,53 @@
1
+ import type { Address } from 'viem'
2
+ import type { EthagentIdentity } from '../../../storage/config.js'
3
+ import type { BrowserWalletReady } from '../../wallet/browserWallet.js'
4
+ import type { Step } from '../identityHubReducer.js'
5
+
6
+ export type IdentityCompletionSource = 'create' | 'restore' | 'update'
7
+
8
+ export type EffectCallbacks = {
9
+ onStep: (step: Step) => void
10
+ onWalletReady: (session: BrowserWalletReady | null) => void
11
+ onIdentityComplete: (identity: EthagentIdentity, message: string, source?: IdentityCompletionSource) => Promise<void>
12
+ onRestoreProgress?: (progress: RestoreProgress | null) => void
13
+ onTokenTransferProgress?: (progress: TokenTransferProgress | null) => void
14
+ onEnsClearProgress?: (progress: EnsClearProgress | null) => void
15
+ onEnsLinkProgress?: (progress: EnsLinkProgress | null) => void
16
+ onEnsUpdateProgress?: (progress: EnsUpdateProgress | null) => void
17
+ onCustodySwitchAdvancedProgress?: (progress: CustodySwitchAdvancedProgress | null) => void
18
+ }
19
+
20
+ export type RestoreProgress = {
21
+ phase: 'decrypting' | 'writing' | 'finishing'
22
+ label: string
23
+ }
24
+
25
+ export type TokenTransferProgress = {
26
+ phase: 'sender-sign' | 'target-sign' | 'pinning' | 'sender-transaction' | 'confirming'
27
+ walletRole: 'sender' | 'receiver' | 'none'
28
+ title: string
29
+ detail: string
30
+ label: string
31
+ expectedAddress?: Address
32
+ walletAction?: string
33
+ }
34
+
35
+ export type EnsClearProgress = {
36
+ phase: 'records-tx' | 'records-confirming' | 'snapshot-sign' | 'snapshot-tx' | 'snapshot-confirming'
37
+ label: string
38
+ }
39
+
40
+ export type EnsLinkProgress = {
41
+ phase: 'registry-tx' | 'registry-confirming' | 'records-tx' | 'records-confirming' | 'snapshot-sign' | 'snapshot-tx' | 'snapshot-confirming'
42
+ label: string
43
+ }
44
+
45
+ export type EnsUpdateProgress = {
46
+ phase: 'records-tx' | 'records-confirming' | 'snapshot-sign' | 'snapshot-tx' | 'snapshot-confirming'
47
+ label: string
48
+ }
49
+
50
+ export type CustodySwitchAdvancedProgress = {
51
+ phase: 'deploy-tx' | 'deploy-confirming' | 'deposit-tx' | 'deposit-confirming' | 'reconcile-tx' | 'reconcile-confirming'
52
+ label: string
53
+ }
@@ -0,0 +1,50 @@
1
+ import { getAddress, type Address, type PublicClient } from 'viem'
2
+ import type { EthagentIdentity } from '../../../../storage/config.js'
3
+ import { readOperatorVaultAddressField } from '../../../identityCompat.js'
4
+ import { createErc8004PublicClient, type Erc8004RegistryConfig } from '../../../registry/erc8004.js'
5
+ import { isAgentInVault, resolveConfiguredOperatorVaultAddress } from '../../../registry/operatorVault.js'
6
+ import { readCustodyMode } from '../../model/custody.js'
7
+
8
+ export class OperatorVaultUnavailableError extends Error {
9
+ constructor(chainId: number) {
10
+ super(`Operator delegation vault is not deployed for chainId ${chainId}. Switching custody mode is unavailable until a deployment is recorded.`)
11
+ this.name = 'OperatorVaultUnavailableError'
12
+ }
13
+ }
14
+
15
+ export class TokenInVaultError extends Error {
16
+ constructor(public vaultAddress: Address) {
17
+ super('Token is in the operator delegation vault. Withdraw it first to prepare a transfer.')
18
+ this.name = 'TokenInVaultError'
19
+ }
20
+ }
21
+
22
+ export async function assertTokenNotInVault(args: {
23
+ identity: EthagentIdentity
24
+ registry: Erc8004RegistryConfig
25
+ operatorVaults?: Readonly<Record<string, string>>
26
+ client?: Pick<PublicClient, 'readContract'>
27
+ }): Promise<void> {
28
+ if (!args.identity.agentId) return
29
+ const vaultAddress = vaultAddressForTransferPreflight(args.identity, args.operatorVaults)
30
+ if (!vaultAddress) return
31
+ const client = args.client ?? createErc8004PublicClient(args.registry)
32
+ const status = await isAgentInVault({
33
+ client,
34
+ vaultAddress,
35
+ registry: getAddress(args.registry.identityRegistryAddress),
36
+ agentId: BigInt(args.identity.agentId),
37
+ })
38
+ if (status.inVault) throw new TokenInVaultError(vaultAddress)
39
+ }
40
+
41
+ function vaultAddressForTransferPreflight(
42
+ identity: EthagentIdentity,
43
+ operatorVaults?: Readonly<Record<string, string>>,
44
+ ): Address | undefined {
45
+ const identityVault = readOperatorVaultAddressField(identity.state as Record<string, unknown> | undefined)
46
+ if (identityVault) return getAddress(identityVault)
47
+ if (readCustodyMode(identity.state as Record<string, unknown> | undefined) !== 'advanced') return undefined
48
+ if (!identity.chainId) return undefined
49
+ return resolveConfiguredOperatorVaultAddress(operatorVaults, identity.chainId)
50
+ }