/*
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()
))
}
}