/* 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::{ fmt::Debug, path::{Path, PathBuf}, str::FromStr, }; use common::{ api_common::Amount, config::TalerConfig, currency::{Currency, CurrencyEth}, log::{fail, OrFail}, metadata::{InMetadata, OutMetadata}, postgres, url::Url, }; use ethereum_types::{Address, H160, H256, U256, U64}; use rpc::{hex::Hex, Rpc, RpcClient, RpcStream, Transaction}; use rpc_utils::default_data_dir; use serde::de::DeserializeOwned; use taler_util::{eth_payto_addr, taler_to_eth}; pub mod rpc; mod rpc_utils; pub mod taler_util; /// An extended geth JSON-RPC api client who can send and retrieve metadata with their transaction pub trait RpcExtended: RpcClient { /// Perform a wire credit fn credit( &mut self, from: Address, to: Address, value: U256, reserve_pub: [u8; 32], ) -> rpc::Result { let metadata = InMetadata::Credit { reserve_pub }; self.send_transaction(&rpc::TransactionRequest { from, to, value, nonce: None, gas_price: None, data: Hex(metadata.encode()), }) } /// Perform a wire debit fn debit( &mut self, from: Address, to: Address, value: U256, wtid: [u8; 32], url: Url, ) -> rpc::Result { let metadata = OutMetadata::Debit { wtid, url }; self.send_transaction(&rpc::TransactionRequest { from, to, value, nonce: None, gas_price: None, data: Hex(metadata.encode().or_fail(|e| format!("{}", e))), }) } /// Perform a Taler bounce fn bounce(&mut self, hash: H256, bounce_fee: U256) -> rpc::Result> { let tx = self .get_transaction(&hash)? .expect("Cannot bounce a non existent transaction"); let bounce_value = tx.value.saturating_sub(bounce_fee); let metadata = OutMetadata::Bounce { bounced: hash.0 }; let mut request = rpc::TransactionRequest { from: tx.to.expect("Cannot bounce contract transaction"), to: tx.from.expect("Cannot bounce coinbase transaction"), value: bounce_value, nonce: None, gas_price: None, data: Hex(metadata.encode().or_fail(|e| format!("{}", e))), }; // Estimate fee price using node let fill = self.fill_transaction(&request)?; // Deduce fee price from bounced value request.value = request .value .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 and the size of the reorganized fork if any, returning a new sync state fn list_since_sync( &mut self, address: &Address, state: SyncState, min_confirmation: u32, ) -> rpc::Result { let match_tx = |txs: Vec, confirmations: u32| -> Vec { txs.into_iter() .filter_map(|tx| { (tx.from == Some(*address) || tx.to == Some(*address)) .then(|| SyncTransaction { tx, confirmations }) }) .collect() }; let mut txs = Vec::new(); let mut removed = Vec::new(); let mut fork_len = 0; // 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; fork_len += 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(ListSinceSync { txs, removed, fork_len, state: SyncState { tip_hash: latest.hash.unwrap(), tip_height: latest.number.unwrap(), conf_height: latest .number .unwrap() .saturating_sub(U64::from(min_confirmation)), }, }) } } impl RpcExtended for Rpc {} impl RpcExtended for RpcStream<'_, N> {} pub struct SyncTransaction { pub tx: Transaction, pub confirmations: u32, } pub struct ListSinceSync { pub txs: Vec, pub removed: Vec, pub state: SyncState, pub fork_len: u32, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SyncState { pub tip_hash: H256, pub tip_height: U64, pub conf_height: U64, } impl SyncState { pub fn to_bytes(&self) -> [u8; 48] { let mut bytes = [0; 48]; bytes[..32].copy_from_slice(self.tip_hash.as_bytes()); self.tip_height.to_little_endian(&mut bytes[32..40]); self.conf_height.to_little_endian(&mut bytes[40..]); bytes } pub fn from_bytes(bytes: &[u8; 48]) -> Self { Self { tip_hash: H256::from_slice(&bytes[..32]), tip_height: U64::from_little_endian(&bytes[32..40]), conf_height: U64::from_little_endian(&bytes[40..]), } } } const DEFAULT_CONFIRMATION: u16 = 37; const DEFAULT_BOUNCE_FEE: &str = "0.00001"; pub struct WireState { pub confirmation: u32, pub max_confirmations: u32, pub address: H160, pub bounce_fee: U256, pub ipc_path: PathBuf, pub lifetime: Option, pub bump_delay: Option, 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, ipc_path, currency) = load_taler_config(file); let init_confirmation = taler_config.confirmation().unwrap_or(DEFAULT_CONFIRMATION) as u32; let payto = taler_config.payto(); Self { confirmation: init_confirmation, max_confirmations: init_confirmation * 2, address: eth_payto_addr(&payto).unwrap(), ipc_path, 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, CurrencyEth) { let config = TalerConfig::load(file); let path = config.path("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, path, currency) } // Parse ethereum value from config bounce fee fn config_bounce_fee(bounce_fee: &Option, currency: CurrencyEth) -> U256 { let config = bounce_fee.as_deref().unwrap_or(DEFAULT_BOUNCE_FEE); Amount::from_str(&format!("{}:{}", currency.to_str(), config)) .map_err(|s| s.to_string()) .and_then(|a| taler_to_eth(&a, currency)) .or_fail(|a| { format!( "config BOUNCE_FEE={} is not a valid ethereum amount: {}", config, a ) }) } #[cfg(test)] mod test { use common::{rand::random, rand_slice}; use ethereum_types::{H256, U64}; use crate::SyncState; #[test] fn to_from_bytes_block_state() { for _ in 0..4 { let state = SyncState { tip_hash: H256::from_slice(&rand_slice::<32>()), tip_height: U64::from(random::()), conf_height: U64::from(random::()), }; let encoded = state.to_bytes(); let decoded = SyncState::from_bytes(&encoded); assert_eq!(state, decoded); } } }