Crypto billing system, Monitoring transactions and derrive addresses in Rust for Bitcoin, Ethereum, Solana and Polkadot

December 2, 2022

Intro

Great weekend for great research followup. This article is about how to implement billing system different crypto currencies using Rust pogramming language. Before getting deeper into the details of implementation o would like to define what is standing under the term “billing system” for crypto currencies.

Crypto Billing system is a software that us used to organize crypto payments in your organization. It is designed to create derevative addresses (one time payment addresses) and monitor incoming transactions executing business logic for your organization. Typically the scenario could be the following:

  • Consumer would like to pay for products in your organization and receive one time address for payment in bitcoin/ethereum/solana/polkadot
  • Address derrived from the organization account, address derivivation described in bitcoin BIP-32 and BIP-44 proposals (follow links to understand this concepts better).
  • Organization knows the account ID for derrived address and for can generate private key for this address (this is needed later)
  • Consumer send specified amount of tokens to speficied derrived address
  • System shoudl be able to monitor incoming transactions, if transaction identified in specified address we can start executing business logic for this address
  • Typical business logic could be transfering of tokens from specified address to exchange address (lets say Binance address). This will allow organization to convert tokens for fiat currencies like USD or EUR.
  • Another business logic example could be transfer of tokens back consumer address in other blockchain netwok. This way you can build automatic crypto excnage.
  • you can code any business logic you wish

implementation

So the idea looks promising ! How to achive this ? Lets start from architecture diagram of components that we will need.

Crypto Billing principal schema

  1. Blockchaon Nodes - are network stakeholders and their devices authorized to keep track of the distributed ledger and serve as communication hubs for various network tasks. Typically all the blockchain nodes provide RPC API for clients. Nodes can be different types depending on use case - regular nodes and archive nodes. Archive node contains full copy of blockchain network strating from genesis block till now as the result such nodes requires hundreds of gigabytes to store the network data - typical use dase is to provide history of all the operations aka blockchain explorer. Regular nodes do not require to store the full blockchain information only to a short period of time to provide - typical use case is to cetch up what is happening now on the blokcchain.
  2. RPC API - provide interface to the blockchain to work with blocks, transactions, wallets and network.
  3. Billing database - provides persistent storage for billing business logic (transactinos, settlements)
  4. Market data - provides information on blockchain assets prices

So far we have all the components needed - now lets rock.

Bitcoin

I will share example of how we can use Bitcoin protocol in Rust programming language and implement monitoring of blocks and transactions. As well i will explain hot to generate derivate adresses for one time use. Full source code is available at https://github.com/VadzimBelski-ScienceSoft/rust-bitcoin-monitor/

#[tokio::main]
fn main() {
    env_logger::init();

    let ticker = Ticker::new(0.., Duration::from_secs(5));

    let rpc = bitcoincore_rpc::Client::new(
        "http://10.60.9.67:10003",
        bitcoincore_rpc::Auth::UserPass("user".to_string(), "user".to_string()),
    )
    .unwrap();

    let block_count = rpc.get_block_count().unwrap();
    println!("block count: {}", block_count);

    let mut next_block = rpc.get_block_hash(block_count).unwrap();
    let mut latest_scanned_block: String = "".to_string();

    for _ in ticker {
        println!("We are on the block {}", next_block);

        let block = rpc.get_block_info(&next_block).unwrap();

        if latest_scanned_block != block.hash.to_string() {
            for tx in &block.tx {
                scan_transaction(&tx, &rpc);
                latest_scanned_block = block.hash.to_string();
            }
        }
        // Lets got to next transaction only after 2 confirmations ?
        if block.nextblockhash != None && block.confirmations >= 2 {
            println!("{}", serde_json::to_string_pretty(&block).unwrap());
            next_block = block.nextblockhash.unwrap();
        } else {
            println!("No more blocks");
        }
    }
}

Before we will start working with addrssess we will need to generate seed.


