Helius DAS, locally. Built on Surfpool.
A Helius-compatible local dev environment for Solana — DAS, compressed NFTs, WebSocket subscriptions, Enhanced Transactions, and webhooks, all from a single Rust binary you run next to your validator. Your production helius-sdk integration works offline, without a key, without cost.
Every release ships from GitHub Actions via OIDC trusted publishing — crates.io and npm both — with signed sigstore provenance on the npm tarball.
Three things you'll notice in the first five minutes.
🌊 Local DAS, including compressed NFTs. getAsset, getAssetBatch, getAssetProof, the getAssetsBy* family, searchAssets. MplCore, Token Metadata (both Token and Token-2022), and Bubblegum cNFTs — all resolved locally from real on-chain bytes. cNFTs go through a full Bubblegum indexer that replays every tree-mutating instruction; authoritative state comes from the noop-CPI LeafSchemaEvent, so proofs match on-chain even after a setAndVerifyCollection.
⚡ confirmTransaction() works against a single Tidepool endpoint. Surfpool now ships native WebSocket subscriptions, but on a separate port (8900 by default). Tidepool reverse-proxies WS through its own port so clients keep one base URL across HTTP, REST, and WS. Every helius-sdk method that composes "send, wait, assert" — sendSmartTransaction, broadcastTransaction, pollTransactionConfirmation — just works.
🧪 Plugs into MSW / Nock / undici for tests. Import @vibestackmd/tidepool from npm, plug handleJsonRpcBody into whichever mock-HTTP layer your team uses. Your test suite gets deterministic Helius responses without standing up a validator.
Three ways to consume it. Pick one.
# Terminal 1
surfpool start
# Terminal 2
cargo install tidepool-cli
tidepool start \
--port 8897 \
--upstream http://127.0.0.1:8899 \
--index-tree <your-cNFT-merkle-tree>// Your app
import { Connection } from "@solana/web3.js";
const connection = new Connection("http://localhost:8897", "confirmed");# Cargo.toml
[dependencies]
tidepool-rpc = "0.1"use tidepool_rpc::cnft::{apply_event, CnftEvent, MemoryCnftStore};
use tidepool_rpc::das::{get_asset, get_asset_proof};Full example in examples/rust-integration/.
npm install @vibestackmd/tidepool msw vitestimport { HeliusContext, handleJsonRpcBody } from "@vibestackmd/tidepool";
import { http, HttpResponse, passthrough } from "msw";
import { setupServer } from "msw/node";
const ctx = new HeliusContext();
setupServer(
http.post("http://127.0.0.1:8899/", async ({ request }) => {
const response = await handleJsonRpcBody(ctx, await request.text());
return response ? HttpResponse.json(JSON.parse(response)) : passthrough();
}),
).listen();Full runnable vitest setup in examples/msw-integration/.
Point the same client at Tidepool — one URL swap, every transport works. JSON-RPC, REST (/v0/…), and WebSocket all resolve against Tidepool because we mirror Helius's transport split exactly.
import { Helius } from "helius-sdk";
const helius = new Helius(
process.env.HELIUS_API_KEY ?? "local-dev",
process.env.NODE_ENV === "development"
? { url: "http://localhost:8897", restUrl: "http://localhost:8897" }
: undefined, // prod: default to mainnet.helius-rpc.com + api.helius.xyz
);
await helius.rpc.getAsset({ id: mintPubkey }); // JSON-RPC
await helius.enhanced.getTransactions([signature]); // REST
await helius.ws.signatureNotifications(signature); // WS proxied to upstreamThe restUrl + url split assumes a small PR landing in helius-labs/helius-sdk to make the REST base URL configurable. Until it merges, JSON-RPC + WS work today; REST needs the SDK's internal base URL overridden via whatever escape hatch your SDK version provides.
Tidepool sits between your app and Surfpool. Requests for methods we own (DAS, cNFT proofs, enhanced tx, webhooks) are served from local state; everything else is forwarded to Surfpool unchanged. The WS port is a thin reverse proxy onto Surfpool's native subscriptions.
- Uncompressed
getAssetfetches the account from the upstream, runs it through a pluggable decoder (mpl-core/mpl-token-metadata), then fetches the off-chain JSON at the asset'suriand folds image / description / attributes / files into the response — matching real Helius. Off-chain fetch supportshttp(s)://andfile://(for locally-seeded dev metadata), fails soft (a blocked or slow fetch degrades to on-chain fields, never errors), and is cached per asset. Disable with--no-offchain-metadatafor hermetic runs. The cache populates as a side effect sosearchAssets,getAssetsByOwner, and the other secondary-index queries work immediately. - Compressed
getAsset/getAssetProofresolve from a local Bubblegum indexer:getSignaturesForAddresswalks the tree,getTransactionpulls each candidate tx, inner Bubblegum + noop CPIs are parsed for authoritative leaf state. Trees are registered via--index-treeat startup ortidepool_indexTreeat runtime. - Everything unknown falls through to the upstream unchanged. Standard Solana RPC (
getSlot,sendTransaction,getProgramAccounts, etc.) works with zero code on our side.
Tidepool works with any standard Solana RPC — solana-test-validator with --clone, real devnet, a self-hosted node. Surfpool is recommended because its mainnet-forking means any real account you ask about "just works" without pre-declaring it. That's what makes the dev-loop feel magic instead of tedious. Tidepool's WS reverse proxy specifically targets Surfpool's native subscription endpoint (default ws://upstream:8900), so against other validators the WS port may have nothing useful to forward to.
Full live truth: POST {"method":"tidepool_info"} returns the complete manifest. Every entry is classified EXACT, LOCAL_INDEX, BEST_EFFORT, SHIM, SDK_WRAPPER, PLANNED, or SKIPPED.
| Method | Status | Notes |
|---|---|---|
getAsset / getAssetBatch |
✅ LOCAL_INDEX | MplCore, Token Metadata (incl. Token-2022), cNFTs |
getAssetProof / getAssetProofBatch |
✅ LOCAL_INDEX | Requires tree registered via --index-tree or runtime method |
getAssetsByOwner / Authority / Creator / Group |
✅ LOCAL_INDEX | Cache-backed secondary indexes |
searchAssets |
✅ LOCAL_INDEX | Multi-filter AND, smallest-index-first narrowing |
getNftEditions |
✅ LOCAL_INDEX | Lazy edition-PDA indexing; master + print editions |
signatureSubscribe / accountSubscribe / logsSubscribe (+ Unsubscribe) |
✅ EXACT | Reverse-proxied to Surfpool's native WS. All filters Surfpool supports work, including logsSubscribe with { mentions: [pubkey] }. |
getPriorityFeeEstimate |
✅ BEST_EFFORT | Local percentile ladder over getRecentPrioritizationFees |
helius-sdk composed methods |
✅ SDK_WRAPPER | Send / broadcast / confirm / staking — all work transparently |
getBalances (REST) |
✅ SHIM | GET /v0/addresses/{addr}/balances |
getTransactions / getTransactionsByAddress (REST) |
✅ SHIM | Enhanced Transactions parsers on /v0/transactions and /v0/addresses/{addr}/transactions |
getTransactionsForAddress (JSON-RPC) |
✅ BEST_EFFORT | Combined sig fetch + tx + classify. limit, paginationToken, minSlot, maxSlot, status filters. History limited to what the upstream has streamed. |
getTransfersByAddress (JSON-RPC) |
✅ BEST_EFFORT | Parsed SOL + SPL transfer history per wallet. mint, direction, limit, sort, paginationToken. Same history caveat. SPL decimals defaults to 0 pending token-info cache. |
createWebhook family (REST) |
✅ SHIM | Polling-simulator on /v0/webhooks + /v0/webhooks/{id} — full CRUD |
| Everything else | ✅ Passthrough | Forwarded to the upstream unchanged |
Tidepool matches Helius's transport split exactly — a method lives where Helius puts it and nowhere else, so you can't write local-dev code that'd fail against production.
| Transport | Where | Methods |
|---|---|---|
| JSON-RPC | POST / |
DAS (getAsset*), Bubblegum control (tidepool_*), standard RPC passthrough |
| REST | /v0/… |
Wallet (getBalances), Enhanced Transactions, Webhooks CRUD |
| WebSocket | ws://host:port+1 |
signatureSubscribe, accountSubscribe, logsSubscribe, *Unsubscribe |
| SDK Wrapper | n/a | sendSmartTransaction, broadcastTransaction, pollTransactionConfirmation, stake/unstake |
tidepool_info returns a transport field per method so tooling can introspect without guessing.
cNFTs live as leaves in a Bubblegum merkle tree, not as standalone accounts. Tidepool ships a local indexer that replays every tree-mutating instruction into an in-memory (or SQLite-backed) store, from which getAsset / getAssetProof serve directly.
Register a tree:
# At startup
tidepool start --index-tree <merkle-tree>
# Or at runtime (in a vitest setup file, CI script, etc.)
curl -X POST http://localhost:8897 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tidepool_indexTree","params":{"tree":"<merkle-tree>"}}'Tracked instructions: createTree, mintV1 / mintV2, mintToCollectionV1, transfer / transferV2, burn / burnV2, delegate / delegateV2, verifyCreator / verifyCreatorV2, unverifyCreator, verifyCollection, unverifyCollection, setAndVerifyCollection / setCollectionV2, updateMetadata / updateMetadataV2. For hash-dependent ixs, authoritative state comes from the noop LeafSchemaEvent CPI — proofs stay correct through multi-step flows. Covers both SPL-NOOP (V1) and MPL-NOOP (V2) noop programs.
Default is in-memory — state is lost on restart. Two flags turn that off, shaped after Surfpool's own persistence UX so the two tools feel familiar.
# Single SQLite file holds cNFT index + DAS cache + webhook registry
tidepool start --db ./tidepool.sqlite
# Preload snapshot(s) at boot; repeatable
tidepool start --snapshot ./trees/foo.json --snapshot ./trees/bar.jsontidepool_exportTreeSnapshot dumps a tree's indexed state at runtime; commit it to your repo and every fresh boot can --snapshot that file to skip re-paging tx history.
examples/msw-integration/— vitest + MSW + Tidepool, three runnable testsexamples/rust-integration/— cargo example composing the service layer directlyexamples/README.md— all consumer patterns indexed
| Crate | Purpose |
|---|---|
tidepool-core |
Pure primitives: keccak, merkle math, LeafSchemaV1 hashing, proof compute/verify. Zero Solana deps — WASM-ready. |
tidepool-rpc |
Service layer: cNFT state machine, DAS handlers, cache, decoders, upstream trait. |
tidepool-server |
axum HTTP + WS front-end. Method-enum dispatch. HttpUpstream via reqwest. |
tidepool-cli |
tidepool binary. clap-derive args + env-var overlay. |
tidepool-node |
napi-rs bridge → the @vibestackmd/tidepool npm package. |
Library consumers pull tidepool-rpc. Binary users cargo install tidepool-cli. JS users npm install @vibestackmd/tidepool. Server builders compose tidepool-rpc + tidepool-server::HttpUpstream themselves.
Is this production-ready?
No. It's a local development tool. Ship to real Helius in production.
Does this replace Helius?
No. It lets you develop against Helius's API locally so your production integration has a tight dev loop.
Is this endorsed by Helius or Surfpool?
Community tool, no official endorsement. Both are great companies and you should use them.
Why not just hit real Helius in dev?
You'd burn rate limits, pollute prod monitoring, require internet on CI, and can't test without an API key. Tidepool is the answer to "I want the dev loop to be instant + offline."
Can I use this with `solana-test-validator` or `litesvm`?
solana-test-validator works — point --upstream at it, clone mainnet accounts via --clone. litesvm is in-process-only, so there's no RPC endpoint for Tidepool to proxy. Use Surfpool for the magic, test-validator for the boring-but-predictable case.
Why Rust?
The previous version was TypeScript (v0.6, preserved at that tag). The Rust rewrite earned: drop-in official mpl-core / mpl-token-metadata / mpl-bubblegum crates (no Codama pipeline), exhaustive-match method dispatch (compile-time safety for adding new handlers), type-level noop-required-vs-optional enforcement on cNFT events, binary distribution via cargo install. The napi-rs bridge means JS consumers still get the test-integration story via npm install @vibestackmd/tidepool — one Rust core, two consumption ecosystems.
Does the WS proxy work over compressed transactions?
Surfpool's native signatureSubscribe resolves any signature the validator has seen — compressed + uncompressed identically. Tidepool just forwards frames; whatever Surfpool tracks is what clients see.
- ✅ v0.1.x — Rust rewrite shipped. MplCore / Token Metadata / cNFT decoders, full DAS surface, WS polyfills (
signatureSubscribe,accountSubscribe,logsSubscribe), axum server, CLI binary, napi bridge, REST transport, webhooks CRUD, Enhanced Transactions. End-to-end OIDC release pipeline (crates.io + multi-platform npm). - ✅ v0.2.0 — Surfpool catch-up. WS polyfills replaced by a thin reverse proxy onto Surfpool's native subscription endpoint (v1.1+). Upstream refs updated to
solana-foundation/surfpool. ~1,000 lines of polling polyfill deleted. - 0.3 — Helius catch-up.
getTransactionsForAddress,getTransfersByAddress,getProgramAccountsV2,getTokenAccountsByOwnerV2, Wallet REST API stubs, optionally an MCP server variant mirroring Helius for Agents. - 0.4 — USD pricing pass-through, Token Metadata owner-resolution polish across all interfaces.
- 1.0 — API freeze. Library crates (
tidepool-core,tidepool-rpc,tidepool-server) become stable surfaces; right now they're publish-as-required-for-the-CLI, not promised. - Maybe — LaserStream-compatible event emitter, Dragon's Mouth (Yellowstone gRPC) polyfill.
- v0.1.0 – v0.6.0 (TypeScript) — original implementation, preserved at git tags
v0.1.0throughv0.6.0. Not on any registry. No longer maintained. - v0.1.0+ (Rust, this codebase) — distributed as
tidepool-clion crates.io and@vibestackmd/tidepoolon npm. Versions are lockstep across both registries.tidepool-cliis the supported entry point; the other crates are internal until 1.0.
- Surfpool — the local Solana validator Tidepool runs on top of
- Helius DAS — the production API Tidepool mimics
- Metaplex MplCore, Bubblegum — the asset standards
- mpl-core, mpl-token-metadata, mpl-bubblegum — the official Rust crates Tidepool uses
MIT · LICENSE