ethagent 2.0.1 → 2.1.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.
- package/README.md +6 -6
- package/package.json +1 -1
- package/src/chat/ChatScreen.tsx +1 -3
- package/src/identity/hub/OperationalRoutes.tsx +3 -3
- package/src/identity/hub/components/IdentitySummary.tsx +1 -1
- package/src/identity/hub/components/MenuScreen.tsx +2 -2
- package/src/identity/hub/effects/index.ts +1 -1
- package/src/identity/hub/effects/profile/profileState.ts +32 -32
- package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +8 -8
- package/src/identity/hub/effects/rebackup/runRebackup.ts +2 -2
- package/src/identity/hub/effects/rebackup/{operatorVault.ts → vault.ts} +8 -8
- package/src/identity/hub/effects/restore/apply.ts +2 -2
- package/src/identity/hub/effects/restore/recovery.ts +2 -2
- package/src/identity/hub/effects/shared/sync.ts +3 -3
- package/src/identity/hub/effects/vault/preflight.ts +8 -8
- package/src/identity/hub/flows/create/CreateFlow.tsx +2 -2
- package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +11 -11
- package/src/identity/hub/flows/custody/custodyEffects.ts +20 -20
- package/src/identity/hub/flows/custody/custodyFlowActions.ts +10 -10
- package/src/identity/hub/flows/custody/custodyFlowEffects.ts +2 -2
- package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +14 -14
- package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +2 -2
- package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +2 -2
- package/src/identity/hub/model/copy.ts +1 -1
- package/src/identity/hub/model/errors.ts +6 -6
- package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +1 -1
- package/src/identity/hub/reconciliation/agentReconciliation/run.ts +4 -4
- package/src/identity/hub/useIdentityHubController.ts +3 -3
- package/src/identity/identityCompat.ts +3 -3
- package/src/identity/registry/erc8004/discovery.ts +1 -1
- package/src/identity/registry/erc8004/ownership.ts +4 -4
- package/src/identity/registry/{operatorVault → vault}/bytecode.ts +15 -15
- package/src/identity/registry/vault/constants.ts +38 -0
- package/src/identity/registry/{operatorVault → vault}/read.ts +14 -14
- package/src/identity/registry/{operatorVault → vault}/transactions.ts +5 -5
- package/src/identity/registry/vault.ts +44 -0
- package/src/identity/wallet/page/copy.ts +11 -11
- package/src/storage/config.ts +3 -3
- package/src/identity/registry/operatorVault/constants.ts +0 -38
- package/src/identity/registry/operatorVault.ts +0 -44
|
@@ -4,32 +4,32 @@ import {
|
|
|
4
4
|
encodeDepositAgent,
|
|
5
5
|
encodeUnwrapAgent,
|
|
6
6
|
isAgentInVault,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
resolveConfiguredVaultAddress,
|
|
8
|
+
VAULT_ABI,
|
|
9
|
+
VAULT_DEPLOY_BYTECODE,
|
|
10
10
|
assertVaultBytecode,
|
|
11
|
-
} from '../../../registry/
|
|
11
|
+
} from '../../../registry/vault.js'
|
|
12
12
|
import {
|
|
13
13
|
createErc8004PublicClient,
|
|
14
14
|
type Erc8004RegistryConfig,
|
|
15
15
|
} from '../../../registry/erc8004.js'
|
|
16
16
|
import type { EthagentIdentity } from '../../../../storage/config.js'
|
|
17
|
-
import {
|
|
17
|
+
import { readVaultAddressField, readOwnerAddressField } from '../../../identityCompat.js'
|
|
18
18
|
import { prepareTransactionGasFee, sendBrowserWalletTransaction } from '../../../wallet/browserWallet.js'
|
|
19
19
|
import { acquireTxGuard, releaseTxGuard, type TxGuardKind } from '../../txGuard.js'
|
|
20
20
|
import { awaitConfirmedReceipt } from '../../effects/receipts.js'
|
|
21
21
|
import type { EffectCallbacks } from '../../effects/types.js'
|
|
22
22
|
import { readCustodyMode } from '../../model/custody.js'
|
|
23
23
|
|
|
24
|
-
export function
|
|
24
|
+
export function resolveVaultAddress(
|
|
25
25
|
identity: EthagentIdentity,
|
|
26
26
|
operatorVaults?: Readonly<Record<string, string>>,
|
|
27
27
|
): Address | undefined {
|
|
28
|
-
const identityVault =
|
|
28
|
+
const identityVault = readVaultAddressField(identity.state as Record<string, unknown> | undefined)
|
|
29
29
|
if (identityVault) return getAddress(identityVault)
|
|
30
30
|
if (readCustodyMode(identity.state as Record<string, unknown> | undefined) !== 'advanced') return undefined
|
|
31
31
|
if (!identity.chainId) return undefined
|
|
32
|
-
return
|
|
32
|
+
return resolveConfiguredVaultAddress(operatorVaults, identity.chainId)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
async function withTxGuard<T>(kind: TxGuardKind, fn: () => Promise<T>): Promise<T> {
|
|
@@ -63,8 +63,8 @@ async function runVaultDeployTransactionInner(args: {
|
|
|
63
63
|
const walletAddress = getAddress(args.walletAddress)
|
|
64
64
|
const registryAddress = getAddress(args.registry.identityRegistryAddress)
|
|
65
65
|
const deployData = encodeDeployData({
|
|
66
|
-
abi:
|
|
67
|
-
bytecode:
|
|
66
|
+
abi: VAULT_ABI,
|
|
67
|
+
bytecode: VAULT_DEPLOY_BYTECODE,
|
|
68
68
|
args: [registryAddress, args.agentId],
|
|
69
69
|
})
|
|
70
70
|
const gasFeeClient = createErc8004PublicClient(args.registry)
|
|
@@ -86,9 +86,9 @@ async function runVaultDeployTransactionInner(args: {
|
|
|
86
86
|
})
|
|
87
87
|
args.callbacks.onWalletReady(null)
|
|
88
88
|
const client = args.publicClient ?? createErc8004PublicClient(args.registry)
|
|
89
|
-
const receipt = await awaitConfirmedReceipt(client, result.txHash, '
|
|
89
|
+
const receipt = await awaitConfirmedReceipt(client, result.txHash, 'Vault deploy', { kind: 'vault-deploy', chainId: args.registry.chainId })
|
|
90
90
|
if (!receipt.contractAddress) {
|
|
91
|
-
throw new Error('
|
|
91
|
+
throw new Error('Vault deploy receipt is missing contractAddress; the transaction was not a contract creation')
|
|
92
92
|
}
|
|
93
93
|
const vaultAddress = getAddress(receipt.contractAddress)
|
|
94
94
|
await assertVaultBytecode(client, vaultAddress, result.txHash)
|
|
@@ -114,7 +114,7 @@ async function runVaultDepositTransactionInner(args: {
|
|
|
114
114
|
}): Promise<{ txHash: string }> {
|
|
115
115
|
const { identity, registry, vaultAddress } = args
|
|
116
116
|
if (!identity.agentId) {
|
|
117
|
-
throw new Error('Cannot deposit token to
|
|
117
|
+
throw new Error('Cannot deposit token to Vault: agent token ID is missing')
|
|
118
118
|
}
|
|
119
119
|
const tokenOwner = getAddress(identity.ownerAddress ?? identity.address)
|
|
120
120
|
await assertVaultCanAcceptAgent({
|
|
@@ -152,7 +152,7 @@ async function runVaultDepositTransactionInner(args: {
|
|
|
152
152
|
await awaitConfirmedReceipt(
|
|
153
153
|
depositClient,
|
|
154
154
|
result.txHash as Hex,
|
|
155
|
-
'
|
|
155
|
+
'Vault deposit',
|
|
156
156
|
{ kind: 'vault-deposit', chainId: registry.chainId },
|
|
157
157
|
)
|
|
158
158
|
return { txHash: result.txHash }
|
|
@@ -168,7 +168,7 @@ async function assertVaultCanAcceptAgent(args: {
|
|
|
168
168
|
try {
|
|
169
169
|
held = await client.readContract({
|
|
170
170
|
address: getAddress(args.vaultAddress),
|
|
171
|
-
abi:
|
|
171
|
+
abi: VAULT_ABI,
|
|
172
172
|
functionName: 'heldAgent',
|
|
173
173
|
}) as readonly [Address, bigint, Address]
|
|
174
174
|
} catch {
|
|
@@ -179,9 +179,9 @@ async function assertVaultCanAcceptAgent(args: {
|
|
|
179
179
|
const expectedRegistry = getAddress(args.registry.identityRegistryAddress)
|
|
180
180
|
const sameAgent = heldRegistry.toLowerCase() === expectedRegistry.toLowerCase() && heldAgentId === args.agentId
|
|
181
181
|
if (sameAgent) {
|
|
182
|
-
throw new Error(`
|
|
182
|
+
throw new Error(`Vault ${getAddress(args.vaultAddress)} already holds ERC-8004 token #${args.agentId.toString()}. Publish the pending update instead of depositing again.`)
|
|
183
183
|
}
|
|
184
|
-
throw new Error(`
|
|
184
|
+
throw new Error(`Vault ${getAddress(args.vaultAddress)} already holds ERC-8004 token #${heldAgentId.toString()} for registry ${getAddress(heldRegistry)}. Deploy a fresh vault for this agent.`)
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
export async function runVaultUnwrapTransaction(args: {
|
|
@@ -206,7 +206,7 @@ async function runVaultUnwrapTransactionInner(args: {
|
|
|
206
206
|
const { identity, registry, vaultAddress } = args
|
|
207
207
|
const targetAgentId = args.agentId ?? (identity.agentId ? BigInt(identity.agentId) : undefined)
|
|
208
208
|
if (targetAgentId === undefined) {
|
|
209
|
-
throw new Error('Cannot unwrap token from
|
|
209
|
+
throw new Error('Cannot unwrap token from Vault: agent token ID is missing')
|
|
210
210
|
}
|
|
211
211
|
const baseState = (identity.state ?? {}) as Record<string, unknown>
|
|
212
212
|
const ownerAddressRaw = readOwnerAddressField(baseState)
|
|
@@ -240,7 +240,7 @@ async function runVaultUnwrapTransactionInner(args: {
|
|
|
240
240
|
await awaitConfirmedReceipt(
|
|
241
241
|
publicClient,
|
|
242
242
|
result.txHash as Hex,
|
|
243
|
-
'
|
|
243
|
+
'Vault unwrap',
|
|
244
244
|
{ kind: 'vault-unwrap', chainId: registry.chainId },
|
|
245
245
|
)
|
|
246
246
|
await confirmAgentWithdrawnFromVault({
|
|
@@ -307,7 +307,7 @@ async function runVaultWithdrawTransactionInner(args: {
|
|
|
307
307
|
await awaitConfirmedReceipt(
|
|
308
308
|
publicClient,
|
|
309
309
|
result.txHash as Hex,
|
|
310
|
-
'
|
|
310
|
+
'Vault withdraw',
|
|
311
311
|
{ kind: 'vault-withdraw', chainId: registry.chainId },
|
|
312
312
|
)
|
|
313
313
|
await confirmAgentWithdrawnFromVault({
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Address } from 'viem'
|
|
2
2
|
import { createErc8004PublicClient } from '../../../registry/erc8004.js'
|
|
3
|
-
import { discoverPriorVaultFromTokenOwner, isAgentInVault } from '../../../registry/
|
|
3
|
+
import { discoverPriorVaultFromTokenOwner, isAgentInVault } from '../../../registry/vault.js'
|
|
4
4
|
import type { ProfileUpdates, Step } from '../../identityHubReducer.js'
|
|
5
5
|
import { isCustodyEditStep } from './CustodyEditFlow.js'
|
|
6
|
-
import {
|
|
6
|
+
import { resolveVaultAddress } from './custodyEffects.js'
|
|
7
7
|
import type { CustodyFlowDeps } from './custodyFlowTypes.js'
|
|
8
8
|
import { humanOwnerAddress } from './custodyFlowHelpers.js'
|
|
9
9
|
|
|
@@ -21,7 +21,7 @@ export function createCustodyFlowActions({
|
|
|
21
21
|
} {
|
|
22
22
|
const beginVaultDeposit = (currentStep: Step, returnTo: Step, profileUpdates: ProfileUpdates): void => {
|
|
23
23
|
if (!isCustodyEditStep(currentStep) || currentStep.kind !== 'custody-advanced-confirm') return
|
|
24
|
-
const vaultAddress =
|
|
24
|
+
const vaultAddress = resolveVaultAddress(currentStep.identity, config?.erc8004?.operatorVaults)
|
|
25
25
|
const expectedOwnerForDiscovery = humanOwnerAddress(currentStep.identity)
|
|
26
26
|
if (!vaultAddress) {
|
|
27
27
|
const registry = currentStep.registry
|
|
@@ -49,7 +49,7 @@ export function createCustodyFlowActions({
|
|
|
49
49
|
}
|
|
50
50
|
if (status.inVault) {
|
|
51
51
|
handleStepError(
|
|
52
|
-
new Error(`Recovered
|
|
52
|
+
new Error(`Recovered Vault ${recoveredVault} holds the token, but the vault-level depositor is ${status.ownerAddress ?? 'unknown'}, not your wallet ${expectedOwnerForDiscovery}. Mid-flow recovery requires the original depositor's wallet to call vault.unwrap.`),
|
|
53
53
|
{ kind: 'custody-model', identity: currentStep.identity, registry, returnTo },
|
|
54
54
|
)
|
|
55
55
|
return
|
|
@@ -95,7 +95,7 @@ export function createCustodyFlowActions({
|
|
|
95
95
|
}
|
|
96
96
|
if (status.inVault) {
|
|
97
97
|
handleStepError(
|
|
98
|
-
new Error(`Token is held by the
|
|
98
|
+
new Error(`Token is held by the Vault, but the vault-level owner is ${status.ownerAddress ?? 'unknown'}, not your wallet ${expectedOwner}. Recovery requires that wallet to call vault.unwrap.`),
|
|
99
99
|
{ kind: 'custody-model', identity: currentStep.identity, registry, returnTo },
|
|
100
100
|
)
|
|
101
101
|
return
|
|
@@ -124,10 +124,10 @@ export function createCustodyFlowActions({
|
|
|
124
124
|
|
|
125
125
|
const beginWithdrawToken = (currentStep: Step, returnTo: Step): void => {
|
|
126
126
|
if (!isCustodyEditStep(currentStep)) return
|
|
127
|
-
const vaultAddress =
|
|
127
|
+
const vaultAddress = resolveVaultAddress(currentStep.identity, config?.erc8004?.operatorVaults)
|
|
128
128
|
if (!vaultAddress) {
|
|
129
129
|
handleStepError(
|
|
130
|
-
new Error('No
|
|
130
|
+
new Error('No Vault is recorded for this identity. There is nothing to withdraw.'),
|
|
131
131
|
{ kind: 'custody-model', identity: currentStep.identity, registry: currentStep.registry, returnTo },
|
|
132
132
|
)
|
|
133
133
|
return
|
|
@@ -160,7 +160,7 @@ export function createCustodyFlowActions({
|
|
|
160
160
|
if (status.inVault) {
|
|
161
161
|
if (status.ownerAddress && status.ownerAddress.toLowerCase() !== depositor.toLowerCase()) {
|
|
162
162
|
handleStepError(
|
|
163
|
-
new Error(`
|
|
163
|
+
new Error(`Vault holds token #${activeAgentId} but recorded the depositor as ${status.ownerAddress}, not your wallet ${depositor}. Only the original depositor can withdraw.`),
|
|
164
164
|
{ kind: 'custody-model', identity: currentStep.identity, registry: currentStep.registry, returnTo },
|
|
165
165
|
)
|
|
166
166
|
return
|
|
@@ -176,7 +176,7 @@ export function createCustodyFlowActions({
|
|
|
176
176
|
return
|
|
177
177
|
}
|
|
178
178
|
handleStepError(
|
|
179
|
-
new Error(`Token #${activeAgentId} is not currently in the
|
|
179
|
+
new Error(`Token #${activeAgentId} is not currently in the Vault. There is nothing to withdraw; the token is already with the owner wallet.`),
|
|
180
180
|
{ kind: 'custody-model', identity: currentStep.identity, registry: currentStep.registry, returnTo },
|
|
181
181
|
)
|
|
182
182
|
} catch (err: unknown) {
|
|
@@ -187,7 +187,7 @@ export function createCustodyFlowActions({
|
|
|
187
187
|
|
|
188
188
|
const beginVaultUnwrap = (currentStep: Step, returnTo: Step, profileUpdates: ProfileUpdates): void => {
|
|
189
189
|
if (!isCustodyEditStep(currentStep) || currentStep.kind !== 'custody-simple-confirm') return
|
|
190
|
-
const vaultAddress =
|
|
190
|
+
const vaultAddress = resolveVaultAddress(currentStep.identity, config?.erc8004?.operatorVaults)
|
|
191
191
|
if (!vaultAddress) {
|
|
192
192
|
triggerRebackup(returnTo, profileUpdates, { useVault: false })
|
|
193
193
|
return
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect } from 'react'
|
|
2
2
|
import { createErc8004PublicClient } from '../../../registry/erc8004.js'
|
|
3
|
-
import { confirmAgentInVault } from '../../../registry/
|
|
3
|
+
import { confirmAgentInVault } from '../../../registry/vault.js'
|
|
4
4
|
import { invalidateOwnershipCache } from '../../reconciliation/index.js'
|
|
5
5
|
import type { ProfileUpdates } from '../../identityHubReducer.js'
|
|
6
6
|
import type { CustodyFlowDeps } from './custodyFlowTypes.js'
|
|
@@ -25,7 +25,7 @@ export function useCustodyTransactionEffects({
|
|
|
25
25
|
let cancelled = false
|
|
26
26
|
if (!step.identity.agentId) {
|
|
27
27
|
handleStepError(
|
|
28
|
-
new Error('Cannot deploy
|
|
28
|
+
new Error('Cannot deploy Vault: agent token ID is missing'),
|
|
29
29
|
{ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo },
|
|
30
30
|
)
|
|
31
31
|
return () => { cancelled = true }
|
|
@@ -24,8 +24,8 @@ export function renderCustodyStep({
|
|
|
24
24
|
if (step.kind === 'custody-vault-deploy-tx') {
|
|
25
25
|
return (
|
|
26
26
|
<WalletApprovalScreen
|
|
27
|
-
title="Deploy
|
|
28
|
-
subtitle={`Deploying a dedicated
|
|
27
|
+
title="Deploy Vault"
|
|
28
|
+
subtitle={`Deploying a dedicated Vault for this ERC-8004 token on ${chainLabel(step.registry.chainId)}.`}
|
|
29
29
|
walletSession={walletSession}
|
|
30
30
|
label="waiting for owner wallet transaction..."
|
|
31
31
|
onCancel={() => setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
|
|
@@ -35,8 +35,8 @@ export function renderCustodyStep({
|
|
|
35
35
|
if (step.kind === 'custody-vault-deposit-tx') {
|
|
36
36
|
return (
|
|
37
37
|
<WalletApprovalScreen
|
|
38
|
-
title="Deposit Token Into
|
|
39
|
-
subtitle={`Sign one ${chainLabel(step.registry.chainId)} transaction. Sends ERC-8004 token #${step.identity.agentId ?? ''} to its
|
|
38
|
+
title="Deposit Token Into Vault"
|
|
39
|
+
subtitle={`Sign one ${chainLabel(step.registry.chainId)} transaction. Sends ERC-8004 token #${step.identity.agentId ?? ''} to its Vault.`}
|
|
40
40
|
walletSession={walletSession}
|
|
41
41
|
label="waiting for token-owner wallet transaction..."
|
|
42
42
|
onCancel={() => setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
|
|
@@ -49,8 +49,8 @@ export function renderCustodyStep({
|
|
|
49
49
|
<Surface
|
|
50
50
|
title="Checking Vault"
|
|
51
51
|
subtitle={targetAgentId
|
|
52
|
-
? `Confirming the
|
|
53
|
-
: `Checking this identity's recorded
|
|
52
|
+
? `Confirming the Vault holds ERC-8004 token #${targetAgentId} on ${chainLabel(step.registry.chainId)}.`
|
|
53
|
+
: `Checking this identity's recorded Vault on ${chainLabel(step.registry.chainId)}.`}
|
|
54
54
|
footer={<Text color={theme.dim}>esc cancel</Text>}
|
|
55
55
|
>
|
|
56
56
|
<Box marginTop={1}>
|
|
@@ -64,7 +64,7 @@ export function renderCustodyStep({
|
|
|
64
64
|
return (
|
|
65
65
|
<WalletApprovalScreen
|
|
66
66
|
title="Withdraw Token"
|
|
67
|
-
subtitle={`Unwraps ERC-8004 token #${targetAgentId} from its
|
|
67
|
+
subtitle={`Unwraps ERC-8004 token #${targetAgentId} from its Vault to your owner wallet on ${chainLabel(step.registry.chainId)}.`}
|
|
68
68
|
walletSession={walletSession}
|
|
69
69
|
label="waiting for owner wallet transaction..."
|
|
70
70
|
onCancel={() => setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
|
|
@@ -135,7 +135,7 @@ export function renderCustodyStep({
|
|
|
135
135
|
return (
|
|
136
136
|
<Surface
|
|
137
137
|
title="Token Returned to Owner Wallet"
|
|
138
|
-
subtitle={`Token returned to ${shortAddress(step.recipient)} on ${chainLabel(step.registry.chainId)}. The
|
|
138
|
+
subtitle={`Token returned to ${shortAddress(step.recipient)} on ${chainLabel(step.registry.chainId)}. The Vault can be reused for this token.`}
|
|
139
139
|
footer={<Text color={theme.dim}>enter select · esc back</Text>}
|
|
140
140
|
>
|
|
141
141
|
<Box flexDirection="column">
|
|
@@ -149,7 +149,7 @@ export function renderCustodyStep({
|
|
|
149
149
|
<Select<'return-to-vault' | 'keep-out'>
|
|
150
150
|
options={[
|
|
151
151
|
{ value: 'return-to-vault', role: 'section', label: 'Resume Advanced Custody' },
|
|
152
|
-
{ value: 'return-to-vault', label: 'Return Token to Vault', hint: 'Redeposit to this
|
|
152
|
+
{ value: 'return-to-vault', label: 'Return Token to Vault', hint: 'Redeposit to this Vault. No redeploy, no operator re-add' },
|
|
153
153
|
{ value: 'keep-out', role: 'section', label: 'Later' },
|
|
154
154
|
{ value: 'keep-out', label: 'Keep Out For Now', hint: 'Token stays with the owner wallet; redeposit any time from Custody Mode', role: 'utility' },
|
|
155
155
|
]}
|
|
@@ -167,8 +167,8 @@ export function renderCustodyStep({
|
|
|
167
167
|
if (step.kind === 'custody-vault-unwrap-tx') {
|
|
168
168
|
return (
|
|
169
169
|
<WalletApprovalScreen
|
|
170
|
-
title="Unwrap Token From
|
|
171
|
-
subtitle={`Sign one ${chainLabel(step.registry.chainId)} transaction. Calls the
|
|
170
|
+
title="Unwrap Token From Vault"
|
|
171
|
+
subtitle={`Sign one ${chainLabel(step.registry.chainId)} transaction. Calls the Vault unwrap function to return ERC-8004 token #${step.identity.agentId ?? ''} to the owner wallet.`}
|
|
172
172
|
walletSession={walletSession}
|
|
173
173
|
label="waiting for owner wallet transaction..."
|
|
174
174
|
onCancel={() => setStep({ kind: 'custody-model', identity: step.identity, registry: step.registry, returnTo: step.returnTo })}
|
|
@@ -182,11 +182,11 @@ export function renderCustodyStep({
|
|
|
182
182
|
return (
|
|
183
183
|
<Surface
|
|
184
184
|
title="Advanced Custody Active"
|
|
185
|
-
subtitle="Your token is held in its own
|
|
185
|
+
subtitle="Your token is held in its own Vault. Authorized operator wallets can rotate the agent URI onchain without owner signatures."
|
|
186
186
|
footer={<Text color={theme.dim}>enter continues</Text>}
|
|
187
187
|
>
|
|
188
188
|
<Box flexDirection="column">
|
|
189
|
-
{step.vaultAddress ? <Row label="
|
|
189
|
+
{step.vaultAddress ? <Row label="Vault" value={shortAddress(step.vaultAddress)} /> : null}
|
|
190
190
|
<Row label="Owner Wallet" value={shortAddress(ownerWallet)} />
|
|
191
191
|
<Row label="Operator Wallets" value={operatorCount === 1 ? '1 approved' : `${operatorCount} approved`} />
|
|
192
192
|
<Box marginTop={1}>
|
|
@@ -235,5 +235,5 @@ export function renderRebackupSubtitle(
|
|
|
235
235
|
vaultRouted: boolean,
|
|
236
236
|
): React.ReactNode {
|
|
237
237
|
if (!vaultRouted) return defaultSubtitle
|
|
238
|
-
return <Text color={theme.textSubtle}>{defaultSubtitle} Routed through this token's
|
|
238
|
+
return <Text color={theme.textSubtle}>{defaultSubtitle} Routed through this token's Vault.</Text>
|
|
239
239
|
}
|
|
@@ -89,7 +89,7 @@ export function renderAdvancedEnsPhase({
|
|
|
89
89
|
<Box marginTop={1} flexDirection="column">
|
|
90
90
|
<EnsSetupRow label="Owner wallet" value={`Holds ERC-8004 token #${agentId ?? 'unknown'} and signs ENS records.`} />
|
|
91
91
|
<EnsSetupRow label="Operator wallet" value="Restores snapshots; never controls the token." />
|
|
92
|
-
<EnsSetupRow label="Token moves" value="If the token is in the
|
|
92
|
+
<EnsSetupRow label="Token moves" value="If the token is in the Vault, withdraw it first from Custody Mode." />
|
|
93
93
|
</Box>
|
|
94
94
|
</Box>
|
|
95
95
|
<Box marginTop={1}>
|
|
@@ -217,7 +217,7 @@ export function renderAdvancedEnsPhase({
|
|
|
217
217
|
<Box flexDirection="column">
|
|
218
218
|
<Text color={theme.dim}>Agent ENS: <Text color={theme.text}>{label}.{rootName}</Text></Text>
|
|
219
219
|
{phase.registryAction ? <Text color={theme.dim}>{advancedSubdomainStatusText(phase.registryAction)}</Text> : null}
|
|
220
|
-
<Text color={theme.dim}>Choose the operator wallet for snapshot restore access and onchain ERC-8004 URI rotation via the
|
|
220
|
+
<Text color={theme.dim}>Choose the operator wallet for snapshot restore access and onchain ERC-8004 URI rotation via the Vault.</Text>
|
|
221
221
|
<Text color={theme.dim}>The operator wallet has no authority over this ENS subdomain or any token transfer; the owner wallet is the sole signer for both.</Text>
|
|
222
222
|
<Text color={theme.dim}>We only read the operator's address here so it can be added to the snapshot envelope and vault operator list later.</Text>
|
|
223
223
|
{savedOperator ? <Text color={theme.dim}>Saved operator wallet: <Text color={theme.text}>{shortAddress(savedOperator)}</Text></Text> : null}
|
|
@@ -218,9 +218,9 @@ export const EnsSetupBlockedScreen: React.FC<EnsSetupBlockedScreenProps> = ({
|
|
|
218
218
|
? (
|
|
219
219
|
<>
|
|
220
220
|
<Box marginTop={1} flexDirection="column">
|
|
221
|
-
<Text color={theme.text}>To proceed: the owner wallet signs ENS records and must hold this token at setup time. Once setup is done you can deposit the token into the
|
|
221
|
+
<Text color={theme.text}>To proceed: the owner wallet signs ENS records and must hold this token at setup time. Once setup is done you can deposit the token into the Vault while the ENS subdomain stays with the owner wallet.</Text>
|
|
222
222
|
</Box>
|
|
223
|
-
<Text color={theme.dim}>Operator wallets have no authority on this name; they only rotate the onchain ERC-8004 URI via the
|
|
223
|
+
<Text color={theme.dim}>Operator wallets have no authority on this name; they only rotate the onchain ERC-8004 URI via the Vault.</Text>
|
|
224
224
|
</>
|
|
225
225
|
)
|
|
226
226
|
: null}
|
|
@@ -21,7 +21,7 @@ export function copyableIdentityFields(identity?: EthagentIdentity, config?: Eth
|
|
|
21
21
|
const custodyMode = readCustodyMode(identity.state)
|
|
22
22
|
if (custodyMode === 'advanced') {
|
|
23
23
|
const vaultAddress = readIdentityStateString(identity.state, 'operatorVaultAddress')
|
|
24
|
-
if (vaultAddress) fields.push({ label: '
|
|
24
|
+
if (vaultAddress) fields.push({ label: 'Vault', value: vaultAddress })
|
|
25
25
|
}
|
|
26
26
|
const activeOperator = readIdentityStateString(identity.state, 'activeOperatorAddress')
|
|
27
27
|
if (activeOperator) fields.push({ label: 'Operator Wallet', value: activeOperator })
|
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
ContinuityTransferSnapshotTargetMismatchError,
|
|
7
7
|
} from '../../continuity/envelope.js'
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from '../../registry/
|
|
9
|
+
VaultBytecodeMismatchError,
|
|
10
|
+
formatVaultBytecodeMismatchDetail,
|
|
11
|
+
} from '../../registry/vault.js'
|
|
12
12
|
import { BrowserWalletError } from '../../wallet/browserWallet.js'
|
|
13
13
|
import { TxGuardBusyError } from '../txGuard.js'
|
|
14
14
|
import { shortAddress } from './format.js'
|
|
@@ -65,11 +65,11 @@ export function identityHubErrorView(err: unknown): IdentityHubErrorView {
|
|
|
65
65
|
hint: `Prepare a new transfer snapshot from ${shortAddress(err.snapshotOwner)} to the intended receiver wallet.`,
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
if (err instanceof
|
|
68
|
+
if (err instanceof VaultBytecodeMismatchError) {
|
|
69
69
|
return {
|
|
70
70
|
title: 'Vault Bytecode Mismatch',
|
|
71
|
-
detail:
|
|
72
|
-
hint: 'Open the deploy tx on the block explorer and confirm the input data matches the
|
|
71
|
+
detail: formatVaultBytecodeMismatchDetail(err),
|
|
72
|
+
hint: 'Open the deploy tx on the block explorer and confirm the input data matches the Vault deploy bytecode plus constructor args before retrying. A persistent mismatch usually means the wallet or RPC substituted the create.',
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
if (err instanceof BrowserWalletError) {
|
|
@@ -85,7 +85,7 @@ async function runOwnershipPreflight(args: {
|
|
|
85
85
|
return {
|
|
86
86
|
ok: false,
|
|
87
87
|
reason: 'not-owned',
|
|
88
|
-
detail: `Token is held by the
|
|
88
|
+
detail: `Token is held by the Vault (${directOwner.owner}). Withdraw it first.`,
|
|
89
89
|
onChainOwner: directOwner.owner,
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
validateErc8004TokenOwner,
|
|
6
6
|
type Erc8004RegistryConfig,
|
|
7
7
|
} from '../../../registry/erc8004.js'
|
|
8
|
-
import { isAgentInVault,
|
|
8
|
+
import { isAgentInVault, resolveConfiguredVaultAddress } from '../../../registry/vault.js'
|
|
9
9
|
import type { EthagentConfig, EthagentIdentity } from '../../../../storage/config.js'
|
|
10
|
-
import {
|
|
10
|
+
import { readVaultAddressField } from '../../../identityCompat.js'
|
|
11
11
|
import { reconcileWalletSetup, type RecordsFixPlan } from '../walletSetup.js'
|
|
12
12
|
import { readCustodyMode } from '../../model/custody.js'
|
|
13
13
|
import { continuityWorkingTreeStatus, type ContinuityWorkingTreeStatus } from '../../../continuity/storage.js'
|
|
@@ -115,11 +115,11 @@ function resolveReconciliationVaultAddress(
|
|
|
115
115
|
identity: EthagentIdentity,
|
|
116
116
|
operatorVaults?: Readonly<Record<string, string>>,
|
|
117
117
|
): Address | undefined {
|
|
118
|
-
const identityVault =
|
|
118
|
+
const identityVault = readVaultAddressField(identity.state as Record<string, unknown> | undefined)
|
|
119
119
|
if (identityVault) return getAddress(identityVault)
|
|
120
120
|
if (readCustodyMode(identity.state as Record<string, unknown> | undefined) !== 'advanced') return undefined
|
|
121
121
|
if (!identity.chainId) return undefined
|
|
122
|
-
return
|
|
122
|
+
return resolveConfiguredVaultAddress(operatorVaults, identity.chainId)
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
type TokenProbe =
|
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
RestoreProgress,
|
|
25
25
|
TokenTransferProgress,
|
|
26
26
|
} from './effects/types.js'
|
|
27
|
-
import {
|
|
27
|
+
import { resolveVaultAddress } from './flows/custody/custodyEffects.js'
|
|
28
28
|
import { useCustodyFlow } from './flows/custody/useCustodyFlow.js'
|
|
29
29
|
import { identityHubErrorView } from './model/errors.js'
|
|
30
30
|
import { readCustodyMode } from './model/custody.js'
|
|
@@ -242,7 +242,7 @@ export function useIdentityHubController({
|
|
|
242
242
|
}
|
|
243
243
|
const vaultAddress = options?.useVault === false
|
|
244
244
|
? undefined
|
|
245
|
-
: options?.vaultAddress ??
|
|
245
|
+
: options?.vaultAddress ?? resolveVaultAddress(identity, config?.erc8004?.operatorVaults)
|
|
246
246
|
;(async () => {
|
|
247
247
|
const role: 'token-holder' | 'vault-level-owner' = vaultAddress ? 'vault-level-owner' : 'token-holder'
|
|
248
248
|
const allowed = await guardOwnership(identity, registry, role, backStep)
|
|
@@ -260,7 +260,7 @@ export function useIdentityHubController({
|
|
|
260
260
|
return
|
|
261
261
|
}
|
|
262
262
|
;(async () => {
|
|
263
|
-
const vaultAddress =
|
|
263
|
+
const vaultAddress = resolveVaultAddress(identity, config?.erc8004?.operatorVaults)
|
|
264
264
|
const allowed = await guardOwnership(identity, registry, 'vault-level-owner', backStep)
|
|
265
265
|
if (!allowed) return
|
|
266
266
|
runPublicProfilePreflight(identity, registry, callbacks, profileUpdates, backStep, vaultAddress)
|
|
@@ -17,15 +17,15 @@ export function clearOwnerAddressField(target: Record<string, unknown>): void {
|
|
|
17
17
|
delete target[LEGACY_OWNER_ADDRESS_KEY]
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function
|
|
20
|
+
export function readVaultAddressField(input: Record<string, unknown> | null | undefined): string | undefined {
|
|
21
21
|
return stringValue(input?.[OPERATOR_VAULT_ADDRESS_KEY])
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function
|
|
24
|
+
export function setVaultAddressField(target: Record<string, unknown>, value: string): void {
|
|
25
25
|
target[OPERATOR_VAULT_ADDRESS_KEY] = value
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function
|
|
28
|
+
export function clearVaultAddressField(target: Record<string, unknown>): void {
|
|
29
29
|
delete target[OPERATOR_VAULT_ADDRESS_KEY]
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getAddress, isAddress, type Address, type PublicClient } from 'viem'
|
|
2
2
|
import { mainnet } from 'viem/chains'
|
|
3
3
|
import { DEFAULT_IPFS_API_URL } from '../../storage/ipfs.js'
|
|
4
|
-
import { isAgentInVault } from '../
|
|
4
|
+
import { isAgentInVault } from '../vault.js'
|
|
5
5
|
import { ERC8004_ABI, TRANSFER_EVENT } from './abi.js'
|
|
6
6
|
import {
|
|
7
7
|
SUPPORTED_ERC8004_CHAINS,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getAddress, type Address, type PublicClient } from 'viem'
|
|
2
|
-
import {
|
|
2
|
+
import { VAULT_ABI } from '../vault.js'
|
|
3
3
|
import { ERC8004_ABI } from './abi.js'
|
|
4
4
|
import { createErc8004PublicClient } from './client.js'
|
|
5
5
|
import type { Erc8004RegistryConfig } from './types.js'
|
|
@@ -54,7 +54,7 @@ export async function validateErc8004TokenOwner(args: Erc8004RegistryConfig & {
|
|
|
54
54
|
return {
|
|
55
55
|
ok: false,
|
|
56
56
|
reason: 'token-owner-lookup-failed',
|
|
57
|
-
detail: `ERC-8004 token #${args.agentId.toString()} is still reported at the
|
|
57
|
+
detail: `ERC-8004 token #${args.agentId.toString()} is still reported at the Vault, but that vault record is empty. Ownership is still settling; retry shortly.`,
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
if (vaultOwnerResult.kind === 'error') {
|
|
@@ -73,7 +73,7 @@ export async function validateErc8004TokenOwner(args: Erc8004RegistryConfig & {
|
|
|
73
73
|
ok: false,
|
|
74
74
|
reason: 'token-owner-mismatch',
|
|
75
75
|
ownerAddress: vaultOwner,
|
|
76
|
-
detail: `ERC-8004 token #${args.agentId.toString()} is held by the
|
|
76
|
+
detail: `ERC-8004 token #${args.agentId.toString()} is held by the Vault for ${vaultOwner}`,
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
return {
|
|
@@ -98,7 +98,7 @@ async function readVaultLevelOwner(args: Erc8004RegistryConfig & {
|
|
|
98
98
|
const client = args.publicClient ?? createErc8004PublicClient(args)
|
|
99
99
|
const vaultOwner = await client.readContract({
|
|
100
100
|
address: args.vaultAddress,
|
|
101
|
-
abi:
|
|
101
|
+
abi: VAULT_ABI,
|
|
102
102
|
functionName: 'agentOwner',
|
|
103
103
|
args: [args.identityRegistryAddress, args.agentId],
|
|
104
104
|
}) as Address
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getAddress, keccak256, type Address, type Hex, type PublicClient } from 'viem'
|
|
2
|
-
import {
|
|
2
|
+
import { VAULT_RUNTIME_BYTECODE, VAULT_RUNTIME_BYTECODE_HASH } from './constants.js'
|
|
3
3
|
|
|
4
|
-
export class
|
|
4
|
+
export class VaultBytecodeMismatchError extends Error {
|
|
5
5
|
readonly vaultAddress: Address
|
|
6
6
|
readonly observedHash: Hex | null
|
|
7
7
|
readonly observedLength: number
|
|
@@ -15,22 +15,22 @@ export class OperatorVaultBytecodeMismatchError extends Error {
|
|
|
15
15
|
txHash?: Hex,
|
|
16
16
|
) {
|
|
17
17
|
super(
|
|
18
|
-
'Deployed contract bytecode does not match the expected
|
|
18
|
+
'Deployed contract bytecode does not match the expected Vault. The deploy transaction may have been intercepted.',
|
|
19
19
|
)
|
|
20
|
-
this.name = '
|
|
20
|
+
this.name = 'VaultBytecodeMismatchError'
|
|
21
21
|
this.vaultAddress = vaultAddress
|
|
22
22
|
this.observedHash = observedHash
|
|
23
23
|
this.observedLength = observedLength
|
|
24
|
-
this.expectedHash =
|
|
25
|
-
this.expectedLength = (
|
|
24
|
+
this.expectedHash = VAULT_RUNTIME_BYTECODE_HASH
|
|
25
|
+
this.expectedLength = (VAULT_RUNTIME_BYTECODE.length - 2) / 2
|
|
26
26
|
if (txHash) this.txHash = txHash
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export type AssertVaultBytecodeClient = Pick<PublicClient, 'getBytecode'>
|
|
31
31
|
|
|
32
|
-
export const
|
|
33
|
-
export const
|
|
32
|
+
export const VAULT_POLL_MAX_ATTEMPTS = 5
|
|
33
|
+
export const VAULT_POLL_DELAY_MS = 1500
|
|
34
34
|
export function delayMs(ms: number): Promise<void> {
|
|
35
35
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
36
36
|
}
|
|
@@ -41,8 +41,8 @@ async function readVaultBytecodeWithPoll(
|
|
|
41
41
|
): Promise<Hex | undefined> {
|
|
42
42
|
let lastErr: unknown
|
|
43
43
|
let lastCode: Hex | undefined
|
|
44
|
-
for (let attempt = 0; attempt <
|
|
45
|
-
if (attempt > 0) await delayMs(
|
|
44
|
+
for (let attempt = 0; attempt < VAULT_POLL_MAX_ATTEMPTS; attempt++) {
|
|
45
|
+
if (attempt > 0) await delayMs(VAULT_POLL_DELAY_MS)
|
|
46
46
|
try {
|
|
47
47
|
const code = await client.getBytecode({ address })
|
|
48
48
|
lastErr = undefined
|
|
@@ -66,13 +66,13 @@ export async function assertVaultBytecode(
|
|
|
66
66
|
const address = getAddress(vaultAddress)
|
|
67
67
|
const code = await readVaultBytecodeWithPoll(client, address)
|
|
68
68
|
if (!code || code === '0x') {
|
|
69
|
-
throw new
|
|
69
|
+
throw new VaultBytecodeMismatchError(address, null, 0, txHash)
|
|
70
70
|
}
|
|
71
71
|
const observedLength = (code.length - 2) / 2
|
|
72
72
|
const observed = keccak256(code).toLowerCase() as Hex
|
|
73
|
-
const expected =
|
|
73
|
+
const expected = VAULT_RUNTIME_BYTECODE_HASH.toLowerCase() as Hex
|
|
74
74
|
if (observed !== expected) {
|
|
75
|
-
throw new
|
|
75
|
+
throw new VaultBytecodeMismatchError(address, observed, observedLength, txHash)
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -80,8 +80,8 @@ function shortHash(hash: Hex): string {
|
|
|
80
80
|
return `${hash.slice(0, 18)}...${hash.slice(-6)}`
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export function
|
|
84
|
-
err:
|
|
83
|
+
export function formatVaultBytecodeMismatchDetail(
|
|
84
|
+
err: VaultBytecodeMismatchError,
|
|
85
85
|
): string {
|
|
86
86
|
const lines = [
|
|
87
87
|
`Vault address: ${err.vaultAddress}`,
|