Skip to content

Commit

Permalink
rpc_api: Add fund_raw_transaction.
Browse files Browse the repository at this point in the history
  • Loading branch information
ceyhunsen committed Aug 22, 2024
1 parent 3c4abfb commit c7dd696
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 9 deletions.
2 changes: 1 addition & 1 deletion src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ impl RpcApiWrapper for bitcoincore_rpc::Client {
}

/// Mock Bitcoin RPC client.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Client {
/// Bitcoin ledger.
ledger: Ledger,
Expand Down
117 changes: 114 additions & 3 deletions src/client/rpc_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
//! `Client`.
use super::Client;
use crate::ledger::Ledger;
use crate::{
ledger::{errors::LedgerError, Ledger},
utils::encode_to_hex,
};
use bitcoin::{
address::NetworkChecked,
consensus::{encode, Encodable},
Expand Down Expand Up @@ -353,12 +356,82 @@ impl RpcApi for Client {
fn get_block_count(&self) -> bitcoincore_rpc::Result<u64> {
Ok(self.ledger.get_block_height()?.into())
}

fn fund_raw_transaction<R: bitcoincore_rpc::RawTx>(
&self,
tx: R,
_options: Option<&json::FundRawTransactionOptions>,
_is_witness: Option<bool>,
) -> bitcoincore_rpc::Result<json::FundRawTransactionResult> {
let mut transaction: Transaction = encode::deserialize_hex(&tx.raw_hex())?;
tracing::debug!("Decoded input transaction: {transaction:?}");

let mut hex: Vec<u8> = Vec::new();
let tx = encode_to_hex(&transaction);
tx.consensus_encode(&mut hex).unwrap();

let diff = match self.ledger.check_transaction_funds(&transaction) {
// If input amount is sufficient, no need to modify anything.
Ok(()) => {
return Ok(json::FundRawTransactionResult {
hex,
fee: Amount::from_sat(0),
change_position: -1,
})
}
// Input funds are lower than the output funds, use the difference.
Err(LedgerError::InputFundsNotEnough(diff)) => diff,
// Other ledger errors.
Err(e) => return Err(e.into()),
};

tracing::debug!(
"Input funds are {diff} sats lower than the output sats, adding new input."
);

// Generate a new txout.
let address = self.get_new_address(None, None)?.assume_checked();
let txid = self.send_to_address(
&address,
Amount::from_sat(diff * diff),
None,
None,
None,
None,
None,
None,
)?;

let txin = self.ledger.create_txin(txid, 0);

transaction.input.insert(0, txin);
tracing::debug!("New transaction: {transaction:?}");

let tx = encode_to_hex(&transaction);
let mut hex: Vec<u8> = Vec::new();
tx.consensus_encode(&mut hex).unwrap();

Ok(json::FundRawTransactionResult {
hex,
fee: Amount::from_sat(0),
change_position: 0,
})
}

fn sign_raw_transaction_with_wallet<R: bitcoincore_rpc::RawTx>(
&self,
_tx: R,
_utxos: Option<&[json::SignRawTransactionInput]>,
_sighash_type: Option<json::SigHashType>,
) -> bitcoincore_rpc::Result<json::SignRawTransactionResult> {
todo!()
}
}

#[cfg(test)]
mod tests {
use crate::{ledger::Ledger, Client, RpcApiWrapper};
use bitcoin::{Amount, Network, OutPoint, TxIn};
use crate::{ledger::Ledger, utils::decode_from_hex, Client, RpcApiWrapper};
use bitcoin::{consensus::Decodable, Amount, Network, OutPoint, Transaction, TxIn};
use bitcoincore_rpc::RpcApi;

#[test]
Expand Down Expand Up @@ -645,4 +718,42 @@ mod tests {

assert_eq!(rpc.get_block_count().unwrap(), 1);
}

#[test]
fn fund_raw_transaction() {
let rpc = Client::new("fund_raw_transaction", bitcoincore_rpc::Auth::None).unwrap();

let address = Ledger::generate_credential_from_witness().address;
let txid = rpc
.send_to_address(
&address,
Amount::from_sat(0x1F),
None,
None,
None,
None,
None,
None,
)
.unwrap();
let txin = rpc.ledger.create_txin(txid, 0);
let txout = rpc
.ledger
.create_txout(Amount::from_sat(0x45), address.script_pubkey());
let og_tx = rpc.ledger.create_transaction(vec![txin], vec![txout]);

let res = rpc.fund_raw_transaction(&og_tx, None, None).unwrap();
let tx = String::consensus_decode(&mut res.hex.as_slice()).unwrap();
let tx = decode_from_hex::<Transaction>(tx).unwrap();

assert_ne!(og_tx, tx);
assert_eq!(res.change_position, 0);

let res = rpc.fund_raw_transaction(&tx, None, None).unwrap();
let new_tx = String::consensus_decode(&mut res.hex.as_slice()).unwrap();
let new_tx = decode_from_hex::<Transaction>(new_tx).unwrap();

assert_eq!(tx, new_tx);
assert_eq!(res.change_position, -1);
}
}
2 changes: 2 additions & 0 deletions src/ledger/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use thiserror::Error;
pub enum LedgerError {
#[error("Transaction error: {0}")]
Transaction(String),
#[error("Transaction's input funds are {0} sats lower than the output funds")]
InputFundsNotEnough(u64),
#[error("UTXO error: {0}")]
Utxo(String),
#[error("SpendingRequirements error: {0}")]
Expand Down
7 changes: 3 additions & 4 deletions src/ledger/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,9 @@ impl Ledger {
let output_value = self.calculate_transaction_output_value(transaction);

if input_value < output_value {
Err(LedgerError::Transaction(format!(
"Input amount is smaller than output amount: {} < {}",
input_value, output_value
)))
Err(LedgerError::InputFundsNotEnough(
output_value.to_sat() - input_value.to_sat(),
))
} else {
Ok(())
}
Expand Down
29 changes: 28 additions & 1 deletion src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
use crate::ledger::errors::LedgerError;
use bitcoin::{
consensus::Encodable,
consensus::{
encode::{deserialize_hex, serialize_hex},
Encodable,
},
hashes::{sha256, Hash},
TxMerkleNode,
};
Expand Down Expand Up @@ -116,6 +119,30 @@ pub fn hex_to_array(hex: &str, output: &mut [u8]) {
});
}

/// Encodes given Rust struct to hex string.
pub fn encode_to_hex<T>(strct: &T) -> String
where
T: bitcoin::consensus::Encodable,
{
serialize_hex::<T>(strct)
}

/// Decodes given hex string to a Rust struct.
pub fn decode_from_hex<T>(hex: String) -> Result<T, bitcoincore_rpc::Error>
where
T: bitcoin::consensus::Decodable,
{
Ok(deserialize_hex::<T>(&hex)?)
}

pub fn encode_decode_to_rpc_error(
error: bitcoin::consensus::encode::Error,
) -> bitcoincore_rpc::Error {
bitcoincore_rpc::Error::BitcoinSerialization(bitcoin::consensus::encode::FromHexError::Decode(
bitcoin::consensus::DecodeError::Consensus(error),
))
}

/// Initializes `tracing` as the logger.
///
/// # Returns
Expand Down

0 comments on commit c7dd696

Please sign in to comment.