ethagent 3.3.0 → 3.3.1

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.
package/README.md CHANGED
@@ -53,7 +53,7 @@ The Identity Hub manages everything portable about the agent:
53
53
  - **Custody Mode** switches between Simple and Advanced by depositing the token into its Vault or unwrapping it back out.
54
54
  - **Prepare Transfer** stages a dual-wallet snapshot so the receiver can restore the agent after the token moves externally.
55
55
  - **Refetch Latest** pulls the most recent published snapshot back to local files.
56
- - **Switch Agent** accepts either an ENS name or a bare token ID, and loads any agent owned by or linked to the connected wallet.
56
+ - **Switch Agent** accepts an ENS name or an ERC-8004 token ID on any supported chain, and loads any agent owned by or linked to the connected wallet.
57
57
 
58
58
  The menu surfaces drift automatically. Token ownership, vault state, ENS record alignment, and pending URI rotations are checked against the live chain when the menu opens.
59
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ethagent",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
4
4
  "description": "A privacy-first AI agent with a portable Ethereum identity",
5
5
  "type": "module",
6
6
  "main": "bin/ethagent.js",
@@ -281,7 +281,6 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
281
281
  onWalletReady={setWalletSession}
282
282
  onTriggerRebackup={triggerRebackup}
283
283
  onTriggerPublicProfileSave={triggerPublicProfileSave}
284
- onWithdrawTokenForEns={currentStep => custodyFlow.beginWithdrawToken(currentStep, currentStep, 'ens')}
285
284
  />
286
285
  )
287
286
  }
@@ -83,7 +83,7 @@ export const IdentityHubRoutes: React.FC<{ controller: IdentityHubController }>
83
83
  <Select<'ens' | 'skip'>
84
84
  options={[
85
85
  { value: 'ens', role: 'section', label: 'Set Up Now' },
86
- { value: 'ens', label: 'Set Up ENS Name', hint: 'Walks you through Root, Name, Review, and Apply' },
86
+ { value: 'ens', label: 'Set Up ENS Name', hint: 'Root Name Review Apply' },
87
87
  { value: 'skip', role: 'section', label: 'Skip' },
88
88
  { value: 'skip', label: 'Skip For Now', hint: 'Continue to model setup; add ENS later', role: 'utility' },
89
89
  ]}
@@ -46,7 +46,6 @@ type AdvancedScreenProps = {
46
46
  runAdvancedSubdomainCheck: (rootName: string, label: string) => void
47
47
  onEnsSetup: EnsEditProps['onEnsSetup']
48
48
  onEnsLink: EnsEditProps['onEnsLink']
49
- onWithdrawToken: () => void
50
49
  }
51
50
 
