@warptoad/skinny-fat-imt-js
v0.0.8
Published
js library counter part of skinny-fat-imt contracts, used to sync the onchain tree client side
Downloads
442
Maintainers
Readme
@warptoad/skinny-fat-imt-js
JS library counterpart of the skinny-fat-imt contracts, used to sync the onchain tree client side.
Reads a contract's LeanIMT state over viem — from storage or from events, whichever the contract exposes — and keeps a local mirror in sync.
TODO
Decide where to keep the create2 artifacts and all that byte code. Using this package in frontend also downloads all that? Probably less of an issues when using bundlers?
Install
pnpm add @warptoad/skinny-fat-imt-js viemviem is a peer dependency, so it isn't installed for you. That's deliberate: two copies of viem in one tree produce PublicClient types that look identical but don't unify, and you'd get baffling assignability errors passing your client into Trees.
ESM only, Node >= 22.
Usage
import { Trees } from "@warptoad/skinny-fat-imt-js";
import { createPublicClient, http } from "viem";
const client = createPublicClient({ transport: http(rpcUrl) });
const trees = new Trees(contractAddress, client);
// full node: read the trees straight from contract storage
const synced = await trees.sync([treeId]);
// archive node: replay the contract's events, discovering treeIds as it goes
const all = await trees.sync([], { fullNodeMode: false, autoDiscovery: true });fullNodeMode (default true) reads current storage, so a plain full node is enough — that's what the name means. It's the cheap path, but it only sees the tree as it is now.
Turning it off replays events, which needs a provider that still has the logs. Recent history is fine on a full node; anything older than roughly a year wants an archive node, and autoDiscovery scans all the way back to the deployment block, so it always does. In exchange you get history: syncToRoot to reconstruct an old root, and treeId discovery, neither of which storage reads can do.
syncTreesStorage / syncTreesEvent are the two paths directly if you'd rather not go through sync.
The event-scanning helpers are a separate entry point, so you can chunk getLogs calls against any contract without pulling in the tree machinery:
import { queryEventInChunks, queryMultiEventsInChunks, minBigInt } from "@warptoad/skinny-fat-imt-js/event-scanning";| Entry point | Exports |
| --- | --- |
| @warptoad/skinny-fat-imt-js | Trees, identifyTree, getEventFilter, copyTree, ERC165_IDS, the ABIs, getInterfaceId, DEPLOY_BLOCK, … |
| @warptoad/skinny-fat-imt-js/event-scanning | queryEventInChunks, queryMultiEventsInChunks, minBigInt, EventLog, PostQueryEventFilter |
| @warptoad/skinny-fat-imt-js/create2 | makeCreate2, deployCreate2, predictCreate2Address, create2MiningTarget, verifyOnEtherscan, verifyOnSourcify, matchesOnchainBytecode |
| @warptoad/skinny-fat-imt-js/create2/evm-artifacts/<Contract> | The frozen Create2Artifact JSON for each shipped IMT library |
Deterministic CREATE2 for any contract
/create2 is a general toolkit, independent of the IMT libraries — point it at any Hardhat 3 contract:
const artifact = await makeCreate2("MyContract", { constructorArgs: [...] });
const { address } = await deployCreate2({ artifact, salt, walletClient, publicClient });
await verifyOnEtherscan({ artifact, address, chainId, apiKey });makeCreate2 freezes the init code together with the solc input that produced it, so nothing downstream recompiles and the address holds on every chain. The IMT libraries ship pre-frozen, so bringing them to a new chain needs no compiler:
import artifact from "@warptoad/skinny-fat-imt-js/create2/evm-artifacts/SkinnyIMTPoseidon2Read" with { type: "json" };See CREATE2.md.
Development
pnpm install
pnpm compile # hardhat compile, then regenerate src/abis.ts
pnpm test # hardhat test
pnpm typecheck # tsc over src + test + scripts + hardhat.config.ts
pnpm build # emit dist/ from src/ via tsconfig.build.jsonAbout src/abis.ts
src/abis.ts is generated and committed. scripts/genAbis.ts copies nine ABIs out of hardhat's artifacts/ into as const tuples.
The library deliberately does not import artifacts/ directly, even though that's the obvious thing to do. artifacts/ is gitignored, ~13MB, and hardhat's generated artifacts.d.ts files end with:
declare module "hardhat/types/artifacts" { interface ArtifactMap { … } }Shipping that would make the published typings require hardhat to be resolvable in the consumer's project, and would inject our contract names into their ArtifactMap. Baking the ABIs in keeps full viem inference with no hardhat dependency at all — hardhat stays a devDependency and never enters a consumer's graph.
Whenever the contracts change, run pnpm compile and commit the resulting src/abis.ts diff. Nothing regenerates it automatically at publish time — that's on purpose, so npm publish can never silently rewrite source.
deployment
pnpm hardhat gen-artifact-create2 --network sepolia;
pnpm hardhat mine-create2 --network sepolia --zeros 6;
pnpm hardhat deploy-create2 --network sepolia;
pnpm hardhat verify-create2 --network sepolia;Publishing to npm
One-time setup
Log in. The @warptoad scope must already exist on npm and your account must be able to publish to it.
npm login
npm whoami # confirm the right accountpublishConfig.access is already set to public in package.json. Without it npm assumes scoped packages are private and rejects the publish on a free account.
The package name is
@warptoad/skinny-fat-imt-js, all lowercase — npm rejects uppercase in package names, soskinny-fat-IMT-jsis not a valid name.
Publishing a version
Make sure the ABIs are current and everything passes:
pnpm compile # only needed if the contracts changed pnpm typecheck pnpm testCommit any
src/abis.tsdiff before continuing.Bump the version. This writes package.json, commits, and tags:
npm version patch # or: minor / majorCheck what will actually ship before you ship it:
npm pack --dry-runExpect
dist/**,src/**,README.md,CREATE2.md,LICENSE,package.json— about 50KB. If you seeartifacts/or a multi-MB tarball, something is wrong withfilesin package.json.Publish.
prepublishOnlyrunspnpm build(a cleanrm -rf distthentsc -p tsconfig.build.json), sodist/is always rebuilt from current source:npm publishPush the commit and tag:
git push --follow-tags
Publishing a prerelease
To try a version out without moving the latest tag:
npm version prerelease --preid=rc # 0.0.1 -> 0.0.2-rc.0
npm publish --tag nextConsumers get it with pnpm add @warptoad/skinny-fat-imt-js@next; plain pnpm add @warptoad/skinny-fat-imt-js still resolves to the last stable release.
Verifying the published package
mkdir /tmp/check && cd /tmp/check && pnpm init
pnpm add @warptoad/skinny-fat-imt-js viem
node -e "import('@warptoad/skinny-fat-imt-js').then(m => console.log(Object.keys(m)))"Notes
- You cannot republish a version. Once
0.0.1is out,0.0.1is taken forever, even if you unpublish. Bump and move on. npm unpublishis only allowed within 72 hours, and only if nothing depends on it. Usenpm deprecateinstead for anything older.- If publish fails with
402 Payment Required,publishConfig.accessgot lost — scoped packages need"access": "public". - If it fails with
404, you're either not logged in or not a member of the@warptoadscope.
Deployed addresses sepolia
SkinnyIMTPoseidon2WriteStorage 0x00000066C760D24272a9E8A424EF7233A0F0da83
SkinnyIMTPoseidon2WriteEvent 0x000000D74387530f88b9Cf1A23CDf16fdDC01bf6
SkinnyIMTPoseidon2Read 0x000000A3a94867DD396753dEFABF989246211329
FatIMTPoseidon2WriteStorage 0x000000F35EFB6F537d3663CF263c04f467f48c73
FatIMTPoseidon2WriteEvent 0x00000012de70B336818C26517A4B2d7c0B949C7b
FatIMTPoseidon2Read 0x000000FF3329863F1Eb55773aDEF01863791370