/* This file is part of TALER Copyright (C) 2022 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with TALER; see the file COPYING. If not, see */ use std::path::{Path, PathBuf}; use std::str::FromStr; use bitcoin::{hashes::hex::FromHex, Address, Amount, Network, Txid}; use btc_config::BitcoinConfig; use common::api_common::Amount as TalerAmount; use common::config::TalerConfig; use common::currency::{Currency, CurrencyBtc}; use common::log::{fail, OrFail}; use common::postgres; use common::url::Url; use rpc::{Category, Rpc, Transaction}; use rpc_utils::{default_data_dir, segwit_min_amount, sender_address}; use segwit::{decode_segwit_msg, encode_segwit_key}; use taler_utils::taler_to_btc; pub mod btc_config; pub mod rpc; pub mod rpc_utils; pub mod segwit; pub mod taler_utils; #[derive(Debug, thiserror::Error)] pub enum GetSegwitErr { #[error(transparent)] Decode(#[from] segwit::DecodeSegWitErr), #[error(transparent)] RPC(#[from] rpc::Error), } #[derive(Debug, thiserror::Error)] pub enum GetOpReturnErr { #[error("Missing opreturn")] MissingOpReturn, #[error(transparent)] RPC(#[from] rpc::Error), } /// An extended bitcoincore JSON-RPC api client who can send and retrieve metadata with their transaction impl Rpc { /// Send a transaction with a 32B key as metadata encoded using fake segwit addresses pub fn send_segwit_key( &mut self, to: &Address, amount: &Amount, metadata: &[u8; 32], ) -> rpc::Result { let hrp = match to.network() { Network::Bitcoin => "bc", Network::Testnet | Network::Signet => "tb", Network::Regtest => "bcrt", _ => unimplemented!(), }; let addresses = encode_segwit_key(hrp, metadata); let addresses = [ Address::from_str(&addresses[0]) .unwrap() .require_network(*to.network()) .unwrap(), Address::from_str(&addresses[1]) .unwrap() .require_network(*to.network()) .unwrap(), ]; let mut recipients = vec![(to, amount)]; let min = segwit_min_amount(); recipients.extend(addresses.iter().map(|addr| (addr, &min))); self.send_many(recipients) } /// Get detailed information about an in-wallet transaction and it's 32B metadata key encoded using fake segwit addresses pub fn get_tx_segwit_key( &mut self, id: &Txid, ) -> Result<(Transaction, [u8; 32]), GetSegwitErr> { let full = self.get_tx(id)?; let addresses: Vec = full .decoded .vout .iter() .filter_map(|it| { it.script_pub_key .address .as_ref() .map(|addr| addr.clone().assume_checked().to_string()) }) .collect(); let metadata = decode_segwit_msg(&addresses)?; Ok((full, metadata)) } /// Get detailed information about an in-wallet transaction and its op_return metadata pub fn get_tx_op_return( &mut self, id: &Txid, ) -> Result<(Transaction, Vec), GetOpReturnErr> { let full = self.get_tx(id)?; let op_return_out = full .decoded .vout .iter() .find(|it| it.script_pub_key.asm.starts_with("OP_RETURN")) .ok_or(GetOpReturnErr::MissingOpReturn)?; let hex = op_return_out.script_pub_key.asm.split_once(' ').unwrap().1; // Op return payload is always encoded in hexadecimal let metadata = Vec::from_hex(hex).unwrap(); Ok((full, metadata)) } /// Bounce a transaction bask to its sender /// /// 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, metadata: Option<&[u8]>, ) -> Result { let full = self.get_tx(id)?; let detail = &full.details[0]; assert!(detail.category == Category::Receive); let amount = detail.amount.to_unsigned().unwrap(); let sender = sender_address(self, &full)?; let bounce_amount = Amount::from_sat(amount.to_sat().saturating_sub(bounce_fee.to_sat())); // Send refund making recipient pay the transaction fees self.send(&sender, &bounce_amount, metadata, true) } } const DEFAULT_CONFIRMATION: u16 = 6; const DEFAULT_BOUNCE_FEE: &str = "0.00001"; pub struct WireState { pub confirmation: u32, pub max_confirmation: u32, pub btc_config: BitcoinConfig, pub bounce_fee: Amount, pub lifetime: Option, pub bump_delay: Option, pub base_url: Url, pub db_config: postgres::Config, pub currency: CurrencyBtc, } impl WireState { pub fn load_taler_config(file: Option<&Path>) -> Self { let (taler_config, path, currency) = load_taler_config(file); let btc_config = BitcoinConfig::load(path, currency).or_fail(|e| format!("bitcoin config: {}", e)); let init_confirmation = taler_config.confirmation().unwrap_or(DEFAULT_CONFIRMATION) as u32; Self { confirmation: init_confirmation, max_confirmation: init_confirmation * 2, bounce_fee: config_bounce_fee(&taler_config.bounce_fee(), currency), lifetime: taler_config.wire_lifetime(), bump_delay: taler_config.bump_delay(), base_url: taler_config.base_url(), db_config: taler_config.db_config(), currency, btc_config, } } } // Load taler config with btc-wire specific config pub fn load_taler_config(file: Option<&Path>) -> (TalerConfig, PathBuf, CurrencyBtc) { let config = TalerConfig::load(file); let path = config.path("CONF_PATH").unwrap_or_else(default_data_dir); let currency = match config.currency { Currency::BTC(it) => it, _ => fail(format!( "currency {} is not supported by btc-wire", config.currency.to_str() )), }; (config, path, currency) } // Parse bitcoin amount from config bounce fee fn config_bounce_fee(bounce_fee: &Option, currency: CurrencyBtc) -> Amount { let config = bounce_fee.as_deref().unwrap_or(DEFAULT_BOUNCE_FEE); TalerAmount::from_str(&format!("{}:{}", currency.to_str(), config)) .map_err(|s| s.to_string()) .and_then(|a| taler_to_btc(&a, currency)) .or_fail(|a| { format!( "config BOUNCE_FEE={} is not a valid bitcoin amount: {}", config, a ) }) } // Check network match config currency fn check_network_currency(network: Network, currency: CurrencyBtc) { let expected = match network { Network::Bitcoin => CurrencyBtc::Main, Network::Testnet => CurrencyBtc::Test, Network::Regtest => CurrencyBtc::Dev, _ => unimplemented!(), }; if currency != expected { fail(format_args!( "config currency is incompatible with node network, CURRENCY = {} expected {}", currency.to_str(), expected.to_str() )) } }