prepare-pay-onchain: add option for drain (#464)

* prepare-pay-onchain: add option for drain in req

* Fix clippy

* ChainSwapStateHandler: gracefully handle building both drain and non-drain lockups

* Send Chain swap: use standard feerate when estimating lockup tx fee

* UDL: move new drain field above the last PreparePayOnchainRequest optional field

* UDL: move new drain field optional

* prepare-pay-onchain: treat normal payment as drain if receiver amount is high enough

If the receiver amount is as high as it would be in case of drain, treat the current prepare-pay-onchain as drain, even if the drain flag is not set.

* build_drain_tx: add optional amount validation

* Add PayOnchainAmount enum to cover amount types (drain, receiver)

* Add ability to find max_receiver_amount_sat for non-drain sends

* Revert "Add ability to find max_receiver_amount_sat for non-drain sends"

This reverts commit 60ee1c768021810f72bc64a8ada69d35b638185e.

* prepare_pay_onchain: treat drain and non-drain cases separately

If the non-drain case is chosen with a receiver_amount equivalent to what drain would have calculated, it results in an error. For drain, the caller has to explicitly choose PayOnchainAmount::Drain.

* CLI: send-onchain-payment accepts optional amount

* CLI: add docs for send-onchain-payment drain arg

* SDK: expand docs for prepare_pay_onchain

* Re-generate RN bindings

* Re-generate flutter bindings
This commit is contained in:
ok300
2024-09-11 15:52:56 +00:00
committed by GitHub
parent d42e37ce1e
commit e8cd66f81f
17 changed files with 749 additions and 72 deletions

View File

@@ -47,10 +47,14 @@ pub(crate) enum Command {
/// Btc onchain address to send to /// Btc onchain address to send to
address: String, address: String,
/// Amount that will be received, in satoshi /// Amount that will be received, in satoshi. Must be set if `drain` is false or unset.
receiver_amount_sat: u64, receiver_amount_sat: Option<u64>,
// The optional fee rate to use, in satoshi/vbyte /// Whether or not this is a drain operation. If true, all available funds will be used.
#[arg(short, long)]
drain: Option<bool>,
/// The optional fee rate to use, in satoshi/vbyte
#[clap(short = 'f', long = "fee_rate")] #[clap(short = 'f', long = "fee_rate")]
sat_per_vbyte: Option<u32>, sat_per_vbyte: Option<u32>,
}, },
@@ -341,19 +345,28 @@ pub(crate) async fn handle_command(
Command::SendOnchainPayment { Command::SendOnchainPayment {
address, address,
receiver_amount_sat, receiver_amount_sat,
drain,
sat_per_vbyte, sat_per_vbyte,
} => { } => {
let amount = match drain.unwrap_or(false) {
true => PayOnchainAmount::Drain,
false => PayOnchainAmount::Receiver {
amount_sat: receiver_amount_sat.ok_or(anyhow::anyhow!(
"Must specify `receiver_amount_sat` if not draining"
))?,
},
};
let prepare_response = sdk let prepare_response = sdk
.prepare_pay_onchain(&PreparePayOnchainRequest { .prepare_pay_onchain(&PreparePayOnchainRequest {
receiver_amount_sat, amount,
sat_per_vbyte, sat_per_vbyte,
}) })
.await?; .await?;
wait_confirmation!( wait_confirmation!(
format!( format!(
"Fees: {} sat (incl claim fee: {} sat). Are the fees acceptable? (y/N) ", "Fees: {} sat (incl claim fee: {} sat). Receiver amount: {} sat. Are the fees acceptable? (y/N) ",
prepare_response.total_fees_sat, prepare_response.claim_fees_sat prepare_response.total_fees_sat, prepare_response.claim_fees_sat, prepare_response.receiver_amount_sat
), ),
"Payment send halted" "Payment send halted"
); );

View File

@@ -133,8 +133,21 @@ typedef struct wire_cst_prepare_buy_bitcoin_request {
uint64_t amount_sat; uint64_t amount_sat;
} wire_cst_prepare_buy_bitcoin_request; } wire_cst_prepare_buy_bitcoin_request;
typedef struct wire_cst_PayOnchainAmount_Receiver {
uint64_t amount_sat;
} wire_cst_PayOnchainAmount_Receiver;
typedef union PayOnchainAmountKind {
struct wire_cst_PayOnchainAmount_Receiver Receiver;
} PayOnchainAmountKind;
typedef struct wire_cst_pay_onchain_amount {
int32_t tag;
union PayOnchainAmountKind kind;
} wire_cst_pay_onchain_amount;
typedef struct wire_cst_prepare_pay_onchain_request { typedef struct wire_cst_prepare_pay_onchain_request {
uint64_t receiver_amount_sat; struct wire_cst_pay_onchain_amount amount;
uint32_t *sat_per_vbyte; uint32_t *sat_per_vbyte;
} wire_cst_prepare_pay_onchain_request; } wire_cst_prepare_pay_onchain_request;

View File

@@ -410,8 +410,14 @@ dictionary OnchainPaymentLimitsResponse {
Limits receive; Limits receive;
}; };
[Enum]
interface PayOnchainAmount {
Receiver(u64 amount_sat);
Drain();
};
dictionary PreparePayOnchainRequest { dictionary PreparePayOnchainRequest {
u64 receiver_amount_sat; PayOnchainAmount amount;
u32? sat_per_vbyte = null; u32? sat_per_vbyte = null;
}; };

View File

@@ -582,14 +582,28 @@ impl ChainSwapStateHandler {
lockup_details.amount, lockup_details.lockup_address lockup_details.amount, lockup_details.lockup_address
); );
let lockup_tx = self let lockup_tx = match self
.onchain_wallet .onchain_wallet
.build_tx( .build_tx(
None, None,
&lockup_details.lockup_address, &lockup_details.lockup_address,
lockup_details.amount as u64, lockup_details.amount as u64,
) )
.await?; .await
{
Err(PaymentError::InsufficientFunds) => {
warn!("Cannot build normal lockup tx due to insufficient funds, attempting to build drain tx");
self.onchain_wallet
.build_drain_tx(
None,
&lockup_details.lockup_address,
Some(lockup_details.amount as u64),
)
.await
}
Err(e) => Err(e),
Ok(lockup_tx) => Ok(lockup_tx),
}?;
let lockup_tx_id = self let lockup_tx_id = self
.liquid_chain_service .liquid_chain_service

View File

@@ -3062,6 +3062,27 @@ impl SseDecode for Option<Vec<crate::model::PaymentType>> {
} }
} }
impl SseDecode for crate::model::PayOnchainAmount {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut tag_ = <i32>::sse_decode(deserializer);
match tag_ {
0 => {
let mut var_amountSat = <u64>::sse_decode(deserializer);
return crate::model::PayOnchainAmount::Receiver {
amount_sat: var_amountSat,
};
}
1 => {
return crate::model::PayOnchainAmount::Drain;
}
_ => {
unimplemented!("");
}
}
}
}
impl SseDecode for crate::model::PayOnchainRequest { impl SseDecode for crate::model::PayOnchainRequest {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -3307,10 +3328,10 @@ impl SseDecode for crate::model::PrepareBuyBitcoinResponse {
impl SseDecode for crate::model::PreparePayOnchainRequest { impl SseDecode for crate::model::PreparePayOnchainRequest {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_receiverAmountSat = <u64>::sse_decode(deserializer); let mut var_amount = <crate::model::PayOnchainAmount>::sse_decode(deserializer);
let mut var_satPerVbyte = <Option<u32>>::sse_decode(deserializer); let mut var_satPerVbyte = <Option<u32>>::sse_decode(deserializer);
return crate::model::PreparePayOnchainRequest { return crate::model::PreparePayOnchainRequest {
receiver_amount_sat: var_receiverAmountSat, amount: var_amount,
sat_per_vbyte: var_satPerVbyte, sat_per_vbyte: var_satPerVbyte,
}; };
} }
@@ -4828,6 +4849,31 @@ impl flutter_rust_bridge::IntoIntoDart<crate::model::OnchainPaymentLimitsRespons
} }
} }
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::model::PayOnchainAmount {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
match self {
crate::model::PayOnchainAmount::Receiver { amount_sat } => {
[0.into_dart(), amount_sat.into_into_dart().into_dart()].into_dart()
}
crate::model::PayOnchainAmount::Drain => [1.into_dart()].into_dart(),
_ => {
unimplemented!("");
}
}
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::model::PayOnchainAmount
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::model::PayOnchainAmount>
for crate::model::PayOnchainAmount
{
fn into_into_dart(self) -> crate::model::PayOnchainAmount {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::model::PayOnchainRequest { impl flutter_rust_bridge::IntoDart for crate::model::PayOnchainRequest {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[ [
@@ -5090,7 +5136,7 @@ impl flutter_rust_bridge::IntoIntoDart<crate::model::PrepareBuyBitcoinResponse>
impl flutter_rust_bridge::IntoDart for crate::model::PreparePayOnchainRequest { impl flutter_rust_bridge::IntoDart for crate::model::PreparePayOnchainRequest {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[ [
self.receiver_amount_sat.into_into_dart().into_dart(), self.amount.into_into_dart().into_dart(),
self.sat_per_vbyte.into_into_dart().into_dart(), self.sat_per_vbyte.into_into_dart().into_dart(),
] ]
.into_dart() .into_dart()
@@ -6532,6 +6578,24 @@ impl SseEncode for Option<Vec<crate::model::PaymentType>> {
} }
} }
impl SseEncode for crate::model::PayOnchainAmount {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
match self {
crate::model::PayOnchainAmount::Receiver { amount_sat } => {
<i32>::sse_encode(0, serializer);
<u64>::sse_encode(amount_sat, serializer);
}
crate::model::PayOnchainAmount::Drain => {
<i32>::sse_encode(1, serializer);
}
_ => {
unimplemented!("");
}
}
}
}
impl SseEncode for crate::model::PayOnchainRequest { impl SseEncode for crate::model::PayOnchainRequest {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -6760,7 +6824,7 @@ impl SseEncode for crate::model::PrepareBuyBitcoinResponse {
impl SseEncode for crate::model::PreparePayOnchainRequest { impl SseEncode for crate::model::PreparePayOnchainRequest {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<u64>::sse_encode(self.receiver_amount_sat, serializer); <crate::model::PayOnchainAmount>::sse_encode(self.amount, serializer);
<Option<u32>>::sse_encode(self.sat_per_vbyte, serializer); <Option<u32>>::sse_encode(self.sat_per_vbyte, serializer);
} }
} }
@@ -8227,6 +8291,21 @@ mod io {
} }
} }
} }
impl CstDecode<crate::model::PayOnchainAmount> for wire_cst_pay_onchain_amount {
// Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::model::PayOnchainAmount {
match self.tag {
0 => {
let ans = unsafe { self.kind.Receiver };
crate::model::PayOnchainAmount::Receiver {
amount_sat: ans.amount_sat.cst_decode(),
}
}
1 => crate::model::PayOnchainAmount::Drain,
_ => unreachable!(),
}
}
}
impl CstDecode<crate::model::PayOnchainRequest> for wire_cst_pay_onchain_request { impl CstDecode<crate::model::PayOnchainRequest> for wire_cst_pay_onchain_request {
// Codec=Cst (C-struct based), see doc to use other codecs // Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::model::PayOnchainRequest { fn cst_decode(self) -> crate::model::PayOnchainRequest {
@@ -8389,7 +8468,7 @@ mod io {
// Codec=Cst (C-struct based), see doc to use other codecs // Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::model::PreparePayOnchainRequest { fn cst_decode(self) -> crate::model::PreparePayOnchainRequest {
crate::model::PreparePayOnchainRequest { crate::model::PreparePayOnchainRequest {
receiver_amount_sat: self.receiver_amount_sat.cst_decode(), amount: self.amount.cst_decode(),
sat_per_vbyte: self.sat_per_vbyte.cst_decode(), sat_per_vbyte: self.sat_per_vbyte.cst_decode(),
} }
} }
@@ -9277,6 +9356,19 @@ mod io {
Self::new_with_null_ptr() Self::new_with_null_ptr()
} }
} }
impl NewWithNullPtr for wire_cst_pay_onchain_amount {
fn new_with_null_ptr() -> Self {
Self {
tag: -1,
kind: PayOnchainAmountKind { nil__: () },
}
}
}
impl Default for wire_cst_pay_onchain_amount {
fn default() -> Self {
Self::new_with_null_ptr()
}
}
impl NewWithNullPtr for wire_cst_pay_onchain_request { impl NewWithNullPtr for wire_cst_pay_onchain_request {
fn new_with_null_ptr() -> Self { fn new_with_null_ptr() -> Self {
Self { Self {
@@ -9365,7 +9457,7 @@ mod io {
impl NewWithNullPtr for wire_cst_prepare_pay_onchain_request { impl NewWithNullPtr for wire_cst_prepare_pay_onchain_request {
fn new_with_null_ptr() -> Self { fn new_with_null_ptr() -> Self {
Self { Self {
receiver_amount_sat: Default::default(), amount: Default::default(),
sat_per_vbyte: core::ptr::null_mut(), sat_per_vbyte: core::ptr::null_mut(),
} }
} }
@@ -11123,6 +11215,23 @@ mod io {
} }
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct wire_cst_pay_onchain_amount {
tag: i32,
kind: PayOnchainAmountKind,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union PayOnchainAmountKind {
Receiver: wire_cst_PayOnchainAmount_Receiver,
nil__: (),
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct wire_cst_PayOnchainAmount_Receiver {
amount_sat: u64,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct wire_cst_pay_onchain_request { pub struct wire_cst_pay_onchain_request {
address: *mut wire_cst_list_prim_u_8_strict, address: *mut wire_cst_list_prim_u_8_strict,
prepare_response: wire_cst_prepare_pay_onchain_response, prepare_response: wire_cst_prepare_pay_onchain_response,
@@ -11265,7 +11374,7 @@ mod io {
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct wire_cst_prepare_pay_onchain_request { pub struct wire_cst_prepare_pay_onchain_request {
receiver_amount_sat: u64, amount: wire_cst_pay_onchain_amount,
sat_per_vbyte: *mut u32, sat_per_vbyte: *mut u32,
} }
#[repr(C)] #[repr(C)]

View File

@@ -307,10 +307,19 @@ pub struct SendPaymentResponse {
pub payment: Payment, pub payment: Payment,
} }
#[derive(Debug, Serialize, Clone)]
pub enum PayOnchainAmount {
/// The amount in satoshi that will be received
Receiver { amount_sat: u64 },
/// Indicates that all available funds should be sent
Drain,
}
/// An argument when calling [crate::sdk::LiquidSdk::prepare_pay_onchain]. /// An argument when calling [crate::sdk::LiquidSdk::prepare_pay_onchain].
#[derive(Debug, Serialize, Clone)] #[derive(Debug, Serialize, Clone)]
pub struct PreparePayOnchainRequest { pub struct PreparePayOnchainRequest {
pub receiver_amount_sat: u64, pub amount: PayOnchainAmount,
/// The optional fee rate of the Bitcoin claim transaction. Defaults to the swapper estimated claim fee.
pub sat_per_vbyte: Option<u32>, pub sat_per_vbyte: Option<u32>,
} }

View File

@@ -688,16 +688,24 @@ impl LiquidSdk {
.sum()) .sum())
} }
async fn estimate_lockup_tx_fee(&self, amount_sat: u64) -> Result<u64, PaymentError> { fn get_temp_p2tr_addr(&self) -> &str {
// TODO Replace this with own address when LWK supports taproot // TODO Replace this with own address when LWK supports taproot
// https://github.com/Blockstream/lwk/issues/31 // https://github.com/Blockstream/lwk/issues/31
let temp_p2tr_addr = match self.config.network { match self.config.network {
LiquidNetwork::Mainnet => "lq1pqvzxvqhrf54dd4sny4cag7497pe38252qefk46t92frs7us8r80ja9ha8r5me09nn22m4tmdqp5p4wafq3s59cql3v9n45t5trwtxrmxfsyxjnstkctj", LiquidNetwork::Mainnet => "lq1pqvzxvqhrf54dd4sny4cag7497pe38252qefk46t92frs7us8r80ja9ha8r5me09nn22m4tmdqp5p4wafq3s59cql3v9n45t5trwtxrmxfsyxjnstkctj",
LiquidNetwork::Testnet => "tlq1pq0wqu32e2xacxeyps22x8gjre4qk3u6r70pj4r62hzczxeyz8x3yxucrpn79zy28plc4x37aaf33kwt6dz2nn6gtkya6h02mwpzy4eh69zzexq7cf5y5" LiquidNetwork::Testnet => "tlq1pq0wqu32e2xacxeyps22x8gjre4qk3u6r70pj4r62hzczxeyz8x3yxucrpn79zy28plc4x37aaf33kwt6dz2nn6gtkya6h02mwpzy4eh69zzexq7cf5y5"
}; }
}
/// Estimate the lockup tx fee for Send swaps
async fn estimate_lockup_tx_fee_send(
&self,
user_lockup_amount_sat: u64,
) -> Result<u64, PaymentError> {
let temp_p2tr_addr = self.get_temp_p2tr_addr();
self.estimate_onchain_tx_fee( self.estimate_onchain_tx_fee(
amount_sat, user_lockup_amount_sat,
temp_p2tr_addr, temp_p2tr_addr,
self.config self.config
.lowball_fee_rate_msat_per_vbyte() .lowball_fee_rate_msat_per_vbyte()
@@ -706,6 +714,31 @@ impl LiquidSdk {
.await .await
} }
/// Estimate the lockup tx fee for Chain Send swaps
async fn estimate_lockup_tx_fee_chain_send(
&self,
user_lockup_amount_sat: u64,
) -> Result<u64, PaymentError> {
let temp_p2tr_addr = self.get_temp_p2tr_addr();
self.estimate_onchain_tx_fee(user_lockup_amount_sat, temp_p2tr_addr, None)
.await
}
async fn estimate_drain_tx_fee(&self) -> Result<u64, PaymentError> {
let temp_p2tr_addr = self.get_temp_p2tr_addr();
let fee_sat = self
.onchain_wallet
.build_drain_tx(None, temp_p2tr_addr, None)
.await?
.all_fees()
.values()
.sum();
Ok(fee_sat)
}
/// Prepares to pay a Lightning invoice via a submarine swap. /// Prepares to pay a Lightning invoice via a submarine swap.
/// ///
/// # Arguments /// # Arguments
@@ -792,7 +825,7 @@ impl LiquidSdk {
None => { None => {
let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat); let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
let lockup_fees_sat = self let lockup_fees_sat = self
.estimate_lockup_tx_fee(receiver_amount_sat + boltz_fees_total) .estimate_lockup_tx_fee_send(receiver_amount_sat + boltz_fees_total)
.await?; .await?;
boltz_fees_total + lockup_fees_sat boltz_fees_total + lockup_fees_sat
} }
@@ -1002,7 +1035,7 @@ impl LiquidSdk {
let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat); let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
let lockup_tx_fees_sat = self let lockup_tx_fees_sat = self
.estimate_lockup_tx_fee(receiver_amount_sat + boltz_fees_total) .estimate_lockup_tx_fee_send(receiver_amount_sat + boltz_fees_total)
.await?; .await?;
ensure_sdk!( ensure_sdk!(
fees_sat == boltz_fees_total + lockup_tx_fees_sat, fees_sat == boltz_fees_total + lockup_tx_fees_sat,
@@ -1142,7 +1175,8 @@ impl LiquidSdk {
/// # Arguments /// # Arguments
/// ///
/// * `req` - the [PreparePayOnchainRequest] containing: /// * `req` - the [PreparePayOnchainRequest] containing:
/// * `receiver_amount_sat` - the amount in satoshi that will be received /// * `amount` - which can be of two types: [PayOnchainAmount::Drain], which uses all funds,
/// and [PayOnchainAmount::Receiver], which sets the amount the receiver should receive
/// * `sat_per_vbyte` - the optional fee rate of the Bitcoin claim transaction. Defaults to the swapper estimated claim fee /// * `sat_per_vbyte` - the optional fee rate of the Bitcoin claim transaction. Defaults to the swapper estimated claim fee
pub async fn prepare_pay_onchain( pub async fn prepare_pay_onchain(
&self, &self,
@@ -1150,7 +1184,7 @@ impl LiquidSdk {
) -> Result<PreparePayOnchainResponse, PaymentError> { ) -> Result<PreparePayOnchainResponse, PaymentError> {
self.ensure_is_started().await?; self.ensure_is_started().await?;
let receiver_amount_sat = req.receiver_amount_sat; let balance_sat = self.get_info().await?.balance_sat;
let pair = self.get_chain_pair(Direction::Outgoing)?; let pair = self.get_chain_pair(Direction::Outgoing)?;
let claim_fees_sat = match req.sat_per_vbyte { let claim_fees_sat = match req.sat_per_vbyte {
Some(sat_per_vbyte) => ESTIMATED_BTC_CLAIM_TX_VSIZE * sat_per_vbyte as u64, Some(sat_per_vbyte) => ESTIMATED_BTC_CLAIM_TX_VSIZE * sat_per_vbyte as u64,
@@ -1158,22 +1192,57 @@ impl LiquidSdk {
}; };
let server_fees_sat = pair.fees.server(); let server_fees_sat = pair.fees.server();
let (payer_amount_sat, receiver_amount_sat, total_fees_sat) = match req.amount {
PayOnchainAmount::Receiver { amount_sat } => {
let receiver_amount_sat = amount_sat;
let user_lockup_amount_sat_without_service_fee = let user_lockup_amount_sat_without_service_fee =
receiver_amount_sat + claim_fees_sat + server_fees_sat; receiver_amount_sat + claim_fees_sat + server_fees_sat;
let boltz_fees_sat = pair.fees.boltz(user_lockup_amount_sat_without_service_fee);
let user_lockup_amount_sat = user_lockup_amount_sat_without_service_fee + boltz_fees_sat; // The resulting invoice amount contains the service fee, which is rounded up with ceil()
// Therefore, when calculating the user_lockup amount, we must also round it up with ceil()
let user_lockup_amount_sat = (user_lockup_amount_sat_without_service_fee as f64
* 100.0
/ (100.0 - pair.fees.percentage))
.ceil() as u64;
self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?; self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
let lockup_fees_sat = self.estimate_lockup_tx_fee(user_lockup_amount_sat).await?;
let lockup_fees_sat = self
.estimate_lockup_tx_fee_chain_send(user_lockup_amount_sat)
.await?;
let boltz_fees_sat =
user_lockup_amount_sat - user_lockup_amount_sat_without_service_fee;
let total_fees_sat =
boltz_fees_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat;
let payer_amount_sat = receiver_amount_sat + total_fees_sat;
(payer_amount_sat, receiver_amount_sat, total_fees_sat)
}
PayOnchainAmount::Drain => {
let payer_amount_sat = balance_sat;
let lockup_fees_sat = self.estimate_drain_tx_fee().await?;
let user_lockup_amount_sat = payer_amount_sat - lockup_fees_sat;
self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
let boltz_fees_sat = pair.fees.boltz(user_lockup_amount_sat);
let total_fees_sat =
boltz_fees_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat;
let receiver_amount_sat = payer_amount_sat - total_fees_sat;
(payer_amount_sat, receiver_amount_sat, total_fees_sat)
}
};
let res = PreparePayOnchainResponse { let res = PreparePayOnchainResponse {
receiver_amount_sat, receiver_amount_sat,
claim_fees_sat, claim_fees_sat,
total_fees_sat: boltz_fees_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat, total_fees_sat,
}; };
let payer_amount_sat = res.receiver_amount_sat + res.total_fees_sat;
ensure_sdk!( ensure_sdk!(
payer_amount_sat <= self.get_info().await?.balance_sat, payer_amount_sat <= balance_sat,
PaymentError::InsufficientFunds PaymentError::InsufficientFunds
); );
@@ -1202,6 +1271,7 @@ impl LiquidSdk {
) -> Result<SendPaymentResponse, PaymentError> { ) -> Result<SendPaymentResponse, PaymentError> {
self.ensure_is_started().await?; self.ensure_is_started().await?;
let balance_sat = self.get_info().await?.balance_sat;
let receiver_amount_sat = req.prepare_response.receiver_amount_sat; let receiver_amount_sat = req.prepare_response.receiver_amount_sat;
let pair = self.get_chain_pair(Direction::Outgoing)?; let pair = self.get_chain_pair(Direction::Outgoing)?;
let claim_fees_sat = req.prepare_response.claim_fees_sat; let claim_fees_sat = req.prepare_response.claim_fees_sat;
@@ -1210,10 +1280,24 @@ impl LiquidSdk {
let user_lockup_amount_sat_without_service_fee = let user_lockup_amount_sat_without_service_fee =
receiver_amount_sat + claim_fees_sat + server_fees_sat; receiver_amount_sat + claim_fees_sat + server_fees_sat;
let boltz_fee_sat = pair.fees.boltz(user_lockup_amount_sat_without_service_fee);
let user_lockup_amount_sat = user_lockup_amount_sat_without_service_fee + boltz_fee_sat; // The resulting invoice amount contains the service fee, which is rounded up with ceil()
// Therefore, when calculating the user_lockup amount, we must also round it up with ceil()
let user_lockup_amount_sat = (user_lockup_amount_sat_without_service_fee as f64 * 100.0
/ (100.0 - pair.fees.percentage))
.ceil() as u64;
let boltz_fee_sat = user_lockup_amount_sat - user_lockup_amount_sat_without_service_fee;
self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?; self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
let lockup_fees_sat = self.estimate_lockup_tx_fee(user_lockup_amount_sat).await?;
let payer_amount_sat = req.prepare_response.total_fees_sat + receiver_amount_sat;
let lockup_fees_sat = match payer_amount_sat == balance_sat {
true => self.estimate_drain_tx_fee().await?,
false => {
self.estimate_lockup_tx_fee_chain_send(user_lockup_amount_sat)
.await?
}
};
ensure_sdk!( ensure_sdk!(
req.prepare_response.total_fees_sat req.prepare_response.total_fees_sat
@@ -1221,9 +1305,8 @@ impl LiquidSdk {
PaymentError::InvalidOrExpiredFees PaymentError::InvalidOrExpiredFees
); );
let payer_amount_sat = req.prepare_response.total_fees_sat + receiver_amount_sat;
ensure_sdk!( ensure_sdk!(
payer_amount_sat <= self.get_info().await?.balance_sat, payer_amount_sat <= balance_sat,
PaymentError::InsufficientFunds PaymentError::InsufficientFunds
); );
@@ -1623,17 +1706,17 @@ impl LiquidSdk {
}) })
} }
async fn create_chain_swap( async fn create_receive_chain_swap(
&self, &self,
payer_amount_sat: u64, user_lockup_amount_sat: u64,
fees_sat: u64, fees_sat: u64,
) -> Result<ChainSwap, PaymentError> { ) -> Result<ChainSwap, PaymentError> {
let pair = self.get_and_validate_chain_pair(Direction::Incoming, payer_amount_sat)?; let pair = self.get_and_validate_chain_pair(Direction::Incoming, user_lockup_amount_sat)?;
let claim_fees_sat = pair.fees.claim_estimate(); let claim_fees_sat = pair.fees.claim_estimate();
let server_fees_sat = pair.fees.server(); let server_fees_sat = pair.fees.server();
ensure_sdk!( ensure_sdk!(
fees_sat == pair.fees.boltz(payer_amount_sat) + claim_fees_sat + server_fees_sat, fees_sat == pair.fees.boltz(user_lockup_amount_sat) + claim_fees_sat + server_fees_sat,
PaymentError::InvalidOrExpiredFees PaymentError::InvalidOrExpiredFees
); );
@@ -1665,7 +1748,7 @@ impl LiquidSdk {
preimage_hash: preimage.sha256, preimage_hash: preimage.sha256,
claim_public_key: Some(claim_public_key), claim_public_key: Some(claim_public_key),
refund_public_key: Some(refund_public_key), refund_public_key: Some(refund_public_key),
user_lock_amount: Some(payer_amount_sat as u32), // TODO update our model user_lock_amount: Some(user_lockup_amount_sat as u32), // TODO update our model
server_lock_amount: None, server_lock_amount: None,
pair_hash: Some(pair.hash), pair_hash: Some(pair.hash),
referral_id: None, referral_id: None,
@@ -1676,8 +1759,8 @@ impl LiquidSdk {
let create_response_json = let create_response_json =
ChainSwap::from_boltz_struct_to_json(&create_response, &swap_id)?; ChainSwap::from_boltz_struct_to_json(&create_response, &swap_id)?;
let accept_zero_conf = payer_amount_sat <= pair.limits.maximal_zero_conf; let accept_zero_conf = user_lockup_amount_sat <= pair.limits.maximal_zero_conf;
let receiver_amount_sat = payer_amount_sat - fees_sat; let receiver_amount_sat = user_lockup_amount_sat - fees_sat;
let claim_address = self.onchain_wallet.next_unused_address().await?.to_string(); let claim_address = self.onchain_wallet.next_unused_address().await?.to_string();
let swap = ChainSwap { let swap = ChainSwap {
@@ -1688,7 +1771,7 @@ impl LiquidSdk {
timeout_block_height: create_response.lockup_details.timeout_block_height, timeout_block_height: create_response.lockup_details.timeout_block_height,
preimage: preimage_str, preimage: preimage_str,
description: Some("Bitcoin transfer".to_string()), description: Some("Bitcoin transfer".to_string()),
payer_amount_sat, payer_amount_sat: user_lockup_amount_sat,
receiver_amount_sat, receiver_amount_sat,
claim_fees_sat, claim_fees_sat,
accept_zero_conf, accept_zero_conf,
@@ -1715,7 +1798,9 @@ impl LiquidSdk {
) -> Result<ReceivePaymentResponse, PaymentError> { ) -> Result<ReceivePaymentResponse, PaymentError> {
self.ensure_is_started().await?; self.ensure_is_started().await?;
let swap = self.create_chain_swap(payer_amount_sat, fees_sat).await?; let swap = self
.create_receive_chain_swap(payer_amount_sat, fees_sat)
.await?;
let create_response = swap.get_boltz_create_response()?; let create_response = swap.get_boltz_create_response()?;
let address = create_response.lockup_details.lockup_address; let address = create_response.lockup_details.lockup_address;
@@ -1836,7 +1921,7 @@ impl LiquidSdk {
/// * `redirect_url` - the optional redirect URL the provider should redirect to after purchase /// * `redirect_url` - the optional redirect URL the provider should redirect to after purchase
pub async fn buy_bitcoin(&self, req: &BuyBitcoinRequest) -> Result<String, PaymentError> { pub async fn buy_bitcoin(&self, req: &BuyBitcoinRequest) -> Result<String, PaymentError> {
let swap = self let swap = self
.create_chain_swap( .create_receive_chain_swap(
req.prepare_response.amount_sat, req.prepare_response.amount_sat,
req.prepare_response.fees_sat, req.prepare_response.fees_sat,
) )

View File

@@ -40,6 +40,15 @@ impl OnchainWallet for MockWallet {
Ok(TEST_LIQUID_TX.clone()) Ok(TEST_LIQUID_TX.clone())
} }
async fn build_drain_tx(
&self,
_fee_rate_sats_per_kvb: Option<f32>,
_recipient_address: &str,
_enforce_amount_sat: Option<u64>,
) -> Result<Transaction, PaymentError> {
Ok(TEST_LIQUID_TX.clone())
}
async fn next_unused_address(&self) -> Result<Address, PaymentError> { async fn next_unused_address(&self) -> Result<Address, PaymentError> {
Ok(TEST_P2TR_ADDR.clone()) Ok(TEST_P2TR_ADDR.clone())
} }

View File

@@ -19,6 +19,7 @@ use sdk_common::lightning::util::message_signing::verify;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::{ use crate::{
ensure_sdk,
error::PaymentError, error::PaymentError,
model::{Config, LiquidNetwork}, model::{Config, LiquidNetwork},
}; };
@@ -38,6 +39,20 @@ pub trait OnchainWallet: Send + Sync {
amount_sat: u64, amount_sat: u64,
) -> Result<Transaction, PaymentError>; ) -> Result<Transaction, PaymentError>;
/// Builds a drain tx.
///
/// ### Arguments
/// - `fee_rate_sats_per_kvb`: custom drain tx feerate
/// - `recipient_address`: drain tx recipient
/// - `enforce_amount_sat`: if set, the drain tx will only be built if the amount transferred is
/// this amount, otherwise it will fail with a validation error
async fn build_drain_tx(
&self,
fee_rate_sats_per_kvb: Option<f32>,
recipient_address: &str,
enforce_amount_sat: Option<u64>,
) -> Result<Transaction, PaymentError>;
/// Get the next unused address in the wallet /// Get the next unused address in the wallet
async fn next_unused_address(&self) -> Result<Address, PaymentError>; async fn next_unused_address(&self) -> Result<Address, PaymentError>;
@@ -139,6 +154,49 @@ impl OnchainWallet for LiquidOnchainWallet {
Ok(lwk_wollet.finalize(&mut pset)?) Ok(lwk_wollet.finalize(&mut pset)?)
} }
async fn build_drain_tx(
&self,
fee_rate_sats_per_kvb: Option<f32>,
recipient_address: &str,
enforce_amount_sat: Option<u64>,
) -> Result<Transaction, PaymentError> {
let lwk_wollet = self.wallet.lock().await;
let address =
ElementsAddress::from_str(recipient_address).map_err(|e| PaymentError::Generic {
err: format!(
"Recipient address {recipient_address} is not a valid ElementsAddress: {e:?}"
),
})?;
let mut pset = lwk_wollet
.tx_builder()
.drain_lbtc_wallet()
.drain_lbtc_to(address)
.fee_rate(fee_rate_sats_per_kvb)
.finish()?;
if let Some(enforce_amount_sat) = enforce_amount_sat {
let pset_details = lwk_wollet.get_details(&pset)?;
let pset_balance_sat = pset_details
.balance
.balances
.get(&lwk_wollet.policy_asset())
.unwrap_or(&0);
let pset_fees = pset_details.balance.fee;
ensure_sdk!(
(*pset_balance_sat * -1) as u64 - pset_fees == enforce_amount_sat,
PaymentError::Generic {
err: format!("Drain tx amount {pset_balance_sat} sat doesn't match enforce_amount_sat {enforce_amount_sat} sat")
}
);
}
let signer = AnySigner::Software(self.lwk_signer.clone());
signer.sign(&mut pset)?;
Ok(lwk_wollet.finalize(&mut pset)?)
}
/// Get the next unused address in the wallet /// Get the next unused address in the wallet
async fn next_unused_address(&self) -> Result<Address, PaymentError> { async fn next_unused_address(&self) -> Result<Address, PaymentError> {
Ok(self.wallet.lock().await.address(None)?.address().clone()) Ok(self.wallet.lock().await.address(None)?.address().clone())

View File

@@ -2199,6 +2199,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw == null ? null : dco_decode_list_payment_type(raw); return raw == null ? null : dco_decode_list_payment_type(raw);
} }
@protected
PayOnchainAmount dco_decode_pay_onchain_amount(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
switch (raw[0]) {
case 0:
return PayOnchainAmount_Receiver(
amountSat: dco_decode_u_64(raw[1]),
);
case 1:
return PayOnchainAmount_Drain();
default:
throw Exception("unreachable");
}
}
@protected @protected
PayOnchainRequest dco_decode_pay_onchain_request(dynamic raw) { PayOnchainRequest dco_decode_pay_onchain_request(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
@@ -2376,7 +2391,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final arr = raw as List<dynamic>; final arr = raw as List<dynamic>;
if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}');
return PreparePayOnchainRequest( return PreparePayOnchainRequest(
receiverAmountSat: dco_decode_u_64(arr[0]), amount: dco_decode_pay_onchain_amount(arr[0]),
satPerVbyte: dco_decode_opt_box_autoadd_u_32(arr[1]), satPerVbyte: dco_decode_opt_box_autoadd_u_32(arr[1]),
); );
} }
@@ -3888,6 +3903,22 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
} }
} }
@protected
PayOnchainAmount sse_decode_pay_onchain_amount(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var tag_ = sse_decode_i_32(deserializer);
switch (tag_) {
case 0:
var var_amountSat = sse_decode_u_64(deserializer);
return PayOnchainAmount_Receiver(amountSat: var_amountSat);
case 1:
return PayOnchainAmount_Drain();
default:
throw UnimplementedError('');
}
}
@protected @protected
PayOnchainRequest sse_decode_pay_onchain_request(SseDeserializer deserializer) { PayOnchainRequest sse_decode_pay_onchain_request(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -4062,9 +4093,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@protected @protected
PreparePayOnchainRequest sse_decode_prepare_pay_onchain_request(SseDeserializer deserializer) { PreparePayOnchainRequest sse_decode_prepare_pay_onchain_request(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
var var_receiverAmountSat = sse_decode_u_64(deserializer); var var_amount = sse_decode_pay_onchain_amount(deserializer);
var var_satPerVbyte = sse_decode_opt_box_autoadd_u_32(deserializer); var var_satPerVbyte = sse_decode_opt_box_autoadd_u_32(deserializer);
return PreparePayOnchainRequest(receiverAmountSat: var_receiverAmountSat, satPerVbyte: var_satPerVbyte); return PreparePayOnchainRequest(amount: var_amount, satPerVbyte: var_satPerVbyte);
} }
@protected @protected
@@ -5487,6 +5518,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
} }
} }
@protected
void sse_encode_pay_onchain_amount(PayOnchainAmount self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
switch (self) {
case PayOnchainAmount_Receiver(amountSat: final amountSat):
sse_encode_i_32(0, serializer);
sse_encode_u_64(amountSat, serializer);
case PayOnchainAmount_Drain():
sse_encode_i_32(1, serializer);
default:
throw UnimplementedError('');
}
}
@protected @protected
void sse_encode_pay_onchain_request(PayOnchainRequest self, SseSerializer serializer) { void sse_encode_pay_onchain_request(PayOnchainRequest self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -5644,7 +5689,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@protected @protected
void sse_encode_prepare_pay_onchain_request(PreparePayOnchainRequest self, SseSerializer serializer) { void sse_encode_prepare_pay_onchain_request(PreparePayOnchainRequest self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_u_64(self.receiverAmountSat, serializer); sse_encode_pay_onchain_amount(self.amount, serializer);
sse_encode_opt_box_autoadd_u_32(self.satPerVbyte, serializer); sse_encode_opt_box_autoadd_u_32(self.satPerVbyte, serializer);
} }

View File

@@ -365,6 +365,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
List<PaymentType>? dco_decode_opt_list_payment_type(dynamic raw); List<PaymentType>? dco_decode_opt_list_payment_type(dynamic raw);
@protected
PayOnchainAmount dco_decode_pay_onchain_amount(dynamic raw);
@protected @protected
PayOnchainRequest dco_decode_pay_onchain_request(dynamic raw); PayOnchainRequest dco_decode_pay_onchain_request(dynamic raw);
@@ -837,6 +840,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
List<PaymentType>? sse_decode_opt_list_payment_type(SseDeserializer deserializer); List<PaymentType>? sse_decode_opt_list_payment_type(SseDeserializer deserializer);
@protected
PayOnchainAmount sse_decode_pay_onchain_amount(SseDeserializer deserializer);
@protected @protected
PayOnchainRequest sse_decode_pay_onchain_request(SseDeserializer deserializer); PayOnchainRequest sse_decode_pay_onchain_request(SseDeserializer deserializer);
@@ -2253,6 +2259,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
cst_api_fill_to_wire_limits(apiObj.receive, wireObj.receive); cst_api_fill_to_wire_limits(apiObj.receive, wireObj.receive);
} }
@protected
void cst_api_fill_to_wire_pay_onchain_amount(PayOnchainAmount apiObj, wire_cst_pay_onchain_amount wireObj) {
if (apiObj is PayOnchainAmount_Receiver) {
var pre_amount_sat = cst_encode_u_64(apiObj.amountSat);
wireObj.tag = 0;
wireObj.kind.Receiver.amount_sat = pre_amount_sat;
return;
}
if (apiObj is PayOnchainAmount_Drain) {
wireObj.tag = 1;
return;
}
}
@protected @protected
void cst_api_fill_to_wire_pay_onchain_request( void cst_api_fill_to_wire_pay_onchain_request(
PayOnchainRequest apiObj, wire_cst_pay_onchain_request wireObj) { PayOnchainRequest apiObj, wire_cst_pay_onchain_request wireObj) {
@@ -2440,7 +2460,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void cst_api_fill_to_wire_prepare_pay_onchain_request( void cst_api_fill_to_wire_prepare_pay_onchain_request(
PreparePayOnchainRequest apiObj, wire_cst_prepare_pay_onchain_request wireObj) { PreparePayOnchainRequest apiObj, wire_cst_prepare_pay_onchain_request wireObj) {
wireObj.receiver_amount_sat = cst_encode_u_64(apiObj.receiverAmountSat); cst_api_fill_to_wire_pay_onchain_amount(apiObj.amount, wireObj.amount);
wireObj.sat_per_vbyte = cst_encode_opt_box_autoadd_u_32(apiObj.satPerVbyte); wireObj.sat_per_vbyte = cst_encode_opt_box_autoadd_u_32(apiObj.satPerVbyte);
} }
@@ -3114,6 +3134,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_opt_list_payment_type(List<PaymentType>? self, SseSerializer serializer); void sse_encode_opt_list_payment_type(List<PaymentType>? self, SseSerializer serializer);
@protected
void sse_encode_pay_onchain_amount(PayOnchainAmount self, SseSerializer serializer);
@protected @protected
void sse_encode_pay_onchain_request(PayOnchainRequest self, SseSerializer serializer); void sse_encode_pay_onchain_request(PayOnchainRequest self, SseSerializer serializer);
@@ -4734,9 +4757,24 @@ final class wire_cst_prepare_buy_bitcoin_request extends ffi.Struct {
external int amount_sat; external int amount_sat;
} }
final class wire_cst_prepare_pay_onchain_request extends ffi.Struct { final class wire_cst_PayOnchainAmount_Receiver extends ffi.Struct {
@ffi.Uint64() @ffi.Uint64()
external int receiver_amount_sat; external int amount_sat;
}
final class PayOnchainAmountKind extends ffi.Union {
external wire_cst_PayOnchainAmount_Receiver Receiver;
}
final class wire_cst_pay_onchain_amount extends ffi.Struct {
@ffi.Int32()
external int tag;
external PayOnchainAmountKind kind;
}
final class wire_cst_prepare_pay_onchain_request extends ffi.Struct {
external wire_cst_pay_onchain_amount amount;
external ffi.Pointer<ffi.Uint32> sat_per_vbyte; external ffi.Pointer<ffi.Uint32> sat_per_vbyte;
} }

View File

@@ -405,6 +405,19 @@ class OnchainPaymentLimitsResponse {
receive == other.receive; receive == other.receive;
} }
@freezed
sealed class PayOnchainAmount with _$PayOnchainAmount {
const PayOnchainAmount._();
/// The amount in satoshi that will be received
const factory PayOnchainAmount.receiver({
required BigInt amountSat,
}) = PayOnchainAmount_Receiver;
/// Indicates that all available funds should be sent
const factory PayOnchainAmount.drain() = PayOnchainAmount_Drain;
}
/// An argument when calling [crate::sdk::LiquidSdk::pay_onchain]. /// An argument when calling [crate::sdk::LiquidSdk::pay_onchain].
class PayOnchainRequest { class PayOnchainRequest {
final String address; final String address;
@@ -695,23 +708,25 @@ class PrepareBuyBitcoinResponse {
/// An argument when calling [crate::sdk::LiquidSdk::prepare_pay_onchain]. /// An argument when calling [crate::sdk::LiquidSdk::prepare_pay_onchain].
class PreparePayOnchainRequest { class PreparePayOnchainRequest {
final BigInt receiverAmountSat; final PayOnchainAmount amount;
/// The optional fee rate of the Bitcoin claim transaction. Defaults to the swapper estimated claim fee.
final int? satPerVbyte; final int? satPerVbyte;
const PreparePayOnchainRequest({ const PreparePayOnchainRequest({
required this.receiverAmountSat, required this.amount,
this.satPerVbyte, this.satPerVbyte,
}); });
@override @override
int get hashCode => receiverAmountSat.hashCode ^ satPerVbyte.hashCode; int get hashCode => amount.hashCode ^ satPerVbyte.hashCode;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is PreparePayOnchainRequest && other is PreparePayOnchainRequest &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
receiverAmountSat == other.receiverAmountSat && amount == other.amount &&
satPerVbyte == other.satPerVbyte; satPerVbyte == other.satPerVbyte;
} }

View File

@@ -283,6 +283,153 @@ abstract class LnUrlPayResult_PayError extends LnUrlPayResult {
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
/// @nodoc
mixin _$PayOnchainAmount {}
/// @nodoc
abstract class $PayOnchainAmountCopyWith<$Res> {
factory $PayOnchainAmountCopyWith(PayOnchainAmount value, $Res Function(PayOnchainAmount) then) =
_$PayOnchainAmountCopyWithImpl<$Res, PayOnchainAmount>;
}
/// @nodoc
class _$PayOnchainAmountCopyWithImpl<$Res, $Val extends PayOnchainAmount>
implements $PayOnchainAmountCopyWith<$Res> {
_$PayOnchainAmountCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of PayOnchainAmount
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$PayOnchainAmount_ReceiverImplCopyWith<$Res> {
factory _$$PayOnchainAmount_ReceiverImplCopyWith(
_$PayOnchainAmount_ReceiverImpl value, $Res Function(_$PayOnchainAmount_ReceiverImpl) then) =
__$$PayOnchainAmount_ReceiverImplCopyWithImpl<$Res>;
@useResult
$Res call({BigInt amountSat});
}
/// @nodoc
class __$$PayOnchainAmount_ReceiverImplCopyWithImpl<$Res>
extends _$PayOnchainAmountCopyWithImpl<$Res, _$PayOnchainAmount_ReceiverImpl>
implements _$$PayOnchainAmount_ReceiverImplCopyWith<$Res> {
__$$PayOnchainAmount_ReceiverImplCopyWithImpl(
_$PayOnchainAmount_ReceiverImpl _value, $Res Function(_$PayOnchainAmount_ReceiverImpl) _then)
: super(_value, _then);
/// Create a copy of PayOnchainAmount
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? amountSat = null,
}) {
return _then(_$PayOnchainAmount_ReceiverImpl(
amountSat: null == amountSat
? _value.amountSat
: amountSat // ignore: cast_nullable_to_non_nullable
as BigInt,
));
}
}
/// @nodoc
class _$PayOnchainAmount_ReceiverImpl extends PayOnchainAmount_Receiver {
const _$PayOnchainAmount_ReceiverImpl({required this.amountSat}) : super._();
@override
final BigInt amountSat;
@override
String toString() {
return 'PayOnchainAmount.receiver(amountSat: $amountSat)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PayOnchainAmount_ReceiverImpl &&
(identical(other.amountSat, amountSat) || other.amountSat == amountSat));
}
@override
int get hashCode => Object.hash(runtimeType, amountSat);
/// Create a copy of PayOnchainAmount
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$PayOnchainAmount_ReceiverImplCopyWith<_$PayOnchainAmount_ReceiverImpl> get copyWith =>
__$$PayOnchainAmount_ReceiverImplCopyWithImpl<_$PayOnchainAmount_ReceiverImpl>(this, _$identity);
}
abstract class PayOnchainAmount_Receiver extends PayOnchainAmount {
const factory PayOnchainAmount_Receiver({required final BigInt amountSat}) =
_$PayOnchainAmount_ReceiverImpl;
const PayOnchainAmount_Receiver._() : super._();
BigInt get amountSat;
/// Create a copy of PayOnchainAmount
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$PayOnchainAmount_ReceiverImplCopyWith<_$PayOnchainAmount_ReceiverImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$PayOnchainAmount_DrainImplCopyWith<$Res> {
factory _$$PayOnchainAmount_DrainImplCopyWith(
_$PayOnchainAmount_DrainImpl value, $Res Function(_$PayOnchainAmount_DrainImpl) then) =
__$$PayOnchainAmount_DrainImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$PayOnchainAmount_DrainImplCopyWithImpl<$Res>
extends _$PayOnchainAmountCopyWithImpl<$Res, _$PayOnchainAmount_DrainImpl>
implements _$$PayOnchainAmount_DrainImplCopyWith<$Res> {
__$$PayOnchainAmount_DrainImplCopyWithImpl(
_$PayOnchainAmount_DrainImpl _value, $Res Function(_$PayOnchainAmount_DrainImpl) _then)
: super(_value, _then);
/// Create a copy of PayOnchainAmount
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$PayOnchainAmount_DrainImpl extends PayOnchainAmount_Drain {
const _$PayOnchainAmount_DrainImpl() : super._();
@override
String toString() {
return 'PayOnchainAmount.drain()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$PayOnchainAmount_DrainImpl);
}
@override
int get hashCode => runtimeType.hashCode;
}
abstract class PayOnchainAmount_Drain extends PayOnchainAmount {
const factory PayOnchainAmount_Drain() = _$PayOnchainAmount_DrainImpl;
const PayOnchainAmount_Drain._() : super._();
}
/// @nodoc /// @nodoc
mixin _$PaymentDetails { mixin _$PaymentDetails {
/// Represents the invoice description /// Represents the invoice description

View File

@@ -1575,9 +1575,24 @@ final class wire_cst_prepare_buy_bitcoin_request extends ffi.Struct {
external int amount_sat; external int amount_sat;
} }
final class wire_cst_prepare_pay_onchain_request extends ffi.Struct { final class wire_cst_PayOnchainAmount_Receiver extends ffi.Struct {
@ffi.Uint64() @ffi.Uint64()
external int receiver_amount_sat; external int amount_sat;
}
final class PayOnchainAmountKind extends ffi.Union {
external wire_cst_PayOnchainAmount_Receiver Receiver;
}
final class wire_cst_pay_onchain_amount extends ffi.Struct {
@ffi.Int32()
external int tag;
external PayOnchainAmountKind kind;
}
final class wire_cst_prepare_pay_onchain_request extends ffi.Struct {
external wire_cst_pay_onchain_amount amount;
external ffi.Pointer<ffi.Uint32> sat_per_vbyte; external ffi.Pointer<ffi.Uint32> sat_per_vbyte;
} }

View File

@@ -1330,13 +1330,13 @@ fun asPreparePayOnchainRequest(preparePayOnchainRequest: ReadableMap): PreparePa
if (!validateMandatoryFields( if (!validateMandatoryFields(
preparePayOnchainRequest, preparePayOnchainRequest,
arrayOf( arrayOf(
"receiverAmountSat", "amount",
), ),
) )
) { ) {
return null return null
} }
val receiverAmountSat = preparePayOnchainRequest.getDouble("receiverAmountSat").toULong() val amount = preparePayOnchainRequest.getMap("amount")?.let { asPayOnchainAmount(it) }!!
val satPerVbyte = val satPerVbyte =
if (hasNonNullKey( if (hasNonNullKey(
preparePayOnchainRequest, preparePayOnchainRequest,
@@ -1347,12 +1347,12 @@ fun asPreparePayOnchainRequest(preparePayOnchainRequest: ReadableMap): PreparePa
} else { } else {
null null
} }
return PreparePayOnchainRequest(receiverAmountSat, satPerVbyte) return PreparePayOnchainRequest(amount, satPerVbyte)
} }
fun readableMapOf(preparePayOnchainRequest: PreparePayOnchainRequest): ReadableMap = fun readableMapOf(preparePayOnchainRequest: PreparePayOnchainRequest): ReadableMap =
readableMapOf( readableMapOf(
"receiverAmountSat" to preparePayOnchainRequest.receiverAmountSat, "amount" to readableMapOf(preparePayOnchainRequest.amount),
"satPerVbyte" to preparePayOnchainRequest.satPerVbyte, "satPerVbyte" to preparePayOnchainRequest.satPerVbyte,
) )
@@ -2485,6 +2485,44 @@ fun asNetworkList(arr: ReadableArray): List<Network> {
return list return list
} }
fun asPayOnchainAmount(payOnchainAmount: ReadableMap): PayOnchainAmount? {
val type = payOnchainAmount.getString("type")
if (type == "receiver") {
val amountSat = payOnchainAmount.getDouble("amountSat").toULong()
return PayOnchainAmount.Receiver(amountSat)
}
if (type == "drain") {
return PayOnchainAmount.Drain
}
return null
}
fun readableMapOf(payOnchainAmount: PayOnchainAmount): ReadableMap? {
val map = Arguments.createMap()
when (payOnchainAmount) {
is PayOnchainAmount.Receiver -> {
pushToMap(map, "type", "receiver")
pushToMap(map, "amountSat", payOnchainAmount.amountSat)
}
is PayOnchainAmount.Drain -> {
pushToMap(map, "type", "drain")
}
}
return map
}
fun asPayOnchainAmountList(arr: ReadableArray): List<PayOnchainAmount> {
val list = ArrayList<PayOnchainAmount>()
for (value in arr.toList()) {
when (value) {
is ReadableMap -> list.add(asPayOnchainAmount(value)!!)
else -> throw SdkException.Generic(errUnexpectedType(value))
}
}
return list
}
fun asPaymentDetails(paymentDetails: ReadableMap): PaymentDetails? { fun asPaymentDetails(paymentDetails: ReadableMap): PaymentDetails? {
val type = paymentDetails.getString("type") val type = paymentDetails.getString("type")

View File

@@ -1571,9 +1571,11 @@ enum BreezSDKLiquidMapper {
} }
static func asPreparePayOnchainRequest(preparePayOnchainRequest: [String: Any?]) throws -> PreparePayOnchainRequest { static func asPreparePayOnchainRequest(preparePayOnchainRequest: [String: Any?]) throws -> PreparePayOnchainRequest {
guard let receiverAmountSat = preparePayOnchainRequest["receiverAmountSat"] as? UInt64 else { guard let amountTmp = preparePayOnchainRequest["amount"] as? [String: Any?] else {
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "receiverAmountSat", typeName: "PreparePayOnchainRequest")) throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "amount", typeName: "PreparePayOnchainRequest"))
} }
let amount = try asPayOnchainAmount(payOnchainAmount: amountTmp)
var satPerVbyte: UInt32? var satPerVbyte: UInt32?
if hasNonNilKey(data: preparePayOnchainRequest, key: "satPerVbyte") { if hasNonNilKey(data: preparePayOnchainRequest, key: "satPerVbyte") {
guard let satPerVbyteTmp = preparePayOnchainRequest["satPerVbyte"] as? UInt32 else { guard let satPerVbyteTmp = preparePayOnchainRequest["satPerVbyte"] as? UInt32 else {
@@ -1582,12 +1584,12 @@ enum BreezSDKLiquidMapper {
satPerVbyte = satPerVbyteTmp satPerVbyte = satPerVbyteTmp
} }
return PreparePayOnchainRequest(receiverAmountSat: receiverAmountSat, satPerVbyte: satPerVbyte) return PreparePayOnchainRequest(amount: amount, satPerVbyte: satPerVbyte)
} }
static func dictionaryOf(preparePayOnchainRequest: PreparePayOnchainRequest) -> [String: Any?] { static func dictionaryOf(preparePayOnchainRequest: PreparePayOnchainRequest) -> [String: Any?] {
return [ return [
"receiverAmountSat": preparePayOnchainRequest.receiverAmountSat, "amount": dictionaryOf(payOnchainAmount: preparePayOnchainRequest.amount),
"satPerVbyte": preparePayOnchainRequest.satPerVbyte == nil ? nil : preparePayOnchainRequest.satPerVbyte, "satPerVbyte": preparePayOnchainRequest.satPerVbyte == nil ? nil : preparePayOnchainRequest.satPerVbyte,
] ]
} }
@@ -3065,6 +3067,55 @@ enum BreezSDKLiquidMapper {
return list return list
} }
static func asPayOnchainAmount(payOnchainAmount: [String: Any?]) throws -> PayOnchainAmount {
let type = payOnchainAmount["type"] as! String
if type == "receiver" {
guard let _amountSat = payOnchainAmount["amountSat"] as? UInt64 else {
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "amountSat", typeName: "PayOnchainAmount"))
}
return PayOnchainAmount.receiver(amountSat: _amountSat)
}
if type == "drain" {
return PayOnchainAmount.drain
}
throw SdkError.Generic(message: "Unexpected type \(type) for enum PayOnchainAmount")
}
static func dictionaryOf(payOnchainAmount: PayOnchainAmount) -> [String: Any?] {
switch payOnchainAmount {
case let .receiver(
amountSat
):
return [
"type": "receiver",
"amountSat": amountSat,
]
case .drain:
return [
"type": "drain",
]
}
}
static func arrayOf(payOnchainAmountList: [PayOnchainAmount]) -> [Any] {
return payOnchainAmountList.map { v -> [String: Any?] in return dictionaryOf(payOnchainAmount: v) }
}
static func asPayOnchainAmountList(arr: [Any]) throws -> [PayOnchainAmount] {
var list = [PayOnchainAmount]()
for value in arr {
if let val = value as? [String: Any?] {
var payOnchainAmount = try asPayOnchainAmount(payOnchainAmount: val)
list.append(payOnchainAmount)
} else {
throw SdkError.Generic(message: errUnexpectedType(typeName: "PayOnchainAmount"))
}
}
return list
}
static func asPaymentDetails(paymentDetails: [String: Any?]) throws -> PaymentDetails { static func asPaymentDetails(paymentDetails: [String: Any?]) throws -> PaymentDetails {
let type = paymentDetails["type"] as! String let type = paymentDetails["type"] as! String
if type == "lightning" { if type == "lightning" {

View File

@@ -244,7 +244,7 @@ export interface PrepareBuyBitcoinResponse {
} }
export interface PreparePayOnchainRequest { export interface PreparePayOnchainRequest {
receiverAmountSat: number amount: PayOnchainAmount
satPerVbyte?: number satPerVbyte?: number
} }
@@ -489,6 +489,18 @@ export enum Network {
REGTEST = "regtest" REGTEST = "regtest"
} }
export enum PayOnchainAmountVariant {
RECEIVER = "receiver",
DRAIN = "drain"
}
export type PayOnchainAmount = {
type: PayOnchainAmountVariant.RECEIVER,
amountSat: number
} | {
type: PayOnchainAmountVariant.DRAIN
}
export enum PaymentDetailsVariant { export enum PaymentDetailsVariant {
LIGHTNING = "lightning", LIGHTNING = "lightning",
LIQUID = "liquid", LIQUID = "liquid",