From 2b11429547256a6baff0c504b3d4635da2254c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Olav?= Date: Wed, 6 Nov 2024 09:25:23 +0100 Subject: [PATCH] rpc+wallet: add list transactions rpc --- Cargo.lock | 1 + Cargo.toml | 1 + src/server.rs | 76 ++++++++++++++++++++++++++++++++++++++--------- src/types.rs | 13 +++++++- src/wallet/mod.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 148 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0bd62e..9045617 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,6 +487,7 @@ dependencies = [ "parking_lot", "prost", "prost-build", + "prost-types", "protox", "rand", "regex", diff --git a/Cargo.toml b/Cargo.toml index a251557..6c221ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ nonempty = "0.10.0" parking_lot = { version = "0.12.3", features = ["send_guard"] } prost = "0.13.2" rand = "0.8.5" +prost-types = "0.13.3" regex = "1.11.0" rusqlite = { version = "0.28.0", features = ["bundled"] } rusqlite_migration = "1.0.2" diff --git a/src/server.rs b/src/server.rs index ebb1f30..bf04f4a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,5 +1,6 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, sync::Arc, vec}; +use bdk_wallet::chain::{ChainPosition, ConfirmationBlockTime}; use bitcoin::{ absolute::Height, hashes::{hmac, ripemd160, sha256, sha512, Hash, HashEngine}, @@ -11,6 +12,7 @@ use futures::{ StreamExt as _, }; use miette::IntoDiagnostic as _; +use prost_types::Timestamp; use thiserror::Error; use tonic::{Request, Response, Status}; @@ -29,19 +31,21 @@ use crate::{ create_sidechain_proposal_response, get_bmm_h_star_commitment_response, get_ctip_response::Ctip, get_sidechain_proposals_response::SidechainProposal, get_sidechains_response::SidechainInfo, server::ValidatorService, - wallet_service_server::WalletService, BroadcastWithdrawalBundleRequest, - BroadcastWithdrawalBundleResponse, CreateBmmCriticalDataTransactionRequest, - CreateBmmCriticalDataTransactionResponse, CreateDepositTransactionRequest, - CreateDepositTransactionResponse, CreateNewAddressRequest, CreateNewAddressResponse, - CreateSidechainProposalRequest, CreateSidechainProposalResponse, GenerateBlocksRequest, - GenerateBlocksResponse, GetBalanceRequest, GetBalanceResponse, - GetBlockHeaderInfoRequest, GetBlockHeaderInfoResponse, GetBlockInfoRequest, - GetBlockInfoResponse, GetBmmHStarCommitmentRequest, GetBmmHStarCommitmentResponse, - GetChainInfoRequest, GetChainInfoResponse, GetChainTipRequest, GetChainTipResponse, - GetCoinbasePsbtRequest, GetCoinbasePsbtResponse, GetCtipRequest, GetCtipResponse, - GetSidechainProposalsRequest, GetSidechainProposalsResponse, GetSidechainsRequest, - GetSidechainsResponse, GetTwoWayPegDataRequest, GetTwoWayPegDataResponse, Network, - SubscribeEventsRequest, SubscribeEventsResponse, + wallet_service_server::WalletService, wallet_transaction::Confirmation, + BroadcastWithdrawalBundleRequest, BroadcastWithdrawalBundleResponse, + CreateBmmCriticalDataTransactionRequest, CreateBmmCriticalDataTransactionResponse, + CreateDepositTransactionRequest, CreateDepositTransactionResponse, + CreateNewAddressRequest, CreateNewAddressResponse, CreateSidechainProposalRequest, + CreateSidechainProposalResponse, GenerateBlocksRequest, GenerateBlocksResponse, + GetBalanceRequest, GetBalanceResponse, GetBlockHeaderInfoRequest, + GetBlockHeaderInfoResponse, GetBlockInfoRequest, GetBlockInfoResponse, + GetBmmHStarCommitmentRequest, GetBmmHStarCommitmentResponse, GetChainInfoRequest, + GetChainInfoResponse, GetChainTipRequest, GetChainTipResponse, GetCoinbasePsbtRequest, + GetCoinbasePsbtResponse, GetCtipRequest, GetCtipResponse, GetSidechainProposalsRequest, + GetSidechainProposalsResponse, GetSidechainsRequest, GetSidechainsResponse, + GetTwoWayPegDataRequest, GetTwoWayPegDataResponse, ListTransactionsRequest, + ListTransactionsResponse, Network, SubscribeEventsRequest, SubscribeEventsResponse, + WalletTransaction, }, }, types::{Event, SidechainNumber}, @@ -925,6 +929,50 @@ impl WalletService for Arc { Ok(tonic::Response::new(response)) } + + async fn list_transactions( + &self, + _: tonic::Request, + ) -> Result, tonic::Status> { + let transactions = self + .list_wallet_transactions() + .await + .map_err(|err| err.into_status())?; + + let response = ListTransactionsResponse { + transactions: transactions + .into_iter() + .map(|tx| WalletTransaction { + txid: Some(ReverseHex::encode(&tx.txid)), + fee_sats: tx.fee.to_sat(), + received_sats: tx.received.to_sat(), + sent_sats: tx.sent.to_sat(), + confirmation_info: match tx.chain_position { + ChainPosition::Confirmed(ConfirmationBlockTime { + block_id, + confirmation_time, + }) => Some(Confirmation { + height: block_id.height, + block_hash: Some(ReverseHex::encode(&block_id.hash)), + timestamp: Some(Timestamp { + seconds: confirmation_time as i64, + nanos: 0, + }), + }), + ChainPosition::Unconfirmed(last_seen) => Some(Confirmation { + height: 0, + block_hash: None, + timestamp: Some(Timestamp { + seconds: last_seen as i64, + nanos: 0, + }), + }), + }, + }) + .collect(), + }; + Ok(tonic::Response::new(response)) + } } #[derive(Debug, Error)] diff --git a/src/types.rs b/src/types.rs index 8a5e639..a43857b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,5 +1,6 @@ -use std::num::TryFromIntError; +use std::{num::TryFromIntError, sync::Arc}; +use bdk_wallet::chain::{ChainPosition, ConfirmationBlockTime}; use bitcoin::{ hashes::{sha256d, Hash as _}, Amount, BlockHash, OutPoint, Txid, Work, @@ -321,6 +322,16 @@ pub enum Event { }, } +#[derive(Debug)] +pub struct BDKWalletTransaction { + pub txid: bitcoin::Txid, + pub chain_position: ChainPosition, + pub transaction: Arc, + pub fee: Amount, + pub received: Amount, + pub sent: Amount, +} + #[cfg(test)] mod tests { use miette::Diagnostic as _; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 1f394ea..17ad268 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -50,7 +50,8 @@ use crate::{ cli::WalletConfig, convert, messages::{self, CoinbaseBuilder, M8_BMM_REQUEST_TAG}, - types::{Ctip, SidechainAck, SidechainNumber, SidechainProposal}, + proto::mainchain::WalletTransaction, + types::{BDKWalletTransaction, Ctip, SidechainAck, SidechainNumber, SidechainProposal}, validator::Validator, }; @@ -980,6 +981,76 @@ impl Wallet { Ok(balance) } + pub async fn list_wallet_transactions(&self) -> Result> { + let wallet = self.bitcoin_wallet.lock(); + let transactions = wallet.transactions(); + let mut txs = Vec::new(); + + for tx in transactions { + // Calculate total input value + let mut input_value = Amount::ZERO; + for input in tx.tx_node.tx.input.iter() { + let transaction_hex = self + .main_client + .get_raw_transaction( + input.previous_output.txid, + GetRawTransactionVerbose::, + None, + ) + .await + .map_err(|err| error::BitcoinCoreRPC { + method: "getrawtransaction".to_string(), + error: err, + })?; + + let prev_output = + bitcoin::consensus::encode::deserialize_hex::(&transaction_hex) + .into_diagnostic()?; + + input_value += prev_output.output[input.previous_output.vout as usize].value; + } + + // Calculate total output value + let output_value = tx.tx_node.tx.output.iter().map(|o| o.value).sum(); + + // Fee is difference between inputs and outputs + let fee = input_value - output_value; + + // Calculate received/sent by checking if outputs/inputs belong to wallet + let mut received = Amount::ZERO; + let mut sent = Amount::ZERO; + + // Check outputs to wallet + for output in tx.tx_node.tx.output.iter() { + if wallet.is_mine(output.script_pubkey.clone()) { + received += output.value; + } + } + + // Check inputs from wallet + for input in tx.tx_node.tx.input.iter() { + if let Some(prev_output) = wallet.get_tx(input.previous_output.txid) { + let prev_txout = + &prev_output.tx_node.tx.output[input.previous_output.vout as usize]; + if wallet.is_mine(prev_txout.script_pubkey.clone()) { + sent += prev_txout.value; + } + } + } + + txs.push(BDKWalletTransaction { + txid: tx.tx_node.txid, + chain_position: tx.chain_position.cloned(), + transaction: tx.tx_node.tx.clone(), + fee, + received, + sent, + }); + } + + Ok(txs) + } + pub fn sync(&self) -> Result<()> { let start = SystemTime::now(); tracing::trace!("starting wallet sync");