ethagent 2.1.1 → 2.2.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 (60) hide show
  1. package/package.json +1 -1
  2. package/src/auth/openaiOAuth/credentials.ts +47 -0
  3. package/src/auth/openaiOAuth/crypto.ts +23 -0
  4. package/src/auth/openaiOAuth/index.ts +238 -0
  5. package/src/auth/openaiOAuth/landingPage.ts +125 -0
  6. package/src/auth/openaiOAuth/listener.ts +151 -0
  7. package/src/auth/openaiOAuth/refresh.ts +70 -0
  8. package/src/auth/openaiOAuth/shared.ts +115 -0
  9. package/src/chat/chatSessionState.ts +2 -1
  10. package/src/chat/commands.ts +2 -1
  11. package/src/identity/ens/agentRecords.ts +5 -19
  12. package/src/identity/ens/ensAutomation/setup.ts +0 -1
  13. package/src/identity/ens/ensAutomation/types.ts +0 -1
  14. package/src/identity/hub/OperationalRoutes.tsx +2 -11
  15. package/src/identity/hub/components/IdentitySummary.tsx +8 -3
  16. package/src/identity/hub/components/MenuScreen.tsx +1 -2
  17. package/src/identity/hub/components/menuFlagsFromReconciliation.ts +1 -3
  18. package/src/identity/hub/effects/ens/transactions.ts +15 -15
  19. package/src/identity/hub/effects/index.ts +0 -1
  20. package/src/identity/hub/effects/profile/profileState.ts +12 -4
  21. package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +37 -159
  22. package/src/identity/hub/effects/rebackup/runRebackup.ts +2 -2
  23. package/src/identity/hub/effects/restoreAdmin.ts +2 -61
  24. package/src/identity/hub/effects/shared/sync.ts +3 -44
  25. package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +1 -39
  26. package/src/identity/hub/flows/custody/custodyFlowActions.ts +5 -3
  27. package/src/identity/hub/flows/custody/custodyFlowTypes.ts +1 -1
  28. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +80 -175
  29. package/src/identity/hub/flows/ens/EnsEditFlow.tsx +20 -75
  30. package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +16 -56
  31. package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +0 -18
  32. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +0 -136
  33. package/src/identity/hub/flows/ens/EnsEditShared.tsx +5 -4
  34. package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +56 -205
  35. package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +7 -0
  36. package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +0 -31
  37. package/src/identity/hub/flows/ens/ensEditCopy.ts +1 -1
  38. package/src/identity/hub/flows/ens/ensEditTypes.ts +6 -20
  39. package/src/identity/hub/flows/profile/EditProfileFlow.tsx +7 -0
  40. package/src/identity/hub/flows/restore/RestoreFlow.tsx +5 -5
  41. package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +0 -1
  42. package/src/identity/hub/reconciliation/agentReconciliation/run.ts +1 -34
  43. package/src/identity/hub/reconciliation/agentReconciliation/types.ts +0 -4
  44. package/src/identity/hub/reconciliation/index.ts +0 -7
  45. package/src/identity/hub/reconciliation/walletSetup.ts +1 -194
  46. package/src/identity/wallet/browserWallet/types.ts +0 -5
  47. package/src/identity/wallet/page/copy.ts +1 -31
  48. package/src/identity/wallet/walletPurposeCompat.ts +0 -2
  49. package/src/models/ModelPicker.tsx +246 -8
  50. package/src/models/catalog.ts +28 -1
  51. package/src/models/modelPickerOptions.ts +15 -1
  52. package/src/providers/openai-responses-format.ts +156 -0
  53. package/src/providers/openai-responses.ts +276 -0
  54. package/src/providers/registry.ts +85 -8
  55. package/src/runtime/systemPrompt.ts +1 -1
  56. package/src/runtime/turn.ts +0 -1
  57. package/src/storage/secrets.ts +4 -1
  58. package/src/tools/privateContinuityEditTool.ts +6 -0
  59. package/src/utils/openExternal.ts +20 -10
  60. package/src/identity/ens/ensRegistration.ts +0 -199
@@ -1,28 +1,18 @@
1
1
  import React from 'react'
2
2
  import { Box, Text } from 'ink'
3
- import { formatEther, type Address } from 'viem'
3
+ import { type Address } from 'viem'
4
4
  import { Surface } from '../../../../ui/Surface.js'
5
5
  import { Select, type SelectOption } from '../../../../ui/Select.js'
6
- import { TextInput } from '../../../../ui/TextInput.js'
7
6
  import { Spinner } from '../../../../ui/Spinner.js'
8
7
  import { theme } from '../../../../ui/theme.js'
9
8
  import {
10
- createMainnetClient,
11
9
  isEthDomain,
12
10
  normalizeEthDomain,
13
11
  } from '../../../ens/ensLookup.js'
14
- import {
15
- generateRegistrationSecret,
16
- ONE_YEAR_SECONDS,
17
- readNameAvailable,
18
- readRentPrice,
19
- validateRegistrableName,
20
- } from '../../../ens/ensRegistration.js'
21
12
  import { isRootEthName } from '../../../ens/ensAutomation.js'
