BAMFS
Reference

SDK

@bamfs/sdk — two entry points, split by whether a chain is involved.

import { BamfsClient }       from "@bamfs/sdk";       // reads, writes, projects — needs an RPC
import { planUploadOffline } from "@bamfs/sdk/core";  // pure computation — needs nothing

@bamfs/sdk/core

Everything about the BAMFS format: chunking, BAMFS and IPFS CID derivation, compression, validation, gas arithmetic, and the canonical addresses. No RPC and no node:* import at load time, so it runs in a browser, a service worker, an edge function, or an offline CI job.

It is not a reimplementation for offline use — the real uploader runs this same code, so a plan is the upload's own arithmetic rather than an estimate of it.

import { planUploadOffline, requireCanonicalAddresses, COMPRESSION_FASTLZ } from "@bamfs/sdk/core";

const plan = await planUploadOffline(
  [{ path: "index.html", bytes: new TextEncoder().encode("<h1>hi</h1>") }],
  {
    backend: requireCanonicalAddresses(84532).storageBackend,
    maxChunkSize: 24_575,
    compression: COMPRESSION_FASTLZ,
  },
);

plan.rootCid       // the directory CID this upload will produce
plan.files         // per-file CIDs, chunk hashes, logical vs stored size
plan.directories   // every directory node, children-first: creation order
plan.uniqueChunks  // chunks actually written, after dedup
plan.storedBytes   // what the chain pays for

backend and maxChunkSize have no defaults

Neither is a preference, and guessing either produces a plan that disagrees with the chain it is predicting.

The backend address is part of every CID — a chunk hash is keccak256(backend ‖ data). The same bytes planned against a different backend land in a different content-address space, where nothing dedups and every CID still looks well-formed. Use requireCanonicalAddresses(chainId).

maxChunkSize is whatever the deployed backend reports. 24_575 is SSTORE2Backend on EIP-170 chains; another backend can report anything. With an RPC endpoint, let planUpload query it.

Stored bytes vs logical bytes

commits tochanges with codec
BAMFS CIDthe stored bytesyes
IPFS CIDthe logical bytesno

Both are correct; they answer different questions. Two consequences: bamfs diff must recompute under the codec the upload used, and a codec change is a new content address rather than a re-encoding of the same one.

Read plan.storedBytes rather than assuming compression is free — FastLZ expands an 11-byte input to 12, because a compressed frame has overhead a tiny payload cannot amortize.

@bamfs/sdk

Everything above, plus reads, writes, projects, and history.

import { BamfsClient, requireCanonicalAddresses } from "@bamfs/sdk";
import { createPublicClient, http } from "viem";
import { baseSepolia } from "viem/chains";

const client = new BamfsClient({
  publicClient: createPublicClient({ chain: baseSepolia, transport: http(RPC) }),
  addresses: requireCanonicalAddresses(84532),
});

await client.readFile(rootCid, "index.html");

Writes additionally need a walletClient.

Projects

Every project query is an eth_call against contract storage — the hooks maintain the indexes on-chain as writes happen:

await readProject(client, contracts, tokenId);        // head CID, name, gateway, owner
await readVersionHistory(client, contracts, tokenId);
await listTags(client, contracts, tokenId);

await readRecentProjects(client, contracts);          // the publish feed
await readOwnedProjects(client, contracts, owner);    // needs the enumeration hook

readOwnedProjects returns undefined when no enumeration hook is configured. That is deliberately distinct from owning nothing: collapsing them would tell someone they own no projects when the truth is their projects are not indexed.

QuerySource
Version history, tagsBamfsProjectHook
Publish feedBamfsProjectHook's append-only ledger
Projects by ownerBamfsProjectEnumeration

Paging

The ledger is append-only, so a cursor is an index that stays valid indefinitely — new publishes append above it and never renumber below:

let cursor: bigint | undefined;
do {
  const page = await readPublishPage(client, contracts, { limit: 20, before: cursor });
  render(page.records);            // newest first
  cursor = page.nextCursor;        // undefined when exhausted
} while (cursor);

iteratePublishLedger and iterateOwnedProjects are async generators, for walking an unbounded ledger without holding all of it.

Addresses

import { CANONICAL_DEPLOYMENTS, requireCanonicalAddresses } from "@bamfs/sdk/core";

Baked in rather than configured: they are public deployment artifacts, and the canonical ones are the only addresses that produce interoperable CIDs. Pass explicit ContractAddresses to override, which is what a local chain needs.

See Deployments.

On this page