🚧 My Bitcoin Wallet Development in Rust (Testnet)
이관호(Gwanho LEE)

이관호(Gwanho LEE) @_56d7718cea8fe00ec1610

About: Rust enthusiast passionate about blockchain and system programming. Based in Seoul, South Korea. Currently working on secure digital identity solutions.

Location:
seoul, South Korea
Joined:
Apr 4, 2025

🚧 My Bitcoin Wallet Development in Rust (Testnet)

Publish Date: Apr 15
0 0

I started this project to deeply understand how Bitcoin wallets work — not just from the UI side, but from the protocol level. From generating private keys to scanning UTXOs and soon signing transactions, this blog documents the evolution of my Rust-based CLI wallet project and shares what I've learned.


🧱 Project Architecture

rust_cli_wallet/
├── src/
│   ├── main.rs             // Entry point: loads wallet, checks balance
│   ├── address.rs          // BitcoinAddress struct, key generation
│   ├── wallet.rs           // Wallet struct: loading, saving, updating balances
│   ├── utxo.rs             // UTXO querying via Esplora API
│   └── transaction.rs      // (Upcoming) Transaction creation and signing
├── wallet.json             // Stores addresses and balance info persistently
Enter fullscreen mode Exit fullscreen mode

🔄 Development Flow Summary

✅ Phase 1: Key Generation & Address Creation (address.rs)

We generate a keypair using secp256k1 and derive a P2PKH testnet address like this:

let secp = Secp256k1::new();
let mut rng = OsRng;
let (sk, _) = secp.generate_keypair(&mut rng);
let priv_key = PrivateKey::new(
    bitcoin::secp256k1::SecretKey::from_slice(&sk[..]).unwrap(),
    network,
);
let pub_key = priv_key.public_key(&Secp256k1::new());
let address = Address::p2pkh(&pub_key, network);
Enter fullscreen mode Exit fullscreen mode

This forms the core identity of our wallet.


✅ Phase 2: Wallet Loading, Saving, and New Address Creation (wallet.rs)

The wallet tries to load from a wallet.json file. If it’s missing or empty, we generate a new address:

let wallet_path = Path::new("wallet.json");
let mut wallet = Wallet::load(wallet_path)?;

if wallet.addresses.is_empty() {
    let new_address = BitcoinAddress::new(Network::Testnet);
    wallet.add_address(new_address);
    wallet.save(wallet_path)?;
}
Enter fullscreen mode Exit fullscreen mode

This makes the wallet persistent across sessions.


✅ Phase 3: Fetch UTXOs and Balance Updates (wallet.rs + utxo.rs)

Using the Blockstream Esplora API, we scan each address and retrieve the associated UTXOs.

In wallet.rs, the method update_balances() looks something like this:

pub async fn update_balances(&mut self) -> Result<(), Box<dyn std::error::Error>> {
    for (i, address) in self.addresses.iter().enumerate() {
        let utxos = fetch_utxos(&address.to_string()).await?;
        self.balances[i].update_from_utxos(&utxos);
    }
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

It calls the fetch_utxos function in utxo.rs, which makes the actual HTTP request:

pub async fn fetch_utxos(address: &str) -> Result<Vec<Utxo>, Box<dyn std::error::Error>> {
    let url = format!("https://blockstream.info/testnet/api/address/{}/utxo", address);
    let response = reqwest::get(&url).await?.json::<Vec<Utxo>>().await?;
    Ok(response)
}
Enter fullscreen mode Exit fullscreen mode

Each balance is updated by summing UTXO values fetched from the Esplora API. The balance is stored in a Vec<Balance>, and each index in the vector corresponds to the associated address index. This line shows the update logic:

self.balances[i].update_from_utxos(&utxos);
Enter fullscreen mode Exit fullscreen mode

We use async networking via reqwest, and call it from main.rs using:

let rt = tokio::runtime::Runtime::new()?;
rt.block_on(wallet.update_balances())?;
Enter fullscreen mode Exit fullscreen mode

This lets us run async functions from sync main(). (I'll write more about async/await in Rust system programming posts later.)


✅ Phase 4: Displaying Wallet Info (main.rs)

After fetching balances, the wallet prints out all known addresses and their total testnet Bitcoin.

wallet.display_all();
Enter fullscreen mode Exit fullscreen mode

📸 Example Output

🔐 Loaded Wallet:
- Address: tb1qxyz...abc
  Balance: 0.00100500 tBTC

- Address: tb1qabc...xyz
  Balance: 0.00000000 tBTC
Enter fullscreen mode Exit fullscreen mode

🔮 Coming Next

  • Transaction and Signing

Comments 0 total

    Add comment