diff options
author | Antoine A <> | 2021-12-28 19:43:39 +0100 |
---|---|---|
committer | Antoine A <> | 2021-12-28 19:43:39 +0100 |
commit | 80005a6b383eeaf4593a25173727ba5c03fa2e74 (patch) | |
tree | 3f629166073fd747fc294f597b77199375f7c506 | |
parent | 976967b98ec4120382e981f0d029cca30d68c908 (diff) | |
download | depolymerization-80005a6b383eeaf4593a25173727ba5c03fa2e74.tar.gz depolymerization-80005a6b383eeaf4593a25173727ba5c03fa2e74.tar.bz2 depolymerization-80005a6b383eeaf4593a25173727ba5c03fa2e74.zip |
btc-wire: add and test bouncing
-rw-r--r-- | btc-wire/src/bin/test.rs | 82 | ||||
-rw-r--r-- | btc-wire/src/info.rs | 8 | ||||
-rw-r--r-- | btc-wire/src/lib.rs | 38 | ||||
-rw-r--r-- | btc-wire/src/main.rs | 191 | ||||
-rw-r--r-- | btc-wire/src/rpc.rs | 36 | ||||
-rw-r--r-- | script/setup.sh | 22 | ||||
-rw-r--r-- | script/test_btc_fail.sh | 38 | ||||
-rw-r--r-- | script/test_btc_stress.sh | 63 | ||||
-rw-r--r-- | script/test_btc_wire.sh | 16 | ||||
-rw-r--r-- | script/test_recover_db.sh | 12 | ||||
-rw-r--r-- | test.conf | 2 |
11 files changed, 352 insertions, 156 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>, diff --git a/script/setup.sh b/script/setup.sh index 827ff56..5ef9d1e 100644 --- a/script/setup.sh +++ b/script/setup.sh @@ -46,7 +46,6 @@ function setup_btc() { WIRE=`$BTC_CLI -rpcwallet=wire getnewaddress` mine_btc 101 $BTC_CLI -rpcwallet=reserve sendtoaddress $CLIENT 10 > /dev/null - $BTC_CLI -rpcwallet=reserve sendtoaddress $WIRE 1 > /dev/null mine_btc } @@ -67,10 +66,14 @@ function next_btc() { # Check client and wire balance function check_balance() { - CLIENT_BALANCE=`$BTC_CLI -rpcwallet=client getbalance` - WIRE_BALANCE=`$BTC_CLI -rpcwallet=wire getbalance` - if [ "$CLIENT_BALANCE" != "$1" ] || [ "$WIRE_BALANCE" != "${2:-$WIRE_BALANCE}" ]; then - echo "expected: client $1 wire ${2:-$WIRE_BALANCE} got: client $CLIENT_BALANCE wire $WIRE_BALANCE" + local CLIENT_BALANCE=`$BTC_CLI -rpcwallet=client getbalance` + local WIRE_BALANCE=`$BTC_CLI -rpcwallet=wire getbalance` + local CLIENT="${1:-*}" + if [ "$1" == "*" ]; then + local CLIENT="$CLIENT_BALANCE" + fi + if [ "$CLIENT_BALANCE" != "$CLIENT" ] || [ "$WIRE_BALANCE" != "${2:-$WIRE_BALANCE}" ]; then + echo "expected: client $CLIENT wire ${2:-$WIRE_BALANCE} got: client $CLIENT_BALANCE wire $WIRE_BALANCE" exit 1 fi } @@ -109,16 +112,17 @@ function gateway() { # usage: check_delta endpoint nb_txs amount_sequence function check_delta() { ALL=`curl -s ${BANK_ENDPOINT}history/$1` + PRE=${3:-0.0000} for n in `$2`; do - if ! `echo $ALL | grep BTC:0.0000$n > /dev/null`; then - echo -n " missing tx with amount: BTC:0.0000$n" + if ! `echo $ALL | grep BTC:$PRE$n > /dev/null`; then + echo -n " missing tx with amount: BTC:$PRE$n" return 1 fi done - NB=`echo $ALL | grep -o BTC:0.0000 | wc -l` + NB=`echo $ALL | grep -o BTC:$PRE | wc -l` EXPECTED=`$2 | wc -w` if [ "$EXPECTED" != "$NB" ]; then echo -n " expected: $EXPECTED txs found $NB" return 1 fi -}
\ No newline at end of file +} diff --git a/script/test_btc_fail.sh b/script/test_btc_fail.sh index 3602364..c9fa930 100644 --- a/script/test_btc_fail.sh +++ b/script/test_btc_fail.sh @@ -35,26 +35,22 @@ echo "" SEQ="seq 10 40" -function check() { - check_delta "$1?delta=-100" "$SEQ" -} - echo "----- Handle incoming -----" echo -n "Making wire transfer to exchange:" for n in `$SEQ`; do - btc-wire-cli -d $BTC_DIR transfer 0.0000$n + btc-wire-cli -d $BTC_DIR transfer 0.000$n mine_btc # Mine transactions done next_btc # Trigger btc_wire echo " OK" echo -n "Requesting exchange incoming transaction list:" -check incoming +check_delta "incoming?delta=-100" "$SEQ" "0.000" echo " OK" echo -n "Check balance:" -check_balance 9.99897979 1.00077500 +check_balance 9.99200479 0.00775000 echo " OK" echo "----- Handle outgoing -----" @@ -66,14 +62,34 @@ for n in `$SEQ`; do -C payto://bitcoin/$CLIENT \ -a BTC:0.0000$n > /dev/null done -sleep 15 +sleep 20 mine_btc # Mine transactions echo " OK" echo -n "Requesting exchange outgoing transaction list:" -check outgoing +check_delta "outgoing?delta=-100" "$SEQ" +echo " OK" + +echo -n "Check balance:" +check_balance 9.99277979 0.00691331 +echo " OK" + +echo "----- Handle bounce -----" + +echo -n "Clear wire wallet:" +$BTC_CLI -rpcwallet=wire sendtoaddress $CLIENT `$BTC_CLI -rpcwallet=wire getbalance` "" "" true > /dev/null +echo " OK" + +echo -n "Making incomplete wire transfer to exchange:" +for n in `$SEQ`; do + $BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.000$n > /dev/null + mine_btc +done +sleep 20 echo " OK" echo -n "Check balance:" -check_balance 9.99975479 -echo " OK"
\ No newline at end of file +check_balance "*" 0.00031000 +echo " OK" + +echo "All tests passed"
\ No newline at end of file diff --git a/script/test_btc_stress.sh b/script/test_btc_stress.sh index b3b6ed7..6b6023f 100644 --- a/script/test_btc_stress.sh +++ b/script/test_btc_stress.sh @@ -28,22 +28,18 @@ init_btc echo "Init bitcoin regtest" setup_btc echo "Start btc-wire stressed" -stressed_btc_wire +btc_wire echo "Start gateway" gateway echo "" SEQ="seq 10 99" -function check() { - check_delta "$1?delta=-100" "$SEQ" -} - echo "----- Handle incoming -----" echo -n "Making wire transfer to exchange:" for n in `$SEQ`; do - btc-wire-cli -d $BTC_DIR transfer 0.0000$n + btc-wire-cli -d $BTC_DIR transfer 0.000$n mine_btc # Mine transactions done sleep 3 # Give time for btc_wire worker to process @@ -52,11 +48,11 @@ sleep 3 # Give time for btc_wire worker to process echo " OK" echo -n "Requesting exchange incoming transaction list:" -check incoming +check_delta "incoming?delta=-100" "$SEQ" "0.000" echo " OK" echo -n "Check balance:" -check_balance 9.99438310 1.00490500 +check_balance 9.95023810 0.04905000 echo " OK" echo "----- Handle outgoing -----" @@ -73,32 +69,71 @@ next_btc # Mine transactions echo " OK" echo -n "Requesting exchange outgoing transaction list:" -check outgoing +check_delta "outgoing?delta=-100" "$SEQ" echo " OK" echo -n "Check balance:" -check_balance 9.99928810 +check_balance 9.95514310 echo " OK" next_btc # Mine transactions + +echo "----- Recover DB -----" + +echo "Reset database" +reset_db # Clear database tables +mine_btc # Trigger worker +sleep 10 + +echo -n "Requesting exchange incoming transaction list:" +check_delta "incoming?delta=-100" "$SEQ" "0.000" +echo " OK" + +echo -n "Requesting exchange outgoing transaction list:" +check_delta "outgoing?delta=-100" "$SEQ" +echo " OK" + +echo -n "Check balance:" +# Balance should not have changed +check_balance 9.95514310 +echo " OK" + +echo "----- Handle bounce -----" + +echo -n "Clear wire wallet:" +$BTC_CLI -rpcwallet=wire sendtoaddress $CLIENT `$BTC_CLI -rpcwallet=wire getbalance` "" "" true > /dev/null +echo " OK" + +echo -n "Making incomplete wire transfer to exchange:" +for n in `$SEQ`; do + $BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.000$n > /dev/null + mine_btc +done +sleep 20 +echo " OK" + +echo -n "Check balance:" +check_balance "*" 0.00090000 +echo " OK" + echo "----- Recover DB -----" echo "Reset database" reset_db # Clear database tables mine_btc # Trigger worker -sleep 3 +sleep 10 echo -n "Requesting exchange incoming transaction list:" -check incoming +check_delta "incoming?delta=-100" "$SEQ" "0.000" echo " OK" echo -n "Requesting exchange outgoing transaction list:" -check outgoing +check_delta "outgoing?delta=-100" "$SEQ" echo " OK" echo -n "Check balance:" # Balance should not have changed -check_balance 9.99928810 +check_balance "*" 0.00090000 echo " OK" echo "All tests passed"
\ No newline at end of file diff --git a/script/test_btc_wire.sh b/script/test_btc_wire.sh index 7795a0e..442dcf3 100644 --- a/script/test_btc_wire.sh +++ b/script/test_btc_wire.sh @@ -33,25 +33,33 @@ echo "Start gateway" gateway echo "" -echo "---- Gateway API -----" +echo "---- Receive -----" echo -n "Making wire transfer to exchange:" btc-wire-cli -d $BTC_DIR transfer 0.00004 next_btc -check_balance 9.99995209 1.00004000 +check_balance 9.99995209 0.00004000 echo " OK" echo -n "Requesting exchange incoming transaction list:" taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -i | grep BTC:0.00004 > /dev/null echo " OK" +echo "---- Bounce-----" +$BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.00042 +next_btc +check_balance 9.99993883 0.00005000 + +echo "---- Send -----" + echo -n "Making wire transfer from exchange:" taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://bitcoin/$CLIENT \ -a BTC:0.00002 > /dev/null -next_btc -check_balance 9.99997209 1.00001801 +sleep 0.3 +mine_btc +check_balance 9.99995883 0.00002801 echo " OK" echo -n "Requesting exchange's outgoing transaction list:" diff --git a/script/test_recover_db.sh b/script/test_recover_db.sh index 3bfc9f9..d90a541 100644 --- a/script/test_recover_db.sh +++ b/script/test_recover_db.sh @@ -37,7 +37,7 @@ echo "----- With DB -----" echo "Making wire transfer to exchange:" btc-wire-cli -d $BTC_DIR transfer 0.000042 next_btc -check_balance 9.99995009 1.00004200 +check_balance 9.99995009 0.00004200 echo -n "Requesting exchange incoming transaction list:" taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -i | grep BTC:0.000042 > /dev/null && echo " OK" || echo " Failed" @@ -45,10 +45,12 @@ echo "----- Without DB -----" echo "Stop database" sudo service postgresql stop > /dev/null +echo "Making incomplete wire transfer to exchange" +$BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.00042 &> /dev/null echo "Making wire transfer to exchange:" btc-wire-cli -d $BTC_DIR transfer 0.00004 next_btc -check_balance 9.99990218 1.00008200 +check_balance 9.99948077 0.00050200 echo -n "Requesting exchange incoming transaction list:" taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -i 2>&1 | grep -q "504" && echo " OK" || echo " Failed" @@ -65,8 +67,8 @@ taler-exchange-wire-gateway-client \ -C payto://bitcoin/$CLIENT \ -a BTC:0.00002 > /dev/null sleep 1 -next_btc -check_balance 9.99992218 1.00006001 +mine_btc +check_balance 9.99990892 0.00007001 echo " OK" echo -n "Requesting exchange's outgoing transaction list:" @@ -95,6 +97,6 @@ done echo "" # Balance should not have changed -check_balance 9.99992218 1.00006001 +check_balance 9.99990892 0.00007001 echo "All tests passed"
\ No newline at end of file @@ -9,4 +9,4 @@ PAYTO = payto://bitcoin/bcrt1qgkgxkjj27g3f7s87mcvjjsghay7gh34cx39prj CONFIRMATION = 1 BTC_WALLET = wire BTC_DATA_DIR = ~/.bitcoin -BOUNCE_FEE = 0
\ No newline at end of file +BOUNCE_FEE = 1000
\ No newline at end of file |