summaryrefslogtreecommitdiff
path: root/btc-wire
diff options
context:
space:
mode:
Diffstat (limited to 'btc-wire')
-rw-r--r--btc-wire/src/bin/test.rs82
-rw-r--r--btc-wire/src/info.rs8
-rw-r--r--btc-wire/src/lib.rs38
-rw-r--r--btc-wire/src/main.rs191
-rw-r--r--btc-wire/src/rpc.rs36
5 files changed, 243 insertions, 112 deletions
diff --git a/btc-wire/src/bin/test.rs b/btc-wire/src/bin/test.rs
index f44495f..442e1a6 100644
--- a/btc-wire/src/bin/test.rs
+++ b/btc-wire/src/bin/test.rs
@@ -7,7 +7,6 @@ use btc_wire::{
rpc::{self, BtcRpc, Category},
rpc_utils::{default_data_dir, CLIENT, WIRE},
test::rand_key,
- BounceErr,
};
use owo_colors::OwoColorize;
@@ -160,7 +159,7 @@ pub fn main() {
// Send metadata
let msg = "J'aime le chocolat".as_bytes();
let id = client_rpc
- .send_op_return(&wire_addr, &test_amount, msg)
+ .send_op_return(&wire_addr, &test_amount, msg, false)
.unwrap();
// Check in mempool
assert!(
@@ -204,7 +203,7 @@ pub fn main() {
let before = client_rpc.get_balance().unwrap();
let send_id = client_rpc.send(&wire_addr, &test_amount, false).unwrap();
wait_for_tx(&mut client_rpc, &mut reserve_rpc, &[send_id]);
- let bounce_id = wire_rpc.bounce(&send_id, &bounce_fee).unwrap();
+ let bounce_id = wire_rpc.bounce(&send_id, &bounce_fee, &[]).unwrap();
wait_for_tx(&mut wire_rpc, &mut reserve_rpc, &[bounce_id]);
let bounce_tx_fee = wire_rpc.get_tx(&bounce_id).unwrap().details[0]
.fee
@@ -227,27 +226,65 @@ pub fn main() {
.send(&wire_addr, &Amount::from_sat(294), false)
.unwrap();
wait_for_tx(&mut client_rpc, &mut reserve_rpc, &[send_id]);
- assert!(match wire_rpc.bounce(&send_id, &bounce_fee) {
- Ok(_) => false,
- Err(err) => match err {
- BounceErr::AmountLessThanFee => true,
- _ => false,
- },
+ assert!(match wire_rpc.bounce(&send_id, &bounce_fee, &[]) {
+ Err(rpc::Error::RPC {
+ code: rpc::ErrorCode::RpcWalletInsufficientFunds,
+ msg: _,
+ }) => true,
+ _ => false,
});
});
+ runner.test("Bounce simple with metadata", || {
+ let before = client_rpc.get_balance().unwrap();
+ let send_id = client_rpc.send(&wire_addr, &test_amount, false).unwrap();
+ wait_for_tx(&mut client_rpc, &mut reserve_rpc, &[send_id]);
+ let bounce_id = wire_rpc
+ .bounce(&send_id, &bounce_fee, &[12, 34, 56, 78])
+ .unwrap();
+ wait_for_tx(&mut wire_rpc, &mut reserve_rpc, &[bounce_id]);
+ let bounce_tx_fee = wire_rpc.get_tx(&bounce_id).unwrap().details[0]
+ .fee
+ .unwrap()
+ .abs()
+ .to_unsigned()
+ .unwrap();
+ let send_tx_fee = client_rpc.get_tx(&send_id).unwrap().details[0]
+ .fee
+ .unwrap()
+ .abs()
+ .to_unsigned()
+ .unwrap();
+ wire_rpc.get_tx_op_return(&bounce_id).unwrap();
+ let after = client_rpc.get_balance().unwrap();
+ assert!(before >= after);
+ assert_eq!(before - after, bounce_tx_fee + bounce_fee + send_tx_fee);
+ });
+ runner.test("Bounce minimal amount with metadata", || {
+ let send_id = client_rpc
+ .send(&wire_addr, &Amount::from_sat(294), false)
+ .unwrap();
+ wait_for_tx(&mut client_rpc, &mut reserve_rpc, &[send_id]);
+ assert!(
+ match wire_rpc.bounce(&send_id, &bounce_fee, &[12, 34, 56]) {
+ Err(rpc::Error::RPC {
+ code: rpc::ErrorCode::RpcWalletError,
+ ..
+ }) => true,
+ _ => false,
+ }
+ );
+ });
runner.test("Bounce too small amount", || {
let send_id = client_rpc
.send(&wire_addr, &(Amount::from_sat(294) + bounce_fee), false)
.unwrap();
wait_for_tx(&mut client_rpc, &mut reserve_rpc, &[send_id]);
-
- assert!(match wire_rpc.bounce(&send_id, &bounce_fee) {
- Ok(_) => false,
- Err(err) => match err {
- BounceErr::RPC(rpc::Error::RPC { code, .. }) =>
- code == rpc::ErrorCode::RpcWalletInsufficientFunds,
- _ => false,
- },
+ assert!(match wire_rpc.bounce(&send_id, &bounce_fee, &[]) {
+ Err(rpc::Error::RPC {
+ code: rpc::ErrorCode::RpcWalletInsufficientFunds,
+ ..
+ }) => true,
+ _ => false,
});
});
runner.test("Bounce complex", || {
@@ -260,7 +297,12 @@ pub fn main() {
.into_iter()
.map(|addresses| {
client_rpc
- .send_custom(&[], addresses.iter().map(|addr| (addr, &test_amount)), None)
+ .send_custom(
+ &[],
+ addresses.iter().map(|addr| (addr, &test_amount)),
+ None,
+ false,
+ )
.unwrap()
})
.collect();
@@ -268,10 +310,10 @@ pub fn main() {
let before = client_rpc.get_balance().unwrap();
// Send a transaction with multiple input from multiple transaction of different outputs len
let send_id = client_rpc
- .send_custom(&txs, [(&wire_addr, &(test_amount * 3))], None)
+ .send_custom(&txs, [(&wire_addr, &(test_amount * 3))], None, false)
.unwrap();
wait_for_tx(&mut client_rpc, &mut reserve_rpc, &[send_id]);
- let bounce_id = wire_rpc.bounce(&send_id, &bounce_fee).unwrap();
+ let bounce_id = wire_rpc.bounce(&send_id, &bounce_fee, &[]).unwrap();
wait_for_tx(&mut wire_rpc, &mut reserve_rpc, &[bounce_id]);
let after = client_rpc.get_balance().unwrap();
let bounce_tx_fee = wire_rpc.get_tx(&bounce_id).unwrap().details[0]
diff --git a/btc-wire/src/info.rs b/btc-wire/src/info.rs
index db1a941..cc61b04 100644
--- a/btc-wire/src/info.rs
+++ b/btc-wire/src/info.rs
@@ -17,7 +17,7 @@ pub enum DecodeErr {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Info {
Transaction { wtid: [u8; 32], url: Url },
- Bounce { id: Txid },
+ Bounce { bounced: Txid },
}
// We leave a potential special meaning for u8::MAX
@@ -34,7 +34,7 @@ pub fn encode_info(info: &Info) -> Vec<u8> {
buffer.extend_from_slice(&packed);
return buffer;
}
- Info::Bounce { id } => {
+ Info::Bounce { bounced: id } => {
buffer.push(BOUNCE_BYTE);
buffer.extend_from_slice(id.as_ref());
}
@@ -63,7 +63,7 @@ pub fn decode_info(bytes: &[u8]) -> Result<Info, DecodeErr> {
})
}
BOUNCE_BYTE => Ok(Info::Bounce {
- id: Txid::from_slice(&bytes[1..])?,
+ bounced: Txid::from_slice(&bytes[1..])?,
}),
unknown => Err(DecodeErr::UnknownFirstByte(unknown)),
}
@@ -100,7 +100,7 @@ mod test {
for _ in 0..4 {
let id = rand_key();
let info = Info::Bounce {
- id: Txid::from_slice(&id).unwrap(),
+ bounced: Txid::from_slice(&id).unwrap(),
};
let encode = encode_info(&info);
let decoded = decode_info(&encode).unwrap();
diff --git a/btc-wire/src/lib.rs b/btc-wire/src/lib.rs
index 2317fa2..ed7d697 100644
--- a/btc-wire/src/lib.rs
+++ b/btc-wire/src/lib.rs
@@ -5,21 +5,11 @@ use rpc::{BtcRpc, Category, TransactionFull};
use rpc_utils::{segwit_min_amount, sender_address};
use segwit::{decode_segwit_msg, encode_segwit_key};
+pub mod config;
pub mod rpc;
pub mod rpc_utils;
pub mod segwit;
pub mod test;
-pub mod config;
-
-#[derive(Debug, thiserror::Error)]
-pub enum BounceErr {
- #[error("Expected 'receive' transaction")]
- NotAReceiveTransaction,
- #[error("Transaction amount less than bounce fee")]
- AmountLessThanFee,
- #[error(transparent)]
- RPC(#[from] rpc::Error),
-}
#[derive(Debug, thiserror::Error)]
pub enum GetSegwitErr {
@@ -92,10 +82,11 @@ impl BtcRpc {
to: &Address,
amount: &Amount,
metadata: &[u8],
+ subtract_fee: bool,
) -> rpc::Result<Txid> {
assert!(metadata.len() > 0, "No medatata");
assert!(metadata.len() <= 80, "Max 80 bytes");
- self.send_custom(&[], [(to, amount)], Some(metadata))
+ self.send_custom(&[], [(to, amount)], Some(metadata), subtract_fee)
}
/// Get detailed information about an in-wallet transaction and its op_return metadata
@@ -124,22 +115,25 @@ impl BtcRpc {
/// There is no reliable way to bounce a transaction as you cannot know if the addresses
/// used are shared or come from a third-party service. We only send back to the first input
/// address as a best-effort gesture.
- pub fn bounce(&mut self, id: &Txid, bounce_fee: &Amount) -> Result<Txid, BounceErr> {
+ pub fn bounce(
+ &mut self,
+ id: &Txid,
+ bounce_fee: &Amount,
+ metadata: &[u8],
+ ) -> Result<Txid, rpc::Error> {
let full = self.get_tx(id)?;
let detail = &full.details[0];
- if detail.category != Category::Receive {
- return Err(BounceErr::NotAReceiveTransaction);
- }
+ assert!(detail.category == Category::Receive);
let amount = detail.amount.to_unsigned().unwrap();
- if amount <= *bounce_fee {
- return Err(BounceErr::AmountLessThanFee);
- }
-
let sender = sender_address(self, &full)?;
- let bounce_amount = amount - *bounce_fee;
+ let bounce_amount = Amount::from_sat(amount.as_sat().saturating_sub(bounce_fee.as_sat()));
// Send refund making recipient pay the transaction fees
- let id = self.send(&sender, &bounce_amount, true)?;
+ let id = if metadata.is_empty() {
+ self.send(&sender, &bounce_amount, true)?
+ } else {
+ self.send_op_return(&sender, &bounce_amount, metadata, true)?
+ };
Ok(id)
}
}
diff --git a/btc-wire/src/main.rs b/btc-wire/src/main.rs
index e728b7f..29ad7ca 100644
--- a/btc-wire/src/main.rs
+++ b/btc-wire/src/main.rs
@@ -1,7 +1,7 @@
use bitcoin::{hashes::Hash, Address, Amount as BtcAmount, BlockHash, SignedAmount, Txid};
use btc_wire::{
config::BitcoinConfig,
- rpc::{BtcRpc, Category},
+ rpc::{self, BtcRpc, Category, ErrorCode},
rpc_utils::{default_data_dir, sender_address},
segwit::DecodeSegWitErr,
GetOpReturnErr, GetSegwitErr,
@@ -24,7 +24,7 @@ use url::Url;
use crate::{
fail_point::fail_point,
info::{encode_info, Info},
- status::TxStatus,
+ status::{BounceStatus, TxStatus},
};
mod fail_point;
@@ -68,7 +68,7 @@ fn last_hash(db: &mut Client) -> Result<Option<BlockHash>, postgres::Error> {
/// Listen for new proposed transactions and announce them on the bitcoin network
fn worker(mut rpc: AutoReconnectRPC, mut db: AutoReconnectSql, config: &Config) {
// Send a transaction on the blockchain, return true if more transactions with the same status remains
- fn send_tx(
+ fn send(
db: &mut Client,
rpc: &mut BtcRpc,
status: TxStatus,
@@ -93,7 +93,7 @@ fn worker(mut rpc: AutoReconnectRPC, mut db: AutoReconnectSql, config: &Config)
let metadata = encode_info(&info);
fail_point("Skip send_op_return", 0.2)?;
- match rpc.send_op_return(&addr, &amount, &metadata) {
+ match rpc.send_op_return(&addr, &amount, &metadata, false) {
Ok(tx_id) => {
fail_point("Fail update db", 0.2)?;
tx.execute(
@@ -101,7 +101,7 @@ fn worker(mut rpc: AutoReconnectRPC, mut db: AutoReconnectSql, config: &Config)
&[&(TxStatus::Sent as i16), &tx_id.as_ref(), &id],
)?;
let amount = btc_amount_to_taler_amount(&amount.to_signed().unwrap());
- info!("SEND >> {} {} in {}", addr, amount, tx_id);
+ info!("send {} {} in {}", addr, amount, tx_id);
}
Err(e) => {
info!("sender: RPC - {}", e);
@@ -116,6 +116,54 @@ fn worker(mut rpc: AutoReconnectRPC, mut db: AutoReconnectSql, config: &Config)
Ok(row.is_some())
}
+ // Bounce a transaction on the blockchain, return true if more bounce with the same status remains
+ fn bounce(
+ db: &mut Client,
+ rpc: &mut BtcRpc,
+ status: BounceStatus,
+ fee: &BtcAmount,
+ ) -> Result<bool, Box<dyn std::error::Error>> {
+ assert!(status == BounceStatus::Delayed || status == BounceStatus::Requested);
+ let mut tx = db.transaction()?;
+ // We lock the row with FOR UPDATE to prevent sending same transaction multiple time
+ let row = tx.query_opt(
+ "SELECT id, bounced FROM bounce WHERE status=$1 LIMIT 1 FOR UPDATE",
+ &[&(status as i16)],
+ )?;
+ if let Some(row) = &row {
+ let id: i32 = row.get(0);
+ let bounced: Txid = Txid::from_slice(row.get(1))?;
+ let info = Info::Bounce { bounced };
+ let metadata = encode_info(&info);
+
+ fail_point("Skip send_op_return", 0.2)?;
+ match rpc.bounce(&bounced, &fee, &metadata) {
+ Ok(it) => {
+ info!("bounce {} in {}", &bounced, &it);
+ tx.execute(
+ "UPDATE bounce SET txid = $1, status = $2 WHERE id = $3",
+ &[&it.as_ref(), &(BounceStatus::Sent as i16), &id],
+ )?;
+ }
+ Err(err) => match err {
+ rpc::Error::RPC {
+ code: ErrorCode::RpcWalletInsufficientFunds | ErrorCode::RpcWalletError,
+ msg,
+ } => {
+ info!("ignore bounce {} because {}", &bounced, msg);
+ tx.execute(
+ "UPDATE bounce SET status = $1 WHERE id = $2",
+ &[&(BounceStatus::Ignored as i16), &id],
+ )?;
+ }
+ _ => Err(err)?,
+ },
+ }
+ tx.commit()?;
+ }
+ Ok(row.is_some())
+ }
+
// TODO check if transactions are abandoned
let mut failed = false;
@@ -143,29 +191,15 @@ fn worker(mut rpc: AutoReconnectRPC, mut db: AutoReconnectSql, config: &Config)
// As we are now in sync with the blockchain if a transaction is in requested or delayed state it have not been sent
// Send delayed transactions
- while send_tx(db, rpc, TxStatus::Delayed)? {}
+ while send(db, rpc, TxStatus::Delayed)? {}
// Send requested transactions
- while send_tx(db, rpc, TxStatus::Requested)? {}
+ while send(db, rpc, TxStatus::Requested)? {}
- // Check if already bounced
- /*if nb > 0 && false {
- // We do not handle failures, bouncing is done in a best effort manner
- match rpc.bounce(&id, &BtcAmount::from_sat(config.bounce_fee)) {
- Ok(it) => {
- info!("bounce {} in {}", &id, &it);
- db.execute(
- "UPDATE bounce SET txid = $1 WHERE bounced = $2",
- &[&it.as_ref(), &id.as_ref()],
- )?;
- }
- Err(err) => match err {
- BounceErr::AmountLessThanFee => { /* Ignore */ }
- BounceErr::NotAReceiveTransaction | BounceErr::RPC(_) => {
- Err(err)?
- }
- },
- }
- }*/
+ let bounce_fee = BtcAmount::from_sat(config.bounce_fee);
+ // Send delayed bounce
+ while bounce(db, rpc, BounceStatus::Delayed, &bounce_fee)? {}
+ // Send requested bounce
+ while bounce(db, rpc, BounceStatus::Requested, &bounce_fee)? {}
Ok(())
})();
@@ -186,22 +220,22 @@ fn sync_chain(
) -> Result<(), Box<dyn std::error::Error>> {
// Get stored last_hash
let last_hash = last_hash(db)?;
- let confirmation = config.confirmation;
+ let min_confirmations = config.confirmation;
// Get a set of transactions ids to parse
- let (txs, lastblock): (HashMap<Txid, Category>, BlockHash) = {
+ let (txs, lastblock): (HashMap<Txid, (Category, i32)>, BlockHash) = {
// Get all transactions made since this block
- let list = rpc.list_since_block(last_hash.as_ref(), confirmation, true)?;
+ let list = rpc.list_since_block(last_hash.as_ref(), min_confirmations, true)?;
// Only keep ids and category
- let txs: HashMap<Txid, Category> = list
+ let txs = list
.transactions
.into_iter()
- .map(|tx| (tx.txid, tx.category))
+ .map(|tx| (tx.txid, (tx.category, tx.confirmations)))
.collect();
(txs, list.lastblock)
};
- for (id, category) in txs {
+ for (id, (category, confirmations)) in txs {
match category {
Category::Send => {
match rpc.get_tx_op_return(&id) {
@@ -211,14 +245,15 @@ fn sync_chain(
Ok(info) => match info {
Info::Transaction { wtid, url } => {
let row = tx.query_opt(
- "SELECT status, id FROM tx_out WHERE wtid=$1 FOR UPDATE",
+ "SELECT id, status FROM tx_out WHERE wtid=$1 FOR UPDATE",
&[&wtid.as_ref()],
)?;
if let Some(row) = row {
- let status: i16 = row.get(0);
- let _id: i32 = row.get(1);
+ let _id: i32 = row.get(0);
+ let status: i16 = row.get(1);
let status: TxStatus =
TxStatus::try_from(status as u8).unwrap();
+ // TODO match
if status != TxStatus::Sent {
tx.execute(
"UPDATE tx_out SET status=$1 where id=$2",
@@ -235,7 +270,7 @@ fn sync_chain(
full.details[0].address.as_ref().unwrap();
let amount =
btc_amount_to_taler_amount(&full.amount);
- info!("SEND >> {} {} in {}", addr, amount, &id);
+ info!("send {} {} in {}", addr, amount, &id);
}
}
} else {
@@ -249,16 +284,53 @@ fn sync_chain(
OsRng.fill_bytes(&mut request_uid);
let nb = tx.execute(
"INSERT INTO tx_out (_date, amount, wtid, debit_acc, credit_acc, exchange_url, status, txid, request_uid) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (wtid) DO NOTHING",
- &[&date, &amount.to_string(), &wtid.as_ref(), &btc_payto_url(&debit_addr).as_ref(), &btc_payto_url(credit_addr).as_ref(), &config.base_url.as_ref(), &(TxStatus::Sent as i16), &id.as_ref(), &request_uid.as_ref()
- ],
- )?;
+ &[&date, &amount.to_string(), &wtid.as_ref(), &btc_payto_url(&debit_addr).as_ref(), &btc_payto_url(credit_addr).as_ref(), &config.base_url.as_ref(), &(TxStatus::Sent as i16), &id.as_ref(), &request_uid.as_ref()],
+ )?;
if nb > 0 {
- warn!("watcher: found an unregistered outgoing address {} {} in tx {}", crockford_base32_encode(&wtid), &url, id);
+ warn!(
+ "recovered {} {} in tx {}",
+ crockford_base32_encode(&wtid),
+ &url,
+ id
+ );
}
}
}
- Info::Bounce { .. } => {
- // TODO
+ Info::Bounce { bounced } => {
+ let row = tx.query_opt(
+ "SELECT id, status FROM bounce WHERE bounced=$1 FOR UPDATE",
+ &[&bounced.as_ref()],
+ )?;
+ if let Some(row) = row {
+ let _id: i32 = row.get(0);
+ let status: i16 = row.get(1);
+ let status: BounceStatus =
+ BounceStatus::try_from(status as u8).unwrap();
+ assert!(status != BounceStatus::Ignored); // TODO
+ if status != BounceStatus::Sent {
+ tx.execute(
+ "UPDATE bounce SET status=$1 where id=$2",
+ &[&(BounceStatus::Sent as i16), &_id],
+ )?;
+ if status == BounceStatus::Delayed
+ || status == BounceStatus::Requested
+ {
+ warn!(
+ "watcher: bounce {} have been recovered automatically",
+ _id
+ );
+ info!("bounce {} in {}", &bounced, &id);
+ }
+ }
+ } else {
+ let nb = tx.execute(
+ "INSERT INTO bounce (bounced, txid, status) VALUES ($1, $2, $3) ON CONFLICT (txid) DO NOTHING",
+ &[&bounced.as_ref(), &id.as_ref(), &(BounceStatus::Sent as i16)],
+ )?;
+ if nb > 0 {
+ info!("recovered bounce {} in {}", &bounced, &id);
+ }
+ }
}
},
Err(err) => warn!("send: decode-info {} - {}", id, err),
@@ -271,9 +343,9 @@ fn sync_chain(
},
}
}
- Category::Receive => match rpc.get_tx_segwit_key(&id) {
- Ok((full, reserve_pub)) => {
- if full.confirmations >= confirmation as i32 {
+ Category::Receive if confirmations >= min_confirmations as i32 => {
+ match rpc.get_tx_segwit_key(&id) {
+ Ok((full, reserve_pub)) => {
let debit_addr = sender_address(rpc, &full)?;
let credit_addr = full.details[0].address.as_ref().unwrap();
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(full.time);
@@ -282,22 +354,25 @@ fn sync_chain(
&date, &amount.to_string(), &reserve_pub.as_ref(), &btc_payto_url(&debit_addr).as_ref(), &btc_payto_url(credit_addr).as_ref()
])?;
if nb > 0 {
- info!("{} << {} {} in {}", &debit_addr, &credit_addr, &amount, &id);
+ info!(
+ "receive {} << {} {} in {}",
+ &debit_addr, &credit_addr, &amount, &id
+ );
}
}
+ Err(err) => match err {
+ GetSegwitErr::Decode(
+ DecodeSegWitErr::MissingSegWitAddress | DecodeSegWitErr::NoMagicIdMatch,
+ ) => {
+ // Request a bounce
+ db.execute("INSERT INTO bounce (bounced) VALUES ($1) ON CONFLICT (bounced) DO NOTHING", &[&id.as_ref()])?;
+ }
+ err => warn!("receive: {} {}", id, err),
+ },
}
- Err(err) => match err {
- GetSegwitErr::Decode(
- DecodeSegWitErr::MissingSegWitAddress | DecodeSegWitErr::NoMagicIdMatch,
- ) => {
- // Request a bounce
- db.execute("INSERT INTO bounce (bounced) VALUES ($1) ON CONFLICT (bounced) DO NOTHING", &[&id.as_ref()])?;
- }
- err => warn!("receive: {} {}", id, err),
- },
- },
- Category::Generate | Category::Immature | Category::Orphan => {
- // Ignore coinbase transactions
+ }
+ _ => {
+ // Ignore coinbase and unconfirmed send transactions
}
}
}
diff --git a/btc-wire/src/rpc.rs b/btc-wire/src/rpc.rs
index 8121e1e..bac9a6b 100644
--- a/btc-wire/src/rpc.rs
+++ b/btc-wire/src/rpc.rs
@@ -206,7 +206,13 @@ impl BtcRpc {
inputs: impl IntoIterator<Item = &'a Txid>,
outputs: impl IntoIterator<Item = (&'b Address, &'c Amount)>,
data: Option<&[u8]>,
+ subtract_fee: bool,
) -> Result<Txid> {
+ let mut outputs: Vec<Value> = outputs
+ .into_iter()
+ .map(|(addr, amount)| json!({&addr.to_string(): amount.as_btc()}))
+ .collect();
+ let len = outputs.len();
let hex: String = self.call(
"createrawtransaction",
&[
@@ -218,18 +224,26 @@ impl BtcRpc {
.collect(),
),
Value::Array({
- let mut vec: Vec<Value> = outputs
- .into_iter()
- .map(|(addr, amount)| json!({&addr.to_string(): amount.as_btc()}))
- .collect();
if let Some(data) = data {
- vec.push(json!({ "data".to_string(): data.to_hex() }));
+ outputs.push(json!({ "data".to_string(): data.to_hex() }));
}
- vec
+ outputs
}),
],
)?;
- let funded: HexWrapper = self.call("fundrawtransaction", &[hex])?;
+ let funded: HexWrapper = self.call(
+ "fundrawtransaction",
+ &(
+ hex,
+ FundOption {
+ subtract_fee_from_outputs: if subtract_fee {
+ (0..len).into_iter().collect()
+ } else {
+ vec![]
+ },
+ },
+ ),
+ )?;
let signed: HexWrapper = self.call("signrawtransactionwithwallet", &[&funded.hex])?;
self.call("sendrawtransaction", &[&signed.hex])
}
@@ -252,7 +266,13 @@ impl BtcRpc {
}
}
-#[derive(Debug, serde::Deserialize, serde::Serialize)]
+#[derive(Debug, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct FundOption {
+ pub subtract_fee_from_outputs: Vec<usize>,
+}
+
+#[derive(Debug, serde::Deserialize)]
pub struct Wallet {
pub name: String,
pub warning: Option<String>,