From df57f14e45478938ff3898d4c7a7317b14c2b5d3 Mon Sep 17 00:00:00 2001 From: Noah Joeris Date: Thu, 23 Jul 2026 17:15:07 +0300 Subject: [PATCH 1/2] fix: preserve signed electrum unconfirmed mempool delta Electrum returns unconfirmed as a signed mempool delta; simply flipping the amount to positive can look like BTC received instead of BTC spent. --- src/custom_serde.rs | 8 -------- src/request.rs | 4 ++-- src/response.rs | 10 ++++++---- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/custom_serde.rs b/src/custom_serde.rs index 722fffb..be11634 100644 --- a/src/custom_serde.rs +++ b/src/custom_serde.rs @@ -88,14 +88,6 @@ where Ok(bitcoin::Amount::from_sat(sats)) } -pub fn amount_from_maybe_negative_sats<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let sats = i64::deserialize(deserializer)?.unsigned_abs(); - Ok(bitcoin::Amount::from_sat(sats)) -} - pub fn all_inputs_confirmed_bool_from_height<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/src/request.rs b/src/request.rs index 9973529..ef2b86c 100644 --- a/src/request.rs +++ b/src/request.rs @@ -286,8 +286,8 @@ impl Request for RelayFee { /// A request for the confirmed and unconfirmed balance of a specific script hash. /// /// This corresponds to the `"blockchain.scripthash.get_balance"` Electrum RPC method. It returns -/// both the confirmed balance (from mined transactions) and unconfirmed balance (from mempool -/// transactions) for the provided script hash. +/// the confirmed balance (from mined transactions) and the unconfirmed balance (from mempool transactions) for the +/// provided script hash. The unconfirmed value is signed and can be negative if confirmed outputs are spent. /// /// See: #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/src/response.rs b/src/response.rs index 7510ec4..64934a1 100644 --- a/src/response.rs +++ b/src/response.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use bitcoin::{ absolute, hashes::{Hash, HashEngine}, - Amount, BlockHash, + Amount, BlockHash, SignedAmount, }; use crate::DoubleSHA; @@ -116,9 +116,11 @@ pub struct GetBalanceResp { #[serde(deserialize_with = "crate::custom_serde::amount_from_sats")] pub confirmed: Amount, - /// The unconfirmed balance in satoshis (may be negative). - #[serde(deserialize_with = "crate::custom_serde::amount_from_maybe_negative_sats")] - pub unconfirmed: Amount, + /// The unconfirmed balance in satoshis. + /// + /// Can be negative when confirmed outputs are spent in the mempool. + #[serde(with = "bitcoin::amount::serde::as_sat")] + pub unconfirmed: SignedAmount, } #[derive(Debug, Clone, serde::Deserialize)] From 4c4704ed1ff6fc1053c8f69125b02059d36f5cd5 Mon Sep 17 00:00:00 2001 From: Noah Joeris Date: Thu, 23 Jul 2026 17:15:56 +0300 Subject: [PATCH 2/2] refactor: use bitcoin amount serde helpers for sats/btc --- src/custom_serde.rs | 16 ---------------- src/response.rs | 8 ++++---- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/custom_serde.rs b/src/custom_serde.rs index be11634..0fbddbc 100644 --- a/src/custom_serde.rs +++ b/src/custom_serde.rs @@ -72,22 +72,6 @@ where Ok(weight) } -pub fn amount_from_btc<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let btc = f64::deserialize(deserializer)?; - bitcoin::Amount::from_btc(btc).map_err(serde::de::Error::custom) -} - -pub fn amount_from_sats<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let sats = u64::deserialize(deserializer)?; - Ok(bitcoin::Amount::from_sat(sats)) -} - pub fn all_inputs_confirmed_bool_from_height<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/src/response.rs b/src/response.rs index 64934a1..ac228ae 100644 --- a/src/response.rs +++ b/src/response.rs @@ -105,7 +105,7 @@ pub struct HeadersSubscribeResp { #[serde(transparent)] pub struct RelayFeeResp { /// The minimum fee amount that the server will accept for relaying transactions. - #[serde(deserialize_with = "crate::custom_serde::amount_from_btc")] + #[serde(with = "bitcoin::amount::serde::as_btc")] pub fee: Amount, } @@ -113,7 +113,7 @@ pub struct RelayFeeResp { #[derive(Debug, Clone, serde::Deserialize)] pub struct GetBalanceResp { /// The confirmed balance in satoshis. - #[serde(deserialize_with = "crate::custom_serde::amount_from_sats")] + #[serde(with = "bitcoin::amount::serde::as_sat")] pub confirmed: Amount, /// The unconfirmed balance in satoshis. @@ -179,7 +179,7 @@ pub struct MempoolTx { pub txid: bitcoin::Txid, /// The fee paid by the transaction in satoshis. - #[serde(deserialize_with = "crate::custom_serde::amount_from_sats")] + #[serde(with = "bitcoin::amount::serde::as_sat")] pub fee: bitcoin::Amount, /// Whether all inputs are confirmed. @@ -204,7 +204,7 @@ pub struct Utxo { pub txid: bitcoin::Txid, /// The value of the UTXO in satoshis. - #[serde(deserialize_with = "crate::custom_serde::amount_from_sats")] + #[serde(with = "bitcoin::amount::serde::as_sat")] pub value: bitcoin::Amount, }