@parity/product-sdk-bulletin 0.1.0 → 0.2.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/dist/index.d.ts +445 -396
- package/dist/index.js +306 -1119
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/authorization.ts +300 -1
- package/src/cid.ts +115 -239
- package/src/client.ts +238 -179
- package/src/errors.ts +65 -138
- package/src/index.ts +85 -20
- package/src/lazy-signer.ts +113 -0
- package/src/networks.ts +49 -0
- package/src/query.ts +165 -31
- package/src/resolve-query.ts +9 -3
- package/src/types.ts +26 -117
- package/src/verify.ts +384 -0
- package/src/gateway.ts +0 -209
- package/src/resolve-signer.ts +0 -66
- package/src/upload.ts +0 -344
package/dist/index.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import { Enum } from
|
|
1
|
+
import { parseCid, CidCodec as CidCodec$1, UnixFsDagBuilder, AsyncBulletinClient } from '@parity/bulletin-sdk';
|
|
2
|
+
export { AsyncBulletinClient, AuthCallBuilder, AuthorizationScope, BulletinError, BulletinPreparer, CID, CallBuilder, ChunkStatus, DEFAULT_CHUNKER_CONFIG, DEFAULT_CLIENT_CONFIG, DEFAULT_STORE_OPTIONS, ErrorCode, MAX_CHUNK_SIZE, MAX_FILE_SIZE, MockBulletinClient, StoreBuilder, TxStatus, WaitFor, calculateCid, cidFromBytes, cidToBytes, convertCid, estimateAuthorization, getContentHash, parseCid, reassembleChunks, resolveClientConfig, validateChunkSize } from '@parity/bulletin-sdk';
|
|
3
|
+
import { getChainAPI, createChainClient } from '@parity/product-sdk-chain-client';
|
|
4
|
+
import { createLogger } from '@parity/product-sdk-logger';
|
|
5
|
+
import { submitAndWatch } from '@parity/product-sdk-tx';
|
|
6
|
+
import { Enum } from 'polkadot-api';
|
|
7
|
+
import { getPreimageManager } from '@parity/product-sdk-host';
|
|
8
|
+
import { CID } from 'multiformats/cid';
|
|
9
|
+
import * as Digest from 'multiformats/hashes/digest';
|
|
10
|
+
import { bulletin } from '@parity/product-sdk-descriptors/bulletin';
|
|
7
11
|
|
|
8
|
-
// src/
|
|
9
|
-
var
|
|
12
|
+
// src/client.ts
|
|
13
|
+
var ProductBulletinError = class extends Error {
|
|
10
14
|
constructor(message, options) {
|
|
11
15
|
super(message, options);
|
|
12
|
-
this.name = "
|
|
16
|
+
this.name = "ProductBulletinError";
|
|
13
17
|
}
|
|
14
18
|
};
|
|
15
|
-
var BulletinHostUnavailableError = class extends
|
|
19
|
+
var BulletinHostUnavailableError = class extends ProductBulletinError {
|
|
16
20
|
constructor(operation) {
|
|
17
21
|
super(
|
|
18
22
|
`Host preimage API unavailable for ${operation}. Ensure you are running inside a host container (Polkadot Browser / Desktop).`
|
|
@@ -20,7 +24,7 @@ var BulletinHostUnavailableError = class extends BulletinError {
|
|
|
20
24
|
this.name = "BulletinHostUnavailableError";
|
|
21
25
|
}
|
|
22
26
|
};
|
|
23
|
-
var BulletinLookupTimeoutError = class extends
|
|
27
|
+
var BulletinLookupTimeoutError = class extends ProductBulletinError {
|
|
24
28
|
/** The CID that was being looked up. */
|
|
25
29
|
cid;
|
|
26
30
|
/** The timeout duration in milliseconds. */
|
|
@@ -32,7 +36,7 @@ var BulletinLookupTimeoutError = class extends BulletinError {
|
|
|
32
36
|
this.timeoutMs = timeoutMs;
|
|
33
37
|
}
|
|
34
38
|
};
|
|
35
|
-
var BulletinLookupInterruptedError = class extends
|
|
39
|
+
var BulletinLookupInterruptedError = class extends ProductBulletinError {
|
|
36
40
|
/** The CID that was being looked up. */
|
|
37
41
|
cid;
|
|
38
42
|
constructor(cid) {
|
|
@@ -41,40 +45,7 @@ var BulletinLookupInterruptedError = class extends BulletinError {
|
|
|
41
45
|
this.cid = cid;
|
|
42
46
|
}
|
|
43
47
|
};
|
|
44
|
-
var
|
|
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 {
|
|
48
|
+
var BulletinCidError = class extends ProductBulletinError {
|
|
78
49
|
/** The invalid CID string, if available. */
|
|
79
50
|
cid;
|
|
80
51
|
constructor(message, cid) {
|
|
@@ -83,84 +54,15 @@ var BulletinCidError = class extends BulletinError {
|
|
|
83
54
|
this.cid = cid;
|
|
84
55
|
}
|
|
85
56
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
}
|
|
57
|
+
var BulletinAuthorizationError = class extends ProductBulletinError {
|
|
58
|
+
/** The address that was being checked. */
|
|
59
|
+
address;
|
|
60
|
+
constructor(address, cause) {
|
|
61
|
+
super(`Failed to check authorization for ${address}`, { cause });
|
|
62
|
+
this.name = "BulletinAuthorizationError";
|
|
63
|
+
this.address = address;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
164
66
|
|
|
165
67
|
// src/authorization.ts
|
|
166
68
|
var log = createLogger("bulletin");
|
|
@@ -198,112 +100,41 @@ async function checkAuthorization(api, address) {
|
|
|
198
100
|
});
|
|
199
101
|
return status;
|
|
200
102
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
});
|
|
103
|
+
async function authorizeAccount(api, who, transactions, bytes, signer, options = {}) {
|
|
104
|
+
const { viaSudo = false, waitFor, timeoutMs, onStatus } = options;
|
|
105
|
+
const apiTx = api.tx;
|
|
106
|
+
log.info("authorizeAccount: building extrinsic", {
|
|
107
|
+
who,
|
|
108
|
+
transactions,
|
|
109
|
+
bytes: bytes.toString(),
|
|
110
|
+
viaSudo
|
|
111
|
+
});
|
|
112
|
+
if (viaSudo && !apiTx.Sudo?.sudo) {
|
|
113
|
+
throw new ProductBulletinError(
|
|
114
|
+
"viaSudo: true requires the Sudo pallet, which is not available on this network. On production networks (Polkadot, Kusama), authorize_account requires governance or a different mechanism."
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const authorizeTx = apiTx.TransactionStorage.authorize_account({
|
|
118
|
+
who,
|
|
119
|
+
transactions,
|
|
120
|
+
bytes
|
|
121
|
+
});
|
|
122
|
+
const txToSubmit = viaSudo ? apiTx.Sudo.sudo({ call: authorizeTx.decodedCall }) : authorizeTx;
|
|
123
|
+
const result = await submitAndWatch(txToSubmit, signer, {
|
|
124
|
+
waitFor,
|
|
125
|
+
timeoutMs,
|
|
126
|
+
onStatus
|
|
293
127
|
});
|
|
128
|
+
log.info("authorizeAccount: included in block", {
|
|
129
|
+
who,
|
|
130
|
+
blockHash: result.block.hash
|
|
131
|
+
});
|
|
132
|
+
return { blockHash: result.block.hash };
|
|
294
133
|
}
|
|
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
134
|
var HashAlgorithm = {
|
|
304
|
-
/** BLAKE2b-256 —
|
|
135
|
+
/** BLAKE2b-256 — chain default. */
|
|
305
136
|
Blake2b256: 45600,
|
|
306
|
-
/** SHA2-256
|
|
137
|
+
/** SHA2-256. */
|
|
307
138
|
Sha2_256: 18,
|
|
308
139
|
/** Keccak-256 — Ethereum compatibility. */
|
|
309
140
|
Keccak256: 27
|
|
@@ -311,7 +142,7 @@ var HashAlgorithm = {
|
|
|
311
142
|
var CidCodec = {
|
|
312
143
|
/** Raw binary — default for single-chunk data. */
|
|
313
144
|
Raw: 85,
|
|
314
|
-
/** DAG-PB — used for multi-chunk manifests /
|
|
145
|
+
/** DAG-PB — used for multi-chunk manifests / IPFS UnixFS. */
|
|
315
146
|
DagPb: 112,
|
|
316
147
|
/** DAG-CBOR — alternative DAG encoding. */
|
|
317
148
|
DagCbor: 113
|
|
@@ -319,29 +150,17 @@ var CidCodec = {
|
|
|
319
150
|
var SUPPORTED_HASH_CODES = new Set(Object.values(HashAlgorithm));
|
|
320
151
|
var SUPPORTED_CODEC_CODES = new Set(Object.values(CidCodec));
|
|
321
152
|
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
153
|
function hashToCid(hexHash, hashCode = HashAlgorithm.Blake2b256, codec = CidCodec.Raw) {
|
|
340
154
|
if (hexHash.length !== EXPECTED_HEX_LENGTH) {
|
|
341
155
|
throw new BulletinCidError(
|
|
342
156
|
`Expected a 0x-prefixed 32-byte hex hash (${EXPECTED_HEX_LENGTH} chars), got ${hexHash.length} chars`
|
|
343
157
|
);
|
|
344
158
|
}
|
|
159
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(hexHash)) {
|
|
160
|
+
throw new BulletinCidError(
|
|
161
|
+
`Invalid hash format: expected 0x-prefixed 32-byte hex string, got: ${hexHash}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
345
164
|
if (!SUPPORTED_HASH_CODES.has(hashCode)) {
|
|
346
165
|
throw new BulletinCidError(
|
|
347
166
|
`Unsupported hash algorithm 0x${hashCode.toString(16)}; expected one of: ${[...SUPPORTED_HASH_CODES].map((c) => `0x${c.toString(16)}`).join(", ")}`
|
|
@@ -352,346 +171,49 @@ function hashToCid(hexHash, hashCode = HashAlgorithm.Blake2b256, codec = CidCode
|
|
|
352
171
|
`Unsupported CID codec 0x${codec.toString(16)}; expected one of: ${[...SUPPORTED_CODEC_CODES].map((c) => `0x${c.toString(16)}`).join(", ")}`
|
|
353
172
|
);
|
|
354
173
|
}
|
|
355
|
-
const digest = hexToBytes(hexHash
|
|
356
|
-
|
|
357
|
-
log2.debug("hashToCid", { hexHash, hashCode, codec, cid });
|
|
358
|
-
return cid;
|
|
174
|
+
const digest = hexToBytes(hexHash);
|
|
175
|
+
return CID.createV1(codec, Digest.create(hashCode, digest)).toString();
|
|
359
176
|
}
|
|
360
|
-
|
|
361
|
-
|
|
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);
|
|
177
|
+
function cidToPreimageKey(cid) {
|
|
178
|
+
let parsed;
|
|
538
179
|
try {
|
|
539
|
-
|
|
540
|
-
method: "HEAD",
|
|
541
|
-
signal: controller.signal
|
|
542
|
-
});
|
|
543
|
-
return response.ok;
|
|
180
|
+
parsed = CID.parse(cid);
|
|
544
181
|
} catch {
|
|
545
|
-
|
|
546
|
-
} finally {
|
|
547
|
-
clearTimeout(timer);
|
|
182
|
+
throw new BulletinCidError(`Invalid CID: ${cid}`, cid);
|
|
548
183
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
throw new BulletinGatewayFetchError(cid, response.status, response.statusText);
|
|
558
|
-
}
|
|
559
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
560
|
-
} finally {
|
|
561
|
-
clearTimeout(timer);
|
|
184
|
+
if (parsed.version !== 1) {
|
|
185
|
+
throw new BulletinCidError(`Expected CIDv1, got CIDv${parsed.version}`, cid);
|
|
186
|
+
}
|
|
187
|
+
if (!SUPPORTED_HASH_CODES.has(parsed.multihash.code)) {
|
|
188
|
+
throw new BulletinCidError(
|
|
189
|
+
`Unsupported hash algorithm 0x${parsed.multihash.code.toString(16)}; expected one of: ${[...SUPPORTED_HASH_CODES].map((c) => `0x${c.toString(16)}`).join(", ")}`,
|
|
190
|
+
cid
|
|
191
|
+
);
|
|
562
192
|
}
|
|
193
|
+
return `0x${bytesToHex(parsed.multihash.digest)}`;
|
|
563
194
|
}
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
|
|
195
|
+
function hexToBytes(hex) {
|
|
196
|
+
const out = new Uint8Array(32);
|
|
197
|
+
for (let i = 0; i < 32; i++) {
|
|
198
|
+
out[i] = Number.parseInt(hex.slice(2 + i * 2, 4 + i * 2), 16);
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
567
201
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
|
|
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
|
-
});
|
|
202
|
+
function bytesToHex(bytes) {
|
|
203
|
+
let s = "";
|
|
204
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
205
|
+
s += bytes[i].toString(16).padStart(2, "0");
|
|
206
|
+
}
|
|
207
|
+
return s;
|
|
681
208
|
}
|
|
682
209
|
|
|
683
|
-
// src/query.ts
|
|
684
|
-
import { createLogger as createLogger4 } from "@parity/product-sdk-logger";
|
|
685
|
-
|
|
686
210
|
// src/resolve-query.ts
|
|
687
|
-
|
|
688
|
-
import { createLogger as createLogger3 } from "@parity/product-sdk-logger";
|
|
689
|
-
var log3 = createLogger3("bulletin");
|
|
211
|
+
var log2 = createLogger("bulletin");
|
|
690
212
|
var DEFAULT_LOOKUP_TIMEOUT_MS = 3e4;
|
|
691
213
|
async function resolveQueryStrategy() {
|
|
692
214
|
const preimageManager = await getPreimageManager();
|
|
693
215
|
if (preimageManager) {
|
|
694
|
-
|
|
216
|
+
log2.info("using host preimage lookup for bulletin queries");
|
|
695
217
|
return {
|
|
696
218
|
kind: "host-lookup",
|
|
697
219
|
lookup: (cid, timeoutMs) => lookupViaHost(preimageManager, cid, timeoutMs)
|
|
@@ -730,84 +252,9 @@ function lookupViaHost(manager, cid, timeoutMs = DEFAULT_LOOKUP_TIMEOUT_MS) {
|
|
|
730
252
|
});
|
|
731
253
|
});
|
|
732
254
|
}
|
|
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
255
|
|
|
809
256
|
// src/query.ts
|
|
810
|
-
var
|
|
257
|
+
var log3 = createLogger("bulletin");
|
|
811
258
|
async function queryBytes(cid, options) {
|
|
812
259
|
const strategy = await resolveQueryStrategy();
|
|
813
260
|
return executeQuery(strategy, cid, options);
|
|
@@ -817,528 +264,268 @@ async function queryJson(cid, options) {
|
|
|
817
264
|
return JSON.parse(new TextDecoder().decode(bytes));
|
|
818
265
|
}
|
|
819
266
|
async function executeQuery(strategy, cid, options) {
|
|
820
|
-
|
|
821
|
-
|
|
267
|
+
log3.info("query: host preimage lookup", { cid });
|
|
268
|
+
const bytes = await strategy.lookup(cid, options?.lookupTimeoutMs);
|
|
269
|
+
if (options?.noReassemble) return bytes;
|
|
270
|
+
const parsed = parseCid(cid);
|
|
271
|
+
if (parsed.code !== CidCodec$1.DagPb) return bytes;
|
|
272
|
+
log3.info("query: reassembling DAG-PB manifest", { cid });
|
|
273
|
+
const builder = new UnixFsDagBuilder();
|
|
274
|
+
const { chunkCids } = await builder.parse(bytes);
|
|
275
|
+
const chunks = await Promise.all(
|
|
276
|
+
chunkCids.map((c) => strategy.lookup(c.toString(), options?.lookupTimeoutMs))
|
|
277
|
+
);
|
|
278
|
+
let total = 0;
|
|
279
|
+
for (const chunk of chunks) total += chunk.length;
|
|
280
|
+
const out = new Uint8Array(total);
|
|
281
|
+
let offset = 0;
|
|
282
|
+
for (const chunk of chunks) {
|
|
283
|
+
out.set(chunk, offset);
|
|
284
|
+
offset += chunk.length;
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
822
287
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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) };
|
|
288
|
+
var HASH_CODE_TO_ENUM_TYPE = {
|
|
289
|
+
[HashAlgorithm.Blake2b256]: "Blake2b256",
|
|
290
|
+
[HashAlgorithm.Sha2_256]: "Sha2_256",
|
|
291
|
+
[HashAlgorithm.Keccak256]: "Keccak256"
|
|
292
|
+
};
|
|
293
|
+
async function verifyOnChain(api, cid, options) {
|
|
294
|
+
const parsed = parseCidForVerify(cid);
|
|
295
|
+
const queryFn = api.query?.TransactionStorage?.Transactions?.getValue;
|
|
296
|
+
if (!queryFn) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
"Bulletin API does not expose query.TransactionStorage.Transactions \u2014 the typed API may be incomplete or the runtime version doesn't match the descriptor."
|
|
299
|
+
);
|
|
861
300
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
if (void 0) {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
};
|
|
301
|
+
const entries = await queryFn(options.block);
|
|
302
|
+
if (!entries || entries.length === 0) return null;
|
|
303
|
+
if (options.index !== void 0) {
|
|
304
|
+
const entry = entries[options.index];
|
|
305
|
+
if (entry && matchesEntry(entry, parsed)) {
|
|
306
|
+
return {
|
|
307
|
+
block: options.block,
|
|
308
|
+
index: options.index,
|
|
309
|
+
size: entry.size,
|
|
310
|
+
blockChunks: entry.block_chunks
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
893
314
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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,
|
|
315
|
+
for (let i = 0; i < entries.length; i++) {
|
|
316
|
+
const entry = entries[i];
|
|
317
|
+
if (matchesEntry(entry, parsed)) {
|
|
318
|
+
return {
|
|
319
|
+
block: options.block,
|
|
965
320
|
index: i,
|
|
966
|
-
|
|
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)
|
|
321
|
+
size: entry.size,
|
|
322
|
+
blockChunks: entry.block_chunks
|
|
974
323
|
};
|
|
975
|
-
results.push(entry);
|
|
976
|
-
options?.onProgress?.(i + 1, items.length, entry);
|
|
977
324
|
}
|
|
978
325
|
}
|
|
979
|
-
return
|
|
326
|
+
return null;
|
|
980
327
|
}
|
|
981
|
-
|
|
982
|
-
let
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
-
});
|
|
328
|
+
function parseCidForVerify(cid) {
|
|
329
|
+
let parsed;
|
|
330
|
+
try {
|
|
331
|
+
parsed = CID.parse(cid);
|
|
332
|
+
} catch {
|
|
333
|
+
throw new BulletinCidError(`Invalid CID: ${cid}`, cid);
|
|
334
|
+
}
|
|
335
|
+
if (parsed.version !== 1) {
|
|
336
|
+
throw new BulletinCidError(`Expected CIDv1, got CIDv${parsed.version}`, cid);
|
|
337
|
+
}
|
|
338
|
+
const hashType = HASH_CODE_TO_ENUM_TYPE[parsed.multihash.code];
|
|
339
|
+
if (!hashType) {
|
|
340
|
+
throw new BulletinCidError(
|
|
341
|
+
`Unsupported hash algorithm 0x${parsed.multihash.code.toString(16)}`,
|
|
342
|
+
cid
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return { digest: parsed.multihash.digest, hashType };
|
|
346
|
+
}
|
|
347
|
+
function matchesEntry(entry, target) {
|
|
348
|
+
if (entry.hashing.type !== target.hashType) return false;
|
|
349
|
+
const onChainBytes = entry.content_hash instanceof Uint8Array ? entry.content_hash : entry.content_hash.asBytes();
|
|
350
|
+
if (onChainBytes.length !== target.digest.length) return false;
|
|
351
|
+
for (let i = 0; i < onChainBytes.length; i++) {
|
|
352
|
+
if (onChainBytes[i] !== target.digest[i]) return false;
|
|
353
|
+
}
|
|
354
|
+
return true;
|
|
1135
355
|
}
|
|
1136
|
-
var createMockApi2;
|
|
1137
356
|
|
|
1138
357
|
// src/client.ts
|
|
358
|
+
var log4 = createLogger("bulletin");
|
|
1139
359
|
var BulletinClient = class _BulletinClient {
|
|
360
|
+
/** Underlying upstream client — exposed for power users. */
|
|
361
|
+
inner;
|
|
362
|
+
/** Typed Bulletin Chain API. */
|
|
1140
363
|
api;
|
|
1141
|
-
|
|
364
|
+
/** Lazy-resolved host-preimage query strategy, cached for the client lifetime. */
|
|
1142
365
|
queryStrategyPromise = null;
|
|
1143
|
-
|
|
366
|
+
/** Constructed via {@link create} or {@link from}. */
|
|
367
|
+
constructor(inner, api) {
|
|
368
|
+
this.inner = inner;
|
|
1144
369
|
this.api = api;
|
|
1145
|
-
this.gateway = gateway;
|
|
1146
370
|
}
|
|
1147
|
-
/**
|
|
371
|
+
/** Resolve and cache the host query strategy on first use. */
|
|
1148
372
|
resolveQuery() {
|
|
1149
373
|
if (!this.queryStrategyPromise) {
|
|
1150
374
|
this.queryStrategyPromise = resolveQueryStrategy();
|
|
1151
375
|
}
|
|
1152
376
|
return this.queryStrategyPromise;
|
|
1153
377
|
}
|
|
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
378
|
/**
|
|
1168
|
-
*
|
|
379
|
+
* Create a client from an environment shorthand or an explicit network.
|
|
1169
380
|
*
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
381
|
+
* Environment form uses our `getChainAPI(env)` to resolve the typed API.
|
|
382
|
+
* Explicit form skips the environment lookup and lets you pass any
|
|
383
|
+
* genesis/descriptor combo.
|
|
1172
384
|
*
|
|
1173
|
-
* @
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
}
|
|
1178
|
-
/**
|
|
1179
|
-
* Upload data to the Bulletin Chain.
|
|
385
|
+
* @example
|
|
386
|
+
* ```ts
|
|
387
|
+
* // Shorthand
|
|
388
|
+
* const client = await BulletinClient.create({ environment: "paseo", signer });
|
|
1180
389
|
*
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
390
|
+
* // Explicit (custom network)
|
|
391
|
+
* const client = await BulletinClient.create({
|
|
392
|
+
* ...BulletinChain.paseo,
|
|
393
|
+
* signer,
|
|
394
|
+
* config: { defaultChunkSize: 1 << 20 },
|
|
395
|
+
* });
|
|
396
|
+
* ```
|
|
1184
397
|
*/
|
|
1185
|
-
async
|
|
1186
|
-
|
|
398
|
+
static async create(options) {
|
|
399
|
+
if ("environment" in options) {
|
|
400
|
+
const chain2 = await getChainAPI(options.environment);
|
|
401
|
+
const inner2 = new AsyncBulletinClient(
|
|
402
|
+
chain2.bulletin,
|
|
403
|
+
options.signer,
|
|
404
|
+
chain2.raw.bulletin.submit,
|
|
405
|
+
options.config,
|
|
406
|
+
() => chain2.destroy()
|
|
407
|
+
);
|
|
408
|
+
log4.info("BulletinClient created (environment shorthand)", {
|
|
409
|
+
environment: options.environment
|
|
410
|
+
});
|
|
411
|
+
return new _BulletinClient(inner2, chain2.bulletin);
|
|
412
|
+
}
|
|
413
|
+
const { genesisHash, descriptor, signer, config } = options;
|
|
414
|
+
if (descriptor.genesis && genesisHash.toLowerCase() !== descriptor.genesis.toLowerCase()) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
`BulletinClient.create: genesisHash (${genesisHash}) does not match descriptor.genesis (${descriptor.genesis}). These must refer to the same network \u2014 check that you're pairing the right descriptor with the right genesis hash.`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const chain = await createChainClient({
|
|
420
|
+
chains: { bulletin: descriptor },
|
|
421
|
+
rpcs: { bulletin: [] }
|
|
422
|
+
});
|
|
423
|
+
const inner = new AsyncBulletinClient(
|
|
424
|
+
chain.bulletin,
|
|
425
|
+
signer,
|
|
426
|
+
chain.raw.bulletin.submit,
|
|
427
|
+
config,
|
|
428
|
+
() => chain.destroy()
|
|
429
|
+
);
|
|
430
|
+
log4.info("BulletinClient created (explicit network)");
|
|
431
|
+
return new _BulletinClient(inner, chain.bulletin);
|
|
1187
432
|
}
|
|
1188
433
|
/**
|
|
1189
|
-
*
|
|
434
|
+
* Construct from a pre-built `AsyncBulletinClient` and PAPI typed API.
|
|
1190
435
|
*
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
436
|
+
* Use this when you already own the connection lifecycle (BYOD setups,
|
|
437
|
+
* tests). The caller is responsible for calling `papiClient.destroy()`
|
|
438
|
+
* — this client's {@link destroy} only tears down the upstream's
|
|
439
|
+
* `onDestroy` hook.
|
|
1194
440
|
*/
|
|
1195
|
-
|
|
1196
|
-
return
|
|
441
|
+
static from(inner, api) {
|
|
442
|
+
return new _BulletinClient(inner, api);
|
|
1197
443
|
}
|
|
444
|
+
// ─── Upload + authorization (forwarded to upstream) ────────────────
|
|
445
|
+
/** Build a store transaction. See upstream `StoreBuilder` for chained options. */
|
|
446
|
+
store(data) {
|
|
447
|
+
return this.inner.store(data);
|
|
448
|
+
}
|
|
449
|
+
/** Authorize an account to store data on the chain (sudo required on most networks). */
|
|
450
|
+
authorizeAccount(who, transactions, bytes) {
|
|
451
|
+
return this.inner.authorizeAccount(who, transactions, bytes);
|
|
452
|
+
}
|
|
453
|
+
/** Authorize content storage by hash (anyone can store; no fees). */
|
|
454
|
+
authorizePreimage(contentHash, maxSize) {
|
|
455
|
+
return this.inner.authorizePreimage(contentHash, maxSize);
|
|
456
|
+
}
|
|
457
|
+
/** Renew a stored transaction by block + index. */
|
|
458
|
+
renew(block, index) {
|
|
459
|
+
return this.inner.renew(block, index);
|
|
460
|
+
}
|
|
461
|
+
/** Estimate the authorization (transactions + bytes) needed for `dataSize`. */
|
|
462
|
+
estimateAuthorization(dataSize) {
|
|
463
|
+
return this.inner.estimateAuthorization(dataSize);
|
|
464
|
+
}
|
|
465
|
+
// ─── Read side (our own helpers) ───────────────────────────────────
|
|
1198
466
|
/**
|
|
1199
|
-
* Fetch raw bytes
|
|
467
|
+
* Fetch raw bytes for a CID via the host's preimage lookup.
|
|
1200
468
|
*
|
|
1201
|
-
*
|
|
469
|
+
* Container-only — outside a Polkadot Browser / Desktop host this
|
|
470
|
+
* throws {@link BulletinHostUnavailableError}. The chain stores
|
|
471
|
+
* content metadata (`content_hash`, size, codec) but the bytes
|
|
472
|
+
* themselves are surfaced through the host's preimage subscription.
|
|
473
|
+
*
|
|
474
|
+
* Use {@link verifyOnChain} if you only need to confirm a CID was
|
|
475
|
+
* recorded on-chain (no byte fetch).
|
|
1202
476
|
*/
|
|
1203
477
|
async fetchBytes(cid, options) {
|
|
1204
478
|
const strategy = await this.resolveQuery();
|
|
1205
479
|
return executeQuery(strategy, cid, options);
|
|
1206
480
|
}
|
|
1207
|
-
/**
|
|
1208
|
-
* Fetch and parse JSON by CID.
|
|
1209
|
-
*
|
|
1210
|
-
* Auto-resolves query path (same as {@link fetchBytes}).
|
|
1211
|
-
*/
|
|
481
|
+
/** Fetch and parse JSON for a CID. */
|
|
1212
482
|
async fetchJson(cid, options) {
|
|
1213
483
|
const bytes = await this.fetchBytes(cid, options);
|
|
1214
484
|
return JSON.parse(new TextDecoder().decode(bytes));
|
|
1215
485
|
}
|
|
1216
|
-
/**
|
|
1217
|
-
async
|
|
1218
|
-
return
|
|
1219
|
-
}
|
|
1220
|
-
/** Build the full gateway URL for a CID. */
|
|
1221
|
-
gatewayUrl(cid) {
|
|
1222
|
-
return gatewayUrl(cid, this.gateway);
|
|
486
|
+
/** Pre-flight: check whether `address` can store on the bulletin chain. */
|
|
487
|
+
async checkAuthorization(address) {
|
|
488
|
+
return checkAuthorization(this.api, address);
|
|
1223
489
|
}
|
|
1224
490
|
/**
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
* Use as a pre-flight check before {@link upload} to provide clear UX
|
|
1228
|
-
* instead of letting the transaction fail mid-execution.
|
|
491
|
+
* Verify that a CID was recorded on-chain at the given block.
|
|
1229
492
|
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
* @see {@link checkAuthorization} for the standalone function equivalent.
|
|
493
|
+
* Common pattern: pass `blockNumber` (and optionally `extrinsicIndex`)
|
|
494
|
+
* from a `store(...).send()` receipt to confirm the upload landed.
|
|
495
|
+
* See {@link verifyOnChain} for details.
|
|
1234
496
|
*/
|
|
1235
|
-
async
|
|
1236
|
-
return
|
|
497
|
+
async verifyOnChain(cid, options) {
|
|
498
|
+
return verifyOnChain(this.api, cid, options);
|
|
499
|
+
}
|
|
500
|
+
/** Tear down the underlying connection. */
|
|
501
|
+
async destroy() {
|
|
502
|
+
await this.inner.destroy();
|
|
1237
503
|
}
|
|
1238
504
|
};
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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
|
-
}
|
|
505
|
+
var BulletinChain = {
|
|
506
|
+
paseo: {
|
|
507
|
+
genesisHash: "0x744960c32e3a3df5440e1ecd4d34096f1ce2230d7016a5ada8a765d5a622b4ea",
|
|
508
|
+
descriptor: bulletin
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// src/lazy-signer.ts
|
|
513
|
+
function createLazySigner(getSigner, onMissing = "No signer available \u2014 connect a wallet and select an account first.") {
|
|
514
|
+
const resolve = () => {
|
|
515
|
+
const inner = getSigner();
|
|
516
|
+
if (!inner) throw new Error(onMissing);
|
|
517
|
+
return inner;
|
|
1264
518
|
};
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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
|
-
});
|
|
519
|
+
const lazy = {
|
|
520
|
+
get publicKey() {
|
|
521
|
+
return resolve().publicKey;
|
|
522
|
+
},
|
|
523
|
+
signTx: async (...args) => resolve().signTx(...args),
|
|
524
|
+
signBytes: async (...args) => resolve().signBytes(...args)
|
|
525
|
+
};
|
|
526
|
+
return lazy;
|
|
1315
527
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
};
|
|
528
|
+
|
|
529
|
+
export { BulletinAuthorizationError, BulletinChain, BulletinCidError, BulletinClient, BulletinHostUnavailableError, BulletinLookupInterruptedError, BulletinLookupTimeoutError, CidCodec, HashAlgorithm, ProductBulletinError, authorizeAccount, checkAuthorization, cidToPreimageKey, createLazySigner, executeQuery, hashToCid, queryBytes, queryJson, resolveQueryStrategy, verifyOnChain };
|
|
530
|
+
//# sourceMappingURL=index.js.map
|
|
1344
531
|
//# sourceMappingURL=index.js.map
|