diff --git a/cli/Cargo.lock b/cli/Cargo.lock index dea6d5481..3c57d571c 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -603,6 +603,7 @@ dependencies = [ "serde_json", "strum", "strum_macros", + "tempfile", "thiserror", "tokio", "tokio-stream", @@ -2900,7 +2901,7 @@ dependencies = [ [[package]] name = "sdk-common" version = "0.5.2" -source = "git+https://github.com/breez/breez-sdk?branch=main#25cff346b7fa74298eef5ae04c361bd3b75d6ad5" +source = "git+https://github.com/breez/breez-sdk?rev=387e37f8ce48ee841762b945137e2c6b4b4b5cfa#387e37f8ce48ee841762b945137e2c6b4b4b5cfa" dependencies = [ "aes 0.8.4", "anyhow", diff --git a/lib/Cargo.lock b/lib/Cargo.lock index a86450b49..43b828ff5 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -704,6 +704,7 @@ dependencies = [ "strum", "strum_macros", "tempdir", + "tempfile", "thiserror", "tokio", "tokio-stream", @@ -3180,7 +3181,7 @@ dependencies = [ [[package]] name = "sdk-common" version = "0.5.2" -source = "git+https://github.com/breez/breez-sdk?branch=main#25cff346b7fa74298eef5ae04c361bd3b75d6ad5" +source = "git+https://github.com/breez/breez-sdk?rev=387e37f8ce48ee841762b945137e2c6b4b4b5cfa#387e37f8ce48ee841762b945137e2c6b4b4b5cfa" dependencies = [ "aes 0.8.4", "anyhow", diff --git a/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h b/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h index d3f160130..d02b1bd78 100644 --- a/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h +++ b/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h @@ -447,8 +447,8 @@ typedef struct wire_cst_config { } wire_cst_config; typedef struct wire_cst_connect_request { - struct wire_cst_list_prim_u_8_strict *mnemonic; struct wire_cst_config config; + struct wire_cst_list_prim_u_8_strict *mnemonic; } wire_cst_connect_request; typedef struct wire_cst_aes_success_action_data_decrypted { diff --git a/lib/bindings/langs/react-native/src/gen_kotlin/mod.rs b/lib/bindings/langs/react-native/src/gen_kotlin/mod.rs index 300044238..da006422a 100644 --- a/lib/bindings/langs/react-native/src/gen_kotlin/mod.rs +++ b/lib/bindings/langs/react-native/src/gen_kotlin/mod.rs @@ -10,7 +10,12 @@ pub use uniffi_bindgen::bindings::kotlin::gen_kotlin::*; use crate::generator::RNConfig; static IGNORED_FUNCTIONS: Lazy> = Lazy::new(|| { - let list: Vec<&str> = vec!["connect", "add_event_listener", "set_logger"]; + let list: Vec<&str> = vec![ + "connect", + "add_event_listener", + "set_logger", + "connect_with_signer", + ]; HashSet::from_iter(list.into_iter().map(|s| s.to_string())) }); diff --git a/lib/bindings/langs/react-native/src/gen_swift/mod.rs b/lib/bindings/langs/react-native/src/gen_swift/mod.rs index 7527c9266..cbe5495bc 100644 --- a/lib/bindings/langs/react-native/src/gen_swift/mod.rs +++ b/lib/bindings/langs/react-native/src/gen_swift/mod.rs @@ -9,7 +9,12 @@ use crate::generator::RNConfig; pub use uniffi_bindgen::bindings::swift::gen_swift::*; static IGNORED_FUNCTIONS: Lazy> = Lazy::new(|| { - let list: Vec<&str> = vec!["connect", "add_event_listener", "set_logger"]; + let list: Vec<&str> = vec![ + "connect", + "add_event_listener", + "set_logger", + "connect_with_signer", + ]; HashSet::from_iter(list.into_iter().map(|s| s.to_string())) }); diff --git a/lib/bindings/langs/react-native/src/gen_typescript/mod.rs b/lib/bindings/langs/react-native/src/gen_typescript/mod.rs index a8bb6912c..3e3b1bfaa 100644 --- a/lib/bindings/langs/react-native/src/gen_typescript/mod.rs +++ b/lib/bindings/langs/react-native/src/gen_typescript/mod.rs @@ -26,7 +26,12 @@ static KEYWORDS: Lazy> = Lazy::new(|| { }); static IGNORED_FUNCTIONS: Lazy> = Lazy::new(|| { - let list: Vec<&str> = vec!["connect", "add_event_listener", "set_logger"]; + let list: Vec<&str> = vec![ + "connect", + "add_event_listener", + "set_logger", + "connect_with_signer", + ]; HashSet::from_iter(list.into_iter().map(|s| s.to_string())) }); diff --git a/lib/bindings/src/breez_sdk_liquid.udl b/lib/bindings/src/breez_sdk_liquid.udl index 86c557bd4..290267558 100644 --- a/lib/bindings/src/breez_sdk_liquid.udl +++ b/lib/bindings/src/breez_sdk_liquid.udl @@ -320,10 +320,14 @@ enum LiquidNetwork { }; dictionary ConnectRequest { - Config config; + Config config; string mnemonic; }; +dictionary ConnectWithSignerRequest { + Config config; +}; + dictionary GetInfoResponse { u64 balance_sat; u64 pending_send_sat; @@ -601,6 +605,9 @@ namespace breez_sdk_liquid { [Throws=SdkError] BindingLiquidSdk connect(ConnectRequest req); + [Throws=SdkError] + BindingLiquidSdk connect_with_signer(ConnectWithSignerRequest req, Signer signer); + [Throws=SdkError] void set_logger(Logger logger); @@ -614,6 +621,31 @@ namespace breez_sdk_liquid { LNInvoice parse_invoice(string input); }; +[Error] +interface SignerError { + Generic(string err); +}; + +callback interface Signer { + [Throws=SignerError] + sequence xpub(); + + [Throws=SignerError] + sequence derive_xpub(string derivation_path); + + [Throws=SignerError] + sequence sign_ecdsa(sequence msg, string derivation_path); + + [Throws=SignerError] + sequence sign_ecdsa_recoverable(sequence msg); + + [Throws=SignerError] + sequence slip77_master_blinding_key(); + + [Throws=SignerError] + sequence hmac_sha256(sequence msg, string derivation_path); +}; + interface BindingLiquidSdk { [Throws=SdkError] string add_event_listener(EventListener listener); diff --git a/lib/bindings/src/lib.rs b/lib/bindings/src/lib.rs index 9dd5e0cdc..56539341e 100644 --- a/lib/bindings/src/lib.rs +++ b/lib/bindings/src/lib.rs @@ -54,6 +54,16 @@ pub fn connect(req: ConnectRequest) -> Result, SdkError> { }) } +pub fn connect_with_signer( + req: ConnectWithSignerRequest, + signer: Box, +) -> Result, SdkError> { + rt().block_on(async { + let sdk = LiquidSdk::connect_with_signer(req, signer).await?; + Ok(Arc::from(BindingLiquidSdk { sdk })) + }) +} + pub fn default_config( network: LiquidNetwork, breez_api_key: Option, diff --git a/lib/core/Cargo.toml b/lib/core/Cargo.toml index 641000eae..5fbce3d50 100644 --- a/lib/core/Cargo.toml +++ b/lib/core/Cargo.toml @@ -27,9 +27,7 @@ lwk_wollet = { git = "https://github.com/dangeross/lwk", branch = "savage-try-he #lwk_wollet = "0.7.0" rusqlite = { version = "0.31", features = ["backup", "bundled"] } rusqlite_migration = "1.0" -sdk-common = { git = "https://github.com/breez/breez-sdk", branch = "main", features = [ - "liquid", -] } +sdk-common = { git = "https://github.com/breez/breez-sdk", rev = "387e37f8ce48ee841762b945137e2c6b4b4b5cfa", features = ["liquid"]} serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.116" strum = "0.25" @@ -50,6 +48,7 @@ reqwest = { version = "=0.11.20", features = ["json"] } electrum-client = { version = "0.19.0" } zbase32 = "0.1.2" x509-parser = { version = "0.16.0" } +tempfile = "3" [dev-dependencies] lazy_static = "1.5.0" diff --git a/lib/core/src/frb_generated.rs b/lib/core/src/frb_generated.rs index 028b97c7b..54c3f65aa 100644 --- a/lib/core/src/frb_generated.rs +++ b/lib/core/src/frb_generated.rs @@ -2305,11 +2305,11 @@ impl SseDecode for crate::model::Config { impl SseDecode for crate::model::ConnectRequest { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_mnemonic = ::sse_decode(deserializer); let mut var_config = ::sse_decode(deserializer); + let mut var_mnemonic = ::sse_decode(deserializer); return crate::model::ConnectRequest { - mnemonic: var_mnemonic, config: var_config, + mnemonic: var_mnemonic, }; } } @@ -4334,8 +4334,8 @@ impl flutter_rust_bridge::IntoIntoDart for crate::model::C impl flutter_rust_bridge::IntoDart for crate::model::ConnectRequest { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ - self.mnemonic.into_into_dart().into_dart(), self.config.into_into_dart().into_dart(), + self.mnemonic.into_into_dart().into_dart(), ] .into_dart() } @@ -6298,8 +6298,8 @@ impl SseEncode for crate::model::Config { impl SseEncode for crate::model::ConnectRequest { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.mnemonic, serializer); ::sse_encode(self.config, serializer); + ::sse_encode(self.mnemonic, serializer); } } @@ -8225,8 +8225,8 @@ mod io { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> crate::model::ConnectRequest { crate::model::ConnectRequest { - mnemonic: self.mnemonic.cst_decode(), config: self.config.cst_decode(), + mnemonic: self.mnemonic.cst_decode(), } } } @@ -9586,8 +9586,8 @@ mod io { impl NewWithNullPtr for wire_cst_connect_request { fn new_with_null_ptr() -> Self { Self { - mnemonic: core::ptr::null_mut(), config: Default::default(), + mnemonic: core::ptr::null_mut(), } } } @@ -11475,8 +11475,8 @@ mod io { #[repr(C)] #[derive(Clone, Copy)] pub struct wire_cst_connect_request { - mnemonic: *mut wire_cst_list_prim_u_8_strict, config: wire_cst_config, + mnemonic: *mut wire_cst_list_prim_u_8_strict, } #[repr(C)] #[derive(Clone, Copy)] diff --git a/lib/core/src/lib.rs b/lib/core/src/lib.rs index baac70978..5190ad22b 100644 --- a/lib/core/src/lib.rs +++ b/lib/core/src/lib.rs @@ -171,6 +171,7 @@ pub mod error; pub(crate) mod event; #[cfg(feature = "frb")] pub(crate) mod frb_generated; +pub(crate) mod lnurl; pub mod logger; pub mod model; pub mod persist; @@ -179,6 +180,7 @@ pub(crate) mod receive_swap; mod restore; pub mod sdk; pub(crate) mod send_swap; +pub(crate) mod signer; pub(crate) mod swapper; pub(crate) mod test_utils; pub(crate) mod utils; @@ -192,4 +194,5 @@ pub mod prelude { pub use crate::*; pub use crate::model::*; pub use crate::sdk::*; + pub use crate::signer::SdkSigner; } diff --git a/lib/core/src/lnurl/auth.rs b/lib/core/src/lnurl/auth.rs new file mode 100644 index 000000000..bdf51b93a --- /dev/null +++ b/lib/core/src/lnurl/auth.rs @@ -0,0 +1,47 @@ +use std::sync::Arc; + +use sdk_common::{ + bitcoin::util::bip32::{ChildNumber, DerivationPath}, + prelude::{LnUrlResult, LnurlAuthSigner}, +}; + +use crate::model::Signer; + +pub(crate) struct SdkLnurlAuthSigner { + signer: Arc>, +} + +impl SdkLnurlAuthSigner { + pub fn new(signer: Arc>) -> Self { + Self { signer } + } +} + +impl LnurlAuthSigner for SdkLnurlAuthSigner { + fn derive_bip32_pub_key(&self, derivation_path: &[ChildNumber]) -> LnUrlResult> { + let derivation: DerivationPath = derivation_path.to_vec().into(); + self.signer + .derive_xpub(derivation.to_string()) + .map_err(|e| sdk_common::prelude::LnUrlError::Generic(e.to_string())) + .map(|xpub| xpub.to_vec()) + } + + fn sign_ecdsa(&self, msg: &[u8], derivation_path: &[ChildNumber]) -> LnUrlResult> { + let derivation: DerivationPath = derivation_path.to_vec().into(); + self.signer + .sign_ecdsa(msg.to_vec(), derivation.to_string()) + .map_err(|e| sdk_common::prelude::LnUrlError::Generic(e.to_string())) + .map(|s: Vec| s.to_vec()) + } + + fn hmac_sha256( + &self, + key_derivation_path: &[ChildNumber], + input: &[u8], + ) -> LnUrlResult> { + let derivation: DerivationPath = key_derivation_path.to_vec().into(); + self.signer + .hmac_sha256(input.to_vec(), derivation.to_string()) + .map_err(|e| sdk_common::prelude::LnUrlError::Generic(e.to_string())) + } +} diff --git a/lib/core/src/lnurl/mod.rs b/lib/core/src/lnurl/mod.rs new file mode 100644 index 000000000..0e4a05d59 --- /dev/null +++ b/lib/core/src/lnurl/mod.rs @@ -0,0 +1 @@ +pub mod auth; diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index a1c3a059b..a4a4acba3 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -8,11 +8,9 @@ use boltz_client::{ swaps::boltz::{ CreateChainResponse, CreateReverseResponse, CreateSubmarineResponse, Leaf, Side, SwapTree, }, - ToHex, }; use boltz_client::{BtcSwapScript, Keypair, LBtcSwapScript}; -use lwk_signer::SwSigner; -use lwk_wollet::ElementsNetwork; +use lwk_wollet::{bitcoin::bip32, ElementsNetwork}; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; use rusqlite::ToSql; use sdk_common::prelude::*; @@ -82,13 +80,13 @@ impl Config { } } - pub(crate) fn get_wallet_working_dir(&self, signer: &SwSigner) -> anyhow::Result { + pub(crate) fn get_wallet_working_dir(&self, fingerprint_hex: String) -> anyhow::Result { Ok(PathBuf::from(self.working_dir.clone()) .join(match self.network { LiquidNetwork::Mainnet => "mainnet", LiquidNetwork::Testnet => "testnet", }) - .join(signer.fingerprint().to_hex()) + .join(fingerprint_hex) .to_str() .ok_or(anyhow::anyhow!( "Could not get retrieve current wallet directory" @@ -202,10 +200,63 @@ pub enum SdkEvent { Synced, } +#[derive(thiserror::Error, Debug)] +pub enum SignerError { + #[error("Signer error: {err}")] + Generic { err: String }, +} + +impl From for SignerError { + fn from(err: anyhow::Error) -> Self { + SignerError::Generic { + err: err.to_string(), + } + } +} + +impl From for SignerError { + fn from(err: bip32::Error) -> Self { + SignerError::Generic { + err: err.to_string(), + } + } +} + +/// A trait that can be used to sign messages and verify signatures. +/// The sdk user can implement this trait to use their own signer. +pub trait Signer: Send + Sync { + /// The master xpub encoded as 78 bytes length as defined in bip32 specification. + /// For reference: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#user-content-Serialization_format + fn xpub(&self) -> Result, SignerError>; + + /// The derived xpub encoded as 78 bytes length as defined in bip32 specification. + /// The derivation path is a string represents the shorter notation of the key tree to derive. For example: + /// m/49'/1'/0'/0/0 + /// m/48'/1'/0'/0/0 + /// For reference: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#user-content-The_key_tree + fn derive_xpub(&self, derivation_path: String) -> Result, SignerError>; + + /// Sign an ECDSA message using the private key derived from the given derivation path + fn sign_ecdsa(&self, msg: Vec, derivation_path: String) -> Result, SignerError>; + + /// Sign an ECDSA message using the private key derived from the master key + fn sign_ecdsa_recoverable(&self, msg: Vec) -> Result, SignerError>; + + /// Return the master blinding key for SLIP77: https://github.com/satoshilabs/slips/blob/master/slip-0077.md + fn slip77_master_blinding_key(&self) -> Result, SignerError>; + + /// HMAC-SHA256 using the private key derived from the given derivation path + /// This is used to calculate the linking key of lnurl-auth specification: https://github.com/lnurl/luds/blob/luds/05.md + fn hmac_sha256(&self, msg: Vec, derivation_path: String) -> Result, SignerError>; +} + /// An argument when calling [crate::sdk::LiquidSdk::connect]. -#[derive(Debug, Serialize)] pub struct ConnectRequest { + pub config: Config, pub mnemonic: String, +} + +pub struct ConnectWithSignerRequest { pub config: Config, } diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 707a8e9dd..cec87701e 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -1,7 +1,3 @@ -use std::collections::HashMap; -use std::time::Instant; -use std::{fs, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; - use anyhow::{anyhow, Result}; use boltz_client::{swaps::boltz::*, util::secrets::Preimage}; use buy::{BuyBitcoinApi, BuyBitcoinService}; @@ -9,23 +5,25 @@ use chain::bitcoin::HybridBitcoinChainService; use chain::liquid::{HybridLiquidChainService, LiquidChainService}; use chain_swap::ESTIMATED_BTC_CLAIM_TX_VSIZE; use futures_util::stream::select_all; -use futures_util::StreamExt; -use futures_util::TryFutureExt; +use futures_util::{StreamExt, TryFutureExt}; +use lnurl::auth::SdkLnurlAuthSigner; use log::{debug, error, info, warn}; use lwk_wollet::bitcoin::base64::Engine as _; use lwk_wollet::elements::{AssetId, Txid}; +use lwk_wollet::elements_miniscript::elements::bitcoin::bip32::Xpub; use lwk_wollet::hashes::{sha256, Hash}; use lwk_wollet::secp256k1::ThirtyTwoByteHash; use lwk_wollet::{ElementsNetwork, WalletTx}; use sdk_common::bitcoin::hashes::hex::ToHex; -use sdk_common::bitcoin::secp256k1::Secp256k1; -use sdk_common::bitcoin::util::bip32::ChildNumber; use sdk_common::liquid::LiquidAddressData; use sdk_common::prelude::{FiatAPI, FiatCurrency, LnUrlPayError, LnUrlWithdrawError, Rate}; +use signer::SdkSigner; +use std::collections::HashMap; +use std::time::Instant; +use std::{fs, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; use tokio::sync::{watch, Mutex, RwLock}; use tokio::time::MissedTickBehavior; use tokio_stream::wrappers::BroadcastStream; -use url::Url; use x509_parser::parse_x509_certificate; use crate::chain::bitcoin::BitcoinChainService; @@ -34,6 +32,7 @@ use crate::ensure_sdk; use crate::error::SdkError; use crate::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use crate::model::PaymentState::*; +use crate::model::Signer; use crate::receive_swap::ReceiveSwapHandler; use crate::send_swap::SendSwapHandler; use crate::swapper::{boltz::BoltzSwapper, Swapper, SwapperReconnectHandler, SwapperStatusStream}; @@ -53,6 +52,7 @@ pub const CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS: u32 = 4320; pub struct LiquidSdk { pub(crate) config: Config, pub(crate) onchain_wallet: Arc, + pub(crate) signer: Arc>, pub(crate) persister: Arc, pub(crate) event_manager: Arc, pub(crate) status_stream: Arc, @@ -81,6 +81,18 @@ impl LiquidSdk { /// * `mnemonic` - the Liquid wallet mnemonic /// * `config` - the SDK [Config] pub async fn connect(req: ConnectRequest) -> Result> { + let signer = Box::new(SdkSigner::new( + req.mnemonic.as_ref(), + req.config.network == LiquidNetwork::Mainnet, + )?); + + Self::connect_with_signer(ConnectWithSignerRequest { config: req.config }, signer).await + } + + pub async fn connect_with_signer( + req: ConnectWithSignerRequest, + signer: Box, + ) -> Result> { let maybe_swapper_proxy_url = match BreezServer::new("https://bs1.breez.technology:443".into(), None) { Ok(breez_server) => breez_server @@ -90,10 +102,8 @@ impl LiquidSdk { .and_then(|swapper_urls| swapper_urls.first().cloned()), Err(_) => None, }; - - let sdk = LiquidSdk::new(req.config, maybe_swapper_proxy_url, req.mnemonic)?; + let sdk = LiquidSdk::new(req.config, maybe_swapper_proxy_url, Arc::new(signer))?; sdk.start().await?; - Ok(sdk) } @@ -125,7 +135,7 @@ impl LiquidSdk { fn new( config: Config, swapper_proxy_url: Option, - mnemonic: String, + signer: Arc>, ) -> Result> { match (config.network, &config.breez_api_key) { (_, Some(api_key)) => Self::validate_api_key(api_key)?, @@ -136,13 +146,16 @@ impl LiquidSdk { }; fs::create_dir_all(&config.working_dir)?; - - let onchain_wallet = Arc::new(LiquidOnchainWallet::new(mnemonic, config.clone())?); - - let persister = Arc::new(Persister::new( - &config.get_wallet_working_dir(&onchain_wallet.lwk_signer)?, - config.network, + let fingerprint_hex: String = + Xpub::decode(signer.xpub()?.as_slice())?.identifier()[0..4].to_hex(); + let working_dir = config.get_wallet_working_dir(fingerprint_hex)?; + let onchain_wallet = Arc::new(LiquidOnchainWallet::new( + signer.clone(), + config.clone(), + &working_dir, )?); + + let persister = Arc::new(Persister::new(&working_dir, config.network)?); persister.init()?; let event_manager = Arc::new(EventManager::new()); @@ -193,6 +206,7 @@ impl LiquidSdk { let sdk = Arc::new(LiquidSdk { config: config.clone(), onchain_wallet, + signer: signer.clone(), persister: persister.clone(), event_manager, status_stream: status_stream.clone(), @@ -526,8 +540,8 @@ impl LiquidSdk { balance_sat: confirmed_received_sat - confirmed_sent_sat - pending_send_sat, pending_send_sat, pending_receive_sat, - fingerprint: self.onchain_wallet.fingerprint(), - pubkey: self.onchain_wallet.pubkey(), + fingerprint: self.onchain_wallet.fingerprint()?, + pubkey: self.onchain_wallet.pubkey()?, }) } @@ -2184,7 +2198,8 @@ impl LiquidSdk { destination: data.pr.clone(), amount_sat: None, }) - .await?; + .await + .map_err(|e| LnUrlPayError::Generic { err: e.to_string() })?; Ok(PrepareLnUrlPayResponse { destination: prepare_response.destination, @@ -2219,7 +2234,8 @@ impl LiquidSdk { fees_sat: prepare_response.fees_sat, }, }) - .await? + .await + .map_err(|e| LnUrlPayError::Generic { err: e.to_string() })? .payment; let maybe_sa_processed: Option = match prepare_response @@ -2313,20 +2329,7 @@ impl LiquidSdk { &self, req_data: LnUrlAuthRequestData, ) -> Result { - // m/138'/0 - let hashing_key = self.onchain_wallet.derive_bip32_key(vec![ - ChildNumber::from_hardened_idx(138).map_err(Into::::into)?, - ChildNumber::from(0), - ])?; - - let url = - Url::from_str(&req_data.url).map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; - - let derivation_path = get_derivation_path(hashing_key, url)?; - let linking_key = self.onchain_wallet.derive_bip32_key(derivation_path)?; - let linking_keys = linking_key.to_keypair(&Secp256k1::new()); - - Ok(perform_lnurl_auth(linking_keys, req_data).await?) + Ok(perform_lnurl_auth(&req_data, &SdkLnurlAuthSigner::new(self.signer.clone())).await?) } /// Register for webhook callbacks at the given `webhook_url`. Each created swap after registering the diff --git a/lib/core/src/signer.rs b/lib/core/src/signer.rs new file mode 100644 index 000000000..cbb2a622a --- /dev/null +++ b/lib/core/src/signer.rs @@ -0,0 +1,461 @@ +use std::sync::Arc; + +use crate::model::{Signer, SignerError}; +use bip39::Mnemonic; +use boltz_client::PublicKey; +use lwk_common::Signer as LwkSigner; +use lwk_wollet::bitcoin::bip32::Xpriv; +use lwk_wollet::bitcoin::Network; +use lwk_wollet::elements_miniscript; +use lwk_wollet::elements_miniscript::{ + bitcoin::{self, bip32::DerivationPath}, + elements::{ + bitcoin::bip32::{self, Fingerprint, Xpub}, + hashes::Hash, + pset::PartiallySignedTransaction, + secp256k1_zkp::{All, Secp256k1}, + sighash::SighashCache, + }, + elementssig_to_rawsig, + psbt::PsbtExt, + slip77::MasterBlindingKey, +}; +use lwk_wollet::hashes::{sha256, HashEngine, Hmac, HmacEngine}; +use lwk_wollet::secp256k1::ecdsa::Signature; +use lwk_wollet::secp256k1::Message; + +#[derive(thiserror::Error, Debug)] +pub enum SignError { + #[error(transparent)] + Pset(#[from] elements_miniscript::elements::pset::Error), + + #[error(transparent)] + ElementsEncode(#[from] elements_miniscript::elements::encode::Error), + + #[error(transparent)] + Sighash(#[from] elements_miniscript::psbt::SighashError), + + #[error(transparent)] + PsetParse(#[from] elements_miniscript::elements::pset::ParseError), + + #[error(transparent)] + Bip32(#[from] bip32::Error), + + #[error(transparent)] + Generic(#[from] anyhow::Error), + + #[error(transparent)] + UserSignerError(#[from] crate::model::SignerError), +} + +/// Possible errors when creating a new software signer [`SwSigner`] +#[derive(thiserror::Error, Debug)] +pub enum NewError { + #[error(transparent)] + Bip39(#[from] bip39::Error), + + #[error(transparent)] + Bip32(#[from] bip32::Error), +} + +/// A software signer +pub struct SdkLwkSigner { + sdk_signer: Arc>, +} + +impl SdkLwkSigner { + /// Creates a new software signer from the given mnemonic. + /// + /// Takes also a flag if the network is mainnet so that generated extended keys are in the + /// correct form xpub/tpub (there is no need to discriminate between regtest and testnet) + pub fn new(sdk_signer: Arc>) -> Result { + Ok(Self { sdk_signer }) + } + + pub fn xpub(&self) -> Result { + let xpub = self.sdk_signer.xpub()?; + Ok(Xpub::decode(&xpub)?) + } + + pub fn fingerprint(&self) -> Result { + let f: Fingerprint = self.xpub()?.identifier()[0..4] + .try_into() + .map_err(|_| SignError::Generic(anyhow::anyhow!("Wrong fingerprint length")))?; + Ok(f) + } + + pub fn sign_ecdsa_recoverable(&self, msg: &Message) -> Result, SignError> { + let sig_bytes = self + .sdk_signer + .sign_ecdsa_recoverable(msg.as_ref().to_vec())?; + Ok(sig_bytes) + } +} + +impl LwkSigner for SdkLwkSigner { + type Error = SignError; + + fn sign(&self, pset: &mut PartiallySignedTransaction) -> Result { + let tx = pset.extract_tx()?; + let mut sighash_cache = SighashCache::new(&tx); + let mut signature_added = 0; + + // genesis hash is not used at all for sighash calculation + let genesis_hash = elements_miniscript::elements::BlockHash::all_zeros(); + let mut messages = vec![]; + for i in 0..pset.inputs().len() { + // computing all the messages to sign, it is not necessary if we are not going to sign + // some input, but since the pset is borrowed, we can't do this action in a inputs_mut() for loop + let msg = pset + .sighash_msg(i, &mut sighash_cache, None, genesis_hash)? + .to_secp_msg(); + messages.push(msg); + } + + // Fixme: Take a parameter + let hash_ty = elements_miniscript::elements::EcdsaSighashType::All; + + let signer_fingerprint = self.fingerprint()?; + for (input, msg) in pset.inputs_mut().iter_mut().zip(messages) { + for (want_public_key, (fingerprint, derivation_path)) in input.bip32_derivation.iter() { + if &signer_fingerprint == fingerprint { + let xpub = self.derive_xpub(derivation_path)?; + let public_key: PublicKey = xpub.public_key.into(); + if want_public_key == &public_key { + // fixme: for taproot use schnorr + let sig_bytes = self + .sdk_signer + .sign_ecdsa(msg.as_ref().to_vec(), derivation_path.to_string())?; + let sig = Signature::from_der(&sig_bytes).map_err(|_| { + SignError::Generic(anyhow::anyhow!("Invalid esda signature")) + })?; + let sig = elementssig_to_rawsig(&(sig, hash_ty)); + + let inserted = input.partial_sigs.insert(public_key, sig); + if inserted.is_none() { + signature_added += 1; + } + } + } + } + } + + Ok(signature_added) + } + + fn slip77_master_blinding_key(&self) -> Result { + let bytes: [u8; 32] = self + .sdk_signer + .slip77_master_blinding_key()? + .try_into() + .map_err(|_| { + SignError::Generic(anyhow::anyhow!("Wrong slip77 master blinding key length")) + })?; + Ok(bytes.into()) + } + + fn derive_xpub(&self, path: &DerivationPath) -> Result { + let pubkey_bytes = self.sdk_signer.derive_xpub(path.to_string())?; + let xpub = Xpub::decode(pubkey_bytes.as_slice())?; + Ok(xpub) + } +} + +pub struct SdkSigner { + xprv: Xpriv, + secp: Secp256k1, // could be sign only, but it is likely the caller already has the All context. + mnemonic: Mnemonic, + network: Network, +} + +impl SdkSigner { + pub fn new(mnemonic: &str, is_mainnet: bool) -> Result { + let secp = Secp256k1::new(); + let mnemonic: Mnemonic = mnemonic.parse()?; + let seed = mnemonic.to_seed(""); + + let network = if is_mainnet { + bitcoin::Network::Bitcoin + } else { + bitcoin::Network::Testnet + }; + + let xprv = Xpriv::new_master(network, &seed)?; + + Ok(Self { + xprv, + secp, + mnemonic, + network, + }) + } + + fn seed(&self) -> [u8; 64] { + self.mnemonic.to_seed("") + } +} + +impl Signer for SdkSigner { + fn xpub(&self) -> Result, SignerError> { + Ok(Xpub::from_priv(&self.secp, &self.xprv).encode().to_vec()) + } + + fn derive_xpub(&self, derivation_path: String) -> Result, SignerError> { + let der: DerivationPath = derivation_path.parse()?; + let derived = self.xprv.derive_priv(&self.secp, &der)?; + Ok(Xpub::from_priv(&self.secp, &derived).encode().to_vec()) + } + + fn sign_ecdsa(&self, msg: Vec, derivation_path: String) -> Result, SignerError> { + let der: DerivationPath = derivation_path.parse()?; + let ext_derived = self.xprv.derive_priv(&self.secp, &der)?; + let sig = self.secp.sign_ecdsa_low_r( + &Message::from_digest( + msg.try_into() + .map_err(|_| anyhow::anyhow!("failed to sign"))?, + ), + &ext_derived.private_key, + ); + Ok(sig.serialize_der().to_vec()) + } + + fn slip77_master_blinding_key(&self) -> Result, SignerError> { + let seed = self.seed(); + let master_blinding_key = MasterBlindingKey::from_seed(&seed[..]); + Ok(master_blinding_key.as_bytes().to_vec()) + } + + fn sign_ecdsa_recoverable(&self, msg: Vec) -> Result, SignerError> { + let seed = self.seed(); + let secp = Secp256k1::new(); + let keypair = Xpriv::new_master(self.network, &seed) + .map_err(|e| anyhow::anyhow!("Could not get signer keypair: {e}"))? + .to_keypair(&secp); + let s = msg.as_slice(); + + let msg: Message = Message::from_digest_slice(s) + .map_err(|e| SignerError::Generic { err: e.to_string() })?; + // Get message signature and encode to zbase32 + let recoverable_sig = secp.sign_ecdsa_recoverable(&msg, &keypair.secret_key()); + let (recovery_id, sig) = recoverable_sig.serialize_compact(); + let mut complete_signature = vec![31 + recovery_id.to_i32() as u8]; + complete_signature.extend_from_slice(&sig); + Ok(complete_signature) + } + + fn hmac_sha256(&self, msg: Vec, derivation_path: String) -> Result, SignerError> { + let der: DerivationPath = derivation_path.parse()?; + let priv_key = self.xprv.derive_priv(&self.secp, &der)?; + let mut engine = HmacEngine::::new(priv_key.to_priv().to_bytes().as_slice()); + + engine.input(msg.as_slice()); + Ok(Hmac::::from_engine(engine) + .as_byte_array() + .to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bip32::KeySource; + use bitcoin::PublicKey; + use elements::{ + pset::{Input, Output, PartiallySignedTransaction}, + AssetId, TxOut, Txid, + }; + use lwk_common::{singlesig_desc, Singlesig}; + use lwk_signer::SwSigner; + use lwk_wollet::{ + elements::{self, Script}, + ElementsNetwork, NoPersist, Wollet, WolletDescriptor, + }; + use std::collections::BTreeMap; + + fn get_descriptor( + signer: &S, + is_mainnet: bool, + ) -> Result { + let descriptor_str = singlesig_desc( + signer, + Singlesig::Wpkh, + lwk_common::DescriptorBlindingKey::Slip77, + is_mainnet, + ) + .map_err(|e| anyhow::anyhow!("Invalid descriptor: {e}"))?; + Ok(descriptor_str.parse()?) + } + + fn create_signers(mnemonic: &str) -> (SwSigner, SdkLwkSigner) { + let sw_signer = SwSigner::new(mnemonic, false).unwrap(); + let sdk_signer: Box = Box::new(SdkSigner::new(mnemonic, false).unwrap()); + let sdk_signer = SdkLwkSigner::new(Arc::new(sdk_signer)).unwrap(); + (sw_signer, sdk_signer) + } + + fn create_pset(signer: &S) -> PartiallySignedTransaction { + // Create a PartiallySignedTransaction + let mut pset = PartiallySignedTransaction::new_v2(); + + // Add a dummy input + let prev_txid = Txid::from_slice(&[0; 32]).unwrap(); + let prev_vout = 0; + + let derivation_path: DerivationPath = "m/84'/0'/0'/0/0".parse().unwrap(); + let xpub = signer.derive_xpub(&derivation_path).unwrap(); + let mut bip32_derivation_map: BTreeMap = BTreeMap::new(); + bip32_derivation_map.insert( + xpub.public_key.into(), + (signer.fingerprint().unwrap(), derivation_path), + ); + let input = Input { + non_witness_utxo: None, + witness_utxo: Some(TxOut::new_fee( + 100_000_000, + AssetId::from_slice(&[1; 32]).unwrap(), + )), + previous_txid: prev_txid, + previous_output_index: prev_vout, + bip32_derivation: bip32_derivation_map, + ..Default::default() + }; + + pset.add_input(input); + + // Add a dummy output using new_explicit + let output_script = Script::new(); + let output_amount = 99_000_000; + let output_asset = AssetId::from_slice(&[1; 32]).unwrap(); + let output = Output::new_explicit( + output_script, + output_amount, + output_asset, + None, // No blinding key for this example + ); + pset.add_output(output); + pset + } + + #[test] + fn test_sign() { + let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let (sw_signer, sdk_signer) = create_signers(mnemonic); + + // Clone the PSET for each signer + let mut pset_sw = create_pset(&sw_signer); + let mut pset_sdk = create_pset(&sdk_signer); + + // Sign with SwSigner + let sw_sig_count = sw_signer.sign(&mut pset_sw).unwrap(); + assert_eq!(sw_sig_count, 1); + + // Sign with SdkLwkSigner + let sdk_sig_count = sdk_signer.sign(&mut pset_sdk).unwrap(); + assert_eq!(sdk_sig_count, 1); + + // Compare the sign results + assert_eq!(pset_sw, pset_sdk); + + // Extract and compare the final transactions + let tx_sw = pset_sw.extract_tx().unwrap(); + let tx_sdk = pset_sdk.extract_tx().unwrap(); + assert_eq!(tx_sw, tx_sdk); + } + + #[test] + fn test_slip77_master_blinding_key() { + let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let (sw_signer, sdk_signer) = create_signers(mnemonic); + + let sw_key = sw_signer.slip77_master_blinding_key().unwrap(); + let sdk_key = sdk_signer.slip77_master_blinding_key().unwrap(); + + assert_eq!( + sw_key, sdk_key, + "SLIP77 master blinding keys should be identical" + ); + } + + #[test] + fn test_derive_xpub() { + let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let (sw_signer, sdk_signer) = create_signers(mnemonic); + + let path = "m/84'/0'/0'/0/0".parse().unwrap(); + let sw_xpub = sw_signer.derive_xpub(&path).unwrap(); + let sdk_xpub = sdk_signer.derive_xpub(&path).unwrap(); + + assert_eq!(sw_xpub, sdk_xpub, "Derived xpubs should be identical"); + } + + #[test] + fn test_identifier() { + let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let (sw_signer, sdk_signer) = create_signers(mnemonic); + + let sw_identifier = sw_signer.xpub().identifier(); + let sdk_identifier = sdk_signer.xpub().unwrap().identifier(); + + assert_eq!( + sw_identifier, sdk_identifier, + "Identifiers should be identical" + ); + } + + #[test] + fn test_fingerprint() { + let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let (sw_signer, sdk_signer) = create_signers(mnemonic); + + let sw_fingerprint = sw_signer.fingerprint(); + let sdk_fingerprint = sdk_signer.fingerprint().unwrap(); + let manual_finger_print = sdk_signer.xpub().unwrap().identifier()[0..4] + .try_into() + .unwrap(); + assert_eq!( + sw_fingerprint, sdk_fingerprint, + "Fingerprints should be identical" + ); + + assert_eq!( + sw_fingerprint, manual_finger_print, + "Fingerprints should be identical" + ); + } + + #[test] + fn test_sdk_signer_vs_sw_signer() { + // Use a test mnemonic (don't use this in production!) + let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let network = ElementsNetwork::LiquidTestnet; + + // 1. Create a wallet using SwSigner + let sw_signer = SwSigner::new(mnemonic, false).unwrap(); + let sw_wallet = Wollet::new( + network, + NoPersist::new(), + get_descriptor(&sw_signer, false).unwrap(), + ) + .unwrap(); + + // 2. Create a wallet using SdkLwkSigner + let sdk_signer: Box = Box::new(SdkSigner::new(mnemonic, false).unwrap()); + let sdk_signer = SdkLwkSigner::new(Arc::new(sdk_signer)).unwrap(); + let sdk_wallet = Wollet::new( + network, + NoPersist::new(), + get_descriptor(&sdk_signer, false).unwrap(), + ) + .unwrap(); + + // Generate new addresses and compare + let sw_address = sw_wallet.address(None).unwrap(); + let sdk_address = sdk_wallet.address(None).unwrap(); + + assert_eq!( + sw_address.address().to_string(), + sdk_address.address().to_string(), + "Addresses should be identical" + ); + } +} diff --git a/lib/core/src/test_utils/sdk.rs b/lib/core/src/test_utils/sdk.rs index f8b2d455f..ae34026bb 100644 --- a/lib/core/src/test_utils/sdk.rs +++ b/lib/core/src/test_utils/sdk.rs @@ -7,8 +7,13 @@ use std::sync::Arc; use tokio::sync::{watch, Mutex, RwLock}; use crate::{ - buy::BuyBitcoinService, chain_swap::ChainSwapHandler, event::EventManager, model::Config, - persist::Persister, receive_swap::ReceiveSwapHandler, sdk::LiquidSdk, + buy::BuyBitcoinService, + chain_swap::ChainSwapHandler, + event::EventManager, + model::{Config, Signer}, + persist::Persister, + receive_swap::ReceiveSwapHandler, + sdk::LiquidSdk, send_swap::SendSwapHandler, }; @@ -16,7 +21,7 @@ use super::{ chain::{MockBitcoinChainService, MockLiquidChainService}, status_stream::MockStatusStream, swapper::MockSwapper, - wallet::MockWallet, + wallet::{MockSigner, MockWallet}, }; pub(crate) fn new_liquid_sdk( @@ -50,6 +55,7 @@ pub(crate) fn new_liquid_sdk_with_chain_services( .ok_or(anyhow!("An invalid SDK directory was specified"))? .to_string(); + let signer: Arc> = Arc::new(Box::new(MockSigner::new())); let onchain_wallet = Arc::new(MockWallet::new()); let send_swap_handler = SendSwapHandler::new( @@ -88,6 +94,7 @@ pub(crate) fn new_liquid_sdk_with_chain_services( Ok(LiquidSdk { config, onchain_wallet, + signer, persister, event_manager, status_stream, diff --git a/lib/core/src/test_utils/wallet.rs b/lib/core/src/test_utils/wallet.rs index f1519de91..e5c277e64 100644 --- a/lib/core/src/test_utils/wallet.rs +++ b/lib/core/src/test_utils/wallet.rs @@ -2,7 +2,12 @@ use std::str::FromStr; -use crate::{error::PaymentError, utils, wallet::OnchainWallet}; +use crate::{ + error::PaymentError, + model::{Signer, SignerError}, + utils, + wallet::OnchainWallet, +}; use anyhow::Result; use async_trait::async_trait; use lazy_static::lazy_static; @@ -10,7 +15,6 @@ use lwk_wollet::{ elements::{Address, Transaction}, Tip, WalletTx, }; -use sdk_common::bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey}; pub(crate) struct MockWallet {} @@ -57,15 +61,11 @@ impl OnchainWallet for MockWallet { unimplemented!() } - fn fingerprint(&self) -> String { - unimplemented!() - } - - fn pubkey(&self) -> String { + fn pubkey(&self) -> Result { unimplemented!() } - fn derive_bip32_key(&self, _path: Vec) -> Result { + fn fingerprint(&self) -> Result { unimplemented!() } @@ -81,3 +81,37 @@ impl OnchainWallet for MockWallet { Ok(()) } } + +pub(crate) struct MockSigner {} + +impl MockSigner { + pub(crate) fn new() -> Self { + Self {} + } +} + +impl Signer for MockSigner { + fn xpub(&self) -> Result, SignerError> { + todo!() + } + + fn derive_xpub(&self, _derivation_path: String) -> Result, SignerError> { + todo!() + } + + fn sign_ecdsa(&self, _msg: Vec, _derivation_path: String) -> Result, SignerError> { + todo!() + } + + fn sign_ecdsa_recoverable(&self, _msg: Vec) -> Result, SignerError> { + todo!() + } + + fn slip77_master_blinding_key(&self) -> Result, SignerError> { + todo!() + } + + fn hmac_sha256(&self, _msg: Vec, _derivation_path: String) -> Result, SignerError> { + todo!() + } +} diff --git a/lib/core/src/wallet.rs b/lib/core/src/wallet.rs index e37880a83..9f6950bee 100644 --- a/lib/core/src/wallet.rs +++ b/lib/core/src/wallet.rs @@ -4,25 +4,26 @@ use std::{str::FromStr, sync::Arc}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use boltz_client::ElementsAddress; -use lwk_common::Signer; +use lwk_common::Signer as LwkSigner; use lwk_common::{singlesig_desc, Singlesig}; -use lwk_signer::{AnySigner, SwSigner}; use lwk_wollet::{ elements::{hex::ToHex, Address, Transaction}, ElectrumClient, ElectrumUrl, ElementsNetwork, FsPersister, Tip, WalletTx, Wollet, WolletDescriptor, }; use sdk_common::bitcoin::hashes::{sha256, Hash}; -use sdk_common::bitcoin::secp256k1::{Message, PublicKey, Secp256k1}; -use sdk_common::bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey}; +use sdk_common::bitcoin::secp256k1::PublicKey; use sdk_common::lightning::util::message_signing::verify; use tokio::sync::Mutex; +use crate::model::Signer; +use crate::signer::SdkLwkSigner; use crate::{ ensure_sdk, error::PaymentError, model::{Config, LiquidNetwork}, }; +use lwk_wollet::secp256k1::Message; static LN_MESSAGE_PREFIX: &[u8] = b"Lightning Signed Message:"; @@ -60,10 +61,10 @@ pub trait OnchainWallet: Send + Sync { async fn tip(&self) -> Tip; /// Get the public key of the wallet - fn pubkey(&self) -> String; - fn fingerprint(&self) -> String; + fn pubkey(&self) -> Result; - fn derive_bip32_key(&self, path: Vec) -> Result; + /// Get the fingerprint of the wallet + fn fingerprint(&self) -> Result; /// Sign given message with the wallet private key. Returns a zbase /// encoded signature. @@ -80,31 +81,30 @@ pub trait OnchainWallet: Send + Sync { pub(crate) struct LiquidOnchainWallet { wallet: Arc>, config: Config, - pub(crate) lwk_signer: SwSigner, + pub(crate) signer: SdkLwkSigner, } impl LiquidOnchainWallet { - pub(crate) fn new(mnemonic: String, config: Config) -> Result { - let is_mainnet = config.network == LiquidNetwork::Mainnet; - let lwk_signer = SwSigner::new(&mnemonic, is_mainnet)?; - let descriptor = LiquidOnchainWallet::get_descriptor(&lwk_signer, config.network)?; + pub(crate) fn new( + user_signer: Arc>, + config: Config, + working_dir: &String, + ) -> Result { + let signer = crate::signer::SdkLwkSigner::new(user_signer)?; + let descriptor = LiquidOnchainWallet::get_descriptor(&signer, config.network)?; let elements_network: ElementsNetwork = config.network.into(); - let lwk_persister = FsPersister::new( - config.get_wallet_working_dir(&lwk_signer)?, - elements_network, - &descriptor, - )?; + let lwk_persister = FsPersister::new(working_dir, elements_network, &descriptor)?; let wollet = Wollet::new(elements_network, lwk_persister, descriptor)?; Ok(Self { wallet: Arc::new(Mutex::new(wollet)), - lwk_signer, + signer, config, }) } fn get_descriptor( - signer: &SwSigner, + signer: &SdkLwkSigner, network: LiquidNetwork, ) -> Result { let is_mainnet = network == LiquidNetwork::Mainnet; @@ -150,8 +150,11 @@ impl OnchainWallet for LiquidOnchainWallet { )? .fee_rate(fee_rate_sats_per_kvb) .finish(&lwk_wollet)?; - let signer = AnySigner::Software(self.lwk_signer.clone()); - signer.sign(&mut pset)?; + self.signer + .sign(&mut pset) + .map_err(|e| PaymentError::Generic { + err: format!("Failed to sign transaction: {e:?}"), + })?; Ok(lwk_wollet.finalize(&mut pset)?) } @@ -193,8 +196,11 @@ impl OnchainWallet for LiquidOnchainWallet { ); } - let signer = AnySigner::Software(self.lwk_signer.clone()); - signer.sign(&mut pset)?; + self.signer + .sign(&mut pset) + .map_err(|e| PaymentError::Generic { + err: format!("Failed to sign transaction: {e:?}"), + })?; Ok(lwk_wollet.finalize(&mut pset)?) } @@ -209,8 +215,13 @@ impl OnchainWallet for LiquidOnchainWallet { } /// Get the public key of the wallet - fn pubkey(&self) -> String { - self.lwk_signer.xpub().public_key.to_string() + fn pubkey(&self) -> Result { + Ok(self.signer.xpub()?.public_key.to_string()) + } + + /// Get the fingerprint of the wallet + fn fingerprint(&self) -> Result { + Ok(self.signer.fingerprint()?.to_hex()) } /// Perform a full scan of the wallet @@ -225,46 +236,90 @@ impl OnchainWallet for LiquidOnchainWallet { Ok(()) } - fn derive_bip32_key(&self, path: Vec) -> Result { - let seed = self.lwk_signer.seed().ok_or(PaymentError::SignerError { - err: "Could not get signer seed".to_string(), - })?; - - let bip32_xpriv = ExtendedPrivKey::new_master(self.config.network.into(), &seed)? - .derive_priv(&Secp256k1::new(), &path)?; - Ok(bip32_xpriv) - } - fn sign_message(&self, message: &str) -> Result { - let seed = self - .lwk_signer - .seed() - .ok_or(anyhow!("Could not get signer seed"))?; - let secp = Secp256k1::new(); - let keypair = ExtendedPrivKey::new_master(self.config.network.into(), &seed) - .map_err(|e| anyhow!("Could not get signer keypair: {e}"))? - .to_keypair(&secp); // Prefix and double hash message let mut engine = sha256::HashEngine::default(); engine.write_all(LN_MESSAGE_PREFIX)?; engine.write_all(message.as_bytes())?; let hashed_msg = sha256::Hash::from_engine(engine); - let double_hashed_msg = Message::from(sha256::Hash::hash(&hashed_msg)); + let double_hashed_msg = Message::from_digest(sha256::Hash::hash(&hashed_msg).into_inner()); // Get message signature and encode to zbase32 - let recoverable_sig = - secp.sign_ecdsa_recoverable(&double_hashed_msg, &keypair.secret_key()); - let (recovery_id, sig) = recoverable_sig.serialize_compact(); - let mut complete_signature = vec![31 + recovery_id.to_i32() as u8]; - complete_signature.extend_from_slice(&sig); - Ok(zbase32::encode_full_bytes(&complete_signature)) + let recoverable_sig = self.signer.sign_ecdsa_recoverable(&double_hashed_msg)?; + Ok(zbase32::encode_full_bytes(recoverable_sig.as_slice())) } fn check_message(&self, message: &str, pubkey: &str, signature: &str) -> Result { let pk = PublicKey::from_str(pubkey)?; Ok(verify(message.as_bytes(), signature, &pk)) } +} - fn fingerprint(&self) -> String { - self.lwk_signer.fingerprint().to_hex() +#[cfg(test)] +mod tests { + use super::*; + use crate::model::Config; + use crate::signer::SdkSigner; + use crate::wallet::LiquidOnchainWallet; + use tempfile::TempDir; + + #[tokio::test] + async fn test_sign_and_check_message() { + let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let sdk_signer: Box = Box::new(SdkSigner::new(mnemonic, false).unwrap()); + let sdk_signer = Arc::new(sdk_signer); + + let config = Config::testnet(None); + + // Create a temporary directory for working_dir + let temp_dir = TempDir::new().unwrap(); + let working_dir = temp_dir.path().to_str().unwrap().to_string(); + + let wallet: Arc = + Arc::new(LiquidOnchainWallet::new(sdk_signer.clone(), config, &working_dir).unwrap()); + + // Test message + let message = "Hello, Liquid!"; + + // Sign the message + let signature = wallet.sign_message(message).unwrap(); + + // Get the public key + let pubkey = wallet.pubkey().unwrap(); + + // Check the message + let is_valid = wallet.check_message(message, &pubkey, &signature).unwrap(); + assert!(is_valid, "Message signature should be valid"); + + // Check with an incorrect message + let incorrect_message = "Wrong message"; + let is_invalid = wallet + .check_message(incorrect_message, &pubkey, &signature) + .unwrap(); + assert!( + !is_invalid, + "Message signature should be invalid for incorrect message" + ); + + // Check with an incorrect public key + let incorrect_pubkey = "02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc"; + let is_invalid = wallet + .check_message(message, incorrect_pubkey, &signature) + .unwrap(); + assert!( + !is_invalid, + "Message signature should be invalid for incorrect public key" + ); + + // Check with an incorrect signature + let incorrect_signature = zbase32::encode_full_bytes(&[0; 65]); + let is_invalid = wallet + .check_message(message, &pubkey, &incorrect_signature) + .unwrap(); + assert!( + !is_invalid, + "Message signature should be invalid for incorrect signature" + ); + + // The temporary directory will be automatically deleted when temp_dir goes out of scope } } diff --git a/packages/dart/lib/src/frb_generated.dart b/packages/dart/lib/src/frb_generated.dart index 3c56a0f82..a382c9dbb 100644 --- a/packages/dart/lib/src/frb_generated.dart +++ b/packages/dart/lib/src/frb_generated.dart @@ -1666,8 +1666,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final arr = raw as List; if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); return ConnectRequest( - mnemonic: dco_decode_String(arr[0]), - config: dco_decode_config(arr[1]), + config: dco_decode_config(arr[0]), + mnemonic: dco_decode_String(arr[1]), ); } @@ -3442,9 +3442,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @protected ConnectRequest sse_decode_connect_request(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs - var var_mnemonic = sse_decode_String(deserializer); var var_config = sse_decode_config(deserializer); - return ConnectRequest(mnemonic: var_mnemonic, config: var_config); + var var_mnemonic = sse_decode_String(deserializer); + return ConnectRequest(config: var_config, mnemonic: var_mnemonic); } @protected @@ -5330,8 +5330,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @protected void sse_encode_connect_request(ConnectRequest self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.mnemonic, serializer); sse_encode_config(self.config, serializer); + sse_encode_String(self.mnemonic, serializer); } @protected diff --git a/packages/dart/lib/src/frb_generated.io.dart b/packages/dart/lib/src/frb_generated.io.dart index 4fa8697c1..d5f2bf4b6 100644 --- a/packages/dart/lib/src/frb_generated.io.dart +++ b/packages/dart/lib/src/frb_generated.io.dart @@ -2003,8 +2003,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void cst_api_fill_to_wire_connect_request(ConnectRequest apiObj, wire_cst_connect_request wireObj) { - wireObj.mnemonic = cst_encode_String(apiObj.mnemonic); cst_api_fill_to_wire_config(apiObj.config, wireObj.config); + wireObj.mnemonic = cst_encode_String(apiObj.mnemonic); } @protected @@ -5580,9 +5580,9 @@ final class wire_cst_config extends ffi.Struct { } final class wire_cst_connect_request extends ffi.Struct { - external ffi.Pointer mnemonic; - external wire_cst_config config; + + external ffi.Pointer mnemonic; } final class wire_cst_aes_success_action_data_decrypted extends ffi.Struct { diff --git a/packages/dart/lib/src/lib.dart b/packages/dart/lib/src/lib.dart new file mode 100644 index 000000000..fb0def6e9 --- /dev/null +++ b/packages/dart/lib/src/lib.dart @@ -0,0 +1,10 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.4.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +// Rust type: RustOpaqueNom >>> +abstract class ArcBoxSigner implements RustOpaqueInterface {} diff --git a/packages/dart/lib/src/model.dart b/packages/dart/lib/src/model.dart index 86caddbcf..bc2ea5cb8 100644 --- a/packages/dart/lib/src/model.dart +++ b/packages/dart/lib/src/model.dart @@ -180,24 +180,24 @@ class Config { /// An argument when calling [crate::sdk::LiquidSdk::connect]. class ConnectRequest { - final String mnemonic; final Config config; + final String mnemonic; const ConnectRequest({ - required this.mnemonic, required this.config, + required this.mnemonic, }); @override - int get hashCode => mnemonic.hashCode ^ config.hashCode; + int get hashCode => config.hashCode ^ mnemonic.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is ConnectRequest && runtimeType == other.runtimeType && - mnemonic == other.mnemonic && - config == other.config; + config == other.config && + mnemonic == other.mnemonic; } /// Returned when calling [crate::sdk::LiquidSdk::get_info]. diff --git a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart index a7d64b8c0..f1efd2be5 100644 --- a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart +++ b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart @@ -4267,9 +4267,9 @@ final class wire_cst_config extends ffi.Struct { } final class wire_cst_connect_request extends ffi.Struct { - external ffi.Pointer mnemonic; - external wire_cst_config config; + + external ffi.Pointer mnemonic; } final class wire_cst_aes_success_action_data_decrypted extends ffi.Struct { diff --git a/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt b/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt index 8a459f0a4..b9c340314 100644 --- a/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt +++ b/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt @@ -341,6 +341,36 @@ fun asConnectRequestList(arr: ReadableArray): List { return list } +fun asConnectWithSignerRequest(connectWithSignerRequest: ReadableMap): ConnectWithSignerRequest? { + if (!validateMandatoryFields( + connectWithSignerRequest, + arrayOf( + "config", + ), + ) + ) { + return null + } + val config = connectWithSignerRequest.getMap("config")?.let { asConfig(it) }!! + return ConnectWithSignerRequest(config) +} + +fun readableMapOf(connectWithSignerRequest: ConnectWithSignerRequest): ReadableMap = + readableMapOf( + "config" to readableMapOf(connectWithSignerRequest.config), + ) + +fun asConnectWithSignerRequestList(arr: ReadableArray): List { + val list = ArrayList() + for (value in arr.toList()) { + when (value) { + is ReadableMap -> list.add(asConnectWithSignerRequest(value)!!) + else -> throw SdkException.Generic(errUnexpectedType(value)) + } + } + return list +} + fun asCurrencyInfo(currencyInfo: ReadableMap): CurrencyInfo? { if (!validateMandatoryFields( currencyInfo, diff --git a/packages/react-native/ios/BreezSDKLiquidMapper.swift b/packages/react-native/ios/BreezSDKLiquidMapper.swift index aca6f2924..2efdbb4e7 100644 --- a/packages/react-native/ios/BreezSDKLiquidMapper.swift +++ b/packages/react-native/ios/BreezSDKLiquidMapper.swift @@ -393,6 +393,38 @@ enum BreezSDKLiquidMapper { return connectRequestList.map { v -> [String: Any?] in return dictionaryOf(connectRequest: v) } } + static func asConnectWithSignerRequest(connectWithSignerRequest: [String: Any?]) throws -> ConnectWithSignerRequest { + guard let configTmp = connectWithSignerRequest["config"] as? [String: Any?] else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "config", typeName: "ConnectWithSignerRequest")) + } + let config = try asConfig(config: configTmp) + + return ConnectWithSignerRequest(config: config) + } + + static func dictionaryOf(connectWithSignerRequest: ConnectWithSignerRequest) -> [String: Any?] { + return [ + "config": dictionaryOf(config: connectWithSignerRequest.config), + ] + } + + static func asConnectWithSignerRequestList(arr: [Any]) throws -> [ConnectWithSignerRequest] { + var list = [ConnectWithSignerRequest]() + for value in arr { + if let val = value as? [String: Any?] { + var connectWithSignerRequest = try asConnectWithSignerRequest(connectWithSignerRequest: val) + list.append(connectWithSignerRequest) + } else { + throw SdkError.Generic(message: errUnexpectedType(typeName: "ConnectWithSignerRequest")) + } + } + return list + } + + static func arrayOf(connectWithSignerRequestList: [ConnectWithSignerRequest]) -> [Any] { + return connectWithSignerRequestList.map { v -> [String: Any?] in return dictionaryOf(connectWithSignerRequest: v) } + } + static func asCurrencyInfo(currencyInfo: [String: Any?]) throws -> CurrencyInfo { guard let name = currencyInfo["name"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "name", typeName: "CurrencyInfo")) diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 1f4db8202..dac70d1d1 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -74,6 +74,10 @@ export interface ConnectRequest { mnemonic: string } +export interface ConnectWithSignerRequest { + config: Config +} + export interface CurrencyInfo { name: string fractionSize: number