Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,15 @@ jobs:
MINA_TEST_SENDER_KEY: ${{ steps.accounts.outputs.sender_pk }}
MINA_TEST_RECEIVER_KEY: ${{ steps.accounts.outputs.receiver_pk }}
run: cargo test --test integration_tests -- --test-threads=1

- name: Run examples
env:
MINA_TEST_SENDER_KEY: ${{ steps.accounts.outputs.sender_pk }}
MINA_TEST_RECEIVER_KEY: ${{ steps.accounts.outputs.receiver_pk }}
run: |
set -euo pipefail
for ex in basic_usage currency_operations custom_query error_handling node_monitoring send_payment stake_delegation; do
echo "::group::example $ex"
cargo run --example "$ex"
echo "::endgroup::"
done
8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ homepage = "https://github.com/MinaProtocol/mina-sdk-rust"
documentation = "https://docs.rs/mina-sdk"
readme = "README.md"
authors = ["Mina Protocol <[email protected]>"]
keywords = ["mina", "blockchain", "graphql", "cryptocurrency"]
categories = ["api-bindings"]
keywords = ["mina", "blockchain", "graphql", "cryptocurrency", "web3"]
categories = ["api-bindings", "cryptography::cryptocurrencies"]
exclude = [".github/", "scripts/", "schema/", "tests/integration_tests.rs"]

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[dependencies]
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,20 @@ println!("{}", a.nanomina()); // 10000000000
assert!(a > b);
```

## Examples

Runnable programs live in [`examples/`](examples/):

| Example | Command | Needs a node |
|---------|---------|--------------|
| `basic_usage` | `cargo run --example basic_usage` | yes |
| `send_payment` | `cargo run --example send_payment` | yes |
| `stake_delegation` | `cargo run --example stake_delegation` | yes |
| `node_monitoring` | `cargo run --example node_monitoring` | yes |
| `custom_query` | `cargo run --example custom_query` | yes |
| `error_handling` | `cargo run --example error_handling` | yes |
| `currency_operations` | `cargo run --example currency_operations` | no |

## Development

```bash
Expand Down
17 changes: 12 additions & 5 deletions examples/basic_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ use std::time::Duration;

#[tokio::main]
async fn main() -> mina_sdk::Result<()> {
// Connect to a local Mina daemon (default: http://127.0.0.1:3085/graphql)
let client = MinaClient::new("http://127.0.0.1:3085/graphql");
// Connect to the default local Mina daemon (http://127.0.0.1:3085/graphql).
// For a custom host/port use MinaClient::from_host_and_port("host", port).
let client = MinaClient::default();

// Check sync status
let sync_status = client.get_sync_status().await?;
Expand All @@ -34,8 +35,14 @@ async fn main() -> mina_sdk::Result<()> {
);
}

