# docs / 06
The SDK
Everything the chainboot command does is built from a small TypeScript library with no dependencies. You can use the same building blocks for your own ideas: store files, publish data that others can verify, keep encrypted state for your users.
npm install chainboot # Node.js 22.15 or newerimport { RpcPool, ChainStore, Keypair } from "chainboot";All examples below are complete programs. They use devnet; get test SOL for your keypair at https://faucet.solana.com.
1. Store a file, read it back
import { RpcPool, ChainStore, Keypair, GENESIS } from "chainboot";
import fs from "node:fs";
const pool = new RpcPool(["https://your-devnet-endpoint.example/key"]);
await pool.probe(GENESIS.devnet); // drop endpoints that are dead or on another network
const store = new ChainStore(pool);
const me = Keypair.fromFile("me.json");
// write: any size, resumable, returns the address of the file
const ref = await store.put(me, fs.readFileSync("photo.jpg"), {
journalDir: "./journal", // lets an interrupted upload continue
onProgress: (p) => console.log(p.phase, p.done, "/", p.total),
});
console.log(ref); // { sig: "5Hk…", size: 482113, sha256: "9f2c…" }
// read: verified against the SHA-256 in `ref`; only accept data published by `me`
const data = await store.get(ref, { signers: [me.publicKey], cacheDir: "./cache" });
fs.writeFileSync("copy.jpg", data);ref.sig alone is enough to fetch the file: await store.get("5Hk…"). Passing the whole ref, or the option sha256, additionally pins the content you expect.
How much will it cost? Ask before sending:
ChainStore.plan(482113); // { transactions: 129, feeLamports: 645000, leafSize: 3912, depth: 2 }2. A name that always points to the newest version
A file's address never changes, so "the newest version of my thing" needs a pointer. A pointer is a small record under a tag that only your key can move.
import { tagAddress } from "chainboot";
const tag = tagAddress("my-app", me.publicKey, "settings");
await store.putPointer(me, tag, Buffer.from(JSON.stringify({ file: ref.sig, version: 3 })));
// anyone can read it; records written by other keys under the same tag are ignored
const latest = await store.getPointer(tag, [me.publicKey]);
console.log(JSON.parse(latest.payload.toString()), latest.slot, latest.blockTime);A pointer holds up to about 3.8 kB. For more, store a file and point at it. With new ChainStore(pool, { accounts: true }), or putPointer(..., { account: true }), a pointer of up to 400 bytes is also kept in an account, where getPointer reads it with one request (a deposit of about 0.0033 SOL, returned by closePointerAccount). getPointer asks up to three endpoints which record is the newest and merges the answers, and between the candidates the sequence number that the writer signed decides (latest.seq), so an endpoint that is behind, hides something, or lists an old record as new does not decide what you see. It returns null only when the address really has no record by your keys; when that cannot be found out, it throws. store.pointerCapacity() says how long a pointer may be on the chain the store is connected to. store.get(sig, { maxSize }) limits what an object fetched by signature alone may claim to be (1 GiB unless you say otherwise).
3. Many endpoints, one pool
const pool = new RpcPool([url1, url2, url3], { onNote: console.log });
await pool.probe(GENESIS["mainnet-beta"]);
const slot = await pool.call("getSlot"); // any JSON-RPC method
const balance = await pool.call("getBalance", [me.address, { commitment: "confirmed" }]);
console.table(pool.stats()); // per endpoint: learned rate, successes, rate limits, latencyWhat the pool does for you:
- learns each endpoint's speed limit: starts carefully, speeds up while requests succeed, backs off on "429 Too Many Requests";
- retries on another endpoint when one fails, and never on the same one twice in a row;
- hedges: if an endpoint is much slower than usual, the same question goes to a second one and the first answer wins;
- batches: calls made together travel in one HTTP request where the endpoint allows it (
pool.call(method, params, { batch: true })); - never prints your API keys (
pool.stats()and notes show masked labels).
4. Transactions by hand
The SDK builds and parses Solana transactions itself, both the legacy format and the new 4096-byte v1 format.
import { buildTransaction, transferInstruction, parseTransaction, verifyPayerSignature, sendAndConfirm, addressToBytes } from "chainboot";
const to = addressToBytes("7xQ…");
const signature = await sendAndConfirm(pool, (blockhash) =>
buildTransaction(me, [transferInstruction(me.publicKey, to, 1_000_000)], blockhash, { version: "legacy" }));
const raw = Buffer.from((await pool.call("getTransaction", [signature, { encoding: "base64", maxSupportedTransactionVersion: 1 }])).transaction[0], "base64");
const tx = parseTransaction(raw);
console.log(tx.version, verifyPayerSignature(tx), tx.instructions.length);5. Images
import { readTar, decompressAuto, buildImage, publishImage, readCatalog, pullImage } from "chainboot";
// build: nothing touches the network here
const entries = readTar(decompressAuto(fs.readFileSync("rootfs.tar.gz")));
const built = buildImage(entries, { name: "mylinux", version: "1.0", arch: "amd64", description: "My Linux" });
console.log(built.manifest.digest, built.manifest.fileCount);
// publish: packs, manifest, catalog entry
await publishImage(store, me, built, { journalDir: "./journal", onProgress: (p) => console.log(p.phase, p.done, p.total) });
// find and download, fully verified
const catalog = await readCatalog(store, me.address);
const ref = catalog.images.mylinux.tags.latest.amd64;
const image = await pullImage(store, me.address, ref, { cacheDir: "./cache" });
const firstPack = image.pack(0); // decompressed and checked file by fileextractImage(image.manifest, (i) => image.pack(i), directory) writes an image to disk. entriesFromImage and writeTar turn it back into an archive.
6. Encrypted state for a wallet
import { vaultFromKeypair, deriveVault, LOGIN_MESSAGE, seal, open } from "chainboot";
// from a keypair file ...
const vault = vaultFromKeypair(me, "devnet");
// ... or from a browser wallet's signature of LOGIN_MESSAGE:
// const vault = deriveVault(walletAddress, signatureBytes, "devnet");
const secret = seal(vault.encKey, Buffer.from("only I can read this"), "note");
const ref2 = await store.put(vault.signer, secret); // the vault account pays and signs
const back = open(vault.encKey, await store.get(ref2, { signers: [vault.signer.publicKey] }), "note");saveSnapshot, readHead, readSnapshot, loadPacks and applySnapshot are the functions behind chainboot save and chainboot restore; scanMachine finds what changed in a machine.
Every sealed object has a key of its own, so one object can be handed to somebody else without giving away the rest. To send something to another login, seal it to their share address:
import { sealWithKey, openWithKey, shareKeys, shareAddress, parseShareAddress, sealTo, openFrom } from "chainboot";
const { sealed, key } = sealWithKey(vault.encKey, Buffer.from("for one friend"), "note"); // key: base64, opens only this object
openWithKey(Buffer.from(key, "base64"), sealed, "note");
const mine = shareKeys(vault); // an X25519 pair derived from the login
console.log(shareAddress(mine.publicKey)); // "share_...": what you give to others
const letter = sealTo(parseShareAddress(theirAddress), mine, Buffer.from(key), "my-app");
const got = openFrom(theirKeys, letter, "my-app"); // { plain, sender } or null; sender is provenshareImage, shareMachine, listShares, acceptImage and acceptMachine are the functions behind chainboot share and chainboot shared. What listShares returns has been rebuilt from checked fields (cleanShareRecord); acceptImage(…, name, { merge: true }) is how a caller says that the shared image may be added to one the vault already has under that name.
7. In a browser
The SDK above is for Node. For web pages there is a separate reader: one script of about 30 kB without dependencies, built on fetch and WebCrypto, that finds, downloads and verifies public images. It is the same format and the same checks, written a second time, which is also the proof that the format can be read by anyone. (One difference: it decodes xz and uncompressed objects. An image published from a computer without the xz tool is stored with zstd, which browsers cannot decode by themselves; chainboot itself reads both.)
<script type="module">
import { Reader, entries, fileContent } from "./chainboot-reader.js";
const reader = new Reader({ endpoints: ["https://api.devnet.solana.com"], onStep: console.log });
const publisher = "HM4msDau4mPX9P2qinBPTGKiU1TZW5e86jTukeufr5uu";
const catalog = await reader.catalog(publisher); // signed by the publisher
const ref = catalog.images.alpine.tags.latest.amd64;
const manifest = await reader.manifest(publisher, ref); // SHA-256 from the catalog
const pack = await reader.pack(manifest, 0, { onProgress: (p) => console.log(p.bytes, "of", p.size) });
const file = entries(manifest).find((e) => e.path === "etc/hostname");
console.log(new TextDecoder().decode(await fileContent(file, pack))); // checked against the manifest
</script>npm run build writes it to dist/reader/ together with a small page that lets you browse a publisher's catalog and look into the files of an image. reader.object(ref) and reader.pointer(tag, signers) read any object and any pointer, so your own data works too.
What to know:
- It needs a browser that can check Ed25519 signatures: Chrome and Edge 137, Firefox 129, Safari 17, or later.
canVerify()tells you. - The page's visitors use the endpoints you name. The public endpoints allow requests from web pages but are slow (a 3 MB system takes about half a minute); a keyed endpoint is far faster, but a key inside a public page can be used by anyone who reads the page.
- It reads public images. Private images and saved machines need a wallet's key and stay with the program.
- Everything is held in memory, so it suits small systems and single files, not a 500 MB image on a phone.
8. Compression
import { compress, decompress, xzDecode } from "chainboot";
const packed = compress(data, { arch: "amd64" }); // xz when the tool is installed, otherwise zstd
const again = decompress(packed.codec, packed.data, data.length);The SDK contains a complete xz decoder written in TypeScript, so reading never depends on installed tools.
Reference at a glance
| Area | Exports |
|---|---|
| Keys | Keypair (generate, fromFile, fromSeed, fromSecretKey, sign, address), verifySignature, addressToBytes, b58encode, b58decode |
| Transactions | buildTransaction, buildNoopTransaction, parseTransaction, verifyPayerSignature, compileMessage, compileMessageV1, noopCapacity, noopInstruction, transferInstruction, computeBudgetInstructions |
| RPC | RpcPool (call, broadcast, probe, stats), RpcError, GENESIS, clusterName, maskRpcUrl |
| Storage | ChainStore (put, get, putPointer, getPointer, listPointers, pointerCapacity, closePointerAccount, plan), tagAddress, capacity, TxSender, sendAndConfirm, TrustError |
| Images | readTar, writeTar, decompressAuto, buildImage, entriesFromImage, verifyPack, extractImage, parseManifest, serializeManifest, validateManifest, treeDigest, configIdentity |
| Registry | publishImage, pullImage, readCatalog, unlistImage, parseImageName, publishPrivateImage, readPrivateCatalog, unlistPrivateImage |
| State | LOGIN_MESSAGE, deriveVault, vaultFromKeypair, seal, open, scanMachine, saveSnapshot, readHead, readSnapshot, snapshotBySeq, checkSnapshot, readIndex, loadPacks, applySnapshot |
| Sharing | shareImage, shareMachine, listShares, acceptImage, acceptMachine, cleanShareRecord, myShareAddress, sealTo, openFrom |
| Compression | compress, decompress, xzDecode, xzUncompressedSize |
Every export has type declarations and a comment in the source. The sources are short and meant to be read: start with src/store/store.ts.