@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,174 @@
|
|
|
1
|
+
import { MantarayNode, NULL_ADDRESS } from "@ethersphere/bee-js"
|
|
2
|
+
import type {
|
|
3
|
+
Bee,
|
|
4
|
+
Stamper,
|
|
5
|
+
UploadOptions,
|
|
6
|
+
BeeRequestOptions,
|
|
7
|
+
} from "@ethersphere/bee-js"
|
|
8
|
+
import {
|
|
9
|
+
makeEncryptedContentAddressedChunk,
|
|
10
|
+
makeContentAddressedChunk,
|
|
11
|
+
} from "../chunk"
|
|
12
|
+
import { uint8ArrayToHex } from "../utils/hex"
|
|
13
|
+
import { uploadSingleChunk } from "./upload-data"
|
|
14
|
+
import { uploadSingleEncryptedChunk } from "./upload-encrypted-data"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options for creating a feed manifest
|
|
18
|
+
*/
|
|
19
|
+
export interface CreateFeedManifestOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Whether to encrypt the manifest.
|
|
22
|
+
* Default: true (encrypted)
|
|
23
|
+
*/
|
|
24
|
+
encrypt?: boolean
|
|
25
|
+
/**
|
|
26
|
+
* Feed type: "Sequence" for sequential feeds, "Epoch" for epoch feeds.
|
|
27
|
+
* Default: "Sequence"
|
|
28
|
+
*/
|
|
29
|
+
feedType?: "Sequence" | "Epoch"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Result of creating a feed manifest
|
|
34
|
+
*/
|
|
35
|
+
export interface CreateFeedManifestResult {
|
|
36
|
+
/**
|
|
37
|
+
* Reference to the feed manifest.
|
|
38
|
+
* - Encrypted: 128 hex chars (64 bytes = address + encryption key)
|
|
39
|
+
* - Unencrypted: 64 hex chars (32 bytes = address)
|
|
40
|
+
*/
|
|
41
|
+
reference: string
|
|
42
|
+
/**
|
|
43
|
+
* Tag UID if a tag was created during upload
|
|
44
|
+
*/
|
|
45
|
+
tagUid?: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a feed manifest directly using chunk upload
|
|
50
|
+
*
|
|
51
|
+
* Instead of using bee.createFeedManifest() which calls the /feeds endpoint,
|
|
52
|
+
* this function builds the manifest locally as a MantarayNode and uploads it
|
|
53
|
+
* via the encrypted chunk endpoint (or plain chunk endpoint if encrypt=false).
|
|
54
|
+
*
|
|
55
|
+
* Feed manifests have a single "/" path with metadata:
|
|
56
|
+
* - swarm-feed-owner: Owner's ethereum address (40 hex chars, no 0x)
|
|
57
|
+
* - swarm-feed-topic: Topic hash (64 hex chars)
|
|
58
|
+
* - swarm-feed-type: "Sequence" for sequential feeds
|
|
59
|
+
*
|
|
60
|
+
* IMPORTANT: This function implements client-side "saveRecursively" logic:
|
|
61
|
+
* 1. Upload the "/" child node first and get its address
|
|
62
|
+
* 2. Set the child's selfAddress to the uploaded address
|
|
63
|
+
* 3. Then upload the root node (which references the child's address)
|
|
64
|
+
*
|
|
65
|
+
* Without this, Bee's /bzz/ endpoint returns 404 because the "/" child chunk
|
|
66
|
+
* doesn't exist (only its calculated hash was stored in the root manifest).
|
|
67
|
+
*
|
|
68
|
+
* @param bee - Bee client instance
|
|
69
|
+
* @param stamper - Stamper for client-side signing
|
|
70
|
+
* @param topic - Topic hex string (64 chars)
|
|
71
|
+
* @param owner - Owner hex string (40 chars, no 0x prefix)
|
|
72
|
+
* @param options - Options for creating the manifest (encrypt, etc.)
|
|
73
|
+
* @param uploadOptions - Upload options (tag, deferred, etc.)
|
|
74
|
+
* @param requestOptions - Bee request options
|
|
75
|
+
* @returns Reference to the feed manifest
|
|
76
|
+
*/
|
|
77
|
+
export async function createFeedManifestDirect(
|
|
78
|
+
bee: Bee,
|
|
79
|
+
stamper: Stamper,
|
|
80
|
+
topic: string,
|
|
81
|
+
owner: string,
|
|
82
|
+
options?: CreateFeedManifestOptions,
|
|
83
|
+
uploadOptions?: UploadOptions,
|
|
84
|
+
requestOptions?: BeeRequestOptions,
|
|
85
|
+
): Promise<CreateFeedManifestResult> {
|
|
86
|
+
// Normalize owner (remove 0x prefix if present)
|
|
87
|
+
const normalizedOwner = owner.startsWith("0x") ? owner.slice(2) : owner
|
|
88
|
+
|
|
89
|
+
// DEBUG: Log what Bee will use for feed lookup
|
|
90
|
+
|
|
91
|
+
// 1. Create tag for upload if not provided
|
|
92
|
+
let tag = uploadOptions?.tag
|
|
93
|
+
if (!tag) {
|
|
94
|
+
const tagResponse = await bee.createTag()
|
|
95
|
+
tag = tagResponse.uid
|
|
96
|
+
}
|
|
97
|
+
const uploadOptionsWithTag = { ...uploadOptions, tag }
|
|
98
|
+
|
|
99
|
+
// 2. Create root MantarayNode with "/" fork containing feed metadata
|
|
100
|
+
const rootNode = new MantarayNode()
|
|
101
|
+
rootNode.addFork("/", NULL_ADDRESS, {
|
|
102
|
+
"swarm-feed-owner": normalizedOwner,
|
|
103
|
+
"swarm-feed-topic": topic,
|
|
104
|
+
"swarm-feed-type": options?.feedType ?? "Sequence",
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// 3. Get the "/" child node (addFork created it, we need to access it)
|
|
108
|
+
// 47 is ASCII code for '/'
|
|
109
|
+
const slashFork = rootNode.forks.get(47)
|
|
110
|
+
if (!slashFork) {
|
|
111
|
+
throw new Error("[FeedManifest] Failed to create '/' fork")
|
|
112
|
+
}
|
|
113
|
+
const slashNode = slashFork.node
|
|
114
|
+
|
|
115
|
+
// 4. Marshal and upload the "/" child node FIRST (saveRecursively pattern)
|
|
116
|
+
// This is critical: Bee's /bzz/ needs this chunk to exist
|
|
117
|
+
const slashNodeData = await slashNode.marshal()
|
|
118
|
+
const slashChunk = makeContentAddressedChunk(slashNodeData)
|
|
119
|
+
|
|
120
|
+
await uploadSingleChunk(
|
|
121
|
+
bee,
|
|
122
|
+
stamper,
|
|
123
|
+
slashChunk,
|
|
124
|
+
uploadOptionsWithTag,
|
|
125
|
+
requestOptions,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
// 5. Set the child's selfAddress to the uploaded chunk address
|
|
129
|
+
// This is used when marshaling the root node
|
|
130
|
+
slashNode.selfAddress = slashChunk.address.toUint8Array()
|
|
131
|
+
|
|
132
|
+
// 6. Now marshal and upload the root node
|
|
133
|
+
const rootNodeData = await rootNode.marshal()
|
|
134
|
+
|
|
135
|
+
// 7. Encrypt and upload OR upload directly
|
|
136
|
+
const shouldEncrypt = options?.encrypt !== false
|
|
137
|
+
|
|
138
|
+
if (shouldEncrypt) {
|
|
139
|
+
// Encrypted upload for root
|
|
140
|
+
const encryptedChunk = makeEncryptedContentAddressedChunk(rootNodeData)
|
|
141
|
+
|
|
142
|
+
await uploadSingleEncryptedChunk(
|
|
143
|
+
bee,
|
|
144
|
+
stamper,
|
|
145
|
+
encryptedChunk,
|
|
146
|
+
uploadOptionsWithTag,
|
|
147
|
+
requestOptions,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
// Return 64-byte reference (address + key)
|
|
151
|
+
const ref = new Uint8Array(64)
|
|
152
|
+
ref.set(encryptedChunk.address.toUint8Array(), 0)
|
|
153
|
+
ref.set(encryptedChunk.encryptionKey, 32)
|
|
154
|
+
const reference = uint8ArrayToHex(ref)
|
|
155
|
+
|
|
156
|
+
return { reference, tagUid: tag }
|
|
157
|
+
} else {
|
|
158
|
+
// Unencrypted upload for root
|
|
159
|
+
const rootChunk = makeContentAddressedChunk(rootNodeData)
|
|
160
|
+
|
|
161
|
+
await uploadSingleChunk(
|
|
162
|
+
bee,
|
|
163
|
+
stamper,
|
|
164
|
+
rootChunk,
|
|
165
|
+
uploadOptionsWithTag,
|
|
166
|
+
requestOptions,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// Return 32-byte reference
|
|
170
|
+
const reference = rootChunk.address.toHex()
|
|
171
|
+
|
|
172
|
+
return { reference, tagUid: tag }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async Epoch Feed Finder (Concurrent)
|
|
3
|
+
*
|
|
4
|
+
* Concurrent implementation for finding feed updates at specific timestamps
|
|
5
|
+
* using epoch-based indexing with parallel chunk fetching.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Binary } from "cafe-utility"
|
|
9
|
+
import type { Bee, BeeRequestOptions } from "@ethersphere/bee-js"
|
|
10
|
+
import { EthAddress, Reference, Topic } from "@ethersphere/bee-js"
|
|
11
|
+
import { EpochIndex, MAX_LEVEL } from "./epoch"
|
|
12
|
+
import type { EpochFinder, EpochLookupResult } from "./types"
|
|
13
|
+
import { downloadEncryptedSOC } from "../../download-data"
|
|
14
|
+
import { findPreviousLeaf } from "./utils"
|
|
15
|
+
|
|
16
|
+
const EPOCH_LOOKUP_TIMEOUT_MS = 2000
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Async concurrent finder for epoch-based feeds
|
|
20
|
+
*
|
|
21
|
+
* Launches parallel chunk fetches along the epoch tree path
|
|
22
|
+
* to find the feed update valid at a specific timestamp.
|
|
23
|
+
*
|
|
24
|
+
* Implements the EpochFinder interface.
|
|
25
|
+
*/
|
|
26
|
+
export class AsyncEpochFinder implements EpochFinder {
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly bee: Bee,
|
|
29
|
+
private readonly topic: Topic,
|
|
30
|
+
private readonly owner: EthAddress,
|
|
31
|
+
private readonly encryptionKey?: Uint8Array,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Find the feed update valid at time `at`
|
|
36
|
+
* @param at - Target unix timestamp (seconds)
|
|
37
|
+
* @param after - Hint of latest known update timestamp (0 if unknown)
|
|
38
|
+
* @returns 32-byte Swarm reference, or undefined if no update found
|
|
39
|
+
*/
|
|
40
|
+
async findAt(
|
|
41
|
+
at: bigint,
|
|
42
|
+
after: bigint = 0n,
|
|
43
|
+
): Promise<Uint8Array | undefined> {
|
|
44
|
+
// Fast path: exact timestamp updates are written at level-0 and should be
|
|
45
|
+
// retrievable without traversing potentially poisoned ancestors.
|
|
46
|
+
const exactEpoch = new EpochIndex(at, 0)
|
|
47
|
+
try {
|
|
48
|
+
const exact = await this.getEpochChunk(at, exactEpoch)
|
|
49
|
+
if (exact) {
|
|
50
|
+
return exact
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Ignore and fall back to tree traversal.
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Start from top epoch and traverse down
|
|
57
|
+
const traversed = await this.findAtEpoch(
|
|
58
|
+
at,
|
|
59
|
+
new EpochIndex(0n, MAX_LEVEL),
|
|
60
|
+
undefined,
|
|
61
|
+
)
|
|
62
|
+
if (traversed) {
|
|
63
|
+
return traversed
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Recovery fallback for poisoned-ancestor histories: only enable bounded
|
|
67
|
+
// leaf back-scan when root epoch exists but is invalid for `at`.
|
|
68
|
+
try {
|
|
69
|
+
const rootProbe = await this.getEpochChunk(
|
|
70
|
+
at,
|
|
71
|
+
new EpochIndex(0n, MAX_LEVEL),
|
|
72
|
+
)
|
|
73
|
+
if (rootProbe === undefined) {
|
|
74
|
+
return this.findPreviousLeaf(at, after)
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Root missing - no evidence of poisoned ancestors.
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Recursively find update at epoch, with parallel fetching
|
|
85
|
+
*
|
|
86
|
+
* @param at - Target timestamp
|
|
87
|
+
* @param epoch - Current epoch to check
|
|
88
|
+
* @param currentBest - Best result found so far
|
|
89
|
+
* @returns Reference if found, undefined otherwise
|
|
90
|
+
*/
|
|
91
|
+
private async findAtEpoch(
|
|
92
|
+
at: bigint,
|
|
93
|
+
epoch: EpochIndex,
|
|
94
|
+
currentBest: Uint8Array | undefined,
|
|
95
|
+
): Promise<Uint8Array | undefined> {
|
|
96
|
+
// Try to get chunk at this epoch.
|
|
97
|
+
// getEpochChunk throws when chunk is missing, returns undefined when
|
|
98
|
+
// chunk exists but timestamp is invalid for `at`.
|
|
99
|
+
let chunk: Uint8Array | undefined
|
|
100
|
+
try {
|
|
101
|
+
chunk = await this.getEpochChunk(at, epoch)
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Chunk missing at this epoch.
|
|
104
|
+
if (epoch.isLeft()) {
|
|
105
|
+
return currentBest
|
|
106
|
+
}
|
|
107
|
+
return this.findAtEpoch(epoch.start - 1n, epoch.left(), currentBest)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If chunk found and valid
|
|
111
|
+
if (chunk) {
|
|
112
|
+
// If at finest resolution, this is our answer
|
|
113
|
+
if (epoch.level === 0) {
|
|
114
|
+
return chunk
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Continue to finer resolution
|
|
118
|
+
return this.findAtEpoch(at, epoch.childAt(at), chunk)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Chunk exists but timestamp invalid.
|
|
122
|
+
// Keep descending towards the target epoch first, because finer epochs
|
|
123
|
+
// may still contain a valid update.
|
|
124
|
+
if (epoch.level > 0) {
|
|
125
|
+
const down = await this.findAtEpoch(at, epoch.childAt(at), currentBest)
|
|
126
|
+
if (down) {
|
|
127
|
+
return down
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (epoch.isLeft()) {
|
|
132
|
+
// Left child - return best we have so far
|
|
133
|
+
return currentBest
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Right child - need to search left sibling branch
|
|
137
|
+
return this.findAtEpoch(epoch.start - 1n, epoch.left(), currentBest)
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Fetch chunk for a specific epoch
|
|
141
|
+
*
|
|
142
|
+
* @param at - Target timestamp for validation
|
|
143
|
+
* @param epoch - Epoch to fetch
|
|
144
|
+
* @returns Chunk payload
|
|
145
|
+
* @throws Error if chunk not found
|
|
146
|
+
*/
|
|
147
|
+
private async getEpochChunk(
|
|
148
|
+
at: bigint,
|
|
149
|
+
epoch: EpochIndex,
|
|
150
|
+
): Promise<Uint8Array | undefined> {
|
|
151
|
+
const result = await this.getEpochChunkWithMetadata(at, epoch)
|
|
152
|
+
return result?.reference
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Find the feed update valid at time `at` with full metadata
|
|
157
|
+
* Used by updater to calculate next epoch when no hints provided
|
|
158
|
+
*
|
|
159
|
+
* @param at - Target unix timestamp (seconds)
|
|
160
|
+
* @returns EpochLookupResult with reference, epoch, and timestamp, or undefined if no update found
|
|
161
|
+
*/
|
|
162
|
+
async findAtWithMetadata(at: bigint): Promise<EpochLookupResult | undefined> {
|
|
163
|
+
// Fast path: exact timestamp updates are written at level-0
|
|
164
|
+
const exactEpoch = new EpochIndex(at, 0)
|
|
165
|
+
try {
|
|
166
|
+
const exact = await this.getEpochChunkWithMetadata(at, exactEpoch)
|
|
167
|
+
if (exact) {
|
|
168
|
+
return {
|
|
169
|
+
reference: exact.reference,
|
|
170
|
+
epoch: exactEpoch,
|
|
171
|
+
timestamp: exact.timestamp,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Ignore and fall back to tree traversal
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Start from top epoch and traverse down, tracking found epoch
|
|
179
|
+
return this.findAtEpochWithMetadata(
|
|
180
|
+
at,
|
|
181
|
+
new EpochIndex(0n, MAX_LEVEL),
|
|
182
|
+
undefined,
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Recursively find update at epoch with full metadata tracking
|
|
188
|
+
*
|
|
189
|
+
* @param at - Target timestamp
|
|
190
|
+
* @param epoch - Current epoch to check
|
|
191
|
+
* @param currentBest - Best result found so far
|
|
192
|
+
* @returns EpochLookupResult if found, undefined otherwise
|
|
193
|
+
*/
|
|
194
|
+
private async findAtEpochWithMetadata(
|
|
195
|
+
at: bigint,
|
|
196
|
+
epoch: EpochIndex,
|
|
197
|
+
currentBest: EpochLookupResult | undefined,
|
|
198
|
+
): Promise<EpochLookupResult | undefined> {
|
|
199
|
+
// Try to get chunk at this epoch
|
|
200
|
+
let chunkData: { reference: Uint8Array; timestamp: bigint } | undefined
|
|
201
|
+
try {
|
|
202
|
+
chunkData = await this.getEpochChunkWithMetadata(at, epoch)
|
|
203
|
+
} catch (error) {
|
|
204
|
+
// Chunk missing at this epoch
|
|
205
|
+
if (epoch.isLeft()) {
|
|
206
|
+
return currentBest
|
|
207
|
+
}
|
|
208
|
+
return this.findAtEpochWithMetadata(
|
|
209
|
+
epoch.start - 1n,
|
|
210
|
+
epoch.left(),
|
|
211
|
+
currentBest,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// If chunk found and valid
|
|
216
|
+
if (chunkData) {
|
|
217
|
+
const result: EpochLookupResult = {
|
|
218
|
+
reference: chunkData.reference,
|
|
219
|
+
epoch,
|
|
220
|
+
timestamp: chunkData.timestamp,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// If at finest resolution, this is our answer
|
|
224
|
+
if (epoch.level === 0) {
|
|
225
|
+
return result
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Continue to finer resolution
|
|
229
|
+
return this.findAtEpochWithMetadata(at, epoch.childAt(at), result)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Chunk exists but timestamp invalid
|
|
233
|
+
// Keep descending towards the target epoch first
|
|
234
|
+
if (epoch.level > 0) {
|
|
235
|
+
const down = await this.findAtEpochWithMetadata(
|
|
236
|
+
at,
|
|
237
|
+
epoch.childAt(at),
|
|
238
|
+
currentBest,
|
|
239
|
+
)
|
|
240
|
+
if (down) {
|
|
241
|
+
return down
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (epoch.isLeft()) {
|
|
246
|
+
return currentBest
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Right child - need to search left sibling branch
|
|
250
|
+
return this.findAtEpochWithMetadata(
|
|
251
|
+
epoch.start - 1n,
|
|
252
|
+
epoch.left(),
|
|
253
|
+
currentBest,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Fetch chunk for a specific epoch and return full metadata
|
|
259
|
+
*
|
|
260
|
+
* @param at - Target timestamp for validation
|
|
261
|
+
* @param epoch - Epoch to fetch
|
|
262
|
+
* @returns Object with reference and timestamp, or undefined if timestamp > at
|
|
263
|
+
* @throws Error if chunk not found
|
|
264
|
+
*/
|
|
265
|
+
private async getEpochChunkWithMetadata(
|
|
266
|
+
at: bigint,
|
|
267
|
+
epoch: EpochIndex,
|
|
268
|
+
): Promise<{ reference: Uint8Array; timestamp: bigint } | undefined> {
|
|
269
|
+
const requestOptions: BeeRequestOptions = {
|
|
270
|
+
timeout: EPOCH_LOOKUP_TIMEOUT_MS,
|
|
271
|
+
}
|
|
272
|
+
// Calculate epoch identifier: Keccak256(topic || Keccak256(start || level))
|
|
273
|
+
const epochHash = await epoch.marshalBinary()
|
|
274
|
+
const identifier = Binary.keccak256(
|
|
275
|
+
Binary.concatBytes(this.topic.toUint8Array(), epochHash),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
let payload: Uint8Array
|
|
279
|
+
if (this.encryptionKey) {
|
|
280
|
+
const soc = await downloadEncryptedSOC(
|
|
281
|
+
this.bee,
|
|
282
|
+
this.owner,
|
|
283
|
+
identifier,
|
|
284
|
+
this.encryptionKey,
|
|
285
|
+
requestOptions,
|
|
286
|
+
)
|
|
287
|
+
payload = soc.payload
|
|
288
|
+
} else {
|
|
289
|
+
// Calculate chunk address: Keccak256(identifier || owner)
|
|
290
|
+
const address = new Reference(
|
|
291
|
+
Binary.keccak256(
|
|
292
|
+
Binary.concatBytes(identifier, this.owner.toUint8Array()),
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
// Download chunk
|
|
297
|
+
const chunkData = await this.bee.downloadChunk(
|
|
298
|
+
address.toHex(),
|
|
299
|
+
undefined,
|
|
300
|
+
requestOptions,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
// Extract payload from SOC (Single Owner Chunk)
|
|
304
|
+
const IDENTIFIER_SIZE = 32
|
|
305
|
+
const SIGNATURE_SIZE = 65
|
|
306
|
+
const SPAN_SIZE = 8
|
|
307
|
+
const SOC_HEADER_SIZE = IDENTIFIER_SIZE + SIGNATURE_SIZE
|
|
308
|
+
|
|
309
|
+
// Read span to get payload length
|
|
310
|
+
const spanStart = SOC_HEADER_SIZE
|
|
311
|
+
const span = chunkData.slice(spanStart, spanStart + SPAN_SIZE)
|
|
312
|
+
const spanView = new DataView(
|
|
313
|
+
span.buffer,
|
|
314
|
+
span.byteOffset,
|
|
315
|
+
span.byteLength,
|
|
316
|
+
)
|
|
317
|
+
const payloadLength = Number(spanView.getBigUint64(0, true)) // little-endian
|
|
318
|
+
|
|
319
|
+
// Extract full payload (timestamp + reference)
|
|
320
|
+
const payloadStart = spanStart + SPAN_SIZE
|
|
321
|
+
payload = chunkData.slice(payloadStart, payloadStart + payloadLength)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Detect payload format based on length:
|
|
325
|
+
// - 40 bytes: timestamp(8) + reference(32) - from /soc endpoint
|
|
326
|
+
// - 48 bytes: span(8) + timestamp(8) + reference(32) - from /chunks with v1 format
|
|
327
|
+
// - 72 bytes: timestamp(8) + encrypted_reference(64) - from /soc endpoint
|
|
328
|
+
// - 80 bytes: span(8) + timestamp(8) + encrypted_reference(64) - from /chunks with v1 format
|
|
329
|
+
const VALID_PAYLOAD_LENGTHS = [40, 48, 72, 80]
|
|
330
|
+
if (!VALID_PAYLOAD_LENGTHS.includes(payload.length)) {
|
|
331
|
+
console.warn(
|
|
332
|
+
`Unexpected feed payload length: ${payload.length}. Expected 40, 48, 72, or 80 bytes.`,
|
|
333
|
+
)
|
|
334
|
+
return undefined
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const TIMESTAMP_SIZE = 8
|
|
338
|
+
const hasSpanPrefix = payload.length === 48 || payload.length === 80
|
|
339
|
+
|
|
340
|
+
const timestampOffset = hasSpanPrefix ? 8 : 0
|
|
341
|
+
const timestampBytes = payload.slice(
|
|
342
|
+
timestampOffset,
|
|
343
|
+
timestampOffset + TIMESTAMP_SIZE,
|
|
344
|
+
)
|
|
345
|
+
const timestampView = new DataView(
|
|
346
|
+
timestampBytes.buffer,
|
|
347
|
+
timestampBytes.byteOffset,
|
|
348
|
+
timestampBytes.byteLength,
|
|
349
|
+
)
|
|
350
|
+
const timestamp = timestampView.getBigUint64(0, false) // big-endian
|
|
351
|
+
|
|
352
|
+
// Validate timestamp - update must be at or before target time
|
|
353
|
+
if (timestamp > at) {
|
|
354
|
+
return undefined
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Return reference and timestamp (skip timestamp and optional span prefix)
|
|
358
|
+
return {
|
|
359
|
+
reference: payload.slice(timestampOffset + TIMESTAMP_SIZE),
|
|
360
|
+
timestamp,
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private findPreviousLeaf(
|
|
365
|
+
at: bigint,
|
|
366
|
+
after: bigint,
|
|
367
|
+
): Promise<Uint8Array | undefined> {
|
|
368
|
+
return findPreviousLeaf(at, after, (targetAt, epoch) =>
|
|
369
|
+
this.getEpochChunk(targetAt, epoch),
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
}
|