52
51
  export function renderAdvancedEnsPhase({
@@ -65,82 +64,7 @@ export function renderAdvancedEnsPhase({
65
64
  runAdvancedSubdomainCheck,
66
65
  onEnsSetup,
67
66
  onEnsLink,
68
- onWithdrawToken,
69
67
  }: AdvancedScreenProps): React.ReactNode | null {
70
- if (phase.kind === 'advanced-transfer-check') {
71
- type TransferCheckAction = 'continue' | 'withdraw' | 'back'
72
- const custody = reconciliation.custody
73
- const tokenInVault = custody === 'advanced' || custody === 'mid-flow-uri-pending'
74
- const tokenInOwnerWallet = custody === 'simple' || custody === 'withdrawn'
75
- const probePending = custody === 'unknown' && reconciliation.rpc !== 'failing'
76
- const probeFailed = reconciliation.rpc === 'failing'
77
-
78
- const options: Array<{ value: TransferCheckAction; role?: 'section' | 'utility'; label: string; hint?: string }> = []
79
- options.push({ value: 'continue', role: 'section', label: 'Setup' })
80
- if (tokenInOwnerWallet) {
81
- options.push({
82
- value: 'continue',
83
- label: 'Continue ENS Setup',
84
- hint: 'Owner wallet holds this token onchain.',
85
- })
86
- } else if (tokenInVault) {
87
- options.push({
88
- value: 'withdraw',
89
- label: 'Withdraw Token',
90
- hint: 'Pull token out to sign ENS records. Redeposit to the Vault any time after.',
91
- })
92
- } else if (probePending) {
93
- options.push({
94
- value: 'continue',
95
- label: 'Checking onchain state…',
96
- hint: 'Try again in a moment.',
97
- })
98
- } else if (probeFailed) {
99
- options.push({
100
- value: 'continue',
101
- label: 'Onchain check unavailable',
102
- hint: 'RPC unreachable. Resolve connectivity, then retry.',
103
- })
104
- } else {
105
- options.push({
106
- value: 'continue',
107
- label: 'Token Owner Unknown',
108
- hint: 'Try again in a moment.',
109
- })
110
- }
111
- options.push({ value: 'back', role: 'section', label: 'Navigation' })
112
- options.push({ value: 'back', label: 'Back', hint: 'Return to setup type', role: 'utility' })
113
-
114
- return (
115
- <Surface
116
- title="Token Custody Check"
117
- subtitle="ENS setup continues only after the owner wallet holds this token onchain."
118
- footer={footerHint('enter select · esc back')}
119
- >
120
- <Box marginTop={1}>
121
- <Select<TransferCheckAction>
122
- options={options}
123
- hintLayout="inline"
124
- onSubmit={choice => {
125
- if (choice === 'withdraw') {
126
- onWithdrawToken()
127
- return
128
- }
129
- if (choice === 'continue' && tokenInOwnerWallet) {
130
- runDiscovery('advanced')
131
- return
132
- }
133
- if (choice === 'back') {
134
- return setPhase({ kind: 'mode-select' })
135
- }
136
- }}
137
- onCancel={() => setPhase({ kind: 'mode-select' })}
138
- />
139
- </Box>
140
- </Surface>
141
- )
142
- }
143
-
144
68
  if (phase.kind === 'advanced-root-check') {
145
69
  return (
146
70
  <Surface
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  import { getAddress, type Address } from 'viem'
3
3
  import type { BrowserWalletReady } from '../../wallet/browserWallet.js'
4
4
  import {
5
+ AGENT_TOKEN_RECORD_KEY,
5
6
  buildAgentEnsRecords,
6
7
  buildEnsip25Key,
7
8
  diffRecords,
@@ -19,6 +20,7 @@ import {
19
20
  preflightEnsRoot,
20
21
  preflightEnsSetup,
21
22
  } from '../../ens/ensAutomation.js'
23
+ import { SUPPORTED_ERC8004_CHAINS } from '../../registry/erc8004.js'
22
24
  import {
23
25
  readCustodyMode,
24
26
  readIdentityStateString,
@@ -50,7 +52,6 @@ export const EnsEditFlow: React.FC<EnsEditProps> = ({
50
52
  onEnsRecordsUpdate,
51
53
  onEnsSetup,
52
54
  onManageOperatorWalletAccess,
53
- onWithdrawToken,
54
55
  initialView,
55
56
  onBack,
56
57
  }) => {
@@ -68,7 +69,7 @@ export const EnsEditFlow: React.FC<EnsEditProps> = ({
68
69
 
69
70
  const [discovery, setDiscovery] = React.useState<DiscoveryState>({ status: 'idle' })
70
71
  const [phase, setPhase] = React.useState<EnsPhase>(() => {
71
- if (initialView === 'advanced' && !hasAdvancedSetup) return { kind: 'advanced-transfer-check' }
72
+ if (initialView === 'advanced' && !hasAdvancedSetup) return { kind: 'pick-parent', mode: 'advanced' }
72
73
  return { kind: 'mode-select' }
73
74
  })
74
75
  const [validationError, setValidationError] = React.useState<string | null>(null)
@@ -138,11 +139,14 @@ export const EnsEditFlow: React.FC<EnsEditProps> = ({
138
139
  try {
139
140
  const validation = await validateAgentEnsLink(fullName, ownerAddress)
140
141
  const readKeys = identity.agentId
141
- ? [buildEnsip25Key({
142
- chainId: registry.chainId,
143
- identityRegistryAddress: registry.identityRegistryAddress,
144
- agentId: identity.agentId,
145
- })]
142
+ ? [
143
+ ...SUPPORTED_ERC8004_CHAINS.map(chain => buildEnsip25Key({
144
+ chainId: chain.chainId,
145
+ identityRegistryAddress: registry.identityRegistryAddress,
146
+ agentId: identity.agentId!,
147
+ })),
148
+ AGENT_TOKEN_RECORD_KEY,
149
+ ]
146
150
  : []
147
151
  const currentText = validation.ok && readKeys.length > 0
148
152
  ? await readEthagentTextRecords(fullName, readKeys)
@@ -260,11 +264,14 @@ export const EnsEditFlow: React.FC<EnsEditProps> = ({
260
264
  setValidationError(null)
261
265
  setPhase({ kind: 'unlink-loading', fullName })
262
266
  const readKeys = identity.agentId
263
- ? [buildEnsip25Key({
264
- chainId: registry.chainId,
265
- identityRegistryAddress: registry.identityRegistryAddress,
266
- agentId: identity.agentId,
267
- })]
267
+ ? [
268
+ ...SUPPORTED_ERC8004_CHAINS.map(chain => buildEnsip25Key({
269
+ chainId: chain.chainId,
270
+ identityRegistryAddress: registry.identityRegistryAddress,
271
+ agentId: identity.agentId!,
272
+ })),
273
+ AGENT_TOKEN_RECORD_KEY,
274
+ ]
268
275
  : []
269
276
  readEthagentTextRecords(fullName, readKeys)
270
277
  .then(currentText => {
@@ -321,7 +328,6 @@ export const EnsEditFlow: React.FC<EnsEditProps> = ({
321
328
  runAdvancedSubdomainCheck,
322
329
  onEnsSetup,
323
330
  onEnsLink,
324
- onWithdrawToken,
325
331
  })
326
332
  if (advancedScreen) return advancedScreen
327
333
 
@@ -82,11 +82,11 @@ export function renderEnsMaintenancePhase({
82
82
  const linkHint = multiNeedsCustodySetup
83
83
  ? 'Set Advanced custody first via Custody Mode'
84
84
  : isAdvanced
85
- ? 'Walks you through Root, Name, Review, and Apply'
86
- : 'Walks you through Root, Name, Review, and Apply'
85
+ ? 'Root Name Review Apply'
86
+ : 'Root Name Review Apply'
87
87
  const options: Array<{ value: EnsAction; role?: 'section' | 'utility'; label: string; hint?: string; disabled?: boolean }> = []
88
88
  if (currentEnsName) {
89
- options.push({ value: 'unlink', label: 'Unlink Name', hint: 'Removes this name from the token. Set up a different name afterward by linking again.' })
89
+ options.push({ value: 'unlink', label: 'Unlink Name', hint: 'Removes the name from the token. Link a different one anytime.' })
90
90
  } else {
91
91
  options.push({
92
92
  value: 'link',
@@ -116,10 +116,6 @@ export function renderEnsMaintenancePhase({
116
116
  }
117
117
  if (choice === 'link') {
118
118
  if (multiNeedsCustodySetup) return
119
- if (isAdvanced && savedOwnerAddress) {
120
- setPhase({ kind: 'advanced-transfer-check' })
121
- return
122
- }
123
119
  runDiscovery()
124
120
  return
125
121
  }
@@ -21,9 +21,9 @@ import {
21
21
  import { ensValidationReasonText } from './state.js'
22
22
  import { shortAddress } from '../shared/model/format.js'
23
23
  import {
24
+ abbreviateHexBlobs,
24
25
  manualReasonTitle,
25
26
  modeSwitchHeading,
26
- setupSwitchNotice,
27
27
  } from './editCopy.js'
28
28
  import {
29
29
  EnsSetupRow,
@@ -112,7 +112,6 @@ export const EnsSetupReviewScreen: React.FC<EnsSetupReviewScreenProps> = ({
112
112
  type Action = 'begin' | 'back'
113
113
  const isSimple = setup.mode === 'simple'
114
114
  const signerLabel = isSimple ? 'Connected wallet' : 'Owner wallet'
115
- const switchNotice = setupSwitchNotice(currentEnsName, currentMode, setup.fullName, setup.mode)
116
115
  const createLabel = setup.registryAction === 'create-subdomain'
117
116
  ? 'Create Subdomain'
118
117
  : setup.registryAction === 'create-wrapped-subdomain'
@@ -138,7 +137,6 @@ export const EnsSetupReviewScreen: React.FC<EnsSetupReviewScreenProps> = ({
138
137
  ) : null}
139
138
  <Box flexDirection="column">
140
139
  <Text color={theme.dim}>{modeSwitchHeading(currentEnsName, currentMode, setup.fullName, setup.mode)}</Text>
141
- {switchNotice ? <Text color={theme.dim}>{switchNotice}</Text> : null}
142
140
  <EnsSetupRow label="ENS name" value={setup.fullName} />
143
141
  <EnsSetupRow label="Parent root" value={setup.rootName} />
144
142
  <EnsSetupRow label="Subdomain label" value={setup.label} />
@@ -270,8 +268,8 @@ export const UnlinkEnsReviewScreen: React.FC<UnlinkEnsReviewScreenProps> = ({
270
268
  <Text color={theme.textSubtle}>Will be cleared:</Text>
271
269
  {changedDiffs.map(diff => (
272
270
  <Text key={diff.key}>
273
- <Text color={theme.dim}>{` ${diff.key} `}</Text>
274
- <Text color={theme.accentPeriwinkle}>{diff.current}</Text>
271
+ <Text color={theme.dim}>{` ${abbreviateHexBlobs(diff.key)} `}</Text>
272
+ <Text color={theme.accentPeriwinkle}>{abbreviateHexBlobs(diff.current)}</Text>
275
273
  </Text>
276
274
  ))}
277
275
  </Box>
@@ -342,7 +340,6 @@ export const ReviewScreen: React.FC<ReviewScreenProps> = ({
342
340
  const changedDiffs = recordsDiff.filter(d => d.changed)
343
341
  const hasRecordChanges = changedDiffs.length > 0
344
342
  const reviewSubtitle = 'Review Ethereum Mainnet ENS address and text records before saving this ENS link. No token approval is requested.'
345
- const switchNotice = setupSwitchNotice(currentEnsName, currentMode, fullName, mode)
346
343
 
347
344
  if (!validation.ok) {
348
345
  const reason = ensValidationReasonText(validation.reason)
@@ -396,7 +393,7 @@ export const ReviewScreen: React.FC<ReviewScreenProps> = ({
396
393
 
397
394
  const options: Array<SelectOption<ReviewAction>> = [
398
395
  { value: 'continue', role: 'section', label: modeSwitchHeading(currentEnsName, currentMode, fullName, mode) },
399
- { value: 'continue', label: 'Continue Setup', hint: hasRecordChanges ? `Ethereum Mainnet: sign ${changedDiffs.length} ENS record value${changedDiffs.length === 1 ? '' : 's'}; ${registryNetworkLabel}: save token URI` : `ENS records already match; ${registryNetworkLabel}: save token URI` },
396
+ { value: 'continue', label: 'Continue Setup', hint: hasRecordChanges ? `Ethereum Mainnet: sign ${changedDiffs.length} record${changedDiffs.length === 1 ? '' : 's'}; ${registryNetworkLabel}: save token URI` : `Records match; ${registryNetworkLabel}: save token URI` },
400
397
  { value: 'change', label: 'Pick A Different Name', hint: 'Return to the name picker' },
401
398
  { value: 'back', role: 'section', label: 'Navigation' },
402
399
  { value: 'back', label: 'Back', hint: 'Return to Identity Hub', role: 'utility' },
@@ -414,13 +411,12 @@ export const ReviewScreen: React.FC<ReviewScreenProps> = ({
414
411
  <Box marginBottom={1} flexDirection="column">
415
412
  <Text color={theme.dim}>Current: <Text color={currentEnsName ? theme.text : theme.dim}>{currentEnsName || 'None'}</Text></Text>
416
413
  <Text color={theme.dim}>Next: <Text color={theme.text}>{fullName}</Text></Text>
417
- {switchNotice ? <Text color={theme.dim}>{switchNotice}</Text> : null}
418
414
  </Box>
419
415
  )
420
416
  : null}
421
417
  {recordsDiff.map(diff => (
422
418
  <Text key={diff.key}>
423
- <Text color={theme.dim}>{`- ${diff.key}: `}</Text>
419
+ <Text color={theme.dim}>{`- ${abbreviateHexBlobs(diff.key)}: `}</Text>
424
420
  {diff.changed
425
421
  ? (
426
422
  <>
@@ -20,9 +20,11 @@ import type { EnsEditProps } from './types.js'
20
20
 
21
21
  export const footerHint = (hint: string) => <Text color={theme.dim}>{hint}</Text>
22
22
 
23
+ import { abbreviateHexBlobs } from './editCopy.js'
24
+
23
25
  export const renderRecordValue = (value: string) =>
24
26
  value
25
- ? <Text color={theme.accentPeriwinkle}>{value}</Text>
27
+ ? <Text color={theme.accentPeriwinkle}>{abbreviateHexBlobs(value)}</Text>
26
28
  : <Text color={theme.dim}>Unset</Text>
27
29
 
28
30
  export function rootErrorMessage(
@@ -135,7 +135,7 @@ export function renderSimpleEnsPhase({
135
135
  ]
136
136
  : []),
137
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' },
138
+ { value: 'open-ens-domains' as DomainAction, label: 'Register .eth Name', hint: 'Opens ENS app; return once this wallet owns one' },
139
139
  ...(noOwnedNames || discovery.status === 'ok'
140
140
  ? [{ value: 'retry' as DomainAction, label: 'Scan Again', hint: 'Re-run root .eth name discovery for this wallet' }]
141
141
  : []),
@@ -356,7 +356,7 @@ export function renderSimpleEnsPhase({
356
356
  onEnsLink(phase.fullName, linkOptions)
357
357
  }}
358
358
  onCheckAgain={() => { void runValidation(phase.fullName, phase.mode, phase.ownerAddress, phase.operatorWallet) }}
359
- onChange={() => setPhase(phase.mode === 'advanced' ? { kind: 'advanced-transfer-check' } : { kind: 'pick-parent' })}
359
+ onChange={() => setPhase({ kind: 'pick-parent', mode: phase.mode })}
360
360
  onCreate={phase.mode === 'simple' && !phase.validation.ok && phase.validation.reason === 'no-owner'
361
361
  ? () => runSimpleCreatePreflight(phase.fullName)
362
362
  : undefined}
@@ -34,7 +34,6 @@ type EnsFlowProps = {
34
34
  onWalletReady: (session: BrowserWalletReady | null) => void
35
35
  onTriggerRebackup: (backStep: Step, profileUpdates?: ProfileUpdates) => void
36
36
  onTriggerPublicProfileSave: (backStep: Step, profileUpdates: ProfileUpdates) => void
37
- onWithdrawTokenForEns: (step: Step) => void
38
37
  }
39
38
 
40
39
  export function isEnsStep(step: Step): step is IdentityHubEnsStep {
@@ -59,7 +58,6 @@ export const EnsFlow: React.FC<EnsFlowProps> = ({
59
58
  onWalletReady,
60
59
  onTriggerRebackup,
61
60
  onTriggerPublicProfileSave,
62
- onWithdrawTokenForEns,
63
61
  }) => {
64
62
  if (step.kind === 'manage-ens-operators') {
65
63
  return (
@@ -81,7 +79,6 @@ export const EnsFlow: React.FC<EnsFlowProps> = ({
81
79
  <EditProfileFlow
82
80
  step={step}
83
81
  reconciliation={reconciliation}
84
- onWithdrawToken={() => onWithdrawTokenForEns(step)}
85
82
  onNameSubmit={name => {
86
83
  if (step.kind !== 'edit-profile-name') return
87
84
  onSetStep({
@@ -3,6 +3,12 @@ import type { AgentEnsRecords, AgentRecordDiff } from '../../ens/agentRecords.js
3
3
  import type { EnsRegistryAction, EnsSetupBlockedPlan } from '../../ens/ensAutomation.js'
4
4
  import type { CustodyMode } from '../custody/state.js'
5
5
 
6
+ export function abbreviateHexBlobs(input: string): string {
7
+ return input.replace(/0x([0-9a-fA-F]{20,})/g, (_match, hex) => {
8
+ return `0x${hex.slice(0, 8)}...${hex.slice(-8)}`
9
+ })
10
+ }
11
+
6
12
  export type EnsLinkOptions = {
7
13
  mode: 'simple' | 'advanced'
8
14
  ownerAddress?: Address
@@ -46,18 +52,6 @@ export function modeSwitchHeading(
46
52
  return 'Automation'
47
53
  }
48
54
 
49
- export function setupSwitchNotice(
50
- currentEnsName: string,
51
- currentMode: CustodyMode | undefined,
52
- nextEnsName: string,
53
- nextMode: 'simple' | 'advanced',
54
- ): string | null {
55
- if (!currentEnsName && !currentMode) return null
56
- const currentTopology = currentMode === 'advanced' ? 'advanced' : currentMode === 'simple' ? 'simple' : undefined
57
- if (currentEnsName === nextEnsName && currentTopology === nextMode) return null
58
- return 'This replaces the saved ENS setup directly. Reset is only for clearing the current link.'
59
- }
60
-
61
55
  export function advancedSubdomainStatusText(action: EnsRegistryAction): string {
62
56
  switch (action) {
63
57
  case 'create-subdomain':
@@ -31,7 +31,6 @@ export type SimpleEnsPhase =
31
31
  | { kind: 'review'; fullName: string; validation: EnsValidation; recordsDiff: AgentRecordDiff[]; currentRecords: AgentEnsRecordState; nextRecords: AgentEnsRecords; mode: 'simple' | 'advanced'; ownerAddress?: Address; operatorWallet?: Address }
32
32
 
33
33
  export type AdvancedEnsPhase =
34
- | { kind: 'advanced-transfer-check' }
35
34
  | { kind: 'advanced-root-check'; rootName: string }
36
35
  | { kind: 'advanced-subdomain'; rootName: string; label?: string; error?: string }
37
36
  | { kind: 'advanced-subdomain-check'; rootName: string; label: string }
@@ -71,7 +70,6 @@ export type EnsEditProps = {
71
70
  onEnsRecordsUpdate: (fullName: string, records: AgentEnsRecords, options: EnsLinkOptions, clearRecords?: boolean, currentRecords?: AgentEnsRecordState) => void
72
71
  onEnsSetup: (setup: EnsSetupPlan) => void
73
72
  onManageOperatorWalletAccess: () => void
74
- onWithdrawToken: () => void
75
73
  initialView?: 'advanced'
76
74
  onBack: () => void
77
75
  }
@@ -26,7 +26,6 @@ type EditProfileFlowProps = {
26
26
  onEnsRecordsUpdate: (fullName: string, records: AgentEnsRecords, options: EnsLinkOptions, clearRecords?: boolean, currentRecords?: AgentEnsRecordState) => void
27
27
  onEnsSetup: (setup: EnsSetupPlan) => void
28
28
  onManageOperatorWalletAccess: () => void
29
- onWithdrawToken: () => void
30
29
  onBack: () => void
31
30
  onMenu: () => void
32
31
  }
@@ -49,7 +48,6 @@ export const EditProfileFlow: React.FC<EditProfileFlowProps> = ({
49
48
  onEnsRecordsUpdate,
50
49
  onEnsSetup,
51
50
  onManageOperatorWalletAccess,
52
- onWithdrawToken,
53
51
  onBack,
54
52
  onMenu,
55
53
  }) => {
@@ -101,7 +99,6 @@ export const EditProfileFlow: React.FC<EditProfileFlowProps> = ({
101
99
  onEnsRecordsUpdate={onEnsRecordsUpdate}
102
100
  onEnsSetup={onEnsSetup}
103
101
  onManageOperatorWalletAccess={onManageOperatorWalletAccess}
104
- onWithdrawToken={onWithdrawToken}
105
102
  initialView={step.initialView}
106
103
  onBack={onBack}
107
104
  />