@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,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test utilities for epoch feeds
|
|
3
|
+
*
|
|
4
|
+
* Provides mock storage and helpers for testing epoch feed operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Binary } from "cafe-utility"
|
|
8
|
+
import { PrivateKey, Topic, EthAddress } from "@ethersphere/bee-js"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* In-memory chunk storage for testing
|
|
12
|
+
*/
|
|
13
|
+
export class MockChunkStore {
|
|
14
|
+
private chunks = new Map<string, Uint8Array>()
|
|
15
|
+
|
|
16
|
+
async put(address: string, data: Uint8Array): Promise<void> {
|
|
17
|
+
this.chunks.set(address.toLowerCase(), data)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async get(address: string): Promise<Uint8Array> {
|
|
21
|
+
const data = this.chunks.get(address.toLowerCase())
|
|
22
|
+
if (!data) {
|
|
23
|
+
throw new Error(`Chunk not found: ${address}`)
|
|
24
|
+
}
|
|
25
|
+
return data
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
has(address: string): boolean {
|
|
29
|
+
return this.chunks.has(address.toLowerCase())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
clear(): void {
|
|
33
|
+
this.chunks.clear()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
size(): number {
|
|
37
|
+
return this.chunks.size
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Mock Bee instance for testing
|
|
43
|
+
*/
|
|
44
|
+
export class MockBee {
|
|
45
|
+
public readonly url = "http://localhost:1633"
|
|
46
|
+
private store: MockChunkStore
|
|
47
|
+
private tagCounter = 0
|
|
48
|
+
|
|
49
|
+
constructor(store?: MockChunkStore) {
|
|
50
|
+
this.store = store || new MockChunkStore()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async createTag(): Promise<{
|
|
54
|
+
uid: number
|
|
55
|
+
split: number
|
|
56
|
+
seen: number
|
|
57
|
+
stored: number
|
|
58
|
+
sent: number
|
|
59
|
+
synced: number
|
|
60
|
+
address: string
|
|
61
|
+
startedAt: string
|
|
62
|
+
}> {
|
|
63
|
+
this.tagCounter++
|
|
64
|
+
return {
|
|
65
|
+
uid: this.tagCounter,
|
|
66
|
+
split: 0,
|
|
67
|
+
seen: 0,
|
|
68
|
+
stored: 0,
|
|
69
|
+
sent: 0,
|
|
70
|
+
synced: 0,
|
|
71
|
+
address: "",
|
|
72
|
+
startedAt: new Date().toISOString(),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async downloadChunk(reference: string): Promise<Uint8Array> {
|
|
77
|
+
return this.store.get(reference)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async uploadChunk(
|
|
81
|
+
data: Uint8Array,
|
|
82
|
+
_postageBatchId: string,
|
|
83
|
+
): Promise<{ reference: string }> {
|
|
84
|
+
const address = Binary.keccak256(data)
|
|
85
|
+
const reference = Binary.uint8ArrayToHex(address)
|
|
86
|
+
await this.store.put(reference, data)
|
|
87
|
+
return { reference }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getStore(): MockChunkStore {
|
|
91
|
+
return this.store
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create test signer with a deterministic private key
|
|
97
|
+
*/
|
|
98
|
+
export function createTestSigner(): PrivateKey {
|
|
99
|
+
// Use a fixed private key for deterministic tests
|
|
100
|
+
const privateKeyHex =
|
|
101
|
+
"634fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd"
|
|
102
|
+
return new PrivateKey(privateKeyHex)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create test topic
|
|
107
|
+
*/
|
|
108
|
+
export function createTestTopic(name: string = "testtopic"): Topic {
|
|
109
|
+
const encoder = new TextEncoder()
|
|
110
|
+
const hash = Binary.keccak256(encoder.encode(name))
|
|
111
|
+
return new Topic(hash)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create test owner address
|
|
116
|
+
*/
|
|
117
|
+
export function createTestOwner(): EthAddress {
|
|
118
|
+
return createTestSigner().publicKey().address()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create test reference (32 bytes)
|
|
123
|
+
*/
|
|
124
|
+
export function createTestReference(value: number | bigint): Uint8Array {
|
|
125
|
+
const ref = new Uint8Array(32)
|
|
126
|
+
const view = new DataView(ref.buffer)
|
|
127
|
+
view.setBigUint64(0, BigInt(value), false) // big-endian
|
|
128
|
+
return ref
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create test payload with timestamp
|
|
133
|
+
*/
|
|
134
|
+
export function createTestPayload(at: bigint): Uint8Array {
|
|
135
|
+
const payload = new Uint8Array(8)
|
|
136
|
+
const view = new DataView(payload.buffer)
|
|
137
|
+
view.setBigUint64(0, at, false) // big-endian
|
|
138
|
+
return payload
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const BATCH_ID_LENGTH = 32
|
|
142
|
+
const INDEX_LENGTH = 8
|
|
143
|
+
const TIMESTAMP_LENGTH = 8
|
|
144
|
+
const SIGNATURE_LENGTH = 65
|
|
145
|
+
const ISSUER_LENGTH = 20
|
|
146
|
+
const SOC_IDENTIFIER_LENGTH = 32
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create a mock Stamper for testing
|
|
150
|
+
*
|
|
151
|
+
* Returns an object with a stamp() method that produces a valid-looking
|
|
152
|
+
* EnvelopeWithBatchId without performing real cryptographic operations.
|
|
153
|
+
*/
|
|
154
|
+
export function createMockStamper() {
|
|
155
|
+
return {
|
|
156
|
+
stamp(_chunk: any) {
|
|
157
|
+
return {
|
|
158
|
+
batchId: {
|
|
159
|
+
toUint8Array: () => new Uint8Array(BATCH_ID_LENGTH),
|
|
160
|
+
},
|
|
161
|
+
index: new Uint8Array(INDEX_LENGTH),
|
|
162
|
+
timestamp: new Uint8Array(TIMESTAMP_LENGTH),
|
|
163
|
+
signature: new Uint8Array(SIGNATURE_LENGTH),
|
|
164
|
+
issuer: new Uint8Array(ISSUER_LENGTH),
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Mock fetch for SOC and chunk uploads
|
|
172
|
+
*
|
|
173
|
+
* Intercepts fetch calls to /soc/ and /chunks endpoints, storing data
|
|
174
|
+
* in the provided MockChunkStore.
|
|
175
|
+
*
|
|
176
|
+
* @param store - Mock chunk store for persisting uploaded data
|
|
177
|
+
* @param owner - Owner address for computing SOC addresses on /chunks uploads
|
|
178
|
+
*/
|
|
179
|
+
export function mockFetch(store?: MockChunkStore, owner?: EthAddress): void {
|
|
180
|
+
const originalFetch = global.fetch
|
|
181
|
+
|
|
182
|
+
global.fetch = async (url: string | URL | Request, init?: RequestInit) => {
|
|
183
|
+
// Parse URL
|
|
184
|
+
const urlStr = typeof url === "string" ? url : url.toString()
|
|
185
|
+
|
|
186
|
+
// Check if it's a SOC upload
|
|
187
|
+
if (urlStr.includes("/soc/")) {
|
|
188
|
+
// Extract body data and SOC info from URL
|
|
189
|
+
const body = init?.body as Uint8Array
|
|
190
|
+
if (!body) {
|
|
191
|
+
throw new Error("Missing body in SOC upload")
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Parse URL: /soc/{owner}/{id}?sig={signature}
|
|
195
|
+
const parts = urlStr.split("/soc/")[1].split("/")
|
|
196
|
+
const socOwner = parts[0]
|
|
197
|
+
const idAndSig = parts[1].split("?sig=")
|
|
198
|
+
const identifier = idAndSig[0]
|
|
199
|
+
|
|
200
|
+
// Calculate SOC address: Keccak256(identifier || owner)
|
|
201
|
+
const identifierBytes = Binary.hexToUint8Array(identifier)
|
|
202
|
+
const ownerBytes = Binary.hexToUint8Array(socOwner)
|
|
203
|
+
const socAddress = Binary.keccak256(
|
|
204
|
+
Binary.concatBytes(identifierBytes, ownerBytes),
|
|
205
|
+
)
|
|
206
|
+
const reference = Binary.uint8ArrayToHex(socAddress)
|
|
207
|
+
|
|
208
|
+
// Build full SOC chunk data (identifier + signature + span + payload)
|
|
209
|
+
// For mock purposes, we need to reconstruct the full SOC
|
|
210
|
+
const signatureHex = idAndSig[1]
|
|
211
|
+
const signatureBytes = Binary.hexToUint8Array(signatureHex)
|
|
212
|
+
const fullSOCData = Binary.concatBytes(
|
|
213
|
+
identifierBytes,
|
|
214
|
+
signatureBytes,
|
|
215
|
+
body, // This is span + payload
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
// Store in mock store if provided
|
|
219
|
+
if (store) {
|
|
220
|
+
await store.put(reference, fullSOCData)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return new Response(JSON.stringify({ reference }), {
|
|
224
|
+
status: 200,
|
|
225
|
+
headers: { "Content-Type": "application/json" },
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check if it's a chunk upload (used by uploadChunkWithFetch)
|
|
230
|
+
if (urlStr.includes("/chunks") && init?.method === "POST") {
|
|
231
|
+
const body = init?.body as Uint8Array
|
|
232
|
+
if (!body) {
|
|
233
|
+
throw new Error("Missing body in chunk upload")
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// SOC data starts with identifier (32 bytes)
|
|
237
|
+
// Compute SOC address: Keccak256(identifier + owner)
|
|
238
|
+
if (store && owner) {
|
|
239
|
+
const identifier = body.slice(0, SOC_IDENTIFIER_LENGTH)
|
|
240
|
+
const socAddress = Binary.keccak256(
|
|
241
|
+
Binary.concatBytes(identifier, owner.toUint8Array()),
|
|
242
|
+
)
|
|
243
|
+
const reference = Binary.uint8ArrayToHex(socAddress)
|
|
244
|
+
await store.put(reference, body)
|
|
245
|
+
return new Response(JSON.stringify({ reference }), {
|
|
246
|
+
status: 200,
|
|
247
|
+
headers: { "Content-Type": "application/json" },
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Fallback: store by content hash
|
|
252
|
+
const address = Binary.keccak256(body)
|
|
253
|
+
const reference = Binary.uint8ArrayToHex(address)
|
|
254
|
+
if (store) {
|
|
255
|
+
await store.put(reference, body)
|
|
256
|
+
}
|
|
257
|
+
return new Response(JSON.stringify({ reference }), {
|
|
258
|
+
status: 200,
|
|
259
|
+
headers: { "Content-Type": "application/json" },
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Fall back to original fetch for other URLs
|
|
264
|
+
return originalFetch(url, init)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Restore original fetch
|
|
270
|
+
*/
|
|
271
|
+
export function restoreFetch(): void {
|
|
272
|
+
// This is a simplified version - in production you'd want to properly restore
|
|
273
|
+
// For now, tests should use the mocked version throughout
|
|
274
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for Epoch-Based Feeds
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Bee,
|
|
7
|
+
EthAddress,
|
|
8
|
+
Topic,
|
|
9
|
+
PrivateKey,
|
|
10
|
+
Stamper,
|
|
11
|
+
} from "@ethersphere/bee-js"
|
|
12
|
+
import type { EpochIndex } from "./epoch"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Options for creating an epoch feed reader
|
|
16
|
+
*/
|
|
17
|
+
export interface EpochFeedOptions {
|
|
18
|
+
/** Bee instance for chunk operations */
|
|
19
|
+
bee: Bee
|
|
20
|
+
|
|
21
|
+
/** Feed topic (32 bytes) */
|
|
22
|
+
topic: Topic
|
|
23
|
+
|
|
24
|
+
/** Feed owner address */
|
|
25
|
+
owner: EthAddress
|
|
26
|
+
|
|
27
|
+
/** Optional encryption key for encrypted feed updates */
|
|
28
|
+
encryptionKey?: Uint8Array
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Options for creating an epoch feed writer
|
|
33
|
+
*/
|
|
34
|
+
export interface EpochFeedWriterOptions extends EpochFeedOptions {
|
|
35
|
+
/** Private key for signing chunks */
|
|
36
|
+
signer: PrivateKey
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Result from an epoch feed lookup
|
|
41
|
+
*/
|
|
42
|
+
export interface EpochLookupResult {
|
|
43
|
+
/** Swarm reference (32 bytes) */
|
|
44
|
+
reference: Uint8Array
|
|
45
|
+
|
|
46
|
+
/** Epoch where the update was found */
|
|
47
|
+
epoch: EpochIndex
|
|
48
|
+
|
|
49
|
+
/** Timestamp of the update */
|
|
50
|
+
timestamp: bigint
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hints for calculating the next epoch in a stateless manner.
|
|
55
|
+
* Callers should store these after each update and pass them to subsequent updates.
|
|
56
|
+
*/
|
|
57
|
+
export interface EpochUpdateHints {
|
|
58
|
+
/** Previous epoch (for calculating next) */
|
|
59
|
+
lastEpoch?: { start: bigint; level: number }
|
|
60
|
+
/** Timestamp of last update */
|
|
61
|
+
lastTimestamp?: bigint
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Result from an epoch feed update, including epoch info for subsequent updates.
|
|
66
|
+
*/
|
|
67
|
+
export interface EpochUpdateResult {
|
|
68
|
+
/** SOC address of the uploaded chunk */
|
|
69
|
+
socAddress: Uint8Array
|
|
70
|
+
/** Epoch used for this update (for caller to store as hint) */
|
|
71
|
+
epoch: { start: bigint; level: number }
|
|
72
|
+
/** Timestamp used (for caller to store as hint) */
|
|
73
|
+
timestamp: bigint
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Interface for epoch feed finders (readers)
|
|
78
|
+
*
|
|
79
|
+
* Implementations: SyncEpochFinder, AsyncEpochFinder
|
|
80
|
+
*/
|
|
81
|
+
export interface EpochFinder {
|
|
82
|
+
/**
|
|
83
|
+
* Find the feed update valid at time `at`
|
|
84
|
+
*
|
|
85
|
+
* @param at - Target unix timestamp (seconds)
|
|
86
|
+
* @param after - Hint of latest known update timestamp (0 if unknown)
|
|
87
|
+
* @returns 32 or 64-byte Swarm reference, or undefined if no update found
|
|
88
|
+
*/
|
|
89
|
+
findAt(at: bigint, after?: bigint): Promise<Uint8Array | undefined>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Interface for epoch feed updaters (writers)
|
|
94
|
+
*
|
|
95
|
+
* Implementation: BasicEpochUpdater
|
|
96
|
+
*
|
|
97
|
+
* Implements Bee-compatible stateless epoch calculation.
|
|
98
|
+
* Each update uses hints from the previous update to calculate the next epoch,
|
|
99
|
+
* creating a proper epoch tree that Bee's finder can traverse.
|
|
100
|
+
*/
|
|
101
|
+
export interface EpochUpdater {
|
|
102
|
+
/**
|
|
103
|
+
* Update feed with a reference at given timestamp
|
|
104
|
+
*
|
|
105
|
+
* Calculates the appropriate epoch based on hints:
|
|
106
|
+
* - First update (no hints): uses root epoch (level 32, start 0)
|
|
107
|
+
* - Subsequent updates: calculates next epoch using LCA-based algorithm
|
|
108
|
+
*
|
|
109
|
+
* @param at - Unix timestamp for this update (seconds)
|
|
110
|
+
* @param reference - 32 or 64-byte Swarm reference to store
|
|
111
|
+
* @param stamper - Stamper object for stamping
|
|
112
|
+
* @param encryptionKey - Optional encryption key for the update
|
|
113
|
+
* @param hints - Optional hints from previous update for calculating epoch
|
|
114
|
+
* @returns Update result with SOC address and epoch info for next update
|
|
115
|
+
*/
|
|
116
|
+
update(
|
|
117
|
+
at: bigint,
|
|
118
|
+
reference: Uint8Array,
|
|
119
|
+
stamper: Stamper,
|
|
120
|
+
encryptionKey?: Uint8Array,
|
|
121
|
+
hints?: EpochUpdateHints,
|
|
122
|
+
): Promise<EpochUpdateResult>
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get the owner address (derived from signer)
|
|
126
|
+
*/
|
|
127
|
+
getOwner(): EthAddress
|
|
128
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic Epoch Feed Updater
|
|
3
|
+
*
|
|
4
|
+
* Handles writing updates to epoch-based feeds by calculating the next
|
|
5
|
+
* epoch and uploading chunks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Binary } from "cafe-utility"
|
|
9
|
+
import type { Bee, Stamper } from "@ethersphere/bee-js"
|
|
10
|
+
import { EthAddress, Topic, PrivateKey, Identifier } from "@ethersphere/bee-js"
|
|
11
|
+
import { EpochIndex, MAX_LEVEL } from "./epoch"
|
|
12
|
+
import {
|
|
13
|
+
uploadEncryptedSOC,
|
|
14
|
+
uploadSOCViaSocEndpoint,
|
|
15
|
+
} from "../../upload-encrypted-data"
|
|
16
|
+
import type { EpochUpdater, EpochUpdateHints, EpochUpdateResult } from "./types"
|
|
17
|
+
import { AsyncEpochFinder } from "./async-finder"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Basic updater for epoch-based feeds
|
|
21
|
+
*
|
|
22
|
+
* Implements Bee-compatible stateless epoch calculation.
|
|
23
|
+
* Each update uses hints from the previous update to calculate the next epoch,
|
|
24
|
+
* creating a proper epoch tree that Bee's finder can traverse.
|
|
25
|
+
*
|
|
26
|
+
* - First update (no hints): uses root epoch (level 32, start 0)
|
|
27
|
+
* - Subsequent updates: calculates next epoch using the standard algorithm:
|
|
28
|
+
* - If new timestamp within previous epoch's range: dive to child
|
|
29
|
+
* - Else: jump to LCA(newTimestamp, prevTimestamp) and dive to child
|
|
30
|
+
*
|
|
31
|
+
* Implements the EpochUpdater interface.
|
|
32
|
+
*/
|
|
33
|
+
export class BasicEpochUpdater implements EpochUpdater {
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly bee: Bee,
|
|
36
|
+
private readonly topic: Topic,
|
|
37
|
+
private readonly signer: PrivateKey,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Update feed with a reference at given timestamp
|
|
42
|
+
*
|
|
43
|
+
* @param at - Unix timestamp for this update (seconds)
|
|
44
|
+
* @param reference - 32 or 64-byte Swarm reference to store
|
|
45
|
+
* @param stamper - Stamper object for stamping
|
|
46
|
+
* @param encryptionKey - Optional encryption key for the update
|
|
47
|
+
* @param hints - Optional hints from previous update for calculating epoch
|
|
48
|
+
* @returns Update result with SOC address and epoch info for next update
|
|
49
|
+
*/
|
|
50
|
+
async update(
|
|
51
|
+
at: bigint,
|
|
52
|
+
reference: Uint8Array,
|
|
53
|
+
stamper: Stamper,
|
|
54
|
+
encryptionKey?: Uint8Array,
|
|
55
|
+
hints?: EpochUpdateHints,
|
|
56
|
+
): Promise<EpochUpdateResult> {
|
|
57
|
+
if (reference.length !== 32 && reference.length !== 64) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Reference must be 32 or 64 bytes, got ${reference.length}`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Calculate epoch - auto-lookup if no hints provided
|
|
64
|
+
const epoch = await this.calculateEpoch(at, hints, encryptionKey)
|
|
65
|
+
|
|
66
|
+
// Upload the chunk with timestamp + reference
|
|
67
|
+
const socAddress = await this.uploadEpochChunk(
|
|
68
|
+
epoch,
|
|
69
|
+
at,
|
|
70
|
+
reference,
|
|
71
|
+
stamper,
|
|
72
|
+
encryptionKey,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// Return result with hints for next update
|
|
76
|
+
return {
|
|
77
|
+
socAddress,
|
|
78
|
+
epoch: { start: epoch.start, level: epoch.level },
|
|
79
|
+
timestamp: at,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Calculate the epoch for this update based on hints or auto-lookup
|
|
85
|
+
*
|
|
86
|
+
* When hints are provided, uses them for fast epoch calculation.
|
|
87
|
+
* When no hints are provided, looks up the current feed state to calculate
|
|
88
|
+
* the next epoch, preventing overwrites of previous updates.
|
|
89
|
+
*
|
|
90
|
+
* @param at - Timestamp for this update
|
|
91
|
+
* @param hints - Optional hints from previous update
|
|
92
|
+
* @param encryptionKey - Optional encryption key for looking up encrypted feeds
|
|
93
|
+
* @returns Epoch to use for this update
|
|
94
|
+
*/
|
|
95
|
+
private async calculateEpoch(
|
|
96
|
+
at: bigint,
|
|
97
|
+
hints?: EpochUpdateHints,
|
|
98
|
+
encryptionKey?: Uint8Array,
|
|
99
|
+
): Promise<EpochIndex> {
|
|
100
|
+
// Fast path: use provided hints
|
|
101
|
+
if (hints?.lastEpoch && hints.lastTimestamp !== undefined) {
|
|
102
|
+
const prevEpoch = new EpochIndex(
|
|
103
|
+
hints.lastEpoch.start,
|
|
104
|
+
hints.lastEpoch.level,
|
|
105
|
+
)
|
|
106
|
+
return prevEpoch.next(hints.lastTimestamp, at)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Slow path: lookup current state
|
|
110
|
+
const owner = this.signer.publicKey().address()
|
|
111
|
+
const finder = new AsyncEpochFinder(
|
|
112
|
+
this.bee,
|
|
113
|
+
this.topic,
|
|
114
|
+
owner,
|
|
115
|
+
encryptionKey,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
// Use findAtWithMetadata to get both reference AND epoch info
|
|
119
|
+
const current = await finder.findAtWithMetadata(at)
|
|
120
|
+
|
|
121
|
+
if (!current) {
|
|
122
|
+
// First update ever - use root epoch
|
|
123
|
+
return new EpochIndex(0n, MAX_LEVEL)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Calculate next epoch based on found state
|
|
127
|
+
return current.epoch.next(current.timestamp, at)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get the owner address (derived from signer)
|
|
132
|
+
*/
|
|
133
|
+
getOwner(): EthAddress {
|
|
134
|
+
return this.signer.publicKey().address()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Upload a chunk for a specific epoch
|
|
139
|
+
*
|
|
140
|
+
* @param epoch - Epoch to upload to
|
|
141
|
+
* @param at - Timestamp of this update
|
|
142
|
+
* @param reference - 32 or 64-byte reference to store
|
|
143
|
+
* @param stamper - Stamper object for stamping
|
|
144
|
+
* @returns SOC chunk address for utilization tracking
|
|
145
|
+
*/
|
|
146
|
+
private async uploadEpochChunk(
|
|
147
|
+
epoch: EpochIndex,
|
|
148
|
+
at: bigint,
|
|
149
|
+
reference: Uint8Array,
|
|
150
|
+
stamper: Stamper,
|
|
151
|
+
encryptionKey?: Uint8Array,
|
|
152
|
+
): Promise<Uint8Array> {
|
|
153
|
+
// Calculate epoch identifier: Keccak256(topic || Keccak256(start || level))
|
|
154
|
+
const epochHash = await epoch.marshalBinary()
|
|
155
|
+
const identifier = new Identifier(
|
|
156
|
+
Binary.keccak256(
|
|
157
|
+
Binary.concatBytes(this.topic.toUint8Array(), epochHash),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
// Timestamp: 8-byte big-endian
|
|
162
|
+
const timestamp = new Uint8Array(8)
|
|
163
|
+
const tsView = new DataView(timestamp.buffer)
|
|
164
|
+
tsView.setBigUint64(0, at, false) // big-endian
|
|
165
|
+
|
|
166
|
+
// Payload: timestamp + reference = 40 or 72 bytes
|
|
167
|
+
// The upload function will wrap this in a CAC (adding span) to get 48 or 80 bytes
|
|
168
|
+
// which is the v1 format Bee expects for /bzz/ compatibility
|
|
169
|
+
const payload = Binary.concatBytes(timestamp, reference)
|
|
170
|
+
|
|
171
|
+
const result = encryptionKey
|
|
172
|
+
? await uploadEncryptedSOC(
|
|
173
|
+
this.bee,
|
|
174
|
+
stamper,
|
|
175
|
+
this.signer,
|
|
176
|
+
identifier,
|
|
177
|
+
payload,
|
|
178
|
+
encryptionKey,
|
|
179
|
+
{ deferred: false }, // deferred setting is NOT the cause of /bzz/ timeout
|
|
180
|
+
)
|
|
181
|
+
: await uploadSOCViaSocEndpoint(
|
|
182
|
+
this.bee,
|
|
183
|
+
stamper,
|
|
184
|
+
this.signer,
|
|
185
|
+
identifier,
|
|
186
|
+
payload,
|
|
187
|
+
{ deferred: false }, // deferred setting is NOT the cause of /bzz/ timeout
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return result.socAddress
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for epoch feed finders
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { EpochIndex } from "./epoch"
|
|
6
|
+
|
|
7
|
+
const MAX_LEAF_BACKSCAN = 128n
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Type for the chunk fetcher callback used by findPreviousLeaf.
|
|
11
|
+
* This allows both sync and async finders to share the same logic.
|
|
12
|
+
*/
|
|
13
|
+
export type EpochChunkFetcher = (
|
|
14
|
+
at: bigint,
|
|
15
|
+
epoch: EpochIndex,
|
|
16
|
+
) => Promise<Uint8Array | undefined>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Find a previous leaf chunk by scanning backwards from the target timestamp.
|
|
20
|
+
*
|
|
21
|
+
* This is a recovery fallback for poisoned-ancestor histories where the tree
|
|
22
|
+
* traversal fails but valid leaf nodes exist.
|
|
23
|
+
*
|
|
24
|
+
* @param at - Target unix timestamp (seconds)
|
|
25
|
+
* @param after - Hint of latest known update timestamp (0 if unknown)
|
|
26
|
+
* @param getEpochChunk - Callback function to fetch epoch chunks
|
|
27
|
+
* @returns 32-byte Swarm reference, or undefined if no update found
|
|
28
|
+
*/
|
|
29
|
+
export async function findPreviousLeaf(
|
|
30
|
+
at: bigint,
|
|
31
|
+
after: bigint,
|
|
32
|
+
getEpochChunk: EpochChunkFetcher,
|
|
33
|
+
): Promise<Uint8Array | undefined> {
|
|
34
|
+
if (at === 0n) {
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const minAt = after > 0n ? after : 0n
|
|
39
|
+
const lowerBound =
|
|
40
|
+
at > MAX_LEAF_BACKSCAN && at - MAX_LEAF_BACKSCAN > minAt
|
|
41
|
+
? at - MAX_LEAF_BACKSCAN
|
|
42
|
+
: minAt
|
|
43
|
+
|
|
44
|
+
let probe = at - 1n
|
|
45
|
+
while (probe >= lowerBound) {
|
|
46
|
+
try {
|
|
47
|
+
const leaf = await getEpochChunk(at, new EpochIndex(probe, 0))
|
|
48
|
+
if (leaf) {
|
|
49
|
+
return leaf
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Missing leaf at probe timestamp.
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (probe === 0n) {
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
probe--
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async Sequential Feed Finder
|
|
3
|
+
*
|
|
4
|
+
* Currently delegates to the sync finder to match Go behavior
|
|
5
|
+
* while keeping an async interface.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
Bee,
|
|
10
|
+
EthAddress,
|
|
11
|
+
Topic,
|
|
12
|
+
BeeRequestOptions,
|
|
13
|
+
} from "@ethersphere/bee-js"
|
|
14
|
+
import type { SequentialFinder, SequentialLookupResult } from "./types"
|
|
15
|
+
import { SyncSequentialFinder } from "./finder"
|
|
16
|
+
|
|
17
|
+
export class AsyncSequentialFinder implements SequentialFinder {
|
|
18
|
+
private readonly syncFinder: SyncSequentialFinder
|
|
19
|
+
|
|
20
|
+
constructor(bee: Bee, topic: Topic, owner: EthAddress) {
|
|
21
|
+
this.syncFinder = new SyncSequentialFinder(bee, topic, owner)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async findAt(
|
|
25
|
+
at: bigint,
|
|
26
|
+
after: bigint = 0n,
|
|
27
|
+
requestOptions?: BeeRequestOptions,
|
|
28
|
+
): Promise<SequentialLookupResult> {
|
|
29
|
+
return this.syncFinder.findAt(at, after, requestOptions)
|
|
30
|
+
}
|
|
31
|
+
}
|