TypeScript SDK for uploading and retrieving data on the Bulletin Chain — a Polkadot system parachain designed for on-chain data storage with IPFS-compatible content addressing.
npm install @polkadot-apps/bulletin
# or
pnpm add @polkadot-apps/bulletin
polkadot-api (required)@novasamatech/product-sdk (optional — needed only when running inside a host container like Polkadot Desktop or the Polkadot mobile app)import { BulletinClient } from "@polkadot-apps/bulletin";
const bulletin = await BulletinClient.create("paseo");
// Upload — signer is auto-resolved (see "Signer resolution" below)
const result = await bulletin.upload(new TextEncoder().encode("hello world"));
console.log(result.cid); // CIDv1 string
// Read back
const data = await bulletin.fetchBytes(result.cid);
When no signer is passed to upload() or batchUpload(), the SDK auto-detects the environment and picks the best strategy:
| Environment | Strategy | What happens |
|---|---|---|
| Inside host container (Polkadot Desktop / Mobile) | Host preimage API | The host app signs and submits the transaction automatically — no user interaction. |
| Standalone (browser, Node, scripts) | Dev signer (Alice) | Uses the well-known Substrate dev account, pre-funded on test chains. |
| Explicit signer provided | Direct transaction | Builds and signs a TransactionStorage.store extrinsic with the given signer. |
You can always pass an explicit signer to override auto-resolution:
import { createDevSigner } from "@polkadot-apps/tx";
const alice = createDevSigner("Alice");
const result = await bulletin.upload(data, alice);
When fetching data with fetchBytes(), fetchJson(), or their standalone equivalents (queryBytes, queryJson), the SDK auto-detects the environment:
| Environment | Strategy | What happens |
|---|---|---|
| Inside host container (Polkadot Desktop / Mobile) | Host preimage lookup | The host checks its local cache first, then polls the IPFS gateway with automatic retry. Data uploaded by any signer is available — the lookup is upload-path-agnostic. |
| Standalone (browser, Node, scripts) | Direct IPFS gateway | Standard HTTP fetch from the configured IPFS gateway URL. |
The query path is transparent — bulletin.fetchBytes(cid) works identically in both environments, but benefits from host-managed caching when available.
BulletinClientHigh-level client that bundles a typed Bulletin API and an IPFS gateway URL.
BulletinClient.create(env)Create a client for an environment. Resolves the chain connection and gateway automatically.
const bulletin = await BulletinClient.create("paseo");
Parameters:
env — "paseo" | "polkadot" | "kusama"BulletinClient.from(api, gateway)Create a client from an explicit API instance and gateway URL. Useful for custom setups and testing.
import { bulletin as bulletinDescriptor } from "@polkadot-apps/descriptors/bulletin";
const api = client.getTypedApi(bulletinDescriptor);
const bulletin = BulletinClient.from(api, "https://paseo-ipfs.polkadot.io/ipfs/");
bulletin.upload(data, signer?, options?)Upload data to the Bulletin Chain. Returns a discriminated union — use result.kind to determine the upload path.
const result = await bulletin.upload(fileBytes);
if (result.kind === "transaction") {
console.log("Block hash:", result.blockHash);
} else {
console.log("Preimage key:", result.preimageKey);
}
console.log("CID:", result.cid);
console.log("Gateway URL:", result.gatewayUrl);
Parameters:
data — Uint8Array of raw bytes to storesigner — optional PolkadotSigner; auto-resolved when omittedoptions — optional UploadOptions:
waitFor — "best-block" (default) or "finalized"timeoutMs — timeout in ms (default: 300,000)onStatus — lifecycle callback for UI progressbulletin.batchUpload(items, signer?, options?)Upload multiple items sequentially. The Bulletin Chain requires sequential submission for nonce ordering. Individual failures are captured per-item — the batch does not abort.
const items = [
{ data: new TextEncoder().encode("doc-1"), label: "document-1" },
{ data: new TextEncoder().encode("doc-2"), label: "document-2" },
];
const results = await bulletin.batchUpload(items, undefined, {
onProgress: (completed, total, current) => {
console.log(`${completed}/${total}: ${current.label} — ${current.success ? "ok" : current.error}`);
},
});
Parameters:
items — array of { data: Uint8Array, label: string }signer — optional PolkadotSigner; auto-resolved when omittedoptions — optional BatchUploadOptions (extends UploadOptions):
onProgress(completed, total, current) — called after each itembulletin.fetchBytes(cid, options?)Fetch raw bytes by CID. Auto-resolves the query path (see "Query resolution" above).
const bytes = await bulletin.fetchBytes("bafk...");
// With host lookup timeout override:
const bytes2 = await bulletin.fetchBytes("bafk...", { lookupTimeoutMs: 10_000 });
Parameters:
cid — CIDv1 stringoptions — optional QueryOptions:
timeoutMs — timeout for gateway fetch in ms (default: 30,000)lookupTimeoutMs — timeout for host preimage lookup in ms (default: 30,000; host path only)bulletin.fetchJson<T>(cid, options?)Fetch and parse JSON by CID. Auto-resolves the query path (same as fetchBytes).
const metadata = await bulletin.fetchJson<{ name: string }>("bafk...");
bulletin.checkAuthorization(address)Check whether an account is authorized to store data. Use as a pre-flight check before upload() to show "not authorized" or "insufficient quota" instead of letting the transaction fail.
const auth = await bulletin.checkAuthorization(myAddress);
if (!auth.authorized) {
console.error("Account is not authorized for bulletin storage");
} else if (auth.remainingBytes < BigInt(fileBytes.length)) {
console.error(`Insufficient quota: ${auth.remainingBytes} bytes remaining`);
}
Parameters:
address — SS58-encoded account addressReturns: AuthorizationStatus with authorized, remainingTransactions, remainingBytes, expiration.
bulletin.cidExists(cid)Check if a CID exists on the gateway (HEAD request). Returns false on any error or timeout.
bulletin.gatewayUrl(cid)Build the full gateway URL for a CID.
BulletinClient.computeCid(data)Compute the CID for data without uploading. Static method — no client instance needed.
const cid = BulletinClient.computeCid(new TextEncoder().encode("hello"));
BulletinClient.hashToCid(hexHash, hashCode?, codec?)Reconstruct a CID from a 0x-prefixed hex hash stored on-chain. Static method — no client instance needed. Supports all hash algorithms and codecs used by the Bulletin Chain.
import { HashAlgorithm, CidCodec } from "@polkadot-apps/bulletin";
// Default (blake2b-256, raw)
const cid = BulletinClient.hashToCid("0x1a2b3c...");
// SHA2-256 content from bulletin-deploy
const cid2 = BulletinClient.hashToCid("0x1a2b3c...", HashAlgorithm.Sha2_256);
const url = bulletin.gatewayUrl(cid);
The same operations are available as standalone functions for lower-level usage:
import {
upload, batchUpload, checkAuthorization,
computeCid, cidToPreimageKey, hashToCid,
HashAlgorithm, CidCodec,
fetchBytes, fetchJson, cidExists,
queryBytes, queryJson, resolveQueryStrategy,
getGateway, gatewayUrl,
} from "@polkadot-apps/bulletin";
upload(api, data, signer?, options?)Upload data using an explicit BulletinApi instance.
batchUpload(api, items, signer?, options?)Batch upload using an explicit BulletinApi instance.
checkAuthorization(api, address)Check whether an account is authorized to store data. Standalone equivalent of bulletin.checkAuthorization().
import { checkAuthorization } from "@polkadot-apps/bulletin";
const auth = await checkAuthorization(api, address);
computeCid(data)Compute a CIDv1 (blake2b-256, raw codec) for arbitrary bytes. Deterministic — same input always produces the same CID.
getGateway(env)Get the IPFS gateway URL for an environment.
resolveUploadStrategy(signer?)Resolve which upload strategy will be used. Returns a discriminated union:
import { resolveUploadStrategy } from "@polkadot-apps/bulletin";
const strategy = await resolveUploadStrategy();
console.log(strategy.kind); // "preimage" or "signer"
queryBytes(cid, gateway, options?)Fetch raw bytes with auto-resolved query strategy. Equivalent to BulletinClient.fetchBytes but as a standalone function requiring an explicit gateway URL.
queryJson<T>(cid, gateway, options?)Fetch and parse JSON with auto-resolved query strategy.
cidToPreimageKey(cid)Extract the content hash digest from a CIDv1 string and return it as a 0x-prefixed hex string — the preimage key format used by the host API. Accepts any hash algorithm supported by the Bulletin Chain (blake2b-256, sha2-256, keccak-256).
import { computeCid, cidToPreimageKey } from "@polkadot-apps/bulletin";
const cid = computeCid(data);
const key = cidToPreimageKey(cid); // "0x1a2b3c..."
hashToCid(hexHash, hashCode?, codec?)Reconstruct a CIDv1 from a 0x-prefixed hex hash — the reverse of cidToPreimageKey. Use this when you have on-chain hashes and need to build IPFS gateway URLs.
The Bulletin Chain supports multiple hash algorithms and codecs — pass the values that match the on-chain TransactionInfo to get the correct CID. Defaults to blake2b-256 + raw (matching computeCid).
import { hashToCid, HashAlgorithm, CidCodec, gatewayUrl, getGateway } from "@polkadot-apps/bulletin";
// Default (blake2b-256, raw) — matches computeCid output
const cid = hashToCid(onChainHash);
// SHA2-256 content stored via bulletin-deploy
const cid2 = hashToCid(onChainHash, HashAlgorithm.Sha2_256);
// DAG-PB manifest with blake2b-256
const cid3 = hashToCid(manifestHash, HashAlgorithm.Blake2b256, CidCodec.DagPb);
const url = gatewayUrl(cid, getGateway("paseo"));
HashAlgorithmHash algorithms supported by the Bulletin Chain (multihash codes):
| Constant | Value | Description |
|---|---|---|
HashAlgorithm.Blake2b256 |
0xb220 |
Default for polkadot-apps and chain SDK |
HashAlgorithm.Sha2_256 |
0x12 |
Default for bulletin-deploy |
HashAlgorithm.Keccak256 |
0x1b |
Ethereum compatibility |
CidCodecCID codecs supported by the Bulletin Chain (multicodec codes):
| Constant | Value | Description |
|---|---|---|
CidCodec.Raw |
0x55 |
Raw binary — default for single-chunk data |
CidCodec.DagPb |
0x70 |
DAG-PB — multi-chunk manifests / directories |
CidCodec.DagCbor |
0x71 |
DAG-CBOR — alternative DAG encoding |
resolveQueryStrategy()Resolve which query strategy will be used. Returns a discriminated union:
import { resolveQueryStrategy } from "@polkadot-apps/bulletin";
const strategy = await resolveQueryStrategy();
console.log(strategy.kind); // "host-lookup" or "gateway"
AuthorizationStatusinterface AuthorizationStatus {
authorized: boolean; // Whether an authorization entry exists
remainingTransactions: number; // Remaining tx slots (0 if not authorized)
remainingBytes: bigint; // Remaining bytes (0n if not authorized)
expiration: number; // Expiration block number (0 if not authorized)
}
UploadResultDiscriminated union on kind:
type UploadResult =
| { kind: "transaction"; cid: string; blockHash: string; gatewayUrl?: string }
| { kind: "preimage"; cid: string; preimageKey: string; gatewayUrl?: string };
BatchUploadResultDiscriminated union on kind and success:
type BatchUploadResult =
| { kind: "transaction"; success: true; label: string; cid: string; blockHash: string; gatewayUrl?: string }
| { kind: "preimage"; success: true; label: string; cid: string; preimageKey: string; gatewayUrl?: string }
| { kind: "transaction" | "preimage"; success: false; label: string; cid: string; error: string; gatewayUrl?: string };
UploadStrategytype UploadStrategy =
| { kind: "preimage"; submit: (data: Uint8Array) => Promise<string> }
| { kind: "signer"; signer: PolkadotSigner };
Environmenttype Environment = "polkadot" | "kusama" | "paseo";
QueryStrategytype QueryStrategy =
| { kind: "host-lookup"; lookup: (cid: string, timeoutMs?: number) => Promise<Uint8Array> }
| { kind: "gateway" };
QueryOptionsinterface QueryOptions extends FetchOptions {
/** Timeout for host preimage lookup in ms (default: 30,000). Host path only. */
lookupTimeoutMs?: number;
}
All CIDs are CIDv1 with:
b)This matches the Bulletin Chain's on-chain CID computation, ensuring the locally computed CID always matches what the chain stores.
Apache-2.0