Skip to content

Commit

Permalink
Merge pull request #5 from chainwayxyz/sendtoaddress_overhaul
Browse files Browse the repository at this point in the history
Send to address simplification
  • Loading branch information
ceyhunsen authored Jul 1, 2024
2 parents 56f2e72 + 02d3394 commit 633d3f1
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 49 deletions.
76 changes: 39 additions & 37 deletions src/client/rpc_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use super::Client;
use crate::ledger::Ledger;
use bitcoin::{
address::NetworkChecked, consensus::encode, hashes::Hash, params::Params, Address, Amount,
BlockHash, SignedAmount, Transaction, TxIn, Wtxid,
BlockHash, SignedAmount, Transaction, Wtxid,
};
use bitcoincore_rpc::{
json::{
Expand All @@ -16,7 +16,6 @@ use bitcoincore_rpc::{
},
RpcApi,
};
use std::io::Error;

impl RpcApi for Client {
/// This function normally talks with Bitcoin network. Therefore, other
Expand Down Expand Up @@ -130,6 +129,8 @@ impl RpcApi for Client {
Ok(res)
}

/// Warning `send_to_address` won't check anything. It will only send funds
/// to specified address. This means: Unlimited free money.
fn send_to_address(
&self,
address: &Address<NetworkChecked>,
Expand All @@ -141,42 +142,13 @@ impl RpcApi for Client {
_confirmation_target: Option<u32>,
_estimate_mode: Option<json::EstimateMode>,
) -> bitcoincore_rpc::Result<bitcoin::Txid> {
let balance = self.ledger.calculate_balance()?;
if balance < amount {
return Err(bitcoincore_rpc::Error::Io(Error::other(format!(
"Output larger than current balance: {amount} > {balance}"
))));
}

// Get latest address of the user. Change will be sent to this address.
let user_address = self
.ledger
.get_credentials()
.last()
.ok_or(bitcoincore_rpc::Error::Io(Error::other(
"No user address found!".to_string(),
)))?
.address
.to_owned();

let (utxos, total_value) = self.ledger.combine_utxos(amount)?;
let txins: Vec<TxIn> = utxos
.iter()
.map(|utxo| self.ledger.create_txin(utxo.txid, utxo.vout))
.collect();

let target_txout = self
.ledger
.create_txout(amount, Some(address.script_pubkey()));
let change = self
.ledger
.create_txout(total_value - amount, Some(user_address.script_pubkey()));

let tx = self
.ledger
.create_transaction(txins, vec![target_txout, change]);
let tx = self.ledger.create_transaction(vec![], vec![target_txout]);

self.send_raw_transaction(&tx)
Ok(self.ledger.add_transaction_unconditionally(tx.clone())?)
}

fn get_new_address(
Expand Down Expand Up @@ -274,14 +246,14 @@ mod tests {
let txid = rpc.ledger.add_transaction_unconditionally(tx).unwrap();

// Create a new raw transactions that is valid.
let txin = rpc.ledger.create_txin(txid, 0);
let txin = rpc.ledger._create_txin(txid, 0);
let txout = rpc
.ledger
.create_txout(Amount::from_sat(0x45), Some(address.script_pubkey()));
let inserted_tx1 = rpc.ledger.create_transaction(vec![txin], vec![txout]);
rpc.send_raw_transaction(&inserted_tx1).unwrap();

let txin = rpc.ledger.create_txin(inserted_tx1.compute_txid(), 0);
let txin = rpc.ledger._create_txin(inserted_tx1.compute_txid(), 0);
let txout = rpc.ledger.create_txout(
Amount::from_sat(0x45),
Some(
Expand Down Expand Up @@ -323,7 +295,7 @@ mod tests {
let txid = rpc.ledger.add_transaction_unconditionally(tx).unwrap();

// Insert raw transactions to Bitcoin.
let txin = rpc.ledger.create_txin(txid, 0);
let txin = rpc.ledger._create_txin(txid, 0);
let txout = rpc
.ledger
.create_txout(Amount::from_sat(0x1F), Some(address.script_pubkey()));
Expand All @@ -338,6 +310,7 @@ mod tests {
}

#[test]
#[ignore = "Not necessary after the send_to_address simplification"]
fn send_to_address() {
let rpc = Client::new("", bitcoincore_rpc::Auth::None).unwrap();

Expand Down Expand Up @@ -389,6 +362,35 @@ mod tests {
);
}

#[test]
fn send_to_address_without_balance_check() {
let rpc = Client::new("", bitcoincore_rpc::Auth::None).unwrap();

let credential = Ledger::generate_credential_from_witness();
let receiver_address = credential.address;

// send_to_address should send `amount` to `address`, regardless of the
// user's balance.
let txid = rpc
.send_to_address(
&receiver_address,
Amount::from_sat(0x45),
None,
None,
None,
None,
None,
None,
)
.unwrap();

let tx = rpc.get_raw_transaction(&txid, None).unwrap();

// Receiver should have this.
assert_eq!(tx.output[0].value.to_sat(), 0x45);
assert_eq!(tx.output[0].script_pubkey, receiver_address.script_pubkey());
}

#[test]
fn get_new_address() {
let rpc = Client::new("", bitcoincore_rpc::Auth::None).unwrap();
Expand Down Expand Up @@ -442,7 +444,7 @@ mod tests {
rpc.generate_to_address(101, &address).unwrap();

// Wallet has funds now. It should not be rejected.
let txin = rpc.ledger.create_txin(
let txin = rpc.ledger._create_txin(
rpc.ledger
._get_transactions()
.get(0)
Expand Down
13 changes: 7 additions & 6 deletions src/ledger/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ impl Ledger {
)));
}

// TODO: Use these checks.
for input in transaction.input.iter() {
for input_idx in 0..transaction.input.len() {
let previous_output = self.get_transaction(input.previous_output.txid)?.output;
Expand All @@ -94,11 +95,11 @@ impl Ledger {
let script_pubkey = previous_output.clone().script_pubkey;

if script_pubkey.is_p2wpkh() {
P2WPKHChecker::check(&transaction, &previous_output, input_idx)?;
let _ = P2WPKHChecker::check(&transaction, &previous_output, input_idx);
} else if script_pubkey.is_p2wsh() {
P2WSHChecker::check(&transaction, &previous_output, input_idx)?;
let _ = P2WSHChecker::check(&transaction, &previous_output, input_idx);
} else if script_pubkey.is_p2tr() {
P2TRChecker::check(&transaction, &previous_output, input_idx)?;
let _ = P2TRChecker::check(&transaction, &previous_output, input_idx);
}
}
}
Expand Down Expand Up @@ -143,7 +144,7 @@ impl Ledger {
}

/// Creates a `TxIn` with some defaults.
pub fn create_txin(&self, txid: Txid, vout: u32) -> TxIn {
pub fn _create_txin(&self, txid: Txid, vout: u32) -> TxIn {
get_item!(self.credentials, credentials);
let witness = match credentials.last() {
Some(c) => match c.to_owned().witness {
Expand Down Expand Up @@ -240,7 +241,7 @@ mod tests {
};

// Create a valid transaction. This should pass checks.
let txin = ledger.create_txin(txid, 0);
let txin = ledger._create_txin(txid, 0);
let txout = ledger.create_txout(
Amount::from_sat(0x44 * 0x45),
Some(credential.address.script_pubkey()),
Expand Down Expand Up @@ -284,7 +285,7 @@ mod tests {
Amount::from_sat(0)
);
// Valid input should be OK.
let txin = ledger.create_txin(txid, 0);
let txin = ledger._create_txin(txid, 0);
let tx = ledger.create_transaction(vec![txin], vec![txout]);
assert_eq!(
ledger.calculate_transaction_input_value(tx).unwrap(),
Expand Down
12 changes: 6 additions & 6 deletions src/ledger/utxo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl Ledger {
/// # Returns
///
/// Returns UTXO's in a `Vec` and their total value.
pub fn combine_utxos(&self, amount: Amount) -> Result<(Vec<OutPoint>, Amount), LedgerError> {
pub fn _combine_utxos(&self, amount: Amount) -> Result<(Vec<OutPoint>, Amount), LedgerError> {
let mut total_value = Amount::from_sat(0);
let mut utxos = Vec::new();

Expand Down Expand Up @@ -196,25 +196,25 @@ mod tests {
// Because combining currently uses FIFO algorithm for choosing UTXO's
// and we know what are getting pushed, we can guess correct txin value.
assert_eq!(
ledger.combine_utxos(Amount::from_sat(1)).unwrap().1,
ledger._combine_utxos(Amount::from_sat(1)).unwrap().1,
Amount::from_sat(1)
);
assert_eq!(
ledger.combine_utxos(Amount::from_sat(4)).unwrap().1,
ledger._combine_utxos(Amount::from_sat(4)).unwrap().1,
Amount::from_sat(6)
);
assert_eq!(
ledger.combine_utxos(Amount::from_sat(10)).unwrap().1,
ledger._combine_utxos(Amount::from_sat(10)).unwrap().1,
Amount::from_sat(10)
);
assert_eq!(
ledger.combine_utxos(Amount::from_sat(11)).unwrap().1,
ledger._combine_utxos(Amount::from_sat(11)).unwrap().1,
Amount::from_sat(15)
);

// Trying to request an amount bigger than current balance should throw
// an error.
if let Ok(_) = ledger.combine_utxos(Amount::from_sat((0..100).sum::<u64>() + 1)) {
if let Ok(_) = ledger._combine_utxos(Amount::from_sat((0..100).sum::<u64>() + 1)) {
assert!(false);
}
}
Expand Down
45 changes: 45 additions & 0 deletions tests/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Transaction related integration tests.
use bitcoin::{Amount, OutPoint, TxIn};
use bitcoin_mock_rpc::{Client, RpcApiWrapper};
use bitcoincore_rpc::{Auth, RpcApi};
use std::thread;
Expand All @@ -8,6 +9,7 @@ mod common;
use common::test_common;

#[test]
#[ignore = "Not necessary after the send_to_address simplification"]
fn send_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
Expand Down Expand Up @@ -68,3 +70,46 @@ fn send_to_address_multi_threaded() {
); // No multiplication over `Amount`.
assert!(rpc.get_balance(None, None).unwrap() < initial_balance);
}

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

let address = rpc.get_new_address(None, None).unwrap().assume_checked();
let deposit_address = test_common::create_address_from_witness();

let deposit_value = Amount::from_sat(0x45);

let txid = rpc
.send_to_address(
&address,
deposit_value * 0x1F,
None,
None,
None,
None,
None,
None,
)
.unwrap();
assert_eq!(rpc.get_balance(None, None).unwrap(), deposit_value * 0x1F);

let tx = rpc.get_raw_transaction(&txid, None).unwrap();
assert_eq!(tx.output.get(0).unwrap().value, deposit_value * 0x1F);

// Valid tx.
let txin = TxIn {
previous_output: OutPoint { txid, vout: 0 },
..Default::default()
};
let txout = test_common::create_txout(0x45, Some(deposit_address.script_pubkey()));
let tx = test_common::create_transaction(vec![txin.clone()], vec![txout]);
rpc.send_raw_transaction(&tx).unwrap();

// Invalid tx.
let txout = test_common::create_txout(0x45 * 0x45, Some(deposit_address.script_pubkey()));
let tx = test_common::create_transaction(vec![txin], vec![txout]);
if let Ok(_) = rpc.send_raw_transaction(&tx) {
assert!(false);
};
}

0 comments on commit 633d3f1

Please sign in to comment.