depolymerization

wire gateway for Bitcoin/Ethereum
Log | Files | Refs | Submodules | README | LICENSE

commit 06cbb24cbc316e7c9eb07480198af885f6ba492e
parent a02daa453da90f15ac1033e44ba3aaa54d38d1c3
Author: Antoine A <>
Date:   Mon,  7 Mar 2022 18:12:04 +0100

Network based currency names

Diffstat:
MREADME.md | 4++--
Mbtc-wire/src/bin/btc-wire-utils.rs | 11++++-------
Abtc-wire/src/btc_config.rs | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dbtc-wire/src/config.rs | 133-------------------------------------------------------------------------------
Mbtc-wire/src/lib.rs | 61+++++++++++++++++++++++++++++++++++++------------------------
Mbtc-wire/src/loops/worker.rs | 15++++++++-------
Mbtc-wire/src/main.rs | 6+++---
Mbtc-wire/src/rpc.rs | 2+-
Mbtc-wire/src/sql.rs | 19++++++++++++-------
Mbtc-wire/src/taler_utils.rs | 21+++++++++++++++------
Mcommon/src/api_common.rs | 10+++++-----
Mcommon/src/api_wire.rs | 10+++++-----
Mcommon/src/config.rs | 26+++++++++++++++++---------
Acommon/src/currency.rs | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/src/lib.rs | 1+
Mcommon/src/log.rs | 4+---
Mcommon/src/sql.rs | 18+++++++++++-------
Mdocs/taler-btc.conf | 4++--
Mdocs/taler-eth.conf | 4++--
Meth-wire/src/bin/eth-wire-utils.rs | 12+++++++-----
Meth-wire/src/lib.rs | 37+++++++++++++++++++++++++++----------
Meth-wire/src/loops/worker.rs | 8++++----
Meth-wire/src/main.rs | 2+-
Meth-wire/src/sql.rs | 15++++++++++-----
Meth-wire/src/taler_util.rs | 16++++++++++------
Minstrumentation/src/btc.rs | 2+-
Minstrumentation/src/eth.rs | 2+-
Minstrumentation/src/main.rs | 10++++++----
Mtest/btc/bumpfee.sh | 6+++---
Mtest/btc/conflict.sh | 4++--
Mtest/btc/lifetime.sh | 2+-
Mtest/btc/maxfee.sh | 2+-
Mtest/btc/reconnect.sh | 2+-
Mtest/btc/reorg.sh | 2+-
Mtest/btc/stress.sh | 2+-
Mtest/btc/wire.sh | 2+-
Mtest/common.sh | 2+-
Mtest/conf/taler_btc.conf | 5++---
Mtest/conf/taler_btc_bump.conf | 7++++---
Mtest/conf/taler_btc_lifetime.conf | 2+-
Mtest/conf/taler_eth.conf | 2+-
Mtest/conf/taler_eth_bump.conf | 2+-
Mtest/conf/taler_eth_lifetime.conf | 2+-
Mtest/eth/bumpfee.sh | 6+++---
Mtest/eth/lifetime.sh | 2+-
Mtest/eth/maxfee.sh | 2+-
Mtest/eth/reconnect.sh | 2+-
Mtest/eth/reorg.sh | 2+-
Mtest/eth/stress.sh | 2+-
Mtest/eth/wire.sh | 2+-
Mtest/gateway/api.sh | 16++++++++--------
Mwire-gateway/Cargo.toml | 9++-------
Mwire-gateway/src/main.rs | 32+++++++++++++++++---------------
53 files changed, 476 insertions(+), 321 deletions(-)

diff --git a/README.md b/README.md @@ -77,7 +77,7 @@ CONF_PATH = ~/.bitcoin # Number of blocks to consider a transactions durable CONFIRMATION = 6 # Amount to keep when bouncing malformed credit -BOUNCE_FEE = BTC:0.00001 +BOUNCE_FEE = 0.00001 ``` ### eth-wire @@ -89,7 +89,7 @@ IPC_PATH = ~/.ethereum/geth/geth.ipc # Number of blocks to consider a transactions durable CONFIRMATION = 24 # Amount to keep when bouncing malformed credit -BOUNCE_FEE = ETH:0.00001 +BOUNCE_FEE = 0.00001 ``` ### Wire gateway diff --git a/btc-wire/src/bin/btc-wire-utils.rs b/btc-wire/src/bin/btc-wire-utils.rs @@ -17,7 +17,7 @@ use std::path::PathBuf; use bitcoin::{Address, Amount, BlockHash, Network}; use btc_wire::{ - config::BitcoinConfig, + btc_config::BitcoinConfig, load_taler_config, rpc::{Category, Rpc}, }; @@ -80,12 +80,9 @@ pub fn auto_wallet(rpc: &mut Rpc, config: &BitcoinConfig, name: &str) -> (Rpc, A fn main() { common::log::init(); let args = Args::parse(); - let taler_config = load_taler_config(args.config.as_deref()); - let btc_config = BitcoinConfig::load( - args.datadir.unwrap_or(taler_config.custom), - &taler_config.currency, - ) - .unwrap(); + let (taler_config, currency) = load_taler_config(args.config.as_deref()); + let btc_config = + BitcoinConfig::load(args.datadir.unwrap_or(taler_config.custom), currency).unwrap(); let mut rpc = Rpc::common(&btc_config).unwrap(); match args.cmd { diff --git a/btc-wire/src/btc_config.rs b/btc-wire/src/btc_config.rs @@ -0,0 +1,136 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +use std::{ + net::SocketAddr, + path::{Path, PathBuf}, + str::FromStr, +}; + +use bitcoin::Network; +use common::{ + currency::CurrencyBtc, + log::{fail, OrFail}, +}; + +use crate::{ + check_network_currency, + rpc_utils::{chain_dir, rpc_port}, +}; + +pub const WIRE_WALLET_NAME: &str = "wire"; + +#[derive(Debug, Clone)] +pub enum BtcAuth { + Cookie(PathBuf), + Auth(String), +} + +/// Bitcoin config relevant for btc-wire +#[derive(Debug, Clone)] +pub struct BitcoinConfig { + pub network: Network, + pub addr: SocketAddr, + pub auth: BtcAuth, +} + +impl BitcoinConfig { + /// Load from bitcoin path + pub fn load(path: impl AsRef<Path>, currency: CurrencyBtc) -> Result<Self, ini::Error> { + let path = path.as_ref(); + let conf = if path.is_dir() { + ini::Ini::load_from_file(path.join("bitcoin.conf")) + } else { + ini::Ini::load_from_file(path) + }?; + + let main = conf.general_section(); + + if !main.contains_key("txindex") { + fail("require a bitcoind node running with 'txindex' option"); + } + + if !main.contains_key("maxtxfee") { + fail("require a bitcoind node running with 'maxtxfee' option"); + } + + let network = if let Some("1") = main.get("testnet") { + Network::Testnet + } else if let Some("1") = main.get("signet") { + Network::Signet + } else if let Some("1") = main.get("regtest") { + Network::Regtest + } else { + Network::Bitcoin + }; + + check_network_currency(network, currency); + + let section = match network { + Network::Bitcoin => Some(main), + Network::Testnet => conf.section(Some("test")), + Network::Signet => conf.section(Some("signet")), + Network::Regtest => conf.section(Some("regtest")), + }; + + let port = if let Some(addr) = section.and_then(|s| s.get("rpcport")) { + addr.parse() + .or_fail(|_| "bitcoin config value 'rpcport' is not a valid port number".into()) + } else { + rpc_port(network) + }; + + let addr = if let Some(addr) = section.and_then(|s| s.get("rpcbind")) { + SocketAddr::from_str(addr) + .or_fail(|_| "bitcoin config value 'rpcbind' is not a valid socket address".into()) + } else { + ([127, 0, 0, 1], port).into() + }; + + let auth = if let (Some(login), Some(passwd)) = ( + section.and_then(|s| s.get("rpcuser")), + section.and_then(|s| s.get("rpcpassword")), + ) { + BtcAuth::Auth(format!("{}:{}", login, passwd)) + } else if let (Some(login), Some(passwd)) = (main.get("rpcuser"), main.get("rpcpassword")) { + BtcAuth::Auth(format!("{}:{}", login, passwd)) + } else { + let cookie_file = if let Some(path) = section.and_then(|s| s.get("rpccookiefile")) { + path + } else if let Some(path) = main.get("rpccookiefile") { + path + } else { + ".cookie" + } + .to_string(); + let data_dir = if let Some(path) = section.and_then(|s| s.get("datadir")) { + PathBuf::from(path) + } else if let Some(path) = main.get("datadir") { + PathBuf::from(path) + } else if path.is_file() { + path.parent().unwrap().to_path_buf() + } else { + path.to_path_buf() + }; + BtcAuth::Cookie(data_dir.join(chain_dir(network)).join(cookie_file)) + }; + + Ok(Self { + network, + addr, + auth, + }) + } +} diff --git a/btc-wire/src/config.rs b/btc-wire/src/config.rs @@ -1,133 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> -*/ -use std::{ - net::SocketAddr, - path::{Path, PathBuf}, - str::FromStr, -}; - -use bitcoin::Network; -use common::log::{fail, OrFail}; - -use crate::{ - check_network_currency, - rpc_utils::{chain_dir, rpc_port}, -}; - -pub const WIRE_WALLET_NAME: &str = "wire"; - -#[derive(Debug, Clone)] -pub enum BtcAuth { - Cookie(PathBuf), - Auth(String), -} - -/// Bitcoin config relevant for btc-wire -#[derive(Debug, Clone)] -pub struct BitcoinConfig { - pub network: Network, - pub addr: SocketAddr, - pub auth: BtcAuth, -} - -impl BitcoinConfig { - /// Load from bitcoin path - pub fn load(path: impl AsRef<Path>, currency: &str) -> Result<Self, ini::Error> { - let path = path.as_ref(); - let conf = if path.is_dir() { - ini::Ini::load_from_file(path.join("bitcoin.conf")) - } else { - ini::Ini::load_from_file(path) - }?; - - let main = conf.general_section(); - - if !main.contains_key("txindex") { - fail("require a bitcoind node running with 'txindex' option"); - } - - if !main.contains_key("maxtxfee") { - fail("require a bitcoind node running with 'maxtxfee' option"); - } - - let network = if let Some("1") = main.get("testnet") { - Network::Testnet - } else if let Some("1") = main.get("signet") { - Network::Signet - } else if let Some("1") = main.get("regtest") { - Network::Regtest - } else { - Network::Bitcoin - }; - - check_network_currency(network, currency); - - let section = match network { - Network::Bitcoin => Some(main), - Network::Testnet => conf.section(Some("test")), - Network::Signet => conf.section(Some("signet")), - Network::Regtest => conf.section(Some("regtest")), - }; - - let port = if let Some(addr) = section.and_then(|s| s.get("rpcport")) { - addr.parse() - .or_fail(|_| "bitcoin config value 'rpcport' is not a valid port number".into()) - } else { - rpc_port(network) - }; - - let addr = if let Some(addr) = section.and_then(|s| s.get("rpcbind")) { - SocketAddr::from_str(addr) - .or_fail(|_| "bitcoin config value 'rpcbind' is not a valid socket address".into()) - } else { - ([127, 0, 0, 1], port).into() - }; - - let auth = if let (Some(login), Some(passwd)) = ( - section.and_then(|s| s.get("rpcuser")), - section.and_then(|s| s.get("rpcpassword")), - ) { - BtcAuth::Auth(format!("{}:{}", login, passwd)) - } else if let (Some(login), Some(passwd)) = (main.get("rpcuser"), main.get("rpcpassword")) { - BtcAuth::Auth(format!("{}:{}", login, passwd)) - } else { - let cookie_file = if let Some(path) = section.and_then(|s| s.get("rpccookiefile")) { - path - } else if let Some(path) = main.get("rpccookiefile") { - path - } else { - ".cookie" - } - .to_string(); - let data_dir = if let Some(path) = section.and_then(|s| s.get("datadir")) { - PathBuf::from(path) - } else if let Some(path) = main.get("datadir") { - PathBuf::from(path) - } else if path.is_file() { - path.parent().unwrap().to_path_buf() - } else { - path.to_path_buf() - }; - BtcAuth::Cookie(data_dir.join(chain_dir(network)).join(cookie_file)) - }; - - Ok(Self { - network, - addr, - auth, - }) - } -} diff --git a/btc-wire/src/lib.rs b/btc-wire/src/lib.rs @@ -1,4 +1,3 @@ -use std::path::{Path, PathBuf}; /* This file is part of TALER Copyright (C) 2022 Taler Systems SA @@ -14,21 +13,23 @@ use std::path::{Path, PathBuf}; You should have received a copy of the GNU Affero General Public License along with TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +use std::path::{Path, PathBuf}; use std::{str::FromStr, sync::atomic::AtomicU32}; 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 config::BitcoinConfig; 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 config; +pub mod btc_config; pub mod metadata; pub mod rpc; pub mod rpc_utils; @@ -144,8 +145,8 @@ impl Rpc { } } -const DEFAULT_BOUNCE_FEE: &str = "BTC:0.00001"; const DEFAULT_CONFIRMATION: u16 = 6; +const DEFAULT_BOUNCE_FEE: &str = "0.00001"; pub struct WireState { pub confirmation: AtomicU32, @@ -156,40 +157,50 @@ pub struct WireState { pub bump_delay: Option<u32>, 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 = load_taler_config(file); - let btc_config = BitcoinConfig::load(taler_config.custom, &taler_config.currency) + let (taler_config, currency) = load_taler_config(file); + let btc_config = BitcoinConfig::load(taler_config.custom, currency) .expect("Failed to read bitcoin configuration file"); let init_confirmation = taler_config.confirmation.unwrap_or(DEFAULT_CONFIRMATION) as u32; Self { confirmation: AtomicU32::new(init_confirmation), max_confirmation: init_confirmation * 2, btc_config, - bounce_fee: config_bounce_fee(&taler_config.bounce_fee), + 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, } } } // Load taler config with btc-wire specific config -pub fn load_taler_config(file: Option<&Path>) -> TalerConfig<PathBuf> { - TalerConfig::load_with_custom(file, |dep| { +pub fn load_taler_config(file: Option<&Path>) -> (TalerConfig<PathBuf>, CurrencyBtc) { + let config = TalerConfig::load_with_custom(file, |dep| { common::config::path(dep, "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, currency) } // Parse bitcoin amount from config bounce fee -fn config_bounce_fee(bounce_fee: &Option<String>) -> Amount { +fn config_bounce_fee(bounce_fee: &Option<String>, currency: CurrencyBtc) -> Amount { let config = bounce_fee.as_deref().unwrap_or(DEFAULT_BOUNCE_FEE); - TalerAmount::from_str(&config) + TalerAmount::from_str(&format!("{}:{}", currency.to_str(), config)) .map_err(|s| s.to_string()) - .and_then(|a| taler_to_btc(&a)) + .and_then(|a| taler_to_btc(&a, currency)) .or_fail(|a| { format!( "config value BOUNCE_FEE={} is not a valid bitcoin amount: {}", @@ -199,16 +210,18 @@ fn config_bounce_fee(bounce_fee: &Option<String>) -> Amount { } // Check network match config currency -fn check_network_currency(network: Network, currency: &str) { - match network { - Network::Bitcoin | Network::Regtest => { - if currency != "BTC" { - fail(format_args!( - "wrong config CURRENCY = {} expected {}", - currency, "BTC" - )) - } - } - Network::Testnet | Network::Signet => unimplemented!(), +fn check_network_currency(network: Network, currency: CurrencyBtc) { + let expected = match network { + Network::Bitcoin => CurrencyBtc::Main, + Network::Testnet => CurrencyBtc::Test, + Network::Regtest => CurrencyBtc::Dev, + Network::Signet => unimplemented!(), + }; + if currency != expected { + fail(format_args!( + "config currency is incompatible with node network, CURRENCY = {} expected {}", + currency.to_str(), + expected.to_str() + )) } } diff --git a/btc-wire/src/loops/worker.rs b/btc-wire/src/loops/worker.rs @@ -101,7 +101,7 @@ pub fn worker(mut rpc: AutoRpcWallet, mut db: AutoReconnectDb, state: &WireState // As we are now in sync with the blockchain if a transaction has Requested status it have not been sent // Send requested withdraws - while withdraw(db, rpc)? {} + while withdraw(db, rpc, state)? {} // Bump stuck transactions for id in stuck { @@ -212,7 +212,7 @@ fn sync_chain( } } Category::Receive if confirmations >= min_confirmations as i32 => { - sync_chain_incoming_confirmed(&id, rpc, db)? + sync_chain_incoming_confirmed(&id, rpc, db, state)? } _ => { // Ignore coinbase and unconfirmed send transactions @@ -311,6 +311,7 @@ fn sync_chain_incoming_confirmed( id: &Txid, rpc: &mut Rpc, db: &mut Client, + state: &WireState, ) -> Result<(), LoopError> { match rpc.get_tx_segwit_key(id) { Ok((full, reserve_pub)) => { @@ -318,7 +319,7 @@ fn sync_chain_incoming_confirmed( 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); - let amount = btc_to_taler(&full.amount); + let amount = btc_to_taler(&full.amount, state.currency); let nb = db.execute("INSERT INTO tx_in (_date, amount, reserve_pub, debit_acc, credit_acc) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (reserve_pub) DO NOTHING ", &[ &date, &amount.to_string(), &reserve_pub.as_ref(), &btc_payto_url(&debit_addr).as_ref(), &btc_payto_url(credit_addr).as_ref() ])?; @@ -357,7 +358,7 @@ fn sync_chain_withdraw( state: &WireState, ) -> LoopResult<bool> { let credit_addr = full.details[0].address.as_ref().unwrap(); - let amount = btc_to_taler(&full.amount); + let amount = btc_to_taler(&full.amount, state.currency); if confirmations < 0 { if full.replaced_by_txid.is_none() { @@ -546,7 +547,7 @@ fn sync_chain_outgoing( } /// Send a withdraw transaction on the blockchain, return false if no more requested transactions are found -fn withdraw(db: &mut Client, rpc: &mut Rpc) -> LoopResult<bool> { +fn withdraw(db: &mut Client, rpc: &mut Rpc, state: &WireState) -> LoopResult<bool> { // We rely on the advisory lock to ensure we are the only one sending transactions let row = db.query_opt( "SELECT id, amount, wtid, credit_acc, exchange_url FROM tx_out WHERE status=$1 ORDER BY _date LIMIT 1", @@ -554,7 +555,7 @@ fn withdraw(db: &mut Client, rpc: &mut Rpc) -> LoopResult<bool> { )?; if let Some(row) = &row { let id: i32 = row.get(0); - let amount = sql_btc_amount(row, 1); + let amount = sql_btc_amount(row, 1, state.currency); let wtid: [u8; 32] = sql_array(row, 2); let addr = sql_addr(row, 3); let url = sql_url(row, 4); @@ -566,7 +567,7 @@ fn withdraw(db: &mut Client, rpc: &mut Rpc) -> LoopResult<bool> { "UPDATE tx_out SET status=$1, txid=$2 WHERE id=$3", &[&(WithdrawStatus::Sent as i16), &tx_id.as_ref(), &id], )?; - let amount = btc_to_taler(&amount.to_signed().unwrap()); + let amount = btc_to_taler(&amount.to_signed().unwrap(), state.currency); info!(">> {} {} in {} to {}", amount, base32(&wtid), tx_id, addr); } Ok(row.is_some()) diff --git a/btc-wire/src/main.rs b/btc-wire/src/main.rs @@ -15,7 +15,7 @@ */ use bitcoin::Network; use btc_wire::{ - config::{BitcoinConfig, WIRE_WALLET_NAME}, + btc_config::{BitcoinConfig, WIRE_WALLET_NAME}, load_taler_config, rpc::{self, auto_rpc_common, auto_rpc_wallet, ErrorCode, Rpc}, WireState, @@ -64,7 +64,7 @@ fn main() { fn init(config: Option<PathBuf>, init: Init) { // Parse taler config - let taler_config = load_taler_config(config.as_deref()); + let (taler_config, currency) = load_taler_config(config.as_deref()); // Connect to database let mut db = taler_config @@ -72,7 +72,7 @@ fn init(config: Option<PathBuf>, init: Init) { .connect(NoTls) .expect("Failed to connect to database"); // Parse bitcoin config - let btc_conf = BitcoinConfig::load(taler_config.custom, &taler_config.currency) + let btc_conf = BitcoinConfig::load(taler_config.custom, currency) .expect("Failed to load bitcoin configuration"); // Connect to bitcoin node let mut rpc = Rpc::common(&btc_conf).expect("Failed to connect to bitcoin RPC server"); diff --git a/btc-wire/src/rpc.rs b/btc-wire/src/rpc.rs @@ -35,7 +35,7 @@ use std::{ time::{Duration, Instant}, }; -use crate::config::{BitcoinConfig, BtcAuth}; +use crate::btc_config::{BitcoinConfig, BtcAuth}; pub type AutoRpcWallet = AutoReconnect<(BitcoinConfig, &'static str), Rpc>; diff --git a/btc-wire/src/sql.rs b/btc-wire/src/sql.rs @@ -15,35 +15,40 @@ */ use bitcoin::{hashes::Hash, Address, Amount as BtcAmount, Txid}; +use common::currency::CurrencyBtc; +use common::log::OrFail; use common::postgres::Row; use common::sql::{sql_amount, sql_url}; use btc_wire::taler_utils::{btc_payto_addr, taler_to_btc}; -pub fn sql_btc_amount(row: &Row, idx: usize) -> BtcAmount { +/// Bitcoin amount from sql +pub fn sql_btc_amount(row: &Row, idx: usize, currency: CurrencyBtc) -> BtcAmount { let amount = sql_amount(row, idx); - taler_to_btc(&amount).unwrap_or_else(|_| { - panic!( + taler_to_btc(&amount, currency).or_fail(|_| { + format!( "Database invariant: expected an bitcoin amount got {}", amount ) }) } +/// Bitcoin address from sql pub fn sql_addr(row: &Row, idx: usize) -> Address { let url = sql_url(row, idx); - btc_payto_addr(&url).unwrap_or_else(|_| { - panic!( + btc_payto_addr(&url).or_fail(|_| { + format!( "Database invariant: expected an bitcoin payto url got {}", url ) }) } +/// Bitcoin transaction id from sql pub fn sql_txid(row: &Row, idx: usize) -> Txid { let slice: &[u8] = row.get(idx); - Txid::from_slice(slice).unwrap_or_else(|_| { - panic!( + Txid::from_slice(slice).or_fail(|_| { + format!( "Database invariant: expected a transaction if got an array of {}B", slice.len() ) diff --git a/btc-wire/src/taler_utils.rs b/btc-wire/src/taler_utils.rs @@ -16,9 +16,10 @@ //! Utils function to convert taler API types to bitcoin API types use bitcoin::{Address, Amount as BtcAmount, SignedAmount}; -use std::str::FromStr; use common::api_common::Amount; +use common::currency::CurrencyBtc; use common::url::Url; +use std::str::FromStr; /// Generate a payto uri from a btc address pub fn btc_payto_url(addr: &Address) -> Url { @@ -38,16 +39,24 @@ pub fn btc_payto_addr(url: &Url) -> Result<Address, String> { } /// Transform a btc amount into a taler amount -pub fn btc_to_taler(amount: &SignedAmount) -> Amount { +pub fn btc_to_taler(amount: &SignedAmount, currency: CurrencyBtc) -> Amount { let unsigned = amount.abs().to_unsigned().unwrap(); let sat = unsigned.as_sat(); - return Amount::new("BTC", sat / 100_000_000, (sat % 100_000_000) as u32); + return Amount::new( + currency.to_str(), + sat / 100_000_000, + (sat % 100_000_000) as u32, + ); } /// Transform a taler amount into a btc amount -pub fn taler_to_btc(amount: &Amount) -> Result<BtcAmount, String> { - if amount.currency != "BTC" { - return Err(format!("expected ETH for {}", amount.currency)); +pub fn taler_to_btc(amount: &Amount, currency: CurrencyBtc) -> Result<BtcAmount, String> { + if amount.currency != currency.to_str() { + return Err(format!( + "expected currency {} got {}", + currency.to_str(), + amount.currency + )); } let sat = amount.value * 100_000_000 + amount.fraction as u64; diff --git a/common/src/api_common.rs b/common/src/api_common.rs @@ -110,30 +110,30 @@ impl From<SystemTime> for Timestamp { /// <https://docs.taler.net/core/api-common.html#tsref-type-SafeUint64> #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)] -pub struct SafeUint64(u64); +pub struct SafeU64(u64); #[derive(Debug, thiserror::Error)] #[error("{0} unsafe, {0} > (2^53 - 1)")] pub struct UnsafeUint64(u64); -impl TryFrom<u64> for SafeUint64 { +impl TryFrom<u64> for SafeU64 { type Error = UnsafeUint64; fn try_from(nb: u64) -> Result<Self, Self::Error> { if nb < (1 << 53) - 1 { - Ok(SafeUint64(nb)) + Ok(SafeU64(nb)) } else { Err(UnsafeUint64(nb)) } } } -impl<'de> Deserialize<'de> for SafeUint64 { +impl<'de> Deserialize<'de> for SafeU64 { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { - SafeUint64::try_from(u64::deserialize(deserializer)?).map_err(D::Error::custom) + SafeU64::try_from(u64::deserialize(deserializer)?).map_err(D::Error::custom) } } diff --git a/common/src/api_wire.rs b/common/src/api_wire.rs @@ -15,13 +15,13 @@ */ use url::Url; -use crate::api_common::{Amount, EddsaPublicKey, HashCode, SafeUint64, ShortHashCode, Timestamp}; +use crate::api_common::{Amount, EddsaPublicKey, HashCode, SafeU64, ShortHashCode, Timestamp}; /// <https://docs.taler.net/core/api-wire.html#tsref-type-TransferResponse> #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TransferResponse { pub timestamp: Timestamp, - pub row_id: SafeUint64, + pub row_id: SafeU64, } /// <https://docs.taler.net/core/api-wire.html#tsref-type-TransferRequest> @@ -43,7 +43,7 @@ pub struct OutgoingHistory { /// <https://docs.taler.net/core/api-wire.html#tsref-type-OutgoingBankTransaction> #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct OutgoingBankTransaction { - pub row_id: SafeUint64, + pub row_id: SafeU64, pub date: Timestamp, pub amount: Amount, pub credit_account: Url, @@ -61,7 +61,7 @@ pub struct IncomingHistory { pub enum IncomingBankTransaction { #[serde(rename = "RESERVE")] IncomingReserveTransaction { - row_id: SafeUint64, + row_id: SafeU64, date: Timestamp, amount: Amount, credit_account: Url, @@ -85,7 +85,7 @@ pub struct AddIncomingRequest { /// <https://docs.taler.net/core/api-wire.html#tsref-type-AddIncomingResponse> #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct AddIncomingResponse { - pub row_id: SafeUint64, + pub row_id: SafeU64, pub timestamp: Timestamp, } diff --git a/common/src/config.rs b/common/src/config.rs @@ -21,14 +21,17 @@ use std::{ }; use url::Url; -use crate::log::{fail, OrFail}; +use crate::{ + currency::Currency, + log::{fail, OrFail}, +}; pub use ini; // Depolymerizer taler config pub struct TalerConfig<T> { // common pub db_config: postgres::Config, - pub currency: String, + pub currency: Currency, pub base_url: Url, // wire-gateway pub http_lifetime: Option<u32>, @@ -63,26 +66,31 @@ impl<T> TalerConfig<T> { .output() .or_fail(|e| format!("Failed to execute taler-config: {}", e)); if !output.status.success() { - panic!( + fail(format_args!( "taler-config failure:\n{}", String::from_utf8_lossy(&output.stderr) - ); + )); } // Parse ini config let conf = ini::Ini::load_from_str(&String::from_utf8_lossy(&output.stdout)) .expect("Failed to parse config"); let taler = section(&conf, "taler"); let currency = required(taler, "CURRENCY", string); - let section_name = match currency.as_str() { - "BTC" => "depolymerizer-bitcoin", - "ETH" => "depolymerizer-ethereum", - currency => fail(format_args!("Unsupported currency {}", currency)), + let currency = Currency::from_str(&currency).or_fail(|_| { + format!( + "config value CURRENCY={} is an unsupported currency", + currency + ) + }); + let section_name = match currency { + Currency::BTC(_) => "depolymerizer-bitcoin", + Currency::ETH(_) => "depolymerizer-ethereum", }; let dep = section(&conf, section_name); Self { db_config: required(dep, "DB_URL", postgres), - currency: currency.to_string(), + currency, base_url: required(section(&conf, "exchange"), "BASE_URL", url), confirmation: nb(dep, "CONFIRMATION"), bounce_fee: string(dep, "BOUNCE_FEE"), diff --git a/common/src/currency.rs b/common/src/currency.rs @@ -0,0 +1,89 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ + +use std::str::FromStr; + +pub const MAIN_ETH: &str = "ETHEREUM-ETH"; +pub const ROPSTEN_ETH: &str = "TEST-ROPSTEN-ETH"; +pub const DEV_ETH: &str = "DEV-ETH"; +pub const MAIN_BTC: &str = "BITCOIN-BTC"; +pub const REGTEST_BTC: &str = "TEST-BTC"; +pub const TESTNET_BTC: &str = "DEV-BTC"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Currency { + ETH(CurrencyEth), + BTC(CurrencyBtc), +} + +impl Currency { + pub const fn to_str(&self) -> &'static str { + match self { + Currency::BTC(btc) => btc.to_str(), + Currency::ETH(eth) => eth.to_str(), + } + } +} + +impl FromStr for Currency { + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(match s { + MAIN_BTC => Currency::BTC(CurrencyBtc::Main), + REGTEST_BTC => Currency::BTC(CurrencyBtc::Test), + TESTNET_BTC => Currency::BTC(CurrencyBtc::Dev), + MAIN_ETH => Currency::ETH(CurrencyEth::Main), + ROPSTEN_ETH => Currency::ETH(CurrencyEth::Ropsten), + DEV_ETH => Currency::ETH(CurrencyEth::Dev), + _ => return Err(()), + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CurrencyBtc { + Main, + Test, + Dev, +} + +impl CurrencyBtc { + pub const fn to_str(&self) -> &'static str { + match self { + CurrencyBtc::Main => MAIN_BTC, + CurrencyBtc::Test => REGTEST_BTC, + CurrencyBtc::Dev => TESTNET_BTC, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CurrencyEth { + Main, + Dev, + Ropsten, +} + +impl CurrencyEth { + pub const fn to_str(&self) -> &'static str { + match self { + CurrencyEth::Main => MAIN_ETH, + CurrencyEth::Ropsten => ROPSTEN_ETH, + CurrencyEth::Dev => DEV_ETH, + } + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs @@ -32,6 +32,7 @@ pub mod log; pub mod reconnect; pub mod sql; pub mod status; +pub mod currency; /// Secure random slice generator using getrandom pub fn rand_slice<const N: usize>() -> [u8; N] { diff --git a/common/src/log.rs b/common/src/log.rs @@ -28,12 +28,10 @@ fn custom_format( now: &mut DeferredNow, record: &Record, ) -> Result<(), std::io::Error> { - let path = record.module_path().unwrap_or(""); write!( w, - "{} {} {} {}", + "{} {} {}", now.format(TIME_FORMAT), - path.split_once(':').map(|s| s.0).unwrap_or(path), record.level(), &record.args() ) diff --git a/common/src/sql.rs b/common/src/sql.rs @@ -19,23 +19,26 @@ use std::str::FromStr; use postgres::Row; use url::Url; -use crate::api_common::{Amount, SafeUint64}; +use crate::{api_common::{Amount, SafeU64}, log::OrFail}; +/// URL from sql pub fn sql_url(row: &Row, idx: usize) -> Url { let str: &str = row.get(idx); - Url::from_str(str).unwrap_or_else(|_| panic!("Database invariant: expected an url got {}", str)) + Url::from_str(str).or_fail(|_| format!("Database invariant: expected an url got {}", str)) } +/// Ethereum amount from sql pub fn sql_amount(row: &Row, idx: usize) -> Amount { let str: &str = row.get(idx); Amount::from_str(str) - .unwrap_or_else(|_| panic!("Database invariant: expected an amount got {}", str)) + .or_fail(|_| format!("Database invariant: expected an amount got {}", str)) } +/// Byte array from sql pub fn sql_array<const N: usize>(row: &Row, idx: usize) -> [u8; N] { let slice: &[u8] = row.get(idx); - slice.try_into().unwrap_or_else(|_| { - panic!( + slice.try_into().or_fail(|_| { + format!( "Database invariant: expected an byte array of {}B for {}B", N, slice.len() @@ -43,7 +46,8 @@ pub fn sql_array<const N: usize>(row: &Row, idx: usize) -> [u8; N] { }) } -pub fn sql_safe_u64(row: &Row, idx: usize) -> SafeUint64 { +/// Safe safe u64 from sql +pub fn sql_safe_u64(row: &Row, idx: usize) -> SafeU64 { let id: i32 = row.get(idx); - SafeUint64::try_from(id as u64).unwrap() + SafeU64::try_from(id as u64).unwrap() } diff --git a/docs/taler-btc.conf b/docs/taler-btc.conf @@ -1,6 +1,6 @@ # Full btc-wire configuration [taler] -CURRENCY = BTC +CURRENCY = BITCOIN-BTC [exchange] BASE_URL = http://test.com @@ -11,7 +11,7 @@ PAYTO = payto://bitcoin/bc1qcr40fzphnh4mcwlv65kvdam4dxq977t2rn54qx PORT = 8080 CONF_PATH = ~/.bitcoin CONFIRMATION = 6 -BOUNCE_FEE = BTC:0.00001 +BOUNCE_FEE = 0.00001 HTTP_LIFETIME = 0 WIRE_LIFETIME = 0 BUMP_DELAY = 0 diff --git a/docs/taler-eth.conf b/docs/taler-eth.conf @@ -1,6 +1,6 @@ # Full eth-wire configuration [taler] -CURRENCY = ETH +CURRENCY = ETHEREUM-ETH [exchange] BASE_URL = http://test.com @@ -11,7 +11,7 @@ PAYTO = payto://ethereum/425870d7b77ad5665ca982ff85c398222a70fe7c PORT = 8080 IPC_PATH = ~/.ethereum/geth/geth.ipc CONFIRMATION = 24 -BOUNCE_FEE = ETH:0.00001 +BOUNCE_FEE = 0.00001 HTTP_LIFETIME = 0 WIRE_LIFETIME = 0 BUMP_DELAY = 0 diff --git a/eth-wire/src/bin/eth-wire-utils.rs b/eth-wire/src/bin/eth-wire-utils.rs @@ -104,7 +104,7 @@ enum Cmd { fn main() { init(); let args: Args = Args::parse(); - let taler_config = load_taler_config(args.config.as_deref()); + let (taler_config, currency) = load_taler_config(args.config.as_deref()); let ipc_path = args.datadir.unwrap_or(taler_config.custom); let mut rpc = Rpc::new(ipc_path).unwrap(); @@ -120,8 +120,9 @@ fn main() { let to = H160::from_str(&to).unwrap(); rpc.unlock_account(&from, &passwd).ok(); for amount in amounts { - let amount = Amount::from_str(&format!("ETH:{}{}", fmt, amount)).unwrap(); - let value = taler_to_eth(&amount).unwrap(); + let amount = + Amount::from_str(&format!("{}:{}{}", currency.to_str(), fmt, amount)).unwrap(); + let value = taler_to_eth(&amount, currency).unwrap(); rpc.deposit(from, to, value, rand_slice()).unwrap(); } } @@ -135,8 +136,9 @@ fn main() { let to = H160::from_str(&to).unwrap(); rpc.unlock_account(&from, &passwd).ok(); for amount in amounts { - let amount = Amount::from_str(&format!("ETH:{}{}", fmt, amount)).unwrap(); - let value = taler_to_eth(&amount).unwrap(); + let amount = + Amount::from_str(&format!("{}:{}{}", currency.to_str(), fmt, amount)).unwrap(); + let value = taler_to_eth(&amount, currency).unwrap(); rpc.send_transaction(&TransactionRequest { from, to, diff --git a/eth-wire/src/lib.rs b/eth-wire/src/lib.rs @@ -21,7 +21,14 @@ use std::{ sync::atomic::AtomicU32, }; -use common::{api_common::Amount, config::TalerConfig, log::OrFail, postgres, url::Url}; +use common::{ + api_common::Amount, + config::TalerConfig, + currency::{Currency, CurrencyEth}, + log::{fail, OrFail}, + postgres, + url::Url, +}; use ethereum_types::{Address, H160, H256, U256, U64}; use metadata::{InMetadata, OutMetadata}; use rpc::{hex::Hex, Rpc, RpcClient, RpcStream, Transaction}; @@ -208,7 +215,7 @@ impl SyncState { } const DEFAULT_CONFIRMATION: u16 = 24; -const DEFAULT_BOUNCE_FEE: &str = "ETH:0.00001"; +const DEFAULT_BOUNCE_FEE: &str = "0.00001"; pub struct WireState { pub confirmation: AtomicU32, @@ -221,11 +228,12 @@ pub struct WireState { pub base_url: Url, pub payto: Url, pub db_config: postgres::Config, + pub currency: CurrencyEth, } impl WireState { pub fn load_taler_config(file: Option<&Path>) -> Self { - let taler_config = load_taler_config(file); + let (taler_config, currency) = load_taler_config(file); let init_confirmation = taler_config.confirmation.unwrap_or(DEFAULT_CONFIRMATION) as u32; let payto = taler_config.require_payto(); Self { @@ -233,29 +241,38 @@ impl WireState { max_confirmations: init_confirmation * 2, address: eth_payto_addr(&payto).unwrap(), ipc_path: taler_config.custom, - bounce_fee: config_bounce_fee(&taler_config.bounce_fee), + 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, payto, + currency, } } } // Load taler config with eth-wire specific config -pub fn load_taler_config(file: Option<&Path>) -> TalerConfig<PathBuf> { - TalerConfig::load_with_custom(file, |dep| { +pub fn load_taler_config(file: Option<&Path>) -> (TalerConfig<PathBuf>, CurrencyEth) { + let config = TalerConfig::load_with_custom(file, |dep| { common::config::path(dep, "IPC_PATH").unwrap_or_else(default_data_dir) - }) + }); + let currency = match config.currency { + Currency::ETH(it) => it, + _ => fail(format!( + "currency {} is not supported by eth-wire", + config.currency.to_str() + )), + }; + (config, currency) } // Parse ethereum value from config bounce fee -fn config_bounce_fee(bounce_fee: &Option<String>) -> U256 { +fn config_bounce_fee(bounce_fee: &Option<String>, currency: CurrencyEth) -> U256 { let config = bounce_fee.as_deref().unwrap_or(DEFAULT_BOUNCE_FEE); - Amount::from_str(config) + Amount::from_str(&format!("{}:{}", currency.to_str(), config)) .map_err(|s| s.to_string()) - .and_then(|a| taler_to_eth(&a)) + .and_then(|a| taler_to_eth(&a, currency)) .or_fail(|a| { format!( "config value BOUNCE_FEE={} is not a valid ethereum amount: {}", diff --git a/eth-wire/src/loops/worker.rs b/eth-wire/src/loops/worker.rs @@ -273,7 +273,7 @@ fn sync_chain_incoming_confirmed( Ok(metadata) => match metadata { InMetadata::Deposit { reserve_pub } => { let date = SystemTime::now(); - let amount = eth_to_taler(&tx.value); + let amount = eth_to_taler(&tx.value, state.currency); let credit_addr = tx.from.expect("Not coinbase"); let nb = db.execute("INSERT INTO tx_in (_date, amount, reserve_pub, debit_acc, credit_acc) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (reserve_pub) DO NOTHING ", &[ &date, &amount.to_string(), &reserve_pub.as_ref(), &eth_payto_url(&credit_addr).as_ref(), &state.payto.as_ref() @@ -306,7 +306,7 @@ fn sync_chain_outgoing(tx: &SyncTransaction, db: &mut Client, state: &WireState) match OutMetadata::decode(&tx.input) { Ok(metadata) => match metadata { OutMetadata::Withdraw { wtid, .. } => { - let amount = eth_to_taler(&tx.value); + let amount = eth_to_taler(&tx.value, state.currency); let credit_addr = tx.to.unwrap(); // Get previous out tx let row = db.query_opt( @@ -422,7 +422,7 @@ fn withdraw(db: &mut Client, rpc: &mut Rpc, state: &WireState) -> LoopResult<boo )?; if let Some(row) = &row { let id: i32 = row.get(0); - let amount = sql_eth_amount(row, 1); + let amount = sql_eth_amount(row, 1, state.currency); let wtid: [u8; 32] = sql_array(row, 2); let addr = sql_addr(row, 3); let url = sql_url(row, 4); @@ -432,7 +432,7 @@ fn withdraw(db: &mut Client, rpc: &mut Rpc, state: &WireState) -> LoopResult<boo "UPDATE tx_out SET status=$1, txid=$2, sent=now() WHERE id=$3", &[&(WithdrawStatus::Sent as i16), &tx_id.as_ref(), &id], )?; - let amount = eth_to_taler(&amount); + let amount = eth_to_taler(&amount, state.currency); info!( ">> {} {} in {} to {}", amount, diff --git a/eth-wire/src/main.rs b/eth-wire/src/main.rs @@ -59,7 +59,7 @@ fn main() { fn init(config: Option<PathBuf>, init: Init) { // Parse taler config - let taler_config = load_taler_config(config.as_deref()); + let (taler_config, _) = load_taler_config(config.as_deref()); // Connect to database let mut db = taler_config .db_config diff --git a/eth-wire/src/sql.rs b/eth-wire/src/sql.rs @@ -14,32 +14,37 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ use common::{ + currency::CurrencyEth, + log::OrFail, postgres::Row, sql::{sql_amount, sql_array, sql_url}, }; use eth_wire::taler_util::{eth_payto_addr, taler_to_eth}; use ethereum_types::{H160, H256, U256}; -pub fn sql_eth_amount(row: &Row, idx: usize) -> U256 { +/// Ethereum amount from sql +pub fn sql_eth_amount(row: &Row, idx: usize, currency: CurrencyEth) -> U256 { let amount = sql_amount(row, idx); - taler_to_eth(&amount).unwrap_or_else(|_| { - panic!( + taler_to_eth(&amount, currency).or_fail(|_| { + format!( "Database invariant: expected an ethereum amount got {}", amount ) }) } +/// Ethereum address from sql pub fn sql_addr(row: &Row, idx: usize) -> H160 { let url = sql_url(row, idx); - eth_payto_addr(&url).unwrap_or_else(|_| { - panic!( + eth_payto_addr(&url).or_fail(|_| { + format!( "Database invariant: expected an ethereum payto url got {}", url ) }) } +/// Ethereum hash from sql pub fn sql_hash(row: &Row, idx: usize) -> H256 { let array: [u8; 32] = sql_array(row, idx); H256::from_slice(&array) diff --git a/eth-wire/src/taler_util.rs b/eth-wire/src/taler_util.rs @@ -15,8 +15,8 @@ */ use std::str::FromStr; +use common::{api_common::Amount, currency::CurrencyEth, url::Url}; use ethereum_types::{Address, U256}; -use common::{api_common::Amount, url::Url}; pub const WEI: u64 = 1_000_000_000_000_000_000; pub const TRUNC: u64 = 10_000_000_000; @@ -43,18 +43,22 @@ pub fn eth_payto_addr(url: &Url) -> Result<Address, String> { } /// Transform a eth amount into a taler amount -pub fn eth_to_taler(amount: &U256) -> Amount { +pub fn eth_to_taler(amount: &U256, currency: CurrencyEth) -> Amount { return Amount::new( - "ETH", + currency.to_str(), (amount / WEI).as_u64(), ((amount % WEI) / TRUNC).as_u32(), ); } /// Transform a eth amount into a btc amount -pub fn taler_to_eth(amount: &Amount) -> Result<U256, String> { - if amount.currency != "ETH" { - return Err(format!("expected ETH for {}", amount.currency)); +pub fn taler_to_eth(amount: &Amount, currency: CurrencyEth) -> Result<U256, String> { + if amount.currency != currency.to_str() { + return Err(format!( + "expected currency {} got {}", + currency.to_str(), + amount.currency + )); } Ok(U256::from(amount.value) * WEI + U256::from(amount.fraction) * TRUNC) diff --git a/instrumentation/src/btc.rs b/instrumentation/src/btc.rs @@ -112,7 +112,7 @@ pub fn btc_test(config: Option<&Path>, base_url: &str) { let test_amount = Amount::from_sat(2000); let min_send_amount = rpc_utils::segwit_min_amount(); // To small to send back let min_bounce_amount = min_send_amount + Amount::from_sat(999); // To small after bounce fee - let taler_test_amount = btc_to_taler(&test_amount.to_signed().unwrap()); + let taler_test_amount = btc_to_taler(&test_amount.to_signed().unwrap(), state.currency); println!("Send transaction"); let reserve_pub_key = rand_slice(); diff --git a/instrumentation/src/eth.rs b/instrumentation/src/eth.rs @@ -28,7 +28,7 @@ pub fn eth_test(config: Option<&Path>, base_url: &str) { // TODO eth network check let min_fund = U256::from(100_000 * TRUNC); let test_amount = U256::from(20_000 * TRUNC); - let taler_test_amount = eth_to_taler(&test_amount); + let taler_test_amount = eth_to_taler(&test_amount, state.currency); let mut rpc = Rpc::new(state.ipc_path).unwrap(); diff --git a/instrumentation/src/main.rs b/instrumentation/src/main.rs @@ -22,6 +22,7 @@ use common::{ api_common::{Amount, Base32}, api_wire::{IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferRequest}, config::TalerConfig, + currency::Currency, rand_slice, url::Url, }; @@ -95,9 +96,10 @@ pub fn main() { let taler_config = TalerConfig::load(args.config.as_deref()); let base_url = format!("http://localhost:{}", taler_config.port); - match taler_config.currency.as_str() { - "BTC" => btc_test(args.config.as_deref(), &base_url), - "ETH" => eth_test(args.config.as_deref(), &base_url), - _ => unimplemented!(), + match taler_config.currency { + Currency::BTC(_) => btc_test(args.config.as_deref(), &base_url), + Currency::ETH(_) => eth_test(args.config.as_deref(), &base_url), } + + println!("Instrumentation test successful"); } diff --git a/test/btc/bumpfee.sh b/test/btc/bumpfee.sh @@ -37,7 +37,7 @@ echo -n "Making wire transfer from exchange:" taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://bitcoin/$CLIENT \ - -a BTC:0.004 > /dev/null + -a $CURRENCY:0.004 > /dev/null sleep 1 check_balance 5.79983389 4.19599801 echo " OK" @@ -62,7 +62,7 @@ echo -n "Making wire transfer from exchange:" taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://bitcoin/$CLIENT \ - -a BTC:0.004 > /dev/null + -a $CURRENCY:0.004 > /dev/null sleep 1 check_balance 5.80383389 4.19196020 echo " OK" @@ -92,7 +92,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://bitcoin/$CLIENT \ - -a BTC:0.00$n > /dev/null + -a $CURRENCY:0.00$n > /dev/null done sleep 5 echo " OK" diff --git a/test/btc/conflict.sh b/test/btc/conflict.sh @@ -32,7 +32,7 @@ echo -n "Making wire transfer from exchange:" taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://bitcoin/$CLIENT \ - -a BTC:0.004 > /dev/null + -a $CURRENCY:0.004 > /dev/null sleep 1 check_balance 9.95799209 0.03799801 echo " OK" @@ -47,7 +47,7 @@ echo -n "Generate conflict:" taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://bitcoin/$CLIENT \ - -a BTC:0.005 > /dev/null + -a $CURRENCY:0.005 > /dev/null sleep 5 restart_btc mine_btc diff --git a/test/btc/lifetime.sh b/test/btc/lifetime.sh @@ -39,7 +39,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://bitcoin/$CLIENT \ - -a BTC:0.0000$n &> /dev/null || break; + -a $CURRENCY:0.0000$n &> /dev/null || break; done echo " OK" diff --git a/test/btc/maxfee.sh b/test/btc/maxfee.sh @@ -40,7 +40,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://bitcoin/$CLIENT \ - -a BTC:0.0$n > /dev/null + -a $CURRENCY:0.0$n > /dev/null done sleep 5 mine_btc diff --git a/test/btc/reconnect.sh b/test/btc/reconnect.sh @@ -54,7 +54,7 @@ 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 + -a $CURRENCY:0.00002 > /dev/null sleep 1 mine_btc check_balance 9.99990892 0.00007001 diff --git a/test/btc/reorg.sh b/test/btc/reorg.sh @@ -60,7 +60,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://bitcoin/$CLIENT \ - -a BTC:0.0000$n > /dev/null + -a $CURRENCY:0.0000$n > /dev/null done sleep 1 mine_btc # Mine transactions diff --git a/test/btc/stress.sh b/test/btc/stress.sh @@ -45,7 +45,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://bitcoin/$CLIENT \ - -a BTC:0.0000$n > /dev/null + -a $CURRENCY:0.0000$n > /dev/null done sleep 10 # Give time for btc-wire worker to process next_btc # Mine transactions diff --git a/test/btc/wire.sh b/test/btc/wire.sh @@ -42,7 +42,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://bitcoin/$CLIENT \ - -a BTC:0.0000$n > /dev/null + -a $CURRENCY:0.0000$n > /dev/null done sleep 15 mine_btc # Mine transactions diff --git a/test/common.sh b/test/common.sh @@ -51,7 +51,7 @@ function load_config() { echo -e "IPC_PATH = ${WIRE_DIR}" >> $CONF source <(grep = $CONF | sed 's/ *= */=/' | sed 's/=\(.*\)/="\1"/g1') BANK_ENDPOINT=http://127.0.0.1:$PORT/ - if [ "$CURRENCY" == "BTC" ]; then + if [[ "$CURRENCY" =~ "BTC" ]]; then WIRE_CLI="btc-wire -c $CONF" WIRE_UTILS="btc-wire-utils -c $CONF" WIRE_UTILS2="btc-wire-utils -c $CONF -d $WIRE_DIR2" diff --git a/test/conf/taler_btc.conf b/test/conf/taler_btc.conf @@ -1,5 +1,5 @@ [taler] -CURRENCY = BTC +CURRENCY = DEV-BTC [exchange] BASE_URL = http://test.com @@ -8,4 +8,4 @@ BASE_URL = http://test.com DB_URL = postgres://localhost:5454/postgres?user=postgres&password=password PORT = 8060 PAYTO = payto://bitcoin/bcrt1qgkgxkjj27g3f7s87mcvjjsghay7gh34cx39prj -CONFIRMATION = 3 -\ No newline at end of file +CONFIRMATION = 3 diff --git a/test/conf/taler_btc_bump.conf b/test/conf/taler_btc_bump.conf @@ -1,5 +1,5 @@ [taler] -CURRENCY = BTC +CURRENCY = DEV-BTC [exchange] BASE_URL = http://test.com @@ -9,4 +9,5 @@ DB_URL = postgres://localhost:5454/postgres?user=postgres&password=passwo PORT = 8060 PAYTO = payto://bitcoin/bcrt1qgkgxkjj27g3f7s87mcvjjsghay7gh34cx39prj CONFIRMATION = 3 -BUMP_DELAY = 5 -\ No newline at end of file +BUMP_DELAY = 5 +BOUNCE_FEE = 0.00001 +\ No newline at end of file diff --git a/test/conf/taler_btc_lifetime.conf b/test/conf/taler_btc_lifetime.conf @@ -1,5 +1,5 @@ [taler] -CURRENCY = BTC +CURRENCY = DEV-BTC [exchange] BASE_URL = http://test.com diff --git a/test/conf/taler_eth.conf b/test/conf/taler_eth.conf @@ -1,5 +1,5 @@ [taler] -CURRENCY = ETH +CURRENCY = DEV-ETH [exchange] BASE_URL = http://test.com diff --git a/test/conf/taler_eth_bump.conf b/test/conf/taler_eth_bump.conf @@ -1,5 +1,5 @@ [taler] -CURRENCY = ETH +CURRENCY = DEV-ETH [exchange] BASE_URL = http://test.com diff --git a/test/conf/taler_eth_lifetime.conf b/test/conf/taler_eth_lifetime.conf @@ -1,5 +1,5 @@ [taler] -CURRENCY = ETH +CURRENCY = DEV-ETH [exchange] BASE_URL = http://test.com diff --git a/test/eth/bumpfee.sh b/test/eth/bumpfee.sh @@ -36,7 +36,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://ethereum/$CLIENT \ - -a ETH:0.0000$n > /dev/null + -a $CURRENCY:0.0000$n > /dev/null done sleep 1 echo "OK" @@ -62,7 +62,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://ethereum/$CLIENT \ - -a ETH:0.0000$n > /dev/null + -a $CURRENCY:0.0000$n > /dev/null done sleep 1 echo "OK" @@ -91,7 +91,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://ethereum/$CLIENT \ - -a ETH:0.0000$n > /dev/null + -a $CURRENCY:0.0000$n > /dev/null done sleep 1 echo "OK" diff --git a/test/eth/lifetime.sh b/test/eth/lifetime.sh @@ -36,7 +36,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://ethereum/$CLIENT \ - -a ETH:0.0000$n &> /dev/null || break; + -a $CURRENCY:0.0000$n &> /dev/null || break; done sleep 1 echo " OK" diff --git a/test/eth/maxfee.sh b/test/eth/maxfee.sh @@ -40,7 +40,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://ethereum/$CLIENT \ - -a ETH:0.0000$n > /dev/null + -a $CURRENCY:0.0000$n > /dev/null done sleep 1 mine_eth # Mine transactions diff --git a/test/eth/reconnect.sh b/test/eth/reconnect.sh @@ -57,7 +57,7 @@ echo -n "Making wire transfer from exchange:" taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://ethereum/$CLIENT \ - -a ETH:0.00002 > /dev/null + -a $CURRENCY:0.00002 > /dev/null sleep 1 mine_eth # Mine transactions check_balance_eth 999992800 7200 diff --git a/test/eth/reorg.sh b/test/eth/reorg.sh @@ -57,7 +57,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://ethereum/$CLIENT \ - -a ETH:0.0000$n > /dev/null + -a $CURRENCY:0.0000$n > /dev/null done sleep 1 mine_eth # Mine transactions diff --git a/test/eth/stress.sh b/test/eth/stress.sh @@ -42,7 +42,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://ethereum/$CLIENT \ - -a ETH:0.0000$n > /dev/null + -a $CURRENCY:0.0000$n > /dev/null done sleep 1 next_eth # Mine transactions diff --git a/test/eth/wire.sh b/test/eth/wire.sh @@ -39,7 +39,7 @@ for n in `$SEQ`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://ethereum/$CLIENT \ - -a ETH:0.0000$n > /dev/null + -a $CURRENCY:0.0000$n > /dev/null done sleep 1 mine_eth # Mine transactions diff --git a/test/gateway/api.sh b/test/gateway/api.sh @@ -36,14 +36,14 @@ for n in `seq 1 9`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -D payto://bitcoin/$ADDRESS \ - -a BTC:0.0000$n > /dev/null + -a $CURRENCY:0.0000$n > /dev/null done echo " OK" echo -n "Requesting exchange incoming transaction list:" ALL=`taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -i` for n in `seq 1 9`; do - echo $ALL | grep BTC:0.0000$n > /dev/null + echo $ALL | grep $CURRENCY:0.0000$n > /dev/null done echo " OK" @@ -52,7 +52,7 @@ for n in `seq 1 9`; do taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://bitcoin/$ADDRESS \ - -a BTC:0.0000$n > /dev/null + -a $CURRENCY:0.0000$n > /dev/null done echo " OK" @@ -60,7 +60,7 @@ echo " OK" echo -n "Requesting exchange's outgoing transaction list:" ALL=`taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -o` for n in `seq 1 9`; do - echo $ALL | grep BTC:0.0000$n > /dev/null + echo $ALL | grep $CURRENCY:0.0000$n > /dev/null done echo " OK" @@ -76,12 +76,12 @@ echo "----- Request format -----" echo -n "Bad payto url:" for bad_payto in http://bitcoin/$ADDRESS payto://btc/$ADDRESS payto://bitcoin/$ADDRESS?id=admin payto://bitcoin/$ADDRESS#admin; do - taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -C $bad_payto -a BTC:0.00042 2>&1 | grep -q "(400/24)" && echo -n " OK" || echo " Failed" + taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -C $bad_payto -a $CURRENCY:0.00042 2>&1 | grep -q "(400/24)" && echo -n " OK" || echo " Failed" done echo "" echo -n "Bad bitcoin address:" -taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -C payto://bitcoin/42$ADDRESS -a BTC:0.00042 2>&1 | grep -q "(400/24)" && echo " OK" || echo " Failed" +taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -C payto://bitcoin/42$ADDRESS -a $CURRENCY:0.00042 2>&1 | grep -q "(400/24)" && echo " OK" || echo " Failed" echo -n "Bad transaction amount:" taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -C payto://bitcoin/$ADDRESS -a ATC:0.00042 2>&1 | grep -q "(400/26)" && echo " OK" || echo " Failed" @@ -106,12 +106,12 @@ for endpoint in incoming outgoing; do done echo "----- Transfer idempotence -----" -DATA="{\"request_uid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"amount\":\"BTC:0.000034\",\"exchange_base_url\":\"$BASE_URL\",\"wtid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"credit_account\":\"payto://bitcoin/$ADDRESS\"}" +DATA="{\"request_uid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"amount\":\"$CURRENCY:0.000034\",\"exchange_base_url\":\"$BASE_URL\",\"wtid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"credit_account\":\"payto://bitcoin/$ADDRESS\"}" echo -n "Same:" test `curl -w %{http_code} -s -o /dev/null -H "Content-Type: application/json" -d $DATA ${BANK_ENDPOINT}transfer` -eq 200 && echo -n " OK" || echo -n " Failed" test `curl -w %{http_code} -s -o /dev/null -H "Content-Type: application/json" -d $DATA ${BANK_ENDPOINT}transfer` -eq 200 && echo " OK" || echo " Failed" echo -n "Collision:" -DATA="{\"request_uid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"amount\":\"BTC:0.000042\",\"exchange_base_url\":\"$BASE_URL\",\"wtid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"credit_account\":\"payto://bitcoin/$ADDRESS\"}" +DATA="{\"request_uid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"amount\":\"$CURRENCY:0.000042\",\"exchange_base_url\":\"$BASE_URL\",\"wtid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"credit_account\":\"payto://bitcoin/$ADDRESS\"}" test `curl -w %{http_code} -s -o /dev/null -H "Content-Type: application/json" -d $DATA ${BANK_ENDPOINT}transfer` -eq 409 && echo " OK" || echo " Failed" echo "----- Security -----" diff --git a/wire-gateway/Cargo.toml b/wire-gateway/Cargo.toml @@ -32,17 +32,12 @@ listenfd = "0.5.0" # Common lib common = { path = "../common" } # Bitcoin types -bitcoin = { version = "0.27.1", optional = true } +bitcoin = { version = "0.27.1" } # Euthereum types -ethereum-types = { version = "0.13.1", default-features = false, optional = true } +ethereum-types = { version = "0.13.1", default-features = false } # Cli args parser clap = { version = "3.1.5", features = ["derive"] } [features] -default = ["btc", "eth"] # Enable test admin endpoint test = [] -# Support btc-wire -btc = ["bitcoin"] -# Support eth-wire -eth = ["ethereum-types"] diff --git a/wire-gateway/src/main.rs b/wire-gateway/src/main.rs @@ -21,8 +21,12 @@ use common::{ OutgoingHistory, TransferRequest, TransferResponse, }, config::TalerConfig, + currency::Currency, error_codes::ErrorCode, - log::log::{error, info, log, Level}, + log::{ + fail, + log::{error, info, log, Level}, + }, postgres::{self, fallible_iterator::FallibleIterator}, sql::{sql_amount, sql_array, sql_safe_u64, sql_url}, url::Url, @@ -53,8 +57,7 @@ struct ServerState { pool: Pool, db_config: postgres::Config, payto: Url, - currency: String, - payto_check: fn(&Url) -> bool, + currency: Currency, notify: Notify, lifetime: Option<AtomicU32>, status: AtomicBool, @@ -134,13 +137,6 @@ async fn main() { let payto = taler_config.require_payto(); let state = ServerState { pool, - payto_check: match taler_config.currency.as_str() { - #[cfg(feature = "btc")] - "BTC" => check_pay_to_btc, - #[cfg(feature = "eth")] - "ETH" => check_pay_to_eth, - currency => unimplemented!("Unsupported currency {}", currency), - }, notify: Notify::new(), lifetime: taler_config.http_lifetime.map(AtomicU32::new), status: AtomicBool::new(true), @@ -199,7 +195,7 @@ async fn main() { info!("Server listening on unix domain socket {:?}", path); if let Err(err) = std::fs::remove_file(&path) { if err.kind() != std::io::ErrorKind::NotFound { - panic!("{}", err); + fail(format_args!("{}", err)); } } let server = Server::bind_unix(path) @@ -221,8 +217,15 @@ async fn main() { }; } +/// Check if an url if a valid payto url for the configured currency +fn check_payto(url: &Url, currency: Currency) -> bool { + match currency { + Currency::ETH(_) => check_pay_to_eth(url), + Currency::BTC(_) => check_pay_to_btc(url), + } +} + /// Check if an url is a valid bitcoin payto url -#[cfg(feature = "btc")] fn check_pay_to_btc(url: &Url) -> bool { return url.domain() == Some("bitcoin") && url.scheme() == "payto" @@ -234,7 +237,6 @@ fn check_pay_to_btc(url: &Url) -> bool { } /// Check if an url is a valid ethereum payto url -#[cfg(feature = "eth")] fn check_pay_to_eth(url: &Url) -> bool { return url.domain() == Some("ethereum") && url.scheme() == "payto" @@ -306,13 +308,13 @@ async fn router( StatusCode::BAD_REQUEST, ErrorCode::GENERIC_PARAMETER_MALFORMED, )?; - if !(state.payto_check)(&request.credit_account) { + if !check_payto(&request.credit_account, state.currency) { return Err(ServerError::code( StatusCode::BAD_REQUEST, ErrorCode::GENERIC_PAYTO_URI_MALFORMED, )); } - if request.amount.currency != state.currency { + if Currency::from_str(&request.amount.currency) != Ok(state.currency) { return Err(ServerError::code( StatusCode::BAD_REQUEST, ErrorCode::GENERIC_PARAMETER_MALFORMED,