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:
So the idea looks promising ! How to achive this ? Lets start from architecture diagram of components that we will need.
So far we have all the components needed - now lets rock.
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 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 (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 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());
}
So now you can create your own billing crypto :) You got the concepts and examples can help you implement your own.