# 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 newer
import { 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, latency

What the pool does for you:

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 file

extractImage(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 proven

shareImage, 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:

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

AreaExports
KeysKeypair (generate, fromFile, fromSeed, fromSecretKey, sign, address), verifySignature, addressToBytes, b58encode, b58decode
TransactionsbuildTransaction, buildNoopTransaction, parseTransaction, verifyPayerSignature, compileMessage, compileMessageV1, noopCapacity, noopInstruction, transferInstruction, computeBudgetInstructions
RPCRpcPool (call, broadcast, probe, stats), RpcError, GENESIS, clusterName, maskRpcUrl
StorageChainStore (put, get, putPointer, getPointer, listPointers, pointerCapacity, closePointerAccount, plan), tagAddress, capacity, TxSender, sendAndConfirm, TrustError
ImagesreadTar, writeTar, decompressAuto, buildImage, entriesFromImage, verifyPack, extractImage, parseManifest, serializeManifest, validateManifest, treeDigest, configIdentity
RegistrypublishImage, pullImage, readCatalog, unlistImage, parseImageName, publishPrivateImage, readPrivateCatalog, unlistPrivateImage
StateLOGIN_MESSAGE, deriveVault, vaultFromKeypair, seal, open, scanMachine, saveSnapshot, readHead, readSnapshot, snapshotBySeq, checkSnapshot, readIndex, loadPacks, applySnapshot
SharingshareImage, shareMachine, listShares, acceptImage, acceptMachine, cleanShareRecord, myShareAddress, sealTo, openFrom
Compressioncompress, 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.

Edit this page on GitHub