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,93 @@
|
|
|
1
|
+
import { getAddress, type Address } from 'viem'
|
|
2
|
+
import type { EthagentConfig, EthagentIdentity } from '../../../storage/config.js'
|
|
3
|
+
import { saveConfig } from '../../../storage/config.js'
|
|
4
|
+
import {
|
|
5
|
+
normalizeErc8004RegistryConfig,
|
|
6
|
+
type Erc8004RegistryConfig,
|
|
7
|
+
} from '../../registry/erc8004.js'
|
|
8
|
+
import { registryConfigFromConfig } from '../../registry/registryConfig.js'
|
|
9
|
+
import { readOwnerAddressField } from '../../identityCompat.js'
|
|
10
|
+
import {
|
|
11
|
+
sendBrowserWalletTransaction,
|
|
12
|
+
} from '../../wallet/browserWallet.js'
|
|
13
|
+
import {
|
|
14
|
+
encodeResolverApprovalChanges,
|
|
15
|
+
verifyResolverApprovalsLanded,
|
|
16
|
+
type RecordsFixPlan,
|
|
17
|
+
} from '../reconciliation/index.js'
|
|
18
|
+
import type { Step } from '../identityHubReducer.js'
|
|
19
|
+
import type { EffectCallbacks } from './types.js'
|
|
20
|
+
import { awaitOptionalReceipt } from './receipts.js'
|
|
21
|
+
import { createMainnetEnsPublicClient } from './ens/transactions.js'
|
|
22
|
+
|
|
23
|
+
export async function runRestoreRegistrySubmit(
|
|
24
|
+
value: string,
|
|
25
|
+
step: Extract<Step, { kind: 'restore-registry' }>,
|
|
26
|
+
config: EthagentConfig | undefined,
|
|
27
|
+
onConfigChange: ((config: EthagentConfig) => void) | undefined,
|
|
28
|
+
callbacks: EffectCallbacks,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const resolution = registryConfigFromConfig(config)
|
|
31
|
+
const registry = normalizeErc8004RegistryConfig({
|
|
32
|
+
chainId: resolution.chainId,
|
|
33
|
+
rpcUrl: resolution.config?.rpcUrl ?? resolution.defaultRpcUrl,
|
|
34
|
+
identityRegistryAddress: value.trim(),
|
|
35
|
+
})
|
|
36
|
+
if (config && onConfigChange) {
|
|
37
|
+
const next: EthagentConfig = {
|
|
38
|
+
...config,
|
|
39
|
+
erc8004: {
|
|
40
|
+
chainId: registry.chainId,
|
|
41
|
+
rpcUrl: registry.rpcUrl,
|
|
42
|
+
identityRegistryAddress: registry.identityRegistryAddress,
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
await saveConfig(next)
|
|
46
|
+
onConfigChange(next)
|
|
47
|
+
}
|
|
48
|
+
callbacks.onStep({ kind: 'restore-discovering', ownerHandle: step.ownerHandle, registry, purpose: step.purpose })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runFixRecordsSubmit(args: {
|
|
52
|
+
identity: EthagentIdentity
|
|
53
|
+
registry: Erc8004RegistryConfig
|
|
54
|
+
plan: RecordsFixPlan
|
|
55
|
+
callbacks: EffectCallbacks
|
|
56
|
+
}): Promise<void> {
|
|
57
|
+
const baseState = (args.identity.state ?? {}) as Record<string, unknown>
|
|
58
|
+
const ensName = args.plan.ensName ?? (typeof baseState.ensName === 'string' ? baseState.ensName.trim() : '')
|
|
59
|
+
if (!ensName) throw new Error('Cannot fix records: identity has no ENS subdomain')
|
|
60
|
+
const missing: Address[] = []
|
|
61
|
+
const stale: Address[] = []
|
|
62
|
+
for (const item of args.plan.items) {
|
|
63
|
+
if (item.kind === 'missing-approval') missing.push(item.address)
|
|
64
|
+
else if (item.kind === 'stale-approval') stale.push(item.address)
|
|
65
|
+
}
|
|
66
|
+
if (missing.length === 0 && stale.length === 0) return
|
|
67
|
+
const encoded = await encodeResolverApprovalChanges({
|
|
68
|
+
ensName,
|
|
69
|
+
diff: { added: missing, removed: stale },
|
|
70
|
+
})
|
|
71
|
+
if (!encoded) return
|
|
72
|
+
const ownerAddressRaw = readOwnerAddressField(baseState) ?? args.identity.ownerAddress ?? args.identity.address
|
|
73
|
+
const ownerAddress = getAddress(ownerAddressRaw)
|
|
74
|
+
const tx = await sendBrowserWalletTransaction({
|
|
75
|
+
chainId: 1,
|
|
76
|
+
expectedAccount: ownerAddress,
|
|
77
|
+
to: encoded.resolverAddress,
|
|
78
|
+
data: encoded.data,
|
|
79
|
+
onReady: args.callbacks.onWalletReady,
|
|
80
|
+
purpose: 'reconcile-resolver-approvals',
|
|
81
|
+
})
|
|
82
|
+
args.callbacks.onWalletReady(null)
|
|
83
|
+
const client = createMainnetEnsPublicClient()
|
|
84
|
+
await awaitOptionalReceipt(client, tx.txHash, 'Resolver approval reconciliation')
|
|
85
|
+
await verifyResolverApprovalsLanded({
|
|
86
|
+
ensName,
|
|
87
|
+
ownerAddress: ownerAddress,
|
|
88
|
+
resolverAddress: encoded.resolverAddress,
|
|
89
|
+
added: encoded.added,
|
|
90
|
+
removed: encoded.removed,
|
|
91
|
+
client,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { getAddress, type Address } from 'viem'
|
|
5
|
+
import type { EthagentIdentity } from '../../../../storage/config.js'
|
|
6
|
+
import type { ProfileUpdates } from '../../identityHubReducer.js'
|
|
7
|
+
import type { Erc8004RegistryConfig } from '../../../registry/erc8004.js'
|
|
8
|
+
import { addFileToIpfs, DEFAULT_IPFS_API_URL, type IpfsAddResult } from '../../../storage/ipfs.js'
|
|
9
|
+
import { agentIconContentType, isAgentIconUrl, validateAgentIconReference } from '../../../profile/agentIcon.js'
|
|
10
|
+
import {
|
|
11
|
+
applyEnsValidationState,
|
|
12
|
+
applyOperatorProfileState,
|
|
13
|
+
validateEnsForProfileUpdate,
|
|
14
|
+
} from '../profile/profileState.js'
|
|
15
|
+
|
|
16
|
+
type PreparedProfileState = {
|
|
17
|
+
state: Record<string, unknown>
|
|
18
|
+
nextName: string | undefined
|
|
19
|
+
nextDescription: string
|
|
20
|
+
nextEnsName: string | undefined
|
|
21
|
+
uploadedImageUri: string | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function deriveAgentName(identity: EthagentIdentity): string {
|
|
25
|
+
const state = (identity.state ?? {}) as Record<string, unknown>
|
|
26
|
+
const name = typeof state.name === 'string' ? state.name.trim() : ''
|
|
27
|
+
if (name) return name
|
|
28
|
+
return identity.agentId ? `agent #${identity.agentId}` : 'unnamed agent'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function resolveAgentIconReference(iconPath: string, pinataJwt: string | undefined): Promise<string> {
|
|
32
|
+
const validationError = validateAgentIconReference(iconPath)
|
|
33
|
+
if (validationError) throw new Error(validationError)
|
|
34
|
+
const trimmed = iconPath.trim()
|
|
35
|
+
if (isAgentIconUrl(trimmed)) return trimmed
|
|
36
|
+
const file = resolveAgentIconPath(trimmed)
|
|
37
|
+
const data = await fs.readFile(file)
|
|
38
|
+
const contentType = agentIconContentType(file)
|
|
39
|
+
const pin = await addFileToIpfs(DEFAULT_IPFS_API_URL, data, path.basename(file), contentType, fetch, { pinataJwt })
|
|
40
|
+
assertVerifiedPin(pin)
|
|
41
|
+
return `ipfs://${pin.cid}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveAgentIconPath(input: string): string {
|
|
45
|
+
const trimmed = input.trim()
|
|
46
|
+
if (!trimmed) throw new Error('Agent Icon path is empty')
|
|
47
|
+
return path.resolve(trimmed.replace(/^~(?=$|[\\/])/, os.homedir()))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function prepareProfileStateForSave(args: {
|
|
51
|
+
identity: EthagentIdentity
|
|
52
|
+
registry: Erc8004RegistryConfig
|
|
53
|
+
profileUpdates: ProfileUpdates | undefined
|
|
54
|
+
pinataJwt: string | undefined
|
|
55
|
+
ownerAddress: Address
|
|
56
|
+
walletAccount: Address
|
|
57
|
+
includeLastBackedUpAt: boolean
|
|
58
|
+
}): Promise<PreparedProfileState> {
|
|
59
|
+
const baseState = (args.identity.state ?? {}) as Record<string, unknown>
|
|
60
|
+
const profile = args.profileUpdates ?? {}
|
|
61
|
+
const nextName = typeof profile.name === 'string' && profile.name.trim()
|
|
62
|
+
? profile.name.trim()
|
|
63
|
+
: typeof baseState.name === 'string'
|
|
64
|
+
? baseState.name
|
|
65
|
+
: undefined
|
|
66
|
+
const nextDescription = profile.description !== undefined
|
|
67
|
+
? profile.description.trim()
|
|
68
|
+
: typeof baseState.description === 'string'
|
|
69
|
+
? baseState.description
|
|
70
|
+
: ''
|
|
71
|
+
const nextEnsName = typeof profile.ensName === 'string'
|
|
72
|
+
? profile.ensName.trim() || undefined
|
|
73
|
+
: typeof baseState.ensName === 'string' && baseState.ensName.trim()
|
|
74
|
+
? baseState.ensName.trim()
|
|
75
|
+
: undefined
|
|
76
|
+
const uploadedImageUri = profile.imagePath === 'delete'
|
|
77
|
+
? ''
|
|
78
|
+
: profile.imagePath
|
|
79
|
+
? await resolveAgentIconReference(profile.imagePath, args.pinataJwt)
|
|
80
|
+
: typeof baseState.imageUrl === 'string' && baseState.imageUrl.trim()
|
|
81
|
+
? baseState.imageUrl.trim()
|
|
82
|
+
: undefined
|
|
83
|
+
|
|
84
|
+
const state: Record<string, unknown> = {
|
|
85
|
+
...baseState,
|
|
86
|
+
ownerAddress: getAddress(args.ownerAddress),
|
|
87
|
+
...(nextName !== undefined ? { name: nextName } : {}),
|
|
88
|
+
description: nextDescription,
|
|
89
|
+
...(args.includeLastBackedUpAt ? { lastBackedUpAt: new Date().toISOString() } : {}),
|
|
90
|
+
}
|
|
91
|
+
if (uploadedImageUri === '') {
|
|
92
|
+
delete state.imageUrl
|
|
93
|
+
} else if (uploadedImageUri) {
|
|
94
|
+
state.imageUrl = uploadedImageUri
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
applyOperatorProfileState(state, profile, baseState)
|
|
98
|
+
if (typeof profile.ensName === 'string') {
|
|
99
|
+
if (nextEnsName) {
|
|
100
|
+
state.ensName = nextEnsName
|
|
101
|
+
const validation = await validateEnsForProfileUpdate(
|
|
102
|
+
nextEnsName,
|
|
103
|
+
args.walletAccount,
|
|
104
|
+
profile,
|
|
105
|
+
baseState,
|
|
106
|
+
args.identity,
|
|
107
|
+
args.registry,
|
|
108
|
+
)
|
|
109
|
+
applyEnsValidationState(state, validation, profile, baseState)
|
|
110
|
+
} else {
|
|
111
|
+
clearEnsProfileState(state)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
state,
|
|
117
|
+
nextName,
|
|
118
|
+
nextDescription,
|
|
119
|
+
nextEnsName,
|
|
120
|
+
uploadedImageUri,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function clearEnsProfileState(state: Record<string, unknown>): void {
|
|
125
|
+
delete state.ensName
|
|
126
|
+
delete state.ensValidation
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function assertVerifiedPin(pin: IpfsAddResult, expectedCid?: string): void {
|
|
130
|
+
if (expectedCid && pin.cid !== expectedCid) throw new Error('IPFS pin verification did not match the published CID')
|
|
131
|
+
if (!pin.pinVerified) throw new Error(`IPFS pin was not verified for ${pin.cid}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function readEnsOkFromState(state: Record<string, unknown> | undefined): boolean | undefined {
|
|
135
|
+
const raw = state?.ensValidation
|
|
136
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined
|
|
137
|
+
const ok = (raw as Record<string, unknown>).ok
|
|
138
|
+
return typeof ok === 'boolean' ? ok : undefined
|
|
139
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { getAddress, isAddress, type Address } from 'viem'
|
|
2
|
+
import type { EthagentIdentity } from '../../../../storage/config.js'
|
|
3
|
+
import type { ProfileUpdates } from '../../identityHubReducer.js'
|
|
4
|
+
import {
|
|
5
|
+
createWalletContinuitySnapshotEnvelope,
|
|
6
|
+
createWalletRestoreAccessKey,
|
|
7
|
+
type ContinuitySnapshotEnvelope,
|
|
8
|
+
type WalletChallengePurpose,
|
|
9
|
+
type WalletContinuityRestoreAccessKey,
|
|
10
|
+
} from '../../../continuity/envelope.js'
|
|
11
|
+
import {
|
|
12
|
+
continuityAgentSnapshot,
|
|
13
|
+
defaultContinuityFiles,
|
|
14
|
+
} from '../../../continuity/storage.js'
|
|
15
|
+
import type { Erc8004RegistryConfig, EthagentOperatorsPointer } from '../../../registry/erc8004.js'
|
|
16
|
+
import { readOwnerAddressField } from '../../../identityCompat.js'
|
|
17
|
+
import { readCustodyMode } from '../../model/custody.js'
|
|
18
|
+
import {
|
|
19
|
+
assertActiveOperatorIsApproved,
|
|
20
|
+
normalizeApprovedOperatorWallets,
|
|
21
|
+
} from '../../operatorWallets.js'
|
|
22
|
+
|
|
23
|
+
export type WalletRestoreAccessContext = {
|
|
24
|
+
token: { chainId: number; identityRegistryAddress: Address; agentId: string }
|
|
25
|
+
accessEpoch: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function expectedAccountForSnapshotSave(
|
|
29
|
+
identity: EthagentIdentity,
|
|
30
|
+
profileUpdates: ProfileUpdates | undefined,
|
|
31
|
+
walletAccess: WalletRestoreAccessContext | null,
|
|
32
|
+
): Address | undefined {
|
|
33
|
+
const ownerAddress = ownerAddressForSnapshotSave(identity, profileUpdates)
|
|
34
|
+
if (snapshotSaveRequiresOwnerSigner(identity, profileUpdates)) return ownerAddress
|
|
35
|
+
if (!walletAccess) return ownerAddress
|
|
36
|
+
if (!hasOwnerRestoreAccessKey(identity)) return ownerAddress
|
|
37
|
+
if (!hasOperatorRestoreAccessKey(identity, profileUpdates)) return ownerAddress
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function operatorSignerFor(identity: EthagentIdentity): Address | undefined {
|
|
42
|
+
const state = (identity.state ?? {}) as Record<string, unknown>
|
|
43
|
+
const activeOp = typeof state.activeOperatorAddress === 'string' && /^0x[a-fA-F0-9]{40}$/.test(state.activeOperatorAddress)
|
|
44
|
+
? getAddress(state.activeOperatorAddress)
|
|
45
|
+
: undefined
|
|
46
|
+
const approvedRaw = state.approvedOperatorWallets
|
|
47
|
+
const approved: Address[] = Array.isArray(approvedRaw)
|
|
48
|
+
? (approvedRaw as Array<{ address?: unknown }>)
|
|
49
|
+
.map(r => typeof r?.address === 'string' && /^0x[a-fA-F0-9]{40}$/.test(r.address) ? getAddress(r.address) : undefined)
|
|
50
|
+
.filter((a): a is Address => Boolean(a))
|
|
51
|
+
: []
|
|
52
|
+
const connected = identity.connectedWallet && /^0x[a-fA-F0-9]{40}$/.test(identity.connectedWallet)
|
|
53
|
+
? getAddress(identity.connectedWallet)
|
|
54
|
+
: undefined
|
|
55
|
+
if (connected) {
|
|
56
|
+
const lower = connected.toLowerCase()
|
|
57
|
+
if (activeOp && activeOp.toLowerCase() === lower) return connected
|
|
58
|
+
if (approved.some(a => a.toLowerCase() === lower)) return connected
|
|
59
|
+
}
|
|
60
|
+
if (activeOp) return activeOp
|
|
61
|
+
if (approved.length > 0) return approved[0]
|
|
62
|
+
return undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function operatorWalletCanSignSnapshotSave(
|
|
66
|
+
identity: EthagentIdentity,
|
|
67
|
+
profileUpdates: ProfileUpdates | undefined,
|
|
68
|
+
): boolean {
|
|
69
|
+
if (snapshotSaveRequiresOwnerSigner(identity, profileUpdates)) return false
|
|
70
|
+
if (!identity.agentId) return false
|
|
71
|
+
if (!hasOwnerRestoreAccessKey(identity)) return false
|
|
72
|
+
if (!hasOperatorRestoreAccessKey(identity, profileUpdates)) return false
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function snapshotSaveWalletRole(
|
|
77
|
+
identity: EthagentIdentity,
|
|
78
|
+
profileUpdates: ProfileUpdates | undefined,
|
|
79
|
+
): 'owner' | 'operator' | 'connected' {
|
|
80
|
+
if (operatorWalletCanSignSnapshotSave(identity, profileUpdates)) return 'operator'
|
|
81
|
+
const baseState = (identity.state ?? {}) as Record<string, unknown>
|
|
82
|
+
const approved = profileUpdates?.approvedOperatorWallets !== undefined
|
|
83
|
+
? normalizeApprovedOperatorWallets(profileUpdates.approvedOperatorWallets)
|
|
84
|
+
: normalizeApprovedOperatorWallets(baseState.approvedOperatorWallets)
|
|
85
|
+
if (approved.length > 0) return 'owner'
|
|
86
|
+
if (readCustodyMode(baseState) === 'advanced') return 'owner'
|
|
87
|
+
return 'connected'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function assertSnapshotSaveSignerAuthorized(
|
|
91
|
+
identity: EthagentIdentity,
|
|
92
|
+
profileUpdates: ProfileUpdates | undefined,
|
|
93
|
+
signerAddress: Address,
|
|
94
|
+
ownerAddress: Address,
|
|
95
|
+
walletAccess: WalletRestoreAccessContext | null,
|
|
96
|
+
): void {
|
|
97
|
+
if (signerAddress.toLowerCase() === ownerAddress.toLowerCase()) return
|
|
98
|
+
if (snapshotSaveRequiresOwnerSigner(identity, profileUpdates) || !walletAccess || !hasOwnerRestoreAccessKey(identity)) {
|
|
99
|
+
throw new Error(`Owner Wallet Required: connected wallet ${signerAddress} does not match owner wallet ${ownerAddress}`)
|
|
100
|
+
}
|
|
101
|
+
const authorized = restoreAccessKeysForSave(identity, profileUpdates)
|
|
102
|
+
.some(key => key.address.toLowerCase() === signerAddress.toLowerCase())
|
|
103
|
+
if (!authorized) {
|
|
104
|
+
throw new Error(`Operator Wallet Required: connected wallet ${signerAddress} is not authorized for this agent`)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function hasOwnerRestoreAccessKey(identity: EthagentIdentity): boolean {
|
|
109
|
+
const baseState = (identity.state ?? {}) as Record<string, unknown>
|
|
110
|
+
return Boolean(readRestoreAccessKey(baseState.ownerRestoreAccessKey))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function snapshotSaveRequiresOwnerSigner(
|
|
114
|
+
identity: EthagentIdentity,
|
|
115
|
+
profileUpdates: ProfileUpdates | undefined,
|
|
116
|
+
): boolean {
|
|
117
|
+
if (!profileUpdates) return false
|
|
118
|
+
if (
|
|
119
|
+
profileUpdates.ensName !== undefined
|
|
120
|
+
|| profileUpdates.custodyMode !== undefined
|
|
121
|
+
|| profileUpdates.ownerAddress !== undefined
|
|
122
|
+
|| profileUpdates.approvedOperatorWallets !== undefined
|
|
123
|
+
|| profileUpdates.activeOperatorAddress !== undefined
|
|
124
|
+
|| profileUpdates.operatorVaultAddress !== undefined
|
|
125
|
+
|| profileUpdates.restoreAccessEpoch !== undefined
|
|
126
|
+
) return true
|
|
127
|
+
const profileFieldChanged =
|
|
128
|
+
profileUpdates.name !== undefined
|
|
129
|
+
|| profileUpdates.description !== undefined
|
|
130
|
+
|| profileUpdates.imagePath !== undefined
|
|
131
|
+
if (!profileFieldChanged) return false
|
|
132
|
+
return !advancedCustodyEnsAvailable(identity)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function advancedCustodyEnsAvailable(identity: EthagentIdentity): boolean {
|
|
136
|
+
const baseState = (identity.state ?? {}) as Record<string, unknown>
|
|
137
|
+
if (readCustodyMode(baseState) !== 'advanced') return false
|
|
138
|
+
const ensName = typeof baseState.ensName === 'string' ? baseState.ensName.trim() : ''
|
|
139
|
+
if (!ensName) return false
|
|
140
|
+
const approved = normalizeApprovedOperatorWallets(baseState.approvedOperatorWallets)
|
|
141
|
+
return approved.length > 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function hasOperatorRestoreAccessKey(
|
|
145
|
+
identity: EthagentIdentity,
|
|
146
|
+
profileUpdates: ProfileUpdates | undefined,
|
|
147
|
+
): boolean {
|
|
148
|
+
return restoreAccessKeysForSave(identity, profileUpdates)
|
|
149
|
+
.some(key => key.address.toLowerCase() !== ownerAddressForSnapshotSave(identity, profileUpdates).toLowerCase())
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function restoreAccessKeysForSave(
|
|
153
|
+
identity: EthagentIdentity,
|
|
154
|
+
profileUpdates: ProfileUpdates | undefined,
|
|
155
|
+
): WalletContinuityRestoreAccessKey[] {
|
|
156
|
+
const baseState = (identity.state ?? {}) as Record<string, unknown>
|
|
157
|
+
const approved = profileUpdates?.approvedOperatorWallets !== undefined
|
|
158
|
+
? normalizeApprovedOperatorWallets(profileUpdates.approvedOperatorWallets)
|
|
159
|
+
: normalizeApprovedOperatorWallets(baseState.approvedOperatorWallets)
|
|
160
|
+
return [
|
|
161
|
+
readRestoreAccessKey(baseState.ownerRestoreAccessKey),
|
|
162
|
+
...approved.flatMap(record => record.restoreAccessKey ? [record.restoreAccessKey] : []),
|
|
163
|
+
].filter((key): key is WalletContinuityRestoreAccessKey => Boolean(key))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function resolveProfileUpdatesEpoch(
|
|
167
|
+
identity: EthagentIdentity,
|
|
168
|
+
profileUpdates: ProfileUpdates | undefined,
|
|
169
|
+
): ProfileUpdates | undefined {
|
|
170
|
+
if (!profileUpdates?.bumpRestoreAccessEpoch) return profileUpdates
|
|
171
|
+
if (profileUpdates.restoreAccessEpoch !== undefined) {
|
|
172
|
+
const { bumpRestoreAccessEpoch: _drop, ...rest } = profileUpdates
|
|
173
|
+
return rest
|
|
174
|
+
}
|
|
175
|
+
const baseState = (identity.state ?? {}) as Record<string, unknown>
|
|
176
|
+
const current = readStateSafeInteger(baseState.restoreAccessEpoch) ?? 0
|
|
177
|
+
const { bumpRestoreAccessEpoch: _drop, ...rest } = profileUpdates
|
|
178
|
+
return { ...rest, restoreAccessEpoch: current + 1 }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function walletRestoreAccessContext(
|
|
182
|
+
identity: EthagentIdentity,
|
|
183
|
+
registry: Erc8004RegistryConfig,
|
|
184
|
+
profileUpdates: ProfileUpdates | undefined,
|
|
185
|
+
_ownerAddress: string,
|
|
186
|
+
): WalletRestoreAccessContext | null {
|
|
187
|
+
if (!identity.agentId) return null
|
|
188
|
+
const baseState = (identity.state ?? {}) as Record<string, unknown>
|
|
189
|
+
return {
|
|
190
|
+
token: {
|
|
191
|
+
chainId: registry.chainId,
|
|
192
|
+
identityRegistryAddress: registry.identityRegistryAddress,
|
|
193
|
+
agentId: identity.agentId,
|
|
194
|
+
},
|
|
195
|
+
accessEpoch: profileUpdates?.restoreAccessEpoch
|
|
196
|
+
?? readStateSafeInteger(baseState.restoreAccessEpoch)
|
|
197
|
+
?? 1,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function createContinuityEnvelopeForSave(args: {
|
|
202
|
+
identity: EthagentIdentity
|
|
203
|
+
registry: Erc8004RegistryConfig
|
|
204
|
+
ownerAddress: Address
|
|
205
|
+
signerAddress: Address
|
|
206
|
+
walletSignature: string
|
|
207
|
+
state: Record<string, unknown>
|
|
208
|
+
files: ReturnType<typeof defaultContinuityFiles>
|
|
209
|
+
walletAccess: WalletRestoreAccessContext
|
|
210
|
+
challengePurpose?: WalletChallengePurpose
|
|
211
|
+
}): ContinuitySnapshotEnvelope {
|
|
212
|
+
const signerIsOwner = args.signerAddress.toLowerCase() === args.ownerAddress.toLowerCase()
|
|
213
|
+
const ownerRestoreAccessKey = signerIsOwner
|
|
214
|
+
? createWalletRestoreAccessKey({
|
|
215
|
+
token: args.walletAccess.token,
|
|
216
|
+
ownerAddress: args.ownerAddress,
|
|
217
|
+
walletAddress: args.ownerAddress,
|
|
218
|
+
walletSignature: args.walletSignature,
|
|
219
|
+
accessEpoch: args.walletAccess.accessEpoch,
|
|
220
|
+
createdAt: new Date().toISOString(),
|
|
221
|
+
...(args.challengePurpose ? { purpose: args.challengePurpose } : {}),
|
|
222
|
+
})
|
|
223
|
+
: readRestoreAccessKey(args.state.ownerRestoreAccessKey)
|
|
224
|
+
if (!ownerRestoreAccessKey) {
|
|
225
|
+
throw new Error('Restore Slot Missing: this agent has not been saved by its owner wallet yet. Switch to the owner wallet and save once to authorize operator wallet writes.')
|
|
226
|
+
}
|
|
227
|
+
if (signerIsOwner) args.state.ownerRestoreAccessKey = ownerRestoreAccessKey
|
|
228
|
+
args.state.restoreAccessEpoch = args.walletAccess.accessEpoch
|
|
229
|
+
let signingOperatorKey: WalletContinuityRestoreAccessKey | undefined
|
|
230
|
+
if (!signerIsOwner) {
|
|
231
|
+
signingOperatorKey = createWalletRestoreAccessKey({
|
|
232
|
+
token: args.walletAccess.token,
|
|
233
|
+
ownerAddress: args.ownerAddress,
|
|
234
|
+
walletAddress: args.signerAddress,
|
|
235
|
+
walletSignature: args.walletSignature,
|
|
236
|
+
accessEpoch: args.walletAccess.accessEpoch,
|
|
237
|
+
createdAt: new Date().toISOString(),
|
|
238
|
+
...(args.challengePurpose ? { purpose: args.challengePurpose } : {}),
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
const operatorKeys = normalizeApprovedOperatorWallets(args.state.approvedOperatorWallets)
|
|
242
|
+
.flatMap(record => record.restoreAccessKey ? [record.restoreAccessKey] : [])
|
|
243
|
+
.map(stored =>
|
|
244
|
+
signingOperatorKey && stored.address.toLowerCase() === args.signerAddress.toLowerCase()
|
|
245
|
+
? signingOperatorKey
|
|
246
|
+
: stored,
|
|
247
|
+
)
|
|
248
|
+
const accessKeys = uniqueRestoreAccessKeys([
|
|
249
|
+
ownerRestoreAccessKey,
|
|
250
|
+
...operatorKeys,
|
|
251
|
+
])
|
|
252
|
+
return createWalletContinuitySnapshotEnvelope({
|
|
253
|
+
ownerAddress: args.ownerAddress,
|
|
254
|
+
signerAddress: args.signerAddress,
|
|
255
|
+
signerWalletSignature: args.walletSignature,
|
|
256
|
+
token: args.walletAccess.token,
|
|
257
|
+
accessEpoch: args.walletAccess.accessEpoch,
|
|
258
|
+
accessKeys,
|
|
259
|
+
payload: {
|
|
260
|
+
agent: continuityAgentSnapshot(args.identity),
|
|
261
|
+
files: args.files,
|
|
262
|
+
transcript: [],
|
|
263
|
+
state: args.state,
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function uniqueRestoreAccessKeys(keys: WalletContinuityRestoreAccessKey[]): WalletContinuityRestoreAccessKey[] {
|
|
269
|
+
const out: WalletContinuityRestoreAccessKey[] = []
|
|
270
|
+
const seen = new Set<string>()
|
|
271
|
+
for (const key of keys) {
|
|
272
|
+
const address = getAddress(key.address)
|
|
273
|
+
const dedupe = address.toLowerCase()
|
|
274
|
+
if (seen.has(dedupe)) continue
|
|
275
|
+
seen.add(dedupe)
|
|
276
|
+
out.push({ ...key, address })
|
|
277
|
+
}
|
|
278
|
+
return out
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function readRestoreAccessKey(input: unknown): WalletContinuityRestoreAccessKey | undefined {
|
|
282
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return undefined
|
|
283
|
+
const obj = input as Partial<WalletContinuityRestoreAccessKey>
|
|
284
|
+
if (typeof obj.address !== 'string' || !isAddress(obj.address, { strict: false })) return undefined
|
|
285
|
+
if (typeof obj.challenge !== 'string' || typeof obj.salt !== 'string' || typeof obj.kemPublicKey !== 'string') return undefined
|
|
286
|
+
return {
|
|
287
|
+
address: getAddress(obj.address),
|
|
288
|
+
challenge: obj.challenge,
|
|
289
|
+
salt: obj.salt,
|
|
290
|
+
kemPublicKey: obj.kemPublicKey,
|
|
291
|
+
...(typeof obj.createdAt === 'string' ? { createdAt: obj.createdAt } : {}),
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function readStateSafeInteger(input: unknown): number | undefined {
|
|
296
|
+
return typeof input === 'number' && Number.isSafeInteger(input) && input > 0 ? input : undefined
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function ownerAddressForSnapshotSave(
|
|
300
|
+
identity: EthagentIdentity,
|
|
301
|
+
profileUpdates: ProfileUpdates | undefined,
|
|
302
|
+
): Address {
|
|
303
|
+
const baseState = (identity.state ?? {}) as Record<string, unknown>
|
|
304
|
+
const profileOwnerRaw = typeof profileUpdates?.ownerAddress === 'string' && profileUpdates.ownerAddress.trim()
|
|
305
|
+
? profileUpdates.ownerAddress.trim()
|
|
306
|
+
: undefined
|
|
307
|
+
const stateOwnerRaw = readOwnerAddressField(baseState)
|
|
308
|
+
const owner = profileOwnerRaw ?? stateOwnerRaw
|
|
309
|
+
if (owner && /^0x[a-fA-F0-9]{40}$/.test(owner)) return getAddress(owner)
|
|
310
|
+
return getAddress(identity.ownerAddress ?? identity.address)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function operatorsPointerFromState(
|
|
314
|
+
state: Record<string, unknown>,
|
|
315
|
+
ensName: string | undefined,
|
|
316
|
+
): EthagentOperatorsPointer | undefined {
|
|
317
|
+
const approvedOperatorWallets = normalizeApprovedOperatorWallets(state.approvedOperatorWallets)
|
|
318
|
+
const activeRaw = typeof state.activeOperatorAddress === 'string' ? state.activeOperatorAddress : undefined
|
|
319
|
+
const activeOperatorAddress = activeRaw
|
|
320
|
+
? assertActiveOperatorIsApproved(approvedOperatorWallets, activeRaw)
|
|
321
|
+
: undefined
|
|
322
|
+
const ownerRaw = readOwnerAddressField(state)
|
|
323
|
+
const ownerAddress = ownerRaw ? getAddress(ownerRaw) : undefined
|
|
324
|
+
const pointerEnsName = ensName ?? (typeof state.ensName === 'string' && state.ensName.trim() ? state.ensName.trim() : undefined)
|
|
325
|
+
const ownerRestoreAccessKey = readRestoreAccessKey(state.ownerRestoreAccessKey)
|
|
326
|
+
const restoreAccessEpoch = readStateSafeInteger(state.restoreAccessEpoch)
|
|
327
|
+
if (approvedOperatorWallets.length === 0 && !activeOperatorAddress && !ownerAddress && !ownerRestoreAccessKey && !restoreAccessEpoch) return undefined
|
|
328
|
+
return {
|
|
329
|
+
approvedOperatorWallets,
|
|
330
|
+
...(activeOperatorAddress ? { activeOperatorAddress } : {}),
|
|
331
|
+
...(ownerAddress ? { ownerAddress } : {}),
|
|
332
|
+
...(pointerEnsName ? { ensName: pointerEnsName } : {}),
|
|
333
|
+
...(restoreAccessEpoch ? { restoreAccessEpoch } : {}),
|
|
334
|
+
...(ownerRestoreAccessKey ? { ownerRestoreAccessKey } : {}),
|
|
335
|
+
}
|
|
336
|
+
}
|