22
13
  import type { Erc8004RegistryConfig } from '../../../registry/erc8004.js'
23
14
  import type { BrowserWalletReady } from '../../../wallet/browserWallet.js'
24
15
  import {
25
- readIdentityStateString,
26
16
  type CustodyMode,
27
17
  } from '../../model/custody.js'
28
18
  import { shortAddress } from '../../model/format.js'
@@ -30,8 +20,9 @@ import {
30
20
  recordsDiffHasChanges,
31
21
  type EnsLinkOptions,
32
22
  } from './ensEditCopy.js'
23
+ import { openExternalUrl } from '../../../../utils/openExternal.js'
24
+ import { TextInput } from '../../../../ui/TextInput.js'
33
25
  import {
34
- EnsSetupRow,
35
26
  EnsStatusBanner,
36
27
  footerHint,
37
28
  SubdomainEntry,
@@ -42,18 +33,15 @@ import {
42
33
  ReviewScreen,
43
34
  SimpleEnsIssueScreen,
44
35
  } from './EnsEditReviewScreens.js'
45
- import {
46
- EscCancel,
47
- RegisterRootCommitRunner,
48
- RegisterRootTxRunner,
49
- RegisterRootWaitScreen,
50
- } from './EnsEditRunners.js'
36
+ import { EscCancel } from './EnsEditRunners.js'
51
37
  import type {
52
38
  DiscoveryState,
53
39
  EnsEditProps,
54
40
  EnsPhase,
55
41
  } from './ensEditTypes.js'
56
42
 
43
+ const ENS_DOMAINS_URL = 'https://app.ens.domains'
44
+
57
45
  type SimpleScreenProps = {
58
46
  phase: EnsPhase
59
47
  discovery: DiscoveryState
@@ -69,8 +57,9 @@ type SimpleScreenProps = {
69
57
  setOperatorWalletSession: (session: BrowserWalletReady | null) => void
70
58
  setPhase: (phase: EnsPhase) => void
71
59
  cancelDiscoveryToModeSelect: () => void
72
- runDiscovery: () => void
60
+ runDiscovery: (mode?: 'simple' | 'advanced') => void
73
61
  runValidation: (fullName: string, mode: 'simple' | 'advanced', phaseOwnerAddress?: Address, operatorWallet?: Address) => Promise<void>
62
+ runAdvancedRootCheck: (rootName: string) => void
74
63
  backToSimpleSubdomain: (fullName: string) => void
75
64
  runSimpleCreatePreflight: (fullName: string) => void
76
65
  onEnsSetup: EnsEditProps['onEnsSetup']
@@ -96,6 +85,7 @@ export function renderSimpleEnsPhase({
96
85
  cancelDiscoveryToModeSelect,
97
86
  runDiscovery,
98
87
  runValidation,
88
+ runAdvancedRootCheck,
99
89
  backToSimpleSubdomain,
100
90
  runSimpleCreatePreflight,
101
91
  onEnsSetup,
@@ -126,7 +116,7 @@ export function renderSimpleEnsPhase({
126
116
  }
127
117
 
128
118
  if (phase.kind === 'pick-parent') {
129
- type DomainAction = `pick:${string}` | 'register-root' | 'manual' | 'retry' | 'back'
119
+ type DomainAction = `pick:${string}` | 'open-ens-domains' | 'manual' | 'retry' | 'back'
130
120
  const ownedNames = discovery.status === 'ok' || discovery.status === 'error' ? discovery.names : []
131
121
  const errorMessage = discovery.status === 'error' ? 'Root ENS Lookup Failed' : null
132
122
  const warningMessage = discovery.status === 'ok' ? discovery.warning : null
@@ -144,10 +134,10 @@ export function renderSimpleEnsPhase({
144
134
  })),
145
135
  ]
146
136
  : []),
147
- { value: 'register-root' as DomainAction, role: 'section' as const, label: 'Get a New One' },
148
- { value: 'register-root' as DomainAction, label: 'Register New ENS Name', hint: 'Register an ENS name through ENS to use as a root' },
149
- ...(noOwnedNames
150
- ? [{ value: 'retry' as DomainAction, label: 'Scan Again', hint: 'Retry the search after registering' }]
137
+ { value: 'open-ens-domains' as DomainAction, role: 'section' as const, label: 'No Parent Name?' },
138
+ { value: 'open-ens-domains' as DomainAction, label: 'Register .eth Name', hint: 'Open the ENS app in your browser; come back when this wallet owns one' },
139
+ ...(noOwnedNames || discovery.status === 'ok'
140
+ ? [{ value: 'retry' as DomainAction, label: 'Scan Again', hint: 'Re-run root .eth name discovery for this wallet' }]
151
141
  : []),
152
142
  ...(discovery.status === 'error'
153
143
  ? [
@@ -159,6 +149,7 @@ export function renderSimpleEnsPhase({
159
149
  { value: 'back', label: 'Back', hint: 'Return to setup type', role: 'utility' },
160
150
  ]
161
151
 
152
+ const advancedMode = phase.mode === 'advanced'
162
153
  return (
163
154
  <Surface
164
155
  title="Assign ENS Name"
@@ -166,198 +157,45 @@ export function renderSimpleEnsPhase({
166
157
  footer={footerHint('enter select · esc back')}
167
158
  >
168
159
  <EnsStatusBanner identity={identity} noRootEnsName={noOwnedNames} />
160
+ {advancedMode
161
+ ? <Text color={theme.dim}>Owner wallet: <Text color={theme.text}>{shortAddress(ownerAddress)}</Text></Text>
162
+ : null}
169
163
  {validationError ? <Text color={theme.accentError}>{validationError}</Text> : null}
164
+ {phase.error ? <Text color={theme.accentError}>{phase.error}</Text> : null}
170
165
  {errorMessage ? <Text color={theme.accentError}>{errorMessage}: {discovery.status === 'error' ? discovery.message : ''}</Text> : null}
171
166
  {warningMessage ? <Text color={theme.accentPeriwinkle}>{warningMessage}</Text> : null}
167
+ {noOwnedNames
168
+ ? (
169
+ <Box marginTop={1} flexDirection="column">
170
+ <Text color={theme.dim}>This wallet does not own a parent <Text color={theme.text}>.eth</Text> name yet.</Text>
171
+ <Text color={theme.dim}>Register one at <Text color={theme.text}>{ENS_DOMAINS_URL}</Text>, then come back and Scan Again.</Text>
172
+ </Box>
173
+ )
174
+ : null}
172
175
  <Box marginTop={1}>
173
176
  <Select<DomainAction>
174
177
  options={options}
175
178
  hintLayout="inline"
176
179
  onSubmit={choice => {
177
180
  if (choice === 'back') return setPhase({ kind: 'mode-select' })
178
- if (choice === 'manual') { setPhase({ kind: 'manual-parent' }); return }
179
- if (choice === 'register-root') { setPhase({ kind: 'register-root-input' }); return }
181
+ if (choice === 'manual') { setPhase({ kind: 'manual-parent', ...(advancedMode ? { mode: 'advanced' as const } : {}) }); return }
182
+ if (choice === 'open-ens-domains') {
183
+ openExternalUrl(ENS_DOMAINS_URL)
184
+ return
185
+ }
180
186
  if (choice === 'retry') {
181
- runDiscovery()
187
+ runDiscovery(advancedMode ? 'advanced' : 'simple')
182
188
  return
183
189
  }
184
190
  if (choice.startsWith('pick:')) {
185
191
  const name = choice.slice('pick:'.length)
186
- if (name) setPhase({ kind: 'pick-subdomain', parent: name })
187
- }
188
- }}
189
- onCancel={() => setPhase({ kind: 'mode-select' })}
190
- />
191
- </Box>
192
- </Surface>
193
- )
194
- }
195
-
196
- if (phase.kind === 'register-root-input') {
197
- return (
198
- <Surface
199
- title="Register an ENS Name"
200
- subtitle="Pick a name to register on Ethereum mainnet for 1 year. The connected wallet pays and becomes the owner."
201
- footer={footerHint('enter continues · esc back')}
202
- >
203
- {phase.error ? <Text color={theme.accentError}>{phase.error}</Text> : null}
204
- <Box marginTop={1}>
205
- <TextInput
206
- placeholder="myname"
207
- validate={value => {
208
- const result = validateRegistrableName(value)
209
- return result.ok ? null : result.detail
210
- }}
211
- onSubmit={value => {
212
- const result = validateRegistrableName(value)
213
- if (!result.ok) {
214
- setPhase({ kind: 'register-root-input', error: result.detail })
215
- return
216
- }
217
- const secret = generateRegistrationSecret()
218
- setPhase({ kind: 'register-root-pricing', label: result.label, secret, busy: true })
219
- const client = createMainnetClient()
220
- Promise.all([
221
- readNameAvailable(client, result.label),
222
- readRentPrice(client, result.label, BigInt(ONE_YEAR_SECONDS)),
223
- ])
224
- .then(([available, price]) => {
225
- if (!available) {
226
- setPhase({ kind: 'register-root-input', error: `${result.label}.eth is already registered. Pick a different name.` })
227
- return
228
- }
229
- setPhase({ kind: 'register-root-pricing', label: result.label, secret, busy: false, price })
230
- })
231
- .catch((err: unknown) => {
232
- setPhase({ kind: 'register-root-input', error: err instanceof Error ? err.message : String(err) })
233
- })
234
- }}
235
- onCancel={() => setPhase({ kind: 'pick-parent' })}
236
- />
237
- </Box>
238
- </Surface>
239
- )
240
- }
241
-
242
- if (phase.kind === 'register-root-pricing') {
243
- const fullName = `${phase.label}.eth`
244
- if (phase.busy || !phase.price) {
245
- return (
246
- <Surface
247
- title="Register an ENS Name"
248
- subtitle={`Checking availability and price for ${fullName}...`}
249
- footer={footerHint('esc back')}
250
- >
251
- <Box marginTop={1}><Text color={theme.textSubtle}>Reading from Ethereum mainnet...</Text></Box>
252
- </Surface>
253
- )
254
- }
255
- const totalEth = formatEther(phase.price.total)
256
- const baseEth = formatEther(phase.price.base)
257
- const premiumEth = formatEther(phase.price.premium)
258
- return (
259
- <Surface
260
- title="Register an ENS Name"
261
- subtitle={`${fullName} is available. Review the cost and continue with two transactions.`}
262
- footer={footerHint('enter select · esc back')}
263
- >
264
- <Box flexDirection="column">
265
- <EnsSetupRow label="Name" value={fullName} />
266
- <EnsSetupRow label="Owner" value={shortAddress(ownerAddress)} />
267
- <EnsSetupRow label="Duration" value="1 year" />
268
- <EnsSetupRow label="Rent" value={`${baseEth} ETH`} />
269
- {phase.price.premium > 0n ? <EnsSetupRow label="Premium" value={`${premiumEth} ETH`} /> : null}
270
- <EnsSetupRow label="Total" value={`${totalEth} ETH`} />
271
- <Box marginTop={1}><Text color={theme.dim}>Two transactions: a commit, a 60-second wait, then the registration. Keep this tab open through both.</Text></Box>
272
- <Box marginTop={1}><Text color={theme.dim}>Manage renewals on https://app.ens.domains/ before the year is up.</Text></Box>
273
- </Box>
274
- <Box marginTop={1}>
275
- <Select<'commit' | 'pick-different' | 'back'>
276
- options={[
277
- { value: 'commit', role: 'section', label: 'Continue' },
278
- { value: 'commit', label: 'Continue to Commit', hint: 'Sign the first transaction' },
279
- { value: 'pick-different', label: 'Pick a Different Name', hint: 'Return to name input' },
280
- { value: 'back', role: 'section', label: 'Navigation' },
281
- { value: 'back', label: 'Back', hint: 'Return to root selection', role: 'utility' },
282
- ]}
283
- hintLayout="inline"
284
- onSubmit={choice => {
285
- if (choice === 'commit') {
286
- setPhase({ kind: 'register-root-commit-tx', label: phase.label, secret: phase.secret, price: phase.price! })
287
- return
288
- }
289
- if (choice === 'pick-different') return setPhase({ kind: 'register-root-input' })
290
- setPhase({ kind: 'pick-parent' })
291
- }}
292
- onCancel={() => setPhase({ kind: 'pick-parent' })}
293
- />
294
- </Box>
295
- </Surface>
296
- )
297
- }
298
-
299
- if (phase.kind === 'register-root-commit-tx') {
300
- return (
301
- <RegisterRootCommitRunner
302
- phase={phase}
303
- ownerAddress={ownerAddress}
304
- walletSession={operatorWalletSession}
305
- onWalletReady={setOperatorWalletSession}
306
- onCommitted={() => setPhase({ kind: 'register-root-wait', label: phase.label, secret: phase.secret, price: phase.price, commitMinedAt: Date.now() })}
307
- onError={msg => setPhase({ kind: 'register-root-input', error: msg })}
308
- />
309
- )
310
- }
311
-
312
- if (phase.kind === 'register-root-wait') {
313
- return (
314
- <RegisterRootWaitScreen
315
- phase={phase}
316
- onReady={() => setPhase({ kind: 'register-root-tx', label: phase.label, secret: phase.secret, price: phase.price })}
317
- onCancel={() => setPhase({ kind: 'register-root-input', error: 'Commit cancelled. Names must be re-committed; secrets do not carry over.' })}
318
- />
319
- )
320
- }
321
-
322
- if (phase.kind === 'register-root-tx') {
323
- return (
324
- <RegisterRootTxRunner
325
- phase={phase}
326
- ownerAddress={ownerAddress}
327
- walletSession={operatorWalletSession}
328
- onWalletReady={setOperatorWalletSession}
329
- onRegistered={() => setPhase({ kind: 'register-root-done', fullName: `${phase.label}.eth` })}
330
- onError={msg => setPhase({ kind: 'register-root-input', error: msg })}
331
- />
332
- )
333
- }
334
-
335
- if (phase.kind === 'register-root-done') {
336
- return (
337
- <Surface
338
- title="ENS Name Registered"
339
- subtitle={`${phase.fullName} is now owned by ${shortAddress(ownerAddress)}.`}
340
- footer={footerHint('enter select · esc back')}
341
- >
342
- <Box flexDirection="column">
343
- <Text color={theme.text}>Manage renewals and other settings on https://app.ens.domains/ before the year is up.</Text>
344
- <Text color={theme.dim}>The name will appear in the picker on the next scan.</Text>
345
- </Box>
346
- <Box marginTop={1}>
347
- <Select<'scan' | 'back'>
348
- options={[
349
- { value: 'scan', role: 'section', label: 'Next' },
350
- { value: 'scan', label: 'Scan Again', hint: 'Re-run root ENS name discovery' },
351
- { value: 'back', role: 'section', label: 'Navigation' },
352
- { value: 'back', label: 'Back to ENS', hint: 'Return to ENS menu', role: 'utility' },
353
- ]}
354
- hintLayout="inline"
355
- onSubmit={choice => {
356
- if (choice === 'scan') {
357
- runDiscovery()
358
- return
192
+ if (!name) return
193
+ if (advancedMode) {
194
+ runAdvancedRootCheck(name)
195
+ return
196
+ }
197
+ setPhase({ kind: 'pick-subdomain', parent: name })
359
198
  }
360
- setPhase({ kind: 'mode-select' })
361
199
  }}
362
200
  onCancel={() => setPhase({ kind: 'mode-select' })}
363
201
  />
@@ -367,6 +205,7 @@ export function renderSimpleEnsPhase({
367
205
  }
368
206
 
369
207
  if (phase.kind === 'manual-parent') {
208
+ const advancedMode = phase.mode === 'advanced'
370
209
  return (
371
210
  <Surface
372
211
  title="Your Root .eth Name"
@@ -374,11 +213,15 @@ export function renderSimpleEnsPhase({
374
213
  footer={footerHint('enter continues · esc back')}
375
214
  >
376
215
  {statusBanner}
216
+ {advancedMode
217
+ ? <Text color={theme.dim}>Owner wallet: <Text color={theme.text}>{shortAddress(ownerAddress)}</Text></Text>
218
+ : null}
377
219
  <Box marginTop={1}>
378
220
  <Text color={theme.dim}>Only root .eth names on Ethereum mainnet are supported.</Text>
379
221
  </Box>
222
+ {phase.error ? <Text color={theme.accentError}>{phase.error}</Text> : null}
380
223
  <TextInput
381
- key="edit-ens-parent-manual"
224
+ key={`edit-ens-parent-manual-${advancedMode ? 'advanced' : 'simple'}`}
382
225
  placeholder="e.g. name.eth"
383
226
  validate={value => {
384
227
  const v = normalizeEthDomain(value)
@@ -387,8 +230,15 @@ export function renderSimpleEnsPhase({
387
230
  if (!isRootEthName(v)) return 'Enter a root .eth name, e.g. name.eth'
388
231
  return null
389
232
  }}
390
- onSubmit={value => setPhase({ kind: 'pick-subdomain', parent: normalizeEthDomain(value) })}
391
- onCancel={() => setPhase({ kind: 'pick-parent' })}
233
+ onSubmit={value => {
234
+ const root = normalizeEthDomain(value)
235
+ if (advancedMode) {
236
+ runAdvancedRootCheck(root)
237
+ return
238
+ }
239
+ setPhase({ kind: 'pick-subdomain', parent: root })
240
+ }}
241
+ onCancel={() => setPhase({ kind: 'pick-parent', ...(advancedMode ? { mode: 'advanced' as const } : {}) })}
392
242
  />
393
243
  </Surface>
394
244
  )
@@ -399,7 +249,8 @@ export function renderSimpleEnsPhase({
399
249
  <SubdomainEntry
400
250
  parent={phase.parent}
401
251
  ownerAddress={ownerAddress}
402
- suggestion={phase.label || agentNameSuggestion}
252
+ initialValue={phase.label}
253
+ placeholder={agentNameSuggestion || 'subdomain name'}
403
254
  error={phase.error}
404
255
  onConfirm={fullName => { void runValidation(fullName, 'simple') }}
405
256
  onBack={() => setPhase({ kind: 'pick-parent' })}
@@ -8,6 +8,7 @@ import { OperatorWalletsScreen } from './OperatorWalletsScreen.js'
8
8
  import { EDIT_PROFILE_STEPS, EditProfileFlow } from '../profile/EditProfileFlow.js'
9
9
  import { FlowTimeline } from '../../components/FlowTimeline.js'
10
10
  import { WalletApprovalScreen } from '../../components/WalletApprovalScreen.js'
11
+ import type { AgentReconciliation } from '../../reconciliation/index.js'
11
12
 
12
13
  type StepOf<K extends Step['kind']> = Extract<Step, { kind: K }>
13
14
 
@@ -27,11 +28,13 @@ type IdentityHubEnsStep = StepOf<
27
28
  type IdentityHubEnsFlowProps = {
28
29
  step: IdentityHubEnsStep
29
30
  walletSession: BrowserWalletReady | null
31
+ reconciliation: AgentReconciliation
30
32
  onSetStep: (step: Step) => void
31
33
  onBack: () => void
32
34
  onWalletReady: (session: BrowserWalletReady | null) => void
33
35
  onTriggerRebackup: (backStep: Step, profileUpdates?: ProfileUpdates) => void
34
36
  onTriggerPublicProfileSave: (backStep: Step, profileUpdates: ProfileUpdates) => void
37
+ onWithdrawTokenForEns: (step: Step) => void
35
38
  }
36
39
 
37
40
  export function isIdentityHubEnsStep(step: Step): step is IdentityHubEnsStep {
@@ -50,11 +53,13 @@ export function isIdentityHubEnsStep(step: Step): step is IdentityHubEnsStep {
50
53
  export const IdentityHubEnsFlow: React.FC<IdentityHubEnsFlowProps> = ({
51
54
  step,
52
55
  walletSession,
56
+ reconciliation,
53
57
  onSetStep,
54
58
  onBack,
55
59
  onWalletReady,
56
60
  onTriggerRebackup,
57
61
  onTriggerPublicProfileSave,
62
+ onWithdrawTokenForEns,
58
63
  }) => {
59
64
  if (step.kind === 'manage-ens-operators') {
60
65
  return (
@@ -75,6 +80,8 @@ export const IdentityHubEnsFlow: React.FC<IdentityHubEnsFlowProps> = ({
75
80
  return (
76
81
  <EditProfileFlow
77
82
  step={step}
83
+ reconciliation={reconciliation}
84
+ onWithdrawToken={() => onWithdrawTokenForEns(step)}
78
85
  onNameSubmit={name => {
79
86
  if (step.kind !== 'edit-profile-name') return
80
87
  onSetStep({
@@ -26,10 +26,6 @@ import {
26
26
  upsertApprovedOperatorWallet,
27
27
  type ApprovedOperatorWalletRecord,
28
28
  } from '../../operatorWallets.js'
29
- import {
30
- reconcileWalletSetup,
31
- type RecordsFixPlan,
32
- } from '../../reconciliation/index.js'
33
29
 
34
30
  type OperatorPhase =
35
31
  | { kind: 'main'; notice?: string; error?: string }
@@ -71,28 +67,11 @@ export const OperatorWalletsScreen: React.FC<OperatorWalletsScreenProps> = ({
71
67
  const restoreAccessEpoch = readStateNumber(state, 'restoreAccessEpoch') ?? 0
72
68
  const records = normalizeApprovedOperatorWallets(state.approvedOperatorWallets)
73
69
  const [phase, setPhase] = React.useState<OperatorPhase>({ kind: 'main', notice, error })
74
- const [fixPlan, setFixPlan] = React.useState<RecordsFixPlan | null>(null)
75
70
 
76
71
  React.useEffect(() => {
77
72
  setPhase(current => current.kind === 'main' ? { kind: 'main', notice, error } : current)
78
73
  }, [notice, error])
79
74
 
80
- React.useEffect(() => {
81
- if (custodyMode !== 'advanced' || records.length === 0) {
82
- setFixPlan(null)
83
- return
84
- }
85
- let cancelled = false
86
- reconcileWalletSetup({ identity, registry })
87
- .then(plan => { if (!cancelled) setFixPlan(plan) })
88
- .catch(() => { if (!cancelled) setFixPlan(null) })
89
- return () => { cancelled = true }
90
- }, [identity, registry, custodyMode, records.length])
91
-
92
- const driftItems = fixPlan?.items.filter(item =>
93
- item.kind === 'missing-approval' || item.kind === 'stale-approval',
94
- ) ?? []
95
-
96
75
  const saveOperators = React.useCallback((
97
76
  approvedOperatorWallets: ApprovedOperatorWalletRecord[],
98
77
  activeOperator: Address | '' | undefined,
@@ -253,16 +232,6 @@ export const OperatorWalletsScreen: React.FC<OperatorWalletsScreenProps> = ({
253
232
  <Text color={theme.dim}>Add as many operator wallets as needed; unlink any saved wallet here.</Text>
254
233
  <Text color={theme.dim}>No approve(), setApprovalForAll(), transferFrom(), or token approval is requested.</Text>
255
234
  </Box>
256
- {driftItems.length > 0
257
- ? (
258
- <Box marginTop={1} flexDirection="column">
259
- <Text color="#e8a070">
260
- {`! ENS resolver drift: ${driftItems.length} operator wallet${driftItems.length === 1 ? '' : 's'} missing onchain approval.`}
261
- </Text>
262
- <Text color={theme.dim}>Run "Fix Records" from Custody Mode to re-sync; onchain ENS writes will fail until then.</Text>
263
- </Box>
264
- )
265
- : null}
266
235
  {phaseNotice ? <Text color={theme.accentPeriwinkle}>{phaseNotice}</Text> : null}
267
236
  {phaseError ? <Text color={theme.accentError}>{phaseError}</Text> : null}
268
237
  </Box>
@@ -18,7 +18,7 @@ export function recordsHaveCurrentValues(recordsDiff: AgentRecordDiff[]): boolea
18
18
  }
19
19
 
20
20
  export function emptyAgentEnsRecords(): AgentEnsRecords {
21
- return { token: '', profile: '' }
21
+ return { token: '' }
22
22
  }
23
23
 
24
24
  export function unlinkEnsLinkOptions(savedCustodyMode: CustodyMode | undefined, savedOwnerAddress: string): EnsLinkOptions {
@@ -13,26 +13,15 @@ import type {
13
13
  EnsSubdomainDeletePlan,
14
14
  } from '../../../ens/ensAutomation.js'
15
15
  import type { Erc8004RegistryConfig } from '../../../registry/erc8004.js'
16
+ import type { AgentReconciliation } from '../../reconciliation/index.js'
16
17
  import type { EnsLinkOptions } from './ensEditCopy.js'
17
18
 
18
19
  export type EnsIssueValidation = Extract<EnsValidation, { ok: false }>
19
20
 
20
- export type RegisterCommitPhase = {
21
- label: string
22
- secret: `0x${string}`
23
- price: { base: bigint; premium: bigint; total: bigint }
24
- }
25
-
26
21
  export type SimpleEnsPhase =
27
- | { kind: 'discovering' }
28
- | { kind: 'pick-parent' }
29
- | { kind: 'manual-parent' }
30
- | { kind: 'register-root-input'; error?: string }
31
- | { kind: 'register-root-pricing'; label: string; secret: `0x${string}`; busy: boolean; error?: string; price?: { base: bigint; premium: bigint; total: bigint } }
32
- | { kind: 'register-root-commit-tx'; label: string; secret: `0x${string}`; price: { base: bigint; premium: bigint; total: bigint } }
33
- | { kind: 'register-root-wait'; label: string; secret: `0x${string}`; price: { base: bigint; premium: bigint; total: bigint }; commitMinedAt: number }
34
- | { kind: 'register-root-tx'; label: string; secret: `0x${string}`; price: { base: bigint; premium: bigint; total: bigint } }
35
- | { kind: 'register-root-done'; fullName: string }
22
+ | { kind: 'discovering'; mode?: 'simple' | 'advanced' }
23
+ | { kind: 'pick-parent'; mode?: 'simple' | 'advanced'; error?: string }
24
+ | { kind: 'manual-parent'; mode?: 'simple' | 'advanced'; error?: string }
36
25
  | { kind: 'pick-subdomain'; parent: string; label?: string; error?: string }
37
26
  | { kind: 'simple-name-missing'; fullName: string; validation: EnsIssueValidation }
38
27
  | { kind: 'simple-create-preflight'; rootName: string; label: string; fullName: string }
@@ -43,14 +32,9 @@ export type SimpleEnsPhase =
43
32
 
44
33
  export type AdvancedEnsPhase =
45
34
  | { kind: 'advanced-transfer-check' }
46
- | { kind: 'advanced-root'; rootName?: string; error?: string }
47
35
  | { kind: 'advanced-root-check'; rootName: string }
48
36
  | { kind: 'advanced-subdomain'; rootName: string; label?: string; error?: string }
49
37
  | { kind: 'advanced-subdomain-check'; rootName: string; label: string }
50
- | { kind: 'advanced-operator-wallet'; rootName: string; label: string; registryAction?: EnsRegistryAction; error?: string }
51
- | { kind: 'advanced-operator-wallet-manual'; rootName: string; label: string; error?: string }
52
- | { kind: 'advanced-operator-wallet-connecting'; rootName: string; label: string }
53
- | { kind: 'advanced-preflight'; rootName: string; label: string; operatorWallet: Address }
54
38
  | { kind: 'advanced-review'; setup: EnsSetupPlan }
55
39
  | { kind: 'advanced-manual'; fallback: EnsSetupBlockedPlan }
56
40
 
@@ -81,11 +65,13 @@ export type DiscoveryState =
81
65
  export type EnsEditProps = {
82
66
  identity: EthagentIdentity
83
67
  registry: Erc8004RegistryConfig
68
+ reconciliation: AgentReconciliation
84
69
  onEnsLink: (fullName: string, options: EnsLinkOptions) => void
85
70
  onEnsUnlink: () => void
86
71
  onEnsRecordsUpdate: (fullName: string, records: AgentEnsRecords, options: EnsLinkOptions, clearRecords?: boolean, currentRecords?: AgentEnsRecordState) => void
87
72
  onEnsSetup: (setup: EnsSetupPlan) => void
88
73
  onManageOperatorWalletAccess: () => void
74
+ onWithdrawToken: () => void
89
75
  initialView?: 'advanced'
90
76
  onBack: () => void
91
77
  }
@@ -11,9 +11,11 @@ import { readIdentityStateString } from '../../model/custody.js'
11
11
  import { FlowTimeline } from '../../components/FlowTimeline.js'
12
12
  import { validateAgentIconReference } from '../../../profile/agentIcon.js'
13
13
  import { EnsEditFlow, type EnsLinkOptions } from '../ens/EnsEditFlow.js'
14
+ import type { AgentReconciliation } from '../../reconciliation/index.js'
14
15
 
15
16
  type EditProfileFlowProps = {
16
17
  step: Extract<Step, { kind: 'edit-profile-name' | 'edit-profile-description' | 'edit-profile-image' | 'edit-profile-review' | 'edit-profile-ens' }>
18
+ reconciliation: AgentReconciliation
17
19
  onNameSubmit: (name: string) => void
18
20
  onDescriptionSubmit: (description: string) => void
19
21
  onIconSubmit: (iconPath?: string) => void
@@ -24,6 +26,7 @@ type EditProfileFlowProps = {
24
26
  onEnsRecordsUpdate: (fullName: string, records: AgentEnsRecords, options: EnsLinkOptions, clearRecords?: boolean, currentRecords?: AgentEnsRecordState) => void
25
27
  onEnsSetup: (setup: EnsSetupPlan) => void
26
28
  onManageOperatorWalletAccess: () => void
29
+ onWithdrawToken: () => void
27
30
  onBack: () => void
28
31
  onMenu: () => void
29
32
  }
@@ -35,6 +38,7 @@ const EDIT_DESCRIPTION_FOOTER = 'enter next · shift+enter newline · esc back'
35
38
 
36
39
  export const EditProfileFlow: React.FC<EditProfileFlowProps> = ({
37
40
  step,
41
+ reconciliation,
38
42
  onNameSubmit,
39
43
  onDescriptionSubmit,
40
44
  onIconSubmit,
@@ -45,6 +49,7 @@ export const EditProfileFlow: React.FC<EditProfileFlowProps> = ({
45
49
  onEnsRecordsUpdate,
46
50
  onEnsSetup,
47
51
  onManageOperatorWalletAccess,
52
+ onWithdrawToken,
48
53
  onBack,
49
54
  onMenu,
50
55
  }) => {
@@ -90,11 +95,13 @@ export const EditProfileFlow: React.FC<EditProfileFlowProps> = ({
90
95
  <EnsEditFlow
91
96
  identity={step.identity}
92
97
  registry={step.registry}
98
+ reconciliation={reconciliation}
93
99
  onEnsLink={onEnsLink}
94
100
  onEnsUnlink={onEnsUnlink}
95
101
  onEnsRecordsUpdate={onEnsRecordsUpdate}
96
102
  onEnsSetup={onEnsSetup}
97
103
  onManageOperatorWalletAccess={onManageOperatorWalletAccess}
104
+ onWithdrawToken={onWithdrawToken}
98
105
  initialView={step.initialView}
99
106
  onBack={onBack}
100
107
  />
@@ -108,8 +108,8 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
108
108
  <Select<'ens' | 'token-id' | 'back'>
109
109
  options={[
110
110
  { value: 'ens', role: 'section', label: 'Recovery Key' },
111
- { value: 'ens', label: 'Enter ENS Name', hint: 'Resolve the agent via its ENS subdomain (e.g. agent.example.eth)' },
112
- { value: 'token-id', label: 'Enter Token ID', hint: 'Look up the agent directly by ERC-8004 token ID (e.g. 45744)' },
111
+ { value: 'ens', label: 'Enter ENS Name', hint: 'Resolve the agent via its ENS subdomain' },
112
+ { value: 'token-id', label: 'Enter Token ID', hint: 'Look up the agent directly by ERC-8004 token ID' },
113
113
  { value: 'back', role: 'section', label: 'Navigation' },
114
114
  { value: 'back', label: 'Back', hint: 'Pick a different network', role: 'utility' },
115
115
  ]}
@@ -146,7 +146,7 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
146
146
  subtitle={`Enter the ERC-8004 token ID on ${networkLabelForRegistry(step.registry)}.`}
147
147
  footer={footerHint(step.busy ? 'Looking up...' : 'enter continue · esc back')}
148
148
  >
149
- <Text color={theme.dim}>The integer token ID assigned at mint (for example, 45744). Store it alongside your wallet seed so the agent stays recoverable even if its ENS record is cleared.</Text>
149
+ <Text color={theme.dim}>The integer token ID assigned at mint.</Text>
150
150
  <TextInput
151
151
  placeholder="45744"
152
152
  onSubmit={value => onTokenIdSubmit(value.trim())}
@@ -202,8 +202,8 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
202
202
  }
203
203
  }),
204
204
  { value: '__ens__', role: 'section', label: 'Recovery Key' },
205
- { value: '__ens__', label: 'Enter ENS Name', hint: 'Resolve the agent via its ENS subdomain (e.g. agent.example.eth)' },
206
- { value: '__token-id__', label: 'Enter Token ID', hint: 'Look up the agent directly by ERC-8004 token ID (e.g. 45744)' },
205
+ { value: '__ens__', label: 'Enter ENS Name', hint: 'Resolve the agent via its ENS subdomain' },
206
+ { value: '__token-id__', label: 'Enter Token ID', hint: 'Look up the agent directly by ERC-8004 token ID' },
207
207
  { value: '__back__', role: 'section', label: 'Navigation' },
208
208
  { value: '__back__', label: 'Back', hint: 'Return to the previous step', role: 'utility' },
209
209
  ]}
@@ -10,7 +10,6 @@ export function useAgentReconciliation(
10
10
  token: 'no-agent',
11
11
  custody: 'unknown',
12
12
  agentUri: 'unknown',
13
- ensRecords: 'unset',
14
13
  vault: 'unset',
15
14
  workingTree: 'unknown',
16
15
  rpc: 'reachable',