commit 4b9821373b8f2ddf3d91fffbaf2d7ac08e0afef6
parent 8f7c61d1dd3c5116ad69f73a57fa54a52254f37f
Author: Antoine A <>
Date: Mon, 22 Nov 2021 16:25:50 +0100
Make refund work with complex transactions
Diffstat:
| M | src/bin/test.rs | | | 204 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------- |
| M | src/lib.rs | | | 40 | ++++++++++++---------------------------- |
| M | src/rpc_patch.rs | | | 32 | ++++++++++++++++++++++++++------ |
3 files changed, 198 insertions(+), 78 deletions(-)
diff --git a/src/bin/test.rs b/src/bin/test.rs
@@ -1,9 +1,10 @@
use core::panic;
-use std::{collections::HashSet, panic::AssertUnwindSafe};
+use std::{collections::HashSet, iter::repeat_with, panic::AssertUnwindSafe};
use bitcoincore_rpc::{
bitcoin::{Address, Amount, Txid},
json::GetTransactionResultDetailCategory as Category,
+ jsonrpc::serde_json::{json, Value},
Client, RpcApi,
};
use depolymerization::{
@@ -14,9 +15,11 @@ use depolymerization::{
};
use owo_colors::OwoColorize;
+const RESERVE: &str = "reserve";
+
/// Instrumentation test
pub fn main() {
- let test_amount = Amount::from_sat(1000);
+ let test_amount = Amount::from_sat(1500);
// Network check
let network = dirty_guess_network();
@@ -38,27 +41,77 @@ pub fn main() {
.collect();
let rpc = common_rpc(network).expect("Failed to open common client");
- if !existing_wallets.contains(CLIENT) || !existing_wallets.contains(WIRE) {
+ if !existing_wallets.contains(CLIENT)
+ || !existing_wallets.contains(WIRE)
+ || !existing_wallets.contains(RESERVE)
+ {
println!("Generate tests wallets");
// Create wallets
- rpc.create_wallet(&WIRE, None, None, None, None).unwrap();
- rpc.create_wallet(&CLIENT, None, None, None, None).unwrap();
+ rpc.create_wallet(&WIRE, None, None, None, None).ok();
+ rpc.create_wallet(&CLIENT, None, None, None, None).ok();
+ rpc.create_wallet(&RESERVE, None, None, None, None).ok();
}
// Load wallets
+
rpc.load_wallet(&WIRE).ok();
rpc.load_wallet(&CLIENT).ok();
+ rpc.load_wallet(&RESERVE).ok();
}
// Client initialization
let client_rpc = wallet_rpc(network, CLIENT);
let wire_rpc = wallet_rpc(network, WIRE);
+ let reserve_rpc = wallet_rpc(network, RESERVE);
let client_addr = client_rpc.get_new_address(None, None).unwrap();
let wire_addr = wire_rpc.get_new_address(None, None).unwrap();
+ let reserve_addr = reserve_rpc.get_new_address(None, None).unwrap();
+
+ let next_block = || {
+ match network {
+ Network::RegTest => {
+ // Manually mine a block
+ reserve_rpc.generate_to_address(1, &reserve_addr).unwrap();
+ }
+ _ => {
+ // Wait for next network block
+ reserve_rpc.wait_for_new_block(0).ok();
+ }
+ }
+ };
// Balance check
{
- // TODO transfer all wire money to client
+ // Transfer all wire money to client
+ let wire_balance = wire_rpc.get_balance(None, None).unwrap();
+ wire_rpc
+ .send_to_address(
+ &client_addr,
+ wire_balance,
+ None,
+ None,
+ Some(true),
+ None,
+ None,
+ None,
+ )
+ .ok();
+ // Transfer all wire money to client
+ let reserve_balance = reserve_rpc.get_balance(None, None).unwrap();
+ reserve_rpc
+ .send_to_address(
+ &client_addr,
+ reserve_balance,
+ None,
+ None,
+ Some(true),
+ None,
+ None,
+ None,
+ )
+ .ok();
+ next_block();
+
let balance = client_rpc.get_balance(None, None).unwrap();
let min_balance = test_amount * 3;
if balance < min_balance {
@@ -116,7 +169,7 @@ pub fn main() {
"Not in mempool"
);
// Check mined
- next_block(network, &client_rpc, &client_addr);
+ next_block();
assert!(
tx_exist(&wire_rpc, &id, 1, Category::Receive).unwrap(),
"Not mined"
@@ -137,7 +190,7 @@ pub fn main() {
"Not in mempool"
);
// Check mined
- next_block(network, &client_rpc, &client_addr);
+ next_block();
assert!(
tx_exist(&wire_rpc, &id, 1, Category::Receive).unwrap(),
"Not mined"
@@ -146,34 +199,110 @@ pub fn main() {
let (_, extracted) = wire_rpc.get_tx_segwit_key(&id).unwrap();
assert_eq!(key, extracted, "Corrupted metadata");
});
- runner.test("Refund simple", || loop {
- let refund_fee = Amount::from_sat(300);
+
+ let refund_fee = Amount::from_sat(300);
+ runner.test("Refund simple", || {
+ let before = client_rpc.get_balance(None, None).unwrap();
let send_id = client_rpc
.send_to_address(&wire_addr, test_amount, None, None, None, None, None, None)
.unwrap();
- next_block(network, &client_rpc, &client_addr);
- if let Some(refund_id) = refund(&wire_rpc, &send_id, refund_fee).unwrap() {
- next_block(network, &client_rpc, &client_addr);
- let fee = wire_rpc.get_transaction(&refund_id, None).unwrap().details[0]
- .fee
- .unwrap()
- .abs()
- .to_unsigned()
- .unwrap();
- let refund = client_rpc
- .get_transaction(&refund_id, None)
- .unwrap()
- .amount
- .abs()
- .to_unsigned()
- .unwrap();
- assert!(refund <= test_amount - refund_fee);
- assert!((fee + refund + refund_fee - test_amount) < Amount::from_sat(200));
- return;
- }
+ next_block();
+ let refund_id = refund(&wire_rpc, &send_id, refund_fee).unwrap();
+ next_block();
+ let refund_tx_fee = wire_rpc.get_transaction(&refund_id, None).unwrap().details[0]
+ .fee
+ .unwrap()
+ .abs()
+ .to_unsigned()
+ .unwrap();
+ let send_tx_fee = client_rpc.get_transaction(&send_id, None).unwrap().details[0]
+ .fee
+ .unwrap()
+ .abs()
+ .to_unsigned()
+ .unwrap();
+ let after = client_rpc.get_balance(None, None).unwrap();
+ assert_eq!(before - after, refund_tx_fee + refund_fee + send_tx_fee);
+ });
+ runner.test("Refund complex", || {
+ // Generate 6 new addresses
+ let addresses: Vec<Address> =
+ repeat_with(|| client_rpc.get_new_address(None, None).unwrap())
+ .take(6)
+ .collect();
+ // Send transaction to self with 1, 2 and 3 outputs
+ let txs: Vec<Txid> = [&addresses[0..1], &addresses[1..3], &addresses[3..]]
+ .into_iter()
+ .map(|addresses| {
+ let hex: String = client_rpc
+ .call(
+ "createrawtransaction",
+ &[
+ Value::Array(vec![]),
+ Value::Array(
+ addresses
+ .iter()
+ .map(|addr| json!({&addr.to_string(): test_amount.as_btc()}))
+ .collect(),
+ ),
+ ],
+ )
+ .unwrap();
+ let funded = client_rpc.fund_raw_transaction(hex, None, None).unwrap();
+ let signed = client_rpc
+ .sign_raw_transaction_with_wallet(&funded.hex, None, None)
+ .unwrap();
+ client_rpc.send_raw_transaction(&signed.hex).unwrap()
+ })
+ .collect();
+ next_block();
+ let before = client_rpc.get_balance(None, None).unwrap();
+ // Send a transaction with multiple input from multiple transaction of different outputs len
+ let hex: String = client_rpc
+ .call(
+ "createrawtransaction",
+ &[
+ Value::Array(
+ txs.into_iter()
+ .enumerate()
+ .map(|(i, tx)| {
+ json!({
+ "txid": tx.to_string(),
+ "vout": i,
+ })
+ })
+ .collect(),
+ ),
+ json!([
+ {&wire_addr.to_string(): (test_amount*3).as_btc()}
+ ]),
+ ],
+ )
+ .unwrap();
+ let funded = client_rpc.fund_raw_transaction(hex, None, None).unwrap();
+ let signed = client_rpc
+ .sign_raw_transaction_with_wallet(&funded.hex, None, None)
+ .unwrap();
+ let send_id = client_rpc.send_raw_transaction(&signed.hex).unwrap();
+ next_block();
+ let refund_id = refund(&wire_rpc, &send_id, refund_fee).unwrap();
+ next_block();
+ let after = client_rpc.get_balance(None, None).unwrap();
+ let refund_tx_fee = wire_rpc.get_transaction(&refund_id, None).unwrap().details[0]
+ .fee
+ .unwrap()
+ .abs()
+ .to_unsigned()
+ .unwrap();
+ let send_tx_fee = client_rpc.get_transaction(&send_id, None).unwrap().details[0]
+ .fee
+ .unwrap()
+ .abs()
+ .to_unsigned()
+ .unwrap();
+ assert_eq!(before - after, refund_tx_fee + refund_fee + send_tx_fee);
});
// TODO refund minimal amount
- // TODO refund send_many
runner.conclude();
}
@@ -194,19 +323,6 @@ fn tx_exist(
Ok(found)
}
-fn next_block(network: Network, rpc: &Client, address: &Address) {
- match network {
- Network::RegTest => {
- // Manually mine a block
- rpc.generate_to_address(1, address).unwrap();
- }
- _ => {
- // Wait for next network block
- rpc.wait_for_new_block(0).ok();
- }
- }
-}
-
/// Run test track success and errors
struct TestRunner {
nb_ok: usize,
diff --git a/src/lib.rs b/src/lib.rs
@@ -155,52 +155,36 @@ fn send_many(
/// Refund a transaction
///
-/// There is no reliable way to refund a transaction as you cannot know
-/// which addresses the sender has used and if it can use the refund
-/// (some addresses are shared)
+/// There is no reliable way to refund a transaction as you cannot know if the addresses used are shared
///
/// This is not a best-effort solution but a reasonable one
pub fn refund(
rpc: &Client,
id: &Txid,
refund_fee: Amount,
-) -> bitcoincore_rpc::Result<Option<Txid>> {
- // TODO take
+) -> bitcoincore_rpc::Result<Txid> {
// TODO handle insufficient_funds error
let full = rpc.get_transaction_full(id)?;
let detail = &full.tx.details[0];
-
- // This is our addresses
- let addr = detail.address.as_ref().unwrap().to_string();
assert_eq!(detail.category, Category::Receive);
- // List all addresses except self
- let addresses: Vec<String> = full
- .decoded
- .vout
- .iter()
- .filter_map(|out| {
- out.script_pub_key
- .address
- .as_ref()
- .map(|addr| addr.to_string())
- .filter(|str| str != &addr)
- })
- .collect();
-
- if addresses.is_empty() {
- // No way to known how to refund we keep the money
- return Ok(None);
+
+ // List all addresses
+ let mut addresses = Vec::new();
+ for vin in full.decoded.vin {
+ if let Some(id) = &vin.txid {
+ let tx = rpc.get_raw_tx(id)?;
+ let it = tx.vout.iter().find(|it| it.n == vin.vout.unwrap()).unwrap();
+ addresses.push(it.script_pub_key.address.as_ref().unwrap().to_string());
+ }
}
- // We assume every address that is not ours are the sender's
-
// Split received amount (minus refund fee) equally to each addresses
let split = (detail.amount.to_unsigned().unwrap() - refund_fee) / addresses.len() as u64;
let recipients: Vec<(String, Amount)> =
addresses.iter().map(|addr| (addr.clone(), split)).collect();
// Send refund making recipient pay the transaction fees
let id = send_many(rpc, recipients, addresses)?;
- Ok(Some(id))
+ Ok(id)
}
/// An extended bitcoincore JSON-RPC api client who can send and retrieve metadata with their transaction
diff --git a/src/rpc_patch.rs b/src/rpc_patch.rs
@@ -1,15 +1,11 @@
//! bitcoincore-rpc does not handle all the command we need and is not compatible with bitcoincore v22.0 that we use.
//! We add additional typed command with a custom trait
-use bitcoincore_rpc::{
- bitcoin::{Address, Amount, Txid, Wtxid},
- json::{GetRawTransactionResultVin, GetTransactionResult, ScriptPubkeyType},
- jsonrpc::serde_json::Value,
- Client, RpcApi,
-};
+use bitcoincore_rpc::{Client, RpcApi, bitcoin::{Address, Amount, BlockHash, Txid, Wtxid}, json::{GetRawTransactionResultVin, GetTransactionResult, ScriptPubkeyType}, jsonrpc::serde_json::Value};
pub trait ClientPatched {
fn get_transaction_full(&self, id: &Txid) -> bitcoincore_rpc::Result<GetTransactionFull>;
+ fn get_raw_tx(&self, id: &Txid) -> bitcoincore_rpc::Result<GetRawTransactionResult22>;
}
impl ClientPatched for Client {
@@ -19,6 +15,9 @@ impl ClientPatched for Client {
&[id.to_string().into(), Value::Null, true.into()],
)
}
+ fn get_raw_tx(&self, id: &Txid) -> bitcoincore_rpc::Result<GetRawTransactionResult22> {
+ self.call("getrawtransaction", &[id.to_string().into(), true.into()])
+ }
}
/// v22.0 replace "reqSigs" and "addresses" for the saner "address"
@@ -42,6 +41,27 @@ pub struct GetRawTransactionResultVout22 {
pub script_pub_key: GetRawTransactionResultVoutScriptPubKey22,
}
+#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetRawTransactionResult22 {
+ #[serde(rename = "in_active_chain")]
+ pub in_active_chain: Option<bool>,
+ #[serde(with = "bitcoincore_rpc::bitcoincore_rpc_json::serde_hex")]
+ pub hex: Vec<u8>,
+ pub txid: Txid,
+ pub hash: Wtxid,
+ pub size: usize,
+ pub vsize: usize,
+ pub version: u32,
+ pub locktime: u32,
+ pub vin: Vec<GetRawTransactionResultVin>,
+ pub vout: Vec<GetRawTransactionResultVout22>,
+ pub blockhash: Option<BlockHash>,
+ pub confirmations: Option<u32>,
+ pub time: Option<usize>,
+ pub blocktime: Option<usize>,
+}
+
/// Decoded raw transtion from"gettransaction" verbose does not return field already given in the simple form
#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)]
pub struct TransactionDecoded {