use bitcoin::{Amount, FeeRate, Txid};
use serde::{
de::{self, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct CreateRawTransactionArguments {
pub inputs: Vec<CreateRawTransactionInput>,
pub outputs: Vec<CreateRawTransactionOutput>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct CreateRawTransactionInput {
pub txid: String,
pub vout: u32,
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum CreateRawTransactionOutput {
AddressAmount {
address: String,
amount: f64,
},
Data {
data: String,
},
}
impl Serialize for CreateRawTransactionOutput {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
CreateRawTransactionOutput::AddressAmount { address, amount } => {
let mut map = serde_json::Map::new();
map.insert(
address.clone(),
serde_json::Value::Number(serde_json::Number::from_f64(*amount).unwrap()),
);
map.serialize(serializer)
}
CreateRawTransactionOutput::Data { data } => {
let mut map = serde_json::Map::new();
map.insert("data".to_string(), serde_json::Value::String(data.clone()));
map.serialize(serializer)
}
}
}
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct PreviousTransactionOutput {
#[serde(deserialize_with = "deserialize_txid")]
pub txid: Txid,
pub vout: u32,
#[serde(rename = "scriptPubKey")]
pub script_pubkey: String,
#[serde(rename = "redeemScript")]
pub redeem_script: Option<String>,
#[serde(rename = "witnessScript")]
pub witness_script: Option<String>,
pub amount: Option<f64>,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct ImportDescriptorInput {
pub desc: String,
pub active: Option<bool>,
pub timestamp: String,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct CreateWalletArguments {
pub name: String,
pub load_on_startup: Option<bool>,
}
fn serialize_option_bitcoin<S>(amount: &Option<Amount>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match amount {
Some(amt) => serializer.serialize_some(&amt.to_btc()),
None => serializer.serialize_none(),
}
}
fn serialize_option_fee_rate<S>(
fee_rate: &Option<FeeRate>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match fee_rate {
Some(fr) => serializer.serialize_some(&(fr.to_sat_per_kwu() as f64 / 250.0)),
None => serializer.serialize_none(),
}
}
fn deserialize_txid<'d, D>(deserializer: D) -> Result<Txid, D::Error>
where
D: Deserializer<'d>,
{
struct TxidVisitor;
impl Visitor<'_> for TxidVisitor {
type Value = Txid;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a transaction id string expected")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let txid = v.parse::<Txid>().expect("invalid txid");
Ok(txid)
}
}
deserializer.deserialize_any(TxidVisitor)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum SighashType {
Default,
All,
None,
Single,
#[serde(rename = "ALL|ANYONECANPAY")]
AllPlusAnyoneCanPay,
#[serde(rename = "NONE|ANYONECANPAY")]
NonePlusAnyoneCanPay,
#[serde(rename = "SINGLE|ANYONECANPAY")]
SinglePlusAnyoneCanPay,
}
#[derive(Clone, Debug, PartialEq, Serialize, Default)]
pub struct WalletCreateFundedPsbtOptions {
#[serde(
default,
rename = "fee_rate",
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_option_fee_rate"
)]
pub fee_rate: Option<FeeRate>,
#[serde(
default,
rename = "lockUnspents",
skip_serializing_if = "Option::is_none"
)]
pub lock_unspents: Option<bool>,
#[serde(
default,
rename = "conf_target",
skip_serializing_if = "Option::is_none"
)]
pub conf_target: Option<u16>,
#[serde(
default,
rename = "replaceable",
skip_serializing_if = "Option::is_none"
)]
pub replaceable: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ListUnspentQueryOptions {
#[serde(serialize_with = "serialize_option_bitcoin")]
pub minimum_amount: Option<Amount>,
#[serde(serialize_with = "serialize_option_bitcoin")]
pub maximum_amount: Option<Amount>,
pub maximum_count: Option<u32>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct PsbtBumpFeeOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub conf_target: Option<u16>,
#[serde(
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_option_fee_rate"
)]
pub fee_rate: Option<FeeRate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub replaceable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub estimate_mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub outputs: Option<Vec<CreateRawTransactionOutput>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub original_change_index: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use serde_json::{json, Value};
#[derive(Serialize)]
struct FeeRateOption {
#[serde(serialize_with = "serialize_option_fee_rate")]
fee_rate: Option<FeeRate>,
}
const MAX_REASONABLE_FEE_RATE_SAT_PER_KWU: u64 = 1_000_000_000;
fn serialized_fee_rate_sat_per_kwu(value: &Value) -> u64 {
let fee_rate = value["fee_rate"].as_f64().unwrap();
(fee_rate * 250.0).round() as u64
}
proptest! {
#[test]
fn serialize_option_fee_rate_roundtrips_sat_per_kwu(
sat_per_kwu in 0u64..=MAX_REASONABLE_FEE_RATE_SAT_PER_KWU,
) {
let value = serde_json::to_value(FeeRateOption {
fee_rate: Some(FeeRate::from_sat_per_kwu(sat_per_kwu)),
})
.unwrap();
prop_assert_eq!(serialized_fee_rate_sat_per_kwu(&value), sat_per_kwu);
}
#[test]
fn wallet_create_funded_psbt_options_roundtrips_fee_rate(
sat_per_kwu in 0u64..=MAX_REASONABLE_FEE_RATE_SAT_PER_KWU,
) {
let value = serde_json::to_value(WalletCreateFundedPsbtOptions {
fee_rate: Some(FeeRate::from_sat_per_kwu(sat_per_kwu)),
..Default::default()
})
.unwrap();
prop_assert_eq!(serialized_fee_rate_sat_per_kwu(&value), sat_per_kwu);
}
#[test]
fn psbt_bump_fee_options_roundtrips_fee_rate(
sat_per_kwu in 0u64..=MAX_REASONABLE_FEE_RATE_SAT_PER_KWU,
) {
let value = serde_json::to_value(PsbtBumpFeeOptions {
fee_rate: Some(FeeRate::from_sat_per_kwu(sat_per_kwu)),
..Default::default()
})
.unwrap();
prop_assert_eq!(serialized_fee_rate_sat_per_kwu(&value), sat_per_kwu);
}
}
#[test]
fn serialize_option_fee_rate_preserves_sub_sat_per_vb_example() {
let value = serde_json::to_value(FeeRateOption {
fee_rate: Some(FeeRate::from_sat_per_kwu(125)),
})
.unwrap();
assert_eq!(value, json!({ "fee_rate": 0.5 }));
}
#[test]
fn wallet_create_funded_psbt_options_serializes_fee_rate_as_sat_per_vb_example() {
let value = serde_json::to_value(WalletCreateFundedPsbtOptions {
fee_rate: Some(FeeRate::from_sat_per_kwu(375)),
..Default::default()
})
.unwrap();
assert_eq!(value, json!({ "fee_rate": 1.5 }));
}
#[test]
fn wallet_create_funded_psbt_options_skips_missing_fee_rate() {
let value = serde_json::to_value(WalletCreateFundedPsbtOptions {
lock_unspents: Some(true),
..Default::default()
})
.unwrap();
assert_eq!(value, json!({ "lockUnspents": true }));
}
#[test]
fn psbt_bump_fee_options_serializes_fee_rate_as_sat_per_vb_example() {
let value = serde_json::to_value(PsbtBumpFeeOptions {
fee_rate: Some(FeeRate::from_sat_per_vb(20).unwrap()),
..Default::default()
})
.unwrap();
assert_eq!(value, json!({ "fee_rate": 20.0 }));
}
#[test]
fn serialize_option_fee_rate_serializes_none_as_null_without_skip() {
let value = serde_json::to_value(FeeRateOption { fee_rate: None }).unwrap();
assert_eq!(value, json!({ "fee_rate": Value::Null }));
}
}