ethagent 1.1.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +124 -32
- package/package.json +8 -3
- 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
|
@@ -1,58 +1,15 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
2
|
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js'
|
|
3
|
-
import {
|
|
3
|
+
import { recoverAddressFromSignature, toChecksumAddress } from './eth.js'
|
|
4
4
|
|
|
5
|
-
export const BACKUP_ENVELOPE_VERSION = 'ethagent-pq-backup-v1'
|
|
6
5
|
export const AGENT_STATE_BACKUP_ENVELOPE_VERSION = 'ethagent-state-backup-v1'
|
|
7
6
|
|
|
8
|
-
type
|
|
9
|
-
privateKey: string
|
|
10
|
-
address: string
|
|
11
|
-
createdAt: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type AgentStatePayload = {
|
|
7
|
+
type AgentStatePayload = {
|
|
15
8
|
ownerAddress: string
|
|
16
9
|
createdAt: string
|
|
17
10
|
state: Record<string, unknown>
|
|
18
11
|
}
|
|
19
12
|
|
|
20
|
-
export type IdentityBackupEnvelope = {
|
|
21
|
-
version: 1
|
|
22
|
-
envelopeVersion: typeof BACKUP_ENVELOPE_VERSION
|
|
23
|
-
address: string
|
|
24
|
-
ownerAddress?: string
|
|
25
|
-
createdAt: string
|
|
26
|
-
challenge: string
|
|
27
|
-
walletSignature: string
|
|
28
|
-
crypto: {
|
|
29
|
-
kem: 'ML-KEM-768'
|
|
30
|
-
aead: 'AES-256-GCM'
|
|
31
|
-
kdf: 'HKDF-SHA256'
|
|
32
|
-
signature: 'EIP-191'
|
|
33
|
-
}
|
|
34
|
-
salt: string
|
|
35
|
-
kemPublicKey: string
|
|
36
|
-
kemCiphertext: string
|
|
37
|
-
nonce: string
|
|
38
|
-
ciphertext: string
|
|
39
|
-
tag: string
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export type CreateIdentityBackupArgs = {
|
|
43
|
-
privateKey: string
|
|
44
|
-
recoveryPassphrase: string
|
|
45
|
-
walletSignature: string
|
|
46
|
-
createdAt?: string
|
|
47
|
-
ownerAddress?: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type RestoreIdentityBackupArgs = {
|
|
51
|
-
envelope: IdentityBackupEnvelope
|
|
52
|
-
recoveryPassphrase: string
|
|
53
|
-
walletSignature: string
|
|
54
|
-
}
|
|
55
|
-
|
|
56
13
|
export type AgentStateBackupEnvelope = {
|
|
57
14
|
version: 1
|
|
58
15
|
envelopeVersion: typeof AGENT_STATE_BACKUP_ENVELOPE_VERSION
|
|
@@ -73,14 +30,14 @@ export type AgentStateBackupEnvelope = {
|
|
|
73
30
|
tag: string
|
|
74
31
|
}
|
|
75
32
|
|
|
76
|
-
|
|
33
|
+
type CreateAgentStateBackupArgs = {
|
|
77
34
|
ownerAddress: string
|
|
78
35
|
walletSignature: string
|
|
79
36
|
state: Record<string, unknown>
|
|
80
37
|
createdAt?: string
|
|
81
38
|
}
|
|
82
39
|
|
|
83
|
-
|
|
40
|
+
type RestoreAgentStateBackupArgs = {
|
|
84
41
|
envelope: AgentStateBackupEnvelope
|
|
85
42
|
walletSignature: string
|
|
86
43
|
}
|
|
@@ -90,121 +47,21 @@ export class AgentStateOwnerMismatchError extends Error {
|
|
|
90
47
|
readonly backupOwner: string,
|
|
91
48
|
readonly currentOwner: string,
|
|
92
49
|
) {
|
|
93
|
-
super('
|
|
50
|
+
super('Agent backup is encrypted for another wallet')
|
|
94
51
|
this.name = 'AgentStateOwnerMismatchError'
|
|
95
52
|
}
|
|
96
53
|
}
|
|
97
54
|
|
|
98
|
-
export function createRecoveryChallenge(address: string): string {
|
|
99
|
-
const checksum = toChecksumAddress(address)
|
|
100
|
-
return [
|
|
101
|
-
'ethagent identity recovery v1',
|
|
102
|
-
`address: ${checksum}`,
|
|
103
|
-
'purpose: authorize encrypted portable agent backup',
|
|
104
|
-
].join('\n')
|
|
105
|
-
}
|
|
106
|
-
|
|
107
55
|
export function createAgentStateRecoveryChallenge(ownerAddress: string): string {
|
|
108
56
|
const checksum = toChecksumAddress(ownerAddress)
|
|
109
57
|
return [
|
|
110
|
-
'
|
|
58
|
+
'Encrypted State Access',
|
|
111
59
|
`Owner: ${checksum}`,
|
|
112
60
|
'Action: authorize this wallet to unlock the encrypted agent backup',
|
|
113
61
|
'Version: 1',
|
|
114
62
|
].join('\n')
|
|
115
63
|
}
|
|
116
64
|
|
|
117
|
-
export function createIdentityBackupEnvelope(args: CreateIdentityBackupArgs): IdentityBackupEnvelope {
|
|
118
|
-
if (!validatePrivateKey(args.privateKey)) throw new Error('invalid private key')
|
|
119
|
-
if (args.recoveryPassphrase.length < 8) throw new Error('recovery passphrase must be at least 8 characters')
|
|
120
|
-
|
|
121
|
-
const address = addressFromPrivateKey(args.privateKey)
|
|
122
|
-
const ownerAddress = args.ownerAddress ? toChecksumAddress(args.ownerAddress) : undefined
|
|
123
|
-
const signingAddress = ownerAddress ?? address
|
|
124
|
-
const challenge = createRecoveryChallenge(signingAddress)
|
|
125
|
-
assertSignatureForAddress(challenge, args.walletSignature, signingAddress)
|
|
126
|
-
|
|
127
|
-
const createdAt = args.createdAt ?? new Date().toISOString()
|
|
128
|
-
const salt = crypto.randomBytes(32)
|
|
129
|
-
const kemSeed = deriveKemSeed(args.recoveryPassphrase, args.walletSignature, salt, address)
|
|
130
|
-
const kemKeys = ml_kem768.keygen(kemSeed)
|
|
131
|
-
const kem = ml_kem768.encapsulate(kemKeys.publicKey)
|
|
132
|
-
const key = deriveAesKey(args.recoveryPassphrase, args.walletSignature, kem.sharedSecret, salt, address)
|
|
133
|
-
const nonce = crypto.randomBytes(12)
|
|
134
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce)
|
|
135
|
-
cipher.setAAD(aadFor(address, createdAt))
|
|
136
|
-
const plaintext = Buffer.from(JSON.stringify({
|
|
137
|
-
privateKey: normalizedPrivateKey(args.privateKey),
|
|
138
|
-
address,
|
|
139
|
-
createdAt,
|
|
140
|
-
} satisfies BackupPayload), 'utf8')
|
|
141
|
-
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()])
|
|
142
|
-
const tag = cipher.getAuthTag()
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
version: 1,
|
|
146
|
-
envelopeVersion: BACKUP_ENVELOPE_VERSION,
|
|
147
|
-
address,
|
|
148
|
-
...(ownerAddress ? { ownerAddress } : {}),
|
|
149
|
-
createdAt,
|
|
150
|
-
challenge,
|
|
151
|
-
walletSignature: args.walletSignature,
|
|
152
|
-
crypto: {
|
|
153
|
-
kem: 'ML-KEM-768',
|
|
154
|
-
aead: 'AES-256-GCM',
|
|
155
|
-
kdf: 'HKDF-SHA256',
|
|
156
|
-
signature: 'EIP-191',
|
|
157
|
-
},
|
|
158
|
-
salt: toBase64(salt),
|
|
159
|
-
kemPublicKey: toBase64(kemKeys.publicKey),
|
|
160
|
-
kemCiphertext: toBase64(kem.cipherText),
|
|
161
|
-
nonce: toBase64(nonce),
|
|
162
|
-
ciphertext: toBase64(encrypted),
|
|
163
|
-
tag: toBase64(tag),
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function restoreIdentityBackupEnvelope(args: RestoreIdentityBackupArgs): BackupPayload {
|
|
168
|
-
const envelope = normalizeEnvelope(args.envelope)
|
|
169
|
-
const signingAddress = envelope.ownerAddress ?? envelope.address
|
|
170
|
-
assertSignatureForAddress(envelope.challenge, args.walletSignature, signingAddress)
|
|
171
|
-
assertSignatureForAddress(envelope.challenge, envelope.walletSignature, signingAddress)
|
|
172
|
-
|
|
173
|
-
const salt = fromBase64(envelope.salt)
|
|
174
|
-
const kemSeed = deriveKemSeed(args.recoveryPassphrase, envelope.walletSignature, salt, envelope.address)
|
|
175
|
-
const kemKeys = ml_kem768.keygen(kemSeed)
|
|
176
|
-
const expectedPublicKey = toBase64(kemKeys.publicKey)
|
|
177
|
-
if (expectedPublicKey !== envelope.kemPublicKey) {
|
|
178
|
-
throw new Error('recovery credentials do not match this backup')
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const sharedSecret = ml_kem768.decapsulate(fromBase64(envelope.kemCiphertext), kemKeys.secretKey)
|
|
182
|
-
const key = deriveAesKey(args.recoveryPassphrase, envelope.walletSignature, sharedSecret, salt, envelope.address)
|
|
183
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', key, fromBase64(envelope.nonce))
|
|
184
|
-
decipher.setAAD(aadFor(envelope.address, envelope.createdAt))
|
|
185
|
-
decipher.setAuthTag(fromBase64(envelope.tag))
|
|
186
|
-
|
|
187
|
-
let decoded: unknown
|
|
188
|
-
try {
|
|
189
|
-
const plaintext = Buffer.concat([
|
|
190
|
-
decipher.update(fromBase64(envelope.ciphertext)),
|
|
191
|
-
decipher.final(),
|
|
192
|
-
]).toString('utf8')
|
|
193
|
-
decoded = JSON.parse(plaintext)
|
|
194
|
-
} catch {
|
|
195
|
-
throw new Error('could not decrypt backup with the supplied credentials')
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (!isBackupPayload(decoded)) throw new Error('backup payload is invalid')
|
|
199
|
-
if (decoded.address.toLowerCase() !== envelope.address.toLowerCase()) {
|
|
200
|
-
throw new Error('backup payload address mismatch')
|
|
201
|
-
}
|
|
202
|
-
if (addressFromPrivateKey(decoded.privateKey).toLowerCase() !== envelope.address.toLowerCase()) {
|
|
203
|
-
throw new Error('backup private key does not match address')
|
|
204
|
-
}
|
|
205
|
-
return decoded
|
|
206
|
-
}
|
|
207
|
-
|
|
208
65
|
export function createAgentStateBackupEnvelope(args: CreateAgentStateBackupArgs): AgentStateBackupEnvelope {
|
|
209
66
|
const ownerAddress = toChecksumAddress(args.ownerAddress)
|
|
210
67
|
const challenge = createAgentStateRecoveryChallenge(ownerAddress)
|
|
@@ -257,7 +114,7 @@ export function restoreAgentStateBackupEnvelope(args: RestoreAgentStateBackupArg
|
|
|
257
114
|
const kemKeys = ml_kem768.keygen(kemSeed)
|
|
258
115
|
const expectedPublicKey = toBase64(kemKeys.publicKey)
|
|
259
116
|
if (expectedPublicKey !== envelope.kemPublicKey) {
|
|
260
|
-
throw new Error('
|
|
117
|
+
throw new Error('Wallet signature does not match this agent backup')
|
|
261
118
|
}
|
|
262
119
|
|
|
263
120
|
const sharedSecret = ml_kem768.decapsulate(fromBase64(envelope.kemCiphertext), kemKeys.secretKey)
|
|
@@ -274,12 +131,12 @@ export function restoreAgentStateBackupEnvelope(args: RestoreAgentStateBackupArg
|
|
|
274
131
|
]).toString('utf8')
|
|
275
132
|
decoded = JSON.parse(plaintext)
|
|
276
133
|
} catch {
|
|
277
|
-
throw new Error('
|
|
134
|
+
throw new Error('Could not decrypt agent state with the supplied wallet signature')
|
|
278
135
|
}
|
|
279
136
|
|
|
280
|
-
if (!isAgentStatePayload(decoded)) throw new Error('
|
|
137
|
+
if (!isAgentStatePayload(decoded)) throw new Error('Agent state backup payload is invalid')
|
|
281
138
|
if (decoded.ownerAddress.toLowerCase() !== envelope.ownerAddress.toLowerCase()) {
|
|
282
|
-
throw new Error('
|
|
139
|
+
throw new Error('Agent state backup owner mismatch')
|
|
283
140
|
}
|
|
284
141
|
return {
|
|
285
142
|
...decoded,
|
|
@@ -295,16 +152,6 @@ export function assertAgentStateBackupOwner(envelope: AgentStateBackupEnvelope,
|
|
|
295
152
|
}
|
|
296
153
|
}
|
|
297
154
|
|
|
298
|
-
export function serializeIdentityBackupEnvelope(envelope: IdentityBackupEnvelope): string {
|
|
299
|
-
return JSON.stringify(normalizeEnvelope(envelope), null, 2)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export function parseIdentityBackupEnvelope(raw: string | Uint8Array): IdentityBackupEnvelope {
|
|
303
|
-
const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw)
|
|
304
|
-
const parsed = JSON.parse(text) as unknown
|
|
305
|
-
return normalizeEnvelope(parsed)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
155
|
export function serializeAgentStateBackupEnvelope(envelope: AgentStateBackupEnvelope): string {
|
|
309
156
|
return JSON.stringify(normalizeAgentStateEnvelope(envelope), null, 2)
|
|
310
157
|
}
|
|
@@ -315,43 +162,11 @@ export function parseAgentStateBackupEnvelope(raw: string | Uint8Array): AgentSt
|
|
|
315
162
|
return normalizeAgentStateEnvelope(parsed)
|
|
316
163
|
}
|
|
317
164
|
|
|
318
|
-
function normalizeEnvelope(input: unknown): IdentityBackupEnvelope {
|
|
319
|
-
if (!isEnvelope(input)) throw new Error('invalid identity backup envelope')
|
|
320
|
-
if (input.envelopeVersion !== BACKUP_ENVELOPE_VERSION) throw new Error('unsupported backup envelope version')
|
|
321
|
-
if (input.crypto.kem !== 'ML-KEM-768' || input.crypto.aead !== 'AES-256-GCM') {
|
|
322
|
-
throw new Error('unsupported backup crypto suite')
|
|
323
|
-
}
|
|
324
|
-
return {
|
|
325
|
-
...input,
|
|
326
|
-
address: toChecksumAddress(input.address),
|
|
327
|
-
...(typeof input.ownerAddress === 'string' ? { ownerAddress: toChecksumAddress(input.ownerAddress) } : {}),
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function isEnvelope(input: unknown): input is IdentityBackupEnvelope {
|
|
332
|
-
if (!input || typeof input !== 'object') return false
|
|
333
|
-
const obj = input as Partial<IdentityBackupEnvelope>
|
|
334
|
-
return obj.version === 1
|
|
335
|
-
&& obj.envelopeVersion === BACKUP_ENVELOPE_VERSION
|
|
336
|
-
&& typeof obj.address === 'string'
|
|
337
|
-
&& (obj.ownerAddress === undefined || typeof obj.ownerAddress === 'string')
|
|
338
|
-
&& typeof obj.createdAt === 'string'
|
|
339
|
-
&& typeof obj.challenge === 'string'
|
|
340
|
-
&& typeof obj.walletSignature === 'string'
|
|
341
|
-
&& typeof obj.salt === 'string'
|
|
342
|
-
&& typeof obj.kemPublicKey === 'string'
|
|
343
|
-
&& typeof obj.kemCiphertext === 'string'
|
|
344
|
-
&& typeof obj.nonce === 'string'
|
|
345
|
-
&& typeof obj.ciphertext === 'string'
|
|
346
|
-
&& typeof obj.tag === 'string'
|
|
347
|
-
&& !!obj.crypto
|
|
348
|
-
}
|
|
349
|
-
|
|
350
165
|
function normalizeAgentStateEnvelope(input: unknown): AgentStateBackupEnvelope {
|
|
351
|
-
if (!isAgentStateEnvelope(input)) throw new Error('
|
|
352
|
-
if (input.envelopeVersion !== AGENT_STATE_BACKUP_ENVELOPE_VERSION) throw new Error('
|
|
166
|
+
if (!isAgentStateEnvelope(input)) throw new Error('Invalid agent state backup envelope')
|
|
167
|
+
if (input.envelopeVersion !== AGENT_STATE_BACKUP_ENVELOPE_VERSION) throw new Error('Unsupported agent state backup envelope version')
|
|
353
168
|
if (input.crypto.kem !== 'ML-KEM-768' || input.crypto.aead !== 'AES-256-GCM') {
|
|
354
|
-
throw new Error('
|
|
169
|
+
throw new Error('Unsupported backup crypto suite')
|
|
355
170
|
}
|
|
356
171
|
return {
|
|
357
172
|
...input,
|
|
@@ -377,15 +192,6 @@ function isAgentStateEnvelope(input: unknown): input is AgentStateBackupEnvelope
|
|
|
377
192
|
&& !!obj.crypto
|
|
378
193
|
}
|
|
379
194
|
|
|
380
|
-
function isBackupPayload(input: unknown): input is BackupPayload {
|
|
381
|
-
if (!input || typeof input !== 'object') return false
|
|
382
|
-
const obj = input as Partial<BackupPayload>
|
|
383
|
-
return typeof obj.privateKey === 'string'
|
|
384
|
-
&& validatePrivateKey(obj.privateKey)
|
|
385
|
-
&& typeof obj.address === 'string'
|
|
386
|
-
&& typeof obj.createdAt === 'string'
|
|
387
|
-
}
|
|
388
|
-
|
|
389
195
|
function isAgentStatePayload(input: unknown): input is AgentStatePayload {
|
|
390
196
|
if (!input || typeof input !== 'object') return false
|
|
391
197
|
const obj = input as Partial<AgentStatePayload>
|
|
@@ -399,40 +205,10 @@ function isAgentStatePayload(input: unknown): input is AgentStatePayload {
|
|
|
399
205
|
function assertSignatureForAddress(challenge: string, signature: string, address: string): void {
|
|
400
206
|
const recovered = recoverAddressFromSignature(challenge, signature)
|
|
401
207
|
if (recovered.toLowerCase() !== address.toLowerCase()) {
|
|
402
|
-
throw new Error('
|
|
208
|
+
throw new Error('Wallet signature does not match backup address')
|
|
403
209
|
}
|
|
404
210
|
}
|
|
405
211
|
|
|
406
|
-
function deriveKemSeed(passphrase: string, walletSignature: string, salt: Uint8Array, address: string): Uint8Array {
|
|
407
|
-
return hkdf(
|
|
408
|
-
Buffer.from(`${walletSignature}\n${passphrase}`, 'utf8'),
|
|
409
|
-
salt,
|
|
410
|
-
`ethagent:${BACKUP_ENVELOPE_VERSION}:ml-kem768:${address.toLowerCase()}`,
|
|
411
|
-
64,
|
|
412
|
-
)
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function deriveAesKey(
|
|
416
|
-
passphrase: string,
|
|
417
|
-
walletSignature: string,
|
|
418
|
-
sharedSecret: Uint8Array,
|
|
419
|
-
salt: Uint8Array,
|
|
420
|
-
address: string,
|
|
421
|
-
): Buffer {
|
|
422
|
-
return Buffer.from(hkdf(
|
|
423
|
-
Buffer.concat([
|
|
424
|
-
Buffer.from(walletSignature, 'utf8'),
|
|
425
|
-
Buffer.from('\n', 'utf8'),
|
|
426
|
-
Buffer.from(passphrase, 'utf8'),
|
|
427
|
-
Buffer.from('\n', 'utf8'),
|
|
428
|
-
Buffer.from(sharedSecret),
|
|
429
|
-
]),
|
|
430
|
-
salt,
|
|
431
|
-
`ethagent:${BACKUP_ENVELOPE_VERSION}:aes-256-gcm:${address.toLowerCase()}`,
|
|
432
|
-
32,
|
|
433
|
-
))
|
|
434
|
-
}
|
|
435
|
-
|
|
436
212
|
function deriveStateKemSeed(walletSignature: string, salt: Uint8Array, ownerAddress: string): Uint8Array {
|
|
437
213
|
return hkdf(
|
|
438
214
|
Buffer.from(walletSignature, 'utf8'),
|
|
@@ -464,19 +240,10 @@ function hkdf(ikm: Uint8Array, salt: Uint8Array, info: string, length: number):
|
|
|
464
240
|
return new Uint8Array(crypto.hkdfSync('sha256', ikm, salt, Buffer.from(info, 'utf8'), length))
|
|
465
241
|
}
|
|
466
242
|
|
|
467
|
-
function aadFor(address: string, createdAt: string): Buffer {
|
|
468
|
-
return Buffer.from(`${BACKUP_ENVELOPE_VERSION}\n${address.toLowerCase()}\n${createdAt}`, 'utf8')
|
|
469
|
-
}
|
|
470
|
-
|
|
471
243
|
function stateAadFor(ownerAddress: string, createdAt: string): Buffer {
|
|
472
244
|
return Buffer.from(`${AGENT_STATE_BACKUP_ENVELOPE_VERSION}\n${ownerAddress.toLowerCase()}\n${createdAt}`, 'utf8')
|
|
473
245
|
}
|
|
474
246
|
|
|
475
|
-
function normalizedPrivateKey(privateKey: string): string {
|
|
476
|
-
const trimmed = privateKey.trim()
|
|
477
|
-
return trimmed.startsWith('0x') || trimmed.startsWith('0X') ? `0x${trimmed.slice(2)}` : `0x${trimmed}`
|
|
478
|
-
}
|
|
479
|
-
|
|
480
247
|
function toBase64(bytes: Uint8Array): string {
|
|
481
248
|
return Buffer.from(bytes).toString('base64')
|
|
482
249
|
}
|
|
@@ -16,11 +16,11 @@ function stripHex(input: string): string {
|
|
|
16
16
|
|
|
17
17
|
function hexToBytes(hex: string): Uint8Array {
|
|
18
18
|
const stripped = stripHex(hex)
|
|
19
|
-
if (stripped.length % 2 !== 0) throw new Error('
|
|
19
|
+
if (stripped.length % 2 !== 0) throw new Error('Hex string has odd length')
|
|
20
20
|
const out = new Uint8Array(stripped.length / 2)
|
|
21
21
|
for (let i = 0; i < out.length; i += 1) {
|
|
22
22
|
const byte = Number.parseInt(stripped.slice(i * 2, i * 2 + 2), 16)
|
|
23
|
-
if (Number.isNaN(byte)) throw new Error('
|
|
23
|
+
if (Number.isNaN(byte)) throw new Error('Invalid hex')
|
|
24
24
|
out[i] = byte
|
|
25
25
|
}
|
|
26
26
|
return out
|
|
@@ -82,14 +82,14 @@ export function validatePrivateKey(input: string): boolean {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
export function addressFromPrivateKey(input: string): string {
|
|
85
|
-
if (!validatePrivateKey(input)) throw new Error('
|
|
85
|
+
if (!validatePrivateKey(input)) throw new Error('Invalid private key')
|
|
86
86
|
const bytes = hexToBytes(stripHex(input.trim()))
|
|
87
87
|
const pub = secp256k1.getPublicKey(bytes, false)
|
|
88
88
|
return addressFromPublicKey(pub)
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
export function toChecksumAddress(address: string): string {
|
|
92
|
-
if (!ADDR_RE.test(address)) throw new Error('
|
|
92
|
+
if (!ADDR_RE.test(address)) throw new Error('Invalid address')
|
|
93
93
|
const lower = address.slice(2).toLowerCase()
|
|
94
94
|
const hashHex = bytesToHex(keccak_256(new TextEncoder().encode(lower)))
|
|
95
95
|
let out = '0x'
|
|
@@ -105,7 +105,7 @@ export function toChecksumAddress(address: string): string {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
export function signMessage(privateKey: string, message: string | Uint8Array): string {
|
|
108
|
-
if (!validatePrivateKey(privateKey)) throw new Error('
|
|
108
|
+
if (!validatePrivateKey(privateKey)) throw new Error('Invalid private key')
|
|
109
109
|
const sk = hexToBytes(stripHex(privateKey.trim()))
|
|
110
110
|
const digest = ethereumMessageDigest(message)
|
|
111
111
|
const sig = secp256k1.sign(digest, sk, { prehash: false })
|
|
@@ -121,11 +121,11 @@ export function signMessage(privateKey: string, message: string | Uint8Array): s
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
export function recoverAddressFromSignature(message: string | Uint8Array, signature: string): string {
|
|
124
|
-
if (!SIG_RE.test(signature)) throw new Error('
|
|
124
|
+
if (!SIG_RE.test(signature)) throw new Error('Invalid signature')
|
|
125
125
|
const bytes = hexToBytes(stripHex(signature))
|
|
126
126
|
const v = bytes[64]!
|
|
127
127
|
const recovery = v >= 27 ? v - 27 : v
|
|
128
|
-
if (recovery < 0 || recovery > 3) throw new Error('
|
|
128
|
+
if (recovery < 0 || recovery > 3) throw new Error('Invalid recovery id')
|
|
129
129
|
const recovered = new Uint8Array(65)
|
|
130
130
|
recovered[0] = recovery
|
|
131
131
|
recovered.set(bytes.subarray(0, 64), 1)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export const AGENT_RECORD_KEYS = {
|
|
2
|
+
token: 'org.ethagent.token',
|
|
3
|
+
profile: 'org.ethagent.profile',
|
|
4
|
+
} as const
|
|
5
|
+
|
|
6
|
+
type AgentRecordKey = typeof AGENT_RECORD_KEYS[keyof typeof AGENT_RECORD_KEYS]
|
|
7
|
+
|
|
8
|
+
export const AGENT_RECORD_KEY_LIST: readonly AgentRecordKey[] = [
|
|
9
|
+
AGENT_RECORD_KEYS.token,
|
|
10
|
+
AGENT_RECORD_KEYS.profile,
|
|
11
|
+
] as const
|
|
12
|
+
|
|
13
|
+
export const AGENT_RECORD_READ_KEY_LIST: readonly string[] = AGENT_RECORD_KEY_LIST
|
|
14
|
+
|
|
15
|
+
export type AgentEnsRecords = {
|
|
16
|
+
token?: string
|
|
17
|
+
profile?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type AgentEnsRecordState = AgentEnsRecords
|
|
21
|
+
|
|
22
|
+
export type AgentRecordDiff = {
|
|
23
|
+
key: AgentRecordKey
|
|
24
|
+
field: keyof AgentEnsRecordState
|
|
25
|
+
current: string
|
|
26
|
+
next: string
|
|
27
|
+
changed: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const FIELD_FOR_KEY: Record<AgentRecordKey, keyof AgentEnsRecords> = {
|
|
31
|
+
[AGENT_RECORD_KEYS.token]: 'token',
|
|
32
|
+
[AGENT_RECORD_KEYS.profile]: 'profile',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const LABEL_FOR_FIELD: Record<keyof AgentEnsRecordState, string> = {
|
|
36
|
+
token: 'Agent token',
|
|
37
|
+
profile: 'Agent profile',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function recordsFromTextMap(text: Record<string, string>): AgentEnsRecordState {
|
|
41
|
+
return {
|
|
42
|
+
token: text[AGENT_RECORD_KEYS.token] ?? '',
|
|
43
|
+
profile: text[AGENT_RECORD_KEYS.profile] ?? '',
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function diffRecords(current: AgentEnsRecordState, next: AgentEnsRecords): AgentRecordDiff[] {
|
|
48
|
+
return AGENT_RECORD_KEY_LIST.map(key => {
|
|
49
|
+
const field = FIELD_FOR_KEY[key]
|
|
50
|
+
const currentValue = (current[field] ?? '').trim()
|
|
51
|
+
const nextValue = (next[field] ?? '').trim()
|
|
52
|
+
return {
|
|
53
|
+
key,
|
|
54
|
+
field,
|
|
55
|
+
current: currentValue,
|
|
56
|
+
next: nextValue,
|
|
57
|
+
changed: currentValue !== nextValue,
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function changedRecords(current: AgentEnsRecordState, next: AgentEnsRecords): Record<string, string> {
|
|
63
|
+
const out: Record<string, string> = {}
|
|
64
|
+
for (const diff of diffRecords(current, next)) {
|
|
65
|
+
if (diff.changed) out[diff.key] = diff.next
|
|
66
|
+
}
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function recordLabel(field: keyof AgentEnsRecordState): string {
|
|
71
|
+
return LABEL_FOR_FIELD[field]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatRecordValue(field: keyof AgentEnsRecordState, value: string): string {
|
|
75
|
+
if (field === 'profile' && value.startsWith('ipfs://')) {
|
|
76
|
+
const cid = value.slice('ipfs://'.length)
|
|
77
|
+
return cid.length > 18 ? `ipfs://${cid.slice(0, 10)}...${cid.slice(-6)}` : value
|
|
78
|
+
}
|
|
79
|
+
return value
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function buildAgentEnsRecords(args: {
|
|
83
|
+
chainId: number
|
|
84
|
+
identityRegistryAddress: string
|
|
85
|
+
agentId: string | undefined
|
|
86
|
+
agentCardCid: string | undefined
|
|
87
|
+
}): AgentEnsRecords {
|
|
88
|
+
const records: AgentEnsRecords = {}
|
|
89
|
+
if (args.agentId) {
|
|
90
|
+
records.token = `eip155:${args.chainId}:${args.identityRegistryAddress.toLowerCase()}:${args.agentId}`
|
|
91
|
+
}
|
|
92
|
+
if (args.agentCardCid) {
|
|
93
|
+
records.profile = `ipfs://${args.agentCardCid}`
|
|
94
|
+
}
|
|
95
|
+
return records
|
|
96
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { parseAbi, type Address } from 'viem'
|
|
2
|
+
|
|
3
|
+
export const ENS_REGISTRY_ADDRESS_MAINNET = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e' as Address
|
|
4
|
+
export const ENS_NAME_WRAPPER_ADDRESS_MAINNET = '0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401' as Address
|
|
5
|
+
export const ENS_PUBLIC_RESOLVER_ADDRESS_MAINNET = '0xF29100983E058B709F3D539b0c765937B804AC15' as Address
|
|
6
|
+
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as Address
|
|
7
|
+
export const DEFAULT_TTL = 0n
|
|
8
|
+
export const DEFAULT_FUSES = 0
|
|
9
|
+
export const DEFAULT_EXPIRY = 0n
|
|
10
|
+
|
|
11
|
+
export const ENS_RPC_URLS = [
|
|
12
|
+
'https://ethereum.publicnode.com',
|
|
13
|
+
'https://eth.llamarpc.com',
|
|
14
|
+
'https://rpc.ankr.com/eth',
|
|
15
|
+
] as const
|
|
16
|
+
|
|
17
|
+
export const ENS_AUTOMATION_REGISTRY_ABI = parseAbi([
|
|
18
|
+
'function owner(bytes32 node) view returns (address)',
|
|
19
|
+
'function resolver(bytes32 node) view returns (address)',
|
|
20
|
+
'function setResolver(bytes32 node, address resolver)',
|
|
21
|
+
'function setSubnodeRecord(bytes32 node, bytes32 label, address owner, address resolver, uint64 ttl)',
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
export const ENS_AUTOMATION_RESOLVER_ABI = parseAbi([
|
|
25
|
+
'function addr(bytes32 node) view returns (address)',
|
|
26
|
+
'function text(bytes32 node, string key) view returns (string)',
|
|
27
|
+
'function setAddr(bytes32 node, address addr)',
|
|
28
|
+
'function setText(bytes32 node, string key, string value)',
|
|
29
|
+
'function multicall(bytes[] data) returns (bytes[])',
|
|
30
|
+
'function approve(bytes32 node, address delegate, bool approved)',
|
|
31
|
+
'function isApprovedFor(address owner, bytes32 node, address delegate) view returns (bool)',
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
export const ENS_AUTOMATION_NAME_WRAPPER_ABI = parseAbi([
|
|
35
|
+
'function ownerOf(uint256 id) view returns (address)',
|
|
36
|
+
'function setResolver(bytes32 node, address resolver)',
|
|
37
|
+
'function setSubnodeRecord(bytes32 parentNode, string label, address owner, address resolver, uint64 ttl, uint32 fuses, uint64 expiry)',
|
|
38
|
+
])
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getAddress, namehash, type Address } from 'viem'
|
|
2
|
+
import { isEthDomain, normalizeEthDomain, splitSubdomainName } from '../ensLookup.js'
|
|
3
|
+
import { ENS_NAME_WRAPPER_ADDRESS_MAINNET } from './contracts.js'
|
|
4
|
+
import {
|
|
5
|
+
createEnsAutomationClient,
|
|
6
|
+
isZero,
|
|
7
|
+
readOwner,
|
|
8
|
+
readWrappedOwner,
|
|
9
|
+
sameAddress,
|
|
10
|
+
shortHex,
|
|
11
|
+
} from './read.js'
|
|
12
|
+
import {
|
|
13
|
+
encodeDeleteSubnodeRegistry,
|
|
14
|
+
encodeDeleteSubnodeWrapped,
|
|
15
|
+
} from './transactions.js'
|
|
16
|
+
import type {
|
|
17
|
+
EnsSubdomainDeletePreflightArgs,
|
|
18
|
+
EnsSubdomainDeletePreflightResult,
|
|
19
|
+
} from './types.js'
|
|
20
|
+
|
|
21
|
+
export async function preflightDeleteSubdomain(args: EnsSubdomainDeletePreflightArgs): Promise<EnsSubdomainDeletePreflightResult> {
|
|
22
|
+
const fullName = normalizeEthDomain(args.fullName)
|
|
23
|
+
if (!isEthDomain(fullName)) {
|
|
24
|
+
return { ok: false, reason: 'invalid-name', detail: `${args.fullName} is not a valid .eth name` }
|
|
25
|
+
}
|
|
26
|
+
const parts = splitSubdomainName(fullName)
|
|
27
|
+
if (!parts) {
|
|
28
|
+
return { ok: false, reason: 'not-a-subdomain', detail: `${fullName} is not a subdomain` }
|
|
29
|
+
}
|
|
30
|
+
const expectedOwner = getAddress(args.expectedOwnerAddress)
|
|
31
|
+
const client = args.ensClient ?? createEnsAutomationClient()
|
|
32
|
+
const parentNode = namehash(parts.parent)
|
|
33
|
+
const fullNode = namehash(fullName)
|
|
34
|
+
let parentOwner: Address
|
|
35
|
+
try {
|
|
36
|
+
parentOwner = await readOwner(client, parentNode)
|
|
37
|
+
} catch (err: unknown) {
|
|
38
|
+
return { ok: false, reason: 'lookup-failed', detail: err instanceof Error ? err.message : String(err) }
|
|
39
|
+
}
|
|
40
|
+
if (isZero(parentOwner)) {
|
|
41
|
+
return { ok: false, reason: 'parent-not-owned', detail: `${parts.parent} does not have an ENS manager on Ethereum mainnet` }
|
|
42
|
+
}
|
|
43
|
+
const parentWrapped = sameAddress(parentOwner, ENS_NAME_WRAPPER_ADDRESS_MAINNET)
|
|
44
|
+
let parentOwnerAddress: Address
|
|
45
|
+
try {
|
|
46
|
+
parentOwnerAddress = parentWrapped ? await readWrappedOwner(client, parentNode) : getAddress(parentOwner)
|
|
47
|
+
} catch (err: unknown) {
|
|
48
|
+
return { ok: false, reason: 'lookup-failed', detail: err instanceof Error ? err.message : String(err) }
|
|
49
|
+
}
|
|
50
|
+
if (!sameAddress(parentOwnerAddress, expectedOwner)) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: 'parent-owner-mismatch',
|
|
54
|
+
detail: `${parts.parent} is managed by ${shortHex(parentOwnerAddress)}, not the connected wallet ${shortHex(expectedOwner)}`,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
let childOwner: Address
|
|
58
|
+
try {
|
|
59
|
+
childOwner = await readOwner(client, fullNode)
|
|
60
|
+
} catch (err: unknown) {
|
|
61
|
+
return { ok: false, reason: 'lookup-failed', detail: err instanceof Error ? err.message : String(err) }
|
|
62
|
+
}
|
|
63
|
+
if (isZero(childOwner)) {
|
|
64
|
+
return { ok: false, reason: 'subdomain-missing', detail: `${fullName} is already cleared on Ethereum mainnet` }
|
|
65
|
+
}
|
|
66
|
+
const transaction = parentWrapped
|
|
67
|
+
? encodeDeleteSubnodeWrapped(parts.parent, parts.label)
|
|
68
|
+
: encodeDeleteSubnodeRegistry(parts.parent, parts.label)
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
plan: {
|
|
72
|
+
fullName,
|
|
73
|
+
parentName: parts.parent,
|
|
74
|
+
label: parts.label,
|
|
75
|
+
parentWrapped,
|
|
76
|
+
parentOwnerAddress,
|
|
77
|
+
transaction,
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isEthDomain, normalizeEthDomain, splitSubdomainName } from '../ensLookup.js'
|
|
2
|
+
|
|
3
|
+
export function isRootEthName(value: string): boolean {
|
|
4
|
+
const normalized = normalizeEthDomain(value)
|
|
5
|
+
return isEthDomain(normalized) && normalized.split('.').length === 2
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isValidSubdomainLabel(value: string): boolean {
|
|
9
|
+
return /^[a-z0-9-]+$/.test(value) && !value.startsWith('-') && !value.endsWith('-')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function hasValidSubdomainParts(fullName: string): boolean {
|
|
13
|
+
return Boolean(splitSubdomainName(fullName))
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getAddress, type Address } from 'viem'
|
|
2
|
+
import type { OperatorSetDiff } from './types.js'
|
|
3
|
+
|
|
4
|
+
export function compareOperatorSets(args: {
|
|
5
|
+
metadataOperators: ReadonlyArray<{ address: string }>
|
|
6
|
+
resolverDelegates: ReadonlyArray<string>
|
|
7
|
+
}): OperatorSetDiff {
|
|
8
|
+
const metadata = new Map<string, Address>()
|
|
9
|
+
for (const record of args.metadataOperators) {
|
|
10
|
+
metadata.set(record.address.toLowerCase(), getAddress(record.address))
|
|
11
|
+
}
|
|
12
|
+
const resolver = new Map<string, Address>()
|
|
13
|
+
for (const raw of args.resolverDelegates) {
|
|
14
|
+
resolver.set(raw.toLowerCase(), getAddress(raw))
|
|
15
|
+
}
|
|
16
|
+
const metadataOnly: Address[] = []
|
|
17
|
+
for (const [key, addr] of metadata) {
|
|
18
|
+
if (!resolver.has(key)) metadataOnly.push(addr)
|
|
19
|
+
}
|
|
20
|
+
const resolverOnly: Address[] = []
|
|
21
|
+
for (const [key, addr] of resolver) {
|
|
22
|
+
if (!metadata.has(key)) resolverOnly.push(addr)
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
inSync: metadataOnly.length === 0 && resolverOnly.length === 0,
|
|
26
|
+
metadataOnly,
|
|
27
|
+
resolverOnly,
|
|
28
|
+
}
|
|
29
|
+
}
|