depolymerization

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

commit f83ecda42ee694ade4e5f4e16ce019d92b3754f4
parent ec1d6043820b5a5d37e46fe2d26f228528663d60
Author: Antoine A <>
Date:   Thu, 24 Feb 2022 17:19:11 +0100

Add eth instrumentation test and fix eth bounce

Diffstat:
MCargo.lock | 2++
Mbtc-wire/src/rpc.rs | 4+---
Meth-wire/src/bin/eth-wire-utils.rs | 6++----
Meth-wire/src/lib.rs | 114++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Meth-wire/src/loops/worker.rs | 73+++----------------------------------------------------------------------
Meth-wire/src/main.rs | 88+++++++++++++++++++++++++++++++++++++++----------------------------------------
Meth-wire/src/rpc.rs | 29+++++++++++++++++++++++------
Meth-wire/src/taler_util.rs | 4++--
Minstrumentation/Cargo.toml | 7+++----
Minstrumentation/src/btc.rs | 65+++++++++++------------------------------------------------------
Ainstrumentation/src/eth.rs | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minstrumentation/src/main.rs | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
12 files changed, 434 insertions(+), 198 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -893,6 +893,8 @@ dependencies = [ "clap 3.1.2", "common", "eth-wire", + "ethereum-types", + "hex", "ureq", ] diff --git a/btc-wire/src/rpc.rs b/btc-wire/src/rpc.rs @@ -46,9 +46,7 @@ pub fn auto_rpc_wallet(config: BitcoinConfig, wallet: &'static str) -> AutoRpcWa let mut rpc = Rpc::wallet(config, wallet) .map_err(|err| error!("connect RPC: {}", err)) .ok()?; - rpc.load_wallet(wallet) - .map_err(|err| error!("connect RPC: {}", err)) - .ok()?; + rpc.load_wallet(wallet).ok(); rpc.unlock_wallet(&password()) .map_err(|err| error!("connect RPC: {}", err)) .ok()?; diff --git a/eth-wire/src/bin/eth-wire-utils.rs b/eth-wire/src/bin/eth-wire-utils.rs @@ -108,7 +108,7 @@ fn main() { .as_ref() .and_then(|it| it.data_dir.as_deref()) .or(args.datadir.as_deref()); - let mut rpc = Rpc::new(data_dir.unwrap().join("geth.ipc")).unwrap(); + let mut rpc = Rpc::new(data_dir.unwrap_or(&PathBuf::from("/tmp/")).join("geth.ipc")).unwrap(); let passwd = password(); match args.cmd { Cmd::Deposit(TransactionCmd { @@ -143,7 +143,6 @@ fn main() { to, value, nonce: None, - gas: None, gas_price: None, data: Hex(vec![]), }) @@ -188,7 +187,7 @@ fn main() { } Cmd::Balance { addr } => { let addr = H160::from_str(&addr).unwrap(); - let balance = rpc.balance(&addr).unwrap(); + let balance = rpc.get_balance(&addr).unwrap(); println!("{}", (balance / 10_000_000_000u64).as_u64()); } Cmd::Connect { datadir } => { @@ -215,7 +214,6 @@ fn main() { from, to: tx.to.unwrap(), value: U256::zero(), - gas: None, gas_price: Some(U256::from(1u8)), // Bigger gas price to replace fee data: Hex(vec![]), nonce: Some(tx.nonce), diff --git a/eth-wire/src/lib.rs b/eth-wire/src/lib.rs @@ -14,17 +14,18 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use common::url::Url; -use ethereum_types::{Address, H256, U256, U64}; +use std::{str::FromStr, sync::atomic::AtomicU16}; + +use common::{api_common::Amount, config::EthConfig, url::Url}; +use ethereum_types::{Address, H160, H256, U256, U64}; use metadata::{InMetadata, OutMetadata}; -use rpc::hex::Hex; +use rpc::{hex::Hex, Transaction}; +use taler_util::{eth_payto_addr, taler_to_eth}; pub mod metadata; pub mod rpc; pub mod taler_util; -const GWEI: u64 = 1_000_000_000; - impl rpc::Rpc { pub fn deposit( &mut self, @@ -39,7 +40,6 @@ impl rpc::Rpc { to, value, nonce: None, - gas: None, gas_price: None, data: Hex(metadata.encode()), }) @@ -59,7 +59,6 @@ impl rpc::Rpc { to, value, nonce: None, - gas: None, gas_price: None, data: Hex(metadata.encode()), }) @@ -76,7 +75,6 @@ impl rpc::Rpc { to: tx.from.expect("Cannot bounce coinbase transaction"), value: bounce_value, nonce: None, - gas: None, gas_price: None, data: Hex(metadata.encode()), }; @@ -85,13 +83,80 @@ impl rpc::Rpc { // Deduce fee price from bounced value request.value = request .value - .saturating_sub(fill.tx.gas * GWEI * fill.tx.gas_price); + .saturating_sub(fill.tx.gas * fill.tx.gas_price.or(fill.tx.max_fee_per_gas).unwrap()); Ok(if request.value.is_zero() { None } else { Some(self.send_transaction(&request)?) }) } + + /// List new and removed transaction since the last sync state, returning a new sync state + pub fn list_since_sync_state( + &mut self, + address: &Address, + state: SyncState, + min_confirmation: u16, + ) -> rpc::Result<(Vec<(Transaction, u16)>, Vec<(Transaction, u16)>, SyncState)> { + let match_tx = |txs: Vec<Transaction>, conf: u16| -> Vec<(Transaction, u16)> { + txs.into_iter() + .filter_map(|tx| { + (tx.from == Some(*address) || tx.to == Some(*address)).then(|| (tx, conf)) + }) + .collect() + }; + + let mut txs = Vec::new(); + let mut removed = Vec::new(); + + // Add pending transaction + txs.extend(match_tx(self.pending_transactions()?, 0)); + + let latest = self.latest_block()?; + + let mut confirmation = 1; + let mut chain_cursor = latest.clone(); + + // Move until tip height + while chain_cursor.number.unwrap() != state.tip_height { + txs.extend(match_tx(chain_cursor.transactions, confirmation)); + chain_cursor = self.block(&chain_cursor.parent_hash)?.unwrap(); + confirmation += 1; + } + + // Check if fork + if chain_cursor.hash.unwrap() != state.tip_hash { + let mut fork_cursor = self.block(&state.tip_hash)?.unwrap(); + // Move until found common parent + while fork_cursor.hash != chain_cursor.hash { + txs.extend(match_tx(chain_cursor.transactions, confirmation)); + removed.extend(match_tx(fork_cursor.transactions, confirmation)); + chain_cursor = self.block(&chain_cursor.parent_hash)?.unwrap(); + fork_cursor = self.block(&fork_cursor.parent_hash)?.unwrap(); + confirmation += 1; + } + } + + // Move until last conf + while chain_cursor.number.unwrap() > state.conf_height { + txs.extend(match_tx(chain_cursor.transactions, confirmation)); + chain_cursor = self.block(&chain_cursor.parent_hash)?.unwrap(); + confirmation += 1; + } + + Ok(( + txs, + removed, + SyncState { + tip_hash: latest.hash.unwrap(), + tip_height: latest.number.unwrap(), + conf_height: latest + .number + .unwrap() + .saturating_sub(U64::from(min_confirmation)), + }, + )) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -119,6 +184,37 @@ impl SyncState { } } +const DEFAULT_CONFIRMATION: u16 = 24; +const DEFAULT_BOUNCE_FEE: &'static str = "ETH:0.00001"; +pub struct WireState { + pub confirmation: AtomicU16, + pub max_confirmations: u16, + pub address: H160, + pub config: EthConfig, + pub bounce_fee: U256, +} + +impl WireState { + pub fn from_taler_config(config: EthConfig) -> Self { + let init_confirmation = config.confirmation.unwrap_or(DEFAULT_CONFIRMATION); + Self { + confirmation: AtomicU16::new(init_confirmation), + max_confirmations: init_confirmation * 2, + address: eth_payto_addr(&config.payto).unwrap(), + bounce_fee: config_bounce_fee(&config), + config, + } + } +} + +fn config_bounce_fee(config: &EthConfig) -> U256 { + let config = config.bounce_fee.as_deref().unwrap_or(DEFAULT_BOUNCE_FEE); + Amount::from_str(&config) + .ok() + .and_then(|a| taler_to_eth(&a).ok()) + .expect("config value BOUNCE_FEE is no a valid ethereum amount") +} + #[cfg(test)] mod test { use common::{rand::random, rand_slice}; diff --git a/eth-wire/src/loops/worker.rs b/eth-wire/src/loops/worker.rs @@ -29,7 +29,7 @@ use eth_wire::{ taler_util::{eth_payto_url, eth_to_taler}, SyncState, }; -use ethereum_types::{Address, H256, U256, U64}; +use ethereum_types::{Address, H256, U256}; use crate::{ fail_point::fail_point, @@ -119,73 +119,6 @@ pub fn worker(mut rpc: AutoRpcWallet, mut db: AutoReconnectDb, state: &WireState } } -/// List new and removed transaction since the last sync state, returning a new sync state -fn list_since_block_state( - rpc: &mut Rpc, - address: &Address, - state: SyncState, - min_confirmation: u16, -) -> LoopResult<(Vec<(Transaction, u16)>, Vec<(Transaction, u16)>, SyncState)> { - let match_tx = |txs: Vec<Transaction>, conf: u16| -> Vec<(Transaction, u16)> { - txs.into_iter() - .filter_map(|tx| { - (tx.from == Some(*address) || tx.to == Some(*address)).then(|| (tx, conf)) - }) - .collect() - }; - - let mut txs = Vec::new(); - let mut removed = Vec::new(); - - // Add pending transaction - txs.extend(match_tx(rpc.pending_transactions()?, 0)); - - let latest = rpc.latest_block()?; - - let mut confirmation = 1; - let mut chain_cursor = latest.clone(); - - // Move until tip height - while chain_cursor.number.unwrap() != state.tip_height { - txs.extend(match_tx(chain_cursor.transactions, confirmation)); - chain_cursor = rpc.block(&chain_cursor.parent_hash)?.unwrap(); - confirmation += 1; - } - - // Check if fork - if chain_cursor.hash.unwrap() != state.tip_hash { - let mut fork_cursor = rpc.block(&state.tip_hash)?.unwrap(); - // Move until found common parent - while fork_cursor.hash != chain_cursor.hash { - txs.extend(match_tx(chain_cursor.transactions, confirmation)); - removed.extend(match_tx(fork_cursor.transactions, confirmation)); - chain_cursor = rpc.block(&chain_cursor.parent_hash)?.unwrap(); - fork_cursor = rpc.block(&fork_cursor.parent_hash)?.unwrap(); - confirmation += 1; - } - } - - // Move until last conf - while chain_cursor.number.unwrap() > state.conf_height { - txs.extend(match_tx(chain_cursor.transactions, confirmation)); - chain_cursor = rpc.block(&chain_cursor.parent_hash)?.unwrap(); - confirmation += 1; - } - - Ok(( - txs, - removed, - SyncState { - tip_hash: latest.hash.unwrap(), - tip_height: latest.number.unwrap(), - conf_height: latest - .number - .unwrap() - .saturating_sub(U64::from(min_confirmation)), - }, - )) -} - /// Parse new transactions, return true if the database is up to date with the latest mined block fn sync_chain( rpc: &mut Rpc, @@ -199,7 +132,7 @@ fn sync_chain( let min_confirmations = state.confirmation.load(Ordering::SeqCst); let (txs, removed, next_state) = - list_since_block_state(rpc, &state.address, block, min_confirmations)?; + rpc.list_since_sync_state(&state.address, block, min_confirmations)?; // Check if a confirmed incoming transaction have been removed by a blockchain reorganisation let new_status = sync_chain_removed(&txs, &removed, db, &state.address, min_confirmations)?; @@ -520,7 +453,7 @@ fn bounce(db: &mut Client, rpc: &mut Rpc, fee: U256) -> LoopResult<bool> { "UPDATE bounce SET status=$1 WHERE id=$2", &[&(BounceStatus::Ignored as i16), &id], )?; - info!("|| (ignore) {} ", &bounced); + info!("|| (ignore) {} ", hex::encode(bounced)); } } } diff --git a/eth-wire/src/main.rs b/eth-wire/src/main.rs @@ -14,47 +14,26 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{path::PathBuf, str::FromStr, sync::atomic::AtomicU16}; +use std::path::PathBuf; use clap::StructOpt; use common::{ - api_common::Amount, - config::{load_eth_config, Config, CoreConfig, EthConfig}, + config::{load_eth_config, Config, CoreConfig}, named_spawn, password, postgres::{Client, NoTls}, reconnect::auto_reconnect_db, }; use eth_wire::{ rpc::{auto_rpc_common, auto_rpc_wallet, Rpc}, - taler_util::{eth_payto_addr, taler_to_eth}, - SyncState, + SyncState, WireState, }; -use ethereum_types::{H160, U256}; +use ethereum_types::H160; use loops::{analysis::analysis, watcher::watcher, worker::worker}; mod fail_point; mod loops; mod sql; -const DEFAULT_CONFIRMATION: u16 = 24; -const DEFAULT_BOUNCE_FEE: &'static str = "ETH:0.00001"; - -pub struct WireState { - confirmation: AtomicU16, - max_confirmations: u16, - address: H160, - config: EthConfig, - bounce_fee: U256, -} - -fn config_bounce_fee(config: &EthConfig) -> U256 { - let config = config.bounce_fee.as_deref().unwrap_or(DEFAULT_BOUNCE_FEE); - Amount::from_str(&config) - .ok() - .and_then(|a| taler_to_eth(&a).ok()) - .expect("config value BOUNCE_FEE is no a valid ethereum amount") -} - /// Taler wire for geth #[derive(clap::Parser, Debug)] struct Args { @@ -89,8 +68,13 @@ fn init(config: Option<PathBuf>, init: Init) { // Connect to database let mut db = Client::connect(&config.db_url, NoTls).expect("Failed to connect to database"); // Connect to ethereum node - let mut rpc = Rpc::new(config.data_dir.unwrap().join("geth.ipc")) - .expect("Failed to connect to ethereum RPC server"); + let mut rpc = Rpc::new( + config + .data_dir + .unwrap_or(PathBuf::from("/tmp/")) + .join("geth.ipc"), + ) + .expect("Failed to connect to ethereum RPC server"); match init { Init::Initdb => { @@ -111,12 +95,11 @@ fn init(config: Option<PathBuf>, init: Init) { tip_height: block.number.unwrap(), conf_height: block.number.unwrap(), }; - db - .execute( - "INSERT INTO state (name, value) VALUES ('sync', $1) ON CONFLICT (name) DO NOTHING", - &[&state.to_bytes().as_ref()], - ) - .expect("Failed to update database state"); + db.execute( + "INSERT INTO state (name, value) VALUES ('sync', $1) ON CONFLICT (name) DO NOTHING", + &[&state.to_bytes().as_ref()], + ) + .expect("Failed to update database state"); println!("Database initialised"); } Init::Initwallet => { @@ -172,18 +155,33 @@ fn init(config: Option<PathBuf>, init: Init) { fn run(config: Option<PathBuf>) { let config = load_eth_config(config.as_deref()); - let init_confirmation = config.confirmation.unwrap_or(DEFAULT_CONFIRMATION); - let state: &'static WireState = Box::leak(Box::new(WireState { - confirmation: AtomicU16::new(init_confirmation), - max_confirmations: init_confirmation * 2, - address: eth_payto_addr(&config.payto).unwrap(), - bounce_fee: config_bounce_fee(&config), - config, - })); - - let rpc_worker = auto_rpc_wallet(state.config.core.data_dir.clone().unwrap(), state.address); - let rpc_analysis = auto_rpc_common(state.config.core.data_dir.clone().unwrap()); - let rpc_watcher = auto_rpc_common(state.config.core.data_dir.clone().unwrap()); + let state: &'static _ = Box::leak(Box::new(WireState::from_taler_config(config))); + + let rpc_worker = auto_rpc_wallet( + state + .config + .core + .data_dir + .clone() + .unwrap_or(PathBuf::from("/tmp/")), + state.address, + ); + let rpc_analysis = auto_rpc_common( + state + .config + .core + .data_dir + .clone() + .unwrap_or(PathBuf::from("/tmp/")), + ); + let rpc_watcher = auto_rpc_common( + state + .config + .core + .data_dir + .clone() + .unwrap_or(PathBuf::from("/tmp/")), + ); let db_watcher = auto_reconnect_db(state.config.core.db_url.clone()); let db_analysis = auto_reconnect_db(state.config.core.db_url.clone()); diff --git a/eth-wire/src/rpc.rs b/eth-wire/src/rpc.rs @@ -224,6 +224,13 @@ impl Rpc { } } + pub fn get_transaction_receipt(&mut self, hash: &H256) -> Result<Option<TransactionReceipt>> { + match self.call("eth_getTransactionReceipt", &[hash]) { + Err(Error::Null) => Ok(None), + r => r, + } + } + pub fn fill_transaction(&mut self, params: &TransactionRequest) -> Result<Filled> { self.call("eth_fillTransaction", &[params]) } @@ -271,7 +278,7 @@ impl Rpc { self.call("eth_getBlockByNumber", &("earliest", &true)) } - pub fn balance(&mut self, addr: &Address) -> Result<U256> { + pub fn get_balance(&mut self, addr: &Address) -> Result<U256> { self.call("eth_getBalance", &(addr, "latest")) } @@ -364,6 +371,17 @@ pub struct Transaction { pub input: Hex, } +/// Description of a Transaction, pending or in the chain. +#[derive(Debug, Clone, serde::Deserialize)] +pub struct TransactionReceipt { + /// Gas used by this transaction alone. + #[serde(rename = "gasUsed")] + pub gas_used: U256, + /// Effective gas price + #[serde(rename = "effectiveGasPrice")] + pub effective_gas_price: Option<U256>, +} + /// Fill result #[derive(Debug, serde::Deserialize)] pub struct Filled { @@ -377,7 +395,10 @@ pub struct FilledGas { pub gas: U256, /// Gas price #[serde(rename = "gasPrice")] - pub gas_price: U256, + pub gas_price: Option<U256>, + /// Max fee per gas + #[serde(rename = "maxFeePerGas")] + pub max_fee_per_gas: Option<U256>, } /// Send Transaction Parameters @@ -389,11 +410,7 @@ pub struct TransactionRequest { pub to: Address, /// Transferred value pub value: U256, - /// Supplied gas (None for sensible default) - #[serde(skip_serializing_if = "Option::is_none")] - pub gas: Option<U256>, /// Gas price (None for sensible default) - #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "gasPrice")] pub gas_price: Option<U256>, /// Transaction data diff --git a/eth-wire/src/taler_util.rs b/eth-wire/src/taler_util.rs @@ -18,8 +18,8 @@ use std::str::FromStr; use ethereum_types::{Address, U256}; use common::{api_common::Amount, url::Url}; -const WEI: u64 = 1_000_000_000_000_000_000; -const TRUNC: u64 = 10_000_000_000; +pub const WEI: u64 = 1_000_000_000_000_000_000; +pub const TRUNC: u64 = 10_000_000_000; /// Generate a payto uri from an eth address pub fn eth_payto_url(addr: &Address) -> Url { diff --git a/instrumentation/Cargo.toml b/instrumentation/Cargo.toml @@ -11,11 +11,10 @@ clap = { version = "3.1.1", features = ["derive"] } common = { path = "../common" } # Bitcoin btc-wire = { path = "../btc-wire" } -bitcoin = { version = "0.27.1", features = [ - "std", - "use-serde", -], default-features = false } +bitcoin = { version = "0.27.1", default-features = false } # Ethereum eth-wire = { path = "../eth-wire" } +ethereum-types = { version = "0.13.0", default-features = false } +hex = "0.4.3" # Wire Gateway ureq = { version = "2.4.0", features = ["json"] } diff --git a/instrumentation/src/btc.rs b/instrumentation/src/btc.rs @@ -24,14 +24,9 @@ use btc_wire::{ taler_utils::{btc_payto_url, btc_to_taler}, WireState, }; -use common::{ - api_common::Base32, - api_wire::{IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferRequest}, - config::load_btc_config, - rand_slice, -}; +use common::{config::load_btc_config, rand_slice}; -use crate::print_now; +use crate::{check_incoming, check_outgoing, print_now, transfer}; pub const CLIENT: &str = "client"; pub const WIRE: &str = "wire"; @@ -187,60 +182,22 @@ pub fn btc_test(config: Option<&Path>, base_url: &str) { new_wire_balance ); - println!("Check history"); - let history: IncomingHistory = ureq::get(&format!("{}/history/incoming", base_url)) - .query("delta", "-5") - .call() - .unwrap() - .into_json() - .unwrap(); - assert!(history - .incoming_transactions - .iter() - .find(|h| { - matches!( - h, - IncomingBankTransaction::IncomingReserveTransaction { - reserve_pub, - amount, - .. - } if reserve_pub == &Base32::from(reserve_pub_key) && amount == &taler_test_amount - ) - }) - .is_some()); + check_incoming(base_url, &reserve_pub_key, &taler_test_amount); println!("Get back some money"); let wtid = rand_slice(); - ureq::post(&format!("{}/transfer", base_url)) - .send_json(TransferRequest { - request_uid: Base32::from(rand_slice()), - amount: taler_test_amount.clone(), - exchange_base_url: state.config.base_url.clone(), - wtid: Base32::from(wtid), - credit_account: btc_payto_url(&client_addr), - }) - .unwrap(); - + transfer( + base_url, + &wtid, + &state.config.base_url, + btc_payto_url(&client_addr), + &taler_test_amount, + ); wait_for_pending(&mut since, &mut client_rpc, &mut wire_rpc); println!("Check balances"); let last_client_balance = client_rpc.get_balance().unwrap(); assert_eq!(new_client_balance + test_amount, last_client_balance); - println!("Check history"); - let history: OutgoingHistory = ureq::get(&format!("{}/history/outgoing", base_url)) - .query("delta", "-5") - .call() - .unwrap() - .into_json() - .unwrap(); - assert!(history - .outgoing_transactions - .iter() - .find(|h| { - h.wtid == Base32::from(wtid) - && h.exchange_base_url == state.config.base_url - && h.amount == taler_test_amount - }) - .is_some()); + check_outgoing(base_url, &wtid, &state.config.base_url, &taler_test_amount); } diff --git a/instrumentation/src/eth.rs b/instrumentation/src/eth.rs @@ -0,0 +1,176 @@ +use std::{ + path::{Path, PathBuf}, + time::Duration, +}; + +use common::{config::load_eth_config, rand_slice}; +use eth_wire::{ + metadata::OutMetadata, + rpc::{hex::Hex, Rpc, TransactionRequest}, + taler_util::{eth_to_taler, TRUNC, eth_payto_url}, + SyncState, WireState, +}; +use ethereum_types::U256; + +use crate::{check_incoming, check_outgoing, print_now, transfer}; + +fn wait_for_pending(rpc: &mut Rpc) { + print_now("Wait for pending transactions mining:"); + let mut notifier = rpc.subscribe_new_head().unwrap(); + std::thread::sleep(Duration::from_secs(1)); // Wait for eth-wire to act + while !rpc.pending_transactions().unwrap().is_empty() { + notifier.next().unwrap(); + print_now("."); + std::thread::sleep(Duration::from_secs(1)); // Wait for eth-wire to act + } + println!(""); +} + +pub fn eth_test(config: Option<&Path>, base_url: &str) { + let config = load_eth_config(config); + let state = WireState::from_taler_config(config); + // TODO eth network check + + let min_fund = U256::from(10_000 * TRUNC); + let test_amount = U256::from(2_000 * TRUNC); + let taler_test_amount = eth_to_taler(&test_amount); + + let mut rpc = Rpc::new( + state + .config + .core + .data_dir + .unwrap_or_else(|| PathBuf::from("/tmp/geth.ipc")), + ) + .unwrap(); + + // Load client + let client = rpc + .list_accounts() + .unwrap() + .into_iter() + .skip(1) // Skip etherbase if dev network + .filter(|addr| addr != &state.address) // Skip wire + .next(); + let client_addr = match client { + Some(addr) => addr, + None => rpc.new_account("password").unwrap(), + }; + rpc.unlock_account(&client_addr, "password").unwrap(); + if rpc.get_balance(&client_addr).unwrap() < min_fund { + println!( + "Client need a minimum of {} WEI to run this test, send coins to this address: {}", + min_fund.as_u64(), + hex::encode(client_addr) + ); + print_now("Waiting for fund:"); + let mut notifier = rpc.subscribe_new_head().unwrap(); + while rpc.get_balance(&client_addr).unwrap() < min_fund { + notifier.next().unwrap(); + print_now("."); + } + println!(""); + } + + wait_for_pending(&mut rpc); + // Load balances + let client_balance = rpc.get_balance(&client_addr).unwrap(); + let wire_balance = rpc.get_balance(&state.address).unwrap(); + // Start sync state + let latest = rpc.latest_block().unwrap(); + let mut sync_state = SyncState { + tip_hash: latest.hash.unwrap(), + tip_height: latest.number.unwrap(), + conf_height: latest.number.unwrap(), + }; + + println!("Send transaction"); + let reserve_pub_key = rand_slice(); + let deposit_id = rpc + .deposit(client_addr, state.address, test_amount, reserve_pub_key) + .unwrap(); + let zero_id = rpc + .send_transaction(&TransactionRequest { + from: client_addr, + to: state.address, + value: U256::zero(), + gas_price: None, + data: Hex(vec![]), + nonce: None, + }) + .unwrap(); + let bounce_id = rpc + .send_transaction(&TransactionRequest { + from: client_addr, + to: state.address, + value: test_amount, + gas_price: None, + data: Hex(vec![]), + nonce: None, + }) + .unwrap(); + print_now("Wait for bounce:"); + let bounce = { + let mut notifier = rpc.subscribe_new_head().unwrap(); + 'l: loop { + let (transactions, _, new_state) = rpc + .list_since_sync_state(&state.address, sync_state, 0) + .unwrap(); + sync_state = new_state; + for (tx, _) in transactions { + if tx.to.unwrap() == client_addr && tx.from.unwrap() == state.address { + let metadata = OutMetadata::decode(&tx.input).unwrap(); + match metadata { + OutMetadata::Withdraw { .. } => {} + OutMetadata::Bounce { bounced } => { + if bounced == bounce_id { + break 'l tx; + } else if bounced == zero_id { + panic!("Bounced zero"); + } + } + } + } + } + notifier.next().unwrap(); + print_now("."); + } + }; + println!(""); + wait_for_pending(&mut rpc); + + println!("Check balance"); + let new_client_balance = rpc.get_balance(&client_addr).unwrap(); + let new_wire_balance = rpc.get_balance(&state.address).unwrap(); + let client_sent_amount_cost = test_amount * U256::from(2u8); + let client_sent_fees_cost = [deposit_id, zero_id, bounce_id] + .into_iter() + .map(|id| { + let receipt = rpc.get_transaction_receipt(&id).unwrap().unwrap(); + receipt.gas_used * receipt.effective_gas_price.unwrap() + }) + .reduce(|acc, i| acc + i) + .unwrap(); + assert_eq!( + client_balance - client_sent_amount_cost - client_sent_fees_cost + bounce.value, + new_client_balance + ); + let receipt = rpc.get_transaction_receipt(&bounce.hash).unwrap().unwrap(); + let bounced_fee = receipt.gas_used * receipt.effective_gas_price.unwrap(); + assert_eq!( + wire_balance + test_amount + (test_amount - bounce.value - bounced_fee), + new_wire_balance + ); + + check_incoming(base_url, &reserve_pub_key, &taler_test_amount); + + let wtid = rand_slice(); + transfer(base_url, &wtid, &state.config.base_url, eth_payto_url(&client_addr), &taler_test_amount); + wait_for_pending(&mut rpc); + + println!("Check balances"); + let last_client_balance = rpc.get_balance(&client_addr).unwrap(); + assert_eq!(new_client_balance + test_amount, last_client_balance); + + check_outgoing(base_url, &wtid, &state.config.base_url, &taler_test_amount); +} diff --git a/instrumentation/src/main.rs b/instrumentation/src/main.rs @@ -18,15 +18,76 @@ use std::{fmt::Display, io::Write, path::PathBuf}; use btc::btc_test; use clap::StructOpt; -use common::config::{Config, GatewayConfig}; +use common::{ + api_common::{Amount, Base32}, + api_wire::{IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferRequest}, + config::{Config, GatewayConfig}, + url::Url, rand_slice, +}; +use eth::eth_test; mod btc; +mod eth; fn print_now(disp: impl Display) { print!("{}", disp); std::io::stdout().flush().unwrap(); } +fn check_incoming(base_url: &str, reserve_pub_key: &[u8; 32], taler_amount: &Amount) { + println!("Check incoming history"); + let history: IncomingHistory = ureq::get(&format!("{}/history/incoming", base_url)) + .query("delta", "-5") + .call() + .unwrap() + .into_json() + .unwrap(); + assert!(history + .incoming_transactions + .iter() + .find(|h| { + matches!( + h, + IncomingBankTransaction::IncomingReserveTransaction { + reserve_pub, + amount, + .. + } if reserve_pub == &Base32::from(*reserve_pub_key) && amount == taler_amount + ) + }) + .is_some()); +} + +fn transfer(base_url: &str, wtid: &[u8; 32], url: &Url, credit_account: Url, amount: &Amount) { + println!("Get back some money"); + ureq::post(&format!("{}/transfer", base_url)) + .send_json(TransferRequest { + request_uid: Base32::from(rand_slice()), + amount: amount.clone(), + exchange_base_url: url.clone(), + wtid: Base32::from(*wtid), + credit_account, + }) + .unwrap(); +} + +fn check_outgoing(base_url: &str, wtid: &[u8; 32], url: &Url, amount: &Amount) { + println!("Check outgoing history"); + let history: OutgoingHistory = ureq::get(&format!("{}/history/outgoing", base_url)) + .query("delta", "-5") + .call() + .unwrap() + .into_json() + .unwrap(); + assert!(history + .outgoing_transactions + .iter() + .find(|h| { + h.wtid == Base32::from(*wtid) && &h.exchange_base_url == url && &h.amount == amount + }) + .is_some()); +} + /// Depolymerizer instrumentation test #[derive(clap::Parser, Debug)] struct Args { @@ -43,6 +104,7 @@ pub fn main() { match gateway_conf.core.currency.as_str() { "BTC" => btc_test(args.config.as_deref(), &base_url), + "ETH" => eth_test(args.config.as_deref(), &base_url), _ => unimplemented!(), } }