commit 491410969b1fec9c0e01def555a970f02c3d76af
parent b19e53a7a82b9972b720f8f0424c17ae1afe2004
Author: Antoine A <>
Date: Mon, 29 Nov 2021 18:56:57 +0100
Draft bitcoin wire implementation
Diffstat:
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
}