fn generates_wallet_seed(passphrase: &str) -> [u8; 64] {
    // Generates an English mnemonic with 12 words randomly
    let mnemonic = Mnemonic::generate(Count::Words12);
    // Gets the phrase
    let _phrase = mnemonic.phrase();
    println!("Phrase generated: {}", _phrase);

    // Generates the HD wallet seed from the mnemonic and the passphrase.
    return mnemonic.to_seed(passphrase);
}

fn generate_segwit_address(seed: [u8; 64]) -> Address {
    println!("--------------generate_segwit_address()------------");

    let network = bitcoin::Network::Bitcoin;

    // we need secp256k1 context for key derivation
    let mut buf: Vec<AlignedType> = Vec::new();
    buf.resize(Secp256k1::preallocate_size(), AlignedType::zeroed());
    let secp = Secp256k1::preallocated_new(buf.as_mut_slice()).unwrap();

    // calculate root key from seed
    let root = ExtendedPrivKey::new_master(network, &seed).unwrap();
    println!("Root key: {}", root);

    // derive child xpub
    let path = DerivationPath::from_str("m/84h/0h/0h").unwrap();
    let child = root.derive_priv(&secp, &path).unwrap();
    println!("Child at {}: {}", path, child);

    let xpub = ExtendedPubKey::from_priv(&secp, &child);
    println!("Public key at {}: {}", path, xpub);

    // generate first receiving address at m/0/0
    // manually creating indexes this time
    let zero = ChildNumber::from_normal_idx(0).unwrap();
    let public_key = xpub
        .derive_pub(&secp, &vec![zero, zero])
        .unwrap()
        .public_key;
    let address = Address::p2wpkh(&PublicKey::new(public_key), network).unwrap();
    println!("First receiving address: {}", address);

    return address;
}

Ethereum

Ethereum provides the same flexibility as bitcoin. Spample Rust code available https://github.com/VadzimBelski-ScienceSoft/rust-ethereum-monitor . Comparing to Bitcoint ethereum rhas different crypto implementations for address generation but fortunately it can rely on the existing bitcoin libraries to manage addresses.


async fn main() -> web3::Result {

    generate_keypair();
    // Sign up at infura > choose the desired network (eg Rinkeby) > copy the endpoint url into the below
    // If you need test ether use a faucet, eg https://faucet.rinkeby.io/
    let transport = web3::transports::Http::new("https://ropsten.infura.io/v3/f1a6a5d57420473b975975c55f5d3666")?;
    let web3http = web3::Web3::new(transport);

    // let ws = web3::transports::WebSocket::new("wss://ropsten.infura.io/ws/v3/f1a6a5d57420473b975975c55f5d3666").await?;
    // let web3 = web3::Web3::new(ws.clone());

    // let mut sub = web3.eth_subscribe().subscribe_new_heads().await?;

    // println!("Got subscription id: {:?}", sub.id());

    let current_block_number = web3.eth().block_number().await;

    let mut next_block_number = current_block_number.unwrap();
    let mut latest_scanned_block = web3::types::U64::from(0);

    let ticker = Ticker::new(0.., Duration::from_secs(5));

    for _ in ticker {
 
        println!("We are on the block {:?}", next_block_number);

        let block = web3.eth().block(BlockId::from(next_block_number)).await.unwrap();

        println!("Block details are {:?}", block);

        if block != None {

            let block_object = block.as_ref().unwrap();
        
            if latest_scanned_block != block_object.number.unwrap() {
                for tx in &block_object.transactions {
                    scan_transaction(*tx, &web3http).await;
                    println!("Transaction {:?}", tx);
                    latest_scanned_block = block_object.number.unwrap();
                }
            }

            let increment = 1 as u64;
            next_block_number = block_object.number.unwrap() + increment;
            println!("Setting next block as {:?}", next_block_number);
            

        }else{
            println!("No more blocks");
        }            
    
    }


    Ok(())
}

