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 forbackend 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 to | changes with codec | |
|---|---|---|
| BAMFS CID | the stored bytes | yes |
| IPFS CID | the logical bytes | no |
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 hookreadOwnedProjects 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.
| Query | Source |
|---|---|
| Version history, tags | BamfsProjectHook |
| Publish feed | BamfsProjectHook's append-only ledger |
| Projects by owner | BamfsProjectEnumeration |
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.
