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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
2
|
import { ml_kem1024 } from '@noble/post-quantum/ml-kem.js'
|
|
3
3
|
import { recoverAddressFromSignature, toChecksumAddress } from '../crypto/eth.js'
|
|
4
|
+
import type { TransferSnapshotMetadata } from '../../storage/config.js'
|
|
4
5
|
|
|
5
6
|
export const CONTINUITY_SNAPSHOT_ENVELOPE_VERSION = 'ethagent-continuity-snapshot-v1'
|
|
6
7
|
|
|
@@ -9,7 +10,7 @@ export type ContinuityFiles = {
|
|
|
9
10
|
'MEMORY.md': string
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
type ContinuityTranscriptSummary = {
|
|
13
14
|
sessionId?: string
|
|
14
15
|
createdAt?: string
|
|
15
16
|
summary: string
|
|
@@ -25,7 +26,7 @@ export type ContinuityAgentSnapshot = {
|
|
|
25
26
|
description?: string
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
type ContinuitySnapshotPayload = {
|
|
29
30
|
version: 1
|
|
30
31
|
ownerAddress: string
|
|
31
32
|
createdAt: string
|
|
@@ -36,7 +37,41 @@ export type ContinuitySnapshotPayload = {
|
|
|
36
37
|
state: Record<string, unknown>
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
type ContinuitySnapshotToken = {
|
|
41
|
+
chainId: number
|
|
42
|
+
identityRegistryAddress: string
|
|
43
|
+
agentId: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type TransferContinuitySnapshotSlot = {
|
|
47
|
+
address: string
|
|
48
|
+
challenge: string
|
|
49
|
+
salt: string
|
|
50
|
+
nonce: string
|
|
51
|
+
encryptedKey: string
|
|
52
|
+
tag: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type WalletContinuityRestoreAccessKey = {
|
|
56
|
+
address: string
|
|
57
|
+
challenge: string
|
|
58
|
+
salt: string
|
|
59
|
+
kemPublicKey: string
|
|
60
|
+
createdAt?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type WalletContinuitySnapshotSlot = {
|
|
64
|
+
address: string
|
|
65
|
+
challenge: string
|
|
66
|
+
salt: string
|
|
67
|
+
kemPublicKey: string
|
|
68
|
+
kemCiphertext: string
|
|
69
|
+
nonce: string
|
|
70
|
+
encryptedKey: string
|
|
71
|
+
tag: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type SignatureContinuitySnapshotEnvelope = {
|
|
40
75
|
version: 1
|
|
41
76
|
envelopeVersion: typeof CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
|
|
42
77
|
ownerAddress: string
|
|
@@ -47,6 +82,7 @@ export type ContinuitySnapshotEnvelope = {
|
|
|
47
82
|
aead: 'AES-256-GCM'
|
|
48
83
|
kdf: 'HKDF-SHA256'
|
|
49
84
|
signature: 'EIP-191'
|
|
85
|
+
decryptsWith?: 'owner-signature'
|
|
50
86
|
}
|
|
51
87
|
salt: string
|
|
52
88
|
kemPublicKey: string
|
|
@@ -56,7 +92,59 @@ export type ContinuitySnapshotEnvelope = {
|
|
|
56
92
|
tag: string
|
|
57
93
|
}
|
|
58
94
|
|
|
59
|
-
|
|
95
|
+
type WalletContinuitySnapshotEnvelope = {
|
|
96
|
+
version: 1
|
|
97
|
+
envelopeVersion: typeof CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
|
|
98
|
+
ownerAddress: string
|
|
99
|
+
createdAt: string
|
|
100
|
+
token: ContinuitySnapshotToken
|
|
101
|
+
accessEpoch: number
|
|
102
|
+
accessManifestHash: string
|
|
103
|
+
crypto: {
|
|
104
|
+
kem: 'ML-KEM-1024'
|
|
105
|
+
aead: 'AES-256-GCM'
|
|
106
|
+
kdf: 'HKDF-SHA256'
|
|
107
|
+
signature: 'EIP-191'
|
|
108
|
+
decryptsWith: 'wallet-signature-slots'
|
|
109
|
+
}
|
|
110
|
+
payloadNonce: string
|
|
111
|
+
payloadCiphertext: string
|
|
112
|
+
payloadTag: string
|
|
113
|
+
payloadHash: string
|
|
114
|
+
slots: WalletContinuitySnapshotSlot[]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type TransferContinuitySnapshotEnvelope = {
|
|
118
|
+
version: 1
|
|
119
|
+
envelopeVersion: typeof CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
|
|
120
|
+
ownerAddress: string
|
|
121
|
+
createdAt: string
|
|
122
|
+
challenge: string
|
|
123
|
+
token: ContinuitySnapshotToken
|
|
124
|
+
targetAddress: string
|
|
125
|
+
targetHandle?: string
|
|
126
|
+
crypto: {
|
|
127
|
+
aead: 'AES-256-GCM'
|
|
128
|
+
kdf: 'HKDF-SHA256'
|
|
129
|
+
signature: 'EIP-191'
|
|
130
|
+
decryptsWith: 'transfer-signature-slot'
|
|
131
|
+
}
|
|
132
|
+
payloadNonce: string
|
|
133
|
+
payloadCiphertext: string
|
|
134
|
+
payloadTag: string
|
|
135
|
+
payloadHash: string
|
|
136
|
+
slots: {
|
|
137
|
+
owner: TransferContinuitySnapshotSlot
|
|
138
|
+
target: TransferContinuitySnapshotSlot
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type ContinuitySnapshotEnvelope =
|
|
143
|
+
| SignatureContinuitySnapshotEnvelope
|
|
144
|
+
| WalletContinuitySnapshotEnvelope
|
|
145
|
+
| TransferContinuitySnapshotEnvelope
|
|
146
|
+
|
|
147
|
+
type CreateContinuitySnapshotEnvelopeArgs = {
|
|
60
148
|
ownerAddress: string
|
|
61
149
|
walletSignature: string
|
|
62
150
|
payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & {
|
|
@@ -64,9 +152,34 @@ export type CreateContinuitySnapshotEnvelopeArgs = {
|
|
|
64
152
|
}
|
|
65
153
|
}
|
|
66
154
|
|
|
67
|
-
|
|
155
|
+
type CreateTransferContinuitySnapshotEnvelopeArgs = {
|
|
156
|
+
ownerAddress: string
|
|
157
|
+
ownerWalletSignature: string
|
|
158
|
+
targetAddress: string
|
|
159
|
+
targetWalletSignature: string
|
|
160
|
+
targetHandle?: string
|
|
161
|
+
token: ContinuitySnapshotToken
|
|
162
|
+
payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & {
|
|
163
|
+
createdAt?: string
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
type CreateWalletContinuitySnapshotEnvelopeArgs = {
|
|
168
|
+
ownerAddress: string
|
|
169
|
+
token: ContinuitySnapshotToken
|
|
170
|
+
signerAddress: string
|
|
171
|
+
signerWalletSignature: string
|
|
172
|
+
accessKeys: WalletContinuityRestoreAccessKey[]
|
|
173
|
+
accessEpoch?: number
|
|
174
|
+
payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & {
|
|
175
|
+
createdAt?: string
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type RestoreContinuitySnapshotEnvelopeArgs = {
|
|
68
180
|
envelope: ContinuitySnapshotEnvelope
|
|
69
181
|
walletSignature: string
|
|
182
|
+
currentOwnerAddress?: string
|
|
70
183
|
}
|
|
71
184
|
|
|
72
185
|
export class ContinuitySnapshotOwnerMismatchError extends Error {
|
|
@@ -74,16 +187,34 @@ export class ContinuitySnapshotOwnerMismatchError extends Error {
|
|
|
74
187
|
readonly snapshotOwner: string,
|
|
75
188
|
readonly currentOwner: string,
|
|
76
189
|
) {
|
|
77
|
-
super('
|
|
190
|
+
super('Continuity snapshot is encrypted for another wallet')
|
|
78
191
|
this.name = 'ContinuitySnapshotOwnerMismatchError'
|
|
79
192
|
}
|
|
80
193
|
}
|
|
81
194
|
|
|
82
|
-
export
|
|
83
|
-
|
|
195
|
+
export class ContinuityTransferSnapshotTargetMismatchError extends Error {
|
|
196
|
+
constructor(
|
|
197
|
+
readonly snapshotOwner: string,
|
|
198
|
+
readonly targetOwner: string,
|
|
199
|
+
readonly currentOwner: string,
|
|
200
|
+
) {
|
|
201
|
+
super('Transfer snapshot receiver does not match the current token owner')
|
|
202
|
+
this.name = 'ContinuityTransferSnapshotTargetMismatchError'
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export class ContinuitySnapshotRestoreSlotMissingError extends Error {
|
|
207
|
+
constructor(readonly walletAddress: string) {
|
|
208
|
+
super('Restore slot missing')
|
|
209
|
+
this.name = 'ContinuitySnapshotRestoreSlotMissingError'
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const CONTINUITY_SNAPSHOT_CHALLENGE_MESSAGES = [
|
|
214
|
+
'Save or Restore Identity Files',
|
|
84
215
|
'Action: encrypt or decrypt local identity files',
|
|
85
216
|
'Private: SOUL.md, MEMORY.md',
|
|
86
|
-
'Public: skills
|
|
217
|
+
'Public: public skills and profile',
|
|
87
218
|
'Safety: no transaction, spending, or approvals',
|
|
88
219
|
'Version: 1',
|
|
89
220
|
] as const
|
|
@@ -97,22 +228,151 @@ export function createContinuitySnapshotChallenge(ownerAddress: string): string
|
|
|
97
228
|
].join('\n')
|
|
98
229
|
}
|
|
99
230
|
|
|
100
|
-
export
|
|
231
|
+
export const TRANSFER_SNAPSHOT_CHALLENGE_HEADER_LEGACY = 'Prepare Transfer Restore Snapshot'
|
|
232
|
+
export const TRANSFER_SNAPSHOT_CHALLENGE_HEADER_SENDER = 'Prepare Transfer Snapshot · Sender Restore Slot'
|
|
233
|
+
export const TRANSFER_SNAPSHOT_CHALLENGE_HEADER_RECEIVER = 'Prepare Transfer Snapshot · Receiver Restore Slot'
|
|
234
|
+
|
|
235
|
+
export function createTransferContinuitySnapshotChallenge(args: {
|
|
236
|
+
token: ContinuitySnapshotToken
|
|
237
|
+
ownerAddress: string
|
|
238
|
+
targetAddress: string
|
|
239
|
+
role?: 'sender' | 'receiver'
|
|
240
|
+
}): string {
|
|
241
|
+
const token = normalizeContinuitySnapshotToken(args.token)
|
|
242
|
+
const ownerAddress = toChecksumAddress(args.ownerAddress)
|
|
243
|
+
const targetAddress = toChecksumAddress(args.targetAddress)
|
|
244
|
+
const header = args.role === 'sender'
|
|
245
|
+
? TRANSFER_SNAPSHOT_CHALLENGE_HEADER_SENDER
|
|
246
|
+
: args.role === 'receiver'
|
|
247
|
+
? TRANSFER_SNAPSHOT_CHALLENGE_HEADER_RECEIVER
|
|
248
|
+
: TRANSFER_SNAPSHOT_CHALLENGE_HEADER_LEGACY
|
|
249
|
+
return [
|
|
250
|
+
header,
|
|
251
|
+
`ERC-8004 Chain ID: ${token.chainId}`,
|
|
252
|
+
`ERC-8004 Registry: ${token.identityRegistryAddress}`,
|
|
253
|
+
`ERC-8004 Token ID: ${token.agentId}`,
|
|
254
|
+
`Sender Owner: ${ownerAddress}`,
|
|
255
|
+
`Receiver Owner: ${targetAddress}`,
|
|
256
|
+
'Action: encrypt or decrypt local identity files for this token transfer',
|
|
257
|
+
'Private: SOUL.md, MEMORY.md',
|
|
258
|
+
'Public: public skills and profile',
|
|
259
|
+
'Safety: no transaction, spending, or approvals',
|
|
260
|
+
'Version: 1',
|
|
261
|
+
].join('\n')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export type WalletChallengePurpose =
|
|
265
|
+
| 'create-agent'
|
|
266
|
+
| 'update-snapshot'
|
|
267
|
+
| 'update-ens-snapshot'
|
|
268
|
+
| 'clear-ens-snapshot'
|
|
269
|
+
| 'update-profile-snapshot'
|
|
270
|
+
| 'update-operators-snapshot'
|
|
271
|
+
| 'refetch-snapshot'
|
|
272
|
+
| 'operator-proof'
|
|
273
|
+
| 'restore-owner'
|
|
274
|
+
| 'restore-operator'
|
|
275
|
+
| 'transfer-prepare-sender'
|
|
276
|
+
| 'transfer-prepare-receiver'
|
|
277
|
+
|
|
278
|
+
const WALLET_CHALLENGE_V2_COPY: Record<WalletChallengePurpose, { title: string; action: string }> = {
|
|
279
|
+
'create-agent': { title: 'Create Agent Snapshot Key', action: 'Action: encrypt the new agent snapshot for owner restore' },
|
|
280
|
+
'update-snapshot': { title: 'Save Snapshot Encryption Key', action: 'Action: encrypt the updated agent snapshot' },
|
|
281
|
+
'update-ens-snapshot': { title: 'Update ENS in Agent Snapshot', action: 'Action: encrypt the snapshot with the new ENS name. No onchain ENS records change.' },
|
|
282
|
+
'clear-ens-snapshot': { title: 'Clear ENS from Agent Snapshot', action: 'Action: encrypt the snapshot with no ENS name. No onchain ENS records change.' },
|
|
283
|
+
'update-profile-snapshot': { title: 'Update Public Profile Snapshot Key', action: 'Action: encrypt the snapshot with the updated profile' },
|
|
284
|
+
'update-operators-snapshot': { title: 'Update Operator Wallets Snapshot Key', action: 'Action: encrypt the snapshot with the updated operator list' },
|
|
285
|
+
'refetch-snapshot': { title: 'Refetch Latest Snapshot', action: 'Action: decrypt the latest published snapshot' },
|
|
286
|
+
'operator-proof': { title: 'Authorize Operator Wallet Restore Access', action: 'Action: prove this operator wallet can decrypt future snapshots' },
|
|
287
|
+
'restore-owner': { title: 'Restore Agent with Owner Wallet', action: 'Action: decrypt the snapshot for the owner wallet' },
|
|
288
|
+
'restore-operator': { title: 'Restore Agent with Operator Wallet', action: 'Action: decrypt the snapshot for the authorized operator wallet' },
|
|
289
|
+
'transfer-prepare-sender': { title: 'Prepare Token Transfer (Sender)', action: 'Action: encrypt the transfer snapshot for the receiver' },
|
|
290
|
+
'transfer-prepare-receiver': { title: 'Receive Token Transfer (Receiver)', action: 'Action: prepare receiver decryption for the transfer snapshot' },
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function createWalletRestoreAccessChallenge(args: {
|
|
294
|
+
token: ContinuitySnapshotToken
|
|
295
|
+
ownerAddress: string
|
|
296
|
+
walletAddress: string
|
|
297
|
+
accessEpoch?: number
|
|
298
|
+
purpose?: WalletChallengePurpose
|
|
299
|
+
}): string {
|
|
300
|
+
const token = normalizeContinuitySnapshotToken(args.token)
|
|
301
|
+
const ownerAddress = toChecksumAddress(args.ownerAddress)
|
|
302
|
+
const walletAddress = toChecksumAddress(args.walletAddress)
|
|
303
|
+
if (args.purpose) {
|
|
304
|
+
const copy = WALLET_CHALLENGE_V2_COPY[args.purpose]
|
|
305
|
+
return [
|
|
306
|
+
copy.title,
|
|
307
|
+
`ERC-8004 Chain ID: ${token.chainId}`,
|
|
308
|
+
`ERC-8004 Registry: ${token.identityRegistryAddress}`,
|
|
309
|
+
`ERC-8004 Token ID: ${token.agentId}`,
|
|
310
|
+
`Owner: ${ownerAddress}`,
|
|
311
|
+
`Wallet: ${walletAddress}`,
|
|
312
|
+
`Access Epoch: ${args.accessEpoch ?? 1}`,
|
|
313
|
+
copy.action,
|
|
314
|
+
'Private: SOUL.md, MEMORY.md',
|
|
315
|
+
'Safety: no transaction, spending, or approvals',
|
|
316
|
+
'Version: 2',
|
|
317
|
+
].join('\n')
|
|
318
|
+
}
|
|
319
|
+
return [
|
|
320
|
+
'Authorize Wallet Restore Access',
|
|
321
|
+
`ERC-8004 Chain ID: ${token.chainId}`,
|
|
322
|
+
`ERC-8004 Registry: ${token.identityRegistryAddress}`,
|
|
323
|
+
`ERC-8004 Token ID: ${token.agentId}`,
|
|
324
|
+
`Owner: ${ownerAddress}`,
|
|
325
|
+
`Wallet: ${walletAddress}`,
|
|
326
|
+
`Access Epoch: ${args.accessEpoch ?? 1}`,
|
|
327
|
+
'Action: create a restore key for encrypted identity snapshots',
|
|
328
|
+
'Private: SOUL.md, MEMORY.md',
|
|
329
|
+
'Safety: no transaction, spending, or approvals',
|
|
330
|
+
'Version: 1',
|
|
331
|
+
].join('\n')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function createWalletRestoreAccessKey(args: {
|
|
335
|
+
token: ContinuitySnapshotToken
|
|
336
|
+
ownerAddress: string
|
|
337
|
+
walletAddress: string
|
|
338
|
+
walletSignature: string
|
|
339
|
+
accessEpoch?: number
|
|
340
|
+
createdAt?: string
|
|
341
|
+
salt?: string
|
|
342
|
+
purpose?: WalletChallengePurpose
|
|
343
|
+
}): WalletContinuityRestoreAccessKey {
|
|
344
|
+
const walletAddress = toChecksumAddress(args.walletAddress)
|
|
345
|
+
const challenge = createWalletRestoreAccessChallenge({
|
|
346
|
+
token: args.token,
|
|
347
|
+
ownerAddress: args.ownerAddress,
|
|
348
|
+
walletAddress,
|
|
349
|
+
accessEpoch: args.accessEpoch,
|
|
350
|
+
purpose: args.purpose,
|
|
351
|
+
})
|
|
352
|
+
assertSignatureForAddress(challenge, args.walletSignature, walletAddress)
|
|
353
|
+
const salt = args.salt ? fromBase64(args.salt) : crypto.randomBytes(32)
|
|
354
|
+
const kemSeed = deriveWalletRestoreKemSeed(args.walletSignature, salt, walletAddress, challenge)
|
|
355
|
+
const kemKeys = ml_kem1024.keygen(kemSeed)
|
|
356
|
+
return {
|
|
357
|
+
address: walletAddress,
|
|
358
|
+
challenge,
|
|
359
|
+
salt: toBase64(salt),
|
|
360
|
+
kemPublicKey: toBase64(kemKeys.publicKey),
|
|
361
|
+
...(args.createdAt ? { createdAt: args.createdAt } : {}),
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function createContinuitySnapshotEnvelope(args: CreateContinuitySnapshotEnvelopeArgs): SignatureContinuitySnapshotEnvelope {
|
|
101
366
|
const ownerAddress = toChecksumAddress(args.ownerAddress)
|
|
102
367
|
const challenge = createContinuitySnapshotChallenge(ownerAddress)
|
|
103
368
|
assertSignatureForAddress(challenge, args.walletSignature, ownerAddress)
|
|
104
369
|
|
|
105
370
|
const createdAt = args.payload.createdAt ?? new Date().toISOString()
|
|
106
|
-
const payload
|
|
107
|
-
version: 1,
|
|
371
|
+
const payload = continuityPayloadFromArgs({
|
|
108
372
|
ownerAddress,
|
|
109
373
|
createdAt,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
files: normalizeContinuityFiles(args.payload.files),
|
|
113
|
-
transcript: normalizeTranscript(args.payload.transcript),
|
|
114
|
-
state: normalizeState(args.payload.state),
|
|
115
|
-
}
|
|
374
|
+
payload: args.payload,
|
|
375
|
+
})
|
|
116
376
|
|
|
117
377
|
const salt = crypto.randomBytes(32)
|
|
118
378
|
const kemSeed = deriveContinuityKemSeed(args.walletSignature, salt, ownerAddress)
|
|
@@ -137,6 +397,7 @@ export function createContinuitySnapshotEnvelope(args: CreateContinuitySnapshotE
|
|
|
137
397
|
aead: 'AES-256-GCM',
|
|
138
398
|
kdf: 'HKDF-SHA256',
|
|
139
399
|
signature: 'EIP-191',
|
|
400
|
+
decryptsWith: 'owner-signature',
|
|
140
401
|
},
|
|
141
402
|
salt: toBase64(salt),
|
|
142
403
|
kemPublicKey: toBase64(kemKeys.publicKey),
|
|
@@ -147,8 +408,162 @@ export function createContinuitySnapshotEnvelope(args: CreateContinuitySnapshotE
|
|
|
147
408
|
}
|
|
148
409
|
}
|
|
149
410
|
|
|
411
|
+
export function createWalletContinuitySnapshotEnvelope(
|
|
412
|
+
args: CreateWalletContinuitySnapshotEnvelopeArgs,
|
|
413
|
+
): WalletContinuitySnapshotEnvelope {
|
|
414
|
+
const ownerAddress = toChecksumAddress(args.ownerAddress)
|
|
415
|
+
const signerAddress = toChecksumAddress(args.signerAddress)
|
|
416
|
+
const token = normalizeContinuitySnapshotToken(args.token)
|
|
417
|
+
const accessEpoch = args.accessEpoch ?? 1
|
|
418
|
+
const accessKeys = normalizeWalletRestoreAccessKeys(args.accessKeys)
|
|
419
|
+
if (accessKeys.length === 0) throw new Error('At least one restore access key is required')
|
|
420
|
+
const signerKey = accessKeys.find(key => key.address.toLowerCase() === signerAddress.toLowerCase())
|
|
421
|
+
if (!signerKey) throw new Error('Snapshot signer is not an authorized restore wallet')
|
|
422
|
+
assertSignatureForAddress(signerKey.challenge, args.signerWalletSignature, signerAddress)
|
|
423
|
+
|
|
424
|
+
const createdAt = args.payload.createdAt ?? new Date().toISOString()
|
|
425
|
+
const payload = continuityPayloadFromArgs({
|
|
426
|
+
ownerAddress,
|
|
427
|
+
createdAt,
|
|
428
|
+
payload: {
|
|
429
|
+
...args.payload,
|
|
430
|
+
agent: normalizeAgentSnapshot({
|
|
431
|
+
...args.payload.agent,
|
|
432
|
+
chainId: args.payload.agent.chainId ?? token.chainId,
|
|
433
|
+
identityRegistryAddress: args.payload.agent.identityRegistryAddress ?? token.identityRegistryAddress,
|
|
434
|
+
agentId: args.payload.agent.agentId ?? token.agentId,
|
|
435
|
+
}),
|
|
436
|
+
},
|
|
437
|
+
})
|
|
438
|
+
const plaintext = Buffer.from(JSON.stringify(payload), 'utf8')
|
|
439
|
+
const contentKey = crypto.randomBytes(32)
|
|
440
|
+
const payloadNonce = crypto.randomBytes(12)
|
|
441
|
+
const accessManifestHash = walletAccessManifestHash({ ownerAddress, token, accessEpoch, accessKeys })
|
|
442
|
+
const payloadAad = walletPayloadAadFor({ ownerAddress, createdAt, token, accessEpoch, accessManifestHash })
|
|
443
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', contentKey, payloadNonce)
|
|
444
|
+
cipher.setAAD(payloadAad)
|
|
445
|
+
const payloadCiphertext = Buffer.concat([cipher.update(plaintext), cipher.final()])
|
|
446
|
+
const payloadTag = cipher.getAuthTag()
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
version: 1,
|
|
450
|
+
envelopeVersion: CONTINUITY_SNAPSHOT_ENVELOPE_VERSION,
|
|
451
|
+
ownerAddress,
|
|
452
|
+
createdAt,
|
|
453
|
+
token,
|
|
454
|
+
accessEpoch,
|
|
455
|
+
accessManifestHash,
|
|
456
|
+
crypto: {
|
|
457
|
+
kem: 'ML-KEM-1024',
|
|
458
|
+
aead: 'AES-256-GCM',
|
|
459
|
+
kdf: 'HKDF-SHA256',
|
|
460
|
+
signature: 'EIP-191',
|
|
461
|
+
decryptsWith: 'wallet-signature-slots',
|
|
462
|
+
},
|
|
463
|
+
payloadNonce: toBase64(payloadNonce),
|
|
464
|
+
payloadCiphertext: toBase64(payloadCiphertext),
|
|
465
|
+
payloadTag: toBase64(payloadTag),
|
|
466
|
+
payloadHash: sha256Hex(plaintext),
|
|
467
|
+
slots: accessKeys.map(key => createWalletSlot({ accessKey: key, contentKey, payloadAad, accessManifestHash })),
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function createTransferContinuitySnapshotEnvelope(
|
|
472
|
+
args: CreateTransferContinuitySnapshotEnvelopeArgs,
|
|
473
|
+
): TransferContinuitySnapshotEnvelope {
|
|
474
|
+
const ownerAddress = toChecksumAddress(args.ownerAddress)
|
|
475
|
+
const targetAddress = toChecksumAddress(args.targetAddress)
|
|
476
|
+
if (ownerAddress.toLowerCase() === targetAddress.toLowerCase()) {
|
|
477
|
+
throw new Error('Receiver wallet must be different from sender wallet')
|
|
478
|
+
}
|
|
479
|
+
const token = normalizeContinuitySnapshotToken(args.token)
|
|
480
|
+
const senderChallenge = createTransferContinuitySnapshotChallenge({ token, ownerAddress, targetAddress, role: 'sender' })
|
|
481
|
+
const receiverChallenge = createTransferContinuitySnapshotChallenge({ token, ownerAddress, targetAddress, role: 'receiver' })
|
|
482
|
+
const challenge = createTransferContinuitySnapshotChallenge({ token, ownerAddress, targetAddress })
|
|
483
|
+
assertSignatureForAddress(senderChallenge, args.ownerWalletSignature, ownerAddress)
|
|
484
|
+
assertSignatureForAddress(receiverChallenge, args.targetWalletSignature, targetAddress)
|
|
485
|
+
|
|
486
|
+
const createdAt = args.payload.createdAt ?? new Date().toISOString()
|
|
487
|
+
const payload = continuityPayloadFromArgs({
|
|
488
|
+
ownerAddress,
|
|
489
|
+
createdAt,
|
|
490
|
+
payload: {
|
|
491
|
+
...args.payload,
|
|
492
|
+
agent: normalizeAgentSnapshot({
|
|
493
|
+
...args.payload.agent,
|
|
494
|
+
chainId: args.payload.agent.chainId ?? token.chainId,
|
|
495
|
+
identityRegistryAddress: args.payload.agent.identityRegistryAddress ?? token.identityRegistryAddress,
|
|
496
|
+
agentId: args.payload.agent.agentId ?? token.agentId,
|
|
497
|
+
}),
|
|
498
|
+
},
|
|
499
|
+
})
|
|
500
|
+
const plaintext = Buffer.from(JSON.stringify(payload), 'utf8')
|
|
501
|
+
const contentKey = crypto.randomBytes(32)
|
|
502
|
+
const payloadNonce = crypto.randomBytes(12)
|
|
503
|
+
const payloadAad = transferPayloadAadFor({ ownerAddress, targetAddress, createdAt, token })
|
|
504
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', contentKey, payloadNonce)
|
|
505
|
+
cipher.setAAD(payloadAad)
|
|
506
|
+
const payloadCiphertext = Buffer.concat([cipher.update(plaintext), cipher.final()])
|
|
507
|
+
const payloadTag = cipher.getAuthTag()
|
|
508
|
+
|
|
509
|
+
const ownerSlot = createTransferSlot({
|
|
510
|
+
address: ownerAddress,
|
|
511
|
+
challenge: senderChallenge,
|
|
512
|
+
walletSignature: args.ownerWalletSignature,
|
|
513
|
+
contentKey,
|
|
514
|
+
payloadAad,
|
|
515
|
+
})
|
|
516
|
+
const targetSlot = createTransferSlot({
|
|
517
|
+
address: targetAddress,
|
|
518
|
+
challenge: receiverChallenge,
|
|
519
|
+
walletSignature: args.targetWalletSignature,
|
|
520
|
+
contentKey,
|
|
521
|
+
payloadAad,
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
version: 1,
|
|
526
|
+
envelopeVersion: CONTINUITY_SNAPSHOT_ENVELOPE_VERSION,
|
|
527
|
+
ownerAddress,
|
|
528
|
+
createdAt,
|
|
529
|
+
challenge,
|
|
530
|
+
token,
|
|
531
|
+
targetAddress,
|
|
532
|
+
...(args.targetHandle ? { targetHandle: args.targetHandle } : {}),
|
|
533
|
+
crypto: {
|
|
534
|
+
aead: 'AES-256-GCM',
|
|
535
|
+
kdf: 'HKDF-SHA256',
|
|
536
|
+
signature: 'EIP-191',
|
|
537
|
+
decryptsWith: 'transfer-signature-slot',
|
|
538
|
+
},
|
|
539
|
+
payloadNonce: toBase64(payloadNonce),
|
|
540
|
+
payloadCiphertext: toBase64(payloadCiphertext),
|
|
541
|
+
payloadTag: toBase64(payloadTag),
|
|
542
|
+
payloadHash: sha256Hex(plaintext),
|
|
543
|
+
slots: {
|
|
544
|
+
owner: ownerSlot,
|
|
545
|
+
target: targetSlot,
|
|
546
|
+
},
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
150
550
|
export function restoreContinuitySnapshotEnvelope(args: RestoreContinuitySnapshotEnvelopeArgs): ContinuitySnapshotPayload {
|
|
151
551
|
const envelope = normalizeContinuitySnapshotEnvelope(args.envelope)
|
|
552
|
+
if (isWalletContinuitySnapshotEnvelope(envelope)) {
|
|
553
|
+
return restoreWalletContinuitySnapshotEnvelope({
|
|
554
|
+
envelope,
|
|
555
|
+
walletSignature: args.walletSignature,
|
|
556
|
+
currentOwnerAddress: args.currentOwnerAddress,
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
if (isTransferContinuitySnapshotEnvelope(envelope)) {
|
|
560
|
+
return restoreTransferContinuitySnapshotEnvelope({
|
|
561
|
+
envelope,
|
|
562
|
+
walletSignature: args.walletSignature,
|
|
563
|
+
currentOwnerAddress: args.currentOwnerAddress,
|
|
564
|
+
})
|
|
565
|
+
}
|
|
566
|
+
|
|
152
567
|
assertSignatureForAddress(envelope.challenge, args.walletSignature, envelope.ownerAddress)
|
|
153
568
|
|
|
154
569
|
const salt = fromBase64(envelope.salt)
|
|
@@ -156,7 +571,7 @@ export function restoreContinuitySnapshotEnvelope(args: RestoreContinuitySnapsho
|
|
|
156
571
|
const kemKeys = ml_kem1024.keygen(kemSeed)
|
|
157
572
|
const expectedPublicKey = toBase64(kemKeys.publicKey)
|
|
158
573
|
if (expectedPublicKey !== envelope.kemPublicKey) {
|
|
159
|
-
throw new Error('
|
|
574
|
+
throw new Error('Wallet signature does not match this continuity snapshot')
|
|
160
575
|
}
|
|
161
576
|
|
|
162
577
|
const sharedSecret = ml_kem1024.decapsulate(fromBase64(envelope.kemCiphertext), kemKeys.secretKey)
|
|
@@ -173,22 +588,36 @@ export function restoreContinuitySnapshotEnvelope(args: RestoreContinuitySnapsho
|
|
|
173
588
|
]).toString('utf8')
|
|
174
589
|
decoded = JSON.parse(plaintext)
|
|
175
590
|
} catch {
|
|
176
|
-
throw new Error('
|
|
591
|
+
throw new Error('Could not decrypt continuity snapshot with the supplied wallet signature')
|
|
177
592
|
}
|
|
178
593
|
|
|
179
594
|
const payload = normalizeContinuityPayload(decoded)
|
|
180
|
-
|
|
181
|
-
throw new Error('continuity snapshot owner mismatch')
|
|
182
|
-
}
|
|
183
|
-
if (payload.createdAt !== envelope.createdAt) {
|
|
184
|
-
throw new Error('continuity snapshot timestamp mismatch')
|
|
185
|
-
}
|
|
595
|
+
assertPayloadMatchesEnvelope(payload, envelope.ownerAddress, envelope.createdAt)
|
|
186
596
|
return payload
|
|
187
597
|
}
|
|
188
598
|
|
|
189
599
|
export function assertContinuitySnapshotOwner(envelope: ContinuitySnapshotEnvelope, currentOwner: string): void {
|
|
190
|
-
const
|
|
600
|
+
const normalized = normalizeContinuitySnapshotEnvelope(envelope)
|
|
191
601
|
const owner = toChecksumAddress(currentOwner)
|
|
602
|
+
if (isWalletContinuitySnapshotEnvelope(normalized)) {
|
|
603
|
+
const snapshotOwner = toChecksumAddress(normalized.ownerAddress)
|
|
604
|
+
if (snapshotOwner.toLowerCase() !== owner.toLowerCase()) {
|
|
605
|
+
throw new ContinuitySnapshotOwnerMismatchError(snapshotOwner, owner)
|
|
606
|
+
}
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
if (isTransferContinuitySnapshotEnvelope(normalized)) {
|
|
610
|
+
const snapshotOwner = toChecksumAddress(normalized.ownerAddress)
|
|
611
|
+
const targetOwner = toChecksumAddress(normalized.targetAddress)
|
|
612
|
+
if (
|
|
613
|
+
owner.toLowerCase() !== snapshotOwner.toLowerCase()
|
|
614
|
+
&& owner.toLowerCase() !== targetOwner.toLowerCase()
|
|
615
|
+
) {
|
|
616
|
+
throw new ContinuityTransferSnapshotTargetMismatchError(snapshotOwner, targetOwner, owner)
|
|
617
|
+
}
|
|
618
|
+
return
|
|
619
|
+
}
|
|
620
|
+
const snapshotOwner = toChecksumAddress(normalized.ownerAddress)
|
|
192
621
|
if (snapshotOwner.toLowerCase() !== owner.toLowerCase()) {
|
|
193
622
|
throw new ContinuitySnapshotOwnerMismatchError(snapshotOwner, owner)
|
|
194
623
|
}
|
|
@@ -204,13 +633,337 @@ export function parseContinuitySnapshotEnvelope(raw: string | Uint8Array): Conti
|
|
|
204
633
|
return normalizeContinuitySnapshotEnvelope(parsed)
|
|
205
634
|
}
|
|
206
635
|
|
|
636
|
+
export function transferSnapshotMetadataFromEnvelope(
|
|
637
|
+
envelope: ContinuitySnapshotEnvelope,
|
|
638
|
+
): TransferSnapshotMetadata | null {
|
|
639
|
+
const normalized = normalizeContinuitySnapshotEnvelope(envelope)
|
|
640
|
+
if (!isTransferContinuitySnapshotEnvelope(normalized)) return null
|
|
641
|
+
const slotCount = [normalized.slots.owner, normalized.slots.target]
|
|
642
|
+
.filter(slot => Boolean(slot?.encryptedKey && slot.address))
|
|
643
|
+
.length
|
|
644
|
+
if (slotCount < 2) return null
|
|
645
|
+
return {
|
|
646
|
+
kind: 'dual-wallet',
|
|
647
|
+
senderAddress: normalized.ownerAddress,
|
|
648
|
+
receiverAddress: normalized.targetAddress,
|
|
649
|
+
...(normalized.targetHandle ? { receiverHandle: normalized.targetHandle } : {}),
|
|
650
|
+
slotCount,
|
|
651
|
+
createdAt: normalized.createdAt,
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export function walletContinuitySnapshotSlotForAddress(
|
|
656
|
+
envelope: ContinuitySnapshotEnvelope,
|
|
657
|
+
address: string,
|
|
658
|
+
): WalletContinuitySnapshotSlot | null {
|
|
659
|
+
const normalized = normalizeContinuitySnapshotEnvelope(envelope)
|
|
660
|
+
if (!isWalletContinuitySnapshotEnvelope(normalized)) return null
|
|
661
|
+
const checksum = toChecksumAddress(address)
|
|
662
|
+
return normalized.slots.find(slot => slot.address.toLowerCase() === checksum.toLowerCase()) ?? null
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export function findRestorableAddressForSnapshot(
|
|
666
|
+
envelope: ContinuitySnapshotEnvelope,
|
|
667
|
+
candidates: ReadonlyArray<string>,
|
|
668
|
+
): string | null {
|
|
669
|
+
const normalized = normalizeContinuitySnapshotEnvelope(envelope)
|
|
670
|
+
const seen = new Set<string>()
|
|
671
|
+
const lowerCandidates: string[] = []
|
|
672
|
+
for (const candidate of candidates) {
|
|
673
|
+
if (!candidate) continue
|
|
674
|
+
const lower = candidate.toLowerCase()
|
|
675
|
+
if (seen.has(lower)) continue
|
|
676
|
+
seen.add(lower)
|
|
677
|
+
lowerCandidates.push(lower)
|
|
678
|
+
}
|
|
679
|
+
if (lowerCandidates.length === 0) return null
|
|
680
|
+
if (isWalletContinuitySnapshotEnvelope(normalized)) {
|
|
681
|
+
for (const slot of normalized.slots) {
|
|
682
|
+
if (lowerCandidates.includes(slot.address.toLowerCase())) return toChecksumAddress(slot.address)
|
|
683
|
+
}
|
|
684
|
+
return null
|
|
685
|
+
}
|
|
686
|
+
if (isTransferContinuitySnapshotEnvelope(normalized)) {
|
|
687
|
+
const ownerLower = normalized.ownerAddress.toLowerCase()
|
|
688
|
+
const targetLower = normalized.targetAddress.toLowerCase()
|
|
689
|
+
if (lowerCandidates.includes(ownerLower)) return toChecksumAddress(normalized.ownerAddress)
|
|
690
|
+
if (lowerCandidates.includes(targetLower)) return toChecksumAddress(normalized.targetAddress)
|
|
691
|
+
return null
|
|
692
|
+
}
|
|
693
|
+
const ownerLower = normalized.ownerAddress.toLowerCase()
|
|
694
|
+
return lowerCandidates.includes(ownerLower) ? toChecksumAddress(normalized.ownerAddress) : null
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export function isWalletContinuitySnapshotEnvelope(input: unknown): input is WalletContinuitySnapshotEnvelope {
|
|
698
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return false
|
|
699
|
+
const obj = input as Partial<WalletContinuitySnapshotEnvelope> & { crypto?: Partial<WalletContinuitySnapshotEnvelope['crypto']> }
|
|
700
|
+
return obj.version === 1
|
|
701
|
+
&& obj.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
|
|
702
|
+
&& typeof obj.ownerAddress === 'string'
|
|
703
|
+
&& typeof obj.createdAt === 'string'
|
|
704
|
+
&& !!obj.token
|
|
705
|
+
&& typeof obj.accessEpoch === 'number'
|
|
706
|
+
&& typeof obj.accessManifestHash === 'string'
|
|
707
|
+
&& obj.crypto?.decryptsWith === 'wallet-signature-slots'
|
|
708
|
+
&& typeof obj.payloadNonce === 'string'
|
|
709
|
+
&& typeof obj.payloadCiphertext === 'string'
|
|
710
|
+
&& typeof obj.payloadTag === 'string'
|
|
711
|
+
&& typeof obj.payloadHash === 'string'
|
|
712
|
+
&& Array.isArray(obj.slots)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function isTransferContinuitySnapshotEnvelope(input: unknown): input is TransferContinuitySnapshotEnvelope {
|
|
716
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return false
|
|
717
|
+
const obj = input as Partial<TransferContinuitySnapshotEnvelope> & { crypto?: Partial<TransferContinuitySnapshotEnvelope['crypto']> }
|
|
718
|
+
return obj.version === 1
|
|
719
|
+
&& obj.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
|
|
720
|
+
&& typeof obj.ownerAddress === 'string'
|
|
721
|
+
&& typeof obj.createdAt === 'string'
|
|
722
|
+
&& typeof obj.challenge === 'string'
|
|
723
|
+
&& !!obj.token
|
|
724
|
+
&& typeof obj.targetAddress === 'string'
|
|
725
|
+
&& obj.crypto?.decryptsWith === 'transfer-signature-slot'
|
|
726
|
+
&& typeof obj.payloadNonce === 'string'
|
|
727
|
+
&& typeof obj.payloadCiphertext === 'string'
|
|
728
|
+
&& typeof obj.payloadTag === 'string'
|
|
729
|
+
&& typeof obj.payloadHash === 'string'
|
|
730
|
+
&& !!obj.slots
|
|
731
|
+
&& !!obj.slots.owner
|
|
732
|
+
&& !!obj.slots.target
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function restoreTransferContinuitySnapshotEnvelope(args: {
|
|
736
|
+
envelope: TransferContinuitySnapshotEnvelope
|
|
737
|
+
walletSignature: string
|
|
738
|
+
currentOwnerAddress?: string
|
|
739
|
+
}): ContinuitySnapshotPayload {
|
|
740
|
+
const currentAddress = args.currentOwnerAddress
|
|
741
|
+
? toChecksumAddress(args.currentOwnerAddress)
|
|
742
|
+
: toChecksumAddress(recoverAddressFromSignature(args.envelope.challenge, args.walletSignature))
|
|
743
|
+
const slot = transferSlotForCurrentOwner(args.envelope, currentAddress)
|
|
744
|
+
assertSignatureForAddress(slot.challenge, args.walletSignature, slot.address)
|
|
745
|
+
|
|
746
|
+
let contentKey: Buffer
|
|
747
|
+
try {
|
|
748
|
+
const payloadAad = transferPayloadAadFor(args.envelope)
|
|
749
|
+
const slotKey = deriveTransferSlotKey(args.walletSignature, fromBase64(slot.salt), slot.address, slot.challenge)
|
|
750
|
+
const keyDecipher = crypto.createDecipheriv('aes-256-gcm', slotKey, fromBase64(slot.nonce))
|
|
751
|
+
keyDecipher.setAAD(transferSlotAadFor(slot, payloadAad))
|
|
752
|
+
keyDecipher.setAuthTag(fromBase64(slot.tag))
|
|
753
|
+
contentKey = Buffer.concat([
|
|
754
|
+
keyDecipher.update(fromBase64(slot.encryptedKey)),
|
|
755
|
+
keyDecipher.final(),
|
|
756
|
+
])
|
|
757
|
+
} catch {
|
|
758
|
+
throw new Error('Could not decrypt transfer snapshot key with the supplied wallet signature')
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
let decoded: unknown
|
|
762
|
+
try {
|
|
763
|
+
const payloadAad = transferPayloadAadFor(args.envelope)
|
|
764
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', contentKey, fromBase64(args.envelope.payloadNonce))
|
|
765
|
+
decipher.setAAD(payloadAad)
|
|
766
|
+
decipher.setAuthTag(fromBase64(args.envelope.payloadTag))
|
|
767
|
+
const plaintext = Buffer.concat([
|
|
768
|
+
decipher.update(fromBase64(args.envelope.payloadCiphertext)),
|
|
769
|
+
decipher.final(),
|
|
770
|
+
])
|
|
771
|
+
if (sha256Hex(plaintext) !== args.envelope.payloadHash) {
|
|
772
|
+
throw new Error('Transfer snapshot payload hash mismatch')
|
|
773
|
+
}
|
|
774
|
+
decoded = JSON.parse(plaintext.toString('utf8')) as unknown
|
|
775
|
+
} catch {
|
|
776
|
+
throw new Error('Could not decrypt continuity transfer snapshot with the supplied wallet signature')
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const payload = normalizeContinuityPayload(decoded)
|
|
780
|
+
assertPayloadMatchesEnvelope(payload, args.envelope.ownerAddress, args.envelope.createdAt)
|
|
781
|
+
return payload
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function restoreWalletContinuitySnapshotEnvelope(args: {
|
|
785
|
+
envelope: WalletContinuitySnapshotEnvelope
|
|
786
|
+
walletSignature: string
|
|
787
|
+
currentOwnerAddress?: string
|
|
788
|
+
}): ContinuitySnapshotPayload {
|
|
789
|
+
const slot = walletSlotForRestore(args.envelope, args.walletSignature, args.currentOwnerAddress)
|
|
790
|
+
assertSignatureForAddress(slot.challenge, args.walletSignature, slot.address)
|
|
791
|
+
|
|
792
|
+
let contentKey: Buffer
|
|
793
|
+
try {
|
|
794
|
+
const salt = fromBase64(slot.salt)
|
|
795
|
+
const kemSeed = deriveWalletRestoreKemSeed(args.walletSignature, salt, slot.address, slot.challenge)
|
|
796
|
+
const kemKeys = ml_kem1024.keygen(kemSeed)
|
|
797
|
+
if (toBase64(kemKeys.publicKey) !== slot.kemPublicKey) {
|
|
798
|
+
throw new Error('Wallet restore key mismatch')
|
|
799
|
+
}
|
|
800
|
+
const sharedSecret = ml_kem1024.decapsulate(fromBase64(slot.kemCiphertext), kemKeys.secretKey)
|
|
801
|
+
const slotKey = deriveWalletSlotAesKey(sharedSecret, salt, slot.address, slot.challenge, args.envelope.accessManifestHash)
|
|
802
|
+
const keyDecipher = crypto.createDecipheriv('aes-256-gcm', slotKey, fromBase64(slot.nonce))
|
|
803
|
+
const payloadAad = walletPayloadAadFor(args.envelope)
|
|
804
|
+
keyDecipher.setAAD(walletSlotAadFor(slot, payloadAad))
|
|
805
|
+
keyDecipher.setAuthTag(fromBase64(slot.tag))
|
|
806
|
+
contentKey = Buffer.concat([
|
|
807
|
+
keyDecipher.update(fromBase64(slot.encryptedKey)),
|
|
808
|
+
keyDecipher.final(),
|
|
809
|
+
])
|
|
810
|
+
} catch {
|
|
811
|
+
throw new Error('Could not decrypt wallet restore snapshot key with the supplied wallet signature')
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
let decoded: unknown
|
|
815
|
+
try {
|
|
816
|
+
const payloadAad = walletPayloadAadFor(args.envelope)
|
|
817
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', contentKey, fromBase64(args.envelope.payloadNonce))
|
|
818
|
+
decipher.setAAD(payloadAad)
|
|
819
|
+
decipher.setAuthTag(fromBase64(args.envelope.payloadTag))
|
|
820
|
+
const plaintext = Buffer.concat([
|
|
821
|
+
decipher.update(fromBase64(args.envelope.payloadCiphertext)),
|
|
822
|
+
decipher.final(),
|
|
823
|
+
])
|
|
824
|
+
if (sha256Hex(plaintext) !== args.envelope.payloadHash) {
|
|
825
|
+
throw new Error('Wallet restore snapshot payload hash mismatch')
|
|
826
|
+
}
|
|
827
|
+
decoded = JSON.parse(plaintext.toString('utf8')) as unknown
|
|
828
|
+
} catch {
|
|
829
|
+
throw new Error('Could not decrypt wallet restore snapshot with the supplied wallet signature')
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const payload = normalizeContinuityPayload(decoded)
|
|
833
|
+
assertPayloadMatchesEnvelope(payload, args.envelope.ownerAddress, args.envelope.createdAt)
|
|
834
|
+
return payload
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function walletSlotForRestore(
|
|
838
|
+
envelope: WalletContinuitySnapshotEnvelope,
|
|
839
|
+
walletSignature: string,
|
|
840
|
+
currentOwnerAddress?: string,
|
|
841
|
+
): WalletContinuitySnapshotSlot {
|
|
842
|
+
if (currentOwnerAddress) {
|
|
843
|
+
const address = toChecksumAddress(currentOwnerAddress)
|
|
844
|
+
const slot = envelope.slots.find(item => item.address.toLowerCase() === address.toLowerCase())
|
|
845
|
+
if (!slot) throw new ContinuitySnapshotRestoreSlotMissingError(address)
|
|
846
|
+
return slot
|
|
847
|
+
}
|
|
848
|
+
for (const slot of envelope.slots) {
|
|
849
|
+
try {
|
|
850
|
+
const recovered = recoverAddressFromSignature(slot.challenge, walletSignature)
|
|
851
|
+
if (recovered.toLowerCase() === slot.address.toLowerCase()) return slot
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
throw new ContinuitySnapshotRestoreSlotMissingError('unknown')
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function transferSlotForCurrentOwner(
|
|
859
|
+
envelope: TransferContinuitySnapshotEnvelope,
|
|
860
|
+
currentOwner: string,
|
|
861
|
+
): TransferContinuitySnapshotSlot {
|
|
862
|
+
if (currentOwner.toLowerCase() === envelope.ownerAddress.toLowerCase()) return envelope.slots.owner
|
|
863
|
+
if (currentOwner.toLowerCase() === envelope.targetAddress.toLowerCase()) return envelope.slots.target
|
|
864
|
+
throw new ContinuityTransferSnapshotTargetMismatchError(envelope.ownerAddress, envelope.targetAddress, currentOwner)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function createTransferSlot(args: {
|
|
868
|
+
address: string
|
|
869
|
+
challenge: string
|
|
870
|
+
walletSignature: string
|
|
871
|
+
contentKey: Uint8Array
|
|
872
|
+
payloadAad: Buffer
|
|
873
|
+
}): TransferContinuitySnapshotSlot {
|
|
874
|
+
const address = toChecksumAddress(args.address)
|
|
875
|
+
const salt = crypto.randomBytes(32)
|
|
876
|
+
const nonce = crypto.randomBytes(12)
|
|
877
|
+
const slotKey = deriveTransferSlotKey(args.walletSignature, salt, address, args.challenge)
|
|
878
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', slotKey, nonce)
|
|
879
|
+
const slot: TransferContinuitySnapshotSlot = {
|
|
880
|
+
address,
|
|
881
|
+
challenge: args.challenge,
|
|
882
|
+
salt: toBase64(salt),
|
|
883
|
+
nonce: toBase64(nonce),
|
|
884
|
+
encryptedKey: '',
|
|
885
|
+
tag: '',
|
|
886
|
+
}
|
|
887
|
+
cipher.setAAD(transferSlotAadFor(slot, args.payloadAad))
|
|
888
|
+
const encryptedKey = Buffer.concat([cipher.update(args.contentKey), cipher.final()])
|
|
889
|
+
return {
|
|
890
|
+
...slot,
|
|
891
|
+
encryptedKey: toBase64(encryptedKey),
|
|
892
|
+
tag: toBase64(cipher.getAuthTag()),
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function createWalletSlot(args: {
|
|
897
|
+
accessKey: WalletContinuityRestoreAccessKey
|
|
898
|
+
contentKey: Uint8Array
|
|
899
|
+
payloadAad: Buffer
|
|
900
|
+
accessManifestHash: string
|
|
901
|
+
}): WalletContinuitySnapshotSlot {
|
|
902
|
+
const accessKey = normalizeWalletRestoreAccessKey(args.accessKey)
|
|
903
|
+
const publicKey = fromBase64(accessKey.kemPublicKey)
|
|
904
|
+
const kem = ml_kem1024.encapsulate(publicKey)
|
|
905
|
+
const salt = fromBase64(accessKey.salt)
|
|
906
|
+
const nonce = crypto.randomBytes(12)
|
|
907
|
+
const slotKey = deriveWalletSlotAesKey(kem.sharedSecret, salt, accessKey.address, accessKey.challenge, args.accessManifestHash)
|
|
908
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', slotKey, nonce)
|
|
909
|
+
const slot: WalletContinuitySnapshotSlot = {
|
|
910
|
+
address: accessKey.address,
|
|
911
|
+
challenge: accessKey.challenge,
|
|
912
|
+
salt: accessKey.salt,
|
|
913
|
+
kemPublicKey: accessKey.kemPublicKey,
|
|
914
|
+
kemCiphertext: toBase64(kem.cipherText),
|
|
915
|
+
nonce: toBase64(nonce),
|
|
916
|
+
encryptedKey: '',
|
|
917
|
+
tag: '',
|
|
918
|
+
}
|
|
919
|
+
cipher.setAAD(walletSlotAadFor(slot, args.payloadAad))
|
|
920
|
+
const encryptedKey = Buffer.concat([cipher.update(args.contentKey), cipher.final()])
|
|
921
|
+
return {
|
|
922
|
+
...slot,
|
|
923
|
+
encryptedKey: toBase64(encryptedKey),
|
|
924
|
+
tag: toBase64(cipher.getAuthTag()),
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
207
928
|
function normalizeContinuitySnapshotEnvelope(input: unknown): ContinuitySnapshotEnvelope {
|
|
208
|
-
if (!isContinuitySnapshotEnvelope(input)) throw new Error('
|
|
929
|
+
if (!isContinuitySnapshotEnvelope(input)) throw new Error('Invalid continuity snapshot envelope')
|
|
209
930
|
if (input.envelopeVersion !== CONTINUITY_SNAPSHOT_ENVELOPE_VERSION) {
|
|
210
|
-
throw new Error('
|
|
931
|
+
throw new Error('Unsupported continuity snapshot envelope version')
|
|
932
|
+
}
|
|
933
|
+
if (isWalletContinuitySnapshotEnvelope(input)) {
|
|
934
|
+
if (input.crypto.kem !== 'ML-KEM-1024' || input.crypto.aead !== 'AES-256-GCM' || input.crypto.decryptsWith !== 'wallet-signature-slots') {
|
|
935
|
+
throw new Error('Unsupported continuity snapshot crypto suite')
|
|
936
|
+
}
|
|
937
|
+
const ownerAddress = toChecksumAddress(input.ownerAddress)
|
|
938
|
+
const token = normalizeContinuitySnapshotToken(input.token)
|
|
939
|
+
const slots = input.slots.map(normalizeWalletSlot)
|
|
940
|
+
if (slots.length === 0) throw new Error('Continuity wallet snapshot needs at least one slot')
|
|
941
|
+
return {
|
|
942
|
+
...input,
|
|
943
|
+
ownerAddress,
|
|
944
|
+
token,
|
|
945
|
+
slots,
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (isTransferContinuitySnapshotEnvelope(input)) {
|
|
949
|
+
if (input.crypto.aead !== 'AES-256-GCM' || input.crypto.decryptsWith !== 'transfer-signature-slot') {
|
|
950
|
+
throw new Error('Unsupported continuity snapshot crypto suite')
|
|
951
|
+
}
|
|
952
|
+
const ownerAddress = toChecksumAddress(input.ownerAddress)
|
|
953
|
+
const targetAddress = toChecksumAddress(input.targetAddress)
|
|
954
|
+
return {
|
|
955
|
+
...input,
|
|
956
|
+
ownerAddress,
|
|
957
|
+
targetAddress,
|
|
958
|
+
token: normalizeContinuitySnapshotToken(input.token),
|
|
959
|
+
slots: {
|
|
960
|
+
owner: normalizeTransferSlot(input.slots.owner, ownerAddress),
|
|
961
|
+
target: normalizeTransferSlot(input.slots.target, targetAddress),
|
|
962
|
+
},
|
|
963
|
+
}
|
|
211
964
|
}
|
|
212
965
|
if (input.crypto.kem !== 'ML-KEM-1024' || input.crypto.aead !== 'AES-256-GCM') {
|
|
213
|
-
throw new Error('
|
|
966
|
+
throw new Error('Unsupported continuity snapshot crypto suite')
|
|
214
967
|
}
|
|
215
968
|
return {
|
|
216
969
|
...input,
|
|
@@ -220,28 +973,48 @@ function normalizeContinuitySnapshotEnvelope(input: unknown): ContinuitySnapshot
|
|
|
220
973
|
|
|
221
974
|
function isContinuitySnapshotEnvelope(input: unknown): input is ContinuitySnapshotEnvelope {
|
|
222
975
|
if (!input || typeof input !== 'object') return false
|
|
223
|
-
const obj = input as
|
|
224
|
-
|
|
976
|
+
const obj = input as Record<string, unknown> & { walletSignature?: unknown; crypto?: unknown }
|
|
977
|
+
const base = obj.version === 1
|
|
225
978
|
&& obj.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
|
|
226
979
|
&& typeof obj.ownerAddress === 'string'
|
|
227
980
|
&& typeof obj.createdAt === 'string'
|
|
228
|
-
&& typeof obj.challenge === 'string'
|
|
229
981
|
&& obj.walletSignature === undefined
|
|
982
|
+
&& !!obj.crypto
|
|
983
|
+
if (!base) return false
|
|
984
|
+
if (isWalletContinuitySnapshotEnvelope(input)) return true
|
|
985
|
+
if (isTransferContinuitySnapshotEnvelope(input)) return true
|
|
986
|
+
return typeof obj.challenge === 'string'
|
|
230
987
|
&& typeof obj.salt === 'string'
|
|
231
988
|
&& typeof obj.kemPublicKey === 'string'
|
|
232
989
|
&& typeof obj.kemCiphertext === 'string'
|
|
233
990
|
&& typeof obj.nonce === 'string'
|
|
234
991
|
&& typeof obj.ciphertext === 'string'
|
|
235
992
|
&& typeof obj.tag === 'string'
|
|
236
|
-
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function continuityPayloadFromArgs(args: {
|
|
996
|
+
ownerAddress: string
|
|
997
|
+
createdAt: string
|
|
998
|
+
payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & { createdAt?: string }
|
|
999
|
+
}): ContinuitySnapshotPayload {
|
|
1000
|
+
return {
|
|
1001
|
+
version: 1,
|
|
1002
|
+
ownerAddress: args.ownerAddress,
|
|
1003
|
+
createdAt: args.createdAt,
|
|
1004
|
+
...(args.payload.sequence !== undefined ? { sequence: args.payload.sequence } : {}),
|
|
1005
|
+
agent: normalizeAgentSnapshot(args.payload.agent),
|
|
1006
|
+
files: normalizeContinuityFiles(args.payload.files),
|
|
1007
|
+
transcript: normalizeTranscript(args.payload.transcript),
|
|
1008
|
+
state: normalizeState(args.payload.state),
|
|
1009
|
+
}
|
|
237
1010
|
}
|
|
238
1011
|
|
|
239
1012
|
function normalizeContinuityPayload(input: unknown): ContinuitySnapshotPayload {
|
|
240
|
-
if (!input || typeof input !== 'object') throw new Error('
|
|
1013
|
+
if (!input || typeof input !== 'object') throw new Error('Continuity snapshot payload is invalid')
|
|
241
1014
|
const obj = input as Partial<ContinuitySnapshotPayload>
|
|
242
|
-
if (obj.version !== 1) throw new Error('
|
|
243
|
-
if (typeof obj.ownerAddress !== 'string') throw new Error('
|
|
244
|
-
if (typeof obj.createdAt !== 'string') throw new Error('
|
|
1015
|
+
if (obj.version !== 1) throw new Error('Continuity snapshot payload version is invalid')
|
|
1016
|
+
if (typeof obj.ownerAddress !== 'string') throw new Error('Continuity snapshot owner is invalid')
|
|
1017
|
+
if (typeof obj.createdAt !== 'string') throw new Error('Continuity snapshot timestamp is invalid')
|
|
245
1018
|
return {
|
|
246
1019
|
version: 1,
|
|
247
1020
|
ownerAddress: toChecksumAddress(obj.ownerAddress),
|
|
@@ -270,7 +1043,7 @@ function normalizeAgentSnapshot(input: unknown): ContinuityAgentSnapshot {
|
|
|
270
1043
|
|
|
271
1044
|
function normalizeContinuityFiles(input: unknown): ContinuityFiles {
|
|
272
1045
|
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
273
|
-
throw new Error('
|
|
1046
|
+
throw new Error('Continuity snapshot files are invalid')
|
|
274
1047
|
}
|
|
275
1048
|
const obj = input as Partial<ContinuityFiles>
|
|
276
1049
|
if (typeof obj['SOUL.md'] !== 'string') throw new Error('SOUL.md is missing from continuity snapshot')
|
|
@@ -300,10 +1073,122 @@ function normalizeState(input: unknown): Record<string, unknown> {
|
|
|
300
1073
|
return input as Record<string, unknown>
|
|
301
1074
|
}
|
|
302
1075
|
|
|
1076
|
+
function normalizeContinuitySnapshotToken(input: unknown): ContinuitySnapshotToken {
|
|
1077
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
1078
|
+
throw new Error('Continuity snapshot token is invalid')
|
|
1079
|
+
}
|
|
1080
|
+
const obj = input as Partial<ContinuitySnapshotToken>
|
|
1081
|
+
if (typeof obj.chainId !== 'number' || !Number.isSafeInteger(obj.chainId) || obj.chainId <= 0) {
|
|
1082
|
+
throw new Error('Continuity snapshot token chain is invalid')
|
|
1083
|
+
}
|
|
1084
|
+
if (typeof obj.identityRegistryAddress !== 'string') {
|
|
1085
|
+
throw new Error('Continuity snapshot token registry is invalid')
|
|
1086
|
+
}
|
|
1087
|
+
if (typeof obj.agentId !== 'string' || !/^\d+$/.test(obj.agentId)) {
|
|
1088
|
+
throw new Error('Continuity snapshot token id is invalid')
|
|
1089
|
+
}
|
|
1090
|
+
return {
|
|
1091
|
+
chainId: obj.chainId,
|
|
1092
|
+
identityRegistryAddress: toChecksumAddress(obj.identityRegistryAddress),
|
|
1093
|
+
agentId: obj.agentId,
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function normalizeTransferSlot(input: unknown, expectedAddress: string): TransferContinuitySnapshotSlot {
|
|
1098
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
1099
|
+
throw new Error('Continuity transfer slot is invalid')
|
|
1100
|
+
}
|
|
1101
|
+
const obj = input as Partial<TransferContinuitySnapshotSlot>
|
|
1102
|
+
if (typeof obj.address !== 'string') throw new Error('Continuity transfer slot address is invalid')
|
|
1103
|
+
const address = toChecksumAddress(obj.address)
|
|
1104
|
+
if (address.toLowerCase() !== expectedAddress.toLowerCase()) {
|
|
1105
|
+
throw new Error('Continuity transfer slot address mismatch')
|
|
1106
|
+
}
|
|
1107
|
+
if (typeof obj.challenge !== 'string') throw new Error('Continuity transfer slot challenge is invalid')
|
|
1108
|
+
if (typeof obj.salt !== 'string') throw new Error('Continuity transfer slot salt is invalid')
|
|
1109
|
+
if (typeof obj.nonce !== 'string') throw new Error('Continuity transfer slot nonce is invalid')
|
|
1110
|
+
if (typeof obj.encryptedKey !== 'string') throw new Error('Continuity transfer slot key is invalid')
|
|
1111
|
+
if (typeof obj.tag !== 'string') throw new Error('Continuity transfer slot tag is invalid')
|
|
1112
|
+
return {
|
|
1113
|
+
address,
|
|
1114
|
+
challenge: obj.challenge,
|
|
1115
|
+
salt: obj.salt,
|
|
1116
|
+
nonce: obj.nonce,
|
|
1117
|
+
encryptedKey: obj.encryptedKey,
|
|
1118
|
+
tag: obj.tag,
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function normalizeWalletRestoreAccessKeys(input: unknown): WalletContinuityRestoreAccessKey[] {
|
|
1123
|
+
if (!Array.isArray(input)) return []
|
|
1124
|
+
const out: WalletContinuityRestoreAccessKey[] = []
|
|
1125
|
+
const seen = new Set<string>()
|
|
1126
|
+
for (const item of input) {
|
|
1127
|
+
const key = normalizeWalletRestoreAccessKey(item)
|
|
1128
|
+
const dedupe = key.address.toLowerCase()
|
|
1129
|
+
if (seen.has(dedupe)) continue
|
|
1130
|
+
seen.add(dedupe)
|
|
1131
|
+
out.push(key)
|
|
1132
|
+
}
|
|
1133
|
+
return out.sort((a, b) => a.address.toLowerCase().localeCompare(b.address.toLowerCase()))
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function normalizeWalletRestoreAccessKey(input: unknown): WalletContinuityRestoreAccessKey {
|
|
1137
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
1138
|
+
throw new Error('Wallet restore access key is invalid')
|
|
1139
|
+
}
|
|
1140
|
+
const obj = input as Partial<WalletContinuityRestoreAccessKey>
|
|
1141
|
+
if (typeof obj.address !== 'string') throw new Error('Wallet restore access address is invalid')
|
|
1142
|
+
if (typeof obj.challenge !== 'string') throw new Error('Wallet restore access challenge is invalid')
|
|
1143
|
+
if (typeof obj.salt !== 'string') throw new Error('Wallet restore access salt is invalid')
|
|
1144
|
+
if (typeof obj.kemPublicKey !== 'string') throw new Error('Wallet restore access public key is invalid')
|
|
1145
|
+
return {
|
|
1146
|
+
address: toChecksumAddress(obj.address),
|
|
1147
|
+
challenge: obj.challenge,
|
|
1148
|
+
salt: obj.salt,
|
|
1149
|
+
kemPublicKey: obj.kemPublicKey,
|
|
1150
|
+
...(typeof obj.createdAt === 'string' ? { createdAt: obj.createdAt } : {}),
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function normalizeWalletSlot(input: unknown): WalletContinuitySnapshotSlot {
|
|
1155
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
1156
|
+
throw new Error('Continuity wallet slot is invalid')
|
|
1157
|
+
}
|
|
1158
|
+
const obj = input as Partial<WalletContinuitySnapshotSlot>
|
|
1159
|
+
if (typeof obj.address !== 'string') throw new Error('Continuity wallet slot address is invalid')
|
|
1160
|
+
if (typeof obj.challenge !== 'string') throw new Error('Continuity wallet slot challenge is invalid')
|
|
1161
|
+
if (typeof obj.salt !== 'string') throw new Error('Continuity wallet slot salt is invalid')
|
|
1162
|
+
if (typeof obj.kemPublicKey !== 'string') throw new Error('Continuity wallet slot public key is invalid')
|
|
1163
|
+
if (typeof obj.kemCiphertext !== 'string') throw new Error('Continuity wallet slot ciphertext is invalid')
|
|
1164
|
+
if (typeof obj.nonce !== 'string') throw new Error('Continuity wallet slot nonce is invalid')
|
|
1165
|
+
if (typeof obj.encryptedKey !== 'string') throw new Error('Continuity wallet slot key is invalid')
|
|
1166
|
+
if (typeof obj.tag !== 'string') throw new Error('Continuity wallet slot tag is invalid')
|
|
1167
|
+
return {
|
|
1168
|
+
address: toChecksumAddress(obj.address),
|
|
1169
|
+
challenge: obj.challenge,
|
|
1170
|
+
salt: obj.salt,
|
|
1171
|
+
kemPublicKey: obj.kemPublicKey,
|
|
1172
|
+
kemCiphertext: obj.kemCiphertext,
|
|
1173
|
+
nonce: obj.nonce,
|
|
1174
|
+
encryptedKey: obj.encryptedKey,
|
|
1175
|
+
tag: obj.tag,
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function assertPayloadMatchesEnvelope(payload: ContinuitySnapshotPayload, ownerAddress: string, createdAt: string): void {
|
|
1180
|
+
if (payload.ownerAddress.toLowerCase() !== ownerAddress.toLowerCase()) {
|
|
1181
|
+
throw new Error('Continuity snapshot owner mismatch')
|
|
1182
|
+
}
|
|
1183
|
+
if (payload.createdAt !== createdAt) {
|
|
1184
|
+
throw new Error('Continuity snapshot timestamp mismatch')
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
303
1188
|
function assertSignatureForAddress(challenge: string, signature: string, address: string): void {
|
|
304
1189
|
const recovered = recoverAddressFromSignature(challenge, signature)
|
|
305
1190
|
if (recovered.toLowerCase() !== address.toLowerCase()) {
|
|
306
|
-
throw new Error('
|
|
1191
|
+
throw new Error('Wallet signature does not match continuity snapshot owner')
|
|
307
1192
|
}
|
|
308
1193
|
}
|
|
309
1194
|
|
|
@@ -334,14 +1219,137 @@ function deriveContinuityAesKey(
|
|
|
334
1219
|
))
|
|
335
1220
|
}
|
|
336
1221
|
|
|
1222
|
+
function deriveTransferSlotKey(walletSignature: string, salt: Uint8Array, address: string, challenge: string): Buffer {
|
|
1223
|
+
return Buffer.from(hkdf(
|
|
1224
|
+
Buffer.from(walletSignature, 'utf8'),
|
|
1225
|
+
salt,
|
|
1226
|
+
`ethagent:${CONTINUITY_SNAPSHOT_ENVELOPE_VERSION}:transfer-slot:${address.toLowerCase()}:${sha256Hex(challenge)}`,
|
|
1227
|
+
32,
|
|
1228
|
+
))
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function deriveWalletRestoreKemSeed(walletSignature: string, salt: Uint8Array, address: string, challenge: string): Uint8Array {
|
|
1232
|
+
return hkdf(
|
|
1233
|
+
Buffer.from(walletSignature, 'utf8'),
|
|
1234
|
+
salt,
|
|
1235
|
+
`ethagent:${CONTINUITY_SNAPSHOT_ENVELOPE_VERSION}:wallet-restore-kem:${address.toLowerCase()}:${sha256Hex(challenge)}`,
|
|
1236
|
+
64,
|
|
1237
|
+
)
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function deriveWalletSlotAesKey(
|
|
1241
|
+
sharedSecret: Uint8Array,
|
|
1242
|
+
salt: Uint8Array,
|
|
1243
|
+
address: string,
|
|
1244
|
+
challenge: string,
|
|
1245
|
+
accessManifestHash: string,
|
|
1246
|
+
): Buffer {
|
|
1247
|
+
return Buffer.from(hkdf(
|
|
1248
|
+
sharedSecret,
|
|
1249
|
+
salt,
|
|
1250
|
+
`ethagent:${CONTINUITY_SNAPSHOT_ENVELOPE_VERSION}:wallet-slot:${address.toLowerCase()}:${sha256Hex(challenge)}:${accessManifestHash}`,
|
|
1251
|
+
32,
|
|
1252
|
+
))
|
|
1253
|
+
}
|
|
1254
|
+
|
|
337
1255
|
function continuityAadFor(ownerAddress: string, createdAt: string): Buffer {
|
|
338
1256
|
return Buffer.from(`${CONTINUITY_SNAPSHOT_ENVELOPE_VERSION}\n${ownerAddress.toLowerCase()}\n${createdAt}`, 'utf8')
|
|
339
1257
|
}
|
|
340
1258
|
|
|
1259
|
+
function walletAccessManifestHash(args: {
|
|
1260
|
+
ownerAddress: string
|
|
1261
|
+
token: ContinuitySnapshotToken
|
|
1262
|
+
accessEpoch: number
|
|
1263
|
+
accessKeys: WalletContinuityRestoreAccessKey[]
|
|
1264
|
+
}): string {
|
|
1265
|
+
const token = normalizeContinuitySnapshotToken(args.token)
|
|
1266
|
+
const accessKeys = normalizeWalletRestoreAccessKeys(args.accessKeys)
|
|
1267
|
+
return sha256Hex(JSON.stringify({
|
|
1268
|
+
ownerAddress: toChecksumAddress(args.ownerAddress).toLowerCase(),
|
|
1269
|
+
token: {
|
|
1270
|
+
chainId: token.chainId,
|
|
1271
|
+
identityRegistryAddress: token.identityRegistryAddress.toLowerCase(),
|
|
1272
|
+
agentId: token.agentId,
|
|
1273
|
+
},
|
|
1274
|
+
accessEpoch: args.accessEpoch,
|
|
1275
|
+
wallets: accessKeys.map(key => ({
|
|
1276
|
+
address: key.address.toLowerCase(),
|
|
1277
|
+
challengeHash: sha256Hex(key.challenge),
|
|
1278
|
+
salt: key.salt,
|
|
1279
|
+
kemPublicKey: key.kemPublicKey,
|
|
1280
|
+
})),
|
|
1281
|
+
}))
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function walletPayloadAadFor(args: {
|
|
1285
|
+
ownerAddress: string
|
|
1286
|
+
createdAt: string
|
|
1287
|
+
token: ContinuitySnapshotToken
|
|
1288
|
+
accessEpoch: number
|
|
1289
|
+
accessManifestHash: string
|
|
1290
|
+
}): Buffer {
|
|
1291
|
+
const token = normalizeContinuitySnapshotToken(args.token)
|
|
1292
|
+
return Buffer.from([
|
|
1293
|
+
CONTINUITY_SNAPSHOT_ENVELOPE_VERSION,
|
|
1294
|
+
'wallet-signature-slots',
|
|
1295
|
+
args.ownerAddress.toLowerCase(),
|
|
1296
|
+
args.createdAt,
|
|
1297
|
+
String(token.chainId),
|
|
1298
|
+
token.identityRegistryAddress.toLowerCase(),
|
|
1299
|
+
token.agentId,
|
|
1300
|
+
String(args.accessEpoch),
|
|
1301
|
+
args.accessManifestHash,
|
|
1302
|
+
].join('\n'), 'utf8')
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function transferPayloadAadFor(args: {
|
|
1306
|
+
ownerAddress: string
|
|
1307
|
+
targetAddress: string
|
|
1308
|
+
createdAt: string
|
|
1309
|
+
token: ContinuitySnapshotToken
|
|
1310
|
+
}): Buffer {
|
|
1311
|
+
const token = normalizeContinuitySnapshotToken(args.token)
|
|
1312
|
+
return Buffer.from([
|
|
1313
|
+
CONTINUITY_SNAPSHOT_ENVELOPE_VERSION,
|
|
1314
|
+
'transfer',
|
|
1315
|
+
args.ownerAddress.toLowerCase(),
|
|
1316
|
+
args.targetAddress.toLowerCase(),
|
|
1317
|
+
args.createdAt,
|
|
1318
|
+
String(token.chainId),
|
|
1319
|
+
token.identityRegistryAddress.toLowerCase(),
|
|
1320
|
+
token.agentId,
|
|
1321
|
+
].join('\n'), 'utf8')
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function walletSlotAadFor(slot: Pick<WalletContinuitySnapshotSlot, 'address' | 'challenge' | 'kemPublicKey' | 'kemCiphertext'>, payloadAad: Buffer): Buffer {
|
|
1325
|
+
return Buffer.concat([
|
|
1326
|
+
payloadAad,
|
|
1327
|
+
Buffer.from([
|
|
1328
|
+
'',
|
|
1329
|
+
'wallet-slot',
|
|
1330
|
+
slot.address.toLowerCase(),
|
|
1331
|
+
sha256Hex(slot.challenge),
|
|
1332
|
+
slot.kemPublicKey,
|
|
1333
|
+
slot.kemCiphertext,
|
|
1334
|
+
].join('\n'), 'utf8'),
|
|
1335
|
+
])
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function transferSlotAadFor(slot: Pick<TransferContinuitySnapshotSlot, 'address' | 'challenge'>, payloadAad: Buffer): Buffer {
|
|
1339
|
+
return Buffer.concat([
|
|
1340
|
+
payloadAad,
|
|
1341
|
+
Buffer.from(`\nslot\n${slot.address.toLowerCase()}\n${sha256Hex(slot.challenge)}`, 'utf8'),
|
|
1342
|
+
])
|
|
1343
|
+
}
|
|
1344
|
+
|
|
341
1345
|
function hkdf(ikm: Uint8Array, salt: Uint8Array, info: string, length: number): Uint8Array {
|
|
342
1346
|
return new Uint8Array(crypto.hkdfSync('sha256', ikm, salt, Buffer.from(info, 'utf8'), length))
|
|
343
1347
|
}
|
|
344
1348
|
|
|
1349
|
+
function sha256Hex(value: string | Uint8Array): string {
|
|
1350
|
+
return crypto.createHash('sha256').update(value).digest('hex')
|
|
1351
|
+
}
|
|
1352
|
+
|
|
345
1353
|
function toBase64(bytes: Uint8Array): string {
|
|
346
1354
|
return Buffer.from(bytes).toString('base64')
|
|
347
1355
|
}
|