| name | polkadot-bulletin |
| description | Use when uploading or retrieving data on the Polkadot Bulletin Chain, working with CID-based decentralized storage, IPFS gateway access, or the BulletinClient SDK. Covers upload, batch upload, fetch, query, CID computation, and gateway utilities.
|
Polkadot Bulletin Chain SDK
@polkadot-apps/bulletin is a TypeScript SDK for uploading and retrieving data on the Polkadot Bulletin Chain -- a purpose-built parachain for decentralized data storage. Data is content-addressed using CIDv1 (blake2b-256 hash, raw codec) and retrievable via IPFS gateways.
Key Concepts
- Content-addressed storage: Data is identified by its CID (Content Identifier), computed deterministically from the bytes via blake2b-256.
- Two upload paths: Inside a host container (Polkadot Desktop/Mobile), uploads go through the host preimage API automatically. Standalone, a
PolkadotSigner or dev signer is used to submit a TransactionStorage.store extrinsic.
- Two query paths: Inside a host container, queries use the host preimage lookup (with caching). Standalone, data is fetched directly from an IPFS gateway.
- Environments:
"polkadot", "kusama", "paseo" -- currently only "paseo" has a live gateway.
Quick Start: Upload and Fetch
import { BulletinClient } from "@polkadot-apps/bulletin";
const bulletin = await BulletinClient.create("paseo");
const data = new TextEncoder().encode(JSON.stringify({ title: "Hello Bulletin" }));
const result = await bulletin.upload(data);
console.log("CID:", result.cid);
const content = await bulletin.fetchJson<{ title: string }>(result.cid);
console.log(content.title);
WARNING: upload() expects Uint8Array, not strings. Always convert with new TextEncoder().encode(...). Passing a string will cause a type error or unexpected behavior.
BulletinClient
The BulletinClient class bundles a typed Bulletin API and IPFS gateway URL so you do not need to pass them on every call. Both upload and query paths auto-resolve based on the runtime environment.
Creating a Client
import { BulletinClient } from "@polkadot-apps/bulletin";
const client = await BulletinClient.create("paseo");
import { getGateway } from "@polkadot-apps/bulletin";
const custom = BulletinClient.from(myApi, "https://my-gateway.example/ipfs/");
When to use each entry point
| Method | When to use | Size cost |
|---|
BulletinClient.create("paseo") | Quick prototyping, scripts | ~6.3 MB (loads all preset descriptors via getChainAPI) |
BulletinClient.from(api, gateway) | Production apps, BYOD setups | Only the descriptors you import (~912 KB for bulletin alone) |
Recommended for production: Use BulletinClient.from() with a BYOD chain client:
import { createChainClient } from "@polkadot-apps/chain-client";
import { bulletin } from "@polkadot-apps/descriptors/bulletin";
const client = await createChainClient({
chains: { bulletin },
rpcs: { bulletin: ["wss://paseo-bulletin-rpc.polkadot.io"] },
});
const bulletinClient = BulletinClient.from(client.bulletin, "https://paseo-ipfs.polkadot.io/ipfs/");
Uploading Data
const data = new TextEncoder().encode("raw file content");
const result = await client.upload(data);
import type { PolkadotSigner } from "polkadot-api";
const result2 = await client.upload(data, mySigner, {
waitFor: "finalized",
timeoutMs: 60_000,
onStatus: (status) => console.log(status),
});
Batch Upload
const items = [
{ data: new TextEncoder().encode("file A"), label: "a.txt" },
{ data: new TextEncoder().encode("file B"), label: "b.txt" },
];
const results = await client.batchUpload(items, undefined, {
onProgress: (completed, total, current) => {
console.log(`${completed}/${total}: ${current.label} ${current.success ? "OK" : "FAILED"}`);
},
});
for (const r of results) {
if (r.success) {
console.log(`${r.label}: ${r.cid}`);
} else {
console.error(`${r.label}: ${r.error}`);
}
}
Fetching Data
const bytes = await client.fetchBytes(cid);
const metadata = await client.fetchJson<{ name: string; size: number }>(cid);
const bytes2 = await client.fetchBytes(cid, {
timeoutMs: 10_000,
lookupTimeoutMs: 5_000,
});
Utility Methods
const cid = BulletinClient.computeCid(new TextEncoder().encode("data"));
const cidFromHash = BulletinClient.hashToCid("0x1a2b3c...");
const exists = await client.cidExists(cid);
const url = client.gatewayUrl(cid);
const auth = await client.checkAuthorization(address);
if (!auth.authorized) { }
if (auth.remainingBytes < BigInt(fileBytes.length)) { }
Standalone Functions
For advanced use cases where you manage the API and gateway yourself, all operations are available as standalone functions.
Authorization
import { checkAuthorization } from "@polkadot-apps/bulletin";
const auth = await checkAuthorization(api, address);
Upload Functions
import { upload, batchUpload } from "@polkadot-apps/bulletin";
import type { BulletinApi } from "@polkadot-apps/bulletin";
const result = await upload(api, new TextEncoder().encode("data"), signer, {
gateway: "https://paseo-ipfs.polkadot.io/ipfs/",
waitFor: "finalized",
timeoutMs: 300_000,
onStatus: (status) => console.log(status),
});
const results = await batchUpload(api, items, signer, {
gateway: "https://paseo-ipfs.polkadot.io/ipfs/",
onProgress: (completed, total, current) => { },
});
CID Functions
import { computeCid, cidToPreimageKey, hashToCid, HashAlgorithm, CidCodec } from "@polkadot-apps/bulletin";
const cid = computeCid(new TextEncoder().encode("hello"));
const hexKey = cidToPreimageKey(cid);
const reconstructed = hashToCid(hexKey);
const sha256Cid = hashToCid(hexKey, HashAlgorithm.Sha2_256);
const manifestCid = hashToCid(hexKey, HashAlgorithm.Blake2b256, CidCodec.DagPb);
Gateway Functions
import { getGateway, gatewayUrl, cidExists, fetchBytes, fetchJson } from "@polkadot-apps/bulletin";
const gw = getGateway("paseo");
const url = gatewayUrl(cid, gw);
const exists = await cidExists(cid, gw);
const bytes = await fetchBytes(cid, gw, { timeoutMs: 10_000 });
const json = await fetchJson<MyType>(cid, gw);
Query Functions (Auto-Resolving)
import { queryBytes, queryJson, resolveQueryStrategy } from "@polkadot-apps/bulletin";
const bytes = await queryBytes(cid, gateway);
const data = await queryJson<MyType>(cid, gateway, { timeoutMs: 15_000 });
const strategy = await resolveQueryStrategy();
Upload Strategy Resolution
import { resolveUploadStrategy } from "@polkadot-apps/bulletin";
const strategy = await resolveUploadStrategy();
const explicit = await resolveUploadStrategy(mySigner);
Upload Result Handling
Results are discriminated unions -- use result.kind to narrow the type:
const result = await client.upload(data);
if (result.kind === "transaction") {
console.log("Block hash:", result.blockHash);
} else {
console.log("Preimage key:", result.preimageKey);
}
Batch results add success and label fields:
for (const r of results) {
if (!r.success) {
console.error(r.label, r.error);
continue;
}
if (r.kind === "transaction") {
console.log(r.label, r.blockHash);
} else {
console.log(r.label, r.preimageKey);
}
}
Common Mistakes
-
Passing a string to upload instead of Uint8Array:
await client.upload("hello");
await client.upload(new TextEncoder().encode("hello"));
-
Forgetting that only "paseo" has a live gateway:
getGateway("polkadot");
-
Not handling batch failures: batchUpload does NOT throw on individual item failures. Always check result.success for each item.
-
Omitting signer in standalone (non-host) mode: When no signer is passed and you are not inside a host container, the SDK falls back to Alice's dev signer. This only works on test networks. For production, always provide a signer.
Reference
See references/bulletin-api.md for full type signatures and API surface.