tidepool-server 0.3.0

Tidepool HTTP + WebSocket JSON-RPC server. Wraps the service layer (cNFT indexing, DAS handlers, passthrough proxy) in an axum front-end.
Documentation

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), returns a DAS-shaped response. 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

No. It's a local development tool. Ship to real Helius in production.

No. It lets you develop against Helius's API locally so your production integration has a tight dev loop.

Community tool, no official endorsement. Both are great companies and you should use them.

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

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.

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.

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