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
@@ -1,58 +1,15 @@
1
1
  import crypto from 'node:crypto'
2
2
  import { ml_kem768 } from '@noble/post-quantum/ml-kem.js'
3
- import { addressFromPrivateKey, recoverAddressFromSignature, toChecksumAddress, validatePrivateKey } from './eth.js'
3
+ import { recoverAddressFromSignature, toChecksumAddress } from './eth.js'
4
4
 
5
- export const BACKUP_ENVELOPE_VERSION = 'ethagent-pq-backup-v1'
6
5
  export const AGENT_STATE_BACKUP_ENVELOPE_VERSION = 'ethagent-state-backup-v1'
7
6
 
8
- type BackupPayload = {
9
- privateKey: string
10
- address: string
11
- createdAt: string
12
- }
13
-
14
- export type AgentStatePayload = {
7
+ type AgentStatePayload = {
15
8
  ownerAddress: string
16
9
  createdAt: string
17
10
  state: Record<string, unknown>
18
11
  }
19
12
 
20
- export type IdentityBackupEnvelope = {
21
- version: 1
22
- envelopeVersion: typeof BACKUP_ENVELOPE_VERSION
23
- address: string
24
- ownerAddress?: string
25
- createdAt: string
26
- challenge: string
27
- walletSignature: string
28
- crypto: {
29
- kem: 'ML-KEM-768'
30
- aead: 'AES-256-GCM'
31
- kdf: 'HKDF-SHA256'
32
- signature: 'EIP-191'
33
- }
34
- salt: string
35
- kemPublicKey: string
36
- kemCiphertext: string
37
- nonce: string
38
- ciphertext: string
39
- tag: string
40
- }
41
-
42
- export type CreateIdentityBackupArgs = {
43
- privateKey: string
44
- recoveryPassphrase: string
45
- walletSignature: string
46
- createdAt?: string
47
- ownerAddress?: string
48
- }
49
-
50
- export type RestoreIdentityBackupArgs = {
51
- envelope: IdentityBackupEnvelope
52
- recoveryPassphrase: string
53
- walletSignature: string
54
- }
55
-
56
13
  export type AgentStateBackupEnvelope = {
57
14
  version: 1
58
15
  envelopeVersion: typeof AGENT_STATE_BACKUP_ENVELOPE_VERSION
@@ -73,14 +30,14 @@ export type AgentStateBackupEnvelope = {
73
30
  tag: string
74
31
  }
75
32
 
76
- export type CreateAgentStateBackupArgs = {
33
+ type CreateAgentStateBackupArgs = {
77
34
  ownerAddress: string
78
35
  walletSignature: string
79
36
  state: Record<string, unknown>
80
37
  createdAt?: string
81
38
  }
82
39
 
83
- export type RestoreAgentStateBackupArgs = {
40
+ type RestoreAgentStateBackupArgs = {
84
41
  envelope: AgentStateBackupEnvelope
85
42
  walletSignature: string
86
43
  }
@@ -90,121 +47,21 @@ export class AgentStateOwnerMismatchError extends Error {
90
47
  readonly backupOwner: string,
91
48
  readonly currentOwner: string,
92
49
  ) {
93
- super('agent backup is encrypted for another wallet')
50
+ super('Agent backup is encrypted for another wallet')
94
51
  this.name = 'AgentStateOwnerMismatchError'
95
52
  }
96
53
  }
97
54
 
98
- export function createRecoveryChallenge(address: string): string {
99
- const checksum = toChecksumAddress(address)
100
- return [
101
- 'ethagent identity recovery v1',
102
- `address: ${checksum}`,
103
- 'purpose: authorize encrypted portable agent backup',
104
- ].join('\n')
105
- }
106
-
107
55
  export function createAgentStateRecoveryChallenge(ownerAddress: string): string {
108
56
  const checksum = toChecksumAddress(ownerAddress)
109
57
  return [
110
- 'ethagent encrypted state access',
58
+ 'Encrypted State Access',
111
59
  `Owner: ${checksum}`,
112
60
  'Action: authorize this wallet to unlock the encrypted agent backup',
113
61
  'Version: 1',
114
62
  ].join('\n')
115
63
  }
116
64
 
117
- export function createIdentityBackupEnvelope(args: CreateIdentityBackupArgs): IdentityBackupEnvelope {
118
- if (!validatePrivateKey(args.privateKey)) throw new Error('invalid private key')
119
- if (args.recoveryPassphrase.length < 8) throw new Error('recovery passphrase must be at least 8 characters')
120
-
121
- const address = addressFromPrivateKey(args.privateKey)
122
- const ownerAddress = args.ownerAddress ? toChecksumAddress(args.ownerAddress) : undefined
123
- const signingAddress = ownerAddress ?? address
124
- const challenge = createRecoveryChallenge(signingAddress)
125
- assertSignatureForAddress(challenge, args.walletSignature, signingAddress)
126
-
127
- const createdAt = args.createdAt ?? new Date().toISOString()
128
- const salt = crypto.randomBytes(32)
129
- const kemSeed = deriveKemSeed(args.recoveryPassphrase, args.walletSignature, salt, address)
130
- const kemKeys = ml_kem768.keygen(kemSeed)
131
- const kem = ml_kem768.encapsulate(kemKeys.publicKey)
132
- const key = deriveAesKey(args.recoveryPassphrase, args.walletSignature, kem.sharedSecret, salt, address)
133
- const nonce = crypto.randomBytes(12)
134
- const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce)
135
- cipher.setAAD(aadFor(address, createdAt))
136
- const plaintext = Buffer.from(JSON.stringify({
137
- privateKey: normalizedPrivateKey(args.privateKey),
138
- address,
139
- createdAt,
140
- } satisfies BackupPayload), 'utf8')
141
- const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()])
142
- const tag = cipher.getAuthTag()
143
-
144
- return {
145
- version: 1,
146
- envelopeVersion: BACKUP_ENVELOPE_VERSION,
147
- address,
148
- ...(ownerAddress ? { ownerAddress } : {}),
149
- createdAt,
150
- challenge,
151
- walletSignature: args.walletSignature,
152
- crypto: {
153
- kem: 'ML-KEM-768',
154
- aead: 'AES-256-GCM',
155
- kdf: 'HKDF-SHA256',
156
- signature: 'EIP-191',
157
- },
158
- salt: toBase64(salt),
159
- kemPublicKey: toBase64(kemKeys.publicKey),
160
- kemCiphertext: toBase64(kem.cipherText),
161
- nonce: toBase64(nonce),
162
- ciphertext: toBase64(encrypted),
163
- tag: toBase64(tag),
164
- }
165
- }
166
-
167
- export function restoreIdentityBackupEnvelope(args: RestoreIdentityBackupArgs): BackupPayload {
168
- const envelope = normalizeEnvelope(args.envelope)
169
- const signingAddress = envelope.ownerAddress ?? envelope.address
170
- assertSignatureForAddress(envelope.challenge, args.walletSignature, signingAddress)
171
- assertSignatureForAddress(envelope.challenge, envelope.walletSignature, signingAddress)
172
-
173
- const salt = fromBase64(envelope.salt)
174
- const kemSeed = deriveKemSeed(args.recoveryPassphrase, envelope.walletSignature, salt, envelope.address)
175
- const kemKeys = ml_kem768.keygen(kemSeed)
176
- const expectedPublicKey = toBase64(kemKeys.publicKey)
177
- if (expectedPublicKey !== envelope.kemPublicKey) {
178
- throw new Error('recovery credentials do not match this backup')
179
- }
180
-
181
- const sharedSecret = ml_kem768.decapsulate(fromBase64(envelope.kemCiphertext), kemKeys.secretKey)
182
- const key = deriveAesKey(args.recoveryPassphrase, envelope.walletSignature, sharedSecret, salt, envelope.address)
183
- const decipher = crypto.createDecipheriv('aes-256-gcm', key, fromBase64(envelope.nonce))
184
- decipher.setAAD(aadFor(envelope.address, envelope.createdAt))
185
- decipher.setAuthTag(fromBase64(envelope.tag))
186
-
187
- let decoded: unknown
188
- try {
189
- const plaintext = Buffer.concat([
190
- decipher.update(fromBase64(envelope.ciphertext)),
191
- decipher.final(),
192
- ]).toString('utf8')
193
- decoded = JSON.parse(plaintext)
194
- } catch {
195
- throw new Error('could not decrypt backup with the supplied credentials')
196
- }
197
-
198
- if (!isBackupPayload(decoded)) throw new Error('backup payload is invalid')
199
- if (decoded.address.toLowerCase() !== envelope.address.toLowerCase()) {
200
- throw new Error('backup payload address mismatch')
201
- }
202
- if (addressFromPrivateKey(decoded.privateKey).toLowerCase() !== envelope.address.toLowerCase()) {
203
- throw new Error('backup private key does not match address')
204
- }
205
- return decoded
206
- }
207
-
208
65
  export function createAgentStateBackupEnvelope(args: CreateAgentStateBackupArgs): AgentStateBackupEnvelope {
209
66
  const ownerAddress = toChecksumAddress(args.ownerAddress)
210
67
  const challenge = createAgentStateRecoveryChallenge(ownerAddress)
@@ -257,7 +114,7 @@ export function restoreAgentStateBackupEnvelope(args: RestoreAgentStateBackupArg
257
114
  const kemKeys = ml_kem768.keygen(kemSeed)
258
115
  const expectedPublicKey = toBase64(kemKeys.publicKey)
259
116
  if (expectedPublicKey !== envelope.kemPublicKey) {
260
- throw new Error('wallet signature does not match this agent backup')
117
+ throw new Error('Wallet signature does not match this agent backup')
261
118
  }
262
119
 
263
120
  const sharedSecret = ml_kem768.decapsulate(fromBase64(envelope.kemCiphertext), kemKeys.secretKey)
@@ -274,12 +131,12 @@ export function restoreAgentStateBackupEnvelope(args: RestoreAgentStateBackupArg
274
131
  ]).toString('utf8')
275
132
  decoded = JSON.parse(plaintext)
276
133
  } catch {
277
- throw new Error('could not decrypt agent state with the supplied wallet signature')
134
+ throw new Error('Could not decrypt agent state with the supplied wallet signature')
278
135
  }
279
136
 
280
- if (!isAgentStatePayload(decoded)) throw new Error('agent state backup payload is invalid')
137
+ if (!isAgentStatePayload(decoded)) throw new Error('Agent state backup payload is invalid')
281
138
  if (decoded.ownerAddress.toLowerCase() !== envelope.ownerAddress.toLowerCase()) {
282
- throw new Error('agent state backup owner mismatch')
139
+ throw new Error('Agent state backup owner mismatch')
283
140
  }
284
141
  return {
285
142
  ...decoded,
@@ -295,16 +152,6 @@ export function assertAgentStateBackupOwner(envelope: AgentStateBackupEnvelope,
295
152
  }
296
153
  }
297
154
 
298
- export function serializeIdentityBackupEnvelope(envelope: IdentityBackupEnvelope): string {
299
- return JSON.stringify(normalizeEnvelope(envelope), null, 2)
300
- }
301
-
302
- export function parseIdentityBackupEnvelope(raw: string | Uint8Array): IdentityBackupEnvelope {
303
- const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw)
304
- const parsed = JSON.parse(text) as unknown
305
- return normalizeEnvelope(parsed)
306
- }
307
-
308
155
  export function serializeAgentStateBackupEnvelope(envelope: AgentStateBackupEnvelope): string {
309
156
  return JSON.stringify(normalizeAgentStateEnvelope(envelope), null, 2)
310
157
  }
@@ -315,43 +162,11 @@ export function parseAgentStateBackupEnvelope(raw: string | Uint8Array): AgentSt
315
162
  return normalizeAgentStateEnvelope(parsed)
316
163
  }
317
164
 
318
- function normalizeEnvelope(input: unknown): IdentityBackupEnvelope {
319
- if (!isEnvelope(input)) throw new Error('invalid identity backup envelope')
320
- if (input.envelopeVersion !== BACKUP_ENVELOPE_VERSION) throw new Error('unsupported backup envelope version')
321
- if (input.crypto.kem !== 'ML-KEM-768' || input.crypto.aead !== 'AES-256-GCM') {
322
- throw new Error('unsupported backup crypto suite')
323
- }
324
- return {
325
- ...input,
326
- address: toChecksumAddress(input.address),
327
- ...(typeof input.ownerAddress === 'string' ? { ownerAddress: toChecksumAddress(input.ownerAddress) } : {}),
328
- }
329
- }
330
-
331
- function isEnvelope(input: unknown): input is IdentityBackupEnvelope {
332
- if (!input || typeof input !== 'object') return false
333
- const obj = input as Partial<IdentityBackupEnvelope>
334
- return obj.version === 1
335
- && obj.envelopeVersion === BACKUP_ENVELOPE_VERSION
336
- && typeof obj.address === 'string'
337
- && (obj.ownerAddress === undefined || typeof obj.ownerAddress === 'string')
338
- && typeof obj.createdAt === 'string'
339
- && typeof obj.challenge === 'string'
340
- && typeof obj.walletSignature === 'string'
341
- && typeof obj.salt === 'string'
342
- && typeof obj.kemPublicKey === 'string'
343
- && typeof obj.kemCiphertext === 'string'
344
- && typeof obj.nonce === 'string'
345
- && typeof obj.ciphertext === 'string'
346
- && typeof obj.tag === 'string'
347
- && !!obj.crypto
348
- }
349
-
350
165
  function normalizeAgentStateEnvelope(input: unknown): AgentStateBackupEnvelope {
351
- if (!isAgentStateEnvelope(input)) throw new Error('invalid agent state backup envelope')
352
- if (input.envelopeVersion !== AGENT_STATE_BACKUP_ENVELOPE_VERSION) throw new Error('unsupported agent state backup envelope version')
166
+ if (!isAgentStateEnvelope(input)) throw new Error('Invalid agent state backup envelope')
167
+ if (input.envelopeVersion !== AGENT_STATE_BACKUP_ENVELOPE_VERSION) throw new Error('Unsupported agent state backup envelope version')
353
168
  if (input.crypto.kem !== 'ML-KEM-768' || input.crypto.aead !== 'AES-256-GCM') {
354
- throw new Error('unsupported backup crypto suite')
169
+ throw new Error('Unsupported backup crypto suite')
355
170
  }
356
171
  return {
357
172
  ...input,
@@ -377,15 +192,6 @@ function isAgentStateEnvelope(input: unknown): input is AgentStateBackupEnvelope
377
192
  && !!obj.crypto
378
193
  }
379
194
 
380
- function isBackupPayload(input: unknown): input is BackupPayload {
381
- if (!input || typeof input !== 'object') return false
382
- const obj = input as Partial<BackupPayload>
383
- return typeof obj.privateKey === 'string'
384
- && validatePrivateKey(obj.privateKey)
385
- && typeof obj.address === 'string'
386
- && typeof obj.createdAt === 'string'
387
- }
388
-
389
195
  function isAgentStatePayload(input: unknown): input is AgentStatePayload {
390
196
  if (!input || typeof input !== 'object') return false
391
197
  const obj = input as Partial<AgentStatePayload>
@@ -399,40 +205,10 @@ function isAgentStatePayload(input: unknown): input is AgentStatePayload {
399
205
  function assertSignatureForAddress(challenge: string, signature: string, address: string): void {
400
206
  const recovered = recoverAddressFromSignature(challenge, signature)
401
207
  if (recovered.toLowerCase() !== address.toLowerCase()) {
402
- throw new Error('wallet signature does not match backup address')
208
+ throw new Error('Wallet signature does not match backup address')
403
209
  }
404
210
  }
405
211
 
406
- function deriveKemSeed(passphrase: string, walletSignature: string, salt: Uint8Array, address: string): Uint8Array {
407
- return hkdf(
408
- Buffer.from(`${walletSignature}\n${passphrase}`, 'utf8'),
409
- salt,
410
- `ethagent:${BACKUP_ENVELOPE_VERSION}:ml-kem768:${address.toLowerCase()}`,
411
- 64,
412
- )
413
- }
414
-
415
- function deriveAesKey(
416
- passphrase: string,
417
- walletSignature: string,
418
- sharedSecret: Uint8Array,
419
- salt: Uint8Array,
420
- address: string,
421
- ): Buffer {
422
- return Buffer.from(hkdf(
423
- Buffer.concat([
424
- Buffer.from(walletSignature, 'utf8'),
425
- Buffer.from('\n', 'utf8'),
426
- Buffer.from(passphrase, 'utf8'),
427
- Buffer.from('\n', 'utf8'),
428
- Buffer.from(sharedSecret),
429
- ]),
430
- salt,
431
- `ethagent:${BACKUP_ENVELOPE_VERSION}:aes-256-gcm:${address.toLowerCase()}`,
432
- 32,
433
- ))
434
- }
435
-
436
212
  function deriveStateKemSeed(walletSignature: string, salt: Uint8Array, ownerAddress: string): Uint8Array {
437
213
  return hkdf(
438
214
  Buffer.from(walletSignature, 'utf8'),
@@ -464,19 +240,10 @@ function hkdf(ikm: Uint8Array, salt: Uint8Array, info: string, length: number):
464
240
  return new Uint8Array(crypto.hkdfSync('sha256', ikm, salt, Buffer.from(info, 'utf8'), length))
465
241
  }
466
242
 
467
- function aadFor(address: string, createdAt: string): Buffer {
468
- return Buffer.from(`${BACKUP_ENVELOPE_VERSION}\n${address.toLowerCase()}\n${createdAt}`, 'utf8')
469
- }
470
-
471
243
  function stateAadFor(ownerAddress: string, createdAt: string): Buffer {
472
244
  return Buffer.from(`${AGENT_STATE_BACKUP_ENVELOPE_VERSION}\n${ownerAddress.toLowerCase()}\n${createdAt}`, 'utf8')
473
245
  }
474
246
 
475
- function normalizedPrivateKey(privateKey: string): string {
476
- const trimmed = privateKey.trim()
477
- return trimmed.startsWith('0x') || trimmed.startsWith('0X') ? `0x${trimmed.slice(2)}` : `0x${trimmed}`
478
- }
479
-
480
247
  function toBase64(bytes: Uint8Array): string {
481
248
  return Buffer.from(bytes).toString('base64')
482
249
  }
@@ -16,11 +16,11 @@ function stripHex(input: string): string {
16
16
 
17
17
  function hexToBytes(hex: string): Uint8Array {
18
18
  const stripped = stripHex(hex)
19
- if (stripped.length % 2 !== 0) throw new Error('hex string has odd length')
19
+ if (stripped.length % 2 !== 0) throw new Error('Hex string has odd length')
20
20
  const out = new Uint8Array(stripped.length / 2)
21
21
  for (let i = 0; i < out.length; i += 1) {
22
22
  const byte = Number.parseInt(stripped.slice(i * 2, i * 2 + 2), 16)
23
- if (Number.isNaN(byte)) throw new Error('invalid hex')
23
+ if (Number.isNaN(byte)) throw new Error('Invalid hex')
24
24
  out[i] = byte
25
25
  }
26
26
  return out
@@ -82,14 +82,14 @@ export function validatePrivateKey(input: string): boolean {
82
82
  }
83
83
 
84
84
  export function addressFromPrivateKey(input: string): string {
85
- if (!validatePrivateKey(input)) throw new Error('invalid private key')
85
+ if (!validatePrivateKey(input)) throw new Error('Invalid private key')
86
86
  const bytes = hexToBytes(stripHex(input.trim()))
87
87
  const pub = secp256k1.getPublicKey(bytes, false)
88
88
  return addressFromPublicKey(pub)
89
89
  }
90
90
 
91
91
  export function toChecksumAddress(address: string): string {
92
- if (!ADDR_RE.test(address)) throw new Error('invalid address')
92
+ if (!ADDR_RE.test(address)) throw new Error('Invalid address')
93
93
  const lower = address.slice(2).toLowerCase()
94
94
  const hashHex = bytesToHex(keccak_256(new TextEncoder().encode(lower)))
95
95
  let out = '0x'
@@ -105,7 +105,7 @@ export function toChecksumAddress(address: string): string {
105
105
  }
106
106
 
107
107
  export function signMessage(privateKey: string, message: string | Uint8Array): string {
108
- if (!validatePrivateKey(privateKey)) throw new Error('invalid private key')
108
+ if (!validatePrivateKey(privateKey)) throw new Error('Invalid private key')
109
109
  const sk = hexToBytes(stripHex(privateKey.trim()))
110
110
  const digest = ethereumMessageDigest(message)
111
111
  const sig = secp256k1.sign(digest, sk, { prehash: false })
@@ -121,11 +121,11 @@ export function signMessage(privateKey: string, message: string | Uint8Array): s
121
121
  }
122
122
 
123
123
  export function recoverAddressFromSignature(message: string | Uint8Array, signature: string): string {
124
- if (!SIG_RE.test(signature)) throw new Error('invalid signature')
124
+ if (!SIG_RE.test(signature)) throw new Error('Invalid signature')
125
125
  const bytes = hexToBytes(stripHex(signature))
126
126
  const v = bytes[64]!
127
127
  const recovery = v >= 27 ? v - 27 : v
128
- if (recovery < 0 || recovery > 3) throw new Error('invalid recovery id')
128
+ if (recovery < 0 || recovery > 3) throw new Error('Invalid recovery id')
129
129
  const recovered = new Uint8Array(65)
130
130
  recovered[0] = recovery
131
131
  recovered.set(bytes.subarray(0, 64), 1)
@@ -0,0 +1,96 @@
1
+ export const AGENT_RECORD_KEYS = {
2
+ token: 'org.ethagent.token',
3
+ profile: 'org.ethagent.profile',
4
+ } as const
5
+
6
+ type AgentRecordKey = typeof AGENT_RECORD_KEYS[keyof typeof AGENT_RECORD_KEYS]
7
+
8
+ export const AGENT_RECORD_KEY_LIST: readonly AgentRecordKey[] = [
9
+ AGENT_RECORD_KEYS.token,
10
+ AGENT_RECORD_KEYS.profile,
11
+ ] as const
12
+
13
+ export const AGENT_RECORD_READ_KEY_LIST: readonly string[] = AGENT_RECORD_KEY_LIST
14
+
15
+ export type AgentEnsRecords = {
16
+ token?: string
17
+ profile?: string
18
+ }
19
+
20
+ export type AgentEnsRecordState = AgentEnsRecords
21
+
22
+ export type AgentRecordDiff = {
23
+ key: AgentRecordKey
24
+ field: keyof AgentEnsRecordState
25
+ current: string
26
+ next: string
27
+ changed: boolean
28
+ }
29
+
30
+ const FIELD_FOR_KEY: Record<AgentRecordKey, keyof AgentEnsRecords> = {
31
+ [AGENT_RECORD_KEYS.token]: 'token',
32
+ [AGENT_RECORD_KEYS.profile]: 'profile',
33
+ }
34
+
35
+ const LABEL_FOR_FIELD: Record<keyof AgentEnsRecordState, string> = {
36
+ token: 'Agent token',
37
+ profile: 'Agent profile',
38
+ }
39
+
40
+ export function recordsFromTextMap(text: Record<string, string>): AgentEnsRecordState {
41
+ return {
42
+ token: text[AGENT_RECORD_KEYS.token] ?? '',
43
+ profile: text[AGENT_RECORD_KEYS.profile] ?? '',
44
+ }
45
+ }
46
+
47
+ export function diffRecords(current: AgentEnsRecordState, next: AgentEnsRecords): AgentRecordDiff[] {
48
+ return AGENT_RECORD_KEY_LIST.map(key => {
49
+ const field = FIELD_FOR_KEY[key]
50
+ const currentValue = (current[field] ?? '').trim()
51
+ const nextValue = (next[field] ?? '').trim()
52
+ return {
53
+ key,
54
+ field,
55
+ current: currentValue,
56
+ next: nextValue,
57
+ changed: currentValue !== nextValue,
58
+ }
59
+ })
60
+ }
61
+
62
+ export function changedRecords(current: AgentEnsRecordState, next: AgentEnsRecords): Record<string, string> {
63
+ const out: Record<string, string> = {}
64
+ for (const diff of diffRecords(current, next)) {
65
+ if (diff.changed) out[diff.key] = diff.next
66
+ }
67
+ return out
68
+ }
69
+
70
+ export function recordLabel(field: keyof AgentEnsRecordState): string {
71
+ return LABEL_FOR_FIELD[field]
72
+ }
73
+
74
+ export function formatRecordValue(field: keyof AgentEnsRecordState, value: string): string {
75
+ if (field === 'profile' && value.startsWith('ipfs://')) {
76
+ const cid = value.slice('ipfs://'.length)
77
+ return cid.length > 18 ? `ipfs://${cid.slice(0, 10)}...${cid.slice(-6)}` : value
78
+ }
79
+ return value
80
+ }
81
+
82
+ export function buildAgentEnsRecords(args: {
83
+ chainId: number
84
+ identityRegistryAddress: string
85
+ agentId: string | undefined
86
+ agentCardCid: string | undefined
87
+ }): AgentEnsRecords {
88
+ const records: AgentEnsRecords = {}
89
+ if (args.agentId) {
90
+ records.token = `eip155:${args.chainId}:${args.identityRegistryAddress.toLowerCase()}:${args.agentId}`
91
+ }
92
+ if (args.agentCardCid) {
93
+ records.profile = `ipfs://${args.agentCardCid}`
94
+ }
95
+ return records
96
+ }
@@ -0,0 +1,38 @@
1
+ import { parseAbi, type Address } from 'viem'
2
+
3
+ export const ENS_REGISTRY_ADDRESS_MAINNET = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e' as Address
4
+ export const ENS_NAME_WRAPPER_ADDRESS_MAINNET = '0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401' as Address
5
+ export const ENS_PUBLIC_RESOLVER_ADDRESS_MAINNET = '0xF29100983E058B709F3D539b0c765937B804AC15' as Address
6
+ export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as Address
7
+ export const DEFAULT_TTL = 0n
8
+ export const DEFAULT_FUSES = 0
9
+ export const DEFAULT_EXPIRY = 0n
10
+
11
+ export const ENS_RPC_URLS = [
12
+ 'https://ethereum.publicnode.com',
13
+ 'https://eth.llamarpc.com',
14
+ 'https://rpc.ankr.com/eth',
15
+ ] as const
16
+
17
+ export const ENS_AUTOMATION_REGISTRY_ABI = parseAbi([
18
+ 'function owner(bytes32 node) view returns (address)',
19
+ 'function resolver(bytes32 node) view returns (address)',
20
+ 'function setResolver(bytes32 node, address resolver)',
21
+ 'function setSubnodeRecord(bytes32 node, bytes32 label, address owner, address resolver, uint64 ttl)',
22
+ ])
23
+
24
+ export const ENS_AUTOMATION_RESOLVER_ABI = parseAbi([
25
+ 'function addr(bytes32 node) view returns (address)',
26
+ 'function text(bytes32 node, string key) view returns (string)',
27
+ 'function setAddr(bytes32 node, address addr)',
28
+ 'function setText(bytes32 node, string key, string value)',
29
+ 'function multicall(bytes[] data) returns (bytes[])',
30
+ 'function approve(bytes32 node, address delegate, bool approved)',
31
+ 'function isApprovedFor(address owner, bytes32 node, address delegate) view returns (bool)',
32
+ ])
33
+
34
+ export const ENS_AUTOMATION_NAME_WRAPPER_ABI = parseAbi([
35
+ 'function ownerOf(uint256 id) view returns (address)',
36
+ 'function setResolver(bytes32 node, address resolver)',
37
+ 'function setSubnodeRecord(bytes32 parentNode, string label, address owner, address resolver, uint64 ttl, uint32 fuses, uint64 expiry)',
38
+ ])
@@ -0,0 +1,80 @@
1
+ import { getAddress, namehash, type Address } from 'viem'
2
+ import { isEthDomain, normalizeEthDomain, splitSubdomainName } from '../ensLookup.js'
3
+ import { ENS_NAME_WRAPPER_ADDRESS_MAINNET } from './contracts.js'
4
+ import {
5
+ createEnsAutomationClient,
6
+ isZero,
7
+ readOwner,
8
+ readWrappedOwner,
9
+ sameAddress,
10
+ shortHex,
11
+ } from './read.js'
12
+ import {
13
+ encodeDeleteSubnodeRegistry,
14
+ encodeDeleteSubnodeWrapped,
15
+ } from './transactions.js'
16
+ import type {
17
+ EnsSubdomainDeletePreflightArgs,
18
+ EnsSubdomainDeletePreflightResult,
19
+ } from './types.js'
20
+
21
+ export async function preflightDeleteSubdomain(args: EnsSubdomainDeletePreflightArgs): Promise<EnsSubdomainDeletePreflightResult> {
22
+ const fullName = normalizeEthDomain(args.fullName)
23
+ if (!isEthDomain(fullName)) {
24
+ return { ok: false, reason: 'invalid-name', detail: `${args.fullName} is not a valid .eth name` }
25
+ }
26
+ const parts = splitSubdomainName(fullName)
27
+ if (!parts) {
28
+ return { ok: false, reason: 'not-a-subdomain', detail: `${fullName} is not a subdomain` }
29
+ }
30
+ const expectedOwner = getAddress(args.expectedOwnerAddress)
31
+ const client = args.ensClient ?? createEnsAutomationClient()
32
+ const parentNode = namehash(parts.parent)
33
+ const fullNode = namehash(fullName)
34
+ let parentOwner: Address
35
+ try {
36
+ parentOwner = await readOwner(client, parentNode)
37
+ } catch (err: unknown) {
38
+ return { ok: false, reason: 'lookup-failed', detail: err instanceof Error ? err.message : String(err) }
39
+ }
40
+ if (isZero(parentOwner)) {
41
+ return { ok: false, reason: 'parent-not-owned', detail: `${parts.parent} does not have an ENS manager on Ethereum mainnet` }
42
+ }
43
+ const parentWrapped = sameAddress(parentOwner, ENS_NAME_WRAPPER_ADDRESS_MAINNET)
44
+ let parentOwnerAddress: Address
45
+ try {
46
+ parentOwnerAddress = parentWrapped ? await readWrappedOwner(client, parentNode) : getAddress(parentOwner)
47
+ } catch (err: unknown) {
48
+ return { ok: false, reason: 'lookup-failed', detail: err instanceof Error ? err.message : String(err) }
49
+ }
50
+ if (!sameAddress(parentOwnerAddress, expectedOwner)) {
51
+ return {
52
+ ok: false,
53
+ reason: 'parent-owner-mismatch',
54
+ detail: `${parts.parent} is managed by ${shortHex(parentOwnerAddress)}, not the connected wallet ${shortHex(expectedOwner)}`,
55
+ }
56
+ }
57
+ let childOwner: Address
58
+ try {
59
+ childOwner = await readOwner(client, fullNode)
60
+ } catch (err: unknown) {
61
+ return { ok: false, reason: 'lookup-failed', detail: err instanceof Error ? err.message : String(err) }
62
+ }
63
+ if (isZero(childOwner)) {
64
+ return { ok: false, reason: 'subdomain-missing', detail: `${fullName} is already cleared on Ethereum mainnet` }
65
+ }
66
+ const transaction = parentWrapped
67
+ ? encodeDeleteSubnodeWrapped(parts.parent, parts.label)
68
+ : encodeDeleteSubnodeRegistry(parts.parent, parts.label)
69
+ return {
70
+ ok: true,
71
+ plan: {
72
+ fullName,
73
+ parentName: parts.parent,
74
+ label: parts.label,
75
+ parentWrapped,
76
+ parentOwnerAddress,
77
+ transaction,
78
+ },
79
+ }
80
+ }
@@ -0,0 +1,14 @@
1
+ import { isEthDomain, normalizeEthDomain, splitSubdomainName } from '../ensLookup.js'
2
+
3
+ export function isRootEthName(value: string): boolean {
4
+ const normalized = normalizeEthDomain(value)
5
+ return isEthDomain(normalized) && normalized.split('.').length === 2
6
+ }
7
+
8
+ export function isValidSubdomainLabel(value: string): boolean {
9
+ return /^[a-z0-9-]+$/.test(value) && !value.startsWith('-') && !value.endsWith('-')
10
+ }
11
+
12
+ export function hasValidSubdomainParts(fullName: string): boolean {
13
+ return Boolean(splitSubdomainName(fullName))
14
+ }
@@ -0,0 +1,29 @@
1
+ import { getAddress, type Address } from 'viem'
2
+ import type { OperatorSetDiff } from './types.js'
3
+
4
+ export function compareOperatorSets(args: {
5
+ metadataOperators: ReadonlyArray<{ address: string }>
6
+ resolverDelegates: ReadonlyArray<string>
7
+ }): OperatorSetDiff {
8
+ const metadata = new Map<string, Address>()
9
+ for (const record of args.metadataOperators) {
10
+ metadata.set(record.address.toLowerCase(), getAddress(record.address))
11
+ }
12
+ const resolver = new Map<string, Address>()
13
+ for (const raw of args.resolverDelegates) {
14
+ resolver.set(raw.toLowerCase(), getAddress(raw))
15
+ }
16
+ const metadataOnly: Address[] = []
17
+ for (const [key, addr] of metadata) {
18
+ if (!resolver.has(key)) metadataOnly.push(addr)
19
+ }
20
+ const resolverOnly: Address[] = []
21
+ for (const [key, addr] of resolver) {
22
+ if (!metadata.has(key)) resolverOnly.push(addr)
23
+ }
24
+ return {
25
+ inSync: metadataOnly.length === 0 && resolverOnly.length === 0,
26
+ metadataOnly,
27
+ resolverOnly,
28
+ }
29
+ }