ethagent 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +126 -30
- package/package.json +7 -2
- package/src/app/FirstRun.tsx +190 -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 +35 -15
- package/src/chat/ContextLimitView.tsx +4 -4
- package/src/chat/ContinuityEditReviewView.tsx +10 -22
- 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/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 +4 -2
- 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 -817
- 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/{screens → flows/continuity}/RecoveryConfirmScreen.tsx +28 -19
- 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 +23 -44
- 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 +164 -99
- 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 +20 -8
- package/src/ui/Spinner.tsx +38 -3
- package/src/ui/Surface.tsx +2 -2
- package/src/ui/TextInput.tsx +63 -20
- 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 -371
- package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -156
- package/src/identity/hub/screens/EditProfileFlow.tsx +0 -146
- package/src/identity/hub/screens/IdentitySummary.tsx +0 -106
- package/src/identity/hub/screens/MenuScreen.tsx +0 -117
- 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,159 @@
|
|
|
1
|
+
import { getAddress, type Address } from 'viem'
|
|
2
|
+
import {
|
|
3
|
+
ContinuitySnapshotOwnerMismatchError,
|
|
4
|
+
ContinuitySnapshotRestoreSlotMissingError,
|
|
5
|
+
findRestorableAddressForSnapshot,
|
|
6
|
+
isWalletContinuitySnapshotEnvelope,
|
|
7
|
+
walletContinuitySnapshotSlotForAddress,
|
|
8
|
+
type ContinuitySnapshotEnvelope,
|
|
9
|
+
} from '../../../continuity/envelope.js'
|
|
10
|
+
import { assertAgentStateBackupOwner } from '../../../crypto/backupEnvelope.js'
|
|
11
|
+
import type { Erc8004AgentCandidate } from '../../../registry/erc8004.js'
|
|
12
|
+
import { requestBrowserWalletAccount, type WalletPurpose } from '../../../wallet/browserWallet.js'
|
|
13
|
+
import type { Step } from '../../identityHubReducer.js'
|
|
14
|
+
import type { EffectCallbacks } from '../types.js'
|
|
15
|
+
import { isContinuitySnapshotEnvelope, parseRestorableEnvelope } from './envelopes.js'
|
|
16
|
+
import {
|
|
17
|
+
isAuthorizedOperatorAddress,
|
|
18
|
+
isTransferContinuitySnapshot,
|
|
19
|
+
ownerCandidateAddresses,
|
|
20
|
+
requesterAddressFromHandle,
|
|
21
|
+
restoreTransferSlotForRequester,
|
|
22
|
+
} from './shared.js'
|
|
23
|
+
|
|
24
|
+
export async function runRestoreConnectWallet(
|
|
25
|
+
step: Extract<Step, { kind: 'restore-wallet' }>,
|
|
26
|
+
callbacks: EffectCallbacks,
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
const wallet = await requestBrowserWalletAccount({
|
|
29
|
+
onReady: callbacks.onWalletReady,
|
|
30
|
+
purpose: 'connect-operator-wallet',
|
|
31
|
+
})
|
|
32
|
+
callbacks.onStep({ kind: 'restore-network', ownerHandle: wallet.account, purpose: step.purpose })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function restoreSignatureRequestForStep(
|
|
36
|
+
step: Extract<Step, { kind: 'restore-authorizing' }>,
|
|
37
|
+
): {
|
|
38
|
+
expectedAccount: Address
|
|
39
|
+
message: string
|
|
40
|
+
purpose: WalletPurpose
|
|
41
|
+
role: 'owner-wallet' | 'operator-wallet'
|
|
42
|
+
} {
|
|
43
|
+
if (isContinuitySnapshotEnvelope(step.envelope)) {
|
|
44
|
+
const requesterAddress = step.requesterAddress ? requesterAddressFromHandle(step.requesterAddress) : undefined
|
|
45
|
+
if (requesterAddress && isWalletContinuitySnapshotEnvelope(step.envelope)) {
|
|
46
|
+
const slot = walletContinuitySnapshotSlotForAddress(step.envelope, requesterAddress)
|
|
47
|
+
if (slot) {
|
|
48
|
+
const ownerAddress = getAddress(step.envelope.ownerAddress)
|
|
49
|
+
const requesterIsOwner = requesterAddress.toLowerCase() === ownerAddress.toLowerCase()
|
|
50
|
+
return {
|
|
51
|
+
expectedAccount: requesterAddress,
|
|
52
|
+
message: slot.challenge,
|
|
53
|
+
purpose: requesterIsOwner ? 'restore-owner-wallet' : 'restore-operator-wallet',
|
|
54
|
+
role: requesterIsOwner ? 'owner-wallet' : 'operator-wallet',
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (isAuthorizedOperatorAddress(step.candidate, requesterAddress)) {
|
|
58
|
+
throw new Error('Restore Slot Missing: re-save this agent once with the owner wallet.')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (requesterAddress && isTransferContinuitySnapshot(step.envelope)) {
|
|
62
|
+
const transferSlot = restoreTransferSlotForRequester(step.envelope, requesterAddress)
|
|
63
|
+
if (transferSlot) {
|
|
64
|
+
const ownerAddress = getAddress(step.envelope.ownerAddress)
|
|
65
|
+
const requesterIsOwner = requesterAddress.toLowerCase() === ownerAddress.toLowerCase()
|
|
66
|
+
return {
|
|
67
|
+
expectedAccount: requesterAddress,
|
|
68
|
+
message: transferSlot.challenge,
|
|
69
|
+
purpose: requesterIsOwner ? 'restore-owner-wallet' : 'restore-operator-wallet',
|
|
70
|
+
role: requesterIsOwner ? 'owner-wallet' : 'operator-wallet',
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const ownerCandidates = ownerCandidateAddresses(step.candidate)
|
|
75
|
+
if (requesterAddress
|
|
76
|
+
&& !ownerCandidates.some(addr => addr.toLowerCase() === requesterAddress.toLowerCase())
|
|
77
|
+
&& isAuthorizedOperatorAddress(step.candidate, requesterAddress)) {
|
|
78
|
+
throw new Error('Restore Slot Missing: re-save this agent once with the owner wallet.')
|
|
79
|
+
}
|
|
80
|
+
if (isWalletContinuitySnapshotEnvelope(step.envelope)) {
|
|
81
|
+
for (const candidateAddress of ownerCandidates) {
|
|
82
|
+
const slot = walletContinuitySnapshotSlotForAddress(step.envelope, candidateAddress)
|
|
83
|
+
if (slot) {
|
|
84
|
+
return {
|
|
85
|
+
expectedAccount: candidateAddress,
|
|
86
|
+
message: slot.challenge,
|
|
87
|
+
purpose: 'restore-owner-wallet',
|
|
88
|
+
role: 'owner-wallet',
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw new Error('Restore Slot Missing: re-save this agent once with the owner wallet.')
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
expectedAccount: ownerCandidates[0] ?? getAddress(step.candidate.ownerAddress),
|
|
96
|
+
message: step.envelope.challenge,
|
|
97
|
+
purpose: 'restore-owner-wallet',
|
|
98
|
+
role: 'owner-wallet',
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
expectedAccount: getAddress(step.candidate.ownerAddress),
|
|
103
|
+
message: step.envelope.challenge,
|
|
104
|
+
purpose: 'restore-owner-wallet',
|
|
105
|
+
role: 'owner-wallet',
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function restoreMessageForWallet(envelope: ContinuitySnapshotEnvelope, address: Address): string {
|
|
110
|
+
if (isWalletContinuitySnapshotEnvelope(envelope)) {
|
|
111
|
+
const slot = walletContinuitySnapshotSlotForAddress(envelope, address)
|
|
112
|
+
if (!slot) throw new Error('Restore Slot Missing: re-save this agent once with the owner wallet.')
|
|
113
|
+
return slot.challenge
|
|
114
|
+
}
|
|
115
|
+
if (isTransferContinuitySnapshot(envelope)) {
|
|
116
|
+
const slot = restoreTransferSlotForRequester(envelope, address)
|
|
117
|
+
if (!slot) throw new Error('Restore Slot Missing: re-save this agent once with the owner wallet.')
|
|
118
|
+
return slot.challenge
|
|
119
|
+
}
|
|
120
|
+
return envelope.challenge
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function assertCandidateCanReadEnvelope(
|
|
124
|
+
candidate: Erc8004AgentCandidate,
|
|
125
|
+
requesterHandle: string | undefined,
|
|
126
|
+
envelope: ReturnType<typeof parseRestorableEnvelope>,
|
|
127
|
+
): void {
|
|
128
|
+
const requester = requesterHandle ? requesterAddressFromHandle(requesterHandle) : undefined
|
|
129
|
+
const candidateAddresses: string[] = []
|
|
130
|
+
const seen = new Set<string>()
|
|
131
|
+
const add = (addr: string | undefined): void => {
|
|
132
|
+
if (!addr) return
|
|
133
|
+
const lower = addr.toLowerCase()
|
|
134
|
+
if (seen.has(lower)) return
|
|
135
|
+
seen.add(lower)
|
|
136
|
+
candidateAddresses.push(addr)
|
|
137
|
+
}
|
|
138
|
+
add(candidate.ownerAddress)
|
|
139
|
+
add(requester)
|
|
140
|
+
if (candidate.operators?.activeOperatorAddress) add(candidate.operators.activeOperatorAddress)
|
|
141
|
+
for (const record of candidate.operators?.approvedOperatorWallets ?? []) add(record.address)
|
|
142
|
+
|
|
143
|
+
if (isContinuitySnapshotEnvelope(envelope)) {
|
|
144
|
+
if (findRestorableAddressForSnapshot(envelope, candidateAddresses)) return
|
|
145
|
+
if (isWalletContinuitySnapshotEnvelope(envelope)) {
|
|
146
|
+
throw new ContinuitySnapshotRestoreSlotMissingError(requester ?? candidate.ownerAddress)
|
|
147
|
+
}
|
|
148
|
+
const snapshotOwner = (envelope as { ownerAddress?: string }).ownerAddress ?? ''
|
|
149
|
+
throw new ContinuitySnapshotOwnerMismatchError(snapshotOwner, requester ?? candidate.ownerAddress)
|
|
150
|
+
}
|
|
151
|
+
for (const addr of candidateAddresses) {
|
|
152
|
+
try {
|
|
153
|
+
assertAgentStateBackupOwner(envelope, addr)
|
|
154
|
+
return
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
assertAgentStateBackupOwner(envelope, candidate.ownerAddress)
|
|
159
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { EthagentConfig } from '../../../../storage/config.js'
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_IPFS_API_URL,
|
|
4
|
+
} from '../../../storage/ipfs.js'
|
|
5
|
+
import {
|
|
6
|
+
discoverOwnedAgentBackups,
|
|
7
|
+
type Erc8004AgentCandidate,
|
|
8
|
+
type Erc8004RegistryConfig,
|
|
9
|
+
} from '../../../registry/erc8004.js'
|
|
10
|
+
import type { RestorePurpose, Step } from '../../identityHubReducer.js'
|
|
11
|
+
import type { EffectCallbacks } from '../types.js'
|
|
12
|
+
import { isAbortError, isAuthorizedOperatorAddress, requesterAddressFromHandle } from './shared.js'
|
|
13
|
+
import type { Address } from 'viem'
|
|
14
|
+
|
|
15
|
+
export async function runRestoreDiscover(
|
|
16
|
+
step: Extract<Step, { kind: 'restore-discovering' }>,
|
|
17
|
+
_config: EthagentConfig | undefined,
|
|
18
|
+
callbacks: EffectCallbacks,
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
const signal = step.abortSignal
|
|
21
|
+
let owned: Erc8004AgentCandidate[]
|
|
22
|
+
try {
|
|
23
|
+
owned = await discoverOwnedAgentBackups({
|
|
24
|
+
...step.registry,
|
|
25
|
+
ownerHandle: step.ownerHandle,
|
|
26
|
+
ipfsApiUrl: DEFAULT_IPFS_API_URL,
|
|
27
|
+
...(signal ? { signal } : {}),
|
|
28
|
+
})
|
|
29
|
+
} catch (err: unknown) {
|
|
30
|
+
if (signal?.aborted || isAbortError(err)) return
|
|
31
|
+
callbacks.onStep({
|
|
32
|
+
kind: 'restore-not-found',
|
|
33
|
+
ownerHandle: step.ownerHandle,
|
|
34
|
+
registry: step.registry,
|
|
35
|
+
requesterAddress: requesterAddressFromHandle(step.ownerHandle),
|
|
36
|
+
reason: 'search-incomplete',
|
|
37
|
+
purpose: step.purpose,
|
|
38
|
+
})
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if (signal?.aborted) return
|
|
42
|
+
if (owned.length === 0) {
|
|
43
|
+
callbacks.onStep({
|
|
44
|
+
kind: 'restore-recovery-input',
|
|
45
|
+
ownerHandle: step.ownerHandle,
|
|
46
|
+
registry: step.registry,
|
|
47
|
+
purpose: step.purpose,
|
|
48
|
+
})
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
callbacks.onStep(restoreTokenSelectionStep({
|
|
52
|
+
ownerHandle: step.ownerHandle,
|
|
53
|
+
registry: step.registry,
|
|
54
|
+
candidates: owned,
|
|
55
|
+
requesterAddress: requesterAddressFromHandle(step.ownerHandle),
|
|
56
|
+
purpose: step.purpose,
|
|
57
|
+
}))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function restoreTokenSelectionStep(args: {
|
|
61
|
+
ownerHandle: string
|
|
62
|
+
registry: Erc8004RegistryConfig
|
|
63
|
+
candidates: Erc8004AgentCandidate[]
|
|
64
|
+
requesterAddress?: Address
|
|
65
|
+
purpose?: RestorePurpose
|
|
66
|
+
}): Extract<Step, { kind: 'restore-select-token' }> {
|
|
67
|
+
const restorable = args.candidates.filter(candidate => candidate.backup?.cid)
|
|
68
|
+
if (restorable.length === 0) {
|
|
69
|
+
throw new Error(args.candidates.length === 0
|
|
70
|
+
? 'No agent identities found for that wallet on this network'
|
|
71
|
+
: 'No matching agent identity has recoverable ethagent state on this network')
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
kind: 'restore-select-token',
|
|
75
|
+
ownerHandle: args.ownerHandle,
|
|
76
|
+
registry: args.registry,
|
|
77
|
+
candidates: restorable,
|
|
78
|
+
requesterAddress: args.requesterAddress,
|
|
79
|
+
purpose: args.purpose,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function canRestoreCandidate(candidate: Erc8004AgentCandidate, address: Address): boolean {
|
|
84
|
+
if (candidate.ownerAddress.toLowerCase() === address.toLowerCase()) return true
|
|
85
|
+
return isAuthorizedOperatorAddress(candidate, address)
|
|
86
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { parseAgentStateBackupEnvelope } from '../../../crypto/backupEnvelope.js'
|
|
2
|
+
import {
|
|
3
|
+
CONTINUITY_SNAPSHOT_ENVELOPE_VERSION,
|
|
4
|
+
parseContinuitySnapshotEnvelope,
|
|
5
|
+
type ContinuitySnapshotEnvelope,
|
|
6
|
+
} from '../../../continuity/envelope.js'
|
|
7
|
+
|
|
8
|
+
export type RestorableEnvelope = ReturnType<typeof parseAgentStateBackupEnvelope> | ContinuitySnapshotEnvelope
|
|
9
|
+
|
|
10
|
+
export function parseRestorableEnvelope(raw: string | Uint8Array): RestorableEnvelope {
|
|
11
|
+
const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw)
|
|
12
|
+
const parsed = JSON.parse(text) as { envelopeVersion?: unknown }
|
|
13
|
+
if (parsed.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION) {
|
|
14
|
+
return parseContinuitySnapshotEnvelope(text)
|
|
15
|
+
}
|
|
16
|
+
return parseAgentStateBackupEnvelope(text)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isContinuitySnapshotEnvelope(envelope: RestorableEnvelope): envelope is ContinuitySnapshotEnvelope {
|
|
20
|
+
return envelope.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { catFromIpfs } from '../../../storage/ipfs.js'
|
|
2
|
+
import type { Step } from '../../identityHubReducer.js'
|
|
3
|
+
import type { EffectCallbacks } from '../types.js'
|
|
4
|
+
import { parseRestorableEnvelope } from './envelopes.js'
|
|
5
|
+
import { assertCandidateCanReadEnvelope, restoreSignatureRequestForStep } from './auth.js'
|
|
6
|
+
|
|
7
|
+
export async function runRestoreFetch(
|
|
8
|
+
step: Extract<Step, { kind: 'restore-fetching' }>,
|
|
9
|
+
callbacks: EffectCallbacks,
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
const raw = await catFromIpfs(step.apiUrl, step.cid)
|
|
12
|
+
const envelope = parseRestorableEnvelope(raw)
|
|
13
|
+
assertCandidateCanReadEnvelope(step.candidate, step.requesterAddress, envelope)
|
|
14
|
+
const nextStep: Extract<Step, { kind: 'restore-authorizing' }> = {
|
|
15
|
+
kind: 'restore-authorizing',
|
|
16
|
+
cid: step.cid,
|
|
17
|
+
apiUrl: step.apiUrl,
|
|
18
|
+
envelope,
|
|
19
|
+
candidate: step.candidate,
|
|
20
|
+
requesterAddress: step.requesterAddress,
|
|
21
|
+
purpose: step.purpose,
|
|
22
|
+
}
|
|
23
|
+
restoreSignatureRequestForStep(nextStep)
|
|
24
|
+
callbacks.onStep(nextStep)
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
runRestoreAuthorize,
|
|
3
|
+
} from './apply.js'
|
|
4
|
+
export {
|
|
5
|
+
restoreSignatureRequestForStep,
|
|
6
|
+
runRestoreConnectWallet,
|
|
7
|
+
} from './auth.js'
|
|
8
|
+
export {
|
|
9
|
+
canRestoreCandidate,
|
|
10
|
+
restoreTokenSelectionStep,
|
|
11
|
+
runRestoreDiscover,
|
|
12
|
+
} from './discover.js'
|
|
13
|
+
export {
|
|
14
|
+
runRestoreFetch,
|
|
15
|
+
} from './fetch.js'
|
|
16
|
+
export {
|
|
17
|
+
runRecoveryRefetch,
|
|
18
|
+
} from './recovery.js'
|
|
19
|
+
export {
|
|
20
|
+
resolveAgentEnsToCandidate,
|
|
21
|
+
resolveAgentTokenIdToCandidate,
|
|
22
|
+
} from './resolve.js'
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { getAddress, type Address } from 'viem'
|
|
2
|
+
import type { EthagentIdentity } from '../../../../storage/config.js'
|
|
3
|
+
import {
|
|
4
|
+
assertContinuitySnapshotOwner,
|
|
5
|
+
isWalletContinuitySnapshotEnvelope,
|
|
6
|
+
restoreContinuitySnapshotEnvelope,
|
|
7
|
+
transferSnapshotMetadataFromEnvelope,
|
|
8
|
+
} from '../../../continuity/envelope.js'
|
|
9
|
+
import { ensureIdentityMarkdownScaffold, localContinuitySnapshotContentHashes, writeContinuityFiles } from '../../../continuity/storage.js'
|
|
10
|
+
import { recordPublishedContinuitySnapshot, updatePublishedContinuitySnapshotContentHashes } from '../../../continuity/snapshots.js'
|
|
11
|
+
import { catFromIpfs, DEFAULT_IPFS_API_URL } from '../../../storage/ipfs.js'
|
|
12
|
+
import {
|
|
13
|
+
discoverOwnedAgentBackupByTokenId,
|
|
14
|
+
type Erc8004RegistryConfig,
|
|
15
|
+
} from '../../../registry/erc8004.js'
|
|
16
|
+
import { requestBrowserWalletSignature } from '../../../wallet/browserWallet.js'
|
|
17
|
+
import { setOperatorVaultAddressField } from '../../../identityCompat.js'
|
|
18
|
+
import type { EffectCallbacks } from '../types.js'
|
|
19
|
+
import { isContinuitySnapshotEnvelope, parseRestorableEnvelope } from './envelopes.js'
|
|
20
|
+
import { restoreMessageForWallet } from './auth.js'
|
|
21
|
+
import { type BackupMetadata, operatorStateFromCandidate, restorePublishedPublicSkills } from './shared.js'
|
|
22
|
+
|
|
23
|
+
export async function runRecoveryRefetch(
|
|
24
|
+
identity: EthagentIdentity,
|
|
25
|
+
registry: Erc8004RegistryConfig,
|
|
26
|
+
callbacks: EffectCallbacks,
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
if (!identity.agentId) throw new Error('Cannot refetch: identity is missing an agent token ID')
|
|
29
|
+
const ownerAddress = getAddress(identity.ownerAddress ?? identity.address)
|
|
30
|
+
const candidate = await discoverOwnedAgentBackupByTokenId({
|
|
31
|
+
...registry,
|
|
32
|
+
ownerHandle: ownerAddress,
|
|
33
|
+
tokenId: BigInt(identity.agentId),
|
|
34
|
+
ipfsApiUrl: identity.backup?.ipfsApiUrl ?? DEFAULT_IPFS_API_URL,
|
|
35
|
+
})
|
|
36
|
+
if (!candidate.backup?.cid) {
|
|
37
|
+
throw new Error('The published agent does not have a recoverable encrypted snapshot')
|
|
38
|
+
}
|
|
39
|
+
const apiUrl = identity.backup?.ipfsApiUrl ?? DEFAULT_IPFS_API_URL
|
|
40
|
+
const raw = await catFromIpfs(apiUrl, candidate.backup.cid)
|
|
41
|
+
const envelope = parseRestorableEnvelope(raw)
|
|
42
|
+
if (!isContinuitySnapshotEnvelope(envelope)) {
|
|
43
|
+
throw new Error('This snapshot is in an unsupported envelope format and cannot be refetched here; use Load Agent')
|
|
44
|
+
}
|
|
45
|
+
const eligibleAddresses: Address[] = [ownerAddress]
|
|
46
|
+
if (isWalletContinuitySnapshotEnvelope(envelope)) {
|
|
47
|
+
for (const slot of envelope.slots) {
|
|
48
|
+
const slotAddress = getAddress(slot.address)
|
|
49
|
+
if (!eligibleAddresses.some(a => a.toLowerCase() === slotAddress.toLowerCase())) {
|
|
50
|
+
eligibleAddresses.push(slotAddress)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
assertContinuitySnapshotOwner(envelope, ownerAddress)
|
|
55
|
+
}
|
|
56
|
+
const wallet = await requestBrowserWalletSignature({
|
|
57
|
+
chainId: candidate.chainId,
|
|
58
|
+
purpose: 'refetch-snapshot',
|
|
59
|
+
messageForAccount: account => {
|
|
60
|
+
const matched = eligibleAddresses.find(a => a.toLowerCase() === account.toLowerCase())
|
|
61
|
+
if (!matched) {
|
|
62
|
+
throw new Error(`Operator Wallet Required: ${account} is not authorized for this agent. Connect the owner wallet or an authorized operator wallet.`)
|
|
63
|
+
}
|
|
64
|
+
return restoreMessageForWallet(envelope, matched)
|
|
65
|
+
},
|
|
66
|
+
onReady: callbacks.onWalletReady,
|
|
67
|
+
})
|
|
68
|
+
callbacks.onWalletReady(null)
|
|
69
|
+
callbacks.onRestoreProgress?.({ phase: 'decrypting', label: 'signature received, decrypting onchain snapshot...' })
|
|
70
|
+
const payload = restoreContinuitySnapshotEnvelope({
|
|
71
|
+
envelope,
|
|
72
|
+
walletSignature: wallet.signature,
|
|
73
|
+
currentOwnerAddress: getAddress(wallet.account),
|
|
74
|
+
})
|
|
75
|
+
callbacks.onRestoreProgress?.({ phase: 'writing', label: 'restoring SOUL.md, MEMORY.md, and skills.json...' })
|
|
76
|
+
const transferSnapshot = transferSnapshotMetadataFromEnvelope(envelope)
|
|
77
|
+
const refreshedBackup: BackupMetadata = {
|
|
78
|
+
cid: candidate.backup.cid,
|
|
79
|
+
createdAt: envelope.createdAt,
|
|
80
|
+
envelopeVersion: envelope.envelopeVersion,
|
|
81
|
+
ipfsApiUrl: apiUrl,
|
|
82
|
+
status: 'restored',
|
|
83
|
+
ownerAddress,
|
|
84
|
+
chainId: candidate.chainId,
|
|
85
|
+
rpcUrl: candidate.rpcUrl,
|
|
86
|
+
identityRegistryAddress: candidate.identityRegistryAddress,
|
|
87
|
+
agentId: candidate.agentId.toString(),
|
|
88
|
+
agentUri: candidate.agentUri,
|
|
89
|
+
metadataCid: candidate.metadataCid,
|
|
90
|
+
...(transferSnapshot ? { transferSnapshot } : {}),
|
|
91
|
+
}
|
|
92
|
+
const refreshedState: Record<string, unknown> = {
|
|
93
|
+
...payload.state,
|
|
94
|
+
...(candidate.name ? { name: candidate.name } : {}),
|
|
95
|
+
...(candidate.description ? { description: candidate.description } : {}),
|
|
96
|
+
...(candidate.imageUrl ? { imageUrl: candidate.imageUrl } : {}),
|
|
97
|
+
...operatorStateFromCandidate(candidate),
|
|
98
|
+
}
|
|
99
|
+
const tokenOwnerAddress = candidate.tokenOwnerAddress ?? candidate.ownerAddress
|
|
100
|
+
if (tokenOwnerAddress.toLowerCase() !== candidate.ownerAddress.toLowerCase()) {
|
|
101
|
+
setOperatorVaultAddressField(refreshedState, getAddress(tokenOwnerAddress))
|
|
102
|
+
}
|
|
103
|
+
const nextIdentity: EthagentIdentity = {
|
|
104
|
+
...identity,
|
|
105
|
+
source: 'erc8004',
|
|
106
|
+
address: ownerAddress,
|
|
107
|
+
ownerAddress,
|
|
108
|
+
chainId: candidate.chainId,
|
|
109
|
+
rpcUrl: candidate.rpcUrl,
|
|
110
|
+
identityRegistryAddress: candidate.identityRegistryAddress,
|
|
111
|
+
agentId: candidate.agentId.toString(),
|
|
112
|
+
agentUri: candidate.agentUri,
|
|
113
|
+
metadataCid: candidate.metadataCid,
|
|
114
|
+
state: refreshedState,
|
|
115
|
+
backup: refreshedBackup,
|
|
116
|
+
...(candidate.publicDiscovery ? {
|
|
117
|
+
publicSkills: {
|
|
118
|
+
...(candidate.publicDiscovery.skillsCid ? { cid: candidate.publicDiscovery.skillsCid } : {}),
|
|
119
|
+
...(candidate.publicDiscovery.agentCardCid ? { agentCardCid: candidate.publicDiscovery.agentCardCid } : {}),
|
|
120
|
+
...(candidate.publicDiscovery.updatedAt ? { updatedAt: candidate.publicDiscovery.updatedAt } : {}),
|
|
121
|
+
status: 'pinned',
|
|
122
|
+
},
|
|
123
|
+
} : {}),
|
|
124
|
+
}
|
|
125
|
+
await writeContinuityFiles(nextIdentity, payload.files)
|
|
126
|
+
callbacks.onRestoreProgress?.({ phase: 'finishing', label: 'finalizing refreshed identity...' })
|
|
127
|
+
const publicSkillsRestored = await restorePublishedPublicSkills(nextIdentity, apiUrl, candidate.publicDiscovery?.skillsCid)
|
|
128
|
+
await ensureIdentityMarkdownScaffold(nextIdentity)
|
|
129
|
+
await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'Refetched Latest Snapshot From Chain' }).catch(() => null)
|
|
130
|
+
if (publicSkillsRestored) {
|
|
131
|
+
const contentHashes = await localContinuitySnapshotContentHashes(nextIdentity)
|
|
132
|
+
await updatePublishedContinuitySnapshotContentHashes(nextIdentity, candidate.backup.cid, contentHashes).catch(() => null)
|
|
133
|
+
}
|
|
134
|
+
await callbacks.onIdentityComplete(nextIdentity, 'Latest Published Snapshot Restored From Chain', 'update')
|
|
135
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type Address } from 'viem'
|
|
2
|
+
import { DEFAULT_IPFS_API_URL } from '../../../storage/ipfs.js'
|
|
3
|
+
import {
|
|
4
|
+
createErc8004PublicClient,
|
|
5
|
+
discoverOwnedAgentBackupByTokenId,
|
|
6
|
+
type Erc8004AgentCandidate,
|
|
7
|
+
type Erc8004RegistryConfig,
|
|
8
|
+
} from '../../../registry/erc8004.js'
|
|
9
|
+
import { parseAgentTokenReference, readEthagentTextRecords } from '../../../ens/ensLookup.js'
|
|
10
|
+
import { AGENT_RECORD_KEYS } from '../../../ens/agentRecords.js'
|
|
11
|
+
|
|
12
|
+
const ETH_NAME_PATTERN = /^([a-z0-9-]+\.)+eth$/i
|
|
13
|
+
|
|
14
|
+
type AgentEnsResolution =
|
|
15
|
+
| { ok: true; candidate: Erc8004AgentCandidate }
|
|
16
|
+
| { ok: false; message: string }
|
|
17
|
+
|
|
18
|
+
export async function resolveAgentEnsToCandidate(
|
|
19
|
+
ensName: string,
|
|
20
|
+
registry: Erc8004RegistryConfig,
|
|
21
|
+
): Promise<AgentEnsResolution> {
|
|
22
|
+
const trimmed = ensName.trim()
|
|
23
|
+
if (!trimmed) return { ok: false, message: 'Enter an agent ENS name (e.g. agent.example.eth).' }
|
|
24
|
+
if (!ETH_NAME_PATTERN.test(trimmed)) return { ok: false, message: 'Enter a valid .eth name.' }
|
|
25
|
+
let records: Record<string, string>
|
|
26
|
+
try {
|
|
27
|
+
records = await readEthagentTextRecords(trimmed, [AGENT_RECORD_KEYS.token])
|
|
28
|
+
} catch (err: unknown) {
|
|
29
|
+
return { ok: false, message: `Could not reach Ethereum mainnet to resolve ${trimmed}: ${err instanceof Error ? err.message : String(err)}` }
|
|
30
|
+
}
|
|
31
|
+
const tokenValue = records[AGENT_RECORD_KEYS.token]
|
|
32
|
+
if (!tokenValue) return { ok: false, message: `${trimmed} has no org.ethagent.token record. Use the full agent subdomain (e.g. agent.${trimmed}).` }
|
|
33
|
+
const tokenRef = parseAgentTokenReference(tokenValue)
|
|
34
|
+
if (!tokenRef) return { ok: false, message: `${trimmed}'s org.ethagent.token record is not a valid eip155 reference.` }
|
|
35
|
+
if (tokenRef.chainId !== registry.chainId) {
|
|
36
|
+
return { ok: false, message: `${trimmed}'s agent token is onchain ${tokenRef.chainId}, not the network you selected.` }
|
|
37
|
+
}
|
|
38
|
+
const finalRegistry: Erc8004RegistryConfig = registry.identityRegistryAddress.toLowerCase() === tokenRef.identityRegistryAddress.toLowerCase()
|
|
39
|
+
? registry
|
|
40
|
+
: { ...registry, identityRegistryAddress: tokenRef.identityRegistryAddress }
|
|
41
|
+
let owner: Address
|
|
42
|
+
try {
|
|
43
|
+
const publicClient = createErc8004PublicClient(finalRegistry)
|
|
44
|
+
owner = await publicClient.readContract({
|
|
45
|
+
address: finalRegistry.identityRegistryAddress,
|
|
46
|
+
abi: [{ inputs: [{ name: 'tokenId', type: 'uint256' }], name: 'ownerOf', outputs: [{ name: '', type: 'address' }], stateMutability: 'view', type: 'function' }] as const,
|
|
47
|
+
functionName: 'ownerOf',
|
|
48
|
+
args: [tokenRef.agentId],
|
|
49
|
+
}) as Address
|
|
50
|
+
} catch (err: unknown) {
|
|
51
|
+
return { ok: false, message: `Could not read ownerOf(token #${tokenRef.agentId.toString()}): ${err instanceof Error ? err.message : String(err)}` }
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const candidate = await discoverOwnedAgentBackupByTokenId({
|
|
55
|
+
...finalRegistry,
|
|
56
|
+
ownerHandle: owner,
|
|
57
|
+
ipfsApiUrl: DEFAULT_IPFS_API_URL,
|
|
58
|
+
tokenId: tokenRef.agentId,
|
|
59
|
+
})
|
|
60
|
+
return { ok: true, candidate }
|
|
61
|
+
} catch (err: unknown) {
|
|
62
|
+
return { ok: false, message: `Could not load agent token #${tokenRef.agentId.toString()}: ${err instanceof Error ? err.message : String(err)}` }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function resolveAgentTokenIdToCandidate(
|
|
67
|
+
rawTokenId: string,
|
|
68
|
+
registry: Erc8004RegistryConfig,
|
|
69
|
+
): Promise<AgentEnsResolution> {
|
|
70
|
+
const trimmed = rawTokenId.trim()
|
|
71
|
+
if (!trimmed) return { ok: false, message: 'Enter the agent token ID (e.g. 45744).' }
|
|
72
|
+
if (!/^\d+$/.test(trimmed)) return { ok: false, message: 'Token ID must be a positive integer (e.g. 45744).' }
|
|
73
|
+
let tokenId: bigint
|
|
74
|
+
try {
|
|
75
|
+
tokenId = BigInt(trimmed)
|
|
76
|
+
} catch {
|
|
77
|
+
return { ok: false, message: 'Token ID must be a positive integer (e.g. 45744).' }
|
|
78
|
+
}
|
|
79
|
+
let owner: Address
|
|
80
|
+
try {
|
|
81
|
+
const publicClient = createErc8004PublicClient(registry)
|
|
82
|
+
owner = await publicClient.readContract({
|
|
83
|
+
address: registry.identityRegistryAddress,
|
|
84
|
+
abi: [{ inputs: [{ name: 'tokenId', type: 'uint256' }], name: 'ownerOf', outputs: [{ name: '', type: 'address' }], stateMutability: 'view', type: 'function' }] as const,
|
|
85
|
+
functionName: 'ownerOf',
|
|
86
|
+
args: [tokenId],
|
|
87
|
+
}) as Address
|
|
88
|
+
} catch (err: unknown) {
|
|
89
|
+
return { ok: false, message: `Token #${trimmed} not found on this network: ${err instanceof Error ? err.message : String(err)}` }
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const candidate = await discoverOwnedAgentBackupByTokenId({
|
|
93
|
+
...registry,
|
|
94
|
+
ownerHandle: owner,
|
|
95
|
+
ipfsApiUrl: DEFAULT_IPFS_API_URL,
|
|
96
|
+
tokenId,
|
|
97
|
+
})
|
|
98
|
+
return { ok: true, candidate }
|
|
99
|
+
} catch (err: unknown) {
|
|
100
|
+
return { ok: false, message: `Could not load agent token #${trimmed}: ${err instanceof Error ? err.message : String(err)}` }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
canRestoreCandidate,
|
|
3
|
+
restoreTokenSelectionStep,
|
|
4
|
+
runRestoreDiscover,
|
|
5
|
+
} from './discover.js'
|
|
6
|
+
export {
|
|
7
|
+
resolveAgentEnsToCandidate,
|
|
8
|
+
resolveAgentTokenIdToCandidate,
|
|
9
|
+
} from './resolve.js'
|
|
10
|
+
export {
|
|
11
|
+
restoreSignatureRequestForStep,
|
|
12
|
+
runRestoreConnectWallet,
|
|
13
|
+
} from './auth.js'
|
|
14
|
+
export {
|
|
15
|
+
runRestoreFetch,
|
|
16
|
+
} from './fetch.js'
|
|
17
|
+
export {
|
|
18
|
+
runRestoreAuthorize,
|
|
19
|
+
} from './apply.js'
|
|
20
|
+
export {
|
|
21
|
+
runRecoveryRefetch,
|
|
22
|
+
} from './recovery.js'
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { getAddress, isAddress, type Address } from 'viem'
|
|
2
|
+
import type { EthagentIdentity } from '../../../../storage/config.js'
|
|
3
|
+
import {
|
|
4
|
+
type ContinuitySnapshotEnvelope,
|
|
5
|
+
type TransferContinuitySnapshotEnvelope,
|
|
6
|
+
} from '../../../continuity/envelope.js'
|
|
7
|
+
import { catFromIpfs } from '../../../storage/ipfs.js'
|
|
8
|
+
import type { Erc8004AgentCandidate } from '../../../registry/erc8004.js'
|
|
9
|
+
import { writePublicSkillsFile } from '../../../continuity/storage.js'
|
|
10
|
+
import { normalizeApprovedOperatorWallets } from '../../operatorWallets.js'
|
|
11
|
+
|
|
12
|
+
export type BackupMetadata = NonNullable<EthagentIdentity['backup']>
|
|
13
|
+
|
|
14
|
+
export function isAbortError(err: unknown): boolean {
|
|
15
|
+
if (!err || typeof err !== 'object') return false
|
|
16
|
+
const name = (err as { name?: unknown }).name
|
|
17
|
+
return name === 'AbortError'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function requesterAddressFromHandle(handle: string): Address | undefined {
|
|
21
|
+
return isAddress(handle, { strict: false }) ? getAddress(handle) : undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isTransferContinuitySnapshot(
|
|
25
|
+
envelope: ContinuitySnapshotEnvelope,
|
|
26
|
+
): envelope is TransferContinuitySnapshotEnvelope {
|
|
27
|
+
return (envelope as TransferContinuitySnapshotEnvelope).crypto?.decryptsWith === 'transfer-signature-slot'
|
|
28
|
+
&& typeof (envelope as TransferContinuitySnapshotEnvelope).targetAddress === 'string'
|
|
29
|
+
&& !!(envelope as TransferContinuitySnapshotEnvelope).slots
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function restoreTransferSlotForRequester(
|
|
33
|
+
envelope: TransferContinuitySnapshotEnvelope,
|
|
34
|
+
requesterAddress: Address,
|
|
35
|
+
): TransferContinuitySnapshotEnvelope['slots']['owner'] | null {
|
|
36
|
+
if (requesterAddress.toLowerCase() === getAddress(envelope.ownerAddress).toLowerCase()) {
|
|
37
|
+
return envelope.slots.owner
|
|
38
|
+
}
|
|
39
|
+
if (requesterAddress.toLowerCase() === getAddress(envelope.targetAddress).toLowerCase()) {
|
|
40
|
+
return envelope.slots.target
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isAuthorizedOperatorAddress(candidate: Erc8004AgentCandidate, address: Address): boolean {
|
|
46
|
+
const target = address.toLowerCase()
|
|
47
|
+
if (candidate.operators?.activeOperatorAddress?.toLowerCase() === target) return true
|
|
48
|
+
return candidate.operators?.approvedOperatorWallets.some(record => record.address.toLowerCase() === target) ?? false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ownerCandidateAddresses(candidate: Erc8004AgentCandidate): Address[] {
|
|
52
|
+
const out: Address[] = []
|
|
53
|
+
const seen = new Set<string>()
|
|
54
|
+
const push = (addr: Address | undefined): void => {
|
|
55
|
+
if (!addr) return
|
|
56
|
+
const lower = addr.toLowerCase()
|
|
57
|
+
if (seen.has(lower)) return
|
|
58
|
+
seen.add(lower)
|
|
59
|
+
out.push(getAddress(addr))
|
|
60
|
+
}
|
|
61
|
+
push(candidate.ownerAddress)
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function operatorStateFromCandidate(candidate: Erc8004AgentCandidate): Record<string, unknown> {
|
|
66
|
+
const operators = candidate.operators
|
|
67
|
+
if (!operators) return {}
|
|
68
|
+
return {
|
|
69
|
+
approvedOperatorWallets: normalizeApprovedOperatorWallets(operators.approvedOperatorWallets),
|
|
70
|
+
...(operators.activeOperatorAddress ? { activeOperatorAddress: operators.activeOperatorAddress } : {}),
|
|
71
|
+
...(operators.ownerAddress ? { ownerAddress: operators.ownerAddress } : {}),
|
|
72
|
+
...(operators.ensName ? { ensName: operators.ensName } : {}),
|
|
73
|
+
...(operators.restoreAccessEpoch ? { restoreAccessEpoch: operators.restoreAccessEpoch } : {}),
|
|
74
|
+
...(operators.ownerRestoreAccessKey ? { ownerRestoreAccessKey: operators.ownerRestoreAccessKey } : {}),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function restorePublishedPublicSkills(
|
|
79
|
+
identity: EthagentIdentity,
|
|
80
|
+
apiUrl: string,
|
|
81
|
+
cid: string | undefined,
|
|
82
|
+
): Promise<boolean> {
|
|
83
|
+
if (!cid) return false
|
|
84
|
+
try {
|
|
85
|
+
const raw = await catFromIpfs(apiUrl, cid)
|
|
86
|
+
await writePublicSkillsFile(identity, new TextDecoder().decode(raw))
|
|
87
|
+
return true
|
|
88
|
+
} catch {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
}
|