Skip to content

rust ​

Send a transaction using Rust with the solana-* crates and reqwest.

Dependencies ​

toml
[dependencies]
solana-address = { version = "2.6", features = ["decode"] }
solana-hash = "4.3"
solana-keypair = "3.0"
solana-signer = "3.0"
solana-message = { version = "4.6", features = ["wincode"] }
solana-transaction = { version = "4.3", features = ["wincode"] }
solana-system-interface = { version = "2.0", features = ["bincode"] }
solana-compute-budget-interface = "3.0"
wincode = "0.6"
reqwest = { version = "0.12", features = ["blocking", "json"] }
serde_json = "1"
base64 = "0.22"
rand = "0.8"

Full Example ​

rust
use std::error::Error;

use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use rand::seq::SliceRandom;
use reqwest::blocking::Client;
use serde_json::{json, Value};
use solana_address::Address;
use solana_hash::Hash;
use solana_keypair::Keypair;
use solana_message::{Instruction, Message, VersionedMessage};
use solana_signer::Signer;
use solana_system_interface::instruction::transfer;
use solana_transaction::versioned::VersionedTransaction;

// Your Solana RPC: blockhash and confirmations
const RPC_URL: &str = "https://api.mainnet-beta.solana.com";
// BlockSprint: sending only
const ENDPOINT: &str = "https://fra.blocksprint.io/api/v1/YOUR_API_KEY";

// BlockSprint tip addresses
const TIP_ADDRESSES: &[&str] = &[
    "Sp1x2AqpQckPLaWnWCJUNg8k6qQexfaEWcSRKf5JcDV",
    "Sp4JHSh9cksfzXbgK7Pq2ovtn8LirLQydaJKTsiNT77",
    "Sp1xMS2cbw83SZDNr4AGqkBYYLjb3LvVnmDSrTMaHkr",
    "SpagSJmnh8E9cGT5Y431xPPaS2c1xLREGGCWN9yDeUf",
    "SpWrza9E63MQuHeGnnfzmtLVCs3pBdjyKPXUABPo9nq",
    "SpXyoqMHJEhsNQ9fHh3DTGGzrGroZ5zfDtJwFvN4kXi",
    "Spv2g3L837bVeHwkyb18LhA5P1bvJbkvaZbxcZXbwC2",
    "SpDUe5MyFGG97SmdBQMWNPbx2Et9bZt2TCbZm3TujgX",
    "SpKZ28tFzQ13kVjHhsyDEiQfkPGjA7Wmk185KcEDyuh",
    "SpxPqegfoR3QkBN7guRUiU6QfhtTW1CnKSTSSapumqm",
];

fn get_random_tip_address() -> Address {
    let address = TIP_ADDRESSES.choose(&mut rand::thread_rng()).unwrap();
    address.parse().unwrap()
}

/// JSON-RPC call to any endpoint. Returns `result`, or the JSON-RPC error.
fn rpc_call(client: &Client, url: &str, method: &str, params: Value) -> Result<Value, Box<dyn Error>> {
    let body = json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params });
    // Errors come back as JSON-RPC errors with an HTTP 4xx/5xx status
    let response: Value = client.post(url).json(&body).send()?.json()?;
    if let Some(error) = response.get("error") {
        return Err(format!("RPC Error: {}", error).into());
    }
    Ok(response["result"].clone())
}

fn main() -> Result<(), Box<dyn Error>> {
    // Reuse one client so connections stay open between calls
    let client = Client::new();

    // Your funded wallet and the recipient
    let wallet = Keypair::from_base58_string(&std::env::var("PAYER_SECRET_BASE58")?);
    let recipient: Address = std::env::var("RECIPIENT")?.parse()?;

    // Build instructions
    let instructions: Vec<Instruction> = vec![
        // Your transfer
        transfer(&wallet.pubkey(), &recipient, 1_000_000),
        // Tip instruction (0.001 SOL minimum)
        transfer(&wallet.pubkey(), &get_random_tip_address(), 1_000_000),
    ];

    // Get blockhash from your RPC
    let latest = rpc_call(&client, RPC_URL, "getLatestBlockhash", json!([{ "commitment": "confirmed" }]))?;
    let blockhash: Hash = latest["value"]["blockhash"].as_str().ok_or("no blockhash")?.parse()?;

    // Build and sign
    let message = Message::new_with_blockhash(&instructions, Some(&wallet.pubkey()), &blockhash);
    let transaction = VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&wallet])?;
    let tx_bytes = wincode::serialize(&transaction)?;

    // Send through BlockSprint
    let signature = rpc_call(
        &client,
        ENDPOINT,
        "sendTransaction",
        json!([BASE64.encode(&tx_bytes), { "encoding": "base64", "skipPreflight": true, "maxRetries": 0 }]),
    )?;

    // "Accepted", not "landed": check it with getSignatureStatuses on your RPC
    println!("Signature: {}", signature.as_str().unwrap_or_default());

    Ok(())
}

With Priority Fee ​

rust
use solana_compute_budget_interface::ComputeBudgetInstruction;

let instructions: Vec<Instruction> = vec![
    // Set priority fee (micro-lamports per compute unit)
    ComputeBudgetInstruction::set_compute_unit_price(1_000_000),
    // Set compute limit
    ComputeBudgetInstruction::set_compute_unit_limit(200_000),
    // Your instruction
    your_instruction,
    // Tip
    transfer(&wallet.pubkey(), &get_random_tip_address(), 1_000_000),
];

Raw HTTP Request ​

rust
use reqwest::blocking::Client;
use serde_json::json;

fn send_transaction_raw(
    client: &Client,
    endpoint: &str,
    transaction_base64: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    let body = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "sendTransaction",
        "params": [
            transaction_base64,
            {
                "encoding": "base64",
                "skipPreflight": true,
                "maxRetries": 0
            }
        ]
    });

    let response: serde_json::Value = client
        .post(endpoint)
        .json(&body)
        .send()?
        .json()?;

    if let Some(error) = response.get("error") {
        return Err(format!("RPC Error: {}", error).into());
    }

    Ok(response["result"].as_str().ok_or("missing result")?.to_string())
}

INFO

Transaction V1 needs the solana-* crates 4.2 or later (the versions above). Compile the message with v1::Message::try_compile_with_config and a v1::TransactionConfig that sets the compute-unit limit and the loaded-accounts data size limit (in V1 an unset limit is 0), wrap it in VersionedMessage::V1, and serialize with wincode, not bincode.