diff --git a/libs/sdk-bindings/src/breez_sdk.udl b/libs/sdk-bindings/src/breez_sdk.udl index f6df084d8..adc0c8ef8 100644 --- a/libs/sdk-bindings/src/breez_sdk.udl +++ b/libs/sdk-bindings/src/breez_sdk.udl @@ -408,7 +408,18 @@ enum BuyBitcoinProvider { "Moonpay", }; -interface BlockingBreezServices { +dictionary PrepareWithdrawRequest { + string to_address; + u32 fee_rate_sats_per_vbyte; +}; + +dictionary PrepareWithdrawResponse { + string raw_tx_hex; + u32 sat_per_vbyte; + u64 fee_sat; +}; + +interface BlockingBreezServices { [Throws=SdkError] void disconnect(); @@ -502,6 +513,9 @@ interface BlockingBreezServices { [Throws=SdkError] string buy_bitcoin(BuyBitcoinProvider provider); + + [Throws=SdkError] + PrepareWithdrawResponse prepare_withdraw(PrepareWithdrawRequest prepare_withdraw_request); }; namespace breez_sdk { @@ -522,4 +536,4 @@ namespace breez_sdk { sequence mnemonic_to_seed(string phrase); Config default_config(EnvironmentType env_type, string api_key, NodeConfig node_config); -}; \ No newline at end of file +}; diff --git a/libs/sdk-bindings/src/uniffi_binding.rs b/libs/sdk-bindings/src/uniffi_binding.rs index 1098d699b..dbbab807a 100644 --- a/libs/sdk-bindings/src/uniffi_binding.rs +++ b/libs/sdk-bindings/src/uniffi_binding.rs @@ -14,9 +14,10 @@ use breez_sdk_core::{ LnUrlErrorData, LnUrlPayRequestData, LnUrlPayResult, LnUrlWithdrawRequestData, LocaleOverrides, LocalizedName, LogEntry, LspInformation, MessageSuccessActionData, MetadataItem, Network, NodeConfig, NodeState, Payment, PaymentDetails, PaymentFailedData, PaymentType, - PaymentTypeFilter, Rate, RecommendedFees, ReverseSwapInfo, ReverseSwapPairInfo, - ReverseSwapStatus, RouteHint, RouteHintHop, SuccessActionProcessed, SwapInfo, SwapStatus, - Symbol, UnspentTransactionOutput, UrlSuccessActionData, + PaymentTypeFilter, PrepareWithdrawRequest, PrepareWithdrawResponse, Rate, RecommendedFees, + ReverseSwapInfo, ReverseSwapPairInfo, ReverseSwapStatus, RouteHint, RouteHintHop, + SuccessActionProcessed, SwapInfo, SwapStatus, Symbol, UnspentTransactionOutput, + UrlSuccessActionData, }; static RT: Lazy = Lazy::new(|| tokio::runtime::Runtime::new().unwrap()); @@ -302,6 +303,17 @@ impl BlockingBreezServices { rt().block_on(self.breez_services.buy_bitcoin(provider)) .map_err(|e| e.into()) } + + pub fn prepare_withdraw( + &self, + prepare_withdraw_request: PrepareWithdrawRequest, + ) -> Result { + rt().block_on( + self.breez_services + .prepare_withdraw(prepare_withdraw_request), + ) + .map_err(|e| e.into()) + } } pub fn parse_invoice(invoice: String) -> SdkResult { diff --git a/libs/sdk-core/src/binding.rs b/libs/sdk-core/src/binding.rs index d3b0ac7d4..555b685bd 100644 --- a/libs/sdk-core/src/binding.rs +++ b/libs/sdk-core/src/binding.rs @@ -32,7 +32,7 @@ use crate::lsp::LspInformation; use crate::models::{Config, LogEntry, NodeState, Payment, PaymentTypeFilter, SwapInfo}; use crate::{ BackupStatus, BuyBitcoinProvider, EnvironmentType, LnUrlCallbackStatus, NodeConfig, - ReverseSwapInfo, ReverseSwapPairInfo, + PrepareWithdrawRequest, PrepareWithdrawResponse, ReverseSwapInfo, ReverseSwapPairInfo, }; static BREEZ_SERVICES_INSTANCE: OnceCell> = OnceCell::new(); @@ -311,6 +311,17 @@ pub fn sweep(to_address: String, fee_rate_sats_per_vbyte: u64) -> Result<()> { }) } +/// See [BreezServices::prepare_withdraw] +pub fn prepare_withdraw( + prepare_withdraw_request: PrepareWithdrawRequest, +) -> Result { + block_on(async { + get_breez_services()? + .prepare_withdraw(prepare_withdraw_request) + .await + }) +} + /* Refundables API's */ /// See [BreezServices::list_refundables] diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 355c4e3c3..fca96ee01 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -386,6 +386,19 @@ impl BreezServices { Ok(()) } + pub async fn prepare_withdraw( + &self, + prepare_withdraw_request: PrepareWithdrawRequest, + ) -> Result { + self.start_node().await?; + let response = self + .node_api + .prepare_withdraw(prepare_withdraw_request) + .await?; + self.sync().await?; + Ok(response) + } + /// Fetch live rates of fiat currencies pub async fn fetch_fiat_rates(&self) -> Result> { self.fiat_api.fetch_fiat_rates().await @@ -931,7 +944,7 @@ impl BreezServices { } } -fn closed_channel_to_transaction(channel: crate::models::Channel) -> Result { +fn closed_channel_to_transaction(channel: models::Channel) -> Result { let now = SystemTime::now(); Ok(Payment { id: channel.funding_txid.clone(), diff --git a/libs/sdk-core/src/bridge_generated.io.rs b/libs/sdk-core/src/bridge_generated.io.rs index 84cbf9581..916224621 100644 --- a/libs/sdk-core/src/bridge_generated.io.rs +++ b/libs/sdk-core/src/bridge_generated.io.rs @@ -209,6 +209,14 @@ pub extern "C" fn wire_sweep( wire_sweep_impl(port_, to_address, fee_rate_sats_per_vbyte) } +#[no_mangle] +pub extern "C" fn wire_prepare_withdraw( + port_: i64, + prepare_withdraw_request: *mut wire_PrepareWithdrawRequest, +) { + wire_prepare_withdraw_impl(port_, prepare_withdraw_request) +} + #[no_mangle] pub extern "C" fn wire_list_refundables(port_: i64) { wire_list_refundables_impl(port_) @@ -292,6 +300,11 @@ pub extern "C" fn new_box_autoadd_node_config_0() -> *mut wire_NodeConfig { support::new_leak_box_ptr(wire_NodeConfig::new_with_null_ptr()) } +#[no_mangle] +pub extern "C" fn new_box_autoadd_prepare_withdraw_request_0() -> *mut wire_PrepareWithdrawRequest { + support::new_leak_box_ptr(wire_PrepareWithdrawRequest::new_with_null_ptr()) +} + #[no_mangle] pub extern "C" fn new_box_autoadd_u64_0(value: u64) -> *mut u64 { support::new_leak_box_ptr(value) @@ -363,6 +376,12 @@ impl Wire2Api for *mut wire_NodeConfig { Wire2Api::::wire2api(*wrap).into() } } +impl Wire2Api for *mut wire_PrepareWithdrawRequest { + fn wire2api(self) -> PrepareWithdrawRequest { + let wrap = unsafe { support::box_from_leak_ptr(self) }; + Wire2Api::::wire2api(*wrap).into() + } +} impl Wire2Api for *mut u64 { fn wire2api(self) -> u64 { unsafe { *support::box_from_leak_ptr(self) } @@ -452,6 +471,15 @@ impl Wire2Api for wire_NodeConfig { } } +impl Wire2Api for wire_PrepareWithdrawRequest { + fn wire2api(self) -> PrepareWithdrawRequest { + PrepareWithdrawRequest { + to_address: self.to_address.wire2api(), + fee_rate_sats_per_vbyte: self.fee_rate_sats_per_vbyte.wire2api(), + } + } +} + impl Wire2Api> for *mut wire_uint_8_list { fn wire2api(self) -> Vec { unsafe { @@ -521,6 +549,13 @@ pub struct wire_LnUrlWithdrawRequestData { max_withdrawable: u64, } +#[repr(C)] +#[derive(Clone)] +pub struct wire_PrepareWithdrawRequest { + to_address: *mut wire_uint_8_list, + fee_rate_sats_per_vbyte: u32, +} + #[repr(C)] #[derive(Clone)] pub struct wire_uint_8_list { @@ -689,6 +724,21 @@ pub extern "C" fn inflate_NodeConfig_Greenlight() -> *mut NodeConfigKind { }) } +impl NewWithNullPtr for wire_PrepareWithdrawRequest { + fn new_with_null_ptr() -> Self { + Self { + to_address: core::ptr::null_mut(), + fee_rate_sats_per_vbyte: Default::default(), + } + } +} + +impl Default for wire_PrepareWithdrawRequest { + fn default() -> Self { + Self::new_with_null_ptr() + } +} + // Section: sync execution mode utility #[no_mangle] diff --git a/libs/sdk-core/src/bridge_generated.rs b/libs/sdk-core/src/bridge_generated.rs index 8b7ed7c01..14e04bbb6 100644 --- a/libs/sdk-core/src/bridge_generated.rs +++ b/libs/sdk-core/src/bridge_generated.rs @@ -63,6 +63,8 @@ use crate::models::Payment; use crate::models::PaymentDetails; use crate::models::PaymentType; use crate::models::PaymentTypeFilter; +use crate::models::PrepareWithdrawRequest; +use crate::models::PrepareWithdrawResponse; use crate::models::ReverseSwapInfo; use crate::models::ReverseSwapPairInfo; use crate::models::ReverseSwapStatus; @@ -521,6 +523,22 @@ fn wire_sweep_impl( }, ) } +fn wire_prepare_withdraw_impl( + port_: MessagePort, + prepare_withdraw_request: impl Wire2Api + UnwindSafe, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap( + WrapInfo { + debug_name: "prepare_withdraw", + port: Some(port_), + mode: FfiCallMode::Normal, + }, + move || { + let api_prepare_withdraw_request = prepare_withdraw_request.wire2api(); + move |task_callback| prepare_withdraw(api_prepare_withdraw_request) + }, + ) +} fn wire_list_refundables_impl(port_: MessagePort) { FLUTTER_RUST_BRIDGE_HANDLER.wrap( WrapInfo { @@ -684,6 +702,7 @@ impl Wire2Api for i32 { } } } + impl Wire2Api for u16 { fn wire2api(self) -> u16 { self @@ -1123,6 +1142,18 @@ impl support::IntoDart for PaymentType { } } impl support::IntoDartExceptPrimitive for PaymentType {} +impl support::IntoDart for PrepareWithdrawResponse { + fn into_dart(self) -> support::DartAbi { + vec![ + self.raw_tx_hex.into_dart(), + self.sat_per_vbyte.into_dart(), + self.fee_sat.into_dart(), + ] + .into_dart() + } +} +impl support::IntoDartExceptPrimitive for PrepareWithdrawResponse {} + impl support::IntoDart for Rate { fn into_dart(self) -> support::DartAbi { vec![self.coin.into_dart(), self.value.into_dart()].into_dart() diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index 6f216b93d..66cdcdb86 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -16,13 +16,14 @@ use gl_client::pb::cln::{ ListpeerchannelsRequest, }; use gl_client::pb::{ - Amount, Invoice, InvoiceRequest, InvoiceStatus, OffChainPayment, PayStatus, Peer, - WithdrawResponse, + Amount, Invoice, InvoiceRequest, InvoiceStatus, ListFundsResponse, OffChainPayment, PayStatus, + Peer, WithdrawResponse, }; use gl_client::scheduler::Scheduler; use gl_client::signer::Signer; use gl_client::tls::TlsConfig; use gl_client::{node, pb, utils}; +use lightning::util::ser::Writeable; use lightning_invoice::{RawInvoice, SignedRawInvoice}; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; @@ -35,7 +36,7 @@ use crate::models::{ PaymentType, SyncResponse, UnspentTransactionOutput, }; use crate::persist::db::SqliteStorage; -use crate::{Channel, ChannelState, NodeConfig}; +use crate::{Channel, ChannelState, NodeConfig, PrepareWithdrawRequest, PrepareWithdrawResponse}; const MAX_PAYMENT_AMOUNT_MSAT: u64 = 4294967000; const MAX_INBOUND_LIQUIDITY_MSAT: u64 = 4000000000; @@ -54,7 +55,7 @@ impl Greenlight { /// If the node is not created, it will register it using the provided partner credentials /// or invite code /// If the node is already registered and an existing credentials were found, it will try to - /// connect to the node using these credentials. + /// connect to the node using these credentials. pub async fn connect( config: Config, seed: Vec, @@ -262,7 +263,7 @@ impl NodeAPI for Greenlight { Ok(client.create_invoice(request).await?.into_inner()) } - // implemenet pull changes from greenlight + // implement pull changes from greenlight async fn pull_changed(&self, since_timestamp: i64) -> Result { info!("pull changed since {}", since_timestamp); let mut client = self.get_client().await?; @@ -281,11 +282,7 @@ impl NodeAPI for Greenlight { .into_inner(); // list both off chain funds and on chain fudns - let funds = client - .list_funds(pb::ListFundsRequest::default()) - .await? - .into_inner(); - let onchain_funds = funds.outputs; + let funds = list_funds(self).await?; // filter only connected peers let connected_peers: Vec = peers @@ -345,34 +342,8 @@ impl NodeAPI for Greenlight { .sum::(); // calculate onchain balance - let onchain_balance = onchain_funds.iter().fold(0, |a, b| { - if b.reserved { - return a; - } - a + amount_to_msat(&b.amount.clone().unwrap_or_default()) - }); - - // Collect utxos from onchain funds - let utxos = onchain_funds - .iter() - .filter_map(|list_funds_output| { - list_funds_output - .output - .as_ref() - .map(|output| UnspentTransactionOutput { - txid: output.txid.clone(), - outnum: output.outnum, - amount_millisatoshi: list_funds_output - .amount - .as_ref() - .map(amount_to_msat) - .unwrap_or_default(), - address: list_funds_output.address.clone(), - reserved: list_funds_output.reserved, - reserved_to_block: list_funds_output.reserved_to_block, - }) - }) - .collect(); + let onchain_balance = self.on_chain_balance(Some(funds.clone())).await?; + let utxos = utxos(self, Some(funds.clone())).await?; // calculate payment limits and inbound liquidity let mut max_payable: u64 = 0; @@ -494,6 +465,48 @@ impl NodeAPI for Greenlight { Ok(client.withdraw(request).await?.into_inner()) } + async fn prepare_withdraw( + &self, + prepare_withdraw_request: PrepareWithdrawRequest, + ) -> Result { + let funds = list_funds(self).await?; + let amount = self.on_chain_balance(Some(funds.clone())).await?; + let utxos = utxos(self, Some(funds)) + .await? + .into_iter() + .map(|it| cln::Outpoint { + txid: it.txid.encode(), + outnum: it.outnum, + }) + .collect(); + debug!("amount: {}", amount); + debug!("utxos: {:?}", utxos); + + let request = cln::TxprepareRequest { + outputs: vec![cln::OutputDesc { + address: prepare_withdraw_request.to_address, + amount: Some(cln::Amount { msat: amount }), + }], + feerate: Some(cln::Feerate { + style: Some(cln::feerate::Style::Perkw( + prepare_withdraw_request.fee_rate_sats_per_vbyte, + )), + }), + minconf: None, + utxos, + }; + + let mut node_client = self.get_node_client().await?; + let response = node_client.tx_prepare(request).await?.into_inner(); + debug!("tx_prepare response: {:?}", response); + debug!("raw_tx_hex: {:?}", String::from_utf8(response.unsigned_tx)?); + return Ok(PrepareWithdrawResponse { + raw_tx_hex: String::new(), //String::from_utf8(response.unsigned_tx)?, + sat_per_vbyte: prepare_withdraw_request.fee_rate_sats_per_vbyte, + fee_sat: 50, + }); + } + async fn start_signer(&self, shutdown: mpsc::Receiver<()>) { _ = self.signer.run_forever(shutdown).await; error!("signer exited"); @@ -682,6 +695,62 @@ impl NodeAPI for Greenlight { fn derive_bip32_key(&self, path: Vec) -> Result { Self::derive_bip32_key(self.sdk_config.network, &self.signer, path) } + + async fn on_chain_balance(&self, funds: Option) -> Result { + let funds = match funds { + Some(f) => f, + None => list_funds(self).await?, + }; + let on_chain_balance = funds.outputs.iter().fold(0, |a, b| { + if b.reserved { + return a; + } + a + amount_to_msat(&b.amount.clone().unwrap_or_default()) + }); + return Ok(on_chain_balance); + } +} + +async fn list_funds(node: &Greenlight) -> Result { + let mut client = node.get_client().await?; + let funds = client + .list_funds(pb::ListFundsRequest::default()) + .await? + .into_inner(); + return Ok(funds); +} + +// Collect utxos from onchain funds +async fn utxos( + node: &Greenlight, + funds: Option, +) -> Result> { + let funds = match funds { + Some(f) => f, + None => list_funds(node).await?, + }; + let utxos: Vec = funds + .outputs + .iter() + .filter_map(|list_funds_output| { + list_funds_output + .output + .as_ref() + .map(|output| UnspentTransactionOutput { + txid: output.txid.clone(), + outnum: output.outnum, + amount_millisatoshi: list_funds_output + .amount + .as_ref() + .map(amount_to_msat) + .unwrap_or_default(), + address: list_funds_output.address.clone(), + reserved: list_funds_output.reserved, + reserved_to_block: list_funds_output.reserved_to_block, + }) + }) + .collect(); + return Ok(utxos); } #[derive(Clone, PartialEq, Eq, Debug, EnumString, Display, Deserialize, Serialize)] diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs index 8c63871e9..eaadf34e5 100644 --- a/libs/sdk-core/src/models.rs +++ b/libs/sdk-core/src/models.rs @@ -8,7 +8,7 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey}; use bitcoin::{Address, Script}; -use gl_client::pb::{Invoice, Peer, WithdrawResponse}; +use gl_client::pb::{Invoice, ListFundsResponse, Peer, WithdrawResponse}; use lightning_invoice::RawInvoice; use ripemd::{Digest, Ripemd160}; use serde::{Deserialize, Serialize}; @@ -47,18 +47,22 @@ pub trait NodeAPI: Send + Sync { &self, bolt11: String, amount_sats: Option, - ) -> Result; + ) -> Result; async fn send_spontaneous_payment( &self, node_id: String, amount_sats: u64, - ) -> Result; + ) -> Result; async fn start(&self) -> Result<()>; async fn sweep( &self, to_address: String, fee_rate_sats_per_vbyte: u64, ) -> Result; + async fn prepare_withdraw( + &self, + prepare_withdraw_request: PrepareWithdrawRequest, + ) -> Result; async fn start_signer(&self, shutdown: mpsc::Receiver<()>); async fn list_peers(&self) -> Result>; async fn connect_peer(&self, node_id: String, addr: String) -> Result<()>; @@ -67,9 +71,9 @@ pub trait NodeAPI: Send + Sync { async fn stream_incoming_payments(&self) -> Result>; async fn stream_log_messages(&self) -> Result>; async fn execute_command(&self, command: String) -> Result; - /// Gets the private key at the path specified fn derive_bip32_key(&self, path: Vec) -> Result; + async fn on_chain_balance(&self, funds: Option) -> Result; } /// Trait covering LSP-related functionality @@ -800,6 +804,25 @@ pub enum BuyBitcoinProvider { Moonpay, } +/// We need to prepare a withdraw transaction to know what fee will be charged in satoshis this +/// model holds the request data which consists of the address to withdraw to and the fee rate in +/// satoshis per vbyte which will be converted to absolute satoshis. +#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] +pub struct PrepareWithdrawRequest { + pub to_address: String, + pub fee_rate_sats_per_vbyte: u32, +} + +/// We need to prepare a withdraw transaction to know what a fee it will be charged in satoshis +/// this model holds the response data, which consists of the raw transaction hex, the fee rate in +/// vbyte and the consolidate fee in satoshis. +#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] +pub struct PrepareWithdrawResponse { + pub raw_tx_hex: String, + pub sat_per_vbyte: u32, + pub fee_sat: u64, +} + impl FromStr for BuyBitcoinProvider { type Err = anyhow::Error; diff --git a/libs/sdk-core/src/test_utils.rs b/libs/sdk-core/src/test_utils.rs index b9981bf6e..338c3b0d4 100644 --- a/libs/sdk-core/src/test_utils.rs +++ b/libs/sdk-core/src/test_utils.rs @@ -10,7 +10,7 @@ use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey}; use bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey}; use bitcoin::Network; use gl_client::pb::amount::Unit; -use gl_client::pb::{Amount, Invoice, Peer, WithdrawResponse}; +use gl_client::pb::{Amount, Invoice, ListFundsResponse, Peer, WithdrawResponse}; use lightning::ln::PaymentSecret; use lightning_invoice::{Currency, InvoiceBuilder, RawInvoice}; use rand::distributions::{Alphanumeric, DistString, Standard}; @@ -30,8 +30,8 @@ use crate::lsp::LspInformation; use crate::models::{FiatAPI, LspAPI, NodeAPI, NodeState, Payment, Swap, SwapperAPI, SyncResponse}; use crate::moonpay::MoonPayApi; use crate::swap::create_submarine_swap_script; -use crate::SwapInfo; -use crate::{parse_invoice, Config, LNInvoice, PaymentResponse, RouteHint}; +use crate::{parse_invoice, Config, LNInvoice, PaymentResponse, PrepareWithdrawRequest, RouteHint}; +use crate::{PrepareWithdrawResponse, SwapInfo}; pub struct MockBackupTransport { pub num_pushed: std::sync::Mutex, @@ -316,6 +316,13 @@ impl NodeAPI for MockNodeAPI { }) } + async fn prepare_withdraw( + &self, + prepare_withdraw_request: PrepareWithdrawRequest, + ) -> Result { + Err(anyhow!("Not implemented")) + } + async fn start_signer(&self, _shutdown: mpsc::Receiver<()>) {} async fn list_peers(&self) -> Result> { @@ -348,6 +355,10 @@ impl NodeAPI for MockNodeAPI { fn derive_bip32_key(&self, _path: Vec) -> Result { Ok(ExtendedPrivKey::new_master(Network::Bitcoin, &[])?) } + + async fn on_chain_balance(&self, funds: Option) -> Result { + Err(anyhow!("Not implemented")) + } } impl MockNodeAPI { diff --git a/libs/sdk-flutter/ios/Classes/bridge_generated.h b/libs/sdk-flutter/ios/Classes/bridge_generated.h index 35a5e0868..9ee5b20ca 100644 --- a/libs/sdk-flutter/ios/Classes/bridge_generated.h +++ b/libs/sdk-flutter/ios/Classes/bridge_generated.h @@ -74,6 +74,11 @@ typedef struct wire_LnUrlAuthRequestData { struct wire_uint_8_list *url; } wire_LnUrlAuthRequestData; +typedef struct wire_PrepareWithdrawRequest { + struct wire_uint_8_list *to_address; + uint32_t fee_rate_sats_per_vbyte; +} wire_PrepareWithdrawRequest; + typedef struct DartCObject *WireSyncReturn; void store_dart_post_cobject(DartPostCObjectFnType ptr); @@ -172,6 +177,9 @@ void wire_sweep(int64_t port_, struct wire_uint_8_list *to_address, uint64_t fee_rate_sats_per_vbyte); +void wire_prepare_withdraw(int64_t port_, + struct wire_PrepareWithdrawRequest *prepare_withdraw_request); + void wire_list_refundables(int64_t port_); void wire_refund(int64_t port_, @@ -205,6 +213,8 @@ struct wire_LnUrlWithdrawRequestData *new_box_autoadd_ln_url_withdraw_request_da struct wire_NodeConfig *new_box_autoadd_node_config_0(void); +struct wire_PrepareWithdrawRequest *new_box_autoadd_prepare_withdraw_request_0(void); + uint64_t *new_box_autoadd_u64_0(uint64_t value); struct wire_uint_8_list *new_uint_8_list_0(int32_t len); @@ -247,6 +257,7 @@ static int64_t dummy_method_to_enforce_bundling(void) { dummy_var ^= ((int64_t) (void*) wire_receive_onchain); dummy_var ^= ((int64_t) (void*) wire_buy_bitcoin); dummy_var ^= ((int64_t) (void*) wire_sweep); + dummy_var ^= ((int64_t) (void*) wire_prepare_withdraw); dummy_var ^= ((int64_t) (void*) wire_list_refundables); dummy_var ^= ((int64_t) (void*) wire_refund); dummy_var ^= ((int64_t) (void*) wire_in_progress_swap); @@ -262,6 +273,7 @@ static int64_t dummy_method_to_enforce_bundling(void) { dummy_var ^= ((int64_t) (void*) new_box_autoadd_ln_url_pay_request_data_0); dummy_var ^= ((int64_t) (void*) new_box_autoadd_ln_url_withdraw_request_data_0); dummy_var ^= ((int64_t) (void*) new_box_autoadd_node_config_0); + dummy_var ^= ((int64_t) (void*) new_box_autoadd_prepare_withdraw_request_0); dummy_var ^= ((int64_t) (void*) new_box_autoadd_u64_0); dummy_var ^= ((int64_t) (void*) new_uint_8_list_0); dummy_var ^= ((int64_t) (void*) inflate_NodeConfig_Greenlight); diff --git a/libs/sdk-flutter/lib/bridge_generated.dart b/libs/sdk-flutter/lib/bridge_generated.dart index 99e934b82..360452ccf 100644 --- a/libs/sdk-flutter/lib/bridge_generated.dart +++ b/libs/sdk-flutter/lib/bridge_generated.dart @@ -185,6 +185,12 @@ abstract class BreezSdkCore { FlutterRustBridgeTaskConstMeta get kSweepConstMeta; + /// See [BreezServices::prepare_withdraw] + Future prepareWithdraw( + {required PrepareWithdrawRequest prepareWithdrawRequest, dynamic hint}); + + FlutterRustBridgeTaskConstMeta get kPrepareWithdrawConstMeta; + /// See [BreezServices::list_refundables] Future> listRefundables({dynamic hint}); @@ -860,6 +866,34 @@ enum PaymentTypeFilter { All, } +/// We need to prepare a withdraw transaction to know what fee will be charged in satoshis this +/// model holds the request data which consists of the address to withdraw to and the fee rate in +/// satoshis per vbyte which will be converted to absolute satoshis. +class PrepareWithdrawRequest { + final String toAddress; + final int feeRateSatsPerVbyte; + + const PrepareWithdrawRequest({ + required this.toAddress, + required this.feeRateSatsPerVbyte, + }); +} + +/// We need to prepare a withdraw transaction to know what a fee it will be charged in satoshis +/// this model holds the response data, which consists of the raw transaction hex, the fee rate in +/// vbyte and the consolidate fee in satoshis. +class PrepareWithdrawResponse { + final String rawTxHex; + final int satPerVbyte; + final int feeSat; + + const PrepareWithdrawResponse({ + required this.rawTxHex, + required this.satPerVbyte, + required this.feeSat, + }); +} + /// Denominator in an exchange rate class Rate { final String coin; @@ -1663,6 +1697,23 @@ class BreezSdkCoreImpl implements BreezSdkCore { argNames: ["toAddress", "feeRateSatsPerVbyte"], ); + Future prepareWithdraw( + {required PrepareWithdrawRequest prepareWithdrawRequest, dynamic hint}) { + var arg0 = _platform.api2wire_box_autoadd_prepare_withdraw_request(prepareWithdrawRequest); + return _platform.executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => _platform.inner.wire_prepare_withdraw(port_, arg0), + parseSuccessData: _wire2api_prepare_withdraw_response, + constMeta: kPrepareWithdrawConstMeta, + argValues: [prepareWithdrawRequest], + hint: hint, + )); + } + + FlutterRustBridgeTaskConstMeta get kPrepareWithdrawConstMeta => const FlutterRustBridgeTaskConstMeta( + debugName: "prepare_withdraw", + argNames: ["prepareWithdrawRequest"], + ); + Future> listRefundables({dynamic hint}) { return _platform.executeNormal(FlutterRustBridgeTask( callFfi: (port_) => _platform.inner.wire_list_refundables(port_), @@ -2425,6 +2476,16 @@ class BreezSdkCoreImpl implements BreezSdkCore { return PaymentType.values[raw as int]; } + PrepareWithdrawResponse _wire2api_prepare_withdraw_response(dynamic raw) { + final arr = raw as List; + if (arr.length != 3) throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + return PrepareWithdrawResponse( + rawTxHex: _wire2api_String(arr[0]), + satPerVbyte: _wire2api_u32(arr[1]), + feeSat: _wire2api_u64(arr[2]), + ); + } + Rate _wire2api_rate(dynamic raw) { final arr = raw as List; if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); @@ -2722,6 +2783,14 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase { return ptr; } + @protected + ffi.Pointer api2wire_box_autoadd_prepare_withdraw_request( + PrepareWithdrawRequest raw) { + final ptr = inner.new_box_autoadd_prepare_withdraw_request_0(); + _api_fill_to_wire_prepare_withdraw_request(raw, ptr.ref); + return ptr; + } + @protected ffi.Pointer api2wire_box_autoadd_u64(int raw) { return inner.new_box_autoadd_u64_0(api2wire_u64(raw)); @@ -2801,6 +2870,11 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase { _api_fill_to_wire_node_config(apiObj, wireObj.ref); } + void _api_fill_to_wire_box_autoadd_prepare_withdraw_request( + PrepareWithdrawRequest apiObj, ffi.Pointer wireObj) { + _api_fill_to_wire_prepare_withdraw_request(apiObj, wireObj.ref); + } + void _api_fill_to_wire_config(Config apiObj, wire_Config wireObj) { wireObj.breezserver = api2wire_String(apiObj.breezserver); wireObj.mempoolspace_url = api2wire_String(apiObj.mempoolspaceUrl); @@ -2867,6 +2941,12 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase { GreenlightCredentials? apiObj, ffi.Pointer wireObj) { if (apiObj != null) _api_fill_to_wire_box_autoadd_greenlight_credentials(apiObj, wireObj); } + + void _api_fill_to_wire_prepare_withdraw_request( + PrepareWithdrawRequest apiObj, wire_PrepareWithdrawRequest wireObj) { + wireObj.to_address = api2wire_String(apiObj.toAddress); + wireObj.fee_rate_sats_per_vbyte = api2wire_u32(apiObj.feeRateSatsPerVbyte); + } } // ignore_for_file: camel_case_types, non_constant_identifier_names, avoid_positional_boolean_parameters, annotate_overrides, constant_identifier_names @@ -3441,6 +3521,22 @@ class BreezSdkCoreWire implements FlutterRustBridgeWireBase { late final _wire_sweep = _wire_sweepPtr.asFunction, int)>(); + void wire_prepare_withdraw( + int port_, + ffi.Pointer prepare_withdraw_request, + ) { + return _wire_prepare_withdraw( + port_, + prepare_withdraw_request, + ); + } + + late final _wire_prepare_withdrawPtr = + _lookup)>>( + 'wire_prepare_withdraw'); + late final _wire_prepare_withdraw = + _wire_prepare_withdrawPtr.asFunction)>(); + void wire_list_refundables( int port_, ) { @@ -3622,6 +3718,16 @@ class BreezSdkCoreWire implements FlutterRustBridgeWireBase { late final _new_box_autoadd_node_config_0 = _new_box_autoadd_node_config_0Ptr.asFunction Function()>(); + ffi.Pointer new_box_autoadd_prepare_withdraw_request_0() { + return _new_box_autoadd_prepare_withdraw_request_0(); + } + + late final _new_box_autoadd_prepare_withdraw_request_0Ptr = + _lookup Function()>>( + 'new_box_autoadd_prepare_withdraw_request_0'); + late final _new_box_autoadd_prepare_withdraw_request_0 = _new_box_autoadd_prepare_withdraw_request_0Ptr + .asFunction Function()>(); + ffi.Pointer new_box_autoadd_u64_0( int value, ) { @@ -3772,6 +3878,13 @@ class wire_LnUrlAuthRequestData extends ffi.Struct { external ffi.Pointer url; } +class wire_PrepareWithdrawRequest extends ffi.Struct { + external ffi.Pointer to_address; + + @ffi.Uint32() + external int fee_rate_sats_per_vbyte; +} + typedef DartPostCObjectFnType = ffi.Pointer message)>>; typedef DartPort = ffi.Int64; diff --git a/tools/sdk-cli/src/command_handlers.rs b/tools/sdk-cli/src/command_handlers.rs index b2a172809..da995e7b9 100644 --- a/tools/sdk-cli/src/command_handlers.rs +++ b/tools/sdk-cli/src/command_handlers.rs @@ -5,6 +5,7 @@ use anyhow::{anyhow, Error, Result}; use breez_sdk_core::InputType::{LnUrlAuth, LnUrlPay, LnUrlWithdraw}; use breez_sdk_core::{ parse, BreezEvent, BreezServices, EventListener, GreenlightCredentials, PaymentTypeFilter, + PrepareWithdrawRequest, }; use breez_sdk_core::{Config, GreenlightNodeConfig, NodeConfig}; use once_cell::sync::OnceCell; @@ -92,7 +93,7 @@ pub(crate) async fn handle_command( } Commands::Sync {} => { sdk()?.sync().await?; - Ok("Sync finished succesfully".to_string()) + Ok("Sync finished successfully".to_string()) } Commands::Parse { input } => parse(&input) .await @@ -167,7 +168,22 @@ pub(crate) async fn handle_command( sat_per_byte, } => { sdk()?.sweep(to_address, sat_per_byte).await?; - Ok("Onchain funds were swept succesfully".to_string()) + Ok("Onchain funds were swept successfully".to_string()) + } + Commands::PrepareWithdraw { + to_address, + fee_rate_sats_per_vbyte, + } => { + let response = sdk()? + .prepare_withdraw(PrepareWithdrawRequest { + to_address, + fee_rate_sats_per_vbyte, + }) + .await?; + Ok(format!( + "PrepareWithdraw ran successfully with response: {:?}", + response + )) } Commands::ListLsps {} => { let lsps = sdk()?.list_lsps().await?; @@ -175,7 +191,7 @@ pub(crate) async fn handle_command( } Commands::ConnectLSP { lsp_id } => { sdk()?.connect_lsp(lsp_id).await?; - Ok("LSP connected succesfully".to_string()) + Ok("LSP connected successfully".to_string()) } Commands::NodeInfo {} => { serde_json::to_string_pretty(&sdk()?.node_info()?).map_err(|e| e.into()) @@ -295,7 +311,7 @@ pub(crate) async fn handle_command( } Commands::Backup {} => { sdk().unwrap().backup().await?; - Ok("Backup completed succesfully".into()) + Ok("Backup completed successfully".into()) } } } diff --git a/tools/sdk-cli/src/commands.rs b/tools/sdk-cli/src/commands.rs index c875c1a28..86ba26d30 100644 --- a/tools/sdk-cli/src/commands.rs +++ b/tools/sdk-cli/src/commands.rs @@ -100,6 +100,15 @@ pub(crate) enum Commands { sat_per_byte: u64, }, + /// Calculate the fee (in sats) for a potential transaction + PrepareWithdraw { + /// The destination address + to_address: String, + + /// The fee rate for the transaction in vbyte/sats + fee_rate_sats_per_vbyte: u32, + }, + /// List available LSPs ListLsps {},