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:
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(), ð_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(), ð_payto_url(&state.address).raw(), ð_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(),
ð_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)?,
})
},
)