async fn scan_transaction( tx: web3::types::H256, web3http: &web3::Web3<web3::transports::Http>){

    let tr = web3http.eth().transaction(TransactionId::Hash(tx)).await.unwrap();

    let transaction = tr.as_ref().unwrap();

    println!("Addres From: {:?}", transaction.from);
    println!("Addres to: {:?}", transaction.to);
}

fn generate_keypair() {
    
    let network = bitcoin::Network::Bitcoin;

    // Generates an English mnemonic with 12 words randomly
    //let mnemonic = Mnemonic::generate(Count::Words12);
    let mnemonic = Mnemonic::from_phrase("jealous picnic lazy lend basic kangaroo debate inspire select brisk neither license").unwrap();
    // Gets the phrase
    let _phrase = mnemonic.phrase();

    println!("Phrase generated: {}", _phrase);

    // Generates the HD wallet seed from the mnemonic and the passphrase.
    let seed = mnemonic.to_seed("");

    // we need secp256k1 context for key derivation
    let mut buf: Vec<AlignedType> = Vec::new();
    buf.resize(Secp256k1::preallocate_size(), AlignedType::zeroed());
    let secp = Secp256k1::preallocated_new(buf.as_mut_slice()).unwrap();

    // calculate root key from seed
    let root = ExtendedPrivKey::new_master(network, &seed).unwrap();
    println!("Root key: {}", root);

    println!("Root hex: {}", hex::encode(root.to_priv().to_bytes()));

    // derive child xpub
    let path = DerivationPath::from_str("m/44'/60'/0'/0/0").unwrap();
    let child = root.derive_priv(&secp, &path).unwrap();
    println!("Child at {}: {}", path, child);

    println!("Child private hex: {}", hex::encode(child.to_priv().to_bytes()));


    let xpub = ExtendedPubKey::from_priv(&secp, &child);
    println!("Public key at {}: {}", path, xpub);


    let public_key = xpub.public_key;

    let public_key = public_key.serialize_uncompressed();
    let hash = keccak256(&public_key[1..]);

    let address = Address::from_slice(&hash[12..]);

    println!("address: {:?}", &address);
}

Polkadot

Polkadot (aka Kusama) blockchain looks like super different from all the others. My oppinion it has very small documentation and specifically examples you can use. So it was some challange to replicate the same functionalyty for polkadot but i did it :) See the repo and sample code for polkadot Rust language https://github.com/VadzimBelski-ScienceSoft/rust-polkadot-monitor


#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {

    tracing_subscriber::fmt::init();

    let api = OnlineClient::<PolkadotConfig>::new().await.unwrap();

    if let Err(_e) = node_runtime::validate_codegen(&api) {
        println!(r#"Generated code is not up to date with node we're connected to"#);
    }

    println!("Everything working good!");

    let ticker = Ticker::new(0.., Duration::from_secs(1));

    let latest_block_hash = api.rpc().block_hash(None).await.unwrap();
    let latest_block = api.rpc().block(latest_block_hash).await.unwrap();

    let mut block_number = latest_block.as_ref().unwrap().block.header.number;

    for _ in ticker {
        

        let block_hash = api.rpc().block_hash(Some(block_number.into())).await?;

        if block_hash != None {

            println!(
                r#"Block hash from number: {} block_hash: {:?}"#,
                block_number, block_hash
            );
    
            let events = api.events().at(block_hash.into()).await?;
    
            /*
            We can dynamically decode events:
            */
            println!("Dynamic event details: {block_hash:?}:");

            for event in events.iter() {
                let event = event?;
                let is_balance_transfer = event
                    .as_event::<node_runtime::balances::events::Transfer>()?
                    .is_some();
                let pallet = event.pallet_name();
                let variant = event.variant_name();
                
                println!("{pallet}::{variant} (is balance transfer? {is_balance_transfer})");
            }
    
            // Or we can find the first transfer event, ignoring any others:
            let transfer_event = events.find_first::<node_runtime::balances::events::Transfer>()?;
    
            if let Some(ev) = transfer_event {
                println!("  - Balance transfer success: value: {:?}", ev.amount);

                // Lets do Scan !

            } else {
                println!("  - No balance transfer event found in this block");
            }
    
            // after scan lets increment block
            block_number += 1;

        } else {
            println!("No more blocks");
        }
    }

    Ok(())
}

