@parity/product-sdk-bulletin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/dist/index.d.ts +555 -0
- package/dist/index.js +1344 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
- package/src/authorization.ts +191 -0
- package/src/cid.ts +338 -0
- package/src/client.ts +255 -0
- package/src/errors.ts +235 -0
- package/src/gateway.ts +209 -0
- package/src/index.ts +38 -0
- package/src/query.ts +82 -0
- package/src/resolve-query.ts +192 -0
- package/src/resolve-signer.ts +66 -0
- package/src/types.ts +140 -0
- package/src/upload.ts +344 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1344 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { getChainAPI } from "@parity/product-sdk-chain-client";
|
|
3
|
+
|
|
4
|
+
// src/authorization.ts
|
|
5
|
+
import { createLogger } from "@parity/product-sdk-logger";
|
|
6
|
+
import { Enum } from "polkadot-api";
|
|
7
|
+
|
|
8
|
+
// src/errors.ts
|
|
9
|
+
var BulletinError = class extends Error {
|
|
10
|
+
constructor(message, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.name = "BulletinError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var BulletinHostUnavailableError = class extends BulletinError {
|
|
16
|
+
constructor(operation) {
|
|
17
|
+
super(
|
|
18
|
+
`Host preimage API unavailable for ${operation}. Ensure you are running inside a host container (Polkadot Browser / Desktop).`
|
|
19
|
+
);
|
|
20
|
+
this.name = "BulletinHostUnavailableError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var BulletinLookupTimeoutError = class extends BulletinError {
|
|
24
|
+
/** The CID that was being looked up. */
|
|
25
|
+
cid;
|
|
26
|
+
/** The timeout duration in milliseconds. */
|
|
27
|
+
timeoutMs;
|
|
28
|
+
constructor(cid, timeoutMs) {
|
|
29
|
+
super(`Host preimage lookup timed out after ${timeoutMs}ms for CID: ${cid}`);
|
|
30
|
+
this.name = "BulletinLookupTimeoutError";
|
|
31
|
+
this.cid = cid;
|
|
32
|
+
this.timeoutMs = timeoutMs;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var BulletinLookupInterruptedError = class extends BulletinError {
|
|
36
|
+
/** The CID that was being looked up. */
|
|
37
|
+
cid;
|
|
38
|
+
constructor(cid) {
|
|
39
|
+
super(`Host preimage lookup was interrupted for CID: ${cid}`);
|
|
40
|
+
this.name = "BulletinLookupInterruptedError";
|
|
41
|
+
this.cid = cid;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var BulletinAuthorizationError = class extends BulletinError {
|
|
45
|
+
/** The address that was being checked. */
|
|
46
|
+
address;
|
|
47
|
+
constructor(address, cause) {
|
|
48
|
+
super(`Failed to check authorization for ${address}`, { cause });
|
|
49
|
+
this.name = "BulletinAuthorizationError";
|
|
50
|
+
this.address = address;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var BulletinGatewayUnavailableError = class extends BulletinError {
|
|
54
|
+
/** The environment that was requested. */
|
|
55
|
+
environment;
|
|
56
|
+
constructor(environment) {
|
|
57
|
+
super(`Bulletin gateway for "${environment}" is not yet available`);
|
|
58
|
+
this.name = "BulletinGatewayUnavailableError";
|
|
59
|
+
this.environment = environment;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var BulletinGatewayFetchError = class extends BulletinError {
|
|
63
|
+
/** The CID that was being fetched. */
|
|
64
|
+
cid;
|
|
65
|
+
/** The HTTP status code returned by the gateway. */
|
|
66
|
+
status;
|
|
67
|
+
/** The HTTP status text returned by the gateway. */
|
|
68
|
+
statusText;
|
|
69
|
+
constructor(cid, status, statusText) {
|
|
70
|
+
super(`Gateway fetch failed for ${cid}: ${status} ${statusText}`);
|
|
71
|
+
this.name = "BulletinGatewayFetchError";
|
|
72
|
+
this.cid = cid;
|
|
73
|
+
this.status = status;
|
|
74
|
+
this.statusText = statusText;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var BulletinCidError = class extends BulletinError {
|
|
78
|
+
/** The invalid CID string, if available. */
|
|
79
|
+
cid;
|
|
80
|
+
constructor(message, cid) {
|
|
81
|
+
super(message);
|
|
82
|
+
this.name = "BulletinCidError";
|
|
83
|
+
this.cid = cid;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
if (void 0) {
|
|
87
|
+
const { describe, test, expect } = void 0;
|
|
88
|
+
describe("BulletinError hierarchy", () => {
|
|
89
|
+
test("BulletinError is instanceof Error", () => {
|
|
90
|
+
const err = new BulletinError("test");
|
|
91
|
+
expect(err).toBeInstanceOf(Error);
|
|
92
|
+
expect(err).toBeInstanceOf(BulletinError);
|
|
93
|
+
expect(err.name).toBe("BulletinError");
|
|
94
|
+
});
|
|
95
|
+
test("BulletinHostUnavailableError", () => {
|
|
96
|
+
const err = new BulletinHostUnavailableError("upload");
|
|
97
|
+
expect(err).toBeInstanceOf(BulletinError);
|
|
98
|
+
expect(err.name).toBe("BulletinHostUnavailableError");
|
|
99
|
+
expect(err.message).toContain("upload");
|
|
100
|
+
expect(err.message).toContain("Host preimage API unavailable");
|
|
101
|
+
});
|
|
102
|
+
test("BulletinLookupTimeoutError", () => {
|
|
103
|
+
const err = new BulletinLookupTimeoutError("bafyabc123", 3e4);
|
|
104
|
+
expect(err).toBeInstanceOf(BulletinError);
|
|
105
|
+
expect(err.name).toBe("BulletinLookupTimeoutError");
|
|
106
|
+
expect(err.cid).toBe("bafyabc123");
|
|
107
|
+
expect(err.timeoutMs).toBe(3e4);
|
|
108
|
+
expect(err.message).toContain("30000ms");
|
|
109
|
+
expect(err.message).toContain("bafyabc123");
|
|
110
|
+
});
|
|
111
|
+
test("BulletinLookupInterruptedError", () => {
|
|
112
|
+
const err = new BulletinLookupInterruptedError("bafyabc123");
|
|
113
|
+
expect(err).toBeInstanceOf(BulletinError);
|
|
114
|
+
expect(err.name).toBe("BulletinLookupInterruptedError");
|
|
115
|
+
expect(err.cid).toBe("bafyabc123");
|
|
116
|
+
expect(err.message).toContain("interrupted");
|
|
117
|
+
});
|
|
118
|
+
test("BulletinAuthorizationError with cause", () => {
|
|
119
|
+
const cause = new Error("RPC timeout");
|
|
120
|
+
const err = new BulletinAuthorizationError("5GrwvaEF...", cause);
|
|
121
|
+
expect(err).toBeInstanceOf(BulletinError);
|
|
122
|
+
expect(err.name).toBe("BulletinAuthorizationError");
|
|
123
|
+
expect(err.address).toBe("5GrwvaEF...");
|
|
124
|
+
expect(err.cause).toBe(cause);
|
|
125
|
+
});
|
|
126
|
+
test("BulletinGatewayUnavailableError", () => {
|
|
127
|
+
const err = new BulletinGatewayUnavailableError("polkadot");
|
|
128
|
+
expect(err).toBeInstanceOf(BulletinError);
|
|
129
|
+
expect(err.name).toBe("BulletinGatewayUnavailableError");
|
|
130
|
+
expect(err.environment).toBe("polkadot");
|
|
131
|
+
expect(err.message).toContain("polkadot");
|
|
132
|
+
});
|
|
133
|
+
test("BulletinGatewayFetchError", () => {
|
|
134
|
+
const err = new BulletinGatewayFetchError("bafyabc", 404, "Not Found");
|
|
135
|
+
expect(err).toBeInstanceOf(BulletinError);
|
|
136
|
+
expect(err.name).toBe("BulletinGatewayFetchError");
|
|
137
|
+
expect(err.cid).toBe("bafyabc");
|
|
138
|
+
expect(err.status).toBe(404);
|
|
139
|
+
expect(err.statusText).toBe("Not Found");
|
|
140
|
+
expect(err.message).toContain("404");
|
|
141
|
+
});
|
|
142
|
+
test("BulletinCidError", () => {
|
|
143
|
+
const err = new BulletinCidError("Expected CIDv1, got CIDv0", "Qmabc");
|
|
144
|
+
expect(err).toBeInstanceOf(BulletinError);
|
|
145
|
+
expect(err.name).toBe("BulletinCidError");
|
|
146
|
+
expect(err.cid).toBe("Qmabc");
|
|
147
|
+
});
|
|
148
|
+
test("all errors can be caught with BulletinError", () => {
|
|
149
|
+
const errors = [
|
|
150
|
+
new BulletinHostUnavailableError("query"),
|
|
151
|
+
new BulletinLookupTimeoutError("cid", 1e3),
|
|
152
|
+
new BulletinLookupInterruptedError("cid"),
|
|
153
|
+
new BulletinAuthorizationError("addr"),
|
|
154
|
+
new BulletinGatewayUnavailableError("env"),
|
|
155
|
+
new BulletinGatewayFetchError("cid", 500, "Error"),
|
|
156
|
+
new BulletinCidError("bad cid")
|
|
157
|
+
];
|
|
158
|
+
for (const err of errors) {
|
|
159
|
+
expect(err).toBeInstanceOf(BulletinError);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/authorization.ts
|
|
166
|
+
var log = createLogger("bulletin");
|
|
167
|
+
var NOT_AUTHORIZED = Object.freeze({
|
|
168
|
+
authorized: false,
|
|
169
|
+
remainingTransactions: 0,
|
|
170
|
+
remainingBytes: 0n,
|
|
171
|
+
expiration: 0
|
|
172
|
+
});
|
|
173
|
+
async function checkAuthorization(api, address) {
|
|
174
|
+
let auth;
|
|
175
|
+
try {
|
|
176
|
+
auth = await api.query.TransactionStorage.Authorizations.getValue(
|
|
177
|
+
Enum("Account", address)
|
|
178
|
+
);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
log.error("checkAuthorization: query failed", { address, error });
|
|
181
|
+
throw new BulletinAuthorizationError(address, error);
|
|
182
|
+
}
|
|
183
|
+
if (!auth) {
|
|
184
|
+
log.debug("checkAuthorization: no authorization found", { address });
|
|
185
|
+
return NOT_AUTHORIZED;
|
|
186
|
+
}
|
|
187
|
+
const status = {
|
|
188
|
+
authorized: true,
|
|
189
|
+
remainingTransactions: auth.extent.transactions,
|
|
190
|
+
remainingBytes: auth.extent.bytes,
|
|
191
|
+
expiration: auth.expiration
|
|
192
|
+
};
|
|
193
|
+
log.debug("checkAuthorization", {
|
|
194
|
+
address,
|
|
195
|
+
remainingTransactions: status.remainingTransactions,
|
|
196
|
+
remainingBytes: status.remainingBytes.toString(),
|
|
197
|
+
expiration: status.expiration
|
|
198
|
+
});
|
|
199
|
+
return status;
|
|
200
|
+
}
|
|
201
|
+
if (void 0) {
|
|
202
|
+
let createMockApi = function(authResult) {
|
|
203
|
+
return {
|
|
204
|
+
query: {
|
|
205
|
+
TransactionStorage: {
|
|
206
|
+
Authorizations: {
|
|
207
|
+
getValue: vi.fn().mockResolvedValue(authResult)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
createMockApi2 = createMockApi;
|
|
214
|
+
const { describe, test, expect, vi } = void 0;
|
|
215
|
+
describe("checkAuthorization", () => {
|
|
216
|
+
test("returns not authorized when no authorization exists", async () => {
|
|
217
|
+
const api = createMockApi(void 0);
|
|
218
|
+
const status = await checkAuthorization(api, "5GrwvaEF...");
|
|
219
|
+
expect(status.authorized).toBe(false);
|
|
220
|
+
expect(status.remainingTransactions).toBe(0);
|
|
221
|
+
expect(status.remainingBytes).toBe(0n);
|
|
222
|
+
expect(status.expiration).toBe(0);
|
|
223
|
+
});
|
|
224
|
+
test("returns authorization with full quota", async () => {
|
|
225
|
+
const api = createMockApi({
|
|
226
|
+
extent: { transactions: 10, bytes: 1000000n },
|
|
227
|
+
expiration: 999
|
|
228
|
+
});
|
|
229
|
+
const status = await checkAuthorization(api, "5GrwvaEF...");
|
|
230
|
+
expect(status.authorized).toBe(true);
|
|
231
|
+
expect(status.remainingTransactions).toBe(10);
|
|
232
|
+
expect(status.remainingBytes).toBe(1000000n);
|
|
233
|
+
expect(status.expiration).toBe(999);
|
|
234
|
+
});
|
|
235
|
+
test("returns authorization with zero transactions remaining", async () => {
|
|
236
|
+
const api = createMockApi({
|
|
237
|
+
extent: { transactions: 0, bytes: 1000000n },
|
|
238
|
+
expiration: 999
|
|
239
|
+
});
|
|
240
|
+
const status = await checkAuthorization(api, "5GrwvaEF...");
|
|
241
|
+
expect(status.authorized).toBe(true);
|
|
242
|
+
expect(status.remainingTransactions).toBe(0);
|
|
243
|
+
});
|
|
244
|
+
test("returns authorization with zero bytes remaining", async () => {
|
|
245
|
+
const api = createMockApi({
|
|
246
|
+
extent: { transactions: 5, bytes: 0n },
|
|
247
|
+
expiration: 999
|
|
248
|
+
});
|
|
249
|
+
const status = await checkAuthorization(api, "5GrwvaEF...");
|
|
250
|
+
expect(status.authorized).toBe(true);
|
|
251
|
+
expect(status.remainingBytes).toBe(0n);
|
|
252
|
+
});
|
|
253
|
+
test("preserves expiration block number", async () => {
|
|
254
|
+
const api = createMockApi({
|
|
255
|
+
extent: { transactions: 1, bytes: 500n },
|
|
256
|
+
expiration: 12345
|
|
257
|
+
});
|
|
258
|
+
const status = await checkAuthorization(api, "5GrwvaEF...");
|
|
259
|
+
expect(status.expiration).toBe(12345);
|
|
260
|
+
});
|
|
261
|
+
test("throws BulletinAuthorizationError when query fails", async () => {
|
|
262
|
+
const api = {
|
|
263
|
+
query: {
|
|
264
|
+
TransactionStorage: {
|
|
265
|
+
Authorizations: {
|
|
266
|
+
getValue: vi.fn().mockRejectedValue(new Error("RPC connection lost"))
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const err = await checkAuthorization(api, "5GrwvaEF...").catch((e) => e);
|
|
272
|
+
expect(err).toBeInstanceOf(BulletinAuthorizationError);
|
|
273
|
+
const error = err;
|
|
274
|
+
expect(error.address).toBe("5GrwvaEF...");
|
|
275
|
+
expect(error.cause).toBeInstanceOf(Error);
|
|
276
|
+
expect(error.cause.message).toBe("RPC connection lost");
|
|
277
|
+
});
|
|
278
|
+
test("passes correct Enum key to the query", async () => {
|
|
279
|
+
const getValue = vi.fn().mockResolvedValue(void 0);
|
|
280
|
+
const api = {
|
|
281
|
+
query: {
|
|
282
|
+
TransactionStorage: {
|
|
283
|
+
Authorizations: { getValue }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
await checkAuthorization(api, "5GrwvaEF...");
|
|
288
|
+
expect(getValue).toHaveBeenCalledTimes(1);
|
|
289
|
+
const arg = getValue.mock.calls[0][0];
|
|
290
|
+
expect(arg.type).toBe("Account");
|
|
291
|
+
expect(arg.value).toBe("5GrwvaEF...");
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
var createMockApi2;
|
|
296
|
+
|
|
297
|
+
// src/cid.ts
|
|
298
|
+
import { blake2b256, bytesToHex, hexToBytes } from "@parity/product-sdk-crypto";
|
|
299
|
+
import { createLogger as createLogger2 } from "@parity/product-sdk-logger";
|
|
300
|
+
import { CID } from "multiformats/cid";
|
|
301
|
+
import * as Digest from "multiformats/hashes/digest";
|
|
302
|
+
var log2 = createLogger2("bulletin");
|
|
303
|
+
var HashAlgorithm = {
|
|
304
|
+
/** BLAKE2b-256 — default for product-sdk and the chain SDK. */
|
|
305
|
+
Blake2b256: 45600,
|
|
306
|
+
/** SHA2-256 — default for bulletin-deploy. */
|
|
307
|
+
Sha2_256: 18,
|
|
308
|
+
/** Keccak-256 — Ethereum compatibility. */
|
|
309
|
+
Keccak256: 27
|
|
310
|
+
};
|
|
311
|
+
var CidCodec = {
|
|
312
|
+
/** Raw binary — default for single-chunk data. */
|
|
313
|
+
Raw: 85,
|
|
314
|
+
/** DAG-PB — used for multi-chunk manifests / directory structures. */
|
|
315
|
+
DagPb: 112,
|
|
316
|
+
/** DAG-CBOR — alternative DAG encoding. */
|
|
317
|
+
DagCbor: 113
|
|
318
|
+
};
|
|
319
|
+
var SUPPORTED_HASH_CODES = new Set(Object.values(HashAlgorithm));
|
|
320
|
+
var SUPPORTED_CODEC_CODES = new Set(Object.values(CidCodec));
|
|
321
|
+
var EXPECTED_HEX_LENGTH = 66;
|
|
322
|
+
function computeCid(data) {
|
|
323
|
+
const hash = blake2b256(data);
|
|
324
|
+
return CID.createV1(CidCodec.Raw, Digest.create(HashAlgorithm.Blake2b256, hash)).toString();
|
|
325
|
+
}
|
|
326
|
+
function cidToPreimageKey(cid) {
|
|
327
|
+
const parsed = CID.parse(cid);
|
|
328
|
+
if (parsed.version !== 1) {
|
|
329
|
+
throw new BulletinCidError(`Expected CIDv1, got CIDv${parsed.version}`, cid);
|
|
330
|
+
}
|
|
331
|
+
if (!SUPPORTED_HASH_CODES.has(parsed.multihash.code)) {
|
|
332
|
+
throw new BulletinCidError(
|
|
333
|
+
`Unsupported hash algorithm 0x${parsed.multihash.code.toString(16)}; expected one of: ${[...SUPPORTED_HASH_CODES].map((c) => `0x${c.toString(16)}`).join(", ")}`,
|
|
334
|
+
cid
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return `0x${bytesToHex(parsed.multihash.digest)}`;
|
|
338
|
+
}
|
|
339
|
+
function hashToCid(hexHash, hashCode = HashAlgorithm.Blake2b256, codec = CidCodec.Raw) {
|
|
340
|
+
if (hexHash.length !== EXPECTED_HEX_LENGTH) {
|
|
341
|
+
throw new BulletinCidError(
|
|
342
|
+
`Expected a 0x-prefixed 32-byte hex hash (${EXPECTED_HEX_LENGTH} chars), got ${hexHash.length} chars`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
if (!SUPPORTED_HASH_CODES.has(hashCode)) {
|
|
346
|
+
throw new BulletinCidError(
|
|
347
|
+
`Unsupported hash algorithm 0x${hashCode.toString(16)}; expected one of: ${[...SUPPORTED_HASH_CODES].map((c) => `0x${c.toString(16)}`).join(", ")}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (!SUPPORTED_CODEC_CODES.has(codec)) {
|
|
351
|
+
throw new BulletinCidError(
|
|
352
|
+
`Unsupported CID codec 0x${codec.toString(16)}; expected one of: ${[...SUPPORTED_CODEC_CODES].map((c) => `0x${c.toString(16)}`).join(", ")}`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
const digest = hexToBytes(hexHash.slice(2));
|
|
356
|
+
const cid = CID.createV1(codec, Digest.create(hashCode, digest)).toString();
|
|
357
|
+
log2.debug("hashToCid", { hexHash, hashCode, codec, cid });
|
|
358
|
+
return cid;
|
|
359
|
+
}
|
|
360
|
+
if (void 0) {
|
|
361
|
+
const { describe, test, expect } = void 0;
|
|
362
|
+
describe("computeCid", () => {
|
|
363
|
+
test("produces known CID for known input", () => {
|
|
364
|
+
const data = new TextEncoder().encode("hello bulletin");
|
|
365
|
+
const cid = computeCid(data);
|
|
366
|
+
expect(cid).toBe(computeCid(new TextEncoder().encode("hello bulletin")));
|
|
367
|
+
expect(cid).toMatch(/^b[a-z2-7]+$/);
|
|
368
|
+
});
|
|
369
|
+
test("deterministic \u2014 same input, same output", () => {
|
|
370
|
+
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
|
371
|
+
expect(computeCid(data)).toBe(computeCid(data));
|
|
372
|
+
});
|
|
373
|
+
test("different inputs produce different CIDs", () => {
|
|
374
|
+
const a = computeCid(new Uint8Array([1]));
|
|
375
|
+
const b = computeCid(new Uint8Array([2]));
|
|
376
|
+
expect(a).not.toBe(b);
|
|
377
|
+
});
|
|
378
|
+
test("empty input produces valid CID", () => {
|
|
379
|
+
const cid = computeCid(new Uint8Array(0));
|
|
380
|
+
expect(cid).toMatch(/^b[a-z2-7]+$/);
|
|
381
|
+
});
|
|
382
|
+
test("matches reference implementation (manual varint)", () => {
|
|
383
|
+
const data = new TextEncoder().encode("test");
|
|
384
|
+
const cid = computeCid(data);
|
|
385
|
+
expect(cid[0]).toBe("b");
|
|
386
|
+
const parsed = CID.parse(cid);
|
|
387
|
+
expect(parsed.version).toBe(1);
|
|
388
|
+
expect(parsed.code).toBe(CidCodec.Raw);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
describe("cidToPreimageKey", () => {
|
|
392
|
+
test("round-trips with computeCid \u2014 returns 0x-prefixed 64-char hex", () => {
|
|
393
|
+
const data = new TextEncoder().encode("hello bulletin");
|
|
394
|
+
const cid = computeCid(data);
|
|
395
|
+
const key = cidToPreimageKey(cid);
|
|
396
|
+
expect(key).toMatch(/^0x[0-9a-f]{64}$/);
|
|
397
|
+
});
|
|
398
|
+
test("deterministic \u2014 same CID always yields same key", () => {
|
|
399
|
+
const cid = computeCid(new Uint8Array([1, 2, 3]));
|
|
400
|
+
expect(cidToPreimageKey(cid)).toBe(cidToPreimageKey(cid));
|
|
401
|
+
});
|
|
402
|
+
test("matches raw blake2b-256 hash", () => {
|
|
403
|
+
const data = new TextEncoder().encode("test");
|
|
404
|
+
const cid = computeCid(data);
|
|
405
|
+
const key = cidToPreimageKey(cid);
|
|
406
|
+
const hash = blake2b256(data);
|
|
407
|
+
const expected = `0x${bytesToHex(hash)}`;
|
|
408
|
+
expect(key).toBe(expected);
|
|
409
|
+
});
|
|
410
|
+
test("accepts CIDv1 with sha2-256", () => {
|
|
411
|
+
const hash = new Uint8Array(32).fill(205);
|
|
412
|
+
const cidV1 = CID.createV1(CidCodec.Raw, Digest.create(HashAlgorithm.Sha2_256, hash));
|
|
413
|
+
const key = cidToPreimageKey(cidV1.toString());
|
|
414
|
+
expect(key).toMatch(/^0x[0-9a-f]{64}$/);
|
|
415
|
+
expect(key).toBe(`0x${bytesToHex(hash)}`);
|
|
416
|
+
});
|
|
417
|
+
test("accepts CIDv1 with keccak-256", () => {
|
|
418
|
+
const hash = new Uint8Array(32).fill(239);
|
|
419
|
+
const cidV1 = CID.createV1(CidCodec.Raw, Digest.create(HashAlgorithm.Keccak256, hash));
|
|
420
|
+
const key = cidToPreimageKey(cidV1.toString());
|
|
421
|
+
expect(key).toBe(`0x${bytesToHex(hash)}`);
|
|
422
|
+
});
|
|
423
|
+
test("throws BulletinCidError for CIDv0 input", () => {
|
|
424
|
+
const hash = new Uint8Array(32).fill(171);
|
|
425
|
+
const cidV0 = CID.create(0, 112, Digest.create(HashAlgorithm.Sha2_256, hash));
|
|
426
|
+
expect(() => cidToPreimageKey(cidV0.toString())).toThrow(BulletinCidError);
|
|
427
|
+
});
|
|
428
|
+
test("throws BulletinCidError for CIDv1 with unsupported hash algorithm", () => {
|
|
429
|
+
const unsupportedCode = 153;
|
|
430
|
+
const hash = new Uint8Array(32).fill(171);
|
|
431
|
+
const cidV1 = CID.createV1(CidCodec.Raw, Digest.create(unsupportedCode, hash));
|
|
432
|
+
expect(() => cidToPreimageKey(cidV1.toString())).toThrow(BulletinCidError);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
describe("hashToCid", () => {
|
|
436
|
+
test("round-trips with cidToPreimageKey \u2014 hex \u2192 CID \u2192 hex", () => {
|
|
437
|
+
const data = new TextEncoder().encode("hello bulletin");
|
|
438
|
+
const originalCid = computeCid(data);
|
|
439
|
+
const hex = cidToPreimageKey(originalCid);
|
|
440
|
+
const reconstructed = hashToCid(hex);
|
|
441
|
+
expect(reconstructed).toBe(originalCid);
|
|
442
|
+
});
|
|
443
|
+
test("full cycle: data \u2192 CID \u2192 hex \u2192 CID", () => {
|
|
444
|
+
const data = new Uint8Array([10, 20, 30, 40, 50]);
|
|
445
|
+
const cid1 = computeCid(data);
|
|
446
|
+
const hex = cidToPreimageKey(cid1);
|
|
447
|
+
const cid2 = hashToCid(hex);
|
|
448
|
+
expect(cid2).toBe(cid1);
|
|
449
|
+
});
|
|
450
|
+
test("deterministic \u2014 same hex always yields same CID", () => {
|
|
451
|
+
const hex = cidToPreimageKey(computeCid(new Uint8Array([1, 2, 3])));
|
|
452
|
+
expect(hashToCid(hex)).toBe(hashToCid(hex));
|
|
453
|
+
});
|
|
454
|
+
test("produces valid base32-lower CIDv1 (default: blake2b-256, raw)", () => {
|
|
455
|
+
const hex = cidToPreimageKey(computeCid(new TextEncoder().encode("test")));
|
|
456
|
+
const cid = hashToCid(hex);
|
|
457
|
+
expect(cid).toMatch(/^b[a-z2-7]+$/);
|
|
458
|
+
const parsed = CID.parse(cid);
|
|
459
|
+
expect(parsed.version).toBe(1);
|
|
460
|
+
expect(parsed.code).toBe(CidCodec.Raw);
|
|
461
|
+
expect(parsed.multihash.code).toBe(HashAlgorithm.Blake2b256);
|
|
462
|
+
});
|
|
463
|
+
test("sha2-256 produces different CID from blake2b-256 for same hash", () => {
|
|
464
|
+
const hex = `0x${"ab".repeat(32)}`;
|
|
465
|
+
const blake = hashToCid(hex, HashAlgorithm.Blake2b256);
|
|
466
|
+
const sha = hashToCid(hex, HashAlgorithm.Sha2_256);
|
|
467
|
+
expect(blake).not.toBe(sha);
|
|
468
|
+
expect(CID.parse(blake).version).toBe(1);
|
|
469
|
+
expect(CID.parse(sha).version).toBe(1);
|
|
470
|
+
});
|
|
471
|
+
test("sha2-256 round-trips through cidToPreimageKey", () => {
|
|
472
|
+
const hex = `0x${"cd".repeat(32)}`;
|
|
473
|
+
const cid = hashToCid(hex, HashAlgorithm.Sha2_256);
|
|
474
|
+
const extracted = cidToPreimageKey(cid);
|
|
475
|
+
expect(extracted).toBe(hex);
|
|
476
|
+
});
|
|
477
|
+
test("keccak-256 round-trips through cidToPreimageKey", () => {
|
|
478
|
+
const hex = `0x${"ef".repeat(32)}`;
|
|
479
|
+
const cid = hashToCid(hex, HashAlgorithm.Keccak256);
|
|
480
|
+
const extracted = cidToPreimageKey(cid);
|
|
481
|
+
expect(extracted).toBe(hex);
|
|
482
|
+
});
|
|
483
|
+
test("dag-pb codec produces different CID from raw for same hash", () => {
|
|
484
|
+
const hex = `0x${"ab".repeat(32)}`;
|
|
485
|
+
const rawCid = hashToCid(hex, HashAlgorithm.Blake2b256, CidCodec.Raw);
|
|
486
|
+
const dagPbCid = hashToCid(hex, HashAlgorithm.Blake2b256, CidCodec.DagPb);
|
|
487
|
+
expect(rawCid).not.toBe(dagPbCid);
|
|
488
|
+
expect(CID.parse(dagPbCid).code).toBe(CidCodec.DagPb);
|
|
489
|
+
});
|
|
490
|
+
test("dag-cbor codec works", () => {
|
|
491
|
+
const hex = `0x${"ab".repeat(32)}`;
|
|
492
|
+
const cid = hashToCid(hex, HashAlgorithm.Blake2b256, CidCodec.DagCbor);
|
|
493
|
+
expect(CID.parse(cid).code).toBe(CidCodec.DagCbor);
|
|
494
|
+
});
|
|
495
|
+
test("throws BulletinCidError for hex that is too short", () => {
|
|
496
|
+
expect(() => hashToCid("0xabcd")).toThrow(BulletinCidError);
|
|
497
|
+
});
|
|
498
|
+
test("throws BulletinCidError for hex that is too long", () => {
|
|
499
|
+
const tooLong = `0x${"aa".repeat(33)}`;
|
|
500
|
+
expect(() => hashToCid(tooLong)).toThrow(BulletinCidError);
|
|
501
|
+
});
|
|
502
|
+
test("throws for non-hex characters", () => {
|
|
503
|
+
const bad = `0x${"zz".repeat(32)}`;
|
|
504
|
+
expect(() => hashToCid(bad)).toThrow();
|
|
505
|
+
});
|
|
506
|
+
test("throws BulletinCidError for unsupported hash algorithm", () => {
|
|
507
|
+
const hex = `0x${"ab".repeat(32)}`;
|
|
508
|
+
expect(() => hashToCid(hex, 153)).toThrow(BulletinCidError);
|
|
509
|
+
});
|
|
510
|
+
test("throws BulletinCidError for unsupported codec", () => {
|
|
511
|
+
const hex = `0x${"ab".repeat(32)}`;
|
|
512
|
+
expect(() => hashToCid(hex, HashAlgorithm.Blake2b256, 153)).toThrow(
|
|
513
|
+
BulletinCidError
|
|
514
|
+
);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/gateway.ts
|
|
520
|
+
var GATEWAYS = {
|
|
521
|
+
paseo: "https://paseo-ipfs.polkadot.io/ipfs/"
|
|
522
|
+
};
|
|
523
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 3e4;
|
|
524
|
+
function getGateway(env) {
|
|
525
|
+
const gw = GATEWAYS[env];
|
|
526
|
+
if (!gw) {
|
|
527
|
+
throw new BulletinGatewayUnavailableError(env);
|
|
528
|
+
}
|
|
529
|
+
return gw;
|
|
530
|
+
}
|
|
531
|
+
function gatewayUrl(cid, gateway) {
|
|
532
|
+
return `${gateway}${cid}`;
|
|
533
|
+
}
|
|
534
|
+
async function cidExists(cid, gateway, options) {
|
|
535
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
536
|
+
const controller = new AbortController();
|
|
537
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
538
|
+
try {
|
|
539
|
+
const response = await fetch(gatewayUrl(cid, gateway), {
|
|
540
|
+
method: "HEAD",
|
|
541
|
+
signal: controller.signal
|
|
542
|
+
});
|
|
543
|
+
return response.ok;
|
|
544
|
+
} catch {
|
|
545
|
+
return false;
|
|
546
|
+
} finally {
|
|
547
|
+
clearTimeout(timer);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async function fetchBytes(cid, gateway, options) {
|
|
551
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
552
|
+
const controller = new AbortController();
|
|
553
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
554
|
+
try {
|
|
555
|
+
const response = await fetch(gatewayUrl(cid, gateway), { signal: controller.signal });
|
|
556
|
+
if (!response.ok) {
|
|
557
|
+
throw new BulletinGatewayFetchError(cid, response.status, response.statusText);
|
|
558
|
+
}
|
|
559
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
560
|
+
} finally {
|
|
561
|
+
clearTimeout(timer);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async function fetchJson(cid, gateway, options) {
|
|
565
|
+
const bytes = await fetchBytes(cid, gateway, options);
|
|
566
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
567
|
+
}
|
|
568
|
+
if (void 0) {
|
|
569
|
+
const { describe, test, expect, vi, afterEach } = void 0;
|
|
570
|
+
afterEach(() => {
|
|
571
|
+
vi.restoreAllMocks();
|
|
572
|
+
});
|
|
573
|
+
describe("getGateway", () => {
|
|
574
|
+
test("returns known URL for paseo", () => {
|
|
575
|
+
const gw = getGateway("paseo");
|
|
576
|
+
expect(gw).toMatch(/^https:\/\//);
|
|
577
|
+
expect(gw).toMatch(/\/ipfs\/$/);
|
|
578
|
+
});
|
|
579
|
+
test("throws BulletinGatewayUnavailableError for environments without a live gateway", () => {
|
|
580
|
+
expect(() => getGateway("polkadot")).toThrow(BulletinGatewayUnavailableError);
|
|
581
|
+
expect(() => getGateway("kusama")).toThrow(BulletinGatewayUnavailableError);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
describe("gatewayUrl", () => {
|
|
585
|
+
test("concatenates gateway and CID", () => {
|
|
586
|
+
expect(gatewayUrl("bafyabc", "https://gw.example/ipfs/")).toBe(
|
|
587
|
+
"https://gw.example/ipfs/bafyabc"
|
|
588
|
+
);
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
describe("cidExists", () => {
|
|
592
|
+
test("returns true for 200 response", async () => {
|
|
593
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
|
|
594
|
+
expect(await cidExists("bafyabc", "https://gw/ipfs/")).toBe(true);
|
|
595
|
+
});
|
|
596
|
+
test("returns false for 404 response", async () => {
|
|
597
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 404 }));
|
|
598
|
+
expect(await cidExists("bafyabc", "https://gw/ipfs/")).toBe(false);
|
|
599
|
+
});
|
|
600
|
+
test("returns false on network error", async () => {
|
|
601
|
+
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("network")));
|
|
602
|
+
expect(await cidExists("bafyabc", "https://gw/ipfs/")).toBe(false);
|
|
603
|
+
});
|
|
604
|
+
test("returns false on timeout", async () => {
|
|
605
|
+
vi.stubGlobal(
|
|
606
|
+
"fetch",
|
|
607
|
+
vi.fn().mockImplementation(
|
|
608
|
+
(_url, init) => new Promise((_resolve, reject) => {
|
|
609
|
+
init.signal.addEventListener(
|
|
610
|
+
"abort",
|
|
611
|
+
() => reject(new DOMException("aborted", "AbortError"))
|
|
612
|
+
);
|
|
613
|
+
})
|
|
614
|
+
)
|
|
615
|
+
);
|
|
616
|
+
expect(await cidExists("bafyabc", "https://gw/ipfs/", { timeoutMs: 10 })).toBe(false);
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
describe("fetchBytes", () => {
|
|
620
|
+
test("returns bytes from response", async () => {
|
|
621
|
+
const payload = new Uint8Array([1, 2, 3]);
|
|
622
|
+
vi.stubGlobal(
|
|
623
|
+
"fetch",
|
|
624
|
+
vi.fn().mockResolvedValue({
|
|
625
|
+
ok: true,
|
|
626
|
+
arrayBuffer: () => Promise.resolve(payload.buffer)
|
|
627
|
+
})
|
|
628
|
+
);
|
|
629
|
+
const result = await fetchBytes("bafyabc", "https://gw/ipfs/");
|
|
630
|
+
expect(result).toEqual(payload);
|
|
631
|
+
});
|
|
632
|
+
test("throws BulletinGatewayFetchError on non-ok response", async () => {
|
|
633
|
+
vi.stubGlobal(
|
|
634
|
+
"fetch",
|
|
635
|
+
vi.fn().mockResolvedValue({
|
|
636
|
+
ok: false,
|
|
637
|
+
status: 500,
|
|
638
|
+
statusText: "Internal Server Error"
|
|
639
|
+
})
|
|
640
|
+
);
|
|
641
|
+
const err = await fetchBytes("bafyabc", "https://gw/ipfs/").catch((e) => e);
|
|
642
|
+
expect(err).toBeInstanceOf(BulletinGatewayFetchError);
|
|
643
|
+
expect(err.cid).toBe("bafyabc");
|
|
644
|
+
expect(err.status).toBe(500);
|
|
645
|
+
});
|
|
646
|
+
test("throws on timeout", async () => {
|
|
647
|
+
vi.stubGlobal(
|
|
648
|
+
"fetch",
|
|
649
|
+
vi.fn().mockImplementation(
|
|
650
|
+
(_url, init) => new Promise((_resolve, reject) => {
|
|
651
|
+
init.signal.addEventListener(
|
|
652
|
+
"abort",
|
|
653
|
+
() => reject(new DOMException("aborted", "AbortError"))
|
|
654
|
+
);
|
|
655
|
+
})
|
|
656
|
+
)
|
|
657
|
+
);
|
|
658
|
+
await expect(
|
|
659
|
+
fetchBytes("bafyabc", "https://gw/ipfs/", { timeoutMs: 10 })
|
|
660
|
+
).rejects.toThrow();
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
describe("fetchJson", () => {
|
|
664
|
+
test("parses JSON from response", async () => {
|
|
665
|
+
const obj = { name: "test", value: 42 };
|
|
666
|
+
const bytes = new TextEncoder().encode(JSON.stringify(obj));
|
|
667
|
+
vi.stubGlobal(
|
|
668
|
+
"fetch",
|
|
669
|
+
vi.fn().mockResolvedValue({
|
|
670
|
+
ok: true,
|
|
671
|
+
arrayBuffer: () => Promise.resolve(bytes.buffer)
|
|
672
|
+
})
|
|
673
|
+
);
|
|
674
|
+
const result = await fetchJson(
|
|
675
|
+
"bafyabc",
|
|
676
|
+
"https://gw/ipfs/"
|
|
677
|
+
);
|
|
678
|
+
expect(result).toEqual(obj);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// src/query.ts
|
|
684
|
+
import { createLogger as createLogger4 } from "@parity/product-sdk-logger";
|
|
685
|
+
|
|
686
|
+
// src/resolve-query.ts
|
|
687
|
+
import { getPreimageManager } from "@parity/product-sdk-host";
|
|
688
|
+
import { createLogger as createLogger3 } from "@parity/product-sdk-logger";
|
|
689
|
+
var log3 = createLogger3("bulletin");
|
|
690
|
+
var DEFAULT_LOOKUP_TIMEOUT_MS = 3e4;
|
|
691
|
+
async function resolveQueryStrategy() {
|
|
692
|
+
const preimageManager = await getPreimageManager();
|
|
693
|
+
if (preimageManager) {
|
|
694
|
+
log3.info("using host preimage lookup for bulletin queries");
|
|
695
|
+
return {
|
|
696
|
+
kind: "host-lookup",
|
|
697
|
+
lookup: (cid, timeoutMs) => lookupViaHost(preimageManager, cid, timeoutMs)
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
throw new BulletinHostUnavailableError("query");
|
|
701
|
+
}
|
|
702
|
+
function lookupViaHost(manager, cid, timeoutMs = DEFAULT_LOOKUP_TIMEOUT_MS) {
|
|
703
|
+
const key = cidToPreimageKey(cid);
|
|
704
|
+
return new Promise((resolve, reject) => {
|
|
705
|
+
const cleanup = () => {
|
|
706
|
+
cancelInterrupt();
|
|
707
|
+
sub.unsubscribe();
|
|
708
|
+
};
|
|
709
|
+
const settle = (fn) => {
|
|
710
|
+
if (timer === null) return;
|
|
711
|
+
clearTimeout(timer);
|
|
712
|
+
timer = null;
|
|
713
|
+
cleanup();
|
|
714
|
+
fn();
|
|
715
|
+
};
|
|
716
|
+
let timer = setTimeout(() => {
|
|
717
|
+
settle(() => {
|
|
718
|
+
reject(new BulletinLookupTimeoutError(cid, timeoutMs));
|
|
719
|
+
});
|
|
720
|
+
}, timeoutMs);
|
|
721
|
+
const sub = manager.lookup(key, (preimage) => {
|
|
722
|
+
if (preimage !== null) {
|
|
723
|
+
settle(() => resolve(preimage));
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
const cancelInterrupt = sub.onInterrupt(() => {
|
|
727
|
+
settle(() => {
|
|
728
|
+
reject(new BulletinLookupInterruptedError(cid));
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
if (void 0) {
|
|
734
|
+
const { describe, test, expect, vi } = void 0;
|
|
735
|
+
describe("lookupViaHost", () => {
|
|
736
|
+
function createMockManager(behavior) {
|
|
737
|
+
const unsubscribe = vi.fn();
|
|
738
|
+
const cancelInterrupt = vi.fn();
|
|
739
|
+
let interruptCb;
|
|
740
|
+
const lookup = vi.fn((_key, callback) => {
|
|
741
|
+
const data = new Uint8Array([10, 20, 30]);
|
|
742
|
+
queueMicrotask(() => {
|
|
743
|
+
if (behavior === "resolve") {
|
|
744
|
+
callback(data);
|
|
745
|
+
} else if (behavior === "null-then-resolve") {
|
|
746
|
+
callback(null);
|
|
747
|
+
queueMicrotask(() => callback(data));
|
|
748
|
+
} else if (behavior === "interrupt") {
|
|
749
|
+
interruptCb?.();
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
return {
|
|
753
|
+
unsubscribe,
|
|
754
|
+
onInterrupt: (cb) => {
|
|
755
|
+
interruptCb = cb;
|
|
756
|
+
return cancelInterrupt;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
});
|
|
760
|
+
return { lookup, unsubscribe, cancelInterrupt, submit: vi.fn() };
|
|
761
|
+
}
|
|
762
|
+
const testCid = computeCid2(new TextEncoder().encode("test"));
|
|
763
|
+
test("resolves on first non-null callback", async () => {
|
|
764
|
+
const manager = createMockManager("resolve");
|
|
765
|
+
const result = await lookupViaHost(manager, testCid);
|
|
766
|
+
expect(result).toEqual(new Uint8Array([10, 20, 30]));
|
|
767
|
+
});
|
|
768
|
+
test("ignores null callbacks and resolves on subsequent data", async () => {
|
|
769
|
+
const manager = createMockManager("null-then-resolve");
|
|
770
|
+
const result = await lookupViaHost(manager, testCid);
|
|
771
|
+
expect(result).toEqual(new Uint8Array([10, 20, 30]));
|
|
772
|
+
});
|
|
773
|
+
test("rejects with BulletinLookupTimeoutError on timeout", async () => {
|
|
774
|
+
const { BulletinLookupTimeoutError: BulletinLookupTimeoutError2 } = await null;
|
|
775
|
+
const manager = createMockManager("hang");
|
|
776
|
+
const err = await lookupViaHost(manager, testCid, 50).catch((e) => e);
|
|
777
|
+
expect(err).toBeInstanceOf(BulletinLookupTimeoutError2);
|
|
778
|
+
expect(err.cid).toBe(testCid);
|
|
779
|
+
expect(err.timeoutMs).toBe(50);
|
|
780
|
+
});
|
|
781
|
+
test("rejects with BulletinLookupInterruptedError on interrupt", async () => {
|
|
782
|
+
const { BulletinLookupInterruptedError: BulletinLookupInterruptedError2 } = await null;
|
|
783
|
+
const manager = createMockManager("interrupt");
|
|
784
|
+
const err = await lookupViaHost(manager, testCid).catch((e) => e);
|
|
785
|
+
expect(err).toBeInstanceOf(BulletinLookupInterruptedError2);
|
|
786
|
+
expect(err.cid).toBe(testCid);
|
|
787
|
+
});
|
|
788
|
+
test("calls unsubscribe and cancelInterrupt on resolution", async () => {
|
|
789
|
+
const manager = createMockManager("resolve");
|
|
790
|
+
await lookupViaHost(manager, testCid);
|
|
791
|
+
expect(manager.unsubscribe).toHaveBeenCalledOnce();
|
|
792
|
+
expect(manager.cancelInterrupt).toHaveBeenCalledOnce();
|
|
793
|
+
});
|
|
794
|
+
test("calls unsubscribe on interrupt", async () => {
|
|
795
|
+
const manager = createMockManager("interrupt");
|
|
796
|
+
await lookupViaHost(manager, testCid).catch(() => {
|
|
797
|
+
});
|
|
798
|
+
expect(manager.unsubscribe).toHaveBeenCalledOnce();
|
|
799
|
+
});
|
|
800
|
+
test("passes correct hex key to manager", async () => {
|
|
801
|
+
const expectedKey = cidToPreimageKey(testCid);
|
|
802
|
+
const manager = createMockManager("resolve");
|
|
803
|
+
await lookupViaHost(manager, testCid);
|
|
804
|
+
expect(manager.lookup).toHaveBeenCalledWith(expectedKey, expect.any(Function));
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/query.ts
|
|
810
|
+
var log4 = createLogger4("bulletin");
|
|
811
|
+
async function queryBytes(cid, options) {
|
|
812
|
+
const strategy = await resolveQueryStrategy();
|
|
813
|
+
return executeQuery(strategy, cid, options);
|
|
814
|
+
}
|
|
815
|
+
async function queryJson(cid, options) {
|
|
816
|
+
const bytes = await queryBytes(cid, options);
|
|
817
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
818
|
+
}
|
|
819
|
+
async function executeQuery(strategy, cid, options) {
|
|
820
|
+
log4.info("querying via host preimage lookup", { cid });
|
|
821
|
+
return strategy.lookup(cid, options?.lookupTimeoutMs);
|
|
822
|
+
}
|
|
823
|
+
if (void 0) {
|
|
824
|
+
const { describe, test, expect, vi } = void 0;
|
|
825
|
+
describe("executeQuery", () => {
|
|
826
|
+
const testData = new Uint8Array([1, 2, 3]);
|
|
827
|
+
test("executes host-lookup strategy", async () => {
|
|
828
|
+
const lookup = vi.fn().mockResolvedValue(testData);
|
|
829
|
+
const strategy = { kind: "host-lookup", lookup };
|
|
830
|
+
const result = await executeQuery(strategy, "bafytest");
|
|
831
|
+
expect(result).toBe(testData);
|
|
832
|
+
expect(lookup).toHaveBeenCalledWith("bafytest", void 0);
|
|
833
|
+
});
|
|
834
|
+
test("passes lookupTimeoutMs to host-lookup", async () => {
|
|
835
|
+
const lookup = vi.fn().mockResolvedValue(testData);
|
|
836
|
+
const strategy = { kind: "host-lookup", lookup };
|
|
837
|
+
await executeQuery(strategy, "bafytest", { lookupTimeoutMs: 5e3 });
|
|
838
|
+
expect(lookup).toHaveBeenCalledWith("bafytest", 5e3);
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/upload.ts
|
|
844
|
+
import { createLogger as createLogger6 } from "@parity/product-sdk-logger";
|
|
845
|
+
import { submitAndWatch, withRetry } from "@parity/product-sdk-tx";
|
|
846
|
+
import { Binary } from "polkadot-api";
|
|
847
|
+
|
|
848
|
+
// src/resolve-signer.ts
|
|
849
|
+
import { getPreimageManager as getPreimageManager2 } from "@parity/product-sdk-host";
|
|
850
|
+
import { createLogger as createLogger5 } from "@parity/product-sdk-logger";
|
|
851
|
+
var log5 = createLogger5("bulletin");
|
|
852
|
+
async function resolveUploadStrategy(explicitSigner) {
|
|
853
|
+
if (explicitSigner) {
|
|
854
|
+
log5.debug("using explicit signer provided by caller");
|
|
855
|
+
return { kind: "signer", signer: explicitSigner };
|
|
856
|
+
}
|
|
857
|
+
const preimageManager = await getPreimageManager2();
|
|
858
|
+
if (preimageManager) {
|
|
859
|
+
log5.info("using host preimage API for bulletin upload");
|
|
860
|
+
return { kind: "preimage", submit: (data) => preimageManager.submit(data) };
|
|
861
|
+
}
|
|
862
|
+
throw new BulletinHostUnavailableError("upload");
|
|
863
|
+
}
|
|
864
|
+
if (void 0) {
|
|
865
|
+
const { describe, test, expect, vi } = void 0;
|
|
866
|
+
describe("resolveUploadStrategy", () => {
|
|
867
|
+
test("returns explicit signer when provided", async () => {
|
|
868
|
+
const signer = { publicKey: new Uint8Array(32) };
|
|
869
|
+
const strategy = await resolveUploadStrategy(signer);
|
|
870
|
+
expect(strategy.kind).toBe("signer");
|
|
871
|
+
if (strategy.kind === "signer") {
|
|
872
|
+
expect(strategy.signer).toBe(signer);
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/upload.ts
|
|
879
|
+
var log6 = createLogger6("bulletin");
|
|
880
|
+
async function upload(api, data, signer, options) {
|
|
881
|
+
const strategy = await resolveUploadStrategy(signer);
|
|
882
|
+
const cid = computeCid(data);
|
|
883
|
+
if (strategy.kind === "preimage") {
|
|
884
|
+
log6.info("uploading via host preimage API", { cid, size: data.byteLength });
|
|
885
|
+
const preimageKey = await strategy.submit(data);
|
|
886
|
+
log6.info("preimage submitted successfully", { cid, preimageKey });
|
|
887
|
+
return {
|
|
888
|
+
kind: "preimage",
|
|
889
|
+
cid,
|
|
890
|
+
preimageKey,
|
|
891
|
+
gatewayUrl: options?.gateway ? gatewayUrl(cid, options.gateway) : void 0
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
log6.info("uploading via TransactionStorage.store", { cid, size: data.byteLength });
|
|
895
|
+
const result = await withRetry(() => {
|
|
896
|
+
const tx = api.tx.TransactionStorage.store({ data: Binary.fromBytes(data) });
|
|
897
|
+
return submitAndWatch(tx, strategy.signer, {
|
|
898
|
+
waitFor: options?.waitFor,
|
|
899
|
+
timeoutMs: options?.timeoutMs,
|
|
900
|
+
onStatus: options?.onStatus
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
log6.info("transaction included in block", { cid, blockHash: result.block.hash });
|
|
904
|
+
return {
|
|
905
|
+
kind: "transaction",
|
|
906
|
+
cid,
|
|
907
|
+
blockHash: result.block.hash,
|
|
908
|
+
gatewayUrl: options?.gateway ? gatewayUrl(cid, options.gateway) : void 0
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
async function batchUpload(api, items, signer, options) {
|
|
912
|
+
if (items.length === 0) return [];
|
|
913
|
+
const strategy = await resolveUploadStrategy(signer);
|
|
914
|
+
const results = [];
|
|
915
|
+
for (let i = 0; i < items.length; i++) {
|
|
916
|
+
const item = items[i];
|
|
917
|
+
const cid = computeCid(item.data);
|
|
918
|
+
try {
|
|
919
|
+
if (strategy.kind === "preimage") {
|
|
920
|
+
log6.info("batch: uploading item via preimage", {
|
|
921
|
+
label: item.label,
|
|
922
|
+
index: i,
|
|
923
|
+
total: items.length
|
|
924
|
+
});
|
|
925
|
+
const preimageKey = await strategy.submit(item.data);
|
|
926
|
+
const entry = {
|
|
927
|
+
kind: "preimage",
|
|
928
|
+
label: item.label,
|
|
929
|
+
cid,
|
|
930
|
+
success: true,
|
|
931
|
+
preimageKey,
|
|
932
|
+
gatewayUrl: options?.gateway ? gatewayUrl(cid, options.gateway) : void 0
|
|
933
|
+
};
|
|
934
|
+
results.push(entry);
|
|
935
|
+
options?.onProgress?.(i + 1, items.length, entry);
|
|
936
|
+
} else {
|
|
937
|
+
log6.info("batch: uploading item via transaction", {
|
|
938
|
+
label: item.label,
|
|
939
|
+
index: i,
|
|
940
|
+
total: items.length
|
|
941
|
+
});
|
|
942
|
+
const result = await withRetry(() => {
|
|
943
|
+
const tx = api.tx.TransactionStorage.store({
|
|
944
|
+
data: Binary.fromBytes(item.data)
|
|
945
|
+
});
|
|
946
|
+
return submitAndWatch(tx, strategy.signer, {
|
|
947
|
+
waitFor: options?.waitFor,
|
|
948
|
+
timeoutMs: options?.timeoutMs
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
const entry = {
|
|
952
|
+
kind: "transaction",
|
|
953
|
+
label: item.label,
|
|
954
|
+
cid,
|
|
955
|
+
success: true,
|
|
956
|
+
blockHash: result.block.hash,
|
|
957
|
+
gatewayUrl: options?.gateway ? gatewayUrl(cid, options.gateway) : void 0
|
|
958
|
+
};
|
|
959
|
+
results.push(entry);
|
|
960
|
+
options?.onProgress?.(i + 1, items.length, entry);
|
|
961
|
+
}
|
|
962
|
+
} catch (err) {
|
|
963
|
+
log6.error("batch: item upload failed", {
|
|
964
|
+
label: item.label,
|
|
965
|
+
index: i,
|
|
966
|
+
error: err instanceof Error ? err.message : String(err)
|
|
967
|
+
});
|
|
968
|
+
const entry = {
|
|
969
|
+
kind: strategy.kind === "preimage" ? "preimage" : "transaction",
|
|
970
|
+
label: item.label,
|
|
971
|
+
cid,
|
|
972
|
+
success: false,
|
|
973
|
+
error: err instanceof Error ? err.message : String(err)
|
|
974
|
+
};
|
|
975
|
+
results.push(entry);
|
|
976
|
+
options?.onProgress?.(i + 1, items.length, entry);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return results;
|
|
980
|
+
}
|
|
981
|
+
if (void 0) {
|
|
982
|
+
let createMockApi = function() {
|
|
983
|
+
return {
|
|
984
|
+
tx: {
|
|
985
|
+
TransactionStorage: {
|
|
986
|
+
store: vi.fn().mockReturnValue({
|
|
987
|
+
signSubmitAndWatch: () => ({
|
|
988
|
+
subscribe: (handlers) => {
|
|
989
|
+
queueMicrotask(() => {
|
|
990
|
+
handlers.next({ type: "signed", txHash: "0xtxhash" });
|
|
991
|
+
handlers.next({
|
|
992
|
+
type: "txBestBlocksState",
|
|
993
|
+
txHash: "0xtxhash",
|
|
994
|
+
found: true,
|
|
995
|
+
ok: true,
|
|
996
|
+
block: { hash: "0xblockhash", number: 1, index: 0 },
|
|
997
|
+
events: []
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
return { unsubscribe: vi.fn() };
|
|
1001
|
+
}
|
|
1002
|
+
})
|
|
1003
|
+
})
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
};
|
|
1008
|
+
createMockApi2 = createMockApi;
|
|
1009
|
+
const { describe, test, expect, vi } = void 0;
|
|
1010
|
+
const mockSigner = {};
|
|
1011
|
+
describe("upload", () => {
|
|
1012
|
+
test("calls TransactionStorage.store and returns CID + blockHash with explicit signer", async () => {
|
|
1013
|
+
const api = createMockApi();
|
|
1014
|
+
const data = new TextEncoder().encode("test data");
|
|
1015
|
+
const result = await upload(api, data, mockSigner);
|
|
1016
|
+
expect(api.tx.TransactionStorage.store).toHaveBeenCalledOnce();
|
|
1017
|
+
expect(result.kind).toBe("transaction");
|
|
1018
|
+
expect(result.cid).toBeTruthy();
|
|
1019
|
+
if (result.kind === "transaction") {
|
|
1020
|
+
expect(result.blockHash).toBe("0xblockhash");
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
test("includes gatewayUrl when gateway option provided", async () => {
|
|
1024
|
+
const api = createMockApi();
|
|
1025
|
+
const data = new TextEncoder().encode("test");
|
|
1026
|
+
const result = await upload(api, data, mockSigner, {
|
|
1027
|
+
gateway: "https://gw/ipfs/"
|
|
1028
|
+
});
|
|
1029
|
+
expect(result.gatewayUrl).toBe(`https://gw/ipfs/${result.cid}`);
|
|
1030
|
+
});
|
|
1031
|
+
test("omits gatewayUrl when no gateway option", async () => {
|
|
1032
|
+
const api = createMockApi();
|
|
1033
|
+
const data = new TextEncoder().encode("test");
|
|
1034
|
+
const result = await upload(api, data, mockSigner);
|
|
1035
|
+
expect(result.gatewayUrl).toBeUndefined();
|
|
1036
|
+
});
|
|
1037
|
+
});
|
|
1038
|
+
describe("batchUpload", () => {
|
|
1039
|
+
test("returns empty array for empty items", async () => {
|
|
1040
|
+
const api = createMockApi();
|
|
1041
|
+
const results = await batchUpload(api, [], mockSigner);
|
|
1042
|
+
expect(results).toEqual([]);
|
|
1043
|
+
});
|
|
1044
|
+
test("processes items sequentially with explicit signer", async () => {
|
|
1045
|
+
const api = createMockApi();
|
|
1046
|
+
const items = [
|
|
1047
|
+
{ data: new TextEncoder().encode("a"), label: "file-a" },
|
|
1048
|
+
{ data: new TextEncoder().encode("b"), label: "file-b" }
|
|
1049
|
+
];
|
|
1050
|
+
const results = await batchUpload(api, items, mockSigner);
|
|
1051
|
+
expect(results).toHaveLength(2);
|
|
1052
|
+
expect(results[0].kind).toBe("transaction");
|
|
1053
|
+
expect(results[0].label).toBe("file-a");
|
|
1054
|
+
expect(results[0].success).toBe(true);
|
|
1055
|
+
expect(results[1].kind).toBe("transaction");
|
|
1056
|
+
expect(results[1].label).toBe("file-b");
|
|
1057
|
+
expect(results[1].success).toBe(true);
|
|
1058
|
+
});
|
|
1059
|
+
test("captures individual failures without aborting batch", async () => {
|
|
1060
|
+
const api = createMockApi();
|
|
1061
|
+
let callCount = 0;
|
|
1062
|
+
api.tx.TransactionStorage.store.mockImplementation(() => {
|
|
1063
|
+
callCount++;
|
|
1064
|
+
if (callCount === 2) {
|
|
1065
|
+
return {
|
|
1066
|
+
signSubmitAndWatch: () => ({
|
|
1067
|
+
subscribe: (handlers) => {
|
|
1068
|
+
queueMicrotask(() => {
|
|
1069
|
+
handlers.next({ type: "signed", txHash: "0x" });
|
|
1070
|
+
handlers.next({
|
|
1071
|
+
type: "txBestBlocksState",
|
|
1072
|
+
txHash: "0x",
|
|
1073
|
+
found: true,
|
|
1074
|
+
ok: false,
|
|
1075
|
+
block: { hash: "0xblock", number: 1, index: 0 },
|
|
1076
|
+
events: [],
|
|
1077
|
+
dispatchError: { type: "BadOrigin" }
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
return { unsubscribe: vi.fn() };
|
|
1081
|
+
}
|
|
1082
|
+
})
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
signSubmitAndWatch: () => ({
|
|
1087
|
+
subscribe: (handlers) => {
|
|
1088
|
+
queueMicrotask(() => {
|
|
1089
|
+
handlers.next({ type: "signed", txHash: "0x" });
|
|
1090
|
+
handlers.next({
|
|
1091
|
+
type: "txBestBlocksState",
|
|
1092
|
+
txHash: "0x",
|
|
1093
|
+
found: true,
|
|
1094
|
+
ok: true,
|
|
1095
|
+
block: { hash: "0xblock", number: 1, index: 0 },
|
|
1096
|
+
events: []
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
return { unsubscribe: vi.fn() };
|
|
1100
|
+
}
|
|
1101
|
+
})
|
|
1102
|
+
};
|
|
1103
|
+
});
|
|
1104
|
+
const items = [
|
|
1105
|
+
{ data: new TextEncoder().encode("a"), label: "ok" },
|
|
1106
|
+
{ data: new TextEncoder().encode("b"), label: "fail" },
|
|
1107
|
+
{ data: new TextEncoder().encode("c"), label: "ok2" }
|
|
1108
|
+
];
|
|
1109
|
+
const results = await batchUpload(api, items, mockSigner);
|
|
1110
|
+
expect(results).toHaveLength(3);
|
|
1111
|
+
expect(results[0].kind).toBe("transaction");
|
|
1112
|
+
expect(results[0].success).toBe(true);
|
|
1113
|
+
const f1 = results[1];
|
|
1114
|
+
expect(f1.kind).toBe("transaction");
|
|
1115
|
+
expect(f1.success).toBe(false);
|
|
1116
|
+
if (!f1.success) expect(f1.error).toContain("BadOrigin");
|
|
1117
|
+
expect(results[2].success).toBe(true);
|
|
1118
|
+
});
|
|
1119
|
+
test("calls onProgress for each item", async () => {
|
|
1120
|
+
const api = createMockApi();
|
|
1121
|
+
const items = [
|
|
1122
|
+
{ data: new TextEncoder().encode("a"), label: "a" },
|
|
1123
|
+
{ data: new TextEncoder().encode("b"), label: "b" }
|
|
1124
|
+
];
|
|
1125
|
+
const progress = [];
|
|
1126
|
+
await batchUpload(api, items, mockSigner, {
|
|
1127
|
+
onProgress: (done, total, current) => progress.push([done, total, current.label])
|
|
1128
|
+
});
|
|
1129
|
+
expect(progress).toEqual([
|
|
1130
|
+
[1, 2, "a"],
|
|
1131
|
+
[2, 2, "b"]
|
|
1132
|
+
]);
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
var createMockApi2;
|
|
1137
|
+
|
|
1138
|
+
// src/client.ts
|
|
1139
|
+
var BulletinClient = class _BulletinClient {
|
|
1140
|
+
api;
|
|
1141
|
+
gateway;
|
|
1142
|
+
queryStrategyPromise = null;
|
|
1143
|
+
constructor(api, gateway) {
|
|
1144
|
+
this.api = api;
|
|
1145
|
+
this.gateway = gateway;
|
|
1146
|
+
}
|
|
1147
|
+
/** Lazily resolve and cache the query strategy for the client lifetime. */
|
|
1148
|
+
resolveQuery() {
|
|
1149
|
+
if (!this.queryStrategyPromise) {
|
|
1150
|
+
this.queryStrategyPromise = resolveQueryStrategy();
|
|
1151
|
+
}
|
|
1152
|
+
return this.queryStrategyPromise;
|
|
1153
|
+
}
|
|
1154
|
+
/** Create from an environment — resolves API via chain-client, gateway from known list. */
|
|
1155
|
+
static async create(env) {
|
|
1156
|
+
const chain = await getChainAPI(env);
|
|
1157
|
+
return new _BulletinClient(chain.bulletin, getGateway(env));
|
|
1158
|
+
}
|
|
1159
|
+
/** Create from an explicit API and gateway (custom setups, testing). */
|
|
1160
|
+
static from(api, gateway) {
|
|
1161
|
+
return new _BulletinClient(api, gateway);
|
|
1162
|
+
}
|
|
1163
|
+
/** Compute CID without uploading. Static — no instance needed. */
|
|
1164
|
+
static computeCid(data) {
|
|
1165
|
+
return computeCid(data);
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Reconstruct a CID from a `0x`-prefixed hex hash. Static — no instance needed.
|
|
1169
|
+
*
|
|
1170
|
+
* Useful for converting on-chain hashes back to CIDs for IPFS gateway lookups.
|
|
1171
|
+
* Pass optional hash algorithm and codec to match the on-chain CID configuration.
|
|
1172
|
+
*
|
|
1173
|
+
* @see {@link hashToCid} for full documentation.
|
|
1174
|
+
*/
|
|
1175
|
+
static hashToCid(hexHash, hashCode, codec) {
|
|
1176
|
+
return hashToCid(hexHash, hashCode, codec);
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Upload data to the Bulletin Chain.
|
|
1180
|
+
*
|
|
1181
|
+
* @param data - Raw bytes to store.
|
|
1182
|
+
* @param signer - Optional signer. When omitted, uses the host preimage API.
|
|
1183
|
+
* @param options - Upload options (timeout, waitFor, status callback).
|
|
1184
|
+
*/
|
|
1185
|
+
async upload(data, signer, options) {
|
|
1186
|
+
return upload(this.api, data, signer, { ...options, gateway: this.gateway });
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Upload multiple items sequentially.
|
|
1190
|
+
*
|
|
1191
|
+
* @param items - Array of items to upload, each with data and a label.
|
|
1192
|
+
* @param signer - Optional signer. When omitted, auto-resolved.
|
|
1193
|
+
* @param options - Batch upload options (timeout, progress callback).
|
|
1194
|
+
*/
|
|
1195
|
+
async batchUpload(items, signer, options) {
|
|
1196
|
+
return batchUpload(this.api, items, signer, { ...options, gateway: this.gateway });
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Fetch raw bytes by CID.
|
|
1200
|
+
*
|
|
1201
|
+
* Uses host preimage lookup with caching.
|
|
1202
|
+
*/
|
|
1203
|
+
async fetchBytes(cid, options) {
|
|
1204
|
+
const strategy = await this.resolveQuery();
|
|
1205
|
+
return executeQuery(strategy, cid, options);
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Fetch and parse JSON by CID.
|
|
1209
|
+
*
|
|
1210
|
+
* Auto-resolves query path (same as {@link fetchBytes}).
|
|
1211
|
+
*/
|
|
1212
|
+
async fetchJson(cid, options) {
|
|
1213
|
+
const bytes = await this.fetchBytes(cid, options);
|
|
1214
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
1215
|
+
}
|
|
1216
|
+
/** Check if a CID exists on the gateway. */
|
|
1217
|
+
async cidExists(cid) {
|
|
1218
|
+
return cidExists(cid, this.gateway);
|
|
1219
|
+
}
|
|
1220
|
+
/** Build the full gateway URL for a CID. */
|
|
1221
|
+
gatewayUrl(cid) {
|
|
1222
|
+
return gatewayUrl(cid, this.gateway);
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Check whether an account is authorized to store data on the Bulletin Chain.
|
|
1226
|
+
*
|
|
1227
|
+
* Use as a pre-flight check before {@link upload} to provide clear UX
|
|
1228
|
+
* instead of letting the transaction fail mid-execution.
|
|
1229
|
+
*
|
|
1230
|
+
* @param address - SS58-encoded account address to check.
|
|
1231
|
+
* @returns Authorization status with remaining quota.
|
|
1232
|
+
*
|
|
1233
|
+
* @see {@link checkAuthorization} for the standalone function equivalent.
|
|
1234
|
+
*/
|
|
1235
|
+
async checkAuthorization(address) {
|
|
1236
|
+
return checkAuthorization(this.api, address);
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
if (void 0) {
|
|
1240
|
+
const { describe, test, expect, vi } = void 0;
|
|
1241
|
+
const mockApi = {
|
|
1242
|
+
tx: {
|
|
1243
|
+
TransactionStorage: {
|
|
1244
|
+
store: vi.fn().mockReturnValue({
|
|
1245
|
+
signSubmitAndWatch: () => ({
|
|
1246
|
+
subscribe: (handlers) => {
|
|
1247
|
+
queueMicrotask(() => {
|
|
1248
|
+
handlers.next({ type: "signed", txHash: "0x" });
|
|
1249
|
+
handlers.next({
|
|
1250
|
+
type: "txBestBlocksState",
|
|
1251
|
+
txHash: "0x",
|
|
1252
|
+
found: true,
|
|
1253
|
+
ok: true,
|
|
1254
|
+
block: { hash: "0xblock", number: 1, index: 0 },
|
|
1255
|
+
events: []
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
return { unsubscribe: vi.fn() };
|
|
1259
|
+
}
|
|
1260
|
+
})
|
|
1261
|
+
})
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
const GATEWAY = "https://test-gw/ipfs/";
|
|
1266
|
+
describe("BulletinClient", () => {
|
|
1267
|
+
test("from() creates client with given API and gateway", () => {
|
|
1268
|
+
const client = BulletinClient.from(mockApi, GATEWAY);
|
|
1269
|
+
expect(client.api).toBe(mockApi);
|
|
1270
|
+
expect(client.gateway).toBe(GATEWAY);
|
|
1271
|
+
});
|
|
1272
|
+
test("computeCid() is static and delegates to standalone", () => {
|
|
1273
|
+
const data = new TextEncoder().encode("hello");
|
|
1274
|
+
const cid = BulletinClient.computeCid(data);
|
|
1275
|
+
expect(cid).toBe(computeCid(data));
|
|
1276
|
+
});
|
|
1277
|
+
test("hashToCid() is static and delegates to standalone", () => {
|
|
1278
|
+
const data = new TextEncoder().encode("hello");
|
|
1279
|
+
const cid = computeCid(data);
|
|
1280
|
+
const key = cidToPreimageKey2(cid);
|
|
1281
|
+
expect(BulletinClient.hashToCid(key)).toBe(cid);
|
|
1282
|
+
});
|
|
1283
|
+
test("gatewayUrl() returns gateway + cid", () => {
|
|
1284
|
+
const client = BulletinClient.from(mockApi, GATEWAY);
|
|
1285
|
+
expect(client.gatewayUrl("bafyabc")).toBe("https://test-gw/ipfs/bafyabc");
|
|
1286
|
+
});
|
|
1287
|
+
test("upload() passes gateway from client with explicit signer", async () => {
|
|
1288
|
+
const client = BulletinClient.from(mockApi, GATEWAY);
|
|
1289
|
+
const data = new TextEncoder().encode("test");
|
|
1290
|
+
const result = await client.upload(data, {});
|
|
1291
|
+
expect(result.gatewayUrl).toContain(GATEWAY);
|
|
1292
|
+
expect(result.cid).toBeTruthy();
|
|
1293
|
+
});
|
|
1294
|
+
test("checkAuthorization delegates to standalone", async () => {
|
|
1295
|
+
const authMockApi = {
|
|
1296
|
+
...mockApi,
|
|
1297
|
+
query: {
|
|
1298
|
+
TransactionStorage: {
|
|
1299
|
+
Authorizations: {
|
|
1300
|
+
getValue: vi.fn().mockResolvedValue({
|
|
1301
|
+
extent: { transactions: 5, bytes: 2000n },
|
|
1302
|
+
expiration: 100
|
|
1303
|
+
})
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
const client = BulletinClient.from(authMockApi, GATEWAY);
|
|
1309
|
+
const status = await client.checkAuthorization("5GrwvaEF...");
|
|
1310
|
+
expect(status.authorized).toBe(true);
|
|
1311
|
+
expect(status.remainingTransactions).toBe(5);
|
|
1312
|
+
expect(status.remainingBytes).toBe(2000n);
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
export {
|
|
1317
|
+
BulletinAuthorizationError,
|
|
1318
|
+
BulletinCidError,
|
|
1319
|
+
BulletinClient,
|
|
1320
|
+
BulletinError,
|
|
1321
|
+
BulletinGatewayFetchError,
|
|
1322
|
+
BulletinGatewayUnavailableError,
|
|
1323
|
+
BulletinHostUnavailableError,
|
|
1324
|
+
BulletinLookupInterruptedError,
|
|
1325
|
+
BulletinLookupTimeoutError,
|
|
1326
|
+
CidCodec,
|
|
1327
|
+
HashAlgorithm,
|
|
1328
|
+
batchUpload,
|
|
1329
|
+
checkAuthorization,
|
|
1330
|
+
cidExists,
|
|
1331
|
+
cidToPreimageKey,
|
|
1332
|
+
computeCid,
|
|
1333
|
+
fetchBytes,
|
|
1334
|
+
fetchJson,
|
|
1335
|
+
gatewayUrl,
|
|
1336
|
+
getGateway,
|
|
1337
|
+
hashToCid,
|
|
1338
|
+
queryBytes,
|
|
1339
|
+
queryJson,
|
|
1340
|
+
resolveQueryStrategy,
|
|
1341
|
+
resolveUploadStrategy,
|
|
1342
|
+
upload
|
|
1343
|
+
};
|
|
1344
|
+
//# sourceMappingURL=index.js.map
|