@snaha/swarm-id 0.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/README.md +431 -0
- package/dist/chunk/bmt.d.ts +17 -0
- package/dist/chunk/bmt.d.ts.map +1 -0
- package/dist/chunk/cac.d.ts +18 -0
- package/dist/chunk/cac.d.ts.map +1 -0
- package/dist/chunk/constants.d.ts +10 -0
- package/dist/chunk/constants.d.ts.map +1 -0
- package/dist/chunk/encrypted-cac.d.ts +48 -0
- package/dist/chunk/encrypted-cac.d.ts.map +1 -0
- package/dist/chunk/encryption.d.ts +86 -0
- package/dist/chunk/encryption.d.ts.map +1 -0
- package/dist/chunk/index.d.ts +6 -0
- package/dist/chunk/index.d.ts.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/proxy/act/act.d.ts +78 -0
- package/dist/proxy/act/act.d.ts.map +1 -0
- package/dist/proxy/act/crypto.d.ts +44 -0
- package/dist/proxy/act/crypto.d.ts.map +1 -0
- package/dist/proxy/act/grantee-list.d.ts +82 -0
- package/dist/proxy/act/grantee-list.d.ts.map +1 -0
- package/dist/proxy/act/history.d.ts +183 -0
- package/dist/proxy/act/history.d.ts.map +1 -0
- package/dist/proxy/act/index.d.ts +104 -0
- package/dist/proxy/act/index.d.ts.map +1 -0
- package/dist/proxy/chunking-encrypted.d.ts +14 -0
- package/dist/proxy/chunking-encrypted.d.ts.map +1 -0
- package/dist/proxy/chunking.d.ts +15 -0
- package/dist/proxy/chunking.d.ts.map +1 -0
- package/dist/proxy/download-data.d.ts +16 -0
- package/dist/proxy/download-data.d.ts.map +1 -0
- package/dist/proxy/feed-manifest.d.ts +62 -0
- package/dist/proxy/feed-manifest.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/async-finder.d.ts +77 -0
- package/dist/proxy/feeds/epochs/async-finder.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/epoch.d.ts +88 -0
- package/dist/proxy/feeds/epochs/epoch.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/finder.d.ts +67 -0
- package/dist/proxy/feeds/epochs/finder.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/index.d.ts +35 -0
- package/dist/proxy/feeds/epochs/index.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/test-utils.d.ts +93 -0
- package/dist/proxy/feeds/epochs/test-utils.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/types.d.ts +109 -0
- package/dist/proxy/feeds/epochs/types.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/updater.d.ts +68 -0
- package/dist/proxy/feeds/epochs/updater.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/utils.d.ts +22 -0
- package/dist/proxy/feeds/epochs/utils.d.ts.map +1 -0
- package/dist/proxy/feeds/index.d.ts +5 -0
- package/dist/proxy/feeds/index.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/async-finder.d.ts +14 -0
- package/dist/proxy/feeds/sequence/async-finder.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/finder.d.ts +17 -0
- package/dist/proxy/feeds/sequence/finder.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/index.d.ts +23 -0
- package/dist/proxy/feeds/sequence/index.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/types.d.ts +80 -0
- package/dist/proxy/feeds/sequence/types.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/updater.d.ts +26 -0
- package/dist/proxy/feeds/sequence/updater.d.ts.map +1 -0
- package/dist/proxy/index.d.ts +6 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/manifest-builder.d.ts +183 -0
- package/dist/proxy/manifest-builder.d.ts.map +1 -0
- package/dist/proxy/mantaray-encrypted.d.ts +27 -0
- package/dist/proxy/mantaray-encrypted.d.ts.map +1 -0
- package/dist/proxy/mantaray.d.ts +26 -0
- package/dist/proxy/mantaray.d.ts.map +1 -0
- package/dist/proxy/types.d.ts +29 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/proxy/upload-data.d.ts +17 -0
- package/dist/proxy/upload-data.d.ts.map +1 -0
- package/dist/proxy/upload-encrypted-data.d.ts +103 -0
- package/dist/proxy/upload-encrypted-data.d.ts.map +1 -0
- package/dist/schemas.d.ts +240 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/storage/debounced-uploader.d.ts +62 -0
- package/dist/storage/debounced-uploader.d.ts.map +1 -0
- package/dist/storage/utilization-store.d.ts +108 -0
- package/dist/storage/utilization-store.d.ts.map +1 -0
- package/dist/swarm-id-auth.d.ts +74 -0
- package/dist/swarm-id-auth.d.ts.map +1 -0
- package/dist/swarm-id-auth.js +2 -0
- package/dist/swarm-id-auth.js.map +1 -0
- package/dist/swarm-id-client.d.ts +878 -0
- package/dist/swarm-id-client.d.ts.map +1 -0
- package/dist/swarm-id-client.js +2 -0
- package/dist/swarm-id-client.js.map +1 -0
- package/dist/swarm-id-proxy.d.ts +236 -0
- package/dist/swarm-id-proxy.d.ts.map +1 -0
- package/dist/swarm-id-proxy.js +2 -0
- package/dist/swarm-id-proxy.js.map +1 -0
- package/dist/swarm-id.esm.js +2 -0
- package/dist/swarm-id.esm.js.map +1 -0
- package/dist/swarm-id.umd.js +2 -0
- package/dist/swarm-id.umd.js.map +1 -0
- package/dist/sync/index.d.ts +9 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/key-derivation.d.ts +25 -0
- package/dist/sync/key-derivation.d.ts.map +1 -0
- package/dist/sync/restore-account.d.ts +28 -0
- package/dist/sync/restore-account.d.ts.map +1 -0
- package/dist/sync/serialization.d.ts +16 -0
- package/dist/sync/serialization.d.ts.map +1 -0
- package/dist/sync/store-interfaces.d.ts +53 -0
- package/dist/sync/store-interfaces.d.ts.map +1 -0
- package/dist/sync/sync-account.d.ts +44 -0
- package/dist/sync/sync-account.d.ts.map +1 -0
- package/dist/sync/types.d.ts +13 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/test-fixtures.d.ts +17 -0
- package/dist/test-fixtures.d.ts.map +1 -0
- package/dist/types-BD_VkNn0.js +2 -0
- package/dist/types-BD_VkNn0.js.map +1 -0
- package/dist/types-lJCaT-50.js +2 -0
- package/dist/types-lJCaT-50.js.map +1 -0
- package/dist/types.d.ts +2157 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/account-payload.d.ts +94 -0
- package/dist/utils/account-payload.d.ts.map +1 -0
- package/dist/utils/account-state-snapshot.d.ts +38 -0
- package/dist/utils/account-state-snapshot.d.ts.map +1 -0
- package/dist/utils/backup-encryption.d.ts +127 -0
- package/dist/utils/backup-encryption.d.ts.map +1 -0
- package/dist/utils/batch-utilization.d.ts +432 -0
- package/dist/utils/batch-utilization.d.ts.map +1 -0
- package/dist/utils/constants.d.ts +11 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/hex.d.ts +17 -0
- package/dist/utils/hex.d.ts.map +1 -0
- package/dist/utils/key-derivation.d.ts +92 -0
- package/dist/utils/key-derivation.d.ts.map +1 -0
- package/dist/utils/storage-managers.d.ts +65 -0
- package/dist/utils/storage-managers.d.ts.map +1 -0
- package/dist/utils/swarm-id-export.d.ts +24 -0
- package/dist/utils/swarm-id-export.d.ts.map +1 -0
- package/dist/utils/ttl.d.ts +49 -0
- package/dist/utils/ttl.d.ts.map +1 -0
- package/dist/utils/url.d.ts +41 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/versioned-storage.d.ts +131 -0
- package/dist/utils/versioned-storage.d.ts.map +1 -0
- package/package.json +78 -0
- package/src/chunk/bmt.test.ts +217 -0
- package/src/chunk/bmt.ts +57 -0
- package/src/chunk/cac.test.ts +214 -0
- package/src/chunk/cac.ts +65 -0
- package/src/chunk/constants.ts +18 -0
- package/src/chunk/encrypted-cac.test.ts +385 -0
- package/src/chunk/encrypted-cac.ts +131 -0
- package/src/chunk/encryption.test.ts +352 -0
- package/src/chunk/encryption.ts +300 -0
- package/src/chunk/index.ts +47 -0
- package/src/index.ts +430 -0
- package/src/proxy/act/act.test.ts +278 -0
- package/src/proxy/act/act.ts +158 -0
- package/src/proxy/act/bee-compat.test.ts +948 -0
- package/src/proxy/act/crypto.test.ts +436 -0
- package/src/proxy/act/crypto.ts +376 -0
- package/src/proxy/act/grantee-list.test.ts +393 -0
- package/src/proxy/act/grantee-list.ts +239 -0
- package/src/proxy/act/history.test.ts +360 -0
- package/src/proxy/act/history.ts +413 -0
- package/src/proxy/act/index.test.ts +748 -0
- package/src/proxy/act/index.ts +853 -0
- package/src/proxy/chunking-encrypted.ts +95 -0
- package/src/proxy/chunking.ts +65 -0
- package/src/proxy/download-data.ts +448 -0
- package/src/proxy/feed-manifest.ts +174 -0
- package/src/proxy/feeds/epochs/async-finder.ts +372 -0
- package/src/proxy/feeds/epochs/epoch.test.ts +249 -0
- package/src/proxy/feeds/epochs/epoch.ts +181 -0
- package/src/proxy/feeds/epochs/finder.ts +282 -0
- package/src/proxy/feeds/epochs/index.ts +73 -0
- package/src/proxy/feeds/epochs/integration.test.ts +1336 -0
- package/src/proxy/feeds/epochs/test-utils.ts +274 -0
- package/src/proxy/feeds/epochs/types.ts +128 -0
- package/src/proxy/feeds/epochs/updater.ts +192 -0
- package/src/proxy/feeds/epochs/utils.ts +62 -0
- package/src/proxy/feeds/index.ts +5 -0
- package/src/proxy/feeds/sequence/async-finder.ts +31 -0
- package/src/proxy/feeds/sequence/finder.ts +73 -0
- package/src/proxy/feeds/sequence/index.ts +54 -0
- package/src/proxy/feeds/sequence/integration.test.ts +966 -0
- package/src/proxy/feeds/sequence/types.ts +103 -0
- package/src/proxy/feeds/sequence/updater.ts +71 -0
- package/src/proxy/index.ts +5 -0
- package/src/proxy/manifest-builder.test.ts +427 -0
- package/src/proxy/manifest-builder.ts +679 -0
- package/src/proxy/mantaray-encrypted.ts +78 -0
- package/src/proxy/mantaray.ts +104 -0
- package/src/proxy/types.ts +32 -0
- package/src/proxy/upload-data.ts +189 -0
- package/src/proxy/upload-encrypted-data.ts +658 -0
- package/src/schemas.ts +299 -0
- package/src/storage/debounced-uploader.ts +192 -0
- package/src/storage/utilization-store.ts +397 -0
- package/src/swarm-id-client.test.ts +99 -0
- package/src/swarm-id-client.ts +3095 -0
- package/src/swarm-id-proxy.ts +3891 -0
- package/src/sync/index.ts +28 -0
- package/src/sync/restore-account.ts +90 -0
- package/src/sync/serialization.ts +39 -0
- package/src/sync/store-interfaces.ts +62 -0
- package/src/sync/sync-account.test.ts +302 -0
- package/src/sync/sync-account.ts +396 -0
- package/src/sync/types.ts +11 -0
- package/src/test-fixtures.ts +109 -0
- package/src/types.ts +1651 -0
- package/src/utils/account-state-snapshot.test.ts +595 -0
- package/src/utils/account-state-snapshot.ts +94 -0
- package/src/utils/backup-encryption.test.ts +442 -0
- package/src/utils/backup-encryption.ts +352 -0
- package/src/utils/batch-utilization.ts +1309 -0
- package/src/utils/constants.ts +20 -0
- package/src/utils/hex.ts +27 -0
- package/src/utils/key-derivation.ts +197 -0
- package/src/utils/storage-managers.ts +365 -0
- package/src/utils/ttl.ts +129 -0
- package/src/utils/url.test.ts +136 -0
- package/src/utils/url.ts +71 -0
- package/src/utils/versioned-storage.ts +323 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Account
|
|
3
|
+
*
|
|
4
|
+
* Factory function that creates a sync function for syncing account state
|
|
5
|
+
* to Swarm. This integrates store access, key derivation, and utilization
|
|
6
|
+
* tracking.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Bee,
|
|
11
|
+
PrivateKey,
|
|
12
|
+
BatchId,
|
|
13
|
+
EthAddress,
|
|
14
|
+
Topic,
|
|
15
|
+
Reference,
|
|
16
|
+
type Chunk,
|
|
17
|
+
} from "@ethersphere/bee-js"
|
|
18
|
+
import { deriveSecret, hexToUint8Array } from "../utils/key-derivation"
|
|
19
|
+
import {
|
|
20
|
+
updateAfterWrite,
|
|
21
|
+
saveUtilizationState,
|
|
22
|
+
calculateUtilization,
|
|
23
|
+
} from "../utils/batch-utilization"
|
|
24
|
+
import type { UtilizationStoreDB } from "../storage/utilization-store"
|
|
25
|
+
import type { DebouncedUtilizationUploader } from "../storage/debounced-uploader"
|
|
26
|
+
import type {
|
|
27
|
+
AccountsStoreInterface,
|
|
28
|
+
IdentitiesStoreInterface,
|
|
29
|
+
ConnectedAppsStoreInterface,
|
|
30
|
+
PostageStampsStoreInterface,
|
|
31
|
+
} from "./store-interfaces"
|
|
32
|
+
import { BasicEpochUpdater } from "../proxy/feeds/epochs"
|
|
33
|
+
import { uploadEncryptedDataWithSigning } from "../proxy/upload-encrypted-data"
|
|
34
|
+
import { serializeAccountState } from "./serialization"
|
|
35
|
+
import type { SyncResult } from "./types"
|
|
36
|
+
import type { AccountStateSnapshot } from "../utils/account-state-snapshot"
|
|
37
|
+
import type { PostageStamp } from "../schemas"
|
|
38
|
+
|
|
39
|
+
// Timeout for utilization upload in milliseconds
|
|
40
|
+
const UTILIZATION_UPLOAD_TIMEOUT_MS = 30000
|
|
41
|
+
|
|
42
|
+
// Topic prefix for sync feeds
|
|
43
|
+
export const ACCOUNT_SYNC_TOPIC_PREFIX = "swarm-id-backup-v1:account"
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Options for creating a sync account function
|
|
47
|
+
*/
|
|
48
|
+
export interface SyncAccountOptions {
|
|
49
|
+
/** Bee client for Swarm operations */
|
|
50
|
+
bee: Bee
|
|
51
|
+
|
|
52
|
+
/** Store providing account data */
|
|
53
|
+
accountsStore: AccountsStoreInterface
|
|
54
|
+
|
|
55
|
+
/** Store providing identity data */
|
|
56
|
+
identitiesStore: IdentitiesStoreInterface
|
|
57
|
+
|
|
58
|
+
/** Store providing connected app data */
|
|
59
|
+
connectedAppsStore: ConnectedAppsStoreInterface
|
|
60
|
+
|
|
61
|
+
/** Store providing postage stamp data */
|
|
62
|
+
postageStampsStore: PostageStampsStoreInterface
|
|
63
|
+
|
|
64
|
+
/** Utilization store for browser-based utilization tracking */
|
|
65
|
+
utilizationStore: UtilizationStoreDB
|
|
66
|
+
|
|
67
|
+
/** Debounced uploader for batch utilization state */
|
|
68
|
+
utilizationUploader: DebouncedUtilizationUploader
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sync account function type
|
|
73
|
+
*/
|
|
74
|
+
export type SyncAccountFunction = (
|
|
75
|
+
accountId: string,
|
|
76
|
+
) => Promise<SyncResult | undefined>
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convert chunk addresses to Chunk objects for utilization tracking
|
|
80
|
+
*
|
|
81
|
+
* Creates minimal chunk objects with just the address property
|
|
82
|
+
* needed for bucket calculation. We don't need actual chunk data
|
|
83
|
+
* since we're only tracking which buckets/slots were used.
|
|
84
|
+
*/
|
|
85
|
+
function createChunksFromAddresses(addresses: Uint8Array[]): Chunk[] {
|
|
86
|
+
return addresses.map((address) => {
|
|
87
|
+
return {
|
|
88
|
+
address: {
|
|
89
|
+
toUint8Array: () => address,
|
|
90
|
+
toHex: () =>
|
|
91
|
+
Array.from(address)
|
|
92
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
93
|
+
.join(""),
|
|
94
|
+
},
|
|
95
|
+
data: new Uint8Array(0), // Not used for utilization tracking
|
|
96
|
+
} as Chunk
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create a sync account function with dependency-injected stores
|
|
102
|
+
*
|
|
103
|
+
* @param options - Configuration options including stores and optional utilization tracking
|
|
104
|
+
* @returns Function that syncs an account to Swarm
|
|
105
|
+
*/
|
|
106
|
+
export function createSyncAccount(
|
|
107
|
+
options: SyncAccountOptions,
|
|
108
|
+
): SyncAccountFunction {
|
|
109
|
+
const {
|
|
110
|
+
bee,
|
|
111
|
+
accountsStore,
|
|
112
|
+
identitiesStore,
|
|
113
|
+
connectedAppsStore,
|
|
114
|
+
postageStampsStore,
|
|
115
|
+
utilizationStore,
|
|
116
|
+
utilizationUploader,
|
|
117
|
+
} = options
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Handle utilization tracking after chunk upload
|
|
121
|
+
*/
|
|
122
|
+
async function handleUtilizationUpdate(
|
|
123
|
+
accountId: string,
|
|
124
|
+
chunkAddresses: Uint8Array[],
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
// Get account
|
|
127
|
+
const account = accountsStore.getAccount(new EthAddress(accountId))
|
|
128
|
+
if (!account) {
|
|
129
|
+
console.warn("[SyncCoordinator] Account not found for utilization update")
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Resolve default stamp
|
|
134
|
+
const defaultStamp =
|
|
135
|
+
account.defaultPostageStampBatchID ??
|
|
136
|
+
identitiesStore.getIdentitiesByAccount(account.id)[0]
|
|
137
|
+
?.defaultPostageStampBatchID
|
|
138
|
+
|
|
139
|
+
if (!defaultStamp) {
|
|
140
|
+
console.warn("[SyncCoordinator] No default stamp, skipping utilization")
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const batchID = new BatchId(defaultStamp)
|
|
145
|
+
const stamp = postageStampsStore.getStamp(batchID)
|
|
146
|
+
|
|
147
|
+
if (!stamp) {
|
|
148
|
+
console.warn("[SyncCoordinator] Stamp not found, skipping utilization")
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Convert chunk addresses to Chunks
|
|
153
|
+
const chunks = createChunksFromAddresses(chunkAddresses)
|
|
154
|
+
|
|
155
|
+
// Derive owner address from backup key
|
|
156
|
+
const backupKeyHex = await deriveSecret(
|
|
157
|
+
account.swarmEncryptionKey,
|
|
158
|
+
"backup-key",
|
|
159
|
+
)
|
|
160
|
+
const backupKey = new PrivateKey(backupKeyHex)
|
|
161
|
+
const owner = backupKey.publicKey().address()
|
|
162
|
+
|
|
163
|
+
// Update utilization state
|
|
164
|
+
const { state: utilizationState, tracker } = await updateAfterWrite(
|
|
165
|
+
batchID,
|
|
166
|
+
chunks,
|
|
167
|
+
stamp.depth,
|
|
168
|
+
{
|
|
169
|
+
bee,
|
|
170
|
+
owner,
|
|
171
|
+
encryptionKey: hexToUint8Array(account.swarmEncryptionKey),
|
|
172
|
+
cache: utilizationStore,
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
// Calculate new utilization fraction
|
|
177
|
+
const newUtilization = calculateUtilization(utilizationState, stamp.depth)
|
|
178
|
+
|
|
179
|
+
// Update stamp in store (without triggering sync)
|
|
180
|
+
postageStampsStore.updateStampUtilization(batchID, newUtilization)
|
|
181
|
+
|
|
182
|
+
// Schedule debounced upload of dirty chunks and WAIT for it
|
|
183
|
+
if (tracker.hasDirtyChunks()) {
|
|
184
|
+
// Get stamper for signing chunks (with loaded bucket state)
|
|
185
|
+
const stamper = await postageStampsStore.getStamper(batchID, {
|
|
186
|
+
owner,
|
|
187
|
+
encryptionKey: hexToUint8Array(account.swarmEncryptionKey),
|
|
188
|
+
})
|
|
189
|
+
if (!stamper) {
|
|
190
|
+
console.warn("[SyncCoordinator] Cannot create stamper, skipping upload")
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const uploadPromise = utilizationUploader.scheduleUpload(
|
|
195
|
+
batchID.toHex(),
|
|
196
|
+
tracker,
|
|
197
|
+
async () => {
|
|
198
|
+
await saveUtilizationState(utilizationState, {
|
|
199
|
+
bee,
|
|
200
|
+
stamper,
|
|
201
|
+
encryptionKey: hexToUint8Array(account.swarmEncryptionKey),
|
|
202
|
+
cache: utilizationStore,
|
|
203
|
+
tracker,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Flush stamper bucket state updates to cache (if supported)
|
|
207
|
+
if (stamper.flush) {
|
|
208
|
+
await stamper.flush()
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
// Add timeout to prevent hanging
|
|
214
|
+
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
215
|
+
setTimeout(
|
|
216
|
+
() =>
|
|
217
|
+
reject(
|
|
218
|
+
new Error(
|
|
219
|
+
`Utilization upload timeout (${UTILIZATION_UPLOAD_TIMEOUT_MS}ms)`,
|
|
220
|
+
),
|
|
221
|
+
),
|
|
222
|
+
UTILIZATION_UPLOAD_TIMEOUT_MS,
|
|
223
|
+
)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
return Promise.race([uploadPromise, timeoutPromise])
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Capture a consistent snapshot of account state for syncing.
|
|
232
|
+
* Must be called before any async operations to ensure consistency.
|
|
233
|
+
*
|
|
234
|
+
* @param accountId - Account ID (hex address)
|
|
235
|
+
* @returns Snapshot and sync context, or undefined if account/stamp not found
|
|
236
|
+
*/
|
|
237
|
+
function getAccountStateSnapshot(accountId: string):
|
|
238
|
+
| {
|
|
239
|
+
snapshot: AccountStateSnapshot
|
|
240
|
+
encryptionKey: string
|
|
241
|
+
defaultStamp: PostageStamp
|
|
242
|
+
}
|
|
243
|
+
| undefined {
|
|
244
|
+
// Get account
|
|
245
|
+
const account = accountsStore.getAccount(new EthAddress(accountId))
|
|
246
|
+
if (!account) {
|
|
247
|
+
console.warn("[SyncCoordinator] Account not found", accountId)
|
|
248
|
+
return undefined
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Resolve default stamp (account or first identity)
|
|
252
|
+
const defaultStampBatchID =
|
|
253
|
+
account.defaultPostageStampBatchID ??
|
|
254
|
+
identitiesStore.getIdentitiesByAccount(account.id)[0]
|
|
255
|
+
?.defaultPostageStampBatchID
|
|
256
|
+
|
|
257
|
+
if (!defaultStampBatchID) {
|
|
258
|
+
console.warn("[SyncCoordinator] No default stamp for account", accountId)
|
|
259
|
+
return undefined
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const defaultStamp = postageStampsStore.getStamp(defaultStampBatchID)
|
|
263
|
+
if (!defaultStamp) {
|
|
264
|
+
console.warn("[SyncCoordinator] Default stamp not found")
|
|
265
|
+
return undefined
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Collect account state
|
|
269
|
+
const identities = identitiesStore.getIdentitiesByAccount(account.id)
|
|
270
|
+
const apps = identities.flatMap((identity) =>
|
|
271
|
+
connectedAppsStore.getAppsByIdentityId(identity.id),
|
|
272
|
+
)
|
|
273
|
+
const stamps = postageStampsStore.getStampsByAccount(accountId)
|
|
274
|
+
|
|
275
|
+
const snapshot: AccountStateSnapshot = {
|
|
276
|
+
version: 1,
|
|
277
|
+
timestamp: Date.now(),
|
|
278
|
+
accountId,
|
|
279
|
+
metadata: {
|
|
280
|
+
accountName: account.name,
|
|
281
|
+
defaultPostageStampBatchID: defaultStampBatchID.toHex(),
|
|
282
|
+
createdAt: account.createdAt,
|
|
283
|
+
lastModified: Date.now(),
|
|
284
|
+
},
|
|
285
|
+
identities,
|
|
286
|
+
connectedApps: apps,
|
|
287
|
+
postageStamps: stamps,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
snapshot,
|
|
292
|
+
encryptionKey: account.swarmEncryptionKey,
|
|
293
|
+
defaultStamp,
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return async function syncAccount(
|
|
298
|
+
accountId: string,
|
|
299
|
+
): Promise<SyncResult | undefined> {
|
|
300
|
+
const startTime = performance.now()
|
|
301
|
+
const timestamp = () => new Date().toISOString()
|
|
302
|
+
|
|
303
|
+
// Capture state snapshot BEFORE any async operations
|
|
304
|
+
const snapshotResult = getAccountStateSnapshot(accountId)
|
|
305
|
+
if (!snapshotResult) {
|
|
306
|
+
return undefined
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const { snapshot: state, encryptionKey, defaultStamp } = snapshotResult
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
// 1. Derive account signing key for feed
|
|
313
|
+
const backupKeyHex = await deriveSecret(encryptionKey, "backup-key")
|
|
314
|
+
const accountKey = new PrivateKey(backupKeyHex)
|
|
315
|
+
const owner = accountKey.publicKey().address()
|
|
316
|
+
|
|
317
|
+
// 2. Serialize account state
|
|
318
|
+
const jsonData = serializeAccountState(state)
|
|
319
|
+
|
|
320
|
+
// 3. Get stamper from store
|
|
321
|
+
const stamper = await postageStampsStore.getStamper(
|
|
322
|
+
defaultStamp.batchID,
|
|
323
|
+
{
|
|
324
|
+
owner,
|
|
325
|
+
encryptionKey: hexToUint8Array(encryptionKey),
|
|
326
|
+
},
|
|
327
|
+
)
|
|
328
|
+
if (!stamper) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`Cannot create stamper for batch ${defaultStamp.batchID.toHex()}`,
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 4. Upload encrypted data to Swarm
|
|
335
|
+
const uploadResult = await uploadEncryptedDataWithSigning(
|
|
336
|
+
{
|
|
337
|
+
bee,
|
|
338
|
+
stamper,
|
|
339
|
+
},
|
|
340
|
+
jsonData,
|
|
341
|
+
hexToUint8Array(encryptionKey),
|
|
342
|
+
undefined,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
// Collect chunk addresses for utilization tracking
|
|
346
|
+
const allChunkAddresses = uploadResult.chunkAddresses
|
|
347
|
+
|
|
348
|
+
// 5. Handle utilization tracking
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
await handleUtilizationUpdate(accountId, allChunkAddresses)
|
|
352
|
+
} catch (error) {
|
|
353
|
+
// Don't fail sync if utilization fails - continue with feed update
|
|
354
|
+
console.error(
|
|
355
|
+
`[SyncCoordinator ${timestamp()}] Utilization upload failed (+${(performance.now() - startTime).toFixed(2)}ms):`,
|
|
356
|
+
error,
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 6. Update epoch feed (after utilization completes)
|
|
361
|
+
const topic = Topic.fromString(
|
|
362
|
+
`${ACCOUNT_SYNC_TOPIC_PREFIX}:${accountId}`,
|
|
363
|
+
)
|
|
364
|
+
const updater = new BasicEpochUpdater(bee, topic, accountKey)
|
|
365
|
+
const feedTimestamp = BigInt(Math.floor(Date.now() / 1000))
|
|
366
|
+
|
|
367
|
+
// Convert 128-char hex reference to 64-byte Uint8Array
|
|
368
|
+
const refBytes = new Reference(uploadResult.reference).toUint8Array()
|
|
369
|
+
|
|
370
|
+
const updateResult = await updater.update(
|
|
371
|
+
feedTimestamp,
|
|
372
|
+
refBytes,
|
|
373
|
+
stamper,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
// Add SOC chunk to tracked addresses
|
|
377
|
+
allChunkAddresses.push(updateResult.socAddress)
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
status: "success",
|
|
381
|
+
reference: uploadResult.reference,
|
|
382
|
+
timestamp: feedTimestamp,
|
|
383
|
+
chunkAddresses: allChunkAddresses,
|
|
384
|
+
}
|
|
385
|
+
} catch (error) {
|
|
386
|
+
console.error(
|
|
387
|
+
`[SyncCoordinator ${timestamp()}] Sync failed (+${(performance.now() - startTime).toFixed(2)}ms):`,
|
|
388
|
+
error,
|
|
389
|
+
)
|
|
390
|
+
return {
|
|
391
|
+
status: "error",
|
|
392
|
+
error: error instanceof Error ? error.message : String(error),
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test fixtures for backup-encryption, account-state-snapshot, and sync tests.
|
|
3
|
+
*/
|
|
4
|
+
import { EthAddress, BatchId, PrivateKey, Bytes } from "@ethersphere/bee-js"
|
|
5
|
+
import type {
|
|
6
|
+
PasskeyAccount,
|
|
7
|
+
EthereumAccount,
|
|
8
|
+
AgentAccount,
|
|
9
|
+
Identity,
|
|
10
|
+
ConnectedApp,
|
|
11
|
+
PostageStamp,
|
|
12
|
+
} from "./schemas"
|
|
13
|
+
|
|
14
|
+
export const TEST_ETH_ADDRESS_HEX = "a".repeat(40)
|
|
15
|
+
export const TEST_ETH_ADDRESS_2_HEX = "b".repeat(40)
|
|
16
|
+
export const TEST_IDENTITY_ADDRESS_HEX = "1".repeat(40)
|
|
17
|
+
export const TEST_IDENTITY_ADDRESS_2_HEX = "2".repeat(40)
|
|
18
|
+
export const TEST_BATCH_ID_HEX = "c".repeat(64)
|
|
19
|
+
export const TEST_BATCH_ID_2_HEX = "e".repeat(64)
|
|
20
|
+
export const TEST_PRIVATE_KEY_HEX = "d".repeat(64)
|
|
21
|
+
export const TEST_ENCRYPTION_KEY_HEX = "f".repeat(64)
|
|
22
|
+
export const DIFFERENT_ENCRYPTION_KEY_HEX = "3".repeat(64)
|
|
23
|
+
|
|
24
|
+
export function createPasskeyAccount(
|
|
25
|
+
overrides?: Partial<PasskeyAccount>,
|
|
26
|
+
): PasskeyAccount {
|
|
27
|
+
return {
|
|
28
|
+
id: new EthAddress(TEST_ETH_ADDRESS_HEX),
|
|
29
|
+
name: "Test Passkey Account",
|
|
30
|
+
createdAt: 1700000000000,
|
|
31
|
+
type: "passkey" as const,
|
|
32
|
+
credentialId: "credential-abc-123",
|
|
33
|
+
swarmEncryptionKey: TEST_ENCRYPTION_KEY_HEX,
|
|
34
|
+
...overrides,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createEthereumAccount(
|
|
39
|
+
overrides?: Partial<EthereumAccount>,
|
|
40
|
+
): EthereumAccount {
|
|
41
|
+
return {
|
|
42
|
+
id: new EthAddress(TEST_ETH_ADDRESS_HEX),
|
|
43
|
+
name: "Test Ethereum Account",
|
|
44
|
+
createdAt: 1700000000000,
|
|
45
|
+
type: "ethereum" as const,
|
|
46
|
+
ethereumAddress: new EthAddress(TEST_ETH_ADDRESS_2_HEX),
|
|
47
|
+
encryptedMasterKey: new Bytes(new Uint8Array([1, 2, 3, 4])),
|
|
48
|
+
encryptionSalt: new Bytes(new Uint8Array([5, 6, 7, 8])),
|
|
49
|
+
encryptedSecretSeed: new Bytes(new Uint8Array([9, 10, 11, 12])),
|
|
50
|
+
swarmEncryptionKey: TEST_ENCRYPTION_KEY_HEX,
|
|
51
|
+
...overrides,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createAgentAccount(
|
|
56
|
+
overrides?: Partial<AgentAccount>,
|
|
57
|
+
): AgentAccount {
|
|
58
|
+
return {
|
|
59
|
+
id: new EthAddress(TEST_ETH_ADDRESS_HEX),
|
|
60
|
+
name: "Test Agent Account",
|
|
61
|
+
createdAt: 1700000000000,
|
|
62
|
+
type: "agent" as const,
|
|
63
|
+
swarmEncryptionKey: TEST_ENCRYPTION_KEY_HEX,
|
|
64
|
+
...overrides,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createIdentity(overrides?: Partial<Identity>): Identity {
|
|
69
|
+
return {
|
|
70
|
+
id: TEST_IDENTITY_ADDRESS_HEX,
|
|
71
|
+
accountId: new EthAddress(TEST_ETH_ADDRESS_HEX),
|
|
72
|
+
name: "Default Identity",
|
|
73
|
+
createdAt: 1700000000000,
|
|
74
|
+
...overrides,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createConnectedApp(
|
|
79
|
+
overrides?: Partial<ConnectedApp>,
|
|
80
|
+
): ConnectedApp {
|
|
81
|
+
return {
|
|
82
|
+
appUrl: "https://app.example.com",
|
|
83
|
+
appName: "Test App",
|
|
84
|
+
lastConnectedAt: 1700000000000,
|
|
85
|
+
identityId: TEST_IDENTITY_ADDRESS_HEX,
|
|
86
|
+
appSecret: "secret-should-be-stripped",
|
|
87
|
+
...overrides,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createPostageStamp(
|
|
92
|
+
overrides?: Partial<PostageStamp>,
|
|
93
|
+
): PostageStamp {
|
|
94
|
+
return {
|
|
95
|
+
accountId: TEST_ETH_ADDRESS_HEX,
|
|
96
|
+
batchID: new BatchId(TEST_BATCH_ID_HEX),
|
|
97
|
+
signerKey: new PrivateKey(TEST_PRIVATE_KEY_HEX),
|
|
98
|
+
utilization: 0,
|
|
99
|
+
usable: true,
|
|
100
|
+
depth: 20,
|
|
101
|
+
amount: 100000000n,
|
|
102
|
+
bucketDepth: 16,
|
|
103
|
+
blockNumber: 12345678,
|
|
104
|
+
immutableFlag: false,
|
|
105
|
+
exists: true,
|
|
106
|
+
createdAt: 1700000000000,
|
|
107
|
+
...overrides,
|
|
108
|
+
}
|
|
109
|
+
}
|