// Query an account (replace with a valid public key)
match client.get_account("B62q...", None).await {
// Query an account. A Mina public key looks like:
// B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS
// Set MINA_TEST_SENDER_KEY to a real key to run this section, otherwise we skip.
let Ok(public_key) = std::env::var("MINA_TEST_SENDER_KEY") else {
println!("Skipping account query (set MINA_TEST_SENDER_KEY to enable)");
return Ok(());
};
match client.get_account(&public_key, None).await {
Ok(account) => {
println!("Balance: {} MINA", account.balance.total);
println!("Nonce: {}", account.nonce);
Expand All @@ -52,7 +59,7 @@ async fn main() -> mina_sdk::Result<()> {
/// Example: send a payment and delegate stake.
#[allow(dead_code)]
async fn send_transactions() -> mina_sdk::Result<()> {
let client = MinaClient::new("http://127.0.0.1:3085/graphql");
let client = MinaClient::default();

// Send a payment with memo
let result = client
Expand Down
91 changes: 91 additions & 0 deletions examples/currency_operations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! Working with Mina currency amounts.
//!
//! Run with: cargo run --example currency_operations
//!
//! This example demonstrates Currency creation, conversion, arithmetic,
//! and comparison — no running node required.

use mina_sdk::Currency;

fn main() -> mina_sdk::Result<()> {
// ---- Creation ----

// From a human-readable MINA string
let one_mina = Currency::from_mina("1.0")?;

// From nanomina (1 MINA = 1,000,000,000 nanomina)
let also_one_mina = Currency::from_nanomina(1_000_000_000);

// From a GraphQL response value (nanomina as string)
let from_graphql = Currency::from_graphql("1000000000")?;

assert_eq!(one_mina, also_one_mina);
assert_eq!(also_one_mina, from_graphql);

// ---- Display & conversion ----

let amount = Currency::from_mina("42.5")?;
println!("Display: {amount}"); // 42.500000000
println!("As MINA: {}", amount.mina()); // 42.500000000
println!("As nanomina: {}", amount.nanomina()); // 42500000000
println!("For GraphQL: {}", amount.to_nanomina_str()); // 42500000000

// ---- Arithmetic ----

let a = Currency::from_mina("10.0")?;
let b = Currency::from_mina("3.5")?;

// Addition
let sum = a + b;
println!("{a} + {b} = {sum}");

// Subtraction
let diff = a - b;
println!("{a} - {b} = {diff}");

// Checked subtraction (returns Error instead of panicking)
let small = Currency::from_mina("1.0")?;
let large = Currency::from_mina("999.0")?;
match small.checked_sub(large) {
Ok(result) => println!("Result: {result}"),
Err(e) => println!("Expected underflow: {e}"),
}

// Multiplication by scalar
let fee = Currency::from_mina("0.01")?;
let ten_fees = fee * 10;
println!("10 x {fee} = {ten_fees}");

// Also works in reverse
let same = 10_u64 * fee;
assert_eq!(ten_fees, same);

// Checked overflow-safe variants
assert!(fee.checked_add(a).is_some());
assert!(fee.checked_mul(100).is_some());

// ---- Comparison ----

let low = Currency::from_mina("0.5")?;
let high = Currency::from_mina("100.0")?;
assert!(low < high);
assert!(high > low);

// Can be used in collections
use std::collections::BTreeSet;
let mut amounts = BTreeSet::new();
amounts.insert(Currency::from_mina("3.0")?);
amounts.insert(Currency::from_mina("1.0")?);
amounts.insert(Currency::from_mina("2.0")?);
println!(
"Sorted: {:?}",
amounts.iter().map(|c| c.mina()).collect::<Vec<_>>()
);

// ---- Smallest unit ----

let one_nanomina = Currency::from_nanomina(1);
println!("Smallest unit: {one_nanomina}"); // 0.000000001

Ok(())
}
67 changes: 67 additions & 0 deletions examples/custom_query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! Execute a custom GraphQL query against the Mina daemon.
//!
//! Run with: cargo run --example custom_query
//!
//! The SDK covers the common queries with typed wrappers (see
//! `basic_usage` and `node_monitoring`). For anything the typed API doesn't
//! expose, `MinaClient::query` returns a builder that runs arbitrary GraphQL
//! through the same retry-aware client.

use mina_sdk::MinaClient;
use serde_json::json;

#[tokio::main]
async fn main() -> mina_sdk::Result<()> {
let client = MinaClient::default();

// Simplest case: no variables.
let data = client.query("query { version }").send().await?;
println!("Node version: {}", data["version"]);

// Query genesisConstants — not wrapped by the SDK, so run it raw.
// `coinbase` / `accountCreationFee` are returned as nanomina strings.
let data = client
.query(
r#"query {
genesisConstants {
coinbase
accountCreationFee
genesisTimestamp
}
}"#,
)
.name("genesis_constants")
.send()
.await?;

let genesis = &data["genesisConstants"];
println!("Genesis timestamp: {}", genesis["genesisTimestamp"]);
println!("Coinbase (nanomina): {}", genesis["coinbase"]);
println!("Account fee (nanomina): {}", genesis["accountCreationFee"]);

// Query with variables — here we look up the transaction pool for a key.
let key = std::env::var("MINA_TEST_SENDER_KEY")
.unwrap_or_else(|_| "B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS".into());
let data = client
.query(
r#"query ($key: PublicKey!) {
pooledUserCommands(publicKey: $key) { hash }
}"#,
)
.variables(json!({ "key": key }))
.name("pooled_for_key")
.send()
.await?;

let pool_size = data["pooledUserCommands"]
.as_array()
.map(|a| a.len())
.unwrap_or(0);
println!(
"Pending txns for {}: {}",
&key[..20.min(key.len())],
pool_size
);

Ok(())
}
68 changes: 68 additions & 0 deletions examples/error_handling.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//! Handling errors from the Mina SDK.
//!
//! Run with: cargo run --example error_handling
//!
//! Demonstrates matching on specific error variants for robust applications.

use mina_sdk::{Currency, Error, MinaClient, Payment};

#[tokio::main]
async fn main() {
let client = MinaClient::default();

// ---- AccountNotFound ----
match client.get_account("B62qNONEXISTENT", None).await {
Ok(account) => println!("Balance: {}", account.balance.total),
Err(Error::AccountNotFound(key)) => {
println!("Account {key} does not exist on chain")
}
Err(e) => eprintln!("Unexpected error: {e}"),
}

// ---- Connection errors (node not reachable) ----
let bad_client = MinaClient::from_host_and_port("127.0.0.1", 9999);
match bad_client.get_sync_status().await {
Ok(status) => println!("Status: {status}"),
Err(Error::Connection { attempts, .. }) => {
eprintln!("Could not reach node after {attempts} attempts")
}
Err(e) => eprintln!("Other error: {e}"),
}

// ---- GraphQL errors (e.g., invalid mutation input) ----
match client
.send_payment(
Payment::sender("INVALID_KEY")
.to("INVALID_KEY")
.amount(Currency::from_nanomina(0))
.fee(Currency::from_nanomina(0)),
)
.await
{
Ok(r) => println!("Unexpected success: {}", r.hash),
Err(Error::Graphql { messages, .. }) => {
eprintln!("GraphQL rejected the request: {messages}")
}
Err(e) => eprintln!("Other error: {e}"),
}

// ---- Currency validation errors (no node needed) ----
match Currency::from_mina("not_a_number") {
Ok(_) => println!("Unexpected success"),
Err(Error::InvalidCurrency(input)) => {
eprintln!("Bad currency input: {input}")
}
Err(e) => eprintln!("Other: {e}"),
}

// ---- Currency underflow ----
let small = Currency::from_mina("1.0").unwrap();
let large = Currency::from_mina("999.0").unwrap();
match small.checked_sub(large) {
Ok(result) => println!("Result: {result}"),
Err(Error::CurrencyUnderflow(a, b)) => {
eprintln!("Cannot subtract {b} from {a} (would go negative)")
}
Err(e) => eprintln!("Other: {e}"),
}
}
72 changes: 72 additions & 0 deletions examples/node_monitoring.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//! Monitor a Mina node — sync status, peers, and recent blocks.
//!
//! Run with: cargo run --example node_monitoring
//!
//! Demonstrates using the SDK for node observability and health checks.

use mina_sdk::{MinaClient, SyncStatus};

#[tokio::main]
async fn main() -> mina_sdk::Result<()> {
let client = MinaClient::default();

// ---- Health check ----
let sync = client.get_sync_status().await?;
match sync {
SyncStatus::Synced => println!("[OK] Node is synced"),
SyncStatus::Bootstrap => println!("[WARN] Node is bootstrapping"),
SyncStatus::Catchup => println!("[WARN] Node is catching up"),
SyncStatus::Connecting => println!("[WARN] Node is connecting to network"),
SyncStatus::Listening => println!("[WARN] Node is listening (not yet syncing)"),
SyncStatus::Offline => println!("[ERR] Node is offline"),
}

// ---- Daemon status ----
let status = client.get_daemon_status().await?;
println!("\nDaemon Status:");
println!(" Blockchain length: {:?}", status.blockchain_length);
println!(
" Highest block received: {:?}",
status.highest_block_length_received
);
if let Some(secs) = status.uptime_secs {
println!(" Uptime: {}h {}m", secs / 3600, (secs % 3600) / 60);
}
if let Some(hash) = &status.state_hash {
println!(" State hash: {}", &hash[..20.min(hash.len())]);
}

// ---- Network info ----
let network_id = client.get_network_id().await?;
println!("\nNetwork: {network_id}");

// ---- Peers ----
let peers = client.get_peers().await?;
println!("\nConnected peers: {}", peers.len());
for (i, peer) in peers.iter().enumerate().take(5) {
println!(
" {}: {}:{} ({})",
i + 1,
peer.host,
peer.port,
peer.peer_id
);
}
if peers.len() > 5 {
println!(" ... and {} more", peers.len() - 5);
}

// ---- Recent blocks ----
let blocks = client.get_best_chain(Some(5)).await?;
println!("\nRecent blocks:");
for block in &blocks {
println!(
" Height {} | {} txns | {}",
block.height,
block.command_transaction_count,
&block.state_hash[..16],
);
}

Ok(())
}
Loading
Loading