From c0d990709f3b3a20066abdfff63b0bff0a94ea8a Mon Sep 17 00:00:00 2001 From: raphjaph Date: Mon, 17 Oct 2022 20:13:17 +0200 Subject: [PATCH 1/9] merging --- src/subcommand/wallet/send.rs | 14 +- src/subcommand/wallet/transaction_builder.rs | 150 ++++++++++++++++++- test-bitcoincore-rpc/src/api.rs | 3 + test-bitcoincore-rpc/src/server.rs | 8 + 4 files changed, 171 insertions(+), 4 deletions(-) diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index db9a94f7ff..27ebfd398e 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -1,4 +1,4 @@ -use {super::*, transaction_builder::TransactionBuilder}; +use {super::*, bitcoincore_rpc::Client, transaction_builder::TransactionBuilder}; #[derive(Debug, Parser)] pub(crate) struct Send { @@ -14,9 +14,13 @@ impl Send { index.index()?; let utxos = list_unspent(&options, &index)?.into_iter().collect(); + let change = [ + self.get_change_address(&client)?, + self.get_change_address(&client)?, + ]; let unsigned_transaction = - TransactionBuilder::build_transaction(utxos, self.ordinal, self.address)?; + TransactionBuilder::build_transaction(utxos, self.ordinal, self.address, change)?; let signed_tx = client .sign_raw_transaction_with_wallet(&unsigned_transaction, None, None)? @@ -27,4 +31,10 @@ impl Send { println!("{txid}"); Ok(()) } + + fn get_change_address(&self, client: &Client) -> Result
{ + client + .call("getrawchangeaddress", &[]) + .context("Could not get change addresses from wallet") + } } diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 5653627dc5..3c5886b796 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -25,6 +25,7 @@ impl std::error::Error for Error {} #[derive(Debug, PartialEq)] pub(crate) struct TransactionBuilder { + change: [Address; 2], inputs: Vec, ordinal: Ordinal, outputs: Vec<(Address, Amount)>, @@ -40,9 +41,11 @@ impl TransactionBuilder { ranges: BTreeMap>, ordinal: Ordinal, recipient: Address, + change: [Address; 2], ) -> Result { - Self::new(ranges, ordinal, recipient) + Self::new(ranges, ordinal, recipient, change) .select_ordinal()? + .strip_excess_postage()? .deduct_fee()? .build() } @@ -51,6 +54,7 @@ impl TransactionBuilder { ranges: BTreeMap>, ordinal: Ordinal, recipient: Address, + change: [Address; 2], ) -> Self { Self { utxos: ranges.keys().cloned().collect(), @@ -59,6 +63,7 @@ impl TransactionBuilder { outputs: Vec::new(), ranges, recipient, + change, } } @@ -84,6 +89,26 @@ impl TransactionBuilder { Ok(self) } + fn strip_excess_postage(mut self) -> Result { + const MIN_POSTAGE: u64 = 10_000; + let ordinal_offset = self.calculate_ordinal_offset(); + let output_amount = self + .outputs + .iter() + .map(|(_address, amount)| *amount) + .sum::(); + + if Amount::from_sat(ordinal_offset + 2 * MIN_POSTAGE) < output_amount { + self.outputs[0].1 = Amount::from_sat(MIN_POSTAGE); + self.outputs.push(( + self.change[0].clone(), + output_amount - Amount::from_sat(ordinal_offset + MIN_POSTAGE), + )); + } + + Ok(self) + } + fn deduct_fee(mut self) -> Result { let ordinal_offset = self.calculate_ordinal_offset(); @@ -208,6 +233,7 @@ impl TransactionBuilder { #[cfg(test)] mod tests { use {super::Error, super::*}; + const MIN_POSTAGE: u64 = 10_000; #[test] fn select_ordinal() { @@ -238,6 +264,14 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), + [ + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), + "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" + .parse() + .unwrap(), + ], ) .select_ordinal() .unwrap(); @@ -295,6 +329,14 @@ mod tests { recipient: "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), + change: [ + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), + "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" + .parse() + .unwrap(), + ], inputs: vec![ "1111111111111111111111111111111111111111111111111111111111111111:1" .parse() @@ -402,6 +444,14 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), + [ + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), + "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" + .parse() + .unwrap(), + ] ), Ok(Transaction { version: 1, @@ -415,7 +465,7 @@ mod tests { witness: Witness::new(), },], output: vec![TxOut { - value: 4836, + value: 5000 - 164, script_pubkey: "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse::
() .unwrap() @@ -441,6 +491,14 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), + [ + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), + "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" + .parse() + .unwrap(), + ] ), Err(Error::ConsumedByFee(Ordinal(14900))) ) @@ -462,6 +520,14 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), + [ + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), + "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" + .parse() + .unwrap(), + ], ) .build() .ok(); @@ -483,6 +549,14 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), + [ + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), + "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" + .parse() + .unwrap(), + ], ) .build() .ok(); @@ -504,6 +578,14 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), + [ + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), + "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" + .parse() + .unwrap(), + ], ) .select_ordinal() .unwrap(); @@ -531,6 +613,14 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), + [ + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), + "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" + .parse() + .unwrap(), + ], ) .select_ordinal() .unwrap(); @@ -539,4 +629,60 @@ mod tests { builder.build().ok(); } + + #[test] + fn excess_postage_is_stripped() { + let utxos = vec![( + "1111111111111111111111111111111111111111111111111111111111111111:1" + .parse() + .unwrap(), + vec![(0, 1_000_000)], + )]; + + pretty_assert_eq!( + TransactionBuilder::build_transaction( + utxos.into_iter().collect(), + Ordinal(0), + "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" + .parse() + .unwrap(), + [ + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), + "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" + .parse() + .unwrap(), + ] + ), + Ok(Transaction { + version: 1, + lock_time: PackedLockTime::ZERO, + input: vec![TxIn { + previous_output: "1111111111111111111111111111111111111111111111111111111111111111:1" + .parse() + .unwrap(), + script_sig: Script::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::new(), + },], + output: vec![ + TxOut { + value: MIN_POSTAGE, + script_pubkey: "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" + .parse::
() + .unwrap() + .script_pubkey(), + }, + TxOut { + value: 1_000_000 - MIN_POSTAGE - 226, + script_pubkey: "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse::
() + .unwrap() + .script_pubkey(), + } + ], + }) + ) + } } diff --git a/test-bitcoincore-rpc/src/api.rs b/test-bitcoincore-rpc/src/api.rs index 41c19d7910..f460ce2cc0 100644 --- a/test-bitcoincore-rpc/src/api.rs +++ b/test-bitcoincore-rpc/src/api.rs @@ -66,4 +66,7 @@ pub trait Api { include_unsafe: Option, query_options: Option, ) -> Result, jsonrpc_core::Error>; + + #[rpc(name = "getrawchangeaddress")] + fn get_raw_change_address(&self) -> Result; } diff --git a/test-bitcoincore-rpc/src/server.rs b/test-bitcoincore-rpc/src/server.rs index e1bee2ff43..4b54041095 100644 --- a/test-bitcoincore-rpc/src/server.rs +++ b/test-bitcoincore-rpc/src/server.rs @@ -299,4 +299,12 @@ impl Api for Server { .collect(), ) } + + fn get_raw_change_address(&self) -> Result { + Ok( + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), + ) + } } From bd4487dfd9fc100a9565a1e8d4485c843a6b27f4 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Mon, 17 Oct 2022 20:31:22 +0200 Subject: [PATCH 2/9] refactor --- src/subcommand/wallet/transaction_builder.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 3c5886b796..6b40959e2e 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -37,6 +37,8 @@ pub(crate) struct TransactionBuilder { type Result = std::result::Result; impl TransactionBuilder { + const TARGET_POSTAGE: u64 = 10_000; + pub(crate) fn build_transaction( ranges: BTreeMap>, ordinal: Ordinal, @@ -90,19 +92,18 @@ impl TransactionBuilder { } fn strip_excess_postage(mut self) -> Result { - const MIN_POSTAGE: u64 = 10_000; let ordinal_offset = self.calculate_ordinal_offset(); - let output_amount = self + let output_total = self .outputs .iter() .map(|(_address, amount)| *amount) .sum::(); - if Amount::from_sat(ordinal_offset + 2 * MIN_POSTAGE) < output_amount { - self.outputs[0].1 = Amount::from_sat(MIN_POSTAGE); + if output_total > Amount::from_sat(ordinal_offset + 2 * Self::TARGET_POSTAGE) { + self.outputs[0].1 = Amount::from_sat(Self::TARGET_POSTAGE); self.outputs.push(( self.change[0].clone(), - output_amount - Amount::from_sat(ordinal_offset + MIN_POSTAGE), + output_total - Amount::from_sat(ordinal_offset + Self::TARGET_POSTAGE), )); } @@ -233,7 +234,6 @@ impl TransactionBuilder { #[cfg(test)] mod tests { use {super::Error, super::*}; - const MIN_POSTAGE: u64 = 10_000; #[test] fn select_ordinal() { @@ -668,14 +668,14 @@ mod tests { },], output: vec![ TxOut { - value: MIN_POSTAGE, + value: TransactionBuilder::TARGET_POSTAGE, script_pubkey: "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse::
() .unwrap() .script_pubkey(), }, TxOut { - value: 1_000_000 - MIN_POSTAGE - 226, + value: 1_000_000 - TransactionBuilder::TARGET_POSTAGE - 226, script_pubkey: "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" .parse::
() .unwrap() From 4cbb6b064be10c593e87726abbe7beb975f097fc Mon Sep 17 00:00:00 2001 From: raphjaph Date: Mon, 17 Oct 2022 20:39:35 +0200 Subject: [PATCH 3/9] removed second change address --- src/subcommand/wallet/send.rs | 1 - src/subcommand/wallet/transaction_builder.rs | 33 ++------------------ 2 files changed, 3 insertions(+), 31 deletions(-) diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 27ebfd398e..69297d4546 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -16,7 +16,6 @@ impl Send { let utxos = list_unspent(&options, &index)?.into_iter().collect(); let change = [ self.get_change_address(&client)?, - self.get_change_address(&client)?, ]; let unsigned_transaction = diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 6b40959e2e..c0ca024679 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -25,7 +25,7 @@ impl std::error::Error for Error {} #[derive(Debug, PartialEq)] pub(crate) struct TransactionBuilder { - change: [Address; 2], + change: [Address; 1], inputs: Vec, ordinal: Ordinal, outputs: Vec<(Address, Amount)>, @@ -43,7 +43,7 @@ impl TransactionBuilder { ranges: BTreeMap>, ordinal: Ordinal, recipient: Address, - change: [Address; 2], + change: [Address; 1], ) -> Result { Self::new(ranges, ordinal, recipient, change) .select_ordinal()? @@ -56,7 +56,7 @@ impl TransactionBuilder { ranges: BTreeMap>, ordinal: Ordinal, recipient: Address, - change: [Address; 2], + change: [Address; 1], ) -> Self { Self { utxos: ranges.keys().cloned().collect(), @@ -268,9 +268,6 @@ mod tests { "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" .parse() .unwrap(), - "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" - .parse() - .unwrap(), ], ) .select_ordinal() @@ -333,9 +330,6 @@ mod tests { "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" .parse() .unwrap(), - "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" - .parse() - .unwrap(), ], inputs: vec![ "1111111111111111111111111111111111111111111111111111111111111111:1" @@ -448,9 +442,6 @@ mod tests { "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" .parse() .unwrap(), - "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" - .parse() - .unwrap(), ] ), Ok(Transaction { @@ -495,9 +486,6 @@ mod tests { "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" .parse() .unwrap(), - "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" - .parse() - .unwrap(), ] ), Err(Error::ConsumedByFee(Ordinal(14900))) @@ -524,9 +512,6 @@ mod tests { "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" .parse() .unwrap(), - "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" - .parse() - .unwrap(), ], ) .build() @@ -553,9 +538,6 @@ mod tests { "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" .parse() .unwrap(), - "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" - .parse() - .unwrap(), ], ) .build() @@ -582,9 +564,6 @@ mod tests { "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" .parse() .unwrap(), - "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" - .parse() - .unwrap(), ], ) .select_ordinal() @@ -617,9 +596,6 @@ mod tests { "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" .parse() .unwrap(), - "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" - .parse() - .unwrap(), ], ) .select_ordinal() @@ -650,9 +626,6 @@ mod tests { "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" .parse() .unwrap(), - "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l" - .parse() - .unwrap(), ] ), Ok(Transaction { From 680ca4b312dee83277d62356000fd0e6e84ab25a Mon Sep 17 00:00:00 2001 From: raphjaph Date: Mon, 17 Oct 2022 21:02:34 +0200 Subject: [PATCH 4/9] removed change address slice --- src/subcommand/wallet/send.rs | 5 +- src/subcommand/wallet/transaction_builder.rs | 93 +++++++++----------- 2 files changed, 46 insertions(+), 52 deletions(-) diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 69297d4546..fddc643006 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -14,9 +14,8 @@ impl Send { index.index()?; let utxos = list_unspent(&options, &index)?.into_iter().collect(); - let change = [ - self.get_change_address(&client)?, - ]; + + let change = self.get_change_address(&client)?; let unsigned_transaction = TransactionBuilder::build_transaction(utxos, self.ordinal, self.address, change)?; diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index c0ca024679..688139cb82 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -25,7 +25,7 @@ impl std::error::Error for Error {} #[derive(Debug, PartialEq)] pub(crate) struct TransactionBuilder { - change: [Address; 1], + change: Address, inputs: Vec, ordinal: Ordinal, outputs: Vec<(Address, Amount)>, @@ -43,7 +43,7 @@ impl TransactionBuilder { ranges: BTreeMap>, ordinal: Ordinal, recipient: Address, - change: [Address; 1], + change: Address, ) -> Result { Self::new(ranges, ordinal, recipient, change) .select_ordinal()? @@ -56,7 +56,7 @@ impl TransactionBuilder { ranges: BTreeMap>, ordinal: Ordinal, recipient: Address, - change: [Address; 1], + change: Address, ) -> Self { Self { utxos: ranges.keys().cloned().collect(), @@ -100,9 +100,13 @@ impl TransactionBuilder { .sum::(); if output_total > Amount::from_sat(ordinal_offset + 2 * Self::TARGET_POSTAGE) { + assert_eq!( + self.outputs[0].0, self.recipient, + "invariant: first output is recipient" + ); self.outputs[0].1 = Amount::from_sat(Self::TARGET_POSTAGE); self.outputs.push(( - self.change[0].clone(), + self.change.clone(), output_total - Amount::from_sat(ordinal_offset + Self::TARGET_POSTAGE), )); } @@ -214,6 +218,15 @@ impl TransactionBuilder { } assert!(found, "invariant: ordinal is found in outputs"); + for output in &transaction.output { + if output.script_pubkey != self.change.script_pubkey() { + assert!( + output.value < 2 * Self::TARGET_POSTAGE, + "invariant: excess postage is stripped" + ); + } + } + Ok(transaction) } @@ -264,11 +277,9 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), - [ - "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" - .parse() - .unwrap(), - ], + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), ) .select_ordinal() .unwrap(); @@ -326,11 +337,9 @@ mod tests { recipient: "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), - change: [ - "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" - .parse() - .unwrap(), - ], + change: "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), inputs: vec![ "1111111111111111111111111111111111111111111111111111111111111111:1" .parse() @@ -438,11 +447,9 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), - [ - "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" - .parse() - .unwrap(), - ] + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), ), Ok(Transaction { version: 1, @@ -482,11 +489,9 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), - [ - "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" - .parse() - .unwrap(), - ] + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), ), Err(Error::ConsumedByFee(Ordinal(14900))) ) @@ -508,11 +513,9 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), - [ - "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" - .parse() - .unwrap(), - ], + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), ) .build() .ok(); @@ -534,11 +537,9 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), - [ - "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" - .parse() - .unwrap(), - ], + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), ) .build() .ok(); @@ -560,11 +561,9 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), - [ - "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" - .parse() - .unwrap(), - ], + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), ) .select_ordinal() .unwrap(); @@ -592,11 +591,9 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), - [ - "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" - .parse() - .unwrap(), - ], + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), ) .select_ordinal() .unwrap(); @@ -622,11 +619,9 @@ mod tests { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse() .unwrap(), - [ - "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" - .parse() - .unwrap(), - ] + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), ), Ok(Transaction { version: 1, From 6313c1dc00e57f1148e126e1e3e4ee3ea17a6ec9 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Mon, 17 Oct 2022 21:12:33 +0200 Subject: [PATCH 5/9] added invariant test --- src/subcommand/wallet/transaction_builder.rs | 52 ++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 688139cb82..e51f49da02 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -653,4 +653,56 @@ mod tests { }) ) } + + #[test] + #[should_panic(expected = "invariant: excess postage is stripped")] + fn invariant_excess_postage_is_stripped() { + let utxos = vec![( + "1111111111111111111111111111111111111111111111111111111111111111:1" + .parse() + .unwrap(), + vec![(0, 1_000_000)], + )]; + + pretty_assert_eq!( + TransactionBuilder::new( + utxos.into_iter().collect(), + Ordinal(0), + "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" + .parse() + .unwrap(), + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), + ).select_ordinal().unwrap().build(), + Ok(Transaction { + version: 1, + lock_time: PackedLockTime::ZERO, + input: vec![TxIn { + previous_output: "1111111111111111111111111111111111111111111111111111111111111111:1" + .parse() + .unwrap(), + script_sig: Script::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::new(), + },], + output: vec![ + TxOut { + value: TransactionBuilder::TARGET_POSTAGE, + script_pubkey: "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" + .parse::
() + .unwrap() + .script_pubkey(), + }, + TxOut { + value: 1_000_000 - TransactionBuilder::TARGET_POSTAGE - 226, + script_pubkey: "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse::
() + .unwrap() + .script_pubkey(), + } + ], + }) + ) + } } From 580f371c2c401f0256df5401937f4edf32a7c6c4 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Mon, 17 Oct 2022 21:17:36 +0200 Subject: [PATCH 6/9] added invariant test --- src/subcommand/wallet/send.rs | 12 ++++-------- src/subcommand/wallet/transaction_builder.rs | 10 +++++++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index fddc643006..fa15275149 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -1,4 +1,4 @@ -use {super::*, bitcoincore_rpc::Client, transaction_builder::TransactionBuilder}; +use {super::*, transaction_builder::TransactionBuilder}; #[derive(Debug, Parser)] pub(crate) struct Send { @@ -15,7 +15,9 @@ impl Send { let utxos = list_unspent(&options, &index)?.into_iter().collect(); - let change = self.get_change_address(&client)?; + let change = client + .call("getrawchangeaddress", &[]) + .context("Could not get change addresses from wallet")?; let unsigned_transaction = TransactionBuilder::build_transaction(utxos, self.ordinal, self.address, change)?; @@ -29,10 +31,4 @@ impl Send { println!("{txid}"); Ok(()) } - - fn get_change_address(&self, client: &Client) -> Result
{ - client - .call("getrawchangeaddress", &[]) - .context("Could not get change addresses from wallet") - } } diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index e51f49da02..71eecaf1aa 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -38,6 +38,7 @@ type Result = std::result::Result; impl TransactionBuilder { const TARGET_POSTAGE: u64 = 10_000; + const MAX_POSTAGE: u64 = 2 * Self::TARGET_POSTAGE; pub(crate) fn build_transaction( ranges: BTreeMap>, @@ -99,7 +100,7 @@ impl TransactionBuilder { .map(|(_address, amount)| *amount) .sum::(); - if output_total > Amount::from_sat(ordinal_offset + 2 * Self::TARGET_POSTAGE) { + if output_total > Amount::from_sat(ordinal_offset + Self::MAX_POSTAGE) { assert_eq!( self.outputs[0].0, self.recipient, "invariant: first output is recipient" @@ -221,7 +222,7 @@ impl TransactionBuilder { for output in &transaction.output { if output.script_pubkey != self.change.script_pubkey() { assert!( - output.value < 2 * Self::TARGET_POSTAGE, + output.value < Self::MAX_POSTAGE, "invariant: excess postage is stripped" ); } @@ -674,7 +675,10 @@ mod tests { "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" .parse() .unwrap(), - ).select_ordinal().unwrap().build(), + ) + .select_ordinal() + .unwrap() + .build(), Ok(Transaction { version: 1, lock_time: PackedLockTime::ZERO, From bdb7bbc19edf65c456b509a021a19fe216f048b5 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Mon, 17 Oct 2022 21:22:32 +0200 Subject: [PATCH 7/9] add invariant --- src/subcommand/wallet/transaction_builder.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 71eecaf1aa..b32c796b53 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -100,11 +100,14 @@ impl TransactionBuilder { .map(|(_address, amount)| *amount) .sum::(); + assert_eq!(self.outputs.len(), 1, "invariant: only one output"); + + assert_eq!( + self.outputs[0].0, self.recipient, + "invariant: first output is recipient" + ); + if output_total > Amount::from_sat(ordinal_offset + Self::MAX_POSTAGE) { - assert_eq!( - self.outputs[0].0, self.recipient, - "invariant: first output is recipient" - ); self.outputs[0].1 = Amount::from_sat(Self::TARGET_POSTAGE); self.outputs.push(( self.change.clone(), From ef20a3e511be463dbf3e38ecd5446f03ce35fd97 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Mon, 17 Oct 2022 21:26:04 +0200 Subject: [PATCH 8/9] simplified invariant test --- src/subcommand/wallet/transaction_builder.rs | 54 +++++--------------- 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index b32c796b53..cab785d17b 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -668,48 +668,18 @@ mod tests { vec![(0, 1_000_000)], )]; - pretty_assert_eq!( - TransactionBuilder::new( - utxos.into_iter().collect(), - Ordinal(0), - "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" - .parse() - .unwrap(), - "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" - .parse() - .unwrap(), - ) - .select_ordinal() - .unwrap() - .build(), - Ok(Transaction { - version: 1, - lock_time: PackedLockTime::ZERO, - input: vec![TxIn { - previous_output: "1111111111111111111111111111111111111111111111111111111111111111:1" - .parse() - .unwrap(), - script_sig: Script::new(), - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - witness: Witness::new(), - },], - output: vec![ - TxOut { - value: TransactionBuilder::TARGET_POSTAGE, - script_pubkey: "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" - .parse::
() - .unwrap() - .script_pubkey(), - }, - TxOut { - value: 1_000_000 - TransactionBuilder::TARGET_POSTAGE - 226, - script_pubkey: "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" - .parse::
() - .unwrap() - .script_pubkey(), - } - ], - }) + TransactionBuilder::new( + utxos.into_iter().collect(), + Ordinal(0), + "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" + .parse() + .unwrap(), + "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww" + .parse() + .unwrap(), ) + .select_ordinal() + .unwrap() + .build().unwrap(); } } From 07ec9666d5ad4ddaea0a8dab111614bf38c209a2 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Mon, 17 Oct 2022 21:28:42 +0200 Subject: [PATCH 9/9] fmt --- src/subcommand/wallet/transaction_builder.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index cab785d17b..9bbe3ebbea 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -680,6 +680,7 @@ mod tests { ) .select_ordinal() .unwrap() - .build().unwrap(); + .build() + .unwrap(); } }