-use crate::{BlockHeaderData, BlockSourceError};
use crate::http::{BinaryResponse, JsonResponse};
use crate::utils::hex_to_uint256;
+use crate::{BlockHeaderData, BlockSourceError};
use bitcoin::blockdata::block::{Block, BlockHeader};
use bitcoin::consensus::encode;
use bitcoin::hash_types::{BlockHash, TxMerkleNode, Txid};
-use bitcoin::hashes::hex::{ToHex, FromHex};
+use bitcoin::hashes::hex::{FromHex, ToHex};
+use bitcoin::Transaction;
use serde::Deserialize;
}
}
-
/// Converts a JSON value into a block. Assumes the block is hex-encoded in a JSON string.
impl TryInto<Block> for JsonResponse {
type Error = std::io::Error;
}
}
+/// Converts a JSON value into a transaction. WATCH OUT! this cannot be used for zero-input transactions
+/// (e.g. createrawtransaction). See https://github.com/rust-bitcoin/rust-bitcoincore-rpc/issues/197
+impl TryInto<Transaction> for JsonResponse {
+ type Error = std::io::Error;
+ fn try_into(self) -> std::io::Result<Transaction> {
+ let hex_tx = if self.0.is_object() {
+ // result is json encoded
+ match &self.0["hex"] {
+ // result has hex field
+ serde_json::Value::String(hex_data) => match self.0["complete"] {
+ // result may or may not be signed (e.g. signrawtransactionwithwallet)
+ serde_json::Value::Bool(x) => {
+ if x == false {
+ let reason = match &self.0["errors"][0]["error"] {
+ serde_json::Value::String(x) => x.as_str(),
+ _ => "Unknown error",
+ };
+
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ format!("transaction couldn't be signed. {}", reason),
+ ));
+ } else {
+ hex_data
+ }
+ }
+ // result is a complete transaction (e.g. getrawtranaction verbose)
+ _ => hex_data,
+ },
+ _ => return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ "expected JSON string",
+ )),
+ }
+ } else {
+ // result is plain text (e.g. getrawtransaction no verbose)
+ match self.0.as_str() {
+ Some(hex_tx) => hex_tx,
+ None => {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ "expected JSON string",
+ ))
+ }
+ }
+ };
+
+ match Vec::<u8>::from_hex(hex_tx) {
+ Err(_) => Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ "invalid hex data",
+ )),
+ Ok(tx_data) => match encode::deserialize(&tx_data) {
+ Err(_) => Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ "invalid transaction",
+ )),
+ Ok(tx) => Ok(tx),
+ },
+ }
+ }
+}
+
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use bitcoin::blockdata::constants::genesis_block;
- use bitcoin::consensus::encode;
use bitcoin::hashes::Hash;
use bitcoin::network::constants::Network;
+ use serde_json::value::Number;
+ use serde_json::Value;
/// Converts from `BlockHeaderData` into a `GetHeaderResponse` JSON value.
impl From<BlockHeaderData> for serde_json::Value {
Ok(txid) => assert_eq!(txid, target_txid),
}
}
+
+ // TryInto<Transaction> can be used in two ways, first with plain hex response where data is
+ // the hex encoded transaction (e.g. as a result of getrawtransaction) or as a JSON object
+ // where the hex encoded transaction can be found in the hex field of the object (if present)
+ // (e.g. as a result of signrawtransactionwithwallet).
+
+ // plain hex transaction
+
+ #[test]
+ fn into_tx_from_json_response_with_invalid_hex_data() {
+ let response = JsonResponse(serde_json::json!("foobar"));
+ match TryInto::<Transaction>::try_into(response) {
+ Err(e) => {
+ assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
+ assert_eq!(e.get_ref().unwrap().to_string(), "invalid hex data");
+ }
+ Ok(_) => panic!("Expected error"),
+ }
+ }
+
+ #[test]
+ fn into_tx_from_json_response_with_invalid_data_type() {
+ let response = JsonResponse(Value::Number(Number::from_f64(1.0).unwrap()));
+ match TryInto::<Transaction>::try_into(response) {
+ Err(e) => {
+ assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
+ assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON string");
+ }
+ Ok(_) => panic!("Expected error"),
+ }
+ }
+
+ #[test]
+ fn into_tx_from_json_response_with_invalid_tx_data() {
+ let response = JsonResponse(serde_json::json!("abcd"));
+ match TryInto::<Transaction>::try_into(response) {
+ Err(e) => {
+ assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
+ assert_eq!(e.get_ref().unwrap().to_string(), "invalid transaction");
+ }
+ Ok(_) => panic!("Expected error"),
+ }
+ }
+
+ #[test]
+ fn into_tx_from_json_response_with_valid_tx_data_plain() {
+ let genesis_block = genesis_block(Network::Bitcoin);
+ let target_tx = genesis_block.txdata.get(0).unwrap();
+ let response = JsonResponse(serde_json::json!(encode::serialize_hex(&target_tx)));
+ match TryInto::<Transaction>::try_into(response) {
+ Err(e) => panic!("Unexpected error: {:?}", e),
+ Ok(tx) => assert_eq!(&tx, target_tx),
+ }
+ }
+
+ #[test]
+ fn into_tx_from_json_response_with_valid_tx_data_hex_field() {
+ let genesis_block = genesis_block(Network::Bitcoin);
+ let target_tx = genesis_block.txdata.get(0).unwrap();
+ let response = JsonResponse(serde_json::json!({"hex": encode::serialize_hex(&target_tx)}));
+ match TryInto::<Transaction>::try_into(response) {
+ Err(e) => panic!("Unexpected error: {:?}", e),
+ Ok(tx) => assert_eq!(&tx, target_tx),
+ }
+ }
+
+ // transaction in hex field of JSON object
+
+ #[test]
+ fn into_tx_from_json_response_with_no_hex_field() {
+ let response = JsonResponse(serde_json::json!({ "error": "foo" }));
+ match TryInto::<Transaction>::try_into(response) {
+ Err(e) => {
+ assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
+ assert_eq!(
+ e.get_ref().unwrap().to_string(),
+ "expected JSON string"
+ );
+ }
+ Ok(_) => panic!("Expected error"),
+ }
+ }
+
+ #[test]
+ fn into_tx_from_json_response_not_signed() {
+ let response = JsonResponse(serde_json::json!({ "hex": "foo", "complete": false }));
+ match TryInto::<Transaction>::try_into(response) {
+ Err(e) => {
+ assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
+ assert!(
+ e.get_ref().unwrap().to_string().contains(
+ "transaction couldn't be signed")
+ );
+ }
+ Ok(_) => panic!("Expected error"),
+ }
+ }
}
}
impl Route {
- /// Returns the total amount of fees paid on this Route.
- /// This doesn't include any extra payment made to the recipient,
- /// which can happen in excess of the amount passed to `get_route`'s `final_value_msat`.
+ /// Returns the total amount of fees paid on this [`Route`].
+ ///
+ /// This doesn't include any extra payment made to the recipient, which can happen in excess of
+ /// the amount passed to [`get_route`]'s `final_value_msat`.
pub fn get_total_fees(&self) -> u64 {
// Do not count last hop of each path since that's the full value of the payment
return self.paths.iter()
- .flat_map(|path| path.split_last().unwrap().1)
+ .flat_map(|path| path.split_last().map(|(_, path_prefix)| path_prefix).unwrap_or(&[]))
.map(|hop| &hop.fee_msat)
.sum();
}
- /// Returns the total amount paid on this Route, excluding the fees.
+
+ /// Returns the total amount paid on this [`Route`], excluding the fees.
pub fn get_total_amount(&self) -> u64 {
return self.paths.iter()
- .map(|path| path.split_last().unwrap().0)
- .map(|hop| &hop.fee_msat)
+ .map(|path| path.split_last().map(|(hop, _)| hop.fee_msat).unwrap_or(0))
.sum();
}
}
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0 // Test vectors are garbage and not generateble from a RouteHop, we fill in payloads manually
+ short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0
},
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0 // Test vectors are garbage and not generateble from a RouteHop, we fill in payloads manually
+ short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0
},
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 225, cltv_expiry_delta: 0 // Test vectors are garbage and not generateble from a RouteHop, we fill in payloads manually
+ short_channel_id: 0, fee_msat: 225, cltv_expiry_delta: 0
},
]],
};
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0 // Test vectors are garbage and not generateble from a RouteHop, we fill in payloads manually
+ short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0
},
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0 // Test vectors are garbage and not generateble from a RouteHop, we fill in payloads manually
+ short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0
},
],vec![
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0 // Test vectors are garbage and not generateble from a RouteHop, we fill in payloads manually
+ short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0
},
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0 // Test vectors are garbage and not generateble from a RouteHop, we fill in payloads manually
+ short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0
},
]],
};
assert_eq!(route.get_total_amount(), 300);
}
+ #[test]
+ fn total_empty_route_no_panic() {
+ // In an earlier version of `Route::get_total_fees` and `Route::get_total_amount`, they
+ // would both panic if the route was completely empty. We test to ensure they return 0
+ // here, even though its somewhat nonsensical as a route.
+ let route = Route { paths: Vec::new() };
+
+ assert_eq!(route.get_total_fees(), 0);
+ assert_eq!(route.get_total_amount(), 0);
+ }
+
#[cfg(not(feature = "no-std"))]
pub(super) fn random_init_seed() -> u64 {
// Because the default HashMap in std pulls OS randomness, we can use it as a (bad) RNG.