depolymerization

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

commit b0dc69e8cb46304b45e17053dbf1774bb32c06ad
parent 4e5d68301bbfafd4db9ee045c9575dc46dd66257
Author: Antoine A <>
Date:   Fri,  4 Apr 2025 12:53:06 +0200

common: store raw crypto address instead of encoded payto URI

Diffstat:
MCargo.lock | 9+++++++--
Mbtc-wire/src/lib.rs | 2+-
Mbtc-wire/src/loops/worker.rs | 6+++---
Mbtc-wire/src/sql.rs | 17++++++++---------
Mbtc-wire/src/taler_utils.rs | 27++-------------------------
Mcommon/Cargo.toml | 3+++
Mcommon/src/lib.rs | 1+
Acommon/src/payto.rs | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdb/btc.sql | 4++--
Mdb/eth.sql | 14+++++++-------
Meth-wire/src/lib.rs | 15+++++++--------
Meth-wire/src/loops/worker.rs | 22++++++++++------------
Meth-wire/src/main.rs | 2+-
Meth-wire/src/sql.rs | 15+++++----------
Meth-wire/src/taler_util.rs | 30++----------------------------
Minstrumentation/Cargo.toml | 2++
Minstrumentation/src/btc.rs | 20+++++++++++++++-----
Minstrumentation/src/eth.rs | 29++++++++++++++++-------------
Minstrumentation/src/gateway.rs | 31++++++++++++++++++++-----------
Minstrumentation/src/utils.rs | 16+---------------
Mwire-gateway/src/main.rs | 102++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
21 files changed, 291 insertions(+), 168 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -477,6 +477,9 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" name = "common" version = "0.1.0" dependencies = [ + "bitcoin", + "const-hex", + "ethereum-types", "exponential-backoff", "flexi_logger", "log", @@ -1638,6 +1641,8 @@ dependencies = [ "owo-colors", "rust-ini", "signal-child", + "taler-api", + "taler-common", "tempfile", "thread-local-panic-hook", "ureq", @@ -3026,7 +3031,7 @@ dependencies = [ [[package]] name = "taler-api" version = "0.0.0" -source = "git+https://git.taler.net/taler-rust.git/#1dac9a6c47ee1e023087cfb6bff2946011bd42d3" +source = "git+https://git.taler.net/taler-rust.git/#1e3783301f34379c6c9fde3f86111534ebcde8d3" dependencies = [ "axum", "dashmap", @@ -3048,7 +3053,7 @@ dependencies = [ [[package]] name = "taler-common" version = "0.0.0" -source = "git+https://git.taler.net/taler-rust.git/#1dac9a6c47ee1e023087cfb6bff2946011bd42d3" +source = "git+https://git.taler.net/taler-rust.git/#1e3783301f34379c6c9fde3f86111534ebcde8d3" dependencies = [ "anyhow", "clap", diff --git a/btc-wire/src/lib.rs b/btc-wire/src/lib.rs @@ -21,7 +21,7 @@ use btc_config::BitcoinConfig; use common::{ config::TalerConfig, currency::{Currency, CurrencyBtc}, - log::{fail, OrFail}, + log::{OrFail, fail}, postgres, taler_common::{api_common::EddsaPublicKey, types::amount::Amount as TalerAmount}, url::Url, diff --git a/btc-wire/src/loops/worker.rs b/btc-wire/src/loops/worker.rs @@ -20,7 +20,7 @@ use btc_wire::{ GetOpReturnErr, GetSegwitErr, rpc::{self, AutoRpcWallet, Category, ErrorCode, Rpc, Transaction}, rpc_utils::sender_address, - taler_utils::{btc_payto_url, btc_to_taler}, + taler_utils::btc_to_taler, }; use common::{ log::{ @@ -324,7 +324,7 @@ fn sync_chain_incoming_confirmed( let credit_addr = full.details[0].address.clone().unwrap().assume_checked(); let amount = btc_to_taler(&full.amount, state.currency); let nb = db.execute("INSERT INTO tx_in (received, amount, reserve_pub, debit_acc, credit_acc) VALUES ($1, ($2, $3)::taler_amount, $4, $5, $6) ON CONFLICT (reserve_pub) DO NOTHING ", &[ - &((full.time * 1000000) as i64), &(amount.val as i64), &(amount.frac as i32), &reserve_pub.as_slice(), &btc_payto_url(&debit_addr).raw(), &btc_payto_url(&credit_addr).raw() + &((full.time * 1000000) as i64), &(amount.val as i64), &(amount.frac as i32), &reserve_pub.as_slice(), &debit_addr.to_string(), &credit_addr.to_string() ])?; if nb > 0 { info!("<< {amount} {reserve_pub} in {id} from {debit_addr}"); @@ -419,7 +419,7 @@ fn sync_chain_debit( let debit_addr = sender_address(rpc, full)?; let nb = db.execute( "INSERT INTO tx_out (created, amount, wtid, debit_acc, credit_acc, exchange_url, status, txid, request_uid) VALUES ($1, ($2, $3)::taler_amount, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (wtid) DO NOTHING", - &[&((full.time*1000000) as i64), &(amount.val as i64), &(amount.frac as i32), &wtid.as_slice(), &btc_payto_url(&debit_addr).raw(), &btc_payto_url(&credit_addr).raw(), &state.base_url.as_ref(), &(DebitStatus::Sent as i16), &id.as_byte_array().as_slice(), &None::<&[u8]>], + &[&((full.time*1000000) as i64), &(amount.val as i64), &(amount.frac as i32), &wtid.as_slice(), &debit_addr.to_string(), &credit_addr.to_string(), &state.base_url.as_ref(), &(DebitStatus::Sent as i16), &id.as_byte_array().as_slice(), &None::<&[u8]>], )?; if nb > 0 { warn!(">> (onchain) {amount} {wtid} in {id} to {credit_addr}",); diff --git a/btc-wire/src/sql.rs b/btc-wire/src/sql.rs @@ -14,13 +14,15 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +use std::str::FromStr as _; + use bitcoin::{Address, Amount as BtcAmount, Txid, hashes::Hash}; use common::currency::CurrencyBtc; use common::log::OrFail; use common::postgres::Row; -use common::sql::{sql_amount, sql_url}; +use common::sql::sql_amount; -use btc_wire::taler_utils::{btc_payto_addr, taler_to_btc}; +use btc_wire::taler_utils::taler_to_btc; /// Bitcoin amount from sql pub fn sql_btc_amount(row: &Row, idx: usize, currency: CurrencyBtc) -> BtcAmount { @@ -35,13 +37,10 @@ pub fn sql_btc_amount(row: &Row, idx: usize, currency: CurrencyBtc) -> BtcAmount /// Bitcoin address from sql pub fn sql_addr(row: &Row, idx: usize) -> Address { - let url = sql_url(row, idx); - btc_payto_addr(&url).or_fail(|_| { - format!( - "Database invariant: expected an bitcoin payto url got {}", - url - ) - }) + let str = row.get(idx); + Address::from_str(str) + .or_fail(|_| format!("Database invariant: expected an bitcoin address got {str}")) + .assume_checked() } /// Bitcoin transaction id from sql diff --git a/btc-wire/src/taler_utils.rs b/btc-wire/src/taler_utils.rs @@ -15,34 +15,11 @@ */ //! Utils function to convert taler API types to bitcoin API types -use bitcoin::{Address, Amount as BtcAmount, SignedAmount}; +use bitcoin::{Amount as BtcAmount, SignedAmount}; use common::{ currency::CurrencyBtc, - taler_common::types::{ - amount::{Amount, FRAC_BASE}, - payto::PaytoURI, - }, - url::Url, + taler_common::types::amount::{Amount, FRAC_BASE}, }; -use std::str::FromStr; - -/// Generate a payto uri from a btc address -pub fn btc_payto_url(addr: &Address) -> PaytoURI { - PaytoURI::from_str(&format!("payto://bitcoin/{}", addr)).unwrap() -} - -/// Extract a btc address from a payto uri -pub fn btc_payto_addr(url: &Url) -> Result<Address, String> { - if url.domain() != Some("bitcoin") { - return Err(format!( - "Expected domain 'bitcoin' got '{}'", - url.domain().unwrap_or_default() - )); - } - let str = url.path().trim_start_matches('/'); - let addr = Address::from_str(str).map_err(|e| e.to_string())?; - Ok(addr.assume_checked()) -} /// Transform a btc amount into a taler amount pub fn btc_to_taler(amount: &SignedAmount, currency: CurrencyBtc) -> Amount { diff --git a/common/Cargo.toml b/common/Cargo.toml @@ -36,3 +36,6 @@ exponential-backoff = "1.2.0" taler-common.workspace = true taler-api.workspace = true sqlx.workspace = true +bitcoin.workspace = true +ethereum-types.workspace = true +hex.workspace = true diff --git a/common/src/lib.rs b/common/src/lib.rs @@ -28,6 +28,7 @@ pub mod config; pub mod currency; pub mod log; pub mod metadata; +pub mod payto; pub mod reconnect; pub mod sql; pub mod status; diff --git a/common/src/payto.rs b/common/src/payto.rs @@ -0,0 +1,92 @@ +/* + This file is part of TALER + Copyright (C) 2025 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; + +use taler_common::types::payto::{PaytoErr, PaytoImpl, PaytoURI}; + +const BITCOIN: &str = "bitcoin"; +const ETHEREUM: &str = "ethereum"; + +pub struct BtcAccount(pub bitcoin::Address); + +#[derive(Debug, thiserror::Error)] +pub enum BtcErr { + #[error("missing bitcoin address in path")] + MissingAddr, + #[error(transparent)] + Addr(#[from] bitcoin::address::ParseError), +} + +impl PaytoImpl for BtcAccount { + fn as_payto(&self) -> PaytoURI { + PaytoURI::from_parts(BITCOIN, format_args!("/{}", self.0)) + } + + fn parse(uri: &PaytoURI) -> Result<Self, PaytoErr> { + let url = uri.as_ref(); + if url.domain() != Some(BITCOIN) { + return Err(PaytoErr::UnsupportedKind( + BITCOIN, + url.domain().unwrap_or_default().to_owned(), + )); + } + let Some(mut segments) = url.path_segments() else { + return Err(PaytoErr::custom(BtcErr::MissingAddr)); + }; + let Some(addr) = segments.next() else { + return Err(PaytoErr::custom(BtcErr::MissingAddr)); + }; + let addr = + bitcoin::Address::from_str(addr).map_err(|e| PaytoErr::custom(BtcErr::Addr(e)))?; + Ok(Self(addr.assume_checked())) + } +} + +pub struct EthAccount(pub ethereum_types::Address); + +#[derive(Debug, thiserror::Error)] +pub enum EthErr { + #[error("missing ethereum address in path")] + MissingAddr, + #[error("malformed ethereum address")] + Addr, +} + +impl PaytoImpl for EthAccount { + fn as_payto(&self) -> PaytoURI { + PaytoURI::from_parts(ETHEREUM, format_args!("/{}", hex::encode(self.0))) + } + + fn parse(uri: &PaytoURI) -> Result<Self, PaytoErr> { + let url = uri.as_ref(); + if url.domain() != Some(ETHEREUM) { + return Err(PaytoErr::UnsupportedKind( + BITCOIN, + url.domain().unwrap_or_default().to_owned(), + )); + } + let Some(mut segments) = url.path_segments() else { + return Err(PaytoErr::custom(EthErr::MissingAddr)); + }; + let Some(addr) = segments.next() else { + return Err(PaytoErr::custom(EthErr::MissingAddr)); + }; + let addr = + ethereum_types::Address::from_str(addr).map_err(|_| PaytoErr::custom(EthErr::Addr))?; + Ok(Self(addr)) + } +} diff --git a/db/btc.sql b/db/btc.sql @@ -29,14 +29,14 @@ CREATE TABLE tx_out ( exchange_url TEXT NOT NULL, request_uid BYTEA UNIQUE CHECK (LENGTH(request_uid)=64), status SMALLINT NOT NULL DEFAULT 0, - txid BYTEA UNIQUE + txid BYTEA UNIQUE CHECK (LENGTH(txid)=32) ); -- Bounced transaction CREATE TABLE bounce ( id INT8 PRIMARY KEY GENERATED ALWAYS AS IDENTITY, bounced BYTEA UNIQUE NOT NULL, - txid BYTEA UNIQUE, + txid BYTEA UNIQUE CHECK (LENGTH(txid)=32), created INT8 NOT NULL, status SMALLINT NOT NULL DEFAULT 0 ) \ No newline at end of file diff --git a/db/eth.sql b/db/eth.sql @@ -14,8 +14,8 @@ CREATE TABLE tx_in ( received INT8 NOT NULL, amount taler_amount NOT NULL, reserve_pub BYTEA NOT NULL UNIQUE CHECK (LENGTH(reserve_pub)=32), - debit_acc TEXT NOT NULL, - credit_acc TEXT NOT NULL + debit_acc BYTEA NOT NULL CHECK (LENGTH(debit_acc)=20), + credit_acc BYTEA NOT NULL CHECK (LENGTH(credit_acc)=20) ); -- Outgoing transactions @@ -24,20 +24,20 @@ CREATE TABLE tx_out ( created INT8 NOT NULL, amount taler_amount NOT NULL, wtid BYTEA NOT NULL UNIQUE CHECK (LENGTH(wtid)=32), - debit_acc TEXT NOT NULL, - credit_acc TEXT NOT NULL, + debit_acc BYTEA NOT NULL CHECK (LENGTH(debit_acc)=20), + credit_acc BYTEA NOT NULL CHECK (LENGTH(credit_acc)=20), exchange_url TEXT NOT NULL, request_uid BYTEA UNIQUE CHECK (LENGTH(request_uid)=64), status SMALLINT NOT NULL DEFAULT 0, - txid BYTEA UNIQUE, + txid BYTEA UNIQUE CHECK (LENGTH(txid)=32), sent INT8 DEFAULT NULL ); -- Bounced transaction CREATE TABLE bounce ( id INT8 PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - bounced BYTEA UNIQUE NOT NULL, - txid BYTEA UNIQUE, + bounced BYTEA UNIQUE NOT NULL CHECK (LENGTH(bounced)=32), + txid BYTEA UNIQUE CHECK (LENGTH(txid)=32), created INT8 NOT NULL, status SMALLINT NOT NULL DEFAULT 0 ) \ No newline at end of file diff --git a/eth-wire/src/lib.rs b/eth-wire/src/lib.rs @@ -25,18 +25,19 @@ use common::{ currency::{Currency, CurrencyEth}, log::{OrFail, fail}, metadata::{InMetadata, OutMetadata}, + payto::EthAccount, postgres, taler_common::{ api_common::{EddsaPublicKey, ShortHashCode}, - types::{amount::Amount, payto::PaytoURI}, + types::{amount::Amount, payto::PaytoImpl}, }, url::Url, }; -use ethereum_types::{Address, H160, H256, U64, U256}; +use ethereum_types::{Address, H256, U64, U256}; use rpc::{Rpc, RpcClient, RpcStream, Transaction, hex::Hex}; use rpc_utils::default_data_dir; use serde::de::DeserializeOwned; -use taler_util::{eth_payto_addr, taler_to_eth}; +use taler_util::taler_to_eth; pub mod rpc; mod rpc_utils; @@ -229,13 +230,12 @@ 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<u32>, pub bump_delay: Option<u32>, pub base_url: Url, - pub payto: PaytoURI, + pub account: EthAccount, pub db_config: postgres::Config, pub currency: CurrencyEth, } @@ -244,18 +244,17 @@ 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(); + let account = EthAccount::parse(&taler_config.payto()).unwrap(); 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, + account, currency, } } diff --git a/eth-wire/src/loops/worker.rs b/eth-wire/src/loops/worker.rs @@ -22,15 +22,12 @@ use common::{ reconnect::AutoReconnectDb, sql::{sql_array, sql_base_32, sql_url}, status::{BounceStatus, DebitStatus}, - taler_common::{ - api_common::ShortHashCode, - types::{timestamp::Timestamp}, - }, + taler_common::{api_common::ShortHashCode, types::timestamp::Timestamp}, }; use eth_wire::{ ListSinceSync, RpcExtended, SyncState, SyncTransaction, rpc::{self, AutoRpcWallet, Rpc, RpcClient, Transaction, TransactionRequest}, - taler_util::{eth_payto_url, eth_to_taler}, + taler_util::eth_to_taler, }; use ethereum_types::{Address, H256, U256}; @@ -98,7 +95,7 @@ pub fn worker(mut rpc: AutoRpcWallet, mut db: AutoReconnectDb, mut state: WireSt let sync_state = SyncState::from_bytes(&sql_array(&row, 0)); // Get changes - let list = rpc.list_since_sync(&state.address, sync_state, state.confirmation)?; + let list = rpc.list_since_sync(&state.account.0, sync_state, state.confirmation)?; // Perform analysis state.confirmation = @@ -147,7 +144,8 @@ fn sync_chain( let conf_delay = state.confirmation; // Check if a confirmed incoming transaction have been removed by a blockchain reorganization - let new_status = sync_chain_removed(&list.txs, &list.removed, db, &state.address, conf_delay)?; + let new_status = + sync_chain_removed(&list.txs, &list.removed, db, &state.account.0, conf_delay)?; // Sync status with database if *status != new_status { @@ -169,9 +167,9 @@ fn sync_chain( for sync_tx in list.txs { let tx = &sync_tx.tx; - if tx.to == Some(state.address) && sync_tx.confirmations >= conf_delay { + if tx.to == Some(state.account.0) && sync_tx.confirmations >= conf_delay { sync_chain_incoming_confirmed(tx, db, state)?; - } else if tx.from == Some(state.address) { + } else if tx.from == Some(state.account.0) { sync_chain_outgoing(&sync_tx, db, state)?; } } @@ -281,7 +279,7 @@ fn sync_chain_incoming_confirmed( 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 (received, amount, reserve_pub, debit_acc, credit_acc) VALUES ($1, ($2, $3)::taler_amount, $4, $5, $6) ON CONFLICT (reserve_pub) DO NOTHING ", &[ - &Timestamp::now().as_sql_micros(), &(amount.val as i64), &(amount.frac as i32), &reserve_pub.as_slice(), &eth_payto_url(&credit_addr).raw(), &state.payto.raw() + &Timestamp::now().as_sql_micros(), &(amount.val as i64), &(amount.frac as i32), &reserve_pub.as_slice(), &credit_addr.as_bytes(), &state.account.0.as_bytes() ])?; if nb > 0 { info!( @@ -351,7 +349,7 @@ fn sync_chain_outgoing(tx: &SyncTransaction, db: &mut Client, state: &WireState) // Else add to database let nb = db.execute( "INSERT INTO tx_out (created, amount, wtid, debit_acc, credit_acc, exchange_url, status, txid, request_uid) VALUES ($1, ($2, $3)::taler_amount, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (wtid) DO NOTHING", - &[&Timestamp::now().as_sql_micros(), &(amount.val as i64), &(amount.frac as i32), &wtid.as_slice(), &eth_payto_url(&state.address).raw(), &eth_payto_url(&credit_addr).raw(), &state.base_url.as_ref(), &(DebitStatus::Sent as i16), &tx.hash.as_ref(), &None::<&[u8]>], + &[&Timestamp::now().as_sql_micros(), &(amount.val as i64), &(amount.frac as i32), &wtid.as_slice(), &state.account.0.as_bytes(), &credit_addr.as_bytes(), &state.base_url.as_ref(), &(DebitStatus::Sent as i16), &tx.hash.as_ref(), &None::<&[u8]>], )?; if nb > 0 { warn!( @@ -434,7 +432,7 @@ fn debit(db: &mut Client, rpc: &mut Rpc, state: &WireState) -> LoopResult<bool> let addr = sql_addr(row, 4); let url = sql_url(row, 5); let now = Timestamp::now(); - let tx_id = rpc.debit(state.address, addr, amount, wtid.clone(), url)?; + let tx_id = rpc.debit(state.account.0, addr, amount, wtid.clone(), url)?; fail_point("(injected) fail debit", 0.3)?; db.execute( "UPDATE tx_out SET status=$1, txid=$2, sent=$3 WHERE id=$4", diff --git a/eth-wire/src/main.rs b/eth-wire/src/main.rs @@ -146,7 +146,7 @@ fn init(config: Option<PathBuf>, init: Init) -> LoopResult<()> { fn run(config: Option<PathBuf>) { let state = WireState::load_taler_config(config.as_deref()); - let rpc_worker = auto_rpc_wallet(state.ipc_path.clone(), state.address); + let rpc_worker = auto_rpc_wallet(state.ipc_path.clone(), state.account.0); let rpc_watcher = auto_rpc_common(state.ipc_path.clone()); let db_watcher = auto_reconnect_db(state.db_config.clone()); diff --git a/eth-wire/src/sql.rs b/eth-wire/src/sql.rs @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022-2025 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 @@ -17,9 +17,9 @@ use common::{ currency::CurrencyEth, log::OrFail, postgres::Row, - sql::{sql_amount, sql_array, sql_payto}, + sql::{sql_amount, sql_array}, }; -use eth_wire::taler_util::{eth_payto_addr, taler_to_eth}; +use eth_wire::taler_util::taler_to_eth; use ethereum_types::{H160, H256, U256}; /// Ethereum amount from sql @@ -35,13 +35,8 @@ pub fn sql_eth_amount(row: &Row, idx: usize, currency: CurrencyEth) -> U256 { /// Ethereum address from sql pub fn sql_addr(row: &Row, idx: usize) -> H160 { - let url = sql_payto(row, idx); - eth_payto_addr(&url).or_fail(|_| { - format!( - "Database invariant: expected an ethereum payto url got {}", - url - ) - }) + let array: [u8; 20] = sql_array(row, idx); + H160::from_slice(&array) } /// Ethereum hash from sql diff --git a/eth-wire/src/taler_util.rs b/eth-wire/src/taler_util.rs @@ -13,42 +13,16 @@ 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; use common::{ currency::CurrencyEth, - taler_common::types::{ - amount::{Amount, FRAC_BASE}, - payto::PaytoURI, - }, + taler_common::types::amount::{Amount, FRAC_BASE}, }; -use ethereum_types::{Address, U256}; +use ethereum_types::U256; pub const WEI: u64 = 1_000_000_000_000_000_000; pub const TRUNC: u64 = WEI / FRAC_BASE as u64; -/// Generate a payto uri from an eth address -pub fn eth_payto_url(addr: &Address) -> PaytoURI { - PaytoURI::from_str(&format!( - "payto://ethereum/{}", - hex::encode(addr.as_bytes()) - )) - .unwrap() -} - -/// Extract an eth address from a payto uri -pub fn eth_payto_addr(payto: &PaytoURI) -> Result<Address, String> { - let url = payto.as_ref(); - if url.domain() != Some("ethereum") { - return Err(format!( - "Expected domain 'ethereum' got '{}'", - url.domain().unwrap_or_default() - )); - } - let str = url.path().trim_start_matches('/'); - Address::from_str(str).map_err(|e| e.to_string()) -} - /// Transform a eth amount into a taler amount pub fn eth_to_taler(amount: &U256, currency: CurrencyEth) -> Amount { Amount::new( diff --git a/instrumentation/Cargo.toml b/instrumentation/Cargo.toml @@ -37,6 +37,8 @@ rust-ini = "0.21.0" # Progress reporting indicatif = "0.17.7" thread-local-panic-hook = "0.1.0" +taler-common.workspace = true +taler-api.workspace = true [build-dependencies] clap_mangen = "0.2.14" diff --git a/instrumentation/src/btc.rs b/instrumentation/src/btc.rs @@ -28,10 +28,20 @@ use btc_wire::{ btc_config::BitcoinConfig, rpc::{self, Category, ErrorCode, Rpc}, rpc_utils::{self, segwit_min_amount}, - taler_utils::{btc_payto_url, btc_to_taler}, + taler_utils::btc_to_taler, +}; +use common::{ + currency::CurrencyBtc, + metadata::OutMetadata, + payto::BtcAccount, + postgres::NoTls, + taler_common::{ + api_common::{EddsaPublicKey, ShortHashCode}, + types::base32::Base32, + }, }; -use common::{currency::CurrencyBtc, metadata::OutMetadata, postgres::NoTls, taler_common::{api_common::{EddsaPublicKey, ShortHashCode}, types::base32::Base32}}; use indicatif::ProgressBar; +use taler_common::types::payto::Payto; use tempfile::TempDir; use crate::utils::{ @@ -200,7 +210,7 @@ pub fn online_test(config: Option<&Path>, base_url: &str) { base_url, &wtid, &state.base_url, - btc_payto_url(&client_addr), + Payto::new(BtcAccount(client_addr)).as_payto(), &taler_test_amount, ); wait_for_pending(&mut since, &mut client_rpc, &mut wire_rpc); @@ -434,7 +444,7 @@ impl BtcCtx { &self.ctx.gateway_url, metadata, &self.state.base_url, - btc_payto_url(&self.client_addr), + Payto::new(BtcAccount(self.client_addr.clone())).as_payto(), &btc_to_taler(&amount.to_signed().unwrap(), self.state.currency), ) } @@ -640,7 +650,7 @@ pub fn reconnect(ctx: TestCtx) { { ctx.stop_db(); ctx.malformed_credit(&Amount::from_sat(24000)); - let metadata =Base32::rand(); + let metadata = Base32::rand(); let amount = Amount::from_sat(40000); ctx.credit(amount, &metadata); credits.push((metadata, amount)); diff --git a/instrumentation/src/eth.rs b/instrumentation/src/eth.rs @@ -23,16 +23,17 @@ use std::{ use common::{ metadata::OutMetadata, + payto::EthAccount, postgres::NoTls, taler_common::{ api_common::{EddsaPublicKey, ShortHashCode}, - types::base32::Base32, + types::{base32::Base32, payto::Payto}, }, }; use eth_wire::{ RpcExtended, SyncState, WireState, rpc::{Rpc, RpcClient, TransactionRequest, hex::Hex}, - taler_util::{TRUNC, eth_payto_url, eth_to_taler}, + taler_util::{TRUNC, eth_to_taler}, }; use ethereum_types::{H160, H256, U256}; @@ -68,7 +69,7 @@ pub fn online_test(config: Option<&Path>, base_url: &str) { .unwrap() .into_iter() .skip(1) // Skip etherbase if dev network - .find(|addr| addr != &state.address) // Skip wire + .find(|addr| addr != &state.account.0) // Skip wire .unwrap_or_else(|| rpc.new_account("password").unwrap()); // Else create account rpc.unlock_account(&client_addr, "password").unwrap(); @@ -90,7 +91,7 @@ pub fn online_test(config: Option<&Path>, base_url: &str) { wait_for_pending(&mut rpc); // Load balances let client_balance = rpc.get_balance_latest(&client_addr).unwrap(); - let wire_balance = rpc.get_balance_latest(&state.address).unwrap(); + let wire_balance = rpc.get_balance_latest(&state.account.0).unwrap(); // Start sync state let latest = rpc.latest_block().unwrap(); let mut sync_state = SyncState { @@ -104,7 +105,7 @@ pub fn online_test(config: Option<&Path>, base_url: &str) { let credit_id = rpc .credit( client_addr, - state.address, + state.account.0, test_amount, reserve_pub_key.clone(), ) @@ -112,7 +113,7 @@ pub fn online_test(config: Option<&Path>, base_url: &str) { let zero_id = rpc .send_transaction(&TransactionRequest { from: client_addr, - to: state.address, + to: state.account.0, value: U256::zero(), gas_price: None, data: Hex(vec![]), @@ -122,7 +123,7 @@ pub fn online_test(config: Option<&Path>, base_url: &str) { let bounce_id = rpc .send_transaction(&TransactionRequest { from: client_addr, - to: state.address, + to: state.account.0, value: test_amount, gas_price: None, data: Hex(vec![]), @@ -133,11 +134,13 @@ pub fn online_test(config: Option<&Path>, base_url: &str) { let bounce = { let mut rpc = rpc.subscribe_new_head().unwrap(); 'l: loop { - let list = rpc.list_since_sync(&state.address, sync_state, 0).unwrap(); + let list = rpc + .list_since_sync(&state.account.0, sync_state, 0) + .unwrap(); sync_state = list.state; for sync_tx in list.txs { let tx = sync_tx.tx; - if tx.to.unwrap() == client_addr && tx.from.unwrap() == state.address { + if tx.to.unwrap() == client_addr && tx.from.unwrap() == state.account.0 { let metadata = OutMetadata::decode(&tx.input).unwrap(); match metadata { OutMetadata::Debit { .. } => {} @@ -161,7 +164,7 @@ pub fn online_test(config: Option<&Path>, base_url: &str) { println!("Check balance"); let new_client_balance = rpc.get_balance_latest(&client_addr).unwrap(); - let new_wire_balance = rpc.get_balance_latest(&state.address).unwrap(); + let new_wire_balance = rpc.get_balance_latest(&state.account.0).unwrap(); let client_sent_amount_cost = test_amount * U256::from(2u8); let client_sent_fees_cost = [credit_id, zero_id, bounce_id] .into_iter() @@ -194,7 +197,7 @@ pub fn online_test(config: Option<&Path>, base_url: &str) { base_url, &wtid, &state.base_url, - eth_payto_url(&client_addr), + Payto::new(EthAccount(client_addr)).as_payto(), &taler_test_amount, ); wait_for_pending(&mut rpc); @@ -534,7 +537,7 @@ impl EthCtx { &self.ctx.gateway_url, wtid, &self.state.base_url, - eth_payto_url(&self.client_addr), + Payto::new(EthAccount(self.client_addr)).as_payto(), &eth_to_taler(&amount, self.state.currency), ) } @@ -751,7 +754,7 @@ pub fn reconnect(ctx: TestCtx) { ctx.credit(amount, &metadata); credits.push((metadata, amount)); ctx.stop_node(); - ctx.expect_error(); + ctx.expect_gateway_down(); } ctx.step("Reconnect DB"); diff --git a/instrumentation/src/gateway.rs b/instrumentation/src/gateway.rs @@ -16,16 +16,19 @@ use std::str::FromStr; -use btc_wire::taler_utils::btc_payto_url; -use common::taler_common::{ - api_wire::TransferRequest, - types::{ - amount::Amount, - base32::Base32, - payto::{PaytoURI, payto}, +use common::{ + payto::BtcAccount, + taler_common::{ + api_wire::TransferRequest, + types::{ + amount::Amount, + base32::Base32, + payto::{PaytoURI, payto}, + }, }, }; use libdeflater::{CompressionLvl, Compressor}; +use taler_common::types::payto::Payto; use crate::{ btc::BtcCtx, @@ -56,7 +59,7 @@ pub fn api(ctx: TestCtx) { "-b", &ctx.gateway_url, "-D", - btc_payto_url(&ctx.client_addr).raw(), + Payto::new(BtcAccount(ctx.client_addr.clone())).as_payto().raw(), "-a", &amount, ], @@ -77,7 +80,11 @@ pub fn api(ctx: TestCtx) { let mut amounts = Vec::new(); for n in 1..10 { let amount = format!("{}:0.0000{}", ctx.taler_conf.currency.to_str(), n); - client_transfer(&ctx.gateway_url, &btc_payto_url(&ctx.client_addr), &amount); + client_transfer( + &ctx.gateway_url, + &Payto::new(BtcAccount(ctx.client_addr.clone())).as_payto(), + &amount, + ); amounts.push(amount); } @@ -100,7 +107,7 @@ pub fn api(ctx: TestCtx) { } let amount = &format!("{}:0.00042", ctx.taler_conf.currency.to_str()); - let btc_payto = btc_payto_url(&ctx.client_addr); + let btc_payto = Payto::new(BtcAccount(ctx.client_addr.clone())).as_payto(); ctx.step("Request format"); { @@ -219,7 +226,9 @@ pub fn auth(ctx: TestCtx) { "-s", "exchange-accountcredentials-admin", "-C", - btc_payto_url(&ctx.client_addr).raw(), + Payto::new(BtcAccount(ctx.client_addr.clone())) + .as_payto() + .raw(), "-a", &format!("{}:0.00042", ctx.taler_conf.currency.to_str()), ], diff --git a/instrumentation/src/utils.rs b/instrumentation/src/utils.rs @@ -93,22 +93,12 @@ pub fn gateway_error(path: &str, error: u16) { } #[must_use] -pub fn check_gateway_error(base_url: &str) -> bool { - matches!( - ureq::get(&format!("{}history/incoming", base_url)) - .query("delta", "-5") - .call(), - Err(ureq::Error::StatusCode(504)) - ) -} - -#[must_use] pub fn check_gateway_down(base_url: &str) -> bool { matches!( ureq::get(&format!("{}history/incoming", base_url)) .query("delta", "-5") .call(), - Err(ureq::Error::StatusCode(502)) + Err(ureq::Error::StatusCode(504 | 502)) ) } @@ -428,10 +418,6 @@ impl TalerCtx { retry(|| check_outgoing(&self.gateway_url, base_url, txs)) } - pub fn expect_error(&self) { - retry(|| check_gateway_error(&self.gateway_url)); - } - pub fn expect_gateway_up(&self) { retry(|| check_gateway_up(&self.gateway_url)); } diff --git a/wire-gateway/src/main.rs b/wire-gateway/src/main.rs @@ -19,6 +19,7 @@ use axum::{ middleware::{self, Next}, response::{IntoResponse, Response}, }; +use bitcoin::address::NetworkUnchecked; use clap::Parser; use common::{ config::WireGatewayCfg, @@ -27,9 +28,10 @@ use common::{ OrFail, log::{error, info}, }, + payto::{BtcAccount, EthAccount}, }; -use sqlx::Row; use sqlx::{PgPool, QueryBuilder, postgres::PgListener}; +use sqlx::{Row, postgres::PgRow}; use std::{ path::PathBuf, str::FromStr as _, @@ -55,10 +57,18 @@ use taler_common::{ TransferList, TransferRequest, TransferResponse, TransferState, TransferStatus, }, error_code::ErrorCode, - types::{payto::PaytoURI, timestamp::Timestamp}, + types::{ + payto::{Payto, PaytoURI}, + timestamp::Timestamp, + }, }; use tokio::time::sleep; +pub enum Address<'a> { + BTC(&'a str), + ETH([u8; 20]), +} + struct ServerState { pool: PgPool, payto: PaytoURI, @@ -66,6 +76,59 @@ struct ServerState { status: AtomicBool, } +impl ServerState { + pub fn sql_payto(&self, row: &PgRow, idx: usize) -> sqlx::Result<PaytoURI> { + Ok(match self.currency { + Currency::ETH(_) => { + let it: [u8; 20] = row.try_get(idx)?; + let addr = ethereum_types::Address::from_slice(&it); + Payto::new(EthAccount(addr)) + .as_payto() + .as_full_payto("Ethereum User") + } + Currency::BTC(_) => { + let addr = row + .try_get_parse::<_, _, bitcoin::Address<NetworkUnchecked>>(idx)? + .assume_checked(); + Payto::new(BtcAccount(addr)) + .as_payto() + .as_full_payto("Bitcoin User") + } + }) + } + + pub fn payto_addr<'a>(&self, payto: &'a PaytoURI) -> Result<Address<'a>, String> { + let url = payto.as_ref(); + Ok(match self.currency { + Currency::ETH(_) => { + if url.domain() != Some("ethereum") { + return Err(format!( + "Expected domain 'ethereum' got '{}'", + url.domain().unwrap_or_default() + )); + } + let str = url.path().trim_start_matches('/'); + Address::ETH( + ethereum_types::Address::from_str(str) + .map_err(|e| e.to_string())? + .to_fixed_bytes(), + ) + } + Currency::BTC(_) => { + if url.domain() != Some("bitcoin") { + return Err(format!( + "Expected domain 'bitcoin' got '{}'", + url.domain().unwrap_or_default() + )); + } + let str = url.path().trim_start_matches('/'); + bitcoin::Address::from_str(str).map_err(|e| e.to_string())?; + Address::BTC(str) + } + }) + } +} + impl TalerApi for ServerState { fn currency(&self) -> &str { self.currency.to_str() @@ -87,12 +150,13 @@ impl WireGateway for ServerState { .fetch_optional(&self.pool) .await?; if let Some(r) = row { + // TODO store names? let prev = TransferRequest { request_uid: req.request_uid.clone(), amount: r.try_get_amount_i(0, self.currency())?, exchange_base_url: r.try_get_url(2)?, wtid: r.try_get_base32(3)?, - credit_account: r.try_get_payto(4)?, + credit_account: self.sql_payto(&r, 4)?, }; if prev == req { // Idempotence @@ -109,21 +173,27 @@ impl WireGateway for ServerState { } let timestamp = Timestamp::now(); - let mut tx = self.pool.begin().await?; - let r = sqlx::query( + let q = sqlx::query( "INSERT INTO tx_out (created, amount, wtid, debit_acc, credit_acc, exchange_url, request_uid) VALUES ($1, ($2, $3)::taler_amount, $4, $5, $6, $7, $8) RETURNING id" ) .bind_timestamp(&Timestamp::now()) .bind_amount(&req.amount) - .bind(req.wtid.as_slice()) - .bind(self.payto.raw()) - .bind(req.credit_account.raw()) - .bind(req.exchange_base_url.as_str()) - .bind(req.request_uid.as_slice()) - .fetch_one(&mut *tx).await?; - sqlx::query("NOTIFY new_tx").execute(&mut *tx).await?; - - tx.commit().await?; + .bind(req.wtid.as_slice()); + let q = match self.payto_addr(&self.payto).unwrap() { + Address::BTC(a) => q.bind(a), + Address::ETH(a) => q.bind(a), + }; + let q = match self.payto_addr(&req.credit_account).unwrap() { + Address::BTC(a) => q.bind(a), + Address::ETH(a) => q.bind(a), + }; + let r = q + .bind(req.exchange_base_url.as_str()) + .bind(req.request_uid.as_slice()) + .fetch_one(&self.pool) + .await?; + sqlx::query("NOTIFY new_tx").execute(&self.pool).await?; + Ok(TransferResponse { timestamp, row_id: r.try_get_safeu64(0)?, @@ -151,7 +221,7 @@ impl WireGateway for ServerState { date: r.try_get_timestamp(1)?, amount: r.try_get_amount_i(2, self.currency())?, wtid: r.try_get_base32(4)?, - credit_account: r.try_get_payto(5)?, + credit_account: self.sql_payto(&r, 5)?, exchange_base_url: r.try_get_url(6)?, }) }).await?; @@ -177,7 +247,7 @@ impl WireGateway for ServerState { date: r.try_get_timestamp(1)?, amount: r.try_get_amount_i(2, self.currency())?, reserve_pub: r.try_get_base32(4)?, - debit_account: r.try_get_payto(5)?, + debit_account: self.sql_payto(&r, 5)?, }) }, )