From e215e09cb2ee3336673031c0fb1c66dd6991179c Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Tue, 20 Aug 2024 19:45:40 -0300 Subject: [PATCH] refactor: RPC client now uses bitcoincore-rpc-async Changes to a thin tuple struct wrapper for `bitcoincore-rpc-async::Client`. --- crates/btcio/src/rpc/client.rs | 564 +++------------------- crates/btcio/src/rpc/traits.rs | 67 ++- crates/btcio/src/writer/broadcast.rs | 29 +- crates/btcio/src/writer/builder.rs | 5 +- crates/btcio/src/writer/config.rs | 12 +- crates/btcio/src/writer/test_utils.rs | 70 ++- crates/btcio/src/writer/utils.rs | 4 +- crates/btcio/src/writer/writer_handler.rs | 4 +- sequencer/src/main.rs | 1 - 9 files changed, 187 insertions(+), 569 deletions(-) diff --git a/crates/btcio/src/rpc/client.rs b/crates/btcio/src/rpc/client.rs index 52c82588e..c5efbd270 100644 --- a/crates/btcio/src/rpc/client.rs +++ b/crates/btcio/src/rpc/client.rs @@ -1,544 +1,104 @@ -use std::sync::atomic::AtomicU64; -use std::time::Duration; -use std::{fmt::Display, str::FromStr}; - use async_trait::async_trait; -use bitcoin::consensus::encode::{deserialize_hex, serialize_hex}; -use bitcoin::Txid; - -use base64::engine::general_purpose; -use base64::Engine; -use bitcoin::{ - block::{Header, Version}, - consensus::deserialize, - hash_types::TxMerkleNode, - hashes::Hash as _, - Address, Block, BlockHash, CompactTarget, Network, Transaction, -}; -use reqwest::header::HeaderMap; -use reqwest::StatusCode; -use serde::{Deserialize, Serialize}; -use serde_json::{json, to_value, value::RawValue, value::Value}; -use thiserror::Error; -use tracing::*; +use bitcoin::{Amount, Txid}; +use bitcoin::{Block, BlockHash, Transaction}; +use bitcoincore_rpc_async::Error as RpcError; +use bitcoincore_rpc_async::{Auth, Client}; +use super::traits::BitcoinClient; use super::types::{RawUTXO, RpcBlockchainInfo}; -use super::{traits::BitcoinClient, types::GetTransactionResponse}; - -const MAX_RETRIES: u32 = 3; -pub fn to_val(value: T) -> ClientResult -where - T: Serialize, -{ - to_value(value).map_err(|e| ClientError::Param(format!("Error creating value: {}", e))) -} - -// Represents a JSON-RPC error. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct RpcError { - pub code: i32, - pub message: String, -} - -impl Display for RpcError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "RPCError code {}: {}", self.code, self.message) - } -} - -// Response is a struct that represents a response returned by the Bitcoin RPC -// It is generic over the type of the result field, which is usually a String in Bitcoin Core -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -struct Response { - pub result: Option, - pub error: Option, - pub id: u64, -} - -// BitcoinClient is a struct that represents a connection to a Bitcoin RPC node +/// Thin wrapper around the [`bitcoincore_rpc_async`]'s [`Client`]. +/// +/// Provides a simple interface to interact asynchronously with a +/// Bitcoin Core node via JSON-RPC. #[derive(Debug)] -pub struct BitcoinDClient { - url: String, - client: reqwest::Client, - network: Network, - next_id: AtomicU64, -} - -#[derive(Debug, Error)] -pub enum ClientError { - #[error("Network: {0}")] - Network(String), - - #[error("RPC server returned error '{1}' (code {0})")] - Server(i32, String), - - #[error("Error parsing rpc response: {0}")] - Parse(String), - - #[error("Could not create RPC Param")] - Param(String), - - #[error("{0}")] - Body(String), - - #[error("Obtained failure status({0}): {1}")] - Status(StatusCode, String), - - #[error("Malformed Response: {0}")] - MalformedResponse(String), - - #[error("Could not connect: {0}")] - Connection(String), - - #[error("Timeout")] - Timeout, - - #[error("HttpRedirect: {0}")] - HttpRedirect(String), - - #[error("Could not build request: {0}")] - ReqBuilder(String), - - #[error("Max retries {0} exceeded")] - MaxRetriesExceeded(u32), - - #[error("Could not create request: {0}")] - Request(String), - - #[error("Network address: {0}")] - WrongNetworkAddress(Network), - - #[error("Could not sign")] - Signing(Vec), - - #[error("{0}")] - Other(String), -} - -impl From for ClientError { - fn from(value: serde_json::error::Error) -> Self { - Self::Parse(format!("Could not parse {}", value)) - } -} - -type ClientResult = Result; +pub struct BitcoinDClient(Client); impl BitcoinDClient { - pub fn new(url: String, username: String, password: String, network: Network) -> Self { - let mut headers = HeaderMap::new(); - let mut user_pw = String::new(); - general_purpose::STANDARD.encode_string(format!("{}:{}", username, password), &mut user_pw); - - headers.insert( - "Authorization", - format!("Basic {}", user_pw) - .parse() - .expect("Failed to parse auth header!"), - ); - headers.insert( - "Content-Type", - "application/json" - .parse() - .expect("Failed to parse content type header!"), - ); - - let client = reqwest::Client::builder() - .default_headers(headers) - .build() - .expect("Failed to build client!"); - - Self { - url, - client, - network, - next_id: AtomicU64::new(0), - } + /// Creates a new [`BitcoinDClient`] instance. + /// + /// # Note + /// + /// The only supported [`Auth`] method is [`UserPass`](Auth::UserPass), + /// by providing a `username` and `password`. + pub async fn new(url: String, username: String, password: String) -> Result { + let auth = Auth::UserPass(username, password); + Ok(BitcoinDClient(Client::new(url, auth).await?)) } +} - pub fn network(&self) -> Network { - self.network +#[async_trait] +impl BitcoinClient for BitcoinDClient { + async fn estimate_smart_fee(&self, conf_target: u16) -> Result { + self.estimate_smart_fee(conf_target).await } - fn next_id(&self) -> u64 { - self.next_id - .fetch_add(1, std::sync::atomic::Ordering::AcqRel) + async fn get_block(&self, hash: BlockHash) -> Result { + self.get_block(hash).await } - async fn call( - &self, - method: &str, - params: &[serde_json::Value], - ) -> ClientResult { - let mut retries = 0; - loop { - let id = self.next_id(); - let response = self - .client - .post(&self.url) - .json(&json!({ - "jsonrpc": "1.0", - "id": id, - "method": method, - "params": params - })) - .send() - .await; - - match response { - Ok(resp) => { - let data = resp - .json::>() - .await - .map_err(|e| ClientError::Parse(e.to_string()))?; - if let Some(err) = data.error { - return Err(ClientError::Server(err.code, err.message)); - } - return data - .result - .ok_or_else(|| ClientError::Other("Empty data received".to_string())); - } - Err(err) => { - warn!(err = %err, "Error calling bitcoin client"); - - if err.is_body() { - // Body error, unlikely to be recoverable by retrying - return Err(ClientError::Body(err.to_string())); - } else if err.is_status() { - // HTTP status error, not retryable - let e = match err.status() { - Some(code) => ClientError::Status(code, err.to_string()), - _ => ClientError::Other(err.to_string()), - }; - return Err(e); - } else if err.is_decode() { - // Error decoding the response, retry might not help - return Err(ClientError::MalformedResponse(err.to_string())); - } else if err.is_connect() { - // Connection error, retry might help - let e = ClientError::Connection(err.to_string()); - warn!(%e, "connection error, retrying..."); - } else if err.is_timeout() { - let e = ClientError::Timeout; - // Timeout error, retry might help - warn!(%e, "timeout error, retrying..."); - } else if err.is_request() { - // General request error, retry might help - let e = ClientError::Request(err.to_string()); - warn!(%e, "request error, retrying..."); - } else if err.is_builder() { - // Error building the request, unlikely to be recoverable - return Err(ClientError::ReqBuilder(err.to_string())); - } else if err.is_redirect() { - // Redirect error, not retryable - return Err(ClientError::HttpRedirect(err.to_string())); - } else { - // Unknown error, unlikely to be recoverable - return Err(ClientError::Other("Unknown error".to_string())); - } - - retries += 1; - if retries >= MAX_RETRIES { - return Err(ClientError::MaxRetriesExceeded(MAX_RETRIES)); - } - tokio::time::sleep(Duration::from_millis(1000)).await; - } - }; - } + async fn get_block_at(&self, height: u64) -> Result { + self.get_block_at(height).await } - // get_block_count returns the current block height - pub async fn get_block_count(&self) -> ClientResult { - self.call::("getblockcount", &[]).await + async fn get_block_count(&self) -> Result { + self.get_block_count().await } - // This returns [(txid, timestamp)] - pub async fn list_transactions(&self, confirmations: u32) -> ClientResult> { - let res = self - .call::("listtransactions", &[to_value(confirmations)?]) - .await?; - if let serde_json::Value::Array(array) = res { - Ok(array - .iter() - .map(|el| { - ( - serde_json::from_value::(el.get("txid").unwrap().clone()) - .unwrap() - .clone(), - serde_json::from_value::(el.get("time").unwrap().clone()).unwrap(), - ) - }) - .collect()) - } else { - Err(ClientError::MalformedResponse(res.to_string())) - } + async fn get_block_hash(&self, height: u64) -> Result { + self.get_block_hash(height).await } - // get_mempool_txids returns a list of txids in the current mempool - pub async fn get_mempool_txids(&self) -> ClientResult> { - let result = self - .call::>("getrawmempool", &[]) - .await? - .to_string(); - - serde_json::from_str::>(&result) - .map_err(|e| ClientError::MalformedResponse(e.to_string())) + async fn get_blockchain_info(&self) -> Result { + self.get_blockchain_info().await } - // get_block returns the block at the given hash - pub async fn get_block(&self, hash: BlockHash) -> ClientResult { - let result = self - .call::>("getblock", &[to_value(hash.to_string())?, to_value(3)?]) - .await? - .to_string(); - - let full_block: serde_json::Value = serde_json::from_str(&result)?; - - let header: anyhow::Result
= (|| { - Ok(Header { - bits: CompactTarget::from_consensus(u32::from_str_radix( - full_block["bits"].as_str().unwrap(), - 16, - )?), - merkle_root: TxMerkleNode::from_str(full_block["merkleroot"].as_str().unwrap())?, - nonce: full_block["nonce"].as_u64().unwrap() as u32, - prev_blockhash: BlockHash::from_str( - full_block["previousblockhash"].as_str().unwrap(), - )?, - time: full_block["time"].as_u64().unwrap() as u32, - version: Version::from_consensus(full_block["version"].as_u64().unwrap() as i32), - }) - })(); - let header = header.map_err(|e| ClientError::Other(e.to_string()))?; - - let txdata = full_block["tx"].as_array().unwrap(); - - let txs: Vec = txdata - .iter() - .map(|tx| { - let tx_hex = tx["hex"].as_str().unwrap(); - deserialize(&hex::decode(tx_hex).unwrap()).unwrap() - // parse_hex_transaction(tx_hex).unwrap() // hex from rpc cannot be invalid - }) - .collect(); - - Ok(Block { - header, - txdata: txs, - }) + async fn get_new_address(&self) -> Result { + self.get_new_address().await } - pub async fn list_since_block(&self, blockhash: String) -> ClientResult> { - let result = self - .call::>("listsinceblock", &[to_value(blockhash)?]) - .await? - .to_string(); - - let rawdata: serde_json::Value = serde_json::from_str(&result)?; - let rawdata = rawdata.get("transactions").unwrap(); - let rawtxns = rawdata.as_array().unwrap(); - let txids = rawtxns - .iter() - .map(|x| x.get("txid").unwrap().as_str().unwrap().to_string()) - .collect(); - Ok(txids) + async fn get_raw_mempool(&self) -> Result, RpcError> { + self.get_raw_mempool().await } - // get_change_address returns a change address for the wallet of bitcoind - async fn get_change_address(&self) -> ClientResult
{ - let address_string = self.call::("getrawchangeaddress", &[]).await?; - let addr = Address::from_str(&address_string).and_then(|x| x.require_network(self.network)); - addr.map_err(|_| ClientError::WrongNetworkAddress(self.network)) + async fn get_transaction_confirmations + Send>( + &self, + txid: T, + ) -> Result { + self.get_transaction_confirmations(txid).await } - pub async fn get_change_addresses(&self) -> ClientResult<[Address; 2]> { - let change_address = self.get_change_address().await?; - let change_address_2 = self.get_change_address().await?; - - Ok([change_address, change_address_2]) + async fn get_utxos(&self) -> Result, RpcError> { + self.get_utxos().await } - #[cfg(test)] - pub async fn send_to_address(&self, address: String, amt: u32) -> anyhow::Result { - if self.network == Network::Regtest { - let result = self - .call::>( - "sendtoaddress", - &[ - to_value(address)?, - to_value(amt)?, - // All the following items are needed to pass the fee-rate and fee-rate - // needs to be passed just in case the regtest - // chain cannot estimate fee rate due to - // insufficient blocks - to_value("")?, - to_value("")?, - to_value(true)?, - to_value(true)?, - to_value(>::None)?, - to_value("unset")?, - to_value(>::None)?, - to_value(1.1)?, // fee rate - ], - ) - .await; - Ok(result.unwrap().to_string()) - } else { - Err(anyhow::anyhow!( - "Cannot send_to_address on non-regtest network" - )) - } - } - pub async fn list_wallets(&self) -> ClientResult> { - self.call::>("listwallets", &[]).await + async fn list_since_block( + &self, + block_hash: BlockHash, + ) -> Result, RpcError> { + self.list_since_block(block_hash).await } -} -#[async_trait] -impl BitcoinClient for BitcoinDClient { - async fn get_blockchain_info(&self) -> ClientResult { - let res = self - .call::("getblockchaininfo", &[]) - .await?; - Ok(res) + async fn list_transactions(&self, count: Option) -> Result, RpcError> { + self.list_transactions(count).await } - // get_block_hash returns the block hash of the block at the given height - async fn get_block_hash(&self, height: u64) -> ClientResult { - let hash = self - .call::("getblockhash", &[to_value(height)?]) - .await?; - Ok( - BlockHash::from_str(&hash) - .map_err(|e| ClientError::MalformedResponse(e.to_string()))?, - ) + async fn list_wallets(&self) -> Result, RpcError> { + self.list_wallets().await } - async fn get_block_at(&self, height: u64) -> ClientResult { - let hash = self.get_block_hash(height).await?; - let block = self.get_block(hash).await?; - Ok(block) + async fn send_raw_transaction + Send>(&self, tx: T) -> Result { + self.send_raw_transaction(tx).await } - // send_raw_transaction sends a raw transaction to the network - async fn send_raw_transaction + Send>(&self, tx: T) -> ClientResult { - let txstr = hex::encode(tx); - let resp = self - .call::("sendrawtransaction", &[to_value(txstr)?]) - .await?; - - let hex = hex::decode(resp.clone()); - match hex { - Ok(hx) => { - if hx.len() != 32 { - return Err(ClientError::MalformedResponse(resp)); - } - let mut arr: [u8; 32] = [0; 32]; - arr.copy_from_slice(&hx); - Ok(Txid::from_slice(&arr) - .map_err(|e| ClientError::MalformedResponse(e.to_string()))?) - } - Err(e) => Err(ClientError::MalformedResponse(e.to_string())), - } + async fn send_to_address(&self, address: &str, amount: Amount) -> Result { + self.send_to_address(address, amount).await } - async fn get_transaction_confirmations + Send>( + async fn sign_raw_transaction_with_wallet( &self, - txid: T, - ) -> ClientResult { - let mut txid = txid.as_ref().to_vec(); - txid.reverse(); - let txid = hex::encode(&txid); - let result = self - .call::("gettransaction", &[to_val(txid)?]) - .await?; - - Ok(result.confirmations) - } - - // get_utxos returns all unspent transaction outputs for the wallets of bitcoind - async fn get_utxos(&self) -> ClientResult> { - let utxos = self - .call::>("listunspent", &[to_value(0)?, to_value(9999999)?]) - .await?; - - if utxos.is_empty() { - return Err(ClientError::Other("No UTXOs found".to_string())); - } - - Ok(utxos) - } - - // estimate_smart_fee estimates the fee to confirm a transaction in the next block - async fn estimate_smart_fee(&self) -> ClientResult { - let result = self - .call::>("estimatesmartfee", &[to_value(1)?]) - .await? - .to_string(); - - let result_map: serde_json::Value = serde_json::from_str(&result)?; - - let btc_vkb = result_map - .get("feerate") - .unwrap_or(&serde_json::Value::from_str("0.00001").unwrap()) - .as_f64() - .unwrap(); - - // convert to sat/vB and round up - Ok((btc_vkb * 100_000_000.0 / 1000.0).ceil() as u64) - } - - // sign_raw_transaction_with_wallet signs a raw transaction with the wallet of bitcoind - async fn sign_raw_transaction_with_wallet(&self, tx: Transaction) -> ClientResult { - #[derive(Serialize, Deserialize, Debug)] - struct SignError { - txid: String, - vout: u32, - witness: Vec, - #[serde(rename = "scriptSig")] - script_sig: String, - sequence: u32, - error: String, - } - - #[derive(Serialize, Deserialize, Debug)] - struct SignRPCResponse { - hex: String, - complete: bool, - errors: Option>, - } - - let txraw = serialize_hex(&tx); - let res = self - .call::("signrawtransactionwithwallet", &[to_value(txraw)?]) - .await?; - - match res.errors { - None => { - let hex = res.hex; - let tx = deserialize_hex(&hex).map_err(|e| ClientError::Parse(e.to_string()))?; - Ok(tx) - } - Some(ref errors) => { - warn!("Error while signing with wallet: {:?}", res.errors); - let errs = errors - .iter() - .map(|x| x.error.clone()) - .collect::>(); - - // TODO: This throws error even when a transaction is partially signed. There does - // not seem to be other way to distinguish partially signed error from other - // errors. So in future, we might need to handle that particular case where error - // message is "CHECK(MULTI)SIG failing with non-zero signature (possibly need more - // signatures)" - Err(ClientError::Signing(errs)) - } - } - } - - fn get_network(&self) -> Network { - self.network + tx: Transaction, + ) -> Result { + self.sign_raw_transaction_with_wallet(tx).await } } - -// TODO: Add functional tests diff --git a/crates/btcio/src/rpc/traits.rs b/crates/btcio/src/rpc/traits.rs index 8970e5d89..7dc01f026 100644 --- a/crates/btcio/src/rpc/traits.rs +++ b/crates/btcio/src/rpc/traits.rs @@ -1,10 +1,8 @@ use async_trait::async_trait; -use bitcoin::{Block, BlockHash, Network, Transaction, Txid}; +use bitcoin::{Amount, Block, BlockHash, Transaction, Txid}; +use bitcoincore_rpc_async::Error as RpcError; -use super::{ - types::{RawUTXO, RpcBlockchainInfo}, - ClientError, -}; +use super::types::{RawUTXO, RpcBlockchainInfo}; /// Basic functionality that any Bitcoin client that interacts with the /// Bitcoin network should provide. @@ -14,6 +12,10 @@ pub trait BitcoinClient: Sync + Send + 'static { /// to begin confirmation within conf_target blocks if possible and return /// the number of blocks for which the estimate is valid. /// + /// # Parameters + /// + /// - `conf_target`: Confirmation target in blocks. + /// /// # Note /// /// Uses virtual transaction size as defined in @@ -22,16 +24,33 @@ pub trait BitcoinClient: Sync + Send + 'static { /// /// By default uses the estimate mode of `CONSERVATIVE` which is the /// default in Bitcoin Core v27. - async fn estimate_smart_fee(&self) -> Result; + async fn estimate_smart_fee(&self, conf_target: u16) -> Result; + + /// Gets a [`Block`] with the given hash. + async fn get_block(&self, hash: BlockHash) -> Result; /// Gets a [`Block`] at given height. - async fn get_block_at(&self, height: u64) -> Result; + async fn get_block_at(&self, height: u64) -> Result; + + /// Gets the height of the most-work fully-validated chain. + /// + /// # Note + /// + /// The genesis block has a height of 0. + async fn get_block_count(&self) -> Result; /// Gets the [`BlockHash`] at given height. - async fn get_block_hash(&self, height: u64) -> Result; + async fn get_block_hash(&self, height: u64) -> Result; + async fn get_blockchain_info(&self) -> Result; /// Gets various state info regarding blockchain processing. - async fn get_blockchain_info(&self) -> Result; + + /// Generates new address under own control for the underlying Bitcoin + /// client's wallet. + async fn get_new_address(&self) -> Result; + + /// Gets all transaction ids in mempool. + async fn get_raw_mempool(&self) -> Result, RpcError>; /// Gets the number of confirmations for a given transaction id /// (txid). @@ -48,11 +67,26 @@ pub trait BitcoinClient: Sync + Send + 'static { async fn get_transaction_confirmations + Send>( &self, txid: T, - ) -> Result; + ) -> Result; /// Gets all Unspent Transaction Outputs (UTXOs) for the underlying Bitcoin /// client's wallet. - async fn get_utxos(&self) -> Result, ClientError>; + async fn get_utxos(&self) -> Result, RpcError>; + + /// Gets all transactions in blocks since block [`Blockhash`]. + async fn list_since_block(&self, block_hash: BlockHash) + -> Result, RpcError>; + + /// Lists transactions in the underlying Bitcoin client's wallet. + /// + /// # Parameters + /// + /// - `count`: The number of transactions to list. If `None`, assumes a maximum of 10 + /// transactions. + async fn list_transactions(&self, count: Option) -> Result, RpcError>; + + /// Lists all wallets in the underlying Bitcoin client. + async fn list_wallets(&self) -> Result, RpcError>; /// Sends a raw transaction to the network. /// @@ -60,8 +94,10 @@ pub trait BitcoinClient: Sync + Send + 'static { /// /// - `tx`: The raw transaction to send. This should be a byte array containing the serialized /// raw transaction data. - async fn send_raw_transaction + Send>(&self, tx: T) - -> Result; + async fn send_raw_transaction + Send>(&self, tx: T) -> Result; + + /// Sends an amount to a given address. + async fn send_to_address(&self, address: &str, amount: Amount) -> Result; /// Signs a transaction using the keys available in the underlying Bitcoin /// client's wallet and returns a signed transaction. @@ -73,8 +109,5 @@ pub trait BitcoinClient: Sync + Send + 'static { async fn sign_raw_transaction_with_wallet( &self, tx: Transaction, - ) -> Result; - - /// Returns the [`Network`] of the underlying Bitcoin client. - fn get_network(&self) -> Network; + ) -> Result; } diff --git a/crates/btcio/src/writer/broadcast.rs b/crates/btcio/src/writer/broadcast.rs index 95d09ed2b..600be9986 100644 --- a/crates/btcio/src/writer/broadcast.rs +++ b/crates/btcio/src/writer/broadcast.rs @@ -3,6 +3,7 @@ use std::{sync::Arc, time::Duration}; use alpen_express_rpc_types::L1Status; use anyhow::anyhow; use bitcoin::{consensus::deserialize, Txid}; +use bitcoincore_rpc_async::Error as RpcError; use tokio::sync::RwLock; use tracing::*; @@ -12,7 +13,7 @@ use alpen_express_db::{ }; use crate::{ - rpc::{traits::BitcoinClient, ClientError}, + rpc::traits::BitcoinClient, writer::utils::{get_blob_by_idx, get_l1_tx}, }; @@ -87,17 +88,17 @@ pub async fn broadcaster_task( } curr_idx += 1; } - Err(SendError::MissingOrInvalidInput) => { + Err(e) => { + // DEBUG: What the hell is this? + // FIXME: remove me + #[cfg(debug_assertions)] + dbg!(&e); // Means need to Resign/Republish blobentry.status = BlobL1Status::NeedsResign; db.sequencer_store() .update_blob_by_idx(curr_idx, blobentry)?; } - Err(SendError::Other(e)) => { - warn!(%e, "Error sending !"); - // TODO: Maybe retry or return? - } } } else { debug!(%curr_idx, "No blob found"); @@ -105,26 +106,16 @@ pub async fn broadcaster_task( } } -enum SendError { - MissingOrInvalidInput, - Other(String), -} - async fn send_commit_reveal_txs( commit_tx_raw: Vec, reveal_tx_raw: Vec, client: &impl BitcoinClient, -) -> Result<(), SendError> { +) -> Result<(), RpcError> { send_tx(commit_tx_raw, client).await?; send_tx(reveal_tx_raw, client).await?; Ok(()) } -async fn send_tx(tx_raw: Vec, client: &impl BitcoinClient) -> Result<(), SendError> { - match client.send_raw_transaction(tx_raw).await { - Ok(_) => Ok(()), - Err(ClientError::Server(-27, _)) => Ok(()), // Tx already in chain - Err(ClientError::Server(-25, _)) => Err(SendError::MissingOrInvalidInput), - Err(e) => Err(SendError::Other(e.to_string())), - } +async fn send_tx(tx_raw: Vec, client: &impl BitcoinClient) -> Result { + client.send_raw_transaction(tx_raw).await } diff --git a/crates/btcio/src/writer/builder.rs b/crates/btcio/src/writer/builder.rs index 3b3bc4703..902ec70ce 100644 --- a/crates/btcio/src/writer/builder.rs +++ b/crates/btcio/src/writer/builder.rs @@ -101,6 +101,7 @@ pub async fn build_inscription_txs( rpc_client: &Arc, config: &WriterConfig, ) -> anyhow::Result<(Transaction, Transaction)> { + let network = config.network; // let (signature, pub_key) = sign_blob_with_private_key(&payload, &config.private_key)?; let utxos = rpc_client.get_utxos().await?; let utxos = utxos @@ -110,7 +111,7 @@ pub async fn build_inscription_txs( .map_err(|e| anyhow::anyhow!("{:?}", e))?; let fee_rate = match config.inscription_fee_policy { - InscriptionFeePolicy::Smart => rpc_client.estimate_smart_fee().await?, + InscriptionFeePolicy::Smart => rpc_client.estimate_smart_fee(1).await?, InscriptionFeePolicy::Fixed(val) => val, }; create_inscription_transactions( @@ -120,7 +121,7 @@ pub async fn build_inscription_txs( config.sequencer_address.clone(), config.amount_for_reveal_txn, fee_rate, - rpc_client.get_network(), + network, ) .map_err(|e| anyhow::anyhow!(e.to_string())) } diff --git a/crates/btcio/src/writer/config.rs b/crates/btcio/src/writer/config.rs index 3975f028e..88908b780 100644 --- a/crates/btcio/src/writer/config.rs +++ b/crates/btcio/src/writer/config.rs @@ -1,4 +1,4 @@ -use bitcoin::Address; +use bitcoin::{Address, Network}; use std::str::FromStr; #[derive(Debug, Clone)] @@ -17,14 +17,13 @@ pub struct WriterConfig { /// How much amount(in sats) to send to reveal address pub(super) amount_for_reveal_txn: u64, + + /// The Bitcoin [`Network`] to use + pub(super) network: Network, } impl WriterConfig { - pub fn new( - address: String, - network: bitcoin::Network, - rollup_name: String, - ) -> anyhow::Result { + pub fn new(address: String, network: Network, rollup_name: String) -> anyhow::Result { let addr = Address::from_str(&address)?.require_network(network)?; Ok(Self { sequencer_address: addr, @@ -33,6 +32,7 @@ impl WriterConfig { inscription_fee_policy: InscriptionFeePolicy::Fixed(100), poll_duration_ms: 1000, amount_for_reveal_txn: 1000, + network, }) } } diff --git a/crates/btcio/src/writer/test_utils.rs b/crates/btcio/src/writer/test_utils.rs index 539480754..78f16331c 100644 --- a/crates/btcio/src/writer/test_utils.rs +++ b/crates/btcio/src/writer/test_utils.rs @@ -1,11 +1,11 @@ use alpen_test_utils::ArbitraryGenerator; use async_trait::async_trait; -use bitcoin::{consensus::deserialize, hashes::Hash, Block, BlockHash, Network, Transaction, Txid}; +use bitcoin::{consensus::deserialize, hashes::Hash, Amount, Block, BlockHash, Transaction, Txid}; +use bitcoincore_rpc_async::Error as RpcError; use crate::rpc::{ traits::BitcoinClient, types::{RawUTXO, RpcBlockchainInfo}, - ClientError, }; pub struct BitcoinDTestClient { @@ -22,35 +22,50 @@ const TEST_BLOCKSTR: &str = "000000207d862a78fcb02ab24ebd154a20b9992af6d2f0c94d3 #[async_trait] impl BitcoinClient for BitcoinDTestClient { - async fn get_blockchain_info(&self) -> Result { - Ok(ArbitraryGenerator::new().generate()) + async fn estimate_smart_fee(&self, _conf_target: u16) -> Result { + Ok(3) // hardcoded to 3 sats/vByte } - async fn get_block_at(&self, _height: u64) -> Result { + async fn get_block(&self, _hash: BlockHash) -> Result { let block: Block = deserialize(&hex::decode(TEST_BLOCKSTR).unwrap()).unwrap(); Ok(block) } - async fn get_block_hash(&self, _h: u64) -> Result { + async fn get_block_at(&self, _height: u64) -> Result { + let block: Block = deserialize(&hex::decode(TEST_BLOCKSTR).unwrap()).unwrap(); + Ok(block) + } + + async fn get_block_count(&self) -> Result { + Ok(1) + } + + async fn get_block_hash(&self, _height: u64) -> Result { let block: Block = deserialize(&hex::decode(TEST_BLOCKSTR).unwrap()).unwrap(); Ok(block.block_hash()) } - async fn send_raw_transaction + Send>( - &self, - _tx: T, - ) -> Result { - Ok(Txid::from_slice(&[1u8; 32]).unwrap()) + async fn get_blockchain_info(&self) -> Result { + Ok(ArbitraryGenerator::new().generate()) + } + + async fn get_new_address(&self) -> Result { + // random regtest address from https://bitcoin.stackexchange.com/q/91222 + Ok("bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw".to_string()) + } + + async fn get_raw_mempool(&self) -> Result, RpcError> { + Ok(vec![]) } async fn get_transaction_confirmations + Send>( &self, _txid: T, - ) -> Result { + ) -> Result { Ok(self.confs) } - async fn get_utxos(&self) -> Result, ClientError> { + async fn get_utxos(&self) -> Result, RpcError> { // Generate enough utxos to cover for the costs later let utxos: Vec<_> = (1..10) .map(|_| ArbitraryGenerator::new().generate()) @@ -67,18 +82,33 @@ impl BitcoinClient for BitcoinDTestClient { Ok(utxos) } - async fn estimate_smart_fee(&self) -> Result { - Ok(3) // hardcoded to 3 sats/vByte + async fn list_since_block( + &self, + _blockhash: BlockHash, + ) -> Result, RpcError> { + Ok(vec![]) + } + + async fn list_transactions(&self, _count: Option) -> Result, RpcError> { + Ok(vec![]) + } + + async fn list_wallets(&self) -> Result, RpcError> { + Ok(vec![]) + } + + async fn send_raw_transaction + Send>(&self, _tx: T) -> Result { + Ok(Txid::from_slice(&[1u8; 32]).unwrap()) + } + + async fn send_to_address(&self, _address: &str, _amount: Amount) -> Result { + Ok(Txid::from_slice(&[0u8; 32]).unwrap()) } async fn sign_raw_transaction_with_wallet( &self, tx: Transaction, - ) -> Result { + ) -> Result { Ok(tx) } - - fn get_network(&self) -> Network { - Network::Regtest - } } diff --git a/crates/btcio/src/writer/utils.rs b/crates/btcio/src/writer/utils.rs index 5e869e4bc..6cd6d0e68 100644 --- a/crates/btcio/src/writer/utils.rs +++ b/crates/btcio/src/writer/utils.rs @@ -166,9 +166,10 @@ mod test { } fn get_config() -> WriterConfig { + let network = Network::Regtest; let addr = Address::from_str("bcrt1q6u6qyya3sryhh42lahtnz2m7zuufe7dlt8j0j5") .unwrap() - .require_network(Network::Regtest) + .require_network(network) .unwrap(); WriterConfig { sequencer_address: addr, @@ -176,6 +177,7 @@ mod test { inscription_fee_policy: InscriptionFeePolicy::Fixed(100), poll_duration_ms: 1000, amount_for_reveal_txn: 1000, + network, } } diff --git a/crates/btcio/src/writer/writer_handler.rs b/crates/btcio/src/writer/writer_handler.rs index cfe6d5a87..cf61a9d05 100644 --- a/crates/btcio/src/writer/writer_handler.rs +++ b/crates/btcio/src/writer/writer_handler.rs @@ -232,9 +232,10 @@ mod test { } fn get_config() -> WriterConfig { + let network = Network::Regtest; let addr = Address::from_str("bcrt1q6u6qyya3sryhh42lahtnz2m7zuufe7dlt8j0j5") .unwrap() - .require_network(Network::Regtest) + .require_network(network) .unwrap(); WriterConfig { sequencer_address: addr, @@ -242,6 +243,7 @@ mod test { inscription_fee_policy: InscriptionFeePolicy::Fixed(100), poll_duration_ms: 1000, amount_for_reveal_txn: 1000, + network, } } diff --git a/sequencer/src/main.rs b/sequencer/src/main.rs index 2de0d85bd..48f5932a6 100644 --- a/sequencer/src/main.rs +++ b/sequencer/src/main.rs @@ -203,7 +203,6 @@ fn main_inner(args: Args) -> anyhow::Result<()> { bitcoind_url, config.bitcoind_rpc.rpc_user.clone(), config.bitcoind_rpc.rpc_password.clone(), - bitcoin::Network::Regtest, ); let btc_rpc = Arc::new(btc_rpc);