ethagent 1.1.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +127 -29
- package/package.json +16 -9
- package/src/app/FirstRun.tsx +192 -146
- package/src/app/FirstRunTimeline.tsx +47 -0
- package/src/app/input/AppInputProvider.tsx +1 -1
- package/src/app/keybindings/KeybindingProvider.tsx +1 -1
- package/src/chat/ChatBottomPane.tsx +0 -1
- package/src/chat/ChatInput.tsx +6 -6
- package/src/chat/ChatScreen.tsx +43 -18
- package/src/chat/ContextLimitView.tsx +4 -4
- package/src/chat/ContinuityEditReviewView.tsx +11 -17
- package/src/chat/ConversationStack.tsx +3 -0
- package/src/chat/CopyPicker.tsx +0 -1
- package/src/chat/MessageList.tsx +62 -45
- package/src/chat/PermissionPrompt.tsx +13 -9
- package/src/chat/PlanApprovalView.tsx +3 -3
- package/src/chat/ResumeView.tsx +1 -4
- package/src/chat/RewindView.tsx +2 -2
- package/src/chat/TranscriptView.tsx +6 -0
- package/src/chat/chatInputState.ts +1 -1
- package/src/chat/chatScreenUtils.ts +22 -11
- package/src/chat/chatSessionState.ts +2 -2
- package/src/chat/chatTurnOrchestrator.ts +16 -81
- package/src/chat/commands.ts +1 -1
- package/src/chat/textCursor.ts +1 -1
- package/src/chat/transcriptViewport.ts +2 -7
- package/src/cli/ResetConfirmView.tsx +1 -1
- package/src/cli/main.tsx +9 -3
- package/src/cli/preview.tsx +0 -5
- package/src/cli/updateNotice.ts +5 -3
- package/src/identity/continuity/editor.ts +7 -107
- package/src/identity/continuity/envelope.ts +1048 -40
- package/src/identity/continuity/history.ts +4 -4
- package/src/identity/continuity/localBackup.ts +249 -0
- package/src/identity/continuity/privateEdit/apply.ts +170 -0
- package/src/identity/continuity/privateEdit/diff.ts +82 -0
- package/src/identity/continuity/privateEdit/files.ts +23 -0
- package/src/identity/continuity/privateEdit/types.ts +28 -0
- package/src/identity/continuity/privateEdit.ts +10 -298
- package/src/identity/continuity/publicSkills.ts +8 -9
- package/src/identity/continuity/snapshots.ts +17 -6
- package/src/identity/continuity/storage/defaults.ts +111 -0
- package/src/identity/continuity/storage/files.ts +72 -0
- package/src/identity/continuity/storage/markdown.ts +81 -0
- package/src/identity/continuity/storage/paths.ts +24 -0
- package/src/identity/continuity/storage/scaffold.ts +124 -0
- package/src/identity/continuity/storage/status.ts +86 -0
- package/src/identity/continuity/storage/types.ts +27 -0
- package/src/identity/continuity/storage.ts +32 -507
- package/src/identity/continuity/zipWriter.ts +95 -0
- package/src/identity/crypto/backupEnvelope.ts +14 -247
- package/src/identity/crypto/eth.ts +7 -7
- package/src/identity/ens/agentRecords.ts +96 -0
- package/src/identity/ens/ensAutomation/contracts.ts +38 -0
- package/src/identity/ens/ensAutomation/delete.ts +80 -0
- package/src/identity/ens/ensAutomation/names.ts +14 -0
- package/src/identity/ens/ensAutomation/operators.ts +29 -0
- package/src/identity/ens/ensAutomation/read.ts +114 -0
- package/src/identity/ens/ensAutomation/root.ts +63 -0
- package/src/identity/ens/ensAutomation/setup.ts +284 -0
- package/src/identity/ens/ensAutomation/transactions.ts +107 -0
- package/src/identity/ens/ensAutomation/types.ts +126 -0
- package/src/identity/ens/ensAutomation.ts +29 -0
- package/src/identity/ens/ensLookup/client.ts +43 -0
- package/src/identity/ens/ensLookup/constants.ts +26 -0
- package/src/identity/ens/ensLookup/discovery.ts +70 -0
- package/src/identity/ens/ensLookup/names.ts +34 -0
- package/src/identity/ens/ensLookup/records.ts +45 -0
- package/src/identity/ens/ensLookup/resolve.ts +75 -0
- package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
- package/src/identity/ens/ensLookup/types.ts +38 -0
- package/src/identity/ens/ensLookup/validation.ts +72 -0
- package/src/identity/ens/ensLookup.ts +19 -0
- package/src/identity/ens/ensRegistration.ts +199 -0
- package/src/identity/ens/resolverDelegation.ts +48 -0
- package/src/identity/hub/IdentityHub.tsx +13 -815
- package/src/identity/hub/OperationalRoutes.tsx +370 -0
- package/src/identity/hub/Routes.tsx +361 -0
- package/src/identity/hub/advancedEnsValidation.ts +45 -0
- package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
- package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
- package/src/identity/hub/components/FlowTimeline.tsx +27 -0
- package/src/identity/hub/components/IdentitySummary.tsx +190 -0
- package/src/identity/hub/components/MenuScreen.tsx +237 -0
- package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
- package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
- package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
- package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
- package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
- package/src/identity/hub/effects/create.ts +310 -0
- package/src/identity/hub/effects/ens/flows.ts +218 -0
- package/src/identity/hub/effects/ens/index.ts +11 -0
- package/src/identity/hub/effects/ens/transactions.ts +239 -0
- package/src/identity/hub/effects/index.ts +74 -0
- package/src/identity/hub/effects/profile/profileState.ts +173 -0
- package/src/identity/hub/effects/publicProfile/index.ts +5 -0
- package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
- package/src/identity/hub/effects/rebackup/index.ts +7 -0
- package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
- package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
- package/src/identity/hub/effects/receipts.ts +46 -0
- package/src/identity/hub/effects/restore/apply.ts +112 -0
- package/src/identity/hub/effects/restore/auth.ts +159 -0
- package/src/identity/hub/effects/restore/discover.ts +86 -0
- package/src/identity/hub/effects/restore/envelopes.ts +21 -0
- package/src/identity/hub/effects/restore/fetch.ts +25 -0
- package/src/identity/hub/effects/restore/index.ts +22 -0
- package/src/identity/hub/effects/restore/recovery.ts +135 -0
- package/src/identity/hub/effects/restore/resolve.ts +102 -0
- package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
- package/src/identity/hub/effects/restore/shared.ts +91 -0
- package/src/identity/hub/effects/restoreAdmin.ts +93 -0
- package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
- package/src/identity/hub/effects/shared/snapshot.ts +336 -0
- package/src/identity/hub/effects/shared/sync.ts +190 -0
- package/src/identity/hub/effects/token-transfer/index.ts +6 -0
- package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
- package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
- package/src/identity/hub/effects/types.ts +53 -0
- package/src/identity/hub/effects/vault/preflight.ts +50 -0
- package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
- package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
- package/src/identity/hub/flows/continuity/RecoveryConfirmScreen.tsx +104 -0
- package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
- package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
- package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
- package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
- package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
- package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
- package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
- package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
- package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
- package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
- package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
- package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
- package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
- package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
- package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
- package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
- package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
- package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
- package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
- package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
- package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
- package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
- package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
- package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
- package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +25 -43
- package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
- package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
- package/src/identity/hub/identityHubReducer.ts +166 -101
- package/src/identity/hub/model/continuity.ts +94 -0
- package/src/identity/hub/model/copy.ts +35 -0
- package/src/identity/hub/model/custody.ts +54 -0
- package/src/identity/hub/model/ens.ts +49 -0
- package/src/identity/hub/model/errors.ts +140 -0
- package/src/identity/hub/model/format.ts +15 -0
- package/src/identity/hub/model/identity.ts +94 -0
- package/src/identity/hub/model/network.ts +32 -0
- package/src/identity/hub/model/transfer.ts +57 -0
- package/src/identity/hub/operatorWallets.ts +131 -0
- package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
- package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
- package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
- package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
- package/src/identity/hub/reconciliation/index.ts +21 -0
- package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
- package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
- package/src/identity/hub/txGuard.ts +51 -0
- package/src/identity/hub/types.ts +17 -0
- package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
- package/src/identity/hub/useIdentityHubController.ts +396 -0
- package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
- package/src/identity/hub/utils.ts +79 -0
- package/src/identity/identityCompat.ts +34 -0
- package/src/identity/profile/agentIcon.ts +61 -0
- package/src/identity/profile/imagePicker.ts +12 -12
- package/src/identity/registry/erc8004/abi.ts +14 -0
- package/src/identity/registry/erc8004/chains.ts +150 -0
- package/src/identity/registry/erc8004/client.ts +11 -0
- package/src/identity/registry/erc8004/discovery.ts +511 -0
- package/src/identity/registry/erc8004/metadata.ts +335 -0
- package/src/identity/registry/erc8004/ownership.ts +121 -0
- package/src/identity/registry/erc8004/preflight.ts +123 -0
- package/src/identity/registry/erc8004/transactions.ts +77 -0
- package/src/identity/registry/erc8004/types.ts +88 -0
- package/src/identity/registry/erc8004/uri.ts +59 -0
- package/src/identity/registry/erc8004/utils.ts +58 -0
- package/src/identity/registry/erc8004.ts +53 -1106
- package/src/identity/registry/fieldParsers.ts +28 -0
- package/src/identity/registry/operatorVault/bytecode.ts +98 -0
- package/src/identity/registry/operatorVault/constants.ts +38 -0
- package/src/identity/registry/operatorVault/read.ts +246 -0
- package/src/identity/registry/operatorVault/transactions.ts +81 -0
- package/src/identity/registry/operatorVault.ts +44 -0
- package/src/identity/storage/ipfs.ts +26 -24
- package/src/identity/wallet/browserWallet/gas.ts +41 -0
- package/src/identity/wallet/browserWallet/html.ts +106 -0
- package/src/identity/wallet/browserWallet/http.ts +28 -0
- package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
- package/src/identity/wallet/browserWallet/requests.ts +191 -0
- package/src/identity/wallet/browserWallet/session.ts +325 -0
- package/src/identity/wallet/browserWallet/types.ts +192 -0
- package/src/identity/wallet/browserWallet/validation.ts +74 -0
- package/src/identity/wallet/browserWallet.ts +30 -393
- package/src/identity/wallet/page/constants.ts +5 -0
- package/src/identity/wallet/page/controller.ts +251 -0
- package/src/identity/wallet/page/copy.ts +340 -0
- package/src/identity/wallet/page/grainient.ts +278 -0
- package/src/identity/wallet/page/html.ts +28 -0
- package/src/identity/wallet/page/markup.ts +50 -0
- package/src/identity/wallet/page/state.ts +9 -0
- package/src/identity/wallet/page/styles/base.ts +259 -0
- package/src/identity/wallet/page/styles/components.ts +262 -0
- package/src/identity/wallet/page/styles/index.ts +5 -0
- package/src/identity/wallet/page/styles/responsive.ts +247 -0
- package/src/identity/wallet/page/types.ts +47 -0
- package/src/identity/wallet/page/view.ts +535 -0
- package/src/identity/wallet/page/walletProvider.ts +70 -0
- package/src/identity/wallet/page.tsx +38 -0
- package/src/identity/wallet/walletPurposeCompat.ts +27 -0
- package/src/mcp/manager.ts +0 -1
- package/src/models/ModelPicker.tsx +36 -30
- package/src/models/catalog.ts +5 -2
- package/src/models/huggingface.ts +9 -9
- package/src/models/llamacpp.ts +13 -13
- package/src/models/modelDisplay.ts +75 -0
- package/src/models/modelPickerOptions.ts +16 -3
- package/src/models/modelRecommendation.ts +0 -1
- package/src/providers/errors.ts +16 -0
- package/src/providers/gemini.ts +252 -39
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +1 -1
- package/src/runtime/sessionMode.ts +1 -1
- package/src/runtime/systemPrompt.ts +2 -0
- package/src/runtime/toolExecution.ts +18 -22
- package/src/runtime/toolIntent.ts +0 -20
- package/src/runtime/turn.ts +0 -92
- package/src/storage/atomicWrite.ts +4 -1
- package/src/storage/config.ts +181 -5
- package/src/storage/identity.ts +9 -3
- package/src/storage/secrets.ts +2 -2
- package/src/tools/bashSafety.ts +8 -0
- package/src/tools/changeDirectoryTool.ts +1 -1
- package/src/tools/deleteFileTool.ts +4 -4
- package/src/tools/editTool.ts +4 -4
- package/src/tools/editUtils.ts +5 -5
- package/src/tools/privateContinuityEditTool.ts +4 -5
- package/src/tools/privateContinuityReadTool.ts +1 -2
- package/src/tools/registry.ts +30 -0
- package/src/tools/writeFileTool.ts +5 -5
- package/src/ui/BrandSplash.tsx +20 -85
- package/src/ui/ProgressBar.tsx +3 -5
- package/src/ui/Select.tsx +21 -9
- package/src/ui/Spinner.tsx +38 -3
- package/src/ui/Surface.tsx +3 -3
- package/src/ui/TextInput.tsx +191 -29
- package/src/ui/theme.ts +7 -34
- package/src/utils/openExternal.ts +21 -0
- package/src/utils/withRetry.ts +47 -3
- package/src/identity/hub/identityHubEffects.ts +0 -937
- package/src/identity/hub/identityHubModel.ts +0 -291
- package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -144
- package/src/identity/hub/screens/EditProfileFlow.tsx +0 -145
- package/src/identity/hub/screens/IdentitySummary.tsx +0 -90
- package/src/identity/hub/screens/MenuScreen.tsx +0 -117
- package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +0 -87
- package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
- package/src/identity/wallet/wallet-page/wallet.html +0 -1202
- /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { getAddress, isAddress, type Address } from 'viem'
|
|
2
|
+
import type { WalletContinuityRestoreAccessKey } from '../continuity/envelope.js'
|
|
3
|
+
|
|
4
|
+
export type ApprovedOperatorWalletRecord = {
|
|
5
|
+
address: Address
|
|
6
|
+
challenge?: string
|
|
7
|
+
verifiedAt?: string
|
|
8
|
+
restoreAccessKey?: WalletContinuityRestoreAccessKey
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ApprovedOperatorWalletInput = string | ApprovedOperatorWalletRecord
|
|
12
|
+
|
|
13
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
|
14
|
+
|
|
15
|
+
function isZeroAddress(addr: string): boolean {
|
|
16
|
+
return addr.toLowerCase() === ZERO_ADDRESS
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeApprovedOperatorWallets(input: unknown): ApprovedOperatorWalletRecord[] {
|
|
20
|
+
if (!Array.isArray(input)) return []
|
|
21
|
+
const out: ApprovedOperatorWalletRecord[] = []
|
|
22
|
+
const seen = new Set<string>()
|
|
23
|
+
for (const item of input) {
|
|
24
|
+
const parsed = parseApprovedOperatorWalletRecord(item)
|
|
25
|
+
if (!parsed) continue
|
|
26
|
+
const key = parsed.address.toLowerCase()
|
|
27
|
+
if (seen.has(key)) continue
|
|
28
|
+
seen.add(key)
|
|
29
|
+
out.push(parsed)
|
|
30
|
+
}
|
|
31
|
+
return out
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function mergeApprovedOperatorWallets(
|
|
35
|
+
existing: unknown,
|
|
36
|
+
additions: unknown,
|
|
37
|
+
options?: { walletAddress?: string },
|
|
38
|
+
): ApprovedOperatorWalletRecord[] {
|
|
39
|
+
let out = normalizeApprovedOperatorWallets(existing)
|
|
40
|
+
for (const record of normalizeApprovedOperatorWallets(additions)) {
|
|
41
|
+
out = upsertApprovedOperatorWallet(out, record, options)
|
|
42
|
+
}
|
|
43
|
+
return out
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function upsertApprovedOperatorWallet(
|
|
47
|
+
existing: unknown,
|
|
48
|
+
record: ApprovedOperatorWalletInput,
|
|
49
|
+
options?: { walletAddress?: string },
|
|
50
|
+
): ApprovedOperatorWalletRecord[] {
|
|
51
|
+
const parsed = parseApprovedOperatorWalletRecord(record)
|
|
52
|
+
if (!parsed) throw new Error('Approved operator wallet must include a valid address')
|
|
53
|
+
if (options?.walletAddress && options.walletAddress.trim()) {
|
|
54
|
+
if (!isAddress(options.walletAddress, { strict: false })) throw new Error('Owner address is invalid')
|
|
55
|
+
if (parsed.address.toLowerCase() === options.walletAddress.toLowerCase()) {
|
|
56
|
+
throw new Error('Operator wallet must be different from the owner wallet')
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const records = normalizeApprovedOperatorWallets(existing)
|
|
60
|
+
const key = parsed.address.toLowerCase()
|
|
61
|
+
const index = records.findIndex(item => item.address.toLowerCase() === key)
|
|
62
|
+
if (index === -1) return [...records, parsed]
|
|
63
|
+
const next = records.slice()
|
|
64
|
+
next[index] = { ...records[index], ...parsed, address: parsed.address }
|
|
65
|
+
return next
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function removeApprovedOperatorWallet(
|
|
69
|
+
existing: unknown,
|
|
70
|
+
address: string,
|
|
71
|
+
activeOperatorAddress?: string,
|
|
72
|
+
): ApprovedOperatorWalletRecord[] {
|
|
73
|
+
if (!isAddress(address, { strict: false })) throw new Error('Operator wallet address is invalid')
|
|
74
|
+
const target = getAddress(address)
|
|
75
|
+
if (activeOperatorAddress && activeOperatorAddress.toLowerCase() === target.toLowerCase()) {
|
|
76
|
+
throw new Error('Set another active operator wallet before removing this wallet')
|
|
77
|
+
}
|
|
78
|
+
return normalizeApprovedOperatorWallets(existing).filter(item => item.address.toLowerCase() !== target.toLowerCase())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function assertActiveOperatorIsApproved(
|
|
82
|
+
records: unknown,
|
|
83
|
+
activeOperatorAddress: string | undefined,
|
|
84
|
+
): Address | undefined {
|
|
85
|
+
if (!activeOperatorAddress || !activeOperatorAddress.trim()) return undefined
|
|
86
|
+
if (!isAddress(activeOperatorAddress, { strict: false })) throw new Error('Active operator wallet address is invalid')
|
|
87
|
+
const active = getAddress(activeOperatorAddress)
|
|
88
|
+
const approved = normalizeApprovedOperatorWallets(records)
|
|
89
|
+
if (!approved.some(item => item.address.toLowerCase() === active.toLowerCase())) {
|
|
90
|
+
throw new Error('Active operator wallet must be one of the approved operator wallets')
|
|
91
|
+
}
|
|
92
|
+
return active
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseApprovedOperatorWalletRecord(input: unknown): ApprovedOperatorWalletRecord | null {
|
|
96
|
+
if (typeof input === 'string') {
|
|
97
|
+
const trimmed = input.trim()
|
|
98
|
+
if (!isAddress(trimmed, { strict: false })) return null
|
|
99
|
+
if (isZeroAddress(trimmed)) return null
|
|
100
|
+
return { address: getAddress(trimmed) }
|
|
101
|
+
}
|
|
102
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return null
|
|
103
|
+
const obj = input as Record<string, unknown>
|
|
104
|
+
const rawAddress = obj.address
|
|
105
|
+
if (typeof rawAddress !== 'string' || !isAddress(rawAddress, { strict: false })) return null
|
|
106
|
+
if (isZeroAddress(rawAddress)) return null
|
|
107
|
+
const challenge = typeof obj.challenge === 'string' && obj.challenge.trim() ? obj.challenge : undefined
|
|
108
|
+
const verifiedAt = typeof obj.verifiedAt === 'string' && obj.verifiedAt.trim() ? obj.verifiedAt : undefined
|
|
109
|
+
return {
|
|
110
|
+
address: getAddress(rawAddress),
|
|
111
|
+
...(challenge ? { challenge } : {}),
|
|
112
|
+
...(verifiedAt ? { verifiedAt } : {}),
|
|
113
|
+
...(parseRestoreAccessKey(obj.restoreAccessKey) ? { restoreAccessKey: parseRestoreAccessKey(obj.restoreAccessKey)! } : {}),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseRestoreAccessKey(input: unknown): WalletContinuityRestoreAccessKey | undefined {
|
|
118
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return undefined
|
|
119
|
+
const obj = input as Partial<WalletContinuityRestoreAccessKey>
|
|
120
|
+
if (typeof obj.address !== 'string' || !isAddress(obj.address, { strict: false })) return undefined
|
|
121
|
+
if (typeof obj.challenge !== 'string' || !obj.challenge.trim()) return undefined
|
|
122
|
+
if (typeof obj.salt !== 'string' || !obj.salt.trim()) return undefined
|
|
123
|
+
if (typeof obj.kemPublicKey !== 'string' || !obj.kemPublicKey.trim()) return undefined
|
|
124
|
+
return {
|
|
125
|
+
address: getAddress(obj.address),
|
|
126
|
+
challenge: obj.challenge,
|
|
127
|
+
salt: obj.salt,
|
|
128
|
+
kemPublicKey: obj.kemPublicKey,
|
|
129
|
+
...(typeof obj.createdAt === 'string' && obj.createdAt.trim() ? { createdAt: obj.createdAt } : {}),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import type { EthagentConfig } from '../../../../storage/config.js'
|
|
3
|
+
import { emptyReconciliation, runReconciliation } from './run.js'
|
|
4
|
+
import type { AgentReconciliation } from './types.js'
|
|
5
|
+
|
|
6
|
+
export function useAgentReconciliation(
|
|
7
|
+
config: EthagentConfig | null,
|
|
8
|
+
): { reconciliation: AgentReconciliation; refresh: () => void } {
|
|
9
|
+
const [reconciliation, setReconciliation] = useState<AgentReconciliation>(() => ({
|
|
10
|
+
token: 'no-agent',
|
|
11
|
+
custody: 'unknown',
|
|
12
|
+
agentUri: 'unknown',
|
|
13
|
+
ensRecords: 'unset',
|
|
14
|
+
vault: 'unset',
|
|
15
|
+
workingTree: 'unknown',
|
|
16
|
+
rpc: 'reachable',
|
|
17
|
+
driftCount: 0,
|
|
18
|
+
lastCheckedAt: new Date(0).toISOString(),
|
|
19
|
+
}))
|
|
20
|
+
const [refreshKey, setRefreshKey] = useState(0)
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!config?.identity || !config.identity.agentId || !config.identity.chainId || !config.identity.identityRegistryAddress) {
|
|
24
|
+
setReconciliation(emptyReconciliation())
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
const identity = config.identity
|
|
28
|
+
let cancelled = false
|
|
29
|
+
;(async () => {
|
|
30
|
+
const result = await runReconciliation(identity, config)
|
|
31
|
+
if (cancelled) return
|
|
32
|
+
setReconciliation(result)
|
|
33
|
+
})()
|
|
34
|
+
return () => { cancelled = true }
|
|
35
|
+
}, [
|
|
36
|
+
config?.identity?.agentId,
|
|
37
|
+
config?.identity?.chainId,
|
|
38
|
+
config?.identity?.address,
|
|
39
|
+
config?.identity?.backup?.agentUri,
|
|
40
|
+
config?.identity?.state,
|
|
41
|
+
config?.erc8004?.operatorVaults,
|
|
42
|
+
refreshKey,
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
return { reconciliation, refresh: () => setRefreshKey(k => k + 1) }
|
|
46
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { getAddress, type Address } from 'viem'
|
|
2
|
+
import {
|
|
3
|
+
createErc8004PublicClient,
|
|
4
|
+
validateErc8004TokenOwner,
|
|
5
|
+
type Erc8004RegistryConfig,
|
|
6
|
+
} from '../../../registry/erc8004.js'
|
|
7
|
+
import type { EthagentIdentity } from '../../../../storage/config.js'
|
|
8
|
+
|
|
9
|
+
export type OwnershipRole = 'token-holder' | 'vault-level-owner'
|
|
10
|
+
|
|
11
|
+
export type OwnershipGuardResult =
|
|
12
|
+
| { ok: true; effectiveOwner: Address; heldByVault: boolean }
|
|
13
|
+
| { ok: false; reason: 'not-owned' | 'lookup-failed'; detail: string; onChainOwner?: Address }
|
|
14
|
+
|
|
15
|
+
type OwnershipCacheEntry = {
|
|
16
|
+
expiresAt: number
|
|
17
|
+
result: OwnershipGuardResult
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const OWNERSHIP_CACHE_TTL_MS = 30_000
|
|
21
|
+
const ownershipCache = new Map<string, OwnershipCacheEntry>()
|
|
22
|
+
|
|
23
|
+
function ownershipCacheKey(args: {
|
|
24
|
+
registry: Erc8004RegistryConfig
|
|
25
|
+
agentId: bigint
|
|
26
|
+
expectedSigner: Address
|
|
27
|
+
requiredRole: OwnershipRole
|
|
28
|
+
}): string {
|
|
29
|
+
return [
|
|
30
|
+
args.registry.chainId,
|
|
31
|
+
args.registry.identityRegistryAddress.toLowerCase(),
|
|
32
|
+
args.agentId.toString(),
|
|
33
|
+
args.expectedSigner.toLowerCase(),
|
|
34
|
+
args.requiredRole,
|
|
35
|
+
].join('|')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function invalidateOwnershipCache(): void {
|
|
39
|
+
ownershipCache.clear()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function preflightTokenOwnership(args: {
|
|
43
|
+
identity: EthagentIdentity
|
|
44
|
+
registry: Erc8004RegistryConfig
|
|
45
|
+
operatorVaults?: Readonly<Record<string, string>>
|
|
46
|
+
requiredRole: OwnershipRole
|
|
47
|
+
expectedSigner?: Address
|
|
48
|
+
}): Promise<OwnershipGuardResult> {
|
|
49
|
+
if (!args.identity.agentId) {
|
|
50
|
+
return { ok: false, reason: 'lookup-failed', detail: 'identity has no agentId' }
|
|
51
|
+
}
|
|
52
|
+
const expectedSigner = getAddress(args.expectedSigner ?? args.identity.ownerAddress ?? args.identity.address)
|
|
53
|
+
const agentId = BigInt(args.identity.agentId)
|
|
54
|
+
const key = ownershipCacheKey({ registry: args.registry, agentId, expectedSigner, requiredRole: args.requiredRole })
|
|
55
|
+
const cached = ownershipCache.get(key)
|
|
56
|
+
if (cached && cached.expiresAt > Date.now()) return cached.result
|
|
57
|
+
const result = await runOwnershipPreflight({ ...args, agentId, expectedSigner })
|
|
58
|
+
ownershipCache.set(key, { result, expiresAt: Date.now() + OWNERSHIP_CACHE_TTL_MS })
|
|
59
|
+
return result
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function runOwnershipPreflight(args: {
|
|
63
|
+
registry: Erc8004RegistryConfig
|
|
64
|
+
operatorVaults?: Readonly<Record<string, string>>
|
|
65
|
+
requiredRole: OwnershipRole
|
|
66
|
+
agentId: bigint
|
|
67
|
+
expectedSigner: Address
|
|
68
|
+
}): Promise<OwnershipGuardResult> {
|
|
69
|
+
const validation = await validateErc8004TokenOwner({
|
|
70
|
+
...args.registry,
|
|
71
|
+
agentId: args.agentId,
|
|
72
|
+
expectedOwner: args.expectedSigner,
|
|
73
|
+
operatorVaults: args.operatorVaults,
|
|
74
|
+
})
|
|
75
|
+
if (validation.ok) {
|
|
76
|
+
const directOwner = await readDirectOwner({
|
|
77
|
+
registry: args.registry,
|
|
78
|
+
agentId: args.agentId,
|
|
79
|
+
})
|
|
80
|
+
if (directOwner.kind === 'error') {
|
|
81
|
+
return { ok: false, reason: 'lookup-failed', detail: directOwner.detail }
|
|
82
|
+
}
|
|
83
|
+
const heldByVault = directOwner.owner.toLowerCase() !== args.expectedSigner.toLowerCase()
|
|
84
|
+
if (args.requiredRole === 'token-holder' && heldByVault) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason: 'not-owned',
|
|
88
|
+
detail: `Token is held by the operator delegation vault (${directOwner.owner}). Withdraw it first.`,
|
|
89
|
+
onChainOwner: directOwner.owner,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, effectiveOwner: validation.ownerAddress, heldByVault }
|
|
93
|
+
}
|
|
94
|
+
if (validation.reason === 'token-owner-lookup-failed') {
|
|
95
|
+
return { ok: false, reason: 'lookup-failed', detail: validation.detail }
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
reason: 'not-owned',
|
|
100
|
+
detail: validation.detail,
|
|
101
|
+
onChainOwner: validation.ownerAddress,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function readDirectOwner(args: {
|
|
106
|
+
registry: Erc8004RegistryConfig
|
|
107
|
+
agentId: bigint
|
|
108
|
+
}): Promise<{ kind: 'ok'; owner: Address } | { kind: 'error'; detail: string }> {
|
|
109
|
+
try {
|
|
110
|
+
const client = createErc8004PublicClient(args.registry)
|
|
111
|
+
const owner = await client.readContract({
|
|
112
|
+
address: args.registry.identityRegistryAddress,
|
|
113
|
+
abi: [
|
|
114
|
+
{
|
|
115
|
+
type: 'function',
|
|
116
|
+
name: 'ownerOf',
|
|
117
|
+
stateMutability: 'view',
|
|
118
|
+
inputs: [{ name: 'tokenId', type: 'uint256' }],
|
|
119
|
+
outputs: [{ name: '', type: 'address' }],
|
|
120
|
+
},
|
|
121
|
+
] as const,
|
|
122
|
+
functionName: 'ownerOf',
|
|
123
|
+
args: [args.agentId],
|
|
124
|
+
})
|
|
125
|
+
return { kind: 'ok', owner: getAddress(owner as Address) }
|
|
126
|
+
} catch (err: unknown) {
|
|
127
|
+
return { kind: 'error', detail: err instanceof Error ? err.message : String(err) }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { getAddress, type Address, type PublicClient } from 'viem'
|
|
2
|
+
import {
|
|
3
|
+
createErc8004PublicClient,
|
|
4
|
+
erc8004ConfigForSupportedChain,
|
|
5
|
+
validateErc8004TokenOwner,
|
|
6
|
+
type Erc8004RegistryConfig,
|
|
7
|
+
} from '../../../registry/erc8004.js'
|
|
8
|
+
import { isAgentInVault, resolveConfiguredOperatorVaultAddress } from '../../../registry/operatorVault.js'
|
|
9
|
+
import type { EthagentConfig, EthagentIdentity } from '../../../../storage/config.js'
|
|
10
|
+
import { readOperatorVaultAddressField } from '../../../identityCompat.js'
|
|
11
|
+
import { reconcileWalletSetup, type RecordsFixPlan } from '../walletSetup.js'
|
|
12
|
+
import { readCustodyMode } from '../../model/custody.js'
|
|
13
|
+
import { continuityWorkingTreeStatus, type ContinuityWorkingTreeStatus } from '../../../continuity/storage.js'
|
|
14
|
+
import type { AgentReconciliation } from './types.js'
|
|
15
|
+
|
|
16
|
+
export function emptyReconciliation(): AgentReconciliation {
|
|
17
|
+
return {
|
|
18
|
+
token: 'no-agent',
|
|
19
|
+
custody: 'unknown',
|
|
20
|
+
agentUri: 'unknown',
|
|
21
|
+
ensRecords: 'unset',
|
|
22
|
+
vault: 'unset',
|
|
23
|
+
workingTree: 'unknown',
|
|
24
|
+
rpc: 'reachable',
|
|
25
|
+
driftCount: 0,
|
|
26
|
+
lastCheckedAt: new Date().toISOString(),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runReconciliation(
|
|
31
|
+
identity: EthagentIdentity,
|
|
32
|
+
config: EthagentConfig,
|
|
33
|
+
): Promise<AgentReconciliation> {
|
|
34
|
+
const fallback = (() => {
|
|
35
|
+
try { return erc8004ConfigForSupportedChain(identity.chainId!) }
|
|
36
|
+
catch { return null }
|
|
37
|
+
})()
|
|
38
|
+
const rpcUrl = identity.rpcUrl ?? config.erc8004?.rpcUrl ?? fallback?.rpcUrl ?? ''
|
|
39
|
+
const registry: Erc8004RegistryConfig = {
|
|
40
|
+
chainId: identity.chainId!,
|
|
41
|
+
rpcUrl,
|
|
42
|
+
identityRegistryAddress: identity.identityRegistryAddress! as `0x${string}`,
|
|
43
|
+
}
|
|
44
|
+
const expectedOwner = getAddress(identity.ownerAddress ?? identity.address) as Address
|
|
45
|
+
const agentId = BigInt(identity.agentId!)
|
|
46
|
+
const operatorVaults = config.erc8004?.operatorVaults
|
|
47
|
+
const vaultAddress = resolveReconciliationVaultAddress(identity, operatorVaults)
|
|
48
|
+
|
|
49
|
+
if (!rpcUrl) {
|
|
50
|
+
return {
|
|
51
|
+
token: 'unknown',
|
|
52
|
+
tokenDetail: `no rpcUrl configured for chain ${identity.chainId}`,
|
|
53
|
+
custody: 'unknown',
|
|
54
|
+
agentUri: 'unknown',
|
|
55
|
+
ensRecords: 'unknown',
|
|
56
|
+
vault: vaultAddress ? 'unknown' : 'unset',
|
|
57
|
+
workingTree: 'unknown',
|
|
58
|
+
rpc: 'failing',
|
|
59
|
+
driftCount: 0,
|
|
60
|
+
lastCheckedAt: new Date().toISOString(),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const client = createErc8004PublicClient(registry)
|
|
65
|
+
|
|
66
|
+
const [
|
|
67
|
+
tokenResult,
|
|
68
|
+
custodyResult,
|
|
69
|
+
agentUriResult,
|
|
70
|
+
ensReconcileResult,
|
|
71
|
+
vaultResult,
|
|
72
|
+
workingTreeResult,
|
|
73
|
+
] = await Promise.allSettled([
|
|
74
|
+
probeToken({ registry, agentId, expectedOwner, operatorVaults }),
|
|
75
|
+
probeCustody({ client, registry, agentId, expectedOwner, vaultAddress, identity }),
|
|
76
|
+
probeAgentUri({ client, registry, agentId, identity }),
|
|
77
|
+
probeEnsRecords({ identity, registry, client }),
|
|
78
|
+
probeVault({ client, vaultAddress }),
|
|
79
|
+
probeWorkingTree(identity),
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
const token = unwrap(tokenResult, fallbackToken)
|
|
83
|
+
const custody = unwrap(custodyResult, () => ({ kind: 'unknown' as const }))
|
|
84
|
+
const agentUri = unwrap(agentUriResult, () => ({ kind: 'unknown' as const }))
|
|
85
|
+
const ensRecords = unwrap(ensReconcileResult, () => ({ kind: 'unknown' as const }))
|
|
86
|
+
const vault = unwrap(vaultResult, () => ({ kind: vaultAddress ? 'unknown' as const : 'unset' as const }))
|
|
87
|
+
const workingTree = unwrap(workingTreeResult, () => ({ kind: 'unknown' as const }))
|
|
88
|
+
|
|
89
|
+
const allFailed = [tokenResult, custodyResult, agentUriResult, ensReconcileResult, vaultResult]
|
|
90
|
+
.every(r => r.status === 'rejected')
|
|
91
|
+
const rpc: 'reachable' | 'failing' = allFailed ? 'failing' : 'reachable'
|
|
92
|
+
|
|
93
|
+
const recon: AgentReconciliation = {
|
|
94
|
+
token: token.kind,
|
|
95
|
+
...(token.kind === 'unlinked' && token.detail ? { tokenDetail: token.detail } : {}),
|
|
96
|
+
...(token.kind === 'unlinked' ? { tokenAgentId: token.agentId } : {}),
|
|
97
|
+
...(token.kind === 'linked' ? { onChainOwner: token.onChainOwner } : {}),
|
|
98
|
+
...(token.kind === 'unlinked' && token.onChainOwner ? { onChainOwner: token.onChainOwner } : {}),
|
|
99
|
+
...(token.kind === 'unknown' && token.detail ? { tokenDetail: token.detail } : {}),
|
|
100
|
+
custody: custody.kind,
|
|
101
|
+
agentUri: agentUri.kind,
|
|
102
|
+
ensRecords: ensRecords.kind,
|
|
103
|
+
...(ensRecords.kind === 'drift' || ensRecords.kind === 'aligned' ? { ensRecordsPlan: ensRecords.plan } : {}),
|
|
104
|
+
vault: vault.kind,
|
|
105
|
+
workingTree: workingTree.kind,
|
|
106
|
+
rpc,
|
|
107
|
+
driftCount: 0,
|
|
108
|
+
lastCheckedAt: new Date().toISOString(),
|
|
109
|
+
}
|
|
110
|
+
recon.driftCount = computeDriftCount(recon)
|
|
111
|
+
return recon
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveReconciliationVaultAddress(
|
|
115
|
+
identity: EthagentIdentity,
|
|
116
|
+
operatorVaults?: Readonly<Record<string, string>>,
|
|
117
|
+
): Address | undefined {
|
|
118
|
+
const identityVault = readOperatorVaultAddressField(identity.state as Record<string, unknown> | undefined)
|
|
119
|
+
if (identityVault) return getAddress(identityVault)
|
|
120
|
+
if (readCustodyMode(identity.state as Record<string, unknown> | undefined) !== 'advanced') return undefined
|
|
121
|
+
if (!identity.chainId) return undefined
|
|
122
|
+
return resolveConfiguredOperatorVaultAddress(operatorVaults, identity.chainId)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type TokenProbe =
|
|
126
|
+
| { kind: 'linked'; onChainOwner: string }
|
|
127
|
+
| { kind: 'unlinked'; detail: string; agentId: string; onChainOwner?: string }
|
|
128
|
+
| { kind: 'unknown'; detail: string }
|
|
129
|
+
| { kind: 'no-agent' }
|
|
130
|
+
|
|
131
|
+
async function probeToken(args: {
|
|
132
|
+
registry: Erc8004RegistryConfig
|
|
133
|
+
agentId: bigint
|
|
134
|
+
expectedOwner: Address
|
|
135
|
+
operatorVaults?: Readonly<Record<string, string>>
|
|
136
|
+
}): Promise<TokenProbe> {
|
|
137
|
+
const result = await validateErc8004TokenOwner({
|
|
138
|
+
...args.registry,
|
|
139
|
+
agentId: args.agentId,
|
|
140
|
+
expectedOwner: args.expectedOwner,
|
|
141
|
+
operatorVaults: args.operatorVaults,
|
|
142
|
+
})
|
|
143
|
+
if (result.ok) return { kind: 'linked', onChainOwner: result.ownerAddress }
|
|
144
|
+
if (result.reason === 'token-owner-lookup-failed') return { kind: 'unknown', detail: result.detail }
|
|
145
|
+
return {
|
|
146
|
+
kind: 'unlinked',
|
|
147
|
+
detail: result.detail,
|
|
148
|
+
agentId: args.agentId.toString(),
|
|
149
|
+
...(result.ownerAddress ? { onChainOwner: result.ownerAddress } : {}),
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function fallbackToken(): TokenProbe {
|
|
154
|
+
return { kind: 'unknown', detail: 'token ownership probe failed' }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type CustodyProbe = { kind: 'simple' | 'advanced' | 'withdrawn' | 'mid-flow-uri-pending' | 'unknown' }
|
|
158
|
+
|
|
159
|
+
async function probeCustody(args: {
|
|
160
|
+
client: PublicClient
|
|
161
|
+
registry: Erc8004RegistryConfig
|
|
162
|
+
agentId: bigint
|
|
163
|
+
expectedOwner: Address
|
|
164
|
+
vaultAddress?: Address
|
|
165
|
+
identity: EthagentIdentity
|
|
166
|
+
}): Promise<CustodyProbe> {
|
|
167
|
+
if (!args.vaultAddress) return { kind: 'simple' }
|
|
168
|
+
try {
|
|
169
|
+
const status = await isAgentInVault({
|
|
170
|
+
client: args.client,
|
|
171
|
+
vaultAddress: args.vaultAddress,
|
|
172
|
+
registry: args.registry.identityRegistryAddress,
|
|
173
|
+
agentId: args.agentId,
|
|
174
|
+
})
|
|
175
|
+
if (!status.inVault) return { kind: 'withdrawn' }
|
|
176
|
+
const localUri = args.identity.agentUri ?? args.identity.backup?.agentUri
|
|
177
|
+
if (localUri) {
|
|
178
|
+
try {
|
|
179
|
+
const onChain = await args.client.readContract({
|
|
180
|
+
address: args.registry.identityRegistryAddress,
|
|
181
|
+
abi: ERC8004_AGENT_URI_ABI,
|
|
182
|
+
functionName: 'agentURI',
|
|
183
|
+
args: [args.agentId],
|
|
184
|
+
}) as string
|
|
185
|
+
if (onChain && onChain !== localUri) {
|
|
186
|
+
return { kind: 'mid-flow-uri-pending' }
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (status.ownerAddress?.toLowerCase() === args.expectedOwner.toLowerCase()) {
|
|
192
|
+
return { kind: 'advanced' }
|
|
193
|
+
}
|
|
194
|
+
return { kind: 'advanced' }
|
|
195
|
+
} catch {
|
|
196
|
+
return { kind: 'unknown' }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
type AgentUriProbe = { kind: 'in-sync' | 'chain-newer' | 'local-newer' | 'unknown' }
|
|
201
|
+
|
|
202
|
+
const ERC8004_AGENT_URI_ABI = [
|
|
203
|
+
{
|
|
204
|
+
type: 'function',
|
|
205
|
+
name: 'agentURI',
|
|
206
|
+
stateMutability: 'view',
|
|
207
|
+
inputs: [{ name: 'agentId', type: 'uint256' }],
|
|
208
|
+
outputs: [{ name: '', type: 'string' }],
|
|
209
|
+
},
|
|
210
|
+
] as const
|
|
211
|
+
|
|
212
|
+
async function probeAgentUri(args: {
|
|
213
|
+
client: PublicClient
|
|
214
|
+
registry: Erc8004RegistryConfig
|
|
215
|
+
agentId: bigint
|
|
216
|
+
identity: EthagentIdentity
|
|
217
|
+
}): Promise<AgentUriProbe> {
|
|
218
|
+
const localUri = args.identity.agentUri ?? args.identity.backup?.agentUri
|
|
219
|
+
if (!localUri) return { kind: 'unknown' }
|
|
220
|
+
try {
|
|
221
|
+
const onChain = await args.client.readContract({
|
|
222
|
+
address: args.registry.identityRegistryAddress,
|
|
223
|
+
abi: ERC8004_AGENT_URI_ABI,
|
|
224
|
+
functionName: 'agentURI',
|
|
225
|
+
args: [args.agentId],
|
|
226
|
+
}) as string
|
|
227
|
+
if (onChain === localUri) return { kind: 'in-sync' }
|
|
228
|
+
if (!onChain) return { kind: 'local-newer' }
|
|
229
|
+
return { kind: 'local-newer' }
|
|
230
|
+
} catch {
|
|
231
|
+
return { kind: 'unknown' }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
type EnsRecordsProbe =
|
|
236
|
+
| { kind: 'aligned'; plan: RecordsFixPlan }
|
|
237
|
+
| { kind: 'drift'; plan: RecordsFixPlan }
|
|
238
|
+
| { kind: 'unset' }
|
|
239
|
+
| { kind: 'unknown' }
|
|
240
|
+
|
|
241
|
+
async function probeEnsRecords(args: {
|
|
242
|
+
identity: EthagentIdentity
|
|
243
|
+
registry: Erc8004RegistryConfig
|
|
244
|
+
client: PublicClient
|
|
245
|
+
}): Promise<EnsRecordsProbe> {
|
|
246
|
+
const baseState = (args.identity.state ?? {}) as Record<string, unknown>
|
|
247
|
+
const ensName = typeof baseState.ensName === 'string' ? baseState.ensName.trim() : ''
|
|
248
|
+
const custody = readCustodyMode(baseState)
|
|
249
|
+
if (!ensName || custody !== 'advanced') return { kind: 'unset' }
|
|
250
|
+
try {
|
|
251
|
+
const plan = await reconcileWalletSetup({ identity: args.identity, registry: args.registry })
|
|
252
|
+
if (plan.items.length === 0) return { kind: 'aligned', plan }
|
|
253
|
+
const actionable = plan.items.some(item => item.kind === 'missing-approval' || item.kind === 'stale-approval')
|
|
254
|
+
return actionable ? { kind: 'drift', plan } : { kind: 'aligned', plan }
|
|
255
|
+
} catch {
|
|
256
|
+
return { kind: 'unknown' }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
type VaultProbe = { kind: 'confirmed' | 'missing' | 'unset' | 'unknown' }
|
|
261
|
+
|
|
262
|
+
async function probeVault(args: {
|
|
263
|
+
client: PublicClient
|
|
264
|
+
vaultAddress?: Address
|
|
265
|
+
}): Promise<VaultProbe> {
|
|
266
|
+
if (!args.vaultAddress) return { kind: 'unset' }
|
|
267
|
+
try {
|
|
268
|
+
const code = await args.client.getBytecode({ address: args.vaultAddress })
|
|
269
|
+
if (!code || code === '0x') return { kind: 'missing' }
|
|
270
|
+
return { kind: 'confirmed' }
|
|
271
|
+
} catch {
|
|
272
|
+
return { kind: 'unknown' }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
type WorkingTreeProbe = { kind: 'clean' | 'dirty' | 'unknown' }
|
|
277
|
+
|
|
278
|
+
async function probeWorkingTree(identity: EthagentIdentity): Promise<WorkingTreeProbe> {
|
|
279
|
+
try {
|
|
280
|
+
const status: ContinuityWorkingTreeStatus = await continuityWorkingTreeStatus(identity)
|
|
281
|
+
if (!status.ready) return { kind: 'unknown' }
|
|
282
|
+
return status.localChangedAfterBackup ? { kind: 'dirty' } : { kind: 'clean' }
|
|
283
|
+
} catch {
|
|
284
|
+
return { kind: 'unknown' }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function unwrap<T>(result: PromiseSettledResult<T>, fallback: () => T): T {
|
|
289
|
+
if (result.status === 'fulfilled') return result.value
|
|
290
|
+
return fallback()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function computeDriftCount(r: AgentReconciliation): number {
|
|
294
|
+
let n = 0
|
|
295
|
+
if (r.token === 'unlinked') n++
|
|
296
|
+
if (r.custody === 'mid-flow-uri-pending') n++
|
|
297
|
+
if (r.agentUri === 'local-newer' || r.agentUri === 'chain-newer') n++
|
|
298
|
+
if (r.ensRecords === 'drift') n++
|
|
299
|
+
if (r.vault === 'missing') n++
|
|
300
|
+
if (r.workingTree === 'dirty') n++
|
|
301
|
+
return n
|
|
302
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RecordsFixPlan } from '../walletSetup.js'
|
|
2
|
+
|
|
3
|
+
export type AgentReconciliation = {
|
|
4
|
+
token: 'linked' | 'unlinked' | 'unknown' | 'no-agent'
|
|
5
|
+
tokenDetail?: string
|
|
6
|
+
tokenAgentId?: string
|
|
7
|
+
onChainOwner?: string
|
|
8
|
+
custody: 'simple' | 'advanced' | 'withdrawn' | 'mid-flow-uri-pending' | 'unknown'
|
|
9
|
+
agentUri: 'in-sync' | 'chain-newer' | 'local-newer' | 'unknown'
|
|
10
|
+
ensRecords: 'aligned' | 'drift' | 'unset' | 'unknown'
|
|
11
|
+
ensRecordsPlan?: RecordsFixPlan
|
|
12
|
+
vault: 'confirmed' | 'missing' | 'unset' | 'unknown'
|
|
13
|
+
workingTree: 'clean' | 'dirty' | 'unknown'
|
|
14
|
+
rpc: 'reachable' | 'failing'
|
|
15
|
+
driftCount: number
|
|
16
|
+
lastCheckedAt: string
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
invalidateOwnershipCache,
|
|
3
|
+
preflightTokenOwnership,
|
|
4
|
+
useAgentReconciliation,
|
|
5
|
+
} from './useAgentReconciliation.js'
|
|
6
|
+
export type {
|
|
7
|
+
AgentReconciliation,
|
|
8
|
+
} from './useAgentReconciliation.js'
|
|
9
|
+
export {
|
|
10
|
+
computeApprovalDiff,
|
|
11
|
+
describeFixPlanItem,
|
|
12
|
+
encodeResolverApprovalChanges,
|
|
13
|
+
fixPlanRequiresOwnerWallet,
|
|
14
|
+
reconcileWalletSetup,
|
|
15
|
+
verifyResolverApprovalsLanded,
|
|
16
|
+
} from './walletSetup.js'
|
|
17
|
+
export type {
|
|
18
|
+
ApprovalDiff,
|
|
19
|
+
RecordsFixPlan,
|
|
20
|
+
RecordsFixPlanItem,
|
|
21
|
+
} from './walletSetup.js'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { useAgentReconciliation } from './agentReconciliation/hook.js'
|
|
2
|
+
export type { AgentReconciliation } from './agentReconciliation/types.js'
|
|
3
|
+
export {
|
|
4
|
+
invalidateOwnershipCache,
|
|
5
|
+
preflightTokenOwnership,
|
|
6
|
+
} from './agentReconciliation/ownership.js'
|
|
7
|
+
export type {
|
|
8
|
+
OwnershipGuardResult,
|
|
9
|
+
OwnershipRole,
|
|
10
|
+
} from './agentReconciliation/ownership.js'
|