Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test improvements #8

Merged
merged 13 commits into from
Jul 4, 2024
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
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [0.0.1] - 2024-07-04

### Added

Expand All @@ -22,4 +22,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `generate_to_address`
- `get_balance`

[unreleased]: https://github.com/chainwayxyz/bitcoin-mock-rpc
[0.0.1]: https://github.com/chainwayxyz/bitcoin-mock-rpc/releases/tag/v0.0.1
52 changes: 52 additions & 0 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! Client crate mocks the `Client` struct in `bitcoincore-rpc`.

use crate::ledger::Ledger;
use bitcoin::Txid;
use bitcoincore_rpc::{Auth, RpcApi};

mod rpc_api;
Expand Down Expand Up @@ -43,6 +44,57 @@ impl RpcApiWrapper for Client {
}
}

/// Dumps complete ledger to a string and returns it. This can help identify
/// bugs as it draws the big picture of the mock blockchain.
pub fn dump_ledger(rpc: Client, pretty: bool) -> String {
dump_ledger_inner(rpc.ledger, pretty)
}
/// Parent of `dump_ledger`. This function accepts private `Ledger` struct. This
/// useful for only crate tests.
pub fn dump_ledger_inner(ledger: Ledger, pretty: bool) -> String {
let mut dump = String::new();

const DELIMETER: &str = "\n-----\n";

let utxos = ledger.get_user_utxos();
let transactions = ledger.get_transactions();
let credentials = ledger.get_credentials();

if pretty {
dump += format!("UTXOs: {:#?}", utxos).as_str();
dump += DELIMETER;
dump += format!("Transactions: {:#?}", transactions).as_str();
dump += DELIMETER;
dump += format!(
"Txids: {:#?}",
transactions
.iter()
.map(|tx| tx.compute_txid())
.collect::<Vec<Txid>>()
)
.as_str();
dump += DELIMETER;
dump += format!("Credentials: {:#?}", credentials).as_str();
} else {
dump += format!("UTXOs: {:?}", utxos).as_str();
dump += DELIMETER;
dump += format!("Transactions: {:?}", transactions).as_str();
dump += DELIMETER;
dump += format!(
"Txids: {:?}",
transactions
.iter()
.map(|tx| tx.compute_txid())
.collect::<Vec<Txid>>()
)
.as_str();
dump += DELIMETER;
dump += format!("Credentials: {:?}", credentials).as_str();
}

dump
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
25 changes: 16 additions & 9 deletions src/client/rpc_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@ use bitcoincore_rpc::{
};

impl RpcApi for Client {
/// TL;DR: If this function is called for `cmd`, it's corresponding mock is
/// not yet implemented. Please consider implementing it. Ellerinden oper
/// diyorum anlamadiysan.
///
/// This function normally talks with Bitcoin network. Therefore, other
/// functions calls this to send requests. In a mock environment though,
/// other functions won't be talking to a regulated interface. Rather will
/// talk with a temporary interfaces, like in-memory databases.
/// access a temporary in-memory database.
///
/// This is the reason, this function will only throw errors in case of a
/// function is not -yet- implemented. Tester should implement corresponding
/// function in this impl block, if this function called for `cmd`.
/// function calls this. Tester should implement corresponding function in
/// this impl block.
fn call<T: for<'a> serde::de::Deserialize<'a>>(
&self,
cmd: &str,
Expand All @@ -48,6 +52,8 @@ impl RpcApi for Client {

Ok(tx.compute_txid())
}
/// Because there are no blocks, this function works pretty much same as
/// `get_transaction`.
fn get_raw_transaction(
&self,
txid: &bitcoin::Txid,
Expand Down Expand Up @@ -151,6 +157,8 @@ impl RpcApi for Client {
Ok(self.ledger.add_transaction_unconditionally(tx)?)
}

/// Creates a random secret/public key pair and generates a Bitcoin address
/// from witness program.
fn get_new_address(
&self,
_label: Option<&str>,
Expand All @@ -162,7 +170,8 @@ impl RpcApi for Client {
Ok(credential.address.as_unchecked().to_owned())
}

/// Generates `block_num` amount of block rewards to user.
/// Generates `block_num` amount of block rewards to user. Block reward is
/// fixed to 1 BTC, regardless of which and how many blocks are generated.
fn generate_to_address(
&self,
block_num: u64,
Expand All @@ -180,6 +189,8 @@ impl RpcApi for Client {
Ok(vec![BlockHash::all_zeros(); block_num as usize])
}

/// Returns user's balance. Balance is calculated using addresses that are
/// generated with `get_new_address` rpc call.
fn get_balance(
&self,
_minconf: Option<usize>,
Expand Down Expand Up @@ -355,11 +366,7 @@ mod tests {

// Wallet has funds now. It should not be rejected.
let txin = rpc.ledger._create_txin(
rpc.ledger
._get_transactions()
.get(0)
.unwrap()
.compute_txid(),
rpc.ledger.get_transactions().get(0).unwrap().compute_txid(),
0,
);
let txout = rpc
Expand Down
54 changes: 36 additions & 18 deletions src/ledger/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ impl Ledger {
// Remove UTXO's that are being used used in this transaction.
transaction.input.iter().for_each(|input| {
if let Ok(tx) = self.get_transaction(input.previous_output.txid) {
let utxo = tx.output.get(0).unwrap().to_owned();
let utxo = tx
.output
.get(input.previous_output.vout as usize)
.unwrap()
.to_owned();
let script_pubkey = utxo.script_pubkey;
for i in 0..credentials.len() {
if credentials[i].address.script_pubkey() == script_pubkey {
Expand All @@ -35,19 +39,23 @@ impl Ledger {
});

// Add new UTXO's to address.
transaction.output.iter().for_each(|output| {
for i in 0..credentials.len() {
if credentials[i].address.script_pubkey() == output.script_pubkey {
let utxo = OutPoint {
txid,
vout: i as u32,
};
self.add_utxo(credentials[i].address.clone(), utxo);

break;
transaction
.output
.iter()
.enumerate()
.for_each(|(vout, output)| {
for i in 0..credentials.len() {
if credentials[i].address.script_pubkey() == output.script_pubkey {
let utxo = OutPoint {
txid,
vout: vout as u32,
};
self.add_utxo(credentials[i].address.clone(), utxo);

break;
}
}
}
});
});

// Add transaction to blockchain.
add_item_to_vec!(self.transactions, transaction.clone());
Expand All @@ -70,7 +78,7 @@ impl Ledger {
Ok(tx)
}
/// Returns user's list of transactions.
pub fn _get_transactions(&self) -> Vec<Transaction> {
pub fn get_transactions(&self) -> Vec<Transaction> {
return_vec_item!(self.transactions);
}

Expand Down Expand Up @@ -120,8 +128,18 @@ impl Ledger {
transaction: Transaction,
) -> Result<Amount, LedgerError> {
let mut amount = Amount::from_sat(0);
let utxos = self.get_user_utxos();

for input in transaction.input {
// Check if input is in UTXO list.
if utxos.iter().all(|utxo| *utxo != input.previous_output) {
return Err(LedgerError::Transaction(format!(
"Input {:?} not found in UTXO list",
input.previous_output
)));
}

// Add valid input amount to total.
amount += self
.get_transaction(input.previous_output.txid)?
.output
Expand Down Expand Up @@ -182,7 +200,7 @@ mod tests {
fn transactions_without_checks() {
let ledger = Ledger::new();

assert_eq!(ledger._get_transactions().len(), 0);
assert_eq!(ledger.get_transactions().len(), 0);

let txout = ledger.create_txout(Amount::from_sat(0x45), ScriptBuf::new());
let tx = ledger.create_transaction(vec![], vec![txout]);
Expand All @@ -193,7 +211,7 @@ mod tests {
ledger.add_transaction_unconditionally(tx.clone()).unwrap()
);

let txs = ledger._get_transactions();
let txs = ledger.get_transactions();
assert_eq!(txs.len(), 1);

let tx2 = txs.get(0).unwrap().to_owned();
Expand All @@ -211,7 +229,7 @@ mod tests {
let credential = Ledger::generate_credential_from_witness();
ledger.add_credential(credential.clone());

assert_eq!(ledger._get_transactions().len(), 0);
assert_eq!(ledger.get_transactions().len(), 0);

// First, add some funds to user, for free.
let txout = ledger.create_txout(
Expand Down Expand Up @@ -241,7 +259,7 @@ mod tests {
let txid = tx.compute_txid();
assert_eq!(txid, ledger.add_transaction(tx.clone()).unwrap());

let txs = ledger._get_transactions();
let txs = ledger.get_transactions();
assert_eq!(txs.len(), 2);

let read_tx = txs.get(1).unwrap().to_owned();
Expand Down
14 changes: 11 additions & 3 deletions tests/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ use bitcoin_mock_rpc::{Client, RpcApiWrapper};
use bitcoincore_rpc::{Auth, RpcApi};
use std::thread;

#[test]
fn generate_to_address() {
let rpc = Client::new("", Auth::None).unwrap();
let address = rpc.get_new_address(None, None).unwrap().assume_checked();

let initial_balance = rpc.get_balance(None, None).unwrap();

rpc.generate_to_address(101, &address).unwrap();
assert!(rpc.get_balance(None, None).unwrap() > initial_balance);
}

#[test]
fn generate_to_address_multi_threaded() {
// Bacause `thread::spawn` moves value to closure, cloning a new is needed. This is good,
// because cloning an rpc struct should have a persistent ledger even though there are more than
// one accessors.
let rpc = Client::new("", Auth::None).unwrap();
let cloned_rpc = rpc.clone();
let address = rpc.get_new_address(None, None).unwrap().assume_checked();
Expand Down
6 changes: 6 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ use bitcoin_mock_rpc::Client;
use bitcoincore_rpc::RpcApi;
use std::str::FromStr;

#[allow(unused)]
pub async fn send_raw_transaction_async(rpc: Client, tx: Transaction) {
rpc.send_raw_transaction(&tx).unwrap();
}

#[allow(unused)]
pub fn create_witness() -> (WitnessProgram, Witness) {
let secp = bitcoin::secp256k1::Secp256k1::new();
let internal_key = UntweakedPublicKey::from(
Expand Down Expand Up @@ -45,26 +47,30 @@ pub fn create_witness() -> (WitnessProgram, Witness) {
(witness_program, witness)
}

#[allow(unused)]
pub fn create_address_from_witness() -> Address {
let witness_program = create_witness().0;

Address::from_witness_program(witness_program, bitcoin::Network::Regtest)
}

#[allow(unused)]
pub fn create_txin(txid: Txid, vout: u32) -> TxIn {
TxIn {
previous_output: OutPoint { txid, vout },
..Default::default()
}
}

#[allow(unused)]
pub fn create_txout(value: Amount, script_pubkey: ScriptBuf) -> TxOut {
TxOut {
value,
script_pubkey,
}
}

#[allow(unused)]
pub fn create_transaction(tx_ins: Vec<TxIn>, tx_outs: Vec<TxOut>) -> Transaction {
bitcoin::Transaction {
version: bitcoin::transaction::Version(2),
Expand Down
Loading