From 8d9ccf8d0b01603f0c5aac15e7a92ba6578996a1 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Fri, 7 Aug 2020 19:40:13 +0200 Subject: [PATCH] [wallet] Allow limiting the use of internal utxos in TxBuilder --- src/blockchain/utils.rs | 1 + src/database/keyvalue.rs | 199 ++++++--------------------------- src/database/memory.rs | 210 ++++------------------------------- src/database/mod.rs | 176 +++++++++++++++++++++++++++++ src/types.rs | 5 + src/wallet/coin_selection.rs | 2 + src/wallet/mod.rs | 37 +++--- src/wallet/tx_builder.rs | 38 +++++++ 8 files changed, 298 insertions(+), 370 deletions(-) diff --git a/src/blockchain/utils.rs b/src/blockchain/utils.rs index f45ec037..6e52ed93 100644 --- a/src/blockchain/utils.rs +++ b/src/blockchain/utils.rs @@ -236,6 +236,7 @@ pub trait ElectrumLikeSync { updates.set_utxo(&UTXO { outpoint: OutPoint::new(tx.txid(), i as u32), txout: output.clone(), + is_internal: script_type.is_internal(), })?; incoming += output.value; diff --git a/src/database/keyvalue.rs b/src/database/keyvalue.rs index 06c55a10..d79c0d8e 100644 --- a/src/database/keyvalue.rs +++ b/src/database/keyvalue.rs @@ -29,8 +29,11 @@ macro_rules! impl_batch_operations { fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error> { let key = MapKey::UTXO(Some(&utxo.outpoint)).as_map_key(); - let value = serialize(&utxo.txout); - self.insert(key, value)$($after_insert)*; + let value = json!({ + "t": utxo.txout, + "i": utxo.is_internal, + }); + self.insert(key, serde_json::to_vec(&value)?)$($after_insert)*; Ok(()) } @@ -101,8 +104,11 @@ macro_rules! impl_batch_operations { match res { None => Ok(None), Some(b) => { - let txout = deserialize(&b)?; - Ok(Some(UTXO { outpoint: outpoint.clone(), txout })) + let mut val: serde_json::Value = serde_json::from_slice(&b)?; + let txout = serde_json::from_value(val["t"].take())?; + let is_internal = serde_json::from_value(val["i"].take())?; + + Ok(Some(UTXO { outpoint: outpoint.clone(), txout, is_internal })) } } } @@ -210,8 +216,16 @@ impl Database for Tree { .map(|x| -> Result<_, Error> { let (k, v) = x?; let outpoint = deserialize(&k[1..])?; - let txout = deserialize(&v)?; - Ok(UTXO { outpoint, txout }) + + let mut val: serde_json::Value = serde_json::from_slice(&v)?; + let txout = serde_json::from_value(val["t"].take())?; + let is_internal = serde_json::from_value(val["i"].take())?; + + Ok(UTXO { + outpoint, + txout, + is_internal, + }) }) .collect() } @@ -271,10 +285,14 @@ impl Database for Tree { let key = MapKey::UTXO(Some(outpoint)).as_map_key(); self.get(key)? .map(|b| -> Result<_, Error> { - let txout = deserialize(&b)?; + let mut val: serde_json::Value = serde_json::from_slice(&b)?; + let txout = serde_json::from_value(val["t"].take())?; + let is_internal = serde_json::from_value(val["i"].take())?; + Ok(UTXO { outpoint: outpoint.clone(), txout, + is_internal, }) }) .transpose() @@ -354,18 +372,11 @@ impl BatchDatabase for Tree { #[cfg(test)] mod test { - use std::str::FromStr; use std::sync::{Arc, Condvar, Mutex, Once}; use std::time::{SystemTime, UNIX_EPOCH}; use sled::{Db, Tree}; - use bitcoin::consensus::encode::deserialize; - use bitcoin::hashes::hex::*; - use bitcoin::*; - - use crate::database::*; - static mut COUNT: usize = 0; lazy_static! { @@ -406,185 +417,41 @@ mod test { #[test] fn test_script_pubkey() { - let mut tree = get_tree(); - - let script = Script::from( - Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), - ); - let path = 42; - let script_type = ScriptType::External; - - tree.set_script_pubkey(&script, script_type, path).unwrap(); - - assert_eq!( - tree.get_script_pubkey_from_path(script_type, path).unwrap(), - Some(script.clone()) - ); - assert_eq!( - tree.get_path_from_script_pubkey(&script).unwrap(), - Some((script_type, path.clone())) - ); + crate::database::test::test_script_pubkey(get_tree()); } #[test] fn test_batch_script_pubkey() { - let mut tree = get_tree(); - let mut batch = tree.begin_batch(); - - let script = Script::from( - Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), - ); - let path = 42; - let script_type = ScriptType::External; - - batch.set_script_pubkey(&script, script_type, path).unwrap(); - - assert_eq!( - tree.get_script_pubkey_from_path(script_type, path).unwrap(), - None - ); - assert_eq!(tree.get_path_from_script_pubkey(&script).unwrap(), None); - - tree.commit_batch(batch).unwrap(); - - assert_eq!( - tree.get_script_pubkey_from_path(script_type, path).unwrap(), - Some(script.clone()) - ); - assert_eq!( - tree.get_path_from_script_pubkey(&script).unwrap(), - Some((script_type, path.clone())) - ); + crate::database::test::test_batch_script_pubkey(get_tree()); } #[test] fn test_iter_script_pubkey() { - let mut tree = get_tree(); - - let script = Script::from( - Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), - ); - let path = 42; - let script_type = ScriptType::External; - - tree.set_script_pubkey(&script, script_type, path).unwrap(); - - assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1); + crate::database::test::test_iter_script_pubkey(get_tree()); } #[test] fn test_del_script_pubkey() { - let mut tree = get_tree(); - - let script = Script::from( - Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), - ); - let path = 42; - let script_type = ScriptType::External; - - tree.set_script_pubkey(&script, script_type, path).unwrap(); - assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1); - - tree.del_script_pubkey_from_path(script_type, path).unwrap(); - assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 0); + crate::database::test::test_del_script_pubkey(get_tree()); } #[test] fn test_utxo() { - let mut tree = get_tree(); - - let outpoint = OutPoint::from_str( - "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:0", - ) - .unwrap(); - let script = Script::from( - Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), - ); - let txout = TxOut { - value: 133742, - script_pubkey: script, - }; - let utxo = UTXO { txout, outpoint }; - - tree.set_utxo(&utxo).unwrap(); - - assert_eq!(tree.get_utxo(&outpoint).unwrap(), Some(utxo)); + crate::database::test::test_utxo(get_tree()); } #[test] fn test_raw_tx() { - let mut tree = get_tree(); - - let hex_tx = Vec::::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap(); - let tx: Transaction = deserialize(&hex_tx).unwrap(); - - tree.set_raw_tx(&tx).unwrap(); - - let txid = tx.txid(); - - assert_eq!(tree.get_raw_tx(&txid).unwrap(), Some(tx)); + crate::database::test::test_raw_tx(get_tree()); } #[test] fn test_tx() { - let mut tree = get_tree(); - - let hex_tx = Vec::::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap(); - let tx: Transaction = deserialize(&hex_tx).unwrap(); - let txid = tx.txid(); - let mut tx_details = TransactionDetails { - transaction: Some(tx), - txid, - timestamp: 123456, - received: 1337, - sent: 420420, - height: Some(1000), - }; - - tree.set_tx(&tx_details).unwrap(); - - // get with raw tx too - assert_eq!( - tree.get_tx(&tx_details.txid, true).unwrap(), - Some(tx_details.clone()) - ); - // get only raw_tx - assert_eq!( - tree.get_raw_tx(&tx_details.txid).unwrap(), - tx_details.transaction - ); - - // now get without raw_tx - tx_details.transaction = None; - assert_eq!( - tree.get_tx(&tx_details.txid, false).unwrap(), - Some(tx_details) - ); + crate::database::test::test_tx(get_tree()); } #[test] fn test_last_index() { - let mut tree = get_tree(); - - tree.set_last_index(ScriptType::External, 1337).unwrap(); - - assert_eq!( - tree.get_last_index(ScriptType::External).unwrap(), - Some(1337) - ); - assert_eq!(tree.get_last_index(ScriptType::Internal).unwrap(), None); - - let res = tree.increment_last_index(ScriptType::External).unwrap(); - assert_eq!(res, 1338); - let res = tree.increment_last_index(ScriptType::Internal).unwrap(); - assert_eq!(res, 0); - - assert_eq!( - tree.get_last_index(ScriptType::External).unwrap(), - Some(1338) - ); - assert_eq!(tree.get_last_index(ScriptType::Internal).unwrap(), Some(0)); + crate::database::test::test_last_index(get_tree()); } - - // TODO: more tests... } diff --git a/src/database/memory.rs b/src/database/memory.rs index b17bd549..987aa7b6 100644 --- a/src/database/memory.rs +++ b/src/database/memory.rs @@ -113,7 +113,8 @@ impl BatchOperations for MemoryDatabase { fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error> { let key = MapKey::UTXO(Some(&utxo.outpoint)).as_map_key(); - self.map.insert(key, Box::new(utxo.txout.clone())); + self.map + .insert(key, Box::new((utxo.txout.clone(), utxo.is_internal))); Ok(()) } @@ -184,10 +185,11 @@ impl BatchOperations for MemoryDatabase { match res { None => Ok(None), Some(b) => { - let txout = b.downcast_ref().cloned().unwrap(); + let (txout, is_internal) = b.downcast_ref().cloned().unwrap(); Ok(Some(UTXO { outpoint: outpoint.clone(), txout, + is_internal, })) } } @@ -274,8 +276,12 @@ impl Database for MemoryDatabase { .range::, _>((Included(&key), Excluded(&after(&key)))) .map(|(k, v)| { let outpoint = deserialize(&k[1..]).unwrap(); - let txout = v.downcast_ref().cloned().unwrap(); - Ok(UTXO { outpoint, txout }) + let (txout, is_internal) = v.downcast_ref().cloned().unwrap(); + Ok(UTXO { + outpoint, + txout, + is_internal, + }) }) .collect() } @@ -333,10 +339,11 @@ impl Database for MemoryDatabase { fn get_utxo(&self, outpoint: &OutPoint) -> Result, Error> { let key = MapKey::UTXO(Some(outpoint)).as_map_key(); Ok(self.map.get(&key).map(|b| { - let txout = b.downcast_ref().cloned().unwrap(); + let (txout, is_internal) = b.downcast_ref().cloned().unwrap(); UTXO { outpoint: outpoint.clone(), txout, + is_internal, } })) } @@ -399,14 +406,7 @@ impl BatchDatabase for MemoryDatabase { #[cfg(test)] mod test { - use std::str::FromStr; - - use bitcoin::consensus::encode::deserialize; - use bitcoin::hashes::hex::*; - use bitcoin::*; - - use super::*; - use crate::database::*; + use super::MemoryDatabase; fn get_tree() -> MemoryDatabase { MemoryDatabase::new() @@ -414,209 +414,41 @@ mod test { #[test] fn test_script_pubkey() { - let mut tree = get_tree(); - - let script = Script::from( - Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), - ); - let path = 42; - let script_type = ScriptType::External; - - tree.set_script_pubkey(&script, script_type, path).unwrap(); - - assert_eq!( - tree.get_script_pubkey_from_path(script_type, path).unwrap(), - Some(script.clone()) - ); - assert_eq!( - tree.get_path_from_script_pubkey(&script).unwrap(), - Some((script_type, path.clone())) - ); + crate::database::test::test_script_pubkey(get_tree()); } #[test] fn test_batch_script_pubkey() { - let mut tree = get_tree(); - let mut batch = tree.begin_batch(); - - let script = Script::from( - Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), - ); - let path = 42; - let script_type = ScriptType::External; - - batch.set_script_pubkey(&script, script_type, path).unwrap(); - - assert_eq!( - tree.get_script_pubkey_from_path(script_type, path).unwrap(), - None - ); - assert_eq!(tree.get_path_from_script_pubkey(&script).unwrap(), None); - - tree.commit_batch(batch).unwrap(); - - assert_eq!( - tree.get_script_pubkey_from_path(script_type, path).unwrap(), - Some(script.clone()) - ); - assert_eq!( - tree.get_path_from_script_pubkey(&script).unwrap(), - Some((script_type, path)) - ); + crate::database::test::test_batch_script_pubkey(get_tree()); } #[test] fn test_iter_script_pubkey() { - let mut tree = get_tree(); - - let script = Script::from( - Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), - ); - let path = 42; - let script_type = ScriptType::External; - - tree.set_script_pubkey(&script, script_type, path).unwrap(); - - assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1); + crate::database::test::test_iter_script_pubkey(get_tree()); } #[test] fn test_del_script_pubkey() { - let mut tree = get_tree(); - - let script = Script::from( - Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), - ); - let path = 42; - let script_type = ScriptType::External; - - tree.set_script_pubkey(&script, script_type, path).unwrap(); - assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1); - - tree.del_script_pubkey_from_path(script_type, path).unwrap(); - assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 0); - } - - #[test] - fn test_del_script_pubkey_batch() { - let mut tree = get_tree(); - - let script = Script::from( - Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), - ); - let path = 42; - let script_type = ScriptType::External; - - tree.set_script_pubkey(&script, script_type, path).unwrap(); - assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1); - - let mut batch = tree.begin_batch(); - batch - .del_script_pubkey_from_path(script_type, path) - .unwrap(); - - assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1); - - tree.commit_batch(batch).unwrap(); - assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 0); + crate::database::test::test_del_script_pubkey(get_tree()); } #[test] fn test_utxo() { - let mut tree = get_tree(); - - let outpoint = OutPoint::from_str( - "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:0", - ) - .unwrap(); - let script = Script::from( - Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), - ); - let txout = TxOut { - value: 133742, - script_pubkey: script, - }; - let utxo = UTXO { txout, outpoint }; - - tree.set_utxo(&utxo).unwrap(); - - assert_eq!(tree.get_utxo(&outpoint).unwrap(), Some(utxo)); + crate::database::test::test_utxo(get_tree()); } #[test] fn test_raw_tx() { - let mut tree = get_tree(); - - let hex_tx = Vec::::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap(); - let tx: Transaction = deserialize(&hex_tx).unwrap(); - - tree.set_raw_tx(&tx).unwrap(); - - let txid = tx.txid(); - - assert_eq!(tree.get_raw_tx(&txid).unwrap(), Some(tx)); + crate::database::test::test_raw_tx(get_tree()); } #[test] fn test_tx() { - let mut tree = get_tree(); - - let hex_tx = Vec::::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap(); - let tx: Transaction = deserialize(&hex_tx).unwrap(); - let txid = tx.txid(); - let mut tx_details = TransactionDetails { - transaction: Some(tx), - txid, - timestamp: 123456, - received: 1337, - sent: 420420, - height: Some(1000), - }; - - tree.set_tx(&tx_details).unwrap(); - - // get with raw tx too - assert_eq!( - tree.get_tx(&tx_details.txid, true).unwrap(), - Some(tx_details.clone()) - ); - // get only raw_tx - assert_eq!( - tree.get_raw_tx(&tx_details.txid).unwrap(), - tx_details.transaction - ); - - // now get without raw_tx - tx_details.transaction = None; - assert_eq!( - tree.get_tx(&tx_details.txid, false).unwrap(), - Some(tx_details) - ); + crate::database::test::test_tx(get_tree()); } #[test] fn test_last_index() { - let mut tree = get_tree(); - - tree.set_last_index(ScriptType::External, 1337).unwrap(); - - assert_eq!( - tree.get_last_index(ScriptType::External).unwrap(), - Some(1337) - ); - assert_eq!(tree.get_last_index(ScriptType::Internal).unwrap(), None); - - let res = tree.increment_last_index(ScriptType::External).unwrap(); - assert_eq!(res, 1338); - let res = tree.increment_last_index(ScriptType::Internal).unwrap(); - assert_eq!(res, 0); - - assert_eq!( - tree.get_last_index(ScriptType::External).unwrap(), - Some(1338) - ); - assert_eq!(tree.get_last_index(ScriptType::Internal).unwrap(), Some(0)); + crate::database::test::test_last_index(get_tree()); } - - // TODO: more tests... } diff --git a/src/database/mod.rs b/src/database/mod.rs index 8fc0c93a..9c0b5235 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -106,3 +106,179 @@ pub trait DatabaseUtils: Database { } impl DatabaseUtils for T {} + +#[cfg(test)] +pub mod test { + use std::str::FromStr; + + use bitcoin::consensus::encode::deserialize; + use bitcoin::hashes::hex::*; + use bitcoin::*; + + use super::*; + + pub fn test_script_pubkey(mut tree: D) { + let script = Script::from( + Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), + ); + let path = 42; + let script_type = ScriptType::External; + + tree.set_script_pubkey(&script, script_type, path).unwrap(); + + assert_eq!( + tree.get_script_pubkey_from_path(script_type, path).unwrap(), + Some(script.clone()) + ); + assert_eq!( + tree.get_path_from_script_pubkey(&script).unwrap(), + Some((script_type, path.clone())) + ); + } + + pub fn test_batch_script_pubkey(mut tree: D) { + let mut batch = tree.begin_batch(); + + let script = Script::from( + Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), + ); + let path = 42; + let script_type = ScriptType::External; + + batch.set_script_pubkey(&script, script_type, path).unwrap(); + + assert_eq!( + tree.get_script_pubkey_from_path(script_type, path).unwrap(), + None + ); + assert_eq!(tree.get_path_from_script_pubkey(&script).unwrap(), None); + + tree.commit_batch(batch).unwrap(); + + assert_eq!( + tree.get_script_pubkey_from_path(script_type, path).unwrap(), + Some(script.clone()) + ); + assert_eq!( + tree.get_path_from_script_pubkey(&script).unwrap(), + Some((script_type, path.clone())) + ); + } + + pub fn test_iter_script_pubkey(mut tree: D) { + let script = Script::from( + Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), + ); + let path = 42; + let script_type = ScriptType::External; + + tree.set_script_pubkey(&script, script_type, path).unwrap(); + + assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1); + } + + pub fn test_del_script_pubkey(mut tree: D) { + let script = Script::from( + Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), + ); + let path = 42; + let script_type = ScriptType::External; + + tree.set_script_pubkey(&script, script_type, path).unwrap(); + assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1); + + tree.del_script_pubkey_from_path(script_type, path).unwrap(); + assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 0); + } + + pub fn test_utxo(mut tree: D) { + let outpoint = OutPoint::from_str( + "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:0", + ) + .unwrap(); + let script = Script::from( + Vec::::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(), + ); + let txout = TxOut { + value: 133742, + script_pubkey: script, + }; + let utxo = UTXO { + txout, + outpoint, + is_internal: false, + }; + + tree.set_utxo(&utxo).unwrap(); + + assert_eq!(tree.get_utxo(&outpoint).unwrap(), Some(utxo)); + } + + pub fn test_raw_tx(mut tree: D) { + let hex_tx = Vec::::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap(); + let tx: Transaction = deserialize(&hex_tx).unwrap(); + + tree.set_raw_tx(&tx).unwrap(); + + let txid = tx.txid(); + + assert_eq!(tree.get_raw_tx(&txid).unwrap(), Some(tx)); + } + + pub fn test_tx(mut tree: D) { + let hex_tx = Vec::::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap(); + let tx: Transaction = deserialize(&hex_tx).unwrap(); + let txid = tx.txid(); + let mut tx_details = TransactionDetails { + transaction: Some(tx), + txid, + timestamp: 123456, + received: 1337, + sent: 420420, + height: Some(1000), + }; + + tree.set_tx(&tx_details).unwrap(); + + // get with raw tx too + assert_eq!( + tree.get_tx(&tx_details.txid, true).unwrap(), + Some(tx_details.clone()) + ); + // get only raw_tx + assert_eq!( + tree.get_raw_tx(&tx_details.txid).unwrap(), + tx_details.transaction + ); + + // now get without raw_tx + tx_details.transaction = None; + assert_eq!( + tree.get_tx(&tx_details.txid, false).unwrap(), + Some(tx_details) + ); + } + + pub fn test_last_index(mut tree: D) { + tree.set_last_index(ScriptType::External, 1337).unwrap(); + + assert_eq!( + tree.get_last_index(ScriptType::External).unwrap(), + Some(1337) + ); + assert_eq!(tree.get_last_index(ScriptType::Internal).unwrap(), None); + + let res = tree.increment_last_index(ScriptType::External).unwrap(); + assert_eq!(res, 1338); + let res = tree.increment_last_index(ScriptType::Internal).unwrap(); + assert_eq!(res, 0); + + assert_eq!( + tree.get_last_index(ScriptType::External).unwrap(), + Some(1338) + ); + assert_eq!(tree.get_last_index(ScriptType::Internal).unwrap(), Some(0)); + } + + // TODO: more tests... +} diff --git a/src/types.rs b/src/types.rs index a3342311..dff48f96 100644 --- a/src/types.rs +++ b/src/types.rs @@ -19,6 +19,10 @@ impl ScriptType { ScriptType::Internal => 'i' as u8, } } + + pub fn is_internal(&self) -> bool { + self == &ScriptType::Internal + } } impl AsRef<[u8]> for ScriptType { @@ -34,6 +38,7 @@ impl AsRef<[u8]> for ScriptType { pub struct UTXO { pub outpoint: OutPoint, pub txout: TxOut, + pub is_internal: bool, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index fc7211e1..ed7ba9eb 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -109,6 +109,7 @@ mod test { value: 100_000, script_pubkey: Script::new(), }, + is_internal: false, }, UTXO { outpoint: OutPoint::from_str( @@ -119,6 +120,7 @@ mod test { value: 200_000, script_pubkey: Script::new(), }, + is_internal: true, }, ] } diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index fe0f2b9b..b6cd8532 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -217,8 +217,12 @@ where .max_satisfaction_weight(), ); - let (available_utxos, use_all_utxos) = - self.get_available_utxos(&builder.utxos, &builder.unspendable, builder.send_all)?; + let (available_utxos, use_all_utxos) = self.get_available_utxos( + builder.change_policy, + &builder.utxos, + &builder.unspendable, + builder.send_all, + )?; let coin_selection::CoinSelectionResult { txin, total_amount, @@ -646,11 +650,11 @@ where fn get_available_utxos( &self, + change_policy: tx_builder::ChangeSpendPolicy, utxo: &Option>, unspendable: &Option>, send_all: bool, ) -> Result<(Vec, bool), Error> { - // TODO: should we consider unconfirmed received rbf txs as "unspendable" too by default? let unspendable_set = match unspendable { None => HashSet::new(), Some(vec) => vec.into_iter().collect(), @@ -660,25 +664,28 @@ where // with manual coin selection we always want to spend all the selected utxos, no matter // what (even if they are marked as unspendable) Some(raw_utxos) => { - // TODO: unwrap to remove - let full_utxos: Vec<_> = raw_utxos + let full_utxos = raw_utxos .iter() - .map(|u| self.database.borrow().get_utxo(&u).unwrap()) - .collect(); + .map(|u| self.database.borrow().get_utxo(&u)) + .collect::, _>>()?; if !full_utxos.iter().all(|u| u.is_some()) { return Err(Error::UnknownUTXO); } Ok((full_utxos.into_iter().map(|x| x.unwrap()).collect(), true)) } - // otherwise limit ourselves to the spendable utxos and the `send_all` setting - None => Ok(( - self.list_unspent()? - .into_iter() - .filter(|u| !unspendable_set.contains(&u.outpoint)) - .collect(), - send_all, - )), + // otherwise limit ourselves to the spendable utxos for the selected policy, and the `send_all` setting + None => { + let utxos = self.list_unspent()?.into_iter(); + let utxos = change_policy.filter_utxos(utxos).into_iter(); + + Ok(( + utxos + .filter(|u| !unspendable_set.contains(&u.outpoint)) + .collect(), + send_all, + )) + } } } diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 1b43fbdb..21fc932e 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -5,6 +5,7 @@ use bitcoin::{Address, OutPoint, SigHashType, Transaction}; use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; use super::utils::FeeRate; +use crate::types::UTXO; // TODO: add a flag to ignore change outputs (make them unspendable) #[derive(Debug, Default)] @@ -20,6 +21,7 @@ pub struct TxBuilder { pub(crate) locktime: Option, pub(crate) rbf: Option, pub(crate) version: Version, + pub(crate) change_policy: ChangeSpendPolicy, pub(crate) coin_selection: Cs, } @@ -59,11 +61,13 @@ impl TxBuilder { self } + /// These have priority over the "unspendable" utxos pub fn utxos(mut self, utxos: Vec) -> Self { self.utxos = Some(utxos); self } + /// This has priority over the "unspendable" utxos pub fn add_utxo(mut self, utxo: OutPoint) -> Self { self.utxos.get_or_insert(vec![]).push(utxo); self @@ -108,6 +112,16 @@ impl TxBuilder { self } + pub fn do_not_spend_change(mut self) -> Self { + self.change_policy = ChangeSpendPolicy::ChangeForbidden; + self + } + + pub fn only_spend_change(mut self) -> Self { + self.change_policy = ChangeSpendPolicy::OnlyChange; + self + } + pub fn coin_selection(self, coin_selection: P) -> TxBuilder

{ TxBuilder { addressees: self.addressees, @@ -121,6 +135,7 @@ impl TxBuilder { locktime: self.locktime, rbf: self.rbf, version: self.version, + change_policy: self.change_policy, coin_selection, } } @@ -176,6 +191,29 @@ impl Default for Version { } } +#[derive(Debug)] +pub enum ChangeSpendPolicy { + ChangeAllowed, + OnlyChange, + ChangeForbidden, +} + +impl Default for ChangeSpendPolicy { + fn default() -> Self { + ChangeSpendPolicy::ChangeAllowed + } +} + +impl ChangeSpendPolicy { + pub(crate) fn filter_utxos>(&self, iter: I) -> Vec { + match self { + ChangeSpendPolicy::ChangeAllowed => iter.collect(), + ChangeSpendPolicy::OnlyChange => iter.filter(|utxo| utxo.is_internal).collect(), + ChangeSpendPolicy::ChangeForbidden => iter.filter(|utxo| !utxo.is_internal).collect(), + } + } +} + #[cfg(test)] mod test { const ORDERING_TEST_TX: &'static str = "0200000003c26f3eb7932f7acddc5ddd26602b77e7516079b03090a16e2c2f54\