depolymerization

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

commit 491410969b1fec9c0e01def555a970f02c3d76af
parent b19e53a7a82b9972b720f8f0424c17ae1afe2004
Author: Antoine A <>
Date:   Mon, 29 Nov 2021 18:56:57 +0100

Draft bitcoin wire implementation

Diffstat:
MCargo.lock | 1+
Mbtc-wire/src/bin/btc-wire-cli.rs | 21+++++++++++++++++++--
Mbtc-wire/src/lib.rs | 31++++++++++++++++++++++++++-----
Mscript/test_bank.sh | 19++++++++-----------
Mwire-gateway/Cargo.toml | 2++
Mwire-gateway/src/api_common.rs | 27++++++++++++++++++++++++---
Mwire-gateway/src/main.rs | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
7 files changed, 228 insertions(+), 36 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1573,6 +1573,7 @@ version = "0.1.0" dependencies = [ "async-compression", "base32", + "btc-wire", "hyper", "rand", "serde", diff --git a/btc-wire/src/bin/btc-wire-cli.rs b/btc-wire/src/bin/btc-wire-cli.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use bitcoincore_rpc::{ bitcoin::{Address, Amount}, Client, RpcApi, @@ -22,6 +20,7 @@ struct Args { enum Cmd { Transfer(TransferCmd), NextBlock(NextBlockCmd), + Init(InitCmd), } #[derive(argh::FromArgs)] @@ -54,6 +53,11 @@ struct NextBlockCmd { to: String, } +#[derive(argh::FromArgs)] +#[argh(subcommand, name = "init")] +/// Init test state +struct InitCmd {} + struct App { network: Network, client: Client, @@ -90,12 +94,25 @@ impl App { } } } + + pub fn init(&self) { + for wallet in ["wire", "client", "reserve"] { + self.client + .create_wallet(wallet, None, None, None, None) + .ok(); + } + if self.network == Network::RegTest { + let (client, client_addr) = self.auto_wallet("client"); + client.generate_to_address(101, &client_addr).unwrap(); + } + } } fn main() { let app = App::start(); let args: Args = argh::from_env(); match args.cmd { + Cmd::Init(_) => app.init(), Cmd::Transfer(TransferCmd { key, from, diff --git a/btc-wire/src/lib.rs b/btc-wire/src/lib.rs @@ -11,9 +11,13 @@ use bitcoincore_rpc::{ use rand::{rngs::OsRng, RngCore}; use rpc_patch::{ClientPatched, GetTransactionFull}; -use crate::rpc_patch::{rpc_error, RpcErrorCode}; +use crate::{ + rpc_patch::{rpc_error, RpcErrorCode}, + utils::rand_key, +}; pub mod rpc_patch; +pub use bitcoincore_rpc; /// Minimum dust amount to perform a transaction to a segwit address fn segwit_min_amount() -> Amount { @@ -56,10 +60,8 @@ pub fn encode_segwit_key(hrp: &str, msg: &[u8; 32]) -> [String; 2] { let mut magic_id = [0; 4]; OsRng.fill_bytes(&mut magic_id); // Split key in half; - let split: (&[u8; 16], &[u8; 16]) = ( - msg[..16].try_into().unwrap(), - msg[16..].try_into().unwrap(), - ); + let split: (&[u8; 16], &[u8; 16]) = + (msg[..16].try_into().unwrap(), msg[16..].try_into().unwrap()); [ encode_segwit_key_half(hrp, true, &magic_id, &split.0), encode_segwit_key_half(hrp, false, &magic_id, &split.1), @@ -118,6 +120,11 @@ pub fn decode_segwit_msg(segwit_addrs: &[impl AsRef<str>]) -> Result<[u8; 32], D > 1 }) .collect(); + + if matches.len() != 2 { + println!("Magic id collision"); + return Ok(rand_key()); + } assert_eq!(matches.len(), 2, "Magic ID collision"); let mut key = [0; 32]; @@ -156,6 +163,20 @@ fn send_many( ) } +/// Get the first sender address from a raw transaction +pub fn sender_address(rpc: &Client, full: &GetTransactionFull) -> bitcoincore_rpc::Result<Address> { + let first = &full.decoded.vin[0]; + let tx = rpc.get_raw_tx(&first.txid.unwrap())?; + Ok(tx + .vout + .into_iter() + .find(|it| it.n == first.vout.unwrap()) + .unwrap() + .script_pub_key + .address + .unwrap()) +} + /// Refund a transaction /// /// There is no reliable way to refund a transaction as you cannot know if the addresses used are shared diff --git a/script/test_bank.sh b/script/test_bank.sh @@ -20,33 +20,30 @@ echo "OK" BANK_ENDPOINT=http://localhost:8080/ echo -n "Making wire transfer to exchange ..." -# btc-wire-cli.exe transfer 0.0004 -taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -S 0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00 \ - -D payto://x-taler-bank/localhost:8899/user \ - -a TESTKUDOS:4 > /dev/null +btc-wire-cli transfer 0.0004 +btc-wire-cli nblock echo " OK" echo -n "Requesting exchange incoming transaction list ..." -taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -i | grep TESTKUDOS:4 > /dev/null +taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -i | grep BTC:0.0004 > /dev/null echo " OK" echo -n "Making wire transfer from exchange..." +ADDRESS=`bitcoin-cli -rpcwallet=wire getnewaddress` taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ - -S 0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00 \ - -C payto://x-taler-bank/$BANK_ENDPOINT/merchant \ - -a TESTKUDOS:2 > /dev/null + -C payto://bitcoin/$ADDRESS \ + -a BTC:0.0002 > /dev/null +btc-wire-cli nblock echo " OK" echo -n "Requesting exchange's outgoing transaction list..." -taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -o | grep TESTKUDOS:2 > /dev/null +taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -o | grep BTC:0.0002 > /dev/null echo " OK" echo "All tests passed" diff --git a/wire-gateway/Cargo.toml b/wire-gateway/Cargo.toml @@ -26,3 +26,5 @@ async-compression = { version = "0.3.8", features = ["tokio", "zlib"] } rand = { version = "0.8.4", features = ["getrandom"] } # Url format url = { version = "2.2.2", features = ["serde"] } +# Bitcoin taler util +btc-wire = { path = "../btc-wire" } diff --git a/wire-gateway/src/api_common.rs b/wire-gateway/src/api_common.rs @@ -1,6 +1,7 @@ use std::{ fmt::Display, num::ParseIntError, + ops::Deref, str::FromStr, time::{Duration, SystemTime}, }; @@ -86,6 +87,12 @@ impl serde::Serialize for Timestamp { } } +impl From<SystemTime> for Timestamp { + fn from(time: SystemTime) -> Self { + Self::Time(time) + } +} + /// <https://docs.taler.net/core/api-common.html#tsref-type-SafeUint64> #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)] pub struct SafeUint64(u64); @@ -128,9 +135,9 @@ pub enum ParseSafeUint64Error { Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, )] pub struct Amount { - currency: String, - value: u64, - fraction: u32, + pub currency: String, + pub value: u64, + pub fraction: u32, } impl Amount { @@ -262,6 +269,20 @@ pub enum ParseBase32Error { #[derive(Debug, Clone)] pub struct Base32<const L: usize>([u8; L]); +impl<const L: usize> From<[u8; L]> for Base32<L> { + fn from(array: [u8; L]) -> Self { + Self(array) + } +} + +impl<const L: usize> Deref for Base32<L> { + type Target = [u8; L]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl<const L: usize> FromStr for Base32<L> { type Err = ParseBase32Error; diff --git a/wire-gateway/src/main.rs b/wire-gateway/src/main.rs @@ -1,6 +1,20 @@ +use std::{ + str::FromStr, + time::{Duration, SystemTime}, +}; + use api_common::{Amount, SafeUint64, ShortHashCode, Timestamp}; use api_wire::{OutgoingBankTransaction, OutgoingHistory}; use async_compression::tokio::bufread::ZlibDecoder; +use btc_wire::{ + bitcoincore_rpc::{ + bitcoin::{Address, Amount as BtcAmount, BlockHash}, + json::GetTransactionResultDetailCategory as Category, + Client, RpcApi, + }, + rpc::{common_rpc, dirty_guess_network, wallet_rpc}, + sender_address, ClientExtended, +}; use error_codes::ErrorCode; use hyper::{ header, @@ -12,7 +26,7 @@ use tokio::{io::AsyncReadExt, sync::Mutex}; use url::Url; use crate::{ - api_common::ErrorDetail, + api_common::{Base32, ErrorDetail}, api_wire::{ AddIncomingRequest, AddIncomingResponse, HistoryParams, IncomingBankTransaction, IncomingHistory, TransferRequest, TransferResponse, @@ -21,14 +35,127 @@ use crate::{ mod error_codes; +fn bitcoin_payto(addr: &Address) -> Url { + Url::from_str(&format!("payto://bitcoin/{}", addr.to_string())).unwrap() +} + +impl Into<Amount> for BtcAmount { + fn into(self) -> Amount { + let sat = self.as_sat(); + return Amount::new("BTC", sat / 100_000_000, ((sat % 100_000_000) / 10) as u32); + } +} + +impl TryFrom<Amount> for BtcAmount { + type Error = String; + + fn try_from(value: Amount) -> Result<Self, Self::Error> { + if value.currency != "BTC" { + return Err("Wrong currency".to_string()); + } + + let sat = value.value * 100_000_000 + value.fraction as u64 * 10; + return Ok(Self::from_sat(sat)); + } +} + #[tokio::main] async fn main() { + let network = dirty_guess_network(); + { + let common = common_rpc(network).unwrap(); + common.load_wallet("wire").ok(); + } let state = ServerState { incoming: Mutex::new(Vec::new()), outgoing: Mutex::new(Vec::new()), + client: Mutex::new(wallet_rpc(network, "wire")), }; let state: &'static ServerState = Box::leak(Box::new(state)); + // BTC worker thread + + std::thread::spawn(move || { + let rpc = wallet_rpc(network, "wire"); + let mut last_hash: Option<BlockHash> = None; + let confirmation = 1; + + loop { + let txs = rpc + .list_since_block(last_hash.as_ref(), Some(confirmation), None, Some(true)) + .unwrap(); + let self_addr = rpc.get_new_address(None, None).unwrap(); + last_hash = Some(txs.lastblock); + + for tx in txs + .transactions + .into_iter() + .filter(|tx| tx.info.confirmations >= confirmation as i32) + { + let id = &tx.info.txid; + match tx.detail.category { + Category::Send => match rpc.get_tx_op_return(&id) { + Ok((full, wtid)) => { + let sender = sender_address(&rpc, &full).unwrap(); + let credit_account = bitcoin_payto(&sender); + let debit_account = bitcoin_payto(&self_addr); + let time = tx.info.blocktime.unwrap(); + let date = + Timestamp::from(SystemTime::UNIX_EPOCH + Duration::from_secs(time)); + let amount = (tx.detail.amount * -1).to_unsigned().unwrap().into(); + let mut lock = state.outgoing.blocking_lock(); + println!( + "Send {} {} {:?} {}", + &debit_account, &credit_account, &date, &amount + ); + let array: [u8; 32] = wtid[..32].try_into().unwrap(); + let wtid = Base32::from(array); + let row_id = lock.len() as u64 + 1; + lock.push(OutgoingTransaction { + row_id: SafeUint64::try_from(row_id).unwrap(), + date, + amount, + debit_account, + credit_account, + wtid, + }); + } + Err(err) => println!("receive: {}", err), + }, + Category::Receive => match rpc.get_tx_segwit_key(&id) { + Ok((full, reserve_pub)) => { + let sender = sender_address(&rpc, &full).unwrap(); + let debit_account = bitcoin_payto(&sender); + let credit_account = bitcoin_payto(tx.detail.address.as_ref().unwrap()); + let time = tx.info.blocktime.unwrap(); + let date = + Timestamp::from(SystemTime::UNIX_EPOCH + Duration::from_secs(time)); + let amount = tx.detail.amount.to_unsigned().unwrap().into(); + let mut lock = state.incoming.blocking_lock(); + println!( + "Receive {} {} {:?} {}", + &debit_account, &credit_account, &date, &amount + ); + let row_id = lock.len() as u64 + 1; + lock.push(IncomingTransaction { + row_id: SafeUint64::try_from(row_id).unwrap(), + date, + amount, + reserve_pub: reserve_pub.into(), + debit_account, + credit_account, + }); + } + Err(err) => println!("receive: {}", err), + }, + Category::Generate | Category::Immature | Category::Orphan => {} + } + } + + rpc.wait_for_new_block(0).ok(); + } + }); + let addr = ([0, 0, 0, 0], 8080).into(); let make_service = make_service_fn(move |_| async move { Ok::<_, Error>(service_fn(move |req| async move { @@ -88,6 +215,7 @@ struct OutgoingTransaction { struct ServerState { incoming: Mutex<Vec<IncomingTransaction>>, outgoing: Mutex<Vec<OutgoingTransaction>>, + client: Mutex<Client>, } pub mod api_common; @@ -149,18 +277,23 @@ async fn router( "/transfer" => { assert_method(&parts, Method::POST)?; let request: TransferRequest = parse_json(&parts, body).await; - let mut guard = state.outgoing.lock().await; - let row_id = SafeUint64::try_from(guard.len() as u64 + 1).unwrap(); + let client = state.client.lock().await; + let address = request.credit_account.path().trim_start_matches('/'); + dbg!(address); + let to = Address::from_str(address).unwrap(); + let amount: BtcAmount = request.amount.try_into().unwrap(); + client + .send_op_return(&to, amount, request.wtid.as_ref()) + .unwrap(); let timestamp = Timestamp::now(); - guard.push(OutgoingTransaction { - row_id, - date: timestamp, - amount: request.amount, - wtid: request.wtid, - credit_account: request.credit_account, - debit_account: Url::parse("payto://bitcoin").unwrap(), - }); - json_response(StatusCode::OK, &TransferResponse { timestamp, row_id }).await + json_response( + StatusCode::OK, + &TransferResponse { + timestamp, + row_id: SafeUint64::try_from(0).unwrap(), + }, + ) + .await } "/history/incoming" => { assert_method(&parts, Method::GET)?; @@ -199,8 +332,8 @@ async fn router( amount: tx.amount.clone(), credit_account: tx.credit_account.clone(), wtid: tx.wtid.clone(), - debit_account: Url::parse("payto://bitcoin").unwrap(), - exchange_base_url: Url::parse("").unwrap(), + debit_account: tx.debit_account.clone(), + exchange_base_url: Url::parse("http://localhost:8080").unwrap(), }) .collect(); json_response( @@ -223,7 +356,7 @@ async fn router( amount: request.amount, reserve_pub: request.reserve_pub, debit_account: request.debit_account, - credit_account: Url::parse("payto://bitcoin").unwrap() + credit_account: Url::parse("payto://bitcoin").unwrap(), }); json_response(StatusCode::OK, &AddIncomingResponse { timestamp, row_id }).await }