/*
If you would like to create and manage several accounts on the network using the same seed, you can use derivation paths. 
We can think of the derived accounts as child accounts of the root account created using the original mnemonic seed phrase. 
Many Polkadot key generation tools support hard and soft derivation. 
For instance, if you intend to create an account to be used on the Polkadot chain, you can derive a hard key child account using // after the mnemonic phrase.
'caution juice atom organ advance problem want pledge someone senior holiday very//0'
and a soft key child account using / after the mnemonic phrase
'caution juice atom organ advance problem want pledge someone senior holiday very/0'
If you would like to create another account for using the Polkadot chain using the same seed, you can change the number at the end of the string above. 
For example, /1, /2, and /3 will create different derived accounts.
There is an additional type of derivation called password derivation. On Polkadot you can derive a password key account using /// after the mnemonic phrase
'caution juice atom organ advance problem want pledge someone senior holiday very///0'
In this type of derivation, if the mnemonic phrase would leak, accounts cannot be derived without the initial password. 
In fact, for soft- and hard-derived accounts, if someone knows the mnemonic phrase and the derivation path, they will have access to your account.
*/

fn get_address() {

    // https://github.com/paritytech/substrate/blob/0ba251c9388452c879bfcca425ada66f1f9bc802/client/cli/src/commands/generate.rs

    let words = MnemonicType::Words12;
    let mnemonic = Mnemonic::new(words, Language::English);

    let derevative_address = format!("{}/{}", mnemonic.to_string(), "1");

    let pair1 = sr25519::Pair::from_string(&derevative_address, None).unwrap();

    println!("Public key 1 sr25519: {}", pair1.public());

    let derevative_address = format!("{}/{}", mnemonic.to_string(), "2");

    let pair2 = sr25519::Pair::from_string(&derevative_address, None).unwrap();

    println!("Public key 2 sr25519: {}", pair2.public());


    let pair3 = ed25519::Pair::from_string(&mnemonic.to_string(), None).unwrap();

    println!("Public key ed25519: {}", pair3.public());

}

Solana

Solana on the other hand is more cleat and has lots of snippets you can use. Plese see my repository https://github.com/VadzimBelski-ScienceSoft/rust-solana-monitor.


fn main() {
    
    get_address();

    let ticker = Ticker::new(0.., Duration::from_secs(5));

    let rpc_client = RpcClient::new("https://api.devnet.solana.com");

    let latest_block = rpc_client.get_latest_blockhash().unwrap();
    println!("block count: {}", latest_block);

    let epoch_info = rpc_client.get_epoch_info().unwrap();

    let absolute_slot = epoch_info.absolute_slot;

    let mut next_block = latest_block;
    let slot = rpc_client.get_slot().unwrap();
    let mut latest_scanned_block: String = "".to_string();

    for _ in ticker {
        println!("We are on the block {}", next_block);

        let block = rpc_client.get_block(slot).unwrap();

        if latest_scanned_block != block.blockhash {
            for tx in &block.transactions {
                // scan_transaction(&tx, &rpc);
                println!("Transaction {:?}", tx);
            }
        }
    }

}

fn get_address()  {

    let words = MnemonicType::Words12;
    let mnemonic = Mnemonic::new(words, Language::English);

    let index = 1;
    let path = format!("m/44'/501'/{}/0'", index);

    let derivation_path = DerivationPath::from_key_str(&path);
    let seed = Seed::new(&mnemonic, "");


    let keypair = keypair_from_seed_and_derivation_path(seed.as_bytes(),derivation_path.ok()).unwrap();
    let secret_key_bytes = keypair.secret().to_bytes();

    println!("Private key: {:?}", secret_key_bytes);
    println!("Public key: {}", keypair.pubkey());
}

Conclusion

So now you can create your own billing crypto :) You got the concepts and examples can help you implement your own.