From 5df1f52b8d3a27e0d90e4f1ab78cc32de9d4efd3 Mon Sep 17 00:00:00 2001 From: TheMhv Date: Fri, 6 Mar 2026 00:29:42 -0300 Subject: [PATCH 1/4] feat(btcfee): add BTCFEE witness --- crates/cashu/src/nuts/nutxx/error.rs | 16 +++++++++++++ crates/cashu/src/nuts/nutxx/mod.rs | 11 +++++++++ crates/cashu/src/nuts/nutxx/witness.rs | 32 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 crates/cashu/src/nuts/nutxx/error.rs create mode 100644 crates/cashu/src/nuts/nutxx/mod.rs create mode 100644 crates/cashu/src/nuts/nutxx/witness.rs diff --git a/crates/cashu/src/nuts/nutxx/error.rs b/crates/cashu/src/nuts/nutxx/error.rs new file mode 100644 index 000000000..6d59f81f3 --- /dev/null +++ b/crates/cashu/src/nuts/nutxx/error.rs @@ -0,0 +1,16 @@ +use thiserror::Error; + +/// BTCFEE Errors +#[derive(Debug, Error)] +pub enum Error { + // Witness Errors + /// Invalid BTCFEE Witness + #[error("Invalid BTCFEE witness data")] + InvalidWitness, + /// Invalid BTCFEE Witness Blockhash + #[error("Invalid BTCFEE witness blockhash data")] + InvalidWitnessBlockhash, + /// Invalid BTCFEE Witness Preimage + #[error("Invalid BTCFEE witness preimage data")] + InvalidWitnessPreimage, +} diff --git a/crates/cashu/src/nuts/nutxx/mod.rs b/crates/cashu/src/nuts/nutxx/mod.rs new file mode 100644 index 000000000..4c0a56620 --- /dev/null +++ b/crates/cashu/src/nuts/nutxx/mod.rs @@ -0,0 +1,11 @@ +//! NUT-XX: Bitcoin External Fee +//! +//! + +/// BTCFEE Error module +pub mod error; +pub use error::Error; + +/// BTCFEE Witness module +pub mod witness; +pub use witness::Witness; diff --git a/crates/cashu/src/nuts/nutxx/witness.rs b/crates/cashu/src/nuts/nutxx/witness.rs new file mode 100644 index 000000000..6e2689def --- /dev/null +++ b/crates/cashu/src/nuts/nutxx/witness.rs @@ -0,0 +1,32 @@ +use bitcoin::{consensus::encode::deserialize_hex, BlockHash}; +use serde::{Deserialize, Serialize}; + +use crate::{nutxx::Error, util::hex}; + +/// BTCFEE Witness +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] +pub struct Witness { + /// Blockhash + pub blockhash: String, + /// Preimage + pub preimage: String, +} + +impl Witness { + /// Decode the blockhash from hex + /// + /// Returns the blockhash + pub fn blockhash_data(&self) -> Result { + let blockhash: BlockHash = + deserialize_hex(&self.blockhash).map_err(|_| Error::InvalidWitnessBlockhash)?; + Ok(blockhash) + } + + /// Decode the preimage from hex + /// + /// Returns the preimage bytes data + pub fn preimage_data(&self) -> Result, Error> { + hex::decode(&self.preimage).map_err(|_| Error::InvalidWitnessPreimage) + } +} From b04c938f6d7668265b4316a6f6f05458fb8f3b69 Mon Sep 17 00:00:00 2001 From: TheMhv Date: Fri, 6 Mar 2026 01:08:15 -0300 Subject: [PATCH 2/4] feat(btcfee): add BTCFEE spending conditions --- crates/cashu/src/nuts/nutxx/mod.rs | 4 +++ .../src/nuts/nutxx/spending_conditions.rs | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 crates/cashu/src/nuts/nutxx/spending_conditions.rs diff --git a/crates/cashu/src/nuts/nutxx/mod.rs b/crates/cashu/src/nuts/nutxx/mod.rs index 4c0a56620..a215d43e2 100644 --- a/crates/cashu/src/nuts/nutxx/mod.rs +++ b/crates/cashu/src/nuts/nutxx/mod.rs @@ -9,3 +9,7 @@ pub use error::Error; /// BTCFEE Witness module pub mod witness; pub use witness::Witness; + +/// BTCFEE Spending Conditions module +pub mod spending_conditions; +pub use spending_conditions::SpendingConditions; diff --git a/crates/cashu/src/nuts/nutxx/spending_conditions.rs b/crates/cashu/src/nuts/nutxx/spending_conditions.rs new file mode 100644 index 000000000..8ce30a573 --- /dev/null +++ b/crates/cashu/src/nuts/nutxx/spending_conditions.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +use crate::Conditions; + +/// BTCFEE SpendingConditions +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct SpendingConditions { + /// Other spending conditions + #[serde(skip_serializing_if = "Option::is_none")] + pub conditions: Option, + /// Number of block confirmations required + /// + /// Default is 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub confirmations: Option, +} + +impl SpendingConditions { + /// Create a BTCFEE spending conditions + pub fn new(conditions: Option, confirmations: Option) -> Self { + Self { + conditions, + confirmations, + } + } +} From 5ff4aed2a1935876dca5baff868d18b5eb28e5c6 Mon Sep 17 00:00:00 2001 From: TheMhv Date: Fri, 6 Mar 2026 02:29:15 -0300 Subject: [PATCH 3/4] feat(btcfee): parse BTCFEE Witness in NUT00 Witness --- crates/cashu/src/nuts/nut00/mod.rs | 7 +++++++ crates/cashu/src/nuts/nutxx/mod.rs | 2 ++ .../src/nuts/nutxx/serde_btcfee_witness.rs | 21 +++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 crates/cashu/src/nuts/nutxx/serde_btcfee_witness.rs diff --git a/crates/cashu/src/nuts/nut00/mod.rs b/crates/cashu/src/nuts/nut00/mod.rs index b7b354f64..319f17e73 100644 --- a/crates/cashu/src/nuts/nut00/mod.rs +++ b/crates/cashu/src/nuts/nut00/mod.rs @@ -30,6 +30,7 @@ use crate::nuts::nut01::SecretKey; use crate::nuts::nut11::{serde_p2pk_witness, P2PKWitness}; use crate::nuts::nut12::BlindSignatureDleq; use crate::nuts::nut14::{serde_htlc_witness, HTLCWitness}; +use crate::nuts::nutxx::{serde_btcfee_witness, Witness as BTCFEEWitness}; use crate::nuts::{Id, ProofDleq}; use crate::secret::Secret; use crate::Amount; @@ -287,6 +288,9 @@ pub enum Witness { /// P2PK Witness #[serde(with = "serde_p2pk_witness")] P2PKWitness(P2PKWitness), + /// BTCFEE Witness + #[serde(with = "serde_btcfee_witness")] + BTCFEEWitness(BTCFEEWitness), } impl From for Witness { @@ -310,6 +314,7 @@ impl Witness { Some(sigs) => sigs.extend(signatures), None => htlc_witness.signatures = Some(signatures), }, + Self::BTCFEEWitness(_btcfee_witness) => {} } } @@ -318,6 +323,7 @@ impl Witness { match self { Self::P2PKWitness(witness) => Some(witness.signatures.clone()), Self::HTLCWitness(witness) => witness.signatures.clone(), + Self::BTCFEEWitness(_witness) => None, } } @@ -326,6 +332,7 @@ impl Witness { match self { Self::P2PKWitness(_witness) => None, Self::HTLCWitness(witness) => Some(witness.preimage.clone()), + Self::BTCFEEWitness(witness) => Some(witness.preimage.clone()), } } } diff --git a/crates/cashu/src/nuts/nutxx/mod.rs b/crates/cashu/src/nuts/nutxx/mod.rs index a215d43e2..e2f4d496e 100644 --- a/crates/cashu/src/nuts/nutxx/mod.rs +++ b/crates/cashu/src/nuts/nutxx/mod.rs @@ -13,3 +13,5 @@ pub use witness::Witness; /// BTCFEE Spending Conditions module pub mod spending_conditions; pub use spending_conditions::SpendingConditions; + +pub mod serde_btcfee_witness; diff --git a/crates/cashu/src/nuts/nutxx/serde_btcfee_witness.rs b/crates/cashu/src/nuts/nutxx/serde_btcfee_witness.rs new file mode 100644 index 000000000..3b9d5042c --- /dev/null +++ b/crates/cashu/src/nuts/nutxx/serde_btcfee_witness.rs @@ -0,0 +1,21 @@ +//! Serde helpers for BTCFEE Witness + +use crate::nutxx::Witness; +use serde::{de, ser, Deserialize, Deserializer, Serializer}; + +/// Serialize [BTCFEEWitness] as stringified JSON +pub fn serialize(x: &Witness, s: S) -> Result +where + S: Serializer, +{ + s.serialize_str(&serde_json::to_string(&x).map_err(ser::Error::custom)?) +} + +/// Deserialize [BTCFEEWitness] from stringified JSON +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: String = String::deserialize(deserializer)?; + serde_json::from_str(&s).map_err(de::Error::custom) +} From d3ebf11044f5017f0f14176a3d1932c128e7200d Mon Sep 17 00:00:00 2001 From: TheMhv Date: Fri, 6 Mar 2026 11:14:24 -0300 Subject: [PATCH 4/4] feat(btcfee): create a util client for json rpc bitcoin server --- Cargo.lock | 26 ++++++++- crates/cashu/Cargo.toml | 2 + crates/cashu/src/util/bitcoin_rpc.rs | 85 ++++++++++++++++++++++++++++ crates/cashu/src/util/mod.rs | 1 + 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 crates/cashu/src/util/bitcoin_rpc.rs diff --git a/Cargo.lock b/Cargo.lock index 6739b6912..cca7f21da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -930,7 +930,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedd23ae0fd321affb4bbbc36126c6f49a32818dc6b979395d24da8c9d4e80ee" dependencies = [ "bitcoincore-rpc-json", - "jsonrpc", + "jsonrpc 0.18.0", "log", "serde", "serde_json", @@ -1115,6 +1115,8 @@ dependencies = [ "bitcoin 0.32.8", "cbor-diag", "ciborium", + "corepc-types", + "jsonrpc 0.19.0", "lightning 0.2.0", "lightning-invoice 0.34.0", "nostr-sdk", @@ -2102,6 +2104,17 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "corepc-types" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6ea6101b2da248ff9c7e0ead02c6e0b8243db140d86c6190e1b043c306d97a" +dependencies = [ + "bitcoin 0.32.8", + "serde", + "serde_json", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -3971,6 +3984,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonrpc" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629d2b4ae586d04b6bae3c75879d7ddd39325c2b67a9c87634f4ec88a488dc65" +dependencies = [ + "base64 0.22.1", + "serde", + "serde_json", +] + [[package]] name = "jsonwebtoken" version = "9.3.1" diff --git a/crates/cashu/Cargo.toml b/crates/cashu/Cargo.toml index cee520591..45bdcfbf7 100644 --- a/crates/cashu/Cargo.toml +++ b/crates/cashu/Cargo.toml @@ -38,6 +38,8 @@ strum_macros.workspace = true nostr-sdk = { workspace = true, optional = true } zeroize = "1" web-time.workspace = true +corepc-types = "0.11.0" +jsonrpc = "0.19.0" [target.'cfg(target_arch = "wasm32")'.dependencies] uuid = { workspace = true, features = ["js"], optional = true } diff --git a/crates/cashu/src/util/bitcoin_rpc.rs b/crates/cashu/src/util/bitcoin_rpc.rs new file mode 100644 index 000000000..89cc6b92c --- /dev/null +++ b/crates/cashu/src/util/bitcoin_rpc.rs @@ -0,0 +1,85 @@ +use bitcoin::{Block, BlockHash, Transaction, Txid}; +use corepc_types::model::{GetBlockVerboseZero, GetRawTransaction}; +use jsonrpc::{simple_http::SimpleHttpTransport, Client}; +use serde_json::json; +use strum_macros::Display; +use thiserror::Error; + +/// Bitcoin RPC Client Errors +#[derive(Debug, Error, Display)] +pub enum Error { + /// Cannot connect with Bitcoin JSON RPC client + CannotConnect, + /// Invalid URL from JSON RPC + InvalidURL, + /// Unable to parse params + InvalidParams, + /// Unable to send request + RequestFailed, + /// Unable to get response + ResponseFailed, +} + +/// BitcoinRPC +#[derive(Debug)] +pub struct BitcoinRPC { + client: Client, +} + +impl BitcoinRPC { + /// Create a BitcoinRPC with Client + pub fn new(url: &str, user: Option, pass: Option) -> Result { + let transport = SimpleHttpTransport::builder() + .url(url) + .map_err(|_| Error::InvalidURL)? + .auth(user.unwrap_or("".into()), pass) + .build(); + + Ok(Self { + client: Client::with_transport(transport), + }) + } + + /// Send a JSON RPC call + fn call_rpc( + &self, + method: &str, + params: Option<&serde_json::value::RawValue>, + ) -> Result + where + T: serde::de::DeserializeOwned, + { + let request = self.client.build_request(method, params); + let response = self + .client + .send_request(request) + .map_err(|_| Error::ResponseFailed)?; + + Ok(response.result().map_err(|_| Error::ResponseFailed)?) + } + + /// Send `getblock` method to JSON RPC + /// + /// Return the Block + pub fn get_block(&self, blockhash: BlockHash) -> Result { + let params = serde_json::value::to_raw_value(&json!([blockhash.to_string(), 0])) + .map_err(|_| Error::InvalidParams)?; + + let response: GetBlockVerboseZero = self.call_rpc("getblock", Some(¶ms))?; + + Ok(response.0) + } + + /// Send `getrawtransaction` method to JSON RPC + /// + /// Return the Transaction + pub fn get_raw_transaction(&self, txid: &Txid) -> Result { + let params = serde_json::value::to_raw_value(&json!([txid.to_string(), false])) + .map_err(|_| Error::InvalidParams)? + .clone(); + + let response: GetRawTransaction = self.call_rpc("getrawtransaction", Some(¶ms))?; + + Ok(response.0) + } +} diff --git a/crates/cashu/src/util/mod.rs b/crates/cashu/src/util/mod.rs index a1779b4a0..4ee789c72 100644 --- a/crates/cashu/src/util/mod.rs +++ b/crates/cashu/src/util/mod.rs @@ -1,5 +1,6 @@ //! Cashu utils +pub mod bitcoin_rpc; pub mod hex; pub mod serde_helpers;