Skip to content

Rust Examples

The Dilithia Rust SDK builds request objects (DilithiaRequest::Get / DilithiaRequest::Post) that describe the HTTP call to make. It does not include an HTTP runtime -- you execute the requests with your own HTTP client (reqwest, ureq, hyper, etc.). This keeps the SDK lightweight and compatible with any async runtime or blocking context.

Prerequisites

Add to your Cargo.toml:

[dependencies]
dilithia-sdk-rust = "0.3.0"
serde_json = "1"

Scenario 1: Balance Monitor Bot

A bot that recovers its wallet, checks its balance, and if the balance exceeds a threshold, sends tokens to a destination address via a contract call.

use dilithia_sdk_rust::{
    DilithiaClient, DilithiaCryptoAdapter, DilithiaRequest, NativeCryptoAdapter,
};
use serde_json::json;

const RPC_URL: &str = "https://rpc.dilithia.network/rpc";
const TOKEN_CONTRACT: &str = "dil1_token_main";
const DESTINATION: &str = "dil1_recipient_address";
const THRESHOLD: u64 = 500_000;
const SEND_AMOUNT: u64 = 100_000;

fn main() -> Result<(), String> {
    // 1. Initialize client and crypto adapter
    let client = DilithiaClient::new(RPC_URL, Some(15_000))
        .map_err(|e| e.to_string())?;
    let crypto = NativeCryptoAdapter;

    // 2. Recover wallet from saved mnemonic
    let mnemonic = std::env::var("BOT_MNEMONIC")
        .map_err(|_| "Set BOT_MNEMONIC env var".to_string())?;
    let account = crypto.recover_hd_wallet(&mnemonic)?;
    println!("Bot address: {}", account.address);

    // 3. Check current balance
    let balance_req = client.get_balance_request(&account.address);
    match &balance_req {
        DilithiaRequest::Get { path } => {
            println!("GET {path}");
            // execute with your HTTP client
            // let response = ureq::get(path).call()?.into_string()?;
            // let balance: u64 = parse_balance(&response);
        }
        _ => return Err("unexpected request type".into()),
    }

    // (Assuming we parsed the balance from the response)
    let balance: u64 = 600_000; // placeholder
    println!("Current balance: {balance}");

    if balance < THRESHOLD {
        println!("Balance {balance} below threshold {THRESHOLD}. Nothing to do.");
        return Ok(());
    }

    // 4. Build a contract call to transfer tokens
    let call = client.build_contract_call(
        TOKEN_CONTRACT,
        "transfer",
        json!({ "to": DESTINATION, "amount": SEND_AMOUNT }),
        None,
    );

    // 5. Simulate first to verify it would succeed
    let sim_req = client.simulate_request(call.clone());
    match &sim_req {
        DilithiaRequest::Post { path, body } => {
            println!("POST {path}");
            println!("Body: {body}");
            // execute with your HTTP client
        }
        _ => return Err("unexpected request type".into()),
    }

    // 6. Sign the call payload
    let call_json = call.to_string();
    let sig = crypto.sign_message(&account.secret_key, &call_json)?;
    println!("Signed with algorithm: {}", sig.algorithm);

    // 7. Submit the signed call
    let signed_call = json!({
        "call": call,
        "signature": sig.signature,
        "public_key": account.public_key,
    });
    let send_req = client.send_call_request(signed_call);
    match &send_req {
        DilithiaRequest::Post { path, body } => {
            println!("POST {path}");
            println!("Body: {body}");
            // execute with your HTTP client
            // let tx_hash = parse_tx_hash(&response);
        }
        _ => return Err("unexpected request type".into()),
    }

    // 8. Poll for receipt
    let tx_hash = "placeholder_tx_hash";
    let receipt_req = client.get_receipt_request(tx_hash);
    match &receipt_req {
        DilithiaRequest::Get { path } => {
            println!("GET {path}");
            // execute with your HTTP client, retry until receipt appears
        }
        _ => return Err("unexpected request type".into()),
    }

    println!("Transaction confirmed");
    Ok(())
}

Scenario 2: Multi-Account Treasury Manager

Derives multiple accounts from a single mnemonic, checks each balance, and prints a treasury summary.

use dilithia_sdk_rust::{
    DilithiaClient, DilithiaCryptoAdapter, DilithiaRequest, NativeCryptoAdapter,
};

const RPC_URL: &str = "https://rpc.dilithia.network/rpc";
const NUM_ACCOUNTS: u32 = 5;

fn main() -> Result<(), String> {
    let client = DilithiaClient::new(RPC_URL, Some(10_000))
        .map_err(|e| e.to_string())?;
    let crypto = NativeCryptoAdapter;

    // 1. Recover the root mnemonic
    let mnemonic = std::env::var("TREASURY_MNEMONIC")
        .map_err(|_| "Set TREASURY_MNEMONIC env var".to_string())?;
    crypto.validate_mnemonic(&mnemonic)?;

    // 2. Derive multiple HD accounts and query each balance
    println!("Treasury accounts:");
    println!("{:<8} {:<50} {}", "Index", "Address", "Balance Request");

    for index in 0..NUM_ACCOUNTS {
        let account = crypto.recover_hd_wallet_account(&mnemonic, index)?;
        let balance_req = client.get_balance_request(&account.address);

        match &balance_req {
            DilithiaRequest::Get { path } => {
                println!("{:<8} {:<50} GET {}", index, account.address, path);
                // execute with your HTTP client
                // let balance = fetch_balance(path)?;
            }
            _ => return Err("unexpected request type".into()),
        }
    }

    // 3. Check nonce on the root account for pending transactions
    let root_account = crypto.recover_hd_wallet(&mnemonic)?;
    let nonce_req = client.get_nonce_request(&root_account.address);
    match &nonce_req {
        DilithiaRequest::Get { path } => {
            println!("\nRoot account nonce request: GET {path}");
            // execute with your HTTP client
        }
        _ => return Err("unexpected request type".into()),
    }

    // 4. Get address summary via JSON-RPC for the root account
    let summary_req = client.get_address_summary_request(&root_account.address);
    match &summary_req {
        DilithiaRequest::Post { path, body } => {
            println!("Address summary: POST {path}");
            println!("Body: {body}");
            // execute with your HTTP client
        }
        _ => return Err("unexpected request type".into()),
    }

    println!("\nTreasury report complete.");
    Ok(())
}

Scenario 3: Signature Verification Service

Generates a keypair, signs a message, then verifies the signature -- demonstrating the full cryptographic round-trip.

use dilithia_sdk_rust::{DilithiaCryptoAdapter, NativeCryptoAdapter};

fn main() -> Result<(), String> {
    let crypto = NativeCryptoAdapter;

    // 1. Generate a fresh keypair
    let keypair = crypto.keygen()?;
    println!("Address:    {}", keypair.address);
    println!("Public key: {} ({} hex chars)", &keypair.public_key[..32], keypair.public_key.len());

    // 2. Sign a message
    let message = "Authorize withdrawal of 1000 tokens";
    let sig = crypto.sign_message(&keypair.secret_key, message)?;
    println!("Algorithm:  {}", sig.algorithm);
    println!("Signature:  {}... ({} hex chars)", &sig.signature[..32], sig.signature.len());

    // 3. Verify with the correct public key
    let valid = crypto.verify_message(&keypair.public_key, message, &sig.signature)?;
    println!("Valid signature: {valid}");
    assert!(valid, "signature should be valid");

    // 4. Verify with a tampered message should fail
    let tampered = crypto.verify_message(&keypair.public_key, "tampered message", &sig.signature)?;
    println!("Tampered message valid: {tampered}");
    assert!(!tampered, "tampered message should fail verification");

    // 5. Validate individual components
    crypto.validate_public_key(&keypair.public_key)?;
    crypto.validate_secret_key(&keypair.secret_key)?;
    crypto.validate_signature(&sig.signature)?;
    println!("All key/signature validations passed.");

    // 6. Derive address from public key and confirm it matches
    let derived_addr = crypto.address_from_public_key(&keypair.public_key)?;
    assert_eq!(derived_addr, keypair.address, "derived address must match");
    println!("Address derivation confirmed: {derived_addr}");

    Ok(())
}

Scenario 4: Wallet Backup and Recovery

Creates an encrypted wallet file from a mnemonic, then recovers the account from that file -- demonstrating the backup/restore workflow.

use dilithia_sdk_rust::{DilithiaCryptoAdapter, NativeCryptoAdapter};

fn main() -> Result<(), String> {
    let crypto = NativeCryptoAdapter;

    // 1. Generate a fresh mnemonic
    let mnemonic = crypto.generate_mnemonic()?;
    println!("Generated mnemonic ({} words)", mnemonic.split_whitespace().count());
    crypto.validate_mnemonic(&mnemonic)?;

    // 2. Create an encrypted wallet file (account index 0)
    let password = "strong-passphrase-here";
    let account = crypto.create_hd_wallet_file_from_mnemonic(&mnemonic, password)?;
    println!("Account address: {}", account.address);
    println!("Account index:   {}", account.account_index);

    // The wallet_file field contains the encrypted JSON to persist
    let wallet_json = account.wallet_file
        .as_ref()
        .ok_or("wallet_file should be present")?;
    println!("Wallet file size: {} bytes", wallet_json.to_string().len());

    // 3. Simulate saving and loading the wallet file
    let wallet_file_str = wallet_json.to_string();
    println!("Wallet file saved to disk (simulated).");

    // 4. Create a second account at index 1 from the same mnemonic
    let account_1 = crypto.create_hd_wallet_account_from_mnemonic(&mnemonic, password, 1)?;
    println!("Account 1 address: {}", account_1.address);
    assert_ne!(account.address, account_1.address, "different indices yield different addresses");

    // 5. Recover the original account: verify the address matches
    let recovered = crypto.recover_hd_wallet(&mnemonic)?;
    assert_eq!(recovered.address, account.address, "recovered address must match");
    println!("Recovery verified: addresses match.");

    // 6. Derive a checksummed address
    let checksummed = crypto.address_from_pk_checksummed(&account.public_key)?;
    println!("Checksummed address: {checksummed}");

    // 7. Demonstrate seed-based key derivation
    let seed = crypto.seed_from_mnemonic(&mnemonic)?;
    let child_seed = crypto.derive_child_seed(&seed, 0)?;
    let child_keypair = crypto.keygen_from_seed(&child_seed)?;
    println!("Child keypair address: {}", child_keypair.address);

    println!("\nWallet backup and recovery complete.");
    Ok(())
}

Scenario 5: Gas-Sponsored Meta-Transaction

Uses the DilithiaGasSponsorConnector to check if a user is eligible for gas sponsorship, then builds and submits a sponsored contract call.

use dilithia_sdk_rust::{
    DilithiaClient, DilithiaCryptoAdapter, DilithiaGasSponsorConnector,
    DilithiaRequest, NativeCryptoAdapter,
};
use serde_json::json;

const RPC_URL: &str = "https://rpc.dilithia.network/rpc";
const SPONSOR_CONTRACT: &str = "wasm:gas_sponsor";
const PAYMASTER: &str = "gas_sponsor";
const TARGET_CONTRACT: &str = "wasm:amm";

fn main() -> Result<(), String> {
    let client = DilithiaClient::new(RPC_URL, Some(10_000))
        .map_err(|e| e.to_string())?;
    let crypto = NativeCryptoAdapter;
    let sponsor = DilithiaGasSponsorConnector::new(SPONSOR_CONTRACT, Some(PAYMASTER.to_string()));

    // 1. Recover the user's wallet
    let mnemonic = std::env::var("USER_MNEMONIC")
        .map_err(|_| "Set USER_MNEMONIC env var".to_string())?;
    let account = crypto.recover_hd_wallet(&mnemonic)?;
    println!("User address: {}", account.address);

    // 2. Check remaining gas quota for the user
    let quota_query = sponsor.build_remaining_quota_query(&account.address);
    let quota_req = client.query_contract_request(
        &quota_query["contract"].as_str().unwrap_or(""),
        &quota_query["method"].as_str().unwrap_or(""),
        quota_query["args"].clone(),
    );
    match &quota_req {
        DilithiaRequest::Get { path } => {
            println!("Quota check: GET {path}");
            // execute with your HTTP client
            // let quota: u64 = parse_quota(&response);
        }
        _ => return Err("unexpected request type".into()),
    }

    // 3. Check if the sponsor accepts this call
    let accept_query = sponsor.build_accept_query(&account.address, TARGET_CONTRACT, "swap");
    let accept_req = client.query_contract_request(
        &accept_query["contract"].as_str().unwrap_or(""),
        &accept_query["method"].as_str().unwrap_or(""),
        accept_query["args"].clone(),
    );
    match &accept_req {
        DilithiaRequest::Get { path } => {
            println!("Accept check: GET {path}");
            // execute with your HTTP client
        }
        _ => return Err("unexpected request type".into()),
    }

    // 4. Build the contract call with gas sponsorship
    let call = client.build_contract_call(
        TARGET_CONTRACT,
        "swap",
        json!({ "token_in": "DAI", "token_out": "USDC", "amount": 1000 }),
        Some(PAYMASTER),
    );
    println!("Call with paymaster: {call}");

    // 5. Alternatively, build a call and apply the paymaster after
    let plain_call = client.build_contract_call(
        TARGET_CONTRACT,
        "swap",
        json!({ "token_in": "DAI", "token_out": "USDC", "amount": 1000 }),
        None,
    );
    let sponsored_call = sponsor.apply_paymaster(&client, plain_call);
    println!("Sponsored call: {sponsored_call}");

    // 6. Sign and submit
    let call_json = sponsored_call.to_string();
    let sig = crypto.sign_message(&account.secret_key, &call_json)?;
    let signed = json!({
        "call": sponsored_call,
        "signature": sig.signature,
        "public_key": account.public_key,
    });

    let send_req = client.send_call_request(signed);
    match &send_req {
        DilithiaRequest::Post { path, body } => {
            println!("Submit sponsored tx: POST {path}");
            println!("Body: {body}");
            // execute with your HTTP client
        }
        _ => return Err("unexpected request type".into()),
    }

    println!("Gas-sponsored transaction submitted.");
    Ok(())
}

Scenario 6: Cross-Chain Message Sender

Uses the DilithiaMessagingConnector to send a cross-chain message and build a receive handler for incoming messages.

use dilithia_sdk_rust::{
    DilithiaClient, DilithiaCryptoAdapter, DilithiaMessagingConnector,
    DilithiaRequest, NativeCryptoAdapter,
};
use serde_json::json;

const RPC_URL: &str = "https://rpc.dilithia.network/rpc";
const MESSAGING_CONTRACT: &str = "wasm:messaging";
const PAYMASTER: &str = "gas_sponsor";

fn main() -> Result<(), String> {
    let client = DilithiaClient::new(RPC_URL, Some(10_000))
        .map_err(|e| e.to_string())?;
    let crypto = NativeCryptoAdapter;
    let messaging = DilithiaMessagingConnector::new(
        MESSAGING_CONTRACT,
        Some(PAYMASTER.to_string()),
    );

    // 1. Recover the sender's wallet
    let mnemonic = std::env::var("SENDER_MNEMONIC")
        .map_err(|_| "Set SENDER_MNEMONIC env var".to_string())?;
    let account = crypto.recover_hd_wallet(&mnemonic)?;
    println!("Sender address: {}", account.address);

    // 2. Build an outbound cross-chain message
    let payload = json!({
        "action": "bridge_transfer",
        "token": "USDC",
        "amount": 5000,
        "recipient": "0xRecipientOnEthereum",
    });
    let outbound_call = messaging.build_send_message_call(&client, "ethereum", payload);
    println!("Outbound call: {outbound_call}");

    // 3. Sign and submit the outbound message
    let call_json = outbound_call.to_string();
    let sig = crypto.sign_message(&account.secret_key, &call_json)?;
    let signed_outbound = json!({
        "call": outbound_call,
        "signature": sig.signature,
        "public_key": account.public_key,
    });

    let send_req = client.send_call_request(signed_outbound);
    match &send_req {
        DilithiaRequest::Post { path, body } => {
            println!("Send cross-chain message: POST {path}");
            println!("Body: {body}");
            // execute with your HTTP client
        }
        _ => return Err("unexpected request type".into()),
    }

    // 4. Build a receive handler for an incoming cross-chain message
    let incoming_payload = json!({
        "action": "bridge_confirmation",
        "tx_hash": "0xabc123",
        "status": "confirmed",
    });
    let inbound_call = messaging.build_receive_message_call(
        &client,
        "ethereum",
        "0xBridgeContract",
        incoming_payload,
    );
    println!("Inbound call: {inbound_call}");

    // 5. Simulate the inbound call before submitting
    let sim_req = client.simulate_request(inbound_call.clone());
    match &sim_req {
        DilithiaRequest::Post { path, body } => {
            println!("Simulate inbound: POST {path}");
            println!("Body: {body}");
            // execute with your HTTP client
        }
        _ => return Err("unexpected request type".into()),
    }

    // 6. Submit the inbound call
    let inbound_json = inbound_call.to_string();
    let inbound_sig = crypto.sign_message(&account.secret_key, &inbound_json)?;
    let signed_inbound = json!({
        "call": inbound_call,
        "signature": inbound_sig.signature,
        "public_key": account.public_key,
    });

    let submit_req = client.send_call_request(signed_inbound);
    match &submit_req {
        DilithiaRequest::Post { path, body } => {
            println!("Submit inbound: POST {path}");
            println!("Body: {body}");
            // execute with your HTTP client
        }
        _ => return Err("unexpected request type".into()),
    }

    println!("Cross-chain messaging complete.");
    Ok(())
}

Scenario 7: Contract Deployment

Deploy a WASM smart contract to the Dilithia chain. Reads the WASM binary, builds and signs a canonical deploy payload (the Rust SDK hashes the bytecode internally), assembles the full DeployPayload, and sends the deploy request.

use dilithia_sdk_rust::{
    read_wasm_file_hex, DilithiaClient, DilithiaCryptoAdapter, DeployPayload,
    DilithiaRequest, NativeCryptoAdapter,
};
use std::path::Path;

const RPC_URL: &str = "https://rpc.dilithia.network/rpc";
const CONTRACT_NAME: &str = "my_contract";
const WASM_PATH: &str = "./my_contract.wasm";
const CHAIN_ID: &str = "dilithia-mainnet";

fn main() -> Result<(), String> {
    // 1. Initialize client and crypto adapter
    let client = DilithiaClient::new(RPC_URL, Some(30_000))
        .map_err(|e| e.to_string())?;
    let crypto = NativeCryptoAdapter;

    // 2. Recover wallet from mnemonic
    let mnemonic = std::env::var("DEPLOYER_MNEMONIC")
        .map_err(|_| "Set DEPLOYER_MNEMONIC env var".to_string())?;
    let account = crypto.recover_hd_wallet(&mnemonic)?;
    println!("Deployer address: {}", account.address);

    // 3. Read the WASM file as hex
    let bytecode_hex = read_wasm_file_hex(Path::new(WASM_PATH))?;
    println!("Bytecode size: {} bytes", bytecode_hex.len() / 2);

    // 4. Get the current nonce from the node
    let nonce_req = client.get_nonce_request(&account.address);
    match &nonce_req {
        DilithiaRequest::Get { path } => {
            println!("Nonce request: GET {path}");
            // execute with your HTTP client
            // let nonce: u64 = parse_nonce(&response);
        }
        _ => return Err("unexpected request type".into()),
    }
    let nonce: u64 = 0; // placeholder -- parse from HTTP response

    // 5. Build the canonical deploy payload
    //    (Rust SDK hashes the bytecode_hex internally)
    let canonical = DilithiaClient::build_deploy_canonical_payload(
        &account.address,
        CONTRACT_NAME,
        &bytecode_hex,
        nonce,
        CHAIN_ID,
    );
    println!("Canonical payload: {canonical}");

    // 6. Sign the canonical payload
    let canonical_json = canonical.to_string();
    let sig = crypto.sign_message(&account.secret_key, &canonical_json)?;
    println!("Signed with algorithm: {}", sig.algorithm);

    // 7. Assemble the full DeployPayload
    let deploy_payload = DeployPayload {
        name: CONTRACT_NAME.to_string(),
        bytecode: bytecode_hex,
        from: account.address.clone(),
        alg: sig.algorithm,
        pk: account.public_key.clone(),
        sig: sig.signature,
        nonce,
        chain_id: CHAIN_ID.to_string(),
        version: 1,
    };

    // 8. Send the deploy request
    let deploy_req = client.deploy_contract_request(&deploy_payload);
    match &deploy_req {
        DilithiaRequest::Post { path, body } => {
            println!("Deploy request: POST {path}");
            println!("Body keys: name, bytecode, from, alg, pk, sig, nonce, chain_id");
            // execute with your HTTP client:
            // let response = ureq::post(path)
            //     .set("Content-Type", "application/json")
            //     .send_string(&body.to_string())?;
            // let tx_hash = parse_tx_hash(&response.into_string()?);
        }
        _ => return Err("unexpected request type".into()),
    }

    // 9. Poll for receipt
    let tx_hash = "placeholder_tx_hash"; // parse from deploy response
    let receipt_req = client.get_receipt_request(tx_hash);
    match &receipt_req {
        DilithiaRequest::Get { path } => {
            println!("Receipt request: GET {path}");
            // execute with your HTTP client, retry until receipt appears
        }
        _ => return Err("unexpected request type".into()),
    }

    println!("Contract deployed successfully.");
    Ok(())
}

Scenario 8: Shielded Pool Deposit & Withdraw

Performs a full shielded deposit-and-withdraw cycle: computes a ZK commitment and nullifier, generates a preimage proof, deposits into the shielded pool, then withdraws using the nullifier -- all with idiomatic Rust error handling.

use dilithia_sdk_rust::{
    DilithiaClient, DilithiaCryptoAdapter, DilithiaZkAdapter,
    NativeCryptoAdapter, NativeZkAdapter,
};

const RPC_URL: &str = "https://rpc.dilithia.network/rpc";
const DEPOSIT_VALUE: u64 = 50_000;
const WITHDRAW_AMOUNT: u64 = 50_000;
const RECIPIENT: &str = "dil1_recipient_address";

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Initialize client, crypto, and ZK adapters
    let client = DilithiaClient::new(RPC_URL, None)?;
    let crypto = NativeCryptoAdapter;
    let zk = NativeZkAdapter;

    // 2. Recover the depositor's wallet
    let mnemonic = std::env::var("DEPOSITOR_MNEMONIC")
        .map_err(|_| "Set DEPOSITOR_MNEMONIC env var")?;
    let account = crypto.recover_hd_wallet(&mnemonic)?;
    println!("Depositor address: {}", account.address);

    // 3. Generate secret and nonce, then compute commitment and nullifier
    let secret = crypto.hash_hex(b"my_deposit_secret")?;
    let nonce = crypto.hash_hex(b"unique_nonce_value")?;

    let commitment_hash = zk.compute_commitment(DEPOSIT_VALUE, &secret, &nonce)?;
    let nullifier_hash = zk.compute_nullifier(&secret, &nonce)?;
    println!("Commitment: {commitment_hash}");
    println!("Nullifier:  {nullifier_hash}");

    // 4. Generate a preimage proof for the deposit
    let proof = zk.generate_preimage_proof(&[
        DEPOSIT_VALUE.to_string(),
        secret.clone(),
        nonce.clone(),
    ])?;
    println!("Proof generated ({} bytes)", proof.proof.len());

    // 5. Deposit into the shielded pool
    let deposit_tx = client.shielded_deposit(
        &commitment_hash,
        DEPOSIT_VALUE,
        &proof.proof,
    )?;
    println!("Deposit tx: {deposit_tx}");

    // 6. Wait for the deposit receipt
    let deposit_receipt = client.wait_for_receipt(&deposit_tx, 20, 2000)?;
    println!("Deposit confirmed: {deposit_receipt}");

    // 7. Verify the commitment is in the tree
    let commitment_root = client.get_commitment_root()?;
    println!("Commitment root: {commitment_root}");

    // 8. Ensure the nullifier has not been spent yet
    let already_spent = client.is_nullifier_spent(&nullifier_hash)?;
    assert!(!already_spent, "nullifier should not be spent before withdrawal");
    println!("Nullifier unspent -- ready to withdraw");

    // 9. Withdraw from the shielded pool
    let withdraw_tx = client.shielded_withdraw(
        &nullifier_hash,
        WITHDRAW_AMOUNT,
        RECIPIENT,
        &proof.proof,
        &commitment_root,
    )?;
    println!("Withdraw tx: {withdraw_tx}");

    // 10. Wait for the withdrawal receipt
    let withdraw_receipt = client.wait_for_receipt(&withdraw_tx, 20, 2000)?;
    println!("Withdrawal confirmed: {withdraw_receipt}");

    // 11. Confirm the nullifier is now spent
    let spent = client.is_nullifier_spent(&nullifier_hash)?;
    assert!(spent, "nullifier should be spent after withdrawal");
    println!("Nullifier spent -- double-spend protection verified");

    println!("\nShielded deposit and withdraw complete.");
    Ok(())
}

Scenario 9: ZK Proof Generation & Verification

Standalone zero-knowledge operations: Poseidon hashing, commitment/nullifier derivation, preimage proof round-trip, and range proof round-trip -- no on-chain interaction required.

use dilithia_sdk_rust::{
    DilithiaCryptoAdapter, DilithiaZkAdapter,
    NativeCryptoAdapter, NativeZkAdapter,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let crypto = NativeCryptoAdapter;
    let zk = NativeZkAdapter;

    // 1. Poseidon hash of field elements
    let hash = zk.poseidon_hash(&[42, 1337, 99])?;
    println!("Poseidon hash: {hash}");

    // 2. Compute a commitment and its corresponding nullifier
    let secret = crypto.hash_hex(b"zk_secret_material")?;
    let nonce = crypto.hash_hex(b"zk_nonce_material")?;
    let value: u64 = 10_000;

    let commitment = zk.compute_commitment(value, &secret, &nonce)?;
    let nullifier = zk.compute_nullifier(&secret, &nonce)?;
    println!("Commitment: {commitment}");
    println!("Nullifier:  {nullifier}");

    // 3. Generate a preimage proof and verify it
    let preimage_proof = zk.generate_preimage_proof(&[
        value.to_string(),
        secret.clone(),
        nonce.clone(),
    ])?;
    println!(
        "Preimage proof: {} bytes, vk: {} bytes",
        preimage_proof.proof.len(),
        preimage_proof.vk.len()
    );

    let preimage_valid = zk.verify_preimage_proof(
        &preimage_proof.proof,
        &preimage_proof.vk,
        &preimage_proof.public_inputs,
    )?;
    assert!(preimage_valid, "preimage proof should verify");
    println!("Preimage proof valid: {preimage_valid}");

    // 4. Tamper with inputs and confirm verification fails
    let mut bad_inputs = preimage_proof.public_inputs.clone();
    bad_inputs.push("extra_field".to_string());
    let tampered_valid = zk.verify_preimage_proof(
        &preimage_proof.proof,
        &preimage_proof.vk,
        &bad_inputs,
    )?;
    assert!(!tampered_valid, "tampered inputs should fail verification");
    println!("Tampered preimage valid: {tampered_valid}");

    // 5. Generate a range proof (prove value is within [min, max])
    let min = 1_000u64;
    let max = 100_000u64;
    let range_proof = zk.generate_range_proof(value, min, max)?;
    println!(
        "Range proof: {} bytes, vk: {} bytes",
        range_proof.proof.len(),
        range_proof.vk.len()
    );

    let range_valid = zk.verify_range_proof(
        &range_proof.proof,
        &range_proof.vk,
        &range_proof.public_inputs,
    )?;
    assert!(range_valid, "range proof should verify");
    println!("Range proof valid: {range_valid}");

    // 6. Generate a range proof that should fail (value outside range)
    let out_of_range = zk.generate_range_proof(200_000, min, max);
    match out_of_range {
        Err(e) => println!("Expected range error for out-of-bounds value: {e}"),
        Ok(bad_proof) => {
            let bad_valid = zk.verify_range_proof(
                &bad_proof.proof,
                &bad_proof.vk,
                &bad_proof.public_inputs,
            )?;
            assert!(!bad_valid, "out-of-range proof should not verify");
            println!("Out-of-range proof valid: {bad_valid}");
        }
    }

    println!("\nZK proof generation and verification complete.");
    Ok(())
}

Scenario 10: Name Service & Identity Profile

A utility that registers a .dili name, configures profile records, resolves names, and demonstrates the full name service lifecycle. Covers: getRegistrationCost, isNameAvailable, registerName, setNameTarget, setNameRecord, getNameRecords, lookupName, resolveName, reverseResolveName, getNamesByOwner, renewName, transferName, releaseName.

use dilithia_sdk_rust::{
    DilithiaClient, DilithiaCryptoAdapter, NativeCryptoAdapter,
};

const RPC_URL: &str = "https://rpc.dilithia.network/rpc";
const NAME: &str = "alice";
const TRANSFER_TO: &str = "dil1_bob_address";

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Initialize client and crypto adapter
    let client = DilithiaClient::new(RPC_URL, None)?;
    let crypto = NativeCryptoAdapter;

    // 2. Recover wallet from mnemonic
    let mnemonic = std::env::var("WALLET_MNEMONIC")
        .map_err(|_| "Set WALLET_MNEMONIC env var")?;
    let account = crypto.recover_hd_wallet(&mnemonic)?;
    println!("Address: {}", account.address);

    // 3. Query registration cost for the name
    let cost = client.get_registration_cost(NAME)?.execute_json()?;
    println!("Registration cost for \"{NAME}\": {cost}");

    // 4. Check if name is available
    let available: bool = client.is_name_available(NAME)?.execute_json()?;
    println!("Name \"{NAME}\" available: {available}");
    if !available {
        return Err(format!("Name \"{NAME}\" is already taken").into());
    }

    // 5. Register name
    let reg_req = client.register_name(NAME, &account.address, &account.secret_key)?;
    let reg_hash: String = reg_req.execute_json()?;
    let reg_receipt = client.wait_for_receipt(&reg_hash, 20, 2_000)?.execute_json()?;
    println!("Name registered in block {}", reg_receipt.block_height);

    // 6. Set target address
    let target_req = client.set_name_target(NAME, &account.address, &account.secret_key)?;
    let target_hash: String = target_req.execute_json()?;
    client.wait_for_receipt(&target_hash, 20, 2_000)?.execute_json()?;
    println!("Target address set to {}", account.address);

    // 7. Set profile records
    let records = [
        ("display_name", "Alice"),
        ("avatar", "https://example.com/alice.png"),
        ("bio", "Builder on Dilithia"),
        ("email", "alice@example.com"),
        ("website", "https://alice.dev"),
    ];
    for (key, value) in records {
        let req = client.set_name_record(NAME, key, value, &account.secret_key)?;
        let hash: String = req.execute_json()?;
        client.wait_for_receipt(&hash, 20, 2_000)?.execute_json()?;
        println!("  Set record \"{key}\" = \"{value}\"");
    }

    // 8. Get all records
    let all_records = client.get_name_records(NAME)?.execute_json::<serde_json::Value>()?;
    println!("All records: {all_records}");

    // 9. Resolve name to address
    let resolved: String = client.resolve_name(NAME)?.execute_json()?;
    println!("resolve_name(\"{NAME}\") -> {resolved}");

    // 10. Reverse resolve address to name
    let reverse_name: String = client.reverse_resolve_name(&account.address)?.execute_json()?;
    println!("reverse_resolve_name(\"{}\") -> {reverse_name}", account.address);

    // 11. List all names by owner
    let owned: Vec<String> = client.get_names_by_owner(&account.address)?.execute_json()?;
    println!("Names owned by {}: {:?}", account.address, owned);

    // 12. Renew name
    let renew_req = client.renew_name(NAME, &account.secret_key)?;
    let renew_hash: String = renew_req.execute_json()?;
    let renew_receipt = client.wait_for_receipt(&renew_hash, 20, 2_000)?.execute_json()?;
    println!("Name renewed in block {}", renew_receipt.block_height);

    // 13. Transfer name to another address
    let transfer_req = client.transfer_name(NAME, TRANSFER_TO, &account.secret_key)?;
    let transfer_hash: String = transfer_req.execute_json()?;
    let transfer_receipt = client.wait_for_receipt(&transfer_hash, 20, 2_000)?.execute_json()?;
    println!("Name transferred to {TRANSFER_TO} in block {}", transfer_receipt.block_height);

    Ok(())
}

Scenario 11: Credential Issuance & Verification

An issuer creates a KYC credential schema, issues a credential to a holder, and a verifier checks it with selective disclosure. Covers: registerSchema, issueCredential, getSchema, getCredential, listCredentialsByHolder, listCredentialsByIssuer, verifyCredential, revokeCredential.

use dilithia_sdk_rust::{
    DilithiaClient, DilithiaCryptoAdapter, NativeCryptoAdapter,
};
use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};

const RPC_URL: &str = "https://rpc.dilithia.network/rpc";
const HOLDER_ADDRESS: &str = "dil1_holder_address";

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Initialize client and crypto adapter
    let client = DilithiaClient::new(RPC_URL, None)?;
    let crypto = NativeCryptoAdapter;

    // 2. Recover issuer wallet
    let mnemonic = std::env::var("ISSUER_MNEMONIC")
        .map_err(|_| "Set ISSUER_MNEMONIC env var")?;
    let issuer = crypto.recover_hd_wallet(&mnemonic)?;
    println!("Issuer address: {}", issuer.address);

    // 3. Register a KYC schema
    let schema = json!({
        "name": "KYC_Basic_v1",
        "attributes": [
            { "name": "full_name", "type": "string" },
            { "name": "country", "type": "string" },
            { "name": "age", "type": "u64" },
            { "name": "verified", "type": "bool" },
        ],
    });
    let schema_req = client.register_schema(&schema, &issuer.secret_key)?;
    let schema_hash: String = schema_req.execute_json()?;
    let schema_receipt = client.wait_for_receipt(&schema_hash, 20, 2_000)?.execute_json()?;
    let schema_hash = schema_receipt.logs[0]["schema_hash"]
        .as_str()
        .ok_or("missing schema_hash in receipt")?;
    println!("Schema registered: {schema_hash}");

    // 4. Issue credential to holder with commitment hash
    let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
    let commitment_input = format!("{HOLDER_ADDRESS}:KYC_Basic_v1:{now}");
    let commitment_hash = crypto.hash_hex(commitment_input.as_bytes())?;

    let issue_req = client.issue_credential(
        schema_hash,
        HOLDER_ADDRESS,
        &commitment_hash,
        &json!({ "full_name": "Alice Smith", "country": "CH", "age": 30, "verified": true }),
        &issuer.secret_key,
    )?;
    let issue_hash: String = issue_req.execute_json()?;
    let issue_receipt = client.wait_for_receipt(&issue_hash, 20, 2_000)?.execute_json()?;
    println!("Credential issued in block {}", issue_receipt.block_height);

    // 5. Get schema by hash
    let fetched_schema = client.get_schema(schema_hash)?.execute_json::<serde_json::Value>()?;
    println!("Schema: {fetched_schema}");

    // 6. Get credential by commitment
    let credential = client.get_credential(&commitment_hash)?.execute_json::<serde_json::Value>()?;
    println!("Credential: {credential}");

    // 7. List credentials by holder
    let holder_creds: Vec<serde_json::Value> = client
        .list_credentials_by_holder(HOLDER_ADDRESS)?
        .execute_json()?;
    println!("Holder has {} credential(s)", holder_creds.len());

    // 8. List credentials by issuer
    let issuer_creds: Vec<serde_json::Value> = client
        .list_credentials_by_issuer(&issuer.address)?
        .execute_json()?;
    println!("Issuer has {} credential(s)", issuer_creds.len());

    // 9. Verify selective disclosure — prove age > 18 without revealing exact age
    let proof = crypto.generate_selective_disclosure_proof(
        &commitment_hash,
        &["age"],
        &json!({ "age": { "operator": "gt", "threshold": 18 } }),
    )?;
    let verified: bool = client
        .verify_credential(&commitment_hash, &proof)?
        .execute_json()?;
    println!("Selective disclosure (age > 18) verified: {verified}");

    // 10. Revoke the credential
    let revoke_req = client.revoke_credential(&commitment_hash, &issuer.secret_key)?;
    let revoke_hash: String = revoke_req.execute_json()?;
    let revoke_receipt = client.wait_for_receipt(&revoke_hash, 20, 2_000)?.execute_json()?;
    println!("Credential revoked in block {}", revoke_receipt.block_height);

    // 11. Verify revocation by fetching credential again
    let revoked = client.get_credential(&commitment_hash)?.execute_json::<serde_json::Value>()?;
    println!("Credential status after revocation: {}", revoked["status"]);

    Ok(())
}