Skip to content

vibestackmd/tidepool

Repository files navigation

Tidepool logo

Tidepool

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.

crates.io npm CI License: MIT MSRV 1.77

Every release ships from GitHub Actions via OIDC trusted publishing — crates.io and npm both — with signed sigstore provenance on the npm tarball.


Why

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.


Quickstart

Three ways to consume it. Pick one.

As a binary

# 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");

As a Rust library

# 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/.

As a Node / JS test integration

npm install @vibestackmd/tidepool msw vitest
import { 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/.

As a drop-in for helius-sdk

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 upstream

The 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.


How it works

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 getAsset fetches 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's uri and folds image / description / attributes / files into the response — matching real Helius. Off-chain fetch supports http(s):// and file:// (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-metadata for hermetic runs. The cache populates as a side effect so searchAssets, getAssetsByOwner, and the other secondary-index queries work immediately.
  • Compressed getAsset / getAssetProof resolve from a local Bubblegum indexer: getSignaturesForAddress walks the tree, getTransaction pulls each candidate tx, inner Bubblegum + noop CPIs are parsed for authoritative leaf state. Trees are registered via --index-tree at startup or tidepool_indexTree at runtime.
  • Everything unknown falls through to the upstream unchanged. Standard Solana RPC (getSlot, sendTransaction, getProgramAccounts, etc.) works with zero code on our side.

Why Surfpool as the upstream?

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.


Supported methods

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

Transports

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.


Compressed NFTs

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.


Persistence

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.json

tidepool_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

Workspace layout

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.


FAQ

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.


Roadmap

  • 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.

Versions

  • v0.1.0 – v0.6.0 (TypeScript) — original implementation, preserved at git tags v0.1.0 through v0.6.0. Not on any registry. No longer maintained.
  • v0.1.0+ (Rust, this codebase) — distributed as tidepool-cli on crates.io and @vibestackmd/tidepool on npm. Versions are lockstep across both registries. tidepool-cli is the supported entry point; the other crates are internal until 1.0.

Related


MIT · LICENSE

About

Helius DAS, running on your laptop. A drop-in Helius-compatible RPC proxy on top of Surfpool.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors