Rework draining to enable BOLT12 drain

(cherry picked from commit dba144f2e483e21d821dc7a19d379ca9ceaad12d)
This commit is contained in:
Ross Savage
2025-05-19 16:56:27 +02:00
committed by Roei Erez
parent f2b303fb33
commit 2fdb397b7f
15 changed files with 319 additions and 202 deletions

View File

@@ -405,24 +405,9 @@ pub(crate) async fn handle_command(
drain,
delay,
} => {
let destination = match (invoice, offer, address) {
(Some(invoice), None, None) => Ok(invoice),
(None, Some(offer), None) => match amount_sat {
Some(_) => Ok(offer),
None => Err(anyhow!(
"Must specify an amount for a BOLT12 offer."
))
},
(None, None, Some(address)) => Ok(address),
(Some(_), _, Some(_)) => {
Err(anyhow::anyhow!(
"Cannot specify both invoice and address at the same time."
))
}
_ => Err(anyhow!(
"Must specify either a BOLT11 invoice, a BOLT12 offer or a direct/BIP21 address."
))
}?;
let destination = invoice.or(offer.or(address)).ok_or(anyhow!(
"Must specify either a BOLT11 invoice, a BOLT12 offer or a direct/BIP21 address."
))?;
let amount = match (asset_id, amount, amount_sat, drain.unwrap_or(false)) {
(Some(asset_id), Some(receiver_amount), _, _) => Some(PayAmount::Asset {
asset_id,

View File

@@ -285,6 +285,26 @@ typedef struct wire_cst_ln_url_pay_request_data {
struct wire_cst_list_prim_u_8_strict *ln_address;
} wire_cst_ln_url_pay_request_data;
typedef struct wire_cst_PayAmount_Bitcoin {
uint64_t receiver_amount_sat;
} wire_cst_PayAmount_Bitcoin;
typedef struct wire_cst_PayAmount_Asset {
struct wire_cst_list_prim_u_8_strict *asset_id;
double receiver_amount;
bool *estimate_asset_fees;
} wire_cst_PayAmount_Asset;
typedef union PayAmountKind {
struct wire_cst_PayAmount_Bitcoin Bitcoin;
struct wire_cst_PayAmount_Asset Asset;
} PayAmountKind;
typedef struct wire_cst_pay_amount {
int32_t tag;
union PayAmountKind kind;
} wire_cst_pay_amount;
typedef struct wire_cst_aes_success_action_data {
struct wire_cst_list_prim_u_8_strict *description;
struct wire_cst_list_prim_u_8_strict *ciphertext;
@@ -328,6 +348,7 @@ typedef struct wire_cst_prepare_ln_url_pay_response {
struct wire_cst_send_destination destination;
uint64_t fees_sat;
struct wire_cst_ln_url_pay_request_data data;
struct wire_cst_pay_amount amount;
struct wire_cst_list_prim_u_8_strict *comment;
struct wire_cst_success_action *success_action;
} wire_cst_prepare_ln_url_pay_response;
@@ -366,26 +387,6 @@ typedef struct wire_cst_prepare_buy_bitcoin_request {
uint64_t amount_sat;
} wire_cst_prepare_buy_bitcoin_request;
typedef struct wire_cst_PayAmount_Bitcoin {
uint64_t receiver_amount_sat;
} wire_cst_PayAmount_Bitcoin;
typedef struct wire_cst_PayAmount_Asset {
struct wire_cst_list_prim_u_8_strict *asset_id;
double receiver_amount;
bool *estimate_asset_fees;
} wire_cst_PayAmount_Asset;
typedef union PayAmountKind {
struct wire_cst_PayAmount_Bitcoin Bitcoin;
struct wire_cst_PayAmount_Asset Asset;
} PayAmountKind;
typedef struct wire_cst_pay_amount {
int32_t tag;
union PayAmountKind kind;
} wire_cst_pay_amount;
typedef struct wire_cst_prepare_ln_url_pay_request {
struct wire_cst_ln_url_pay_request_data data;
struct wire_cst_pay_amount amount;
@@ -461,6 +462,7 @@ typedef struct wire_cst_restore_request {
typedef struct wire_cst_prepare_send_response {
struct wire_cst_send_destination destination;
struct wire_cst_pay_amount *amount;
uint64_t *fees_sat;
double *estimated_asset_fees;
} wire_cst_prepare_send_response;

View File

@@ -423,6 +423,7 @@ dictionary PrepareLnUrlPayResponse {
SendDestination destination;
u64 fees_sat;
LnUrlPayRequestData data;
PayAmount amount;
string? comment = null;
SuccessAction? success_action = null;
};
@@ -445,6 +446,7 @@ interface SendDestination {
dictionary PrepareSendResponse {
SendDestination destination;
PayAmount? amount;
u64? fees_sat;
f64? estimated_asset_fees;
};

View File

@@ -4297,6 +4297,7 @@ impl SseDecode for crate::model::PrepareLnUrlPayResponse {
let mut var_destination = <crate::model::SendDestination>::sse_decode(deserializer);
let mut var_feesSat = <u64>::sse_decode(deserializer);
let mut var_data = <crate::bindings::LnUrlPayRequestData>::sse_decode(deserializer);
let mut var_amount = <crate::model::PayAmount>::sse_decode(deserializer);
let mut var_comment = <Option<String>>::sse_decode(deserializer);
let mut var_successAction =
<Option<crate::bindings::SuccessAction>>::sse_decode(deserializer);
@@ -4304,6 +4305,7 @@ impl SseDecode for crate::model::PrepareLnUrlPayResponse {
destination: var_destination,
fees_sat: var_feesSat,
data: var_data,
amount: var_amount,
comment: var_comment,
success_action: var_successAction,
};
@@ -4412,10 +4414,12 @@ impl SseDecode for crate::model::PrepareSendResponse {
// 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_destination = <crate::model::SendDestination>::sse_decode(deserializer);
let mut var_amount = <Option<crate::model::PayAmount>>::sse_decode(deserializer);
let mut var_feesSat = <Option<u64>>::sse_decode(deserializer);
let mut var_estimatedAssetFees = <Option<f64>>::sse_decode(deserializer);
return crate::model::PrepareSendResponse {
destination: var_destination,
amount: var_amount,
fees_sat: var_feesSat,
estimated_asset_fees: var_estimatedAssetFees,
};
@@ -6745,6 +6749,7 @@ impl flutter_rust_bridge::IntoDart for crate::model::PrepareLnUrlPayResponse {
self.destination.into_into_dart().into_dart(),
self.fees_sat.into_into_dart().into_dart(),
self.data.into_into_dart().into_dart(),
self.amount.into_into_dart().into_dart(),
self.comment.into_into_dart().into_dart(),
self.success_action.into_into_dart().into_dart(),
]
@@ -6921,6 +6926,7 @@ impl flutter_rust_bridge::IntoDart for crate::model::PrepareSendResponse {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.destination.into_into_dart().into_dart(),
self.amount.into_into_dart().into_dart(),
self.fees_sat.into_into_dart().into_dart(),
self.estimated_asset_fees.into_into_dart().into_dart(),
]
@@ -9085,6 +9091,7 @@ impl SseEncode for crate::model::PrepareLnUrlPayResponse {
<crate::model::SendDestination>::sse_encode(self.destination, serializer);
<u64>::sse_encode(self.fees_sat, serializer);
<crate::bindings::LnUrlPayRequestData>::sse_encode(self.data, serializer);
<crate::model::PayAmount>::sse_encode(self.amount, serializer);
<Option<String>>::sse_encode(self.comment, serializer);
<Option<crate::bindings::SuccessAction>>::sse_encode(self.success_action, serializer);
}
@@ -9157,6 +9164,7 @@ impl SseEncode for crate::model::PrepareSendResponse {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<crate::model::SendDestination>::sse_encode(self.destination, serializer);
<Option<crate::model::PayAmount>>::sse_encode(self.amount, serializer);
<Option<u64>>::sse_encode(self.fees_sat, serializer);
<Option<f64>>::sse_encode(self.estimated_asset_fees, serializer);
}
@@ -11324,6 +11332,7 @@ mod io {
destination: self.destination.cst_decode(),
fees_sat: self.fees_sat.cst_decode(),
data: self.data.cst_decode(),
amount: self.amount.cst_decode(),
comment: self.comment.cst_decode(),
success_action: self.success_action.cst_decode(),
}
@@ -11404,6 +11413,7 @@ mod io {
fn cst_decode(self) -> crate::model::PrepareSendResponse {
crate::model::PrepareSendResponse {
destination: self.destination.cst_decode(),
amount: self.amount.cst_decode(),
fees_sat: self.fees_sat.cst_decode(),
estimated_asset_fees: self.estimated_asset_fees.cst_decode(),
}
@@ -12696,6 +12706,7 @@ mod io {
destination: Default::default(),
fees_sat: Default::default(),
data: Default::default(),
amount: Default::default(),
comment: core::ptr::null_mut(),
success_action: core::ptr::null_mut(),
}
@@ -12808,6 +12819,7 @@ mod io {
fn new_with_null_ptr() -> Self {
Self {
destination: Default::default(),
amount: core::ptr::null_mut(),
fees_sat: core::ptr::null_mut(),
estimated_asset_fees: core::ptr::null_mut(),
}
@@ -15213,6 +15225,7 @@ mod io {
destination: wire_cst_send_destination,
fees_sat: u64,
data: wire_cst_ln_url_pay_request_data,
amount: wire_cst_pay_amount,
comment: *mut wire_cst_list_prim_u_8_strict,
success_action: *mut wire_cst_success_action,
}
@@ -15269,6 +15282,7 @@ mod io {
#[derive(Clone, Copy)]
pub struct wire_cst_prepare_send_response {
destination: wire_cst_send_destination,
amount: *mut wire_cst_pay_amount,
fees_sat: *mut u64,
estimated_asset_fees: *mut f64,
}

View File

@@ -729,6 +729,8 @@ pub enum SendDestination {
#[derive(Debug, Serialize, Clone)]
pub struct PrepareSendResponse {
pub destination: SendDestination,
/// The optional amount to be sent in either Bitcoin or another asset
pub amount: Option<PayAmount>,
/// The optional estimated fee in satoshi. Is set when there is Bitcoin available
/// to pay fees. When not set, there are asset fees available to pay fees.
pub fees_sat: Option<u64>,
@@ -2296,6 +2298,8 @@ pub struct PrepareLnUrlPayResponse {
pub fees_sat: u64,
/// The [LnUrlPayRequestData] returned by [parse]
pub data: LnUrlPayRequestData,
/// The amount to send
pub amount: PayAmount,
/// An optional comment for this payment
pub comment: Option<String>,
/// The unprocessed LUD-09 success action. This will be processed and decrypted if

View File

@@ -1219,6 +1219,7 @@ impl LiquidSdk {
/// # Returns
/// Returns a [PrepareSendResponse] containing:
/// * `destination` - the parsed destination, of type [SendDestination]
/// * `amount` - the optional [PayAmount] to be sent in either Bitcoin or another asset
/// * `fees_sat` - the optional estimated fee in satoshi. Is set when there is Bitcoin
/// available to pay fees. When not set, there are asset fees available to pay fees.
/// * `estimated_asset_fees` - the optional estimated fee in the asset. Is set when
@@ -1399,76 +1400,105 @@ impl LiquidSdk {
.map(|(address, _)| address);
asset_id = self.config.lbtc_asset_id();
estimated_asset_fees = None;
(receiver_amount_sat, fees_sat, payment_destination) =
match (mrh_address.clone(), req.amount.clone()) {
(Some(lbtc_address), Some(PayAmount::Drain)) => {
// The BOLT11 invoice has an MRH and it is requested that the wallet balance is to be drained,
// therefore we use the MRH address and drain the balance (overpaying the invoice if neccessary)
let drain_fees_sat = self
.estimate_drain_tx_fee(None, Some(&lbtc_address))
.await?;
let drain_amount_sat =
get_info_res.wallet_info.balance_sat - drain_fees_sat;
let payment_destination = SendDestination::LiquidAddress {
address_data: LiquidAddressData {
address: lbtc_address,
asset_id: Some(asset_id.clone()),
amount: None,
amount_sat: Some(drain_amount_sat),
network: self.config.network.into(),
label: None,
message: None,
},
bip353_address: None,
};
(drain_amount_sat, Some(drain_fees_sat), payment_destination)
}
(Some(lbtc_address), _) => {
// The BOLT11 invoice has an MRH but no drain is requested,
// so we calculate the fees of a direct Liquid transaction
let fees_sat = self
.estimate_onchain_tx_or_drain_tx_fee(
invoice_amount_sat,
&lbtc_address,
&asset_id,
)
.await?;
(
(receiver_amount_sat, fees_sat) = match (mrh_address.clone(), req.amount.clone()) {
(Some(lbtc_address), Some(PayAmount::Drain)) => {
// The BOLT11 invoice has an MRH and it is requested that the
// wallet balance is to be drained, so we calculate the fees of
// a direct Liquid drain transaction
let drain_fees_sat = self
.estimate_drain_tx_fee(None, Some(&lbtc_address))
.await?;
let drain_amount_sat =
get_info_res.wallet_info.balance_sat - drain_fees_sat;
(drain_amount_sat, Some(drain_fees_sat))
}
(Some(lbtc_address), _) => {
// The BOLT11 invoice has an MRH but no drain is requested,
// so we calculate the fees of a direct Liquid transaction
let fees_sat = self
.estimate_onchain_tx_or_drain_tx_fee(
invoice_amount_sat,
Some(fees_sat),
SendDestination::Bolt11 {
invoice,
bip353_address: None,
},
&lbtc_address,
&asset_id,
)
}
(None, _) => {
// The BOLT11 invoice has no MRH, so we calculate the fees using a swap
let boltz_fees_total = lbtc_pair.fees.total(invoice_amount_sat);
let user_lockup_amount_sat = invoice_amount_sat + boltz_fees_total;
let lockup_fees_sat = self
.estimate_lockup_tx_or_drain_tx_fee(user_lockup_amount_sat)
.await?;
let fees_sat = boltz_fees_total + lockup_fees_sat;
(
invoice_amount_sat,
Some(fees_sat),
SendDestination::Bolt11 {
invoice,
bip353_address: None,
},
)
}
};
.await?;
(invoice_amount_sat, Some(fees_sat))
}
(None, _) => {
// The BOLT11 invoice has no MRH, so we calculate the fees using a swap
let boltz_fees_total = lbtc_pair.fees.total(invoice_amount_sat);
let user_lockup_amount_sat = invoice_amount_sat + boltz_fees_total;
let lockup_fees_sat = self
.estimate_lockup_tx_or_drain_tx_fee(user_lockup_amount_sat)
.await?;
let fees_sat = boltz_fees_total + lockup_fees_sat;
(invoice_amount_sat, Some(fees_sat))
}
};
payment_destination = SendDestination::Bolt11 {
invoice,
bip353_address: None,
};
}
Ok(InputType::Bolt12Offer {
offer,
bip353_address,
}) => {
receiver_amount_sat = match req.amount {
asset_id = self.config.lbtc_asset_id();
estimated_asset_fees = None;
(receiver_amount_sat, fees_sat) = match req.amount {
Some(PayAmount::Drain) => {
ensure_sdk!(
get_info_res.wallet_info.pending_receive_sat == 0
&& get_info_res.wallet_info.pending_send_sat == 0,
PaymentError::Generic {
err: "Cannot drain while there are pending payments".to_string(),
}
);
let lbtc_pair = self
.swapper
.get_submarine_pairs()
.await?
.ok_or(PaymentError::PairsNotFound)?;
let drain_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
let drain_amount_sat =
get_info_res.wallet_info.balance_sat - drain_fees_sat;
// Get the inverse receiver amount by calculating a dummy amount then increment up to the drain amount
let dummy_fees_sat = lbtc_pair.fees.total(drain_amount_sat);
let dummy_amount_sat = drain_amount_sat - dummy_fees_sat;
let receiver_amount_sat =
utils::increment_receiver_amount_up_to_drain_amount(
dummy_amount_sat,
&lbtc_pair,
drain_amount_sat,
);
lbtc_pair.limits.within(receiver_amount_sat)?;
// Validate if we can actually drain the wallet with a swap
let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
ensure_sdk!(
receiver_amount_sat + boltz_fees_total == drain_amount_sat,
PaymentError::Generic {
err: "Cannot drain without leaving a remainder".to_string(),
}
);
let fees_sat = Some(boltz_fees_total + drain_fees_sat);
info!("Drain amount: {receiver_amount_sat} sat");
Ok((receiver_amount_sat, fees_sat))
}
Some(PayAmount::Bitcoin {
receiver_amount_sat: amount_sat,
}) => Ok(amount_sat),
receiver_amount_sat,
}) => {
let lbtc_pair = self.validate_submarine_pairs(receiver_amount_sat).await?;
let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
let lockup_fees_sat = self
.estimate_lockup_tx_or_drain_tx_fee(
receiver_amount_sat + boltz_fees_total,
)
.await?;
let fees_sat = Some(boltz_fees_total + lockup_fees_sat);
Ok((receiver_amount_sat, fees_sat))
}
_ => Err(PaymentError::amount_missing(
"Expected PayAmount of type Receiver when processing a Bolt12 offer",
)),
@@ -1482,16 +1512,6 @@ impl LiquidSdk {
);
}
let lbtc_pair = self.validate_submarine_pairs(receiver_amount_sat).await?;
let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
let lockup_fees_sat = self
.estimate_lockup_tx_or_drain_tx_fee(receiver_amount_sat + boltz_fees_total)
.await?;
asset_id = self.config.lbtc_asset_id();
fees_sat = Some(boltz_fees_total + lockup_fees_sat);
estimated_asset_fees = None;
payment_destination = SendDestination::Bolt12 {
offer,
receiver_amount_sat,
@@ -1514,6 +1534,7 @@ impl LiquidSdk {
destination: payment_destination,
fees_sat,
estimated_asset_fees,
amount: req.amount.clone(),
})
}
@@ -1548,8 +1569,10 @@ impl LiquidSdk {
let PrepareSendResponse {
fees_sat,
destination: payment_destination,
amount,
..
} = &req.prepare_response;
let is_drain = matches!(amount, Some(PayAmount::Drain));
match payment_destination {
SendDestination::LiquidAddress {
@@ -1606,7 +1629,9 @@ impl LiquidSdk {
bip353_address,
} => {
let fees_sat = fees_sat.ok_or(PaymentError::InsufficientFunds)?;
let mut response = self.pay_bolt11_invoice(&invoice.bolt11, fees_sat).await?;
let mut response = self
.pay_bolt11_invoice(&invoice.bolt11, fees_sat, is_drain)
.await?;
self.insert_bip353_payment_details(bip353_address, &mut response)?;
Ok(response)
}
@@ -1621,7 +1646,13 @@ impl LiquidSdk {
.get_bolt12_info(&offer.offer, *receiver_amount_sat)
.await?;
let mut response = self
.pay_bolt12_invoice(offer, *receiver_amount_sat, bolt12_info, fees_sat)
.pay_bolt12_invoice(
offer,
*receiver_amount_sat,
bolt12_info,
fees_sat,
is_drain,
)
.await?;
self.insert_bip353_payment_details(bip353_address, &mut response)?;
Ok(response)
@@ -1660,6 +1691,7 @@ impl LiquidSdk {
&self,
invoice: &str,
fees_sat: u64,
is_drain: bool,
) -> Result<SendPaymentResponse, PaymentError> {
self.ensure_send_is_not_self_transfer(invoice)?;
let bolt11_invoice = self.validate_bolt11_invoice(invoice)?;
@@ -1671,8 +1703,9 @@ impl LiquidSdk {
err: "Invoice amount is missing".to_string(),
})?;
let payer_amount_sat = amount_sat + fees_sat;
let get_info_response = self.get_info().await?;
ensure_sdk!(
payer_amount_sat <= self.get_info().await?.wallet_info.balance_sat,
payer_amount_sat <= get_info_response.wallet_info.balance_sat,
PaymentError::InsufficientFunds
);
@@ -1685,6 +1718,16 @@ impl LiquidSdk {
// If we find a valid MRH, extract the BIP21 address and pay to it via onchain tx
Some((address, _)) => {
info!("Found MRH for L-BTC address {address}, invoice amount_sat {amount_sat}");
let (amount_sat, fees_sat) = if is_drain {
let drain_fees_sat = self.estimate_drain_tx_fee(None, Some(&address)).await?;
let drain_amount_sat =
get_info_response.wallet_info.balance_sat - drain_fees_sat;
info!("Drain amount: {drain_amount_sat} sat");
(drain_amount_sat, drain_fees_sat)
} else {
(amount_sat, fees_sat)
};
self.pay_liquid(
LiquidAddressData {
address,
@@ -1723,6 +1766,7 @@ impl LiquidSdk {
user_specified_receiver_amount_sat: u64,
bolt12_info: GetBolt12FetchResponse,
fees_sat: u64,
is_drain: bool,
) -> Result<SendPaymentResponse, PaymentError> {
let invoice = self.validate_bolt12_invoice(
offer,
@@ -1732,8 +1776,9 @@ impl LiquidSdk {
let receiver_amount_sat = invoice.amount_msats() / 1_000;
let payer_amount_sat = receiver_amount_sat + fees_sat;
let get_info_response = self.get_info().await?;
ensure_sdk!(
payer_amount_sat <= self.get_info().await?.wallet_info.balance_sat,
payer_amount_sat <= get_info_response.wallet_info.balance_sat,
PaymentError::InsufficientFunds
);
@@ -1745,6 +1790,15 @@ impl LiquidSdk {
);
let signing_pubkey = invoice.signing_pubkey().to_string();
let (_, address, _, _) = verify_mrh_signature(&bip21, &signing_pubkey, &signature)?;
let (receiver_amount_sat, fees_sat) = if is_drain {
let drain_fees_sat = self.estimate_drain_tx_fee(None, Some(&address)).await?;
let drain_amount_sat =
get_info_response.wallet_info.balance_sat - drain_fees_sat;
info!("Drain amount: {drain_amount_sat} sat");
(drain_amount_sat, drain_fees_sat)
} else {
(receiver_amount_sat, fees_sat)
};
self.pay_liquid(
LiquidAddressData {
@@ -4020,10 +4074,10 @@ impl LiquidSdk {
///
/// * `req` - the [PrepareLnUrlPayRequest] containing:
/// * `data` - the [LnUrlPayRequestData] returned by [LiquidSdk::parse]
/// * `amount` - The optional amount of type [PayAmount].
/// - [PayAmount::Drain] which uses all funds
/// - [PayAmount::Receiver] which sets the amount the receiver should receive
/// * `bip353_address` - A BIP353 address, in case one was used in order to fetch the LNURL
/// * `amount` - the [PayAmount] to send:
/// - [PayAmount::Drain] which uses all Bitcoin funds
/// - [PayAmount::Bitcoin] which sets the amount in satoshi that will be received
/// * `bip353_address` - a BIP353 address, in case one was used in order to fetch the LNURL
/// Pay request data. Returned by [parse].
/// * `comment` - an optional comment for this payment
/// * `validate_success_action_url` - validates that, if there is a URL success action, the URL domain matches
@@ -4032,9 +4086,10 @@ impl LiquidSdk {
/// # Returns
/// Returns a [PrepareLnUrlPayResponse] containing:
/// * `destination` - the destination of the payment
/// * `fees_sat` - The fees in satoshis to send the payment
/// * `data` - The [LnUrlPayRequestData] returned by [parse]
/// * `comment` - An optional comment for this payment
/// * `amount` - the [PayAmount] to send
/// * `fees_sat` - the fees in satoshis to send the payment
/// * `data` - the [LnUrlPayRequestData] returned by [parse]
/// * `comment` - an optional comment for this payment
/// * `success_action` - the optional unprocessed LUD-09 success action
pub async fn prepare_lnurl_pay(
&self,
@@ -4060,28 +4115,28 @@ impl LiquidSdk {
.ok_or(PaymentError::PairsNotFound)?;
let drain_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
let drain_amount_sat = get_info_res.wallet_info.balance_sat - drain_fees_sat;
// Get the inverse invoice amount by calculating a dummy amount then increment up to the drain amount
// Get the inverse receiver amount by calculating a dummy amount then increment up to the drain amount
let dummy_fees_sat = lbtc_pair.fees.total(drain_amount_sat);
let dummy_amount_sat = drain_amount_sat - dummy_fees_sat;
let invoice_amount_sat = utils::increment_invoice_amount_up_to_drain_amount(
let receiver_amount_sat = utils::increment_receiver_amount_up_to_drain_amount(
dummy_amount_sat,
&lbtc_pair,
drain_amount_sat,
);
lbtc_pair
.limits
.within(invoice_amount_sat)
.within(receiver_amount_sat)
.map_err(|e| LnUrlPayError::Generic { err: e.message() })?;
// Validate if we can actually drain the wallet with a swap
let pair_fees_sat = lbtc_pair.fees.total(invoice_amount_sat);
let pair_fees_sat = lbtc_pair.fees.total(receiver_amount_sat);
ensure_sdk!(
invoice_amount_sat + pair_fees_sat == drain_amount_sat,
receiver_amount_sat + pair_fees_sat == drain_amount_sat,
LnUrlPayError::Generic {
err: "Cannot drain without leaving a remainder".to_string(),
}
);
invoice_amount_sat * 1000
receiver_amount_sat * 1000
}
PayAmount::Bitcoin {
receiver_amount_sat,
@@ -4110,7 +4165,7 @@ impl LiquidSdk {
let prepare_response = self
.prepare_send_payment(&PrepareSendRequest {
destination: data.pr.clone(),
amount: Some(req.amount),
amount: Some(req.amount.clone()),
})
.await
.map_err(|e| LnUrlPayError::Generic { err: e.to_string() })?;
@@ -4136,6 +4191,7 @@ impl LiquidSdk {
destination,
fees_sat,
data: req.data,
amount: req.amount,
comment: req.comment,
success_action: data.success_action,
})
@@ -4166,6 +4222,7 @@ impl LiquidSdk {
destination: prepare_response.destination.clone(),
fees_sat: Some(prepare_response.fees_sat),
estimated_asset_fees: None,
amount: Some(prepare_response.amount),
},
use_asset_fees: None,
})

View File

@@ -151,23 +151,23 @@ pub(crate) fn lbtc_asset_id(network: LiquidNetwork) -> AssetId {
}
}
/// Increments the inversely calculated invoice amount up to the maximum drainable amount,
/// as calculating the inverse invoice amount in some cases has rounding down errors
pub(crate) fn increment_invoice_amount_up_to_drain_amount(
invoice_amount_sat: u64,
/// Increments the inversely calculated receiver amount up to the maximum drainable amount,
/// as calculating the inverse receiver amount in some cases has rounding down errors
pub(crate) fn increment_receiver_amount_up_to_drain_amount(
receiver_amount_sat: u64,
lbtc_pair: &SubmarinePair,
drain_amount_sat: u64,
) -> u64 {
let incremented_amount_sat = invoice_amount_sat + 1;
let incremented_amount_sat = receiver_amount_sat + 1;
let fees_sat = lbtc_pair.fees.total(incremented_amount_sat);
if incremented_amount_sat + fees_sat <= drain_amount_sat {
increment_invoice_amount_up_to_drain_amount(
increment_receiver_amount_up_to_drain_amount(
incremented_amount_sat,
lbtc_pair,
drain_amount_sat,
)
} else {
invoice_amount_sat
receiver_amount_sat
}
}

View File

@@ -457,6 +457,7 @@ pub enum SendDestination {
#[sdk_macros::extern_wasm_bindgen(breez_sdk_liquid::prelude::PrepareSendResponse)]
pub struct PrepareSendResponse {
pub destination: SendDestination,
pub amount: Option<PayAmount>,
pub fees_sat: Option<u64>,
pub estimated_asset_fees: Option<f64>,
}
@@ -789,6 +790,7 @@ pub struct PrepareLnUrlPayResponse {
pub destination: SendDestination,
pub fees_sat: u64,
pub data: LnUrlPayRequestData,
pub amount: PayAmount,
pub comment: Option<String>,
pub success_action: Option<SuccessAction>,
}

View File

@@ -2986,13 +2986,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
PrepareLnUrlPayResponse dco_decode_prepare_ln_url_pay_response(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 5) throw Exception('unexpected arr length: expect 5 but see ${arr.length}');
if (arr.length != 6) throw Exception('unexpected arr length: expect 6 but see ${arr.length}');
return PrepareLnUrlPayResponse(
destination: dco_decode_send_destination(arr[0]),
feesSat: dco_decode_u_64(arr[1]),
data: dco_decode_ln_url_pay_request_data(arr[2]),
comment: dco_decode_opt_String(arr[3]),
successAction: dco_decode_opt_box_autoadd_success_action(arr[4]),
amount: dco_decode_pay_amount(arr[3]),
comment: dco_decode_opt_String(arr[4]),
successAction: dco_decode_opt_box_autoadd_success_action(arr[5]),
);
}
@@ -3084,11 +3085,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
PrepareSendResponse dco_decode_prepare_send_response(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 3) throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
if (arr.length != 4) throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
return PrepareSendResponse(
destination: dco_decode_send_destination(arr[0]),
feesSat: dco_decode_opt_box_autoadd_u_64(arr[1]),
estimatedAssetFees: dco_decode_opt_box_autoadd_f_64(arr[2]),
amount: dco_decode_opt_box_autoadd_pay_amount(arr[1]),
feesSat: dco_decode_opt_box_autoadd_u_64(arr[2]),
estimatedAssetFees: dco_decode_opt_box_autoadd_f_64(arr[3]),
);
}
@@ -5404,12 +5406,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
var var_destination = sse_decode_send_destination(deserializer);
var var_feesSat = sse_decode_u_64(deserializer);
var var_data = sse_decode_ln_url_pay_request_data(deserializer);
var var_amount = sse_decode_pay_amount(deserializer);
var var_comment = sse_decode_opt_String(deserializer);
var var_successAction = sse_decode_opt_box_autoadd_success_action(deserializer);
return PrepareLnUrlPayResponse(
destination: var_destination,
feesSat: var_feesSat,
data: var_data,
amount: var_amount,
comment: var_comment,
successAction: var_successAction,
);
@@ -5501,10 +5505,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
PrepareSendResponse sse_decode_prepare_send_response(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_destination = sse_decode_send_destination(deserializer);
var var_amount = sse_decode_opt_box_autoadd_pay_amount(deserializer);
var var_feesSat = sse_decode_opt_box_autoadd_u_64(deserializer);
var var_estimatedAssetFees = sse_decode_opt_box_autoadd_f_64(deserializer);
return PrepareSendResponse(
destination: var_destination,
amount: var_amount,
feesSat: var_feesSat,
estimatedAssetFees: var_estimatedAssetFees,
);
@@ -7683,6 +7689,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_send_destination(self.destination, serializer);
sse_encode_u_64(self.feesSat, serializer);
sse_encode_ln_url_pay_request_data(self.data, serializer);
sse_encode_pay_amount(self.amount, serializer);
sse_encode_opt_String(self.comment, serializer);
sse_encode_opt_box_autoadd_success_action(self.successAction, serializer);
}
@@ -7747,6 +7754,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
void sse_encode_prepare_send_response(PrepareSendResponse self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_send_destination(self.destination, serializer);
sse_encode_opt_box_autoadd_pay_amount(self.amount, serializer);
sse_encode_opt_box_autoadd_u_64(self.feesSat, serializer);
sse_encode_opt_box_autoadd_f_64(self.estimatedAssetFees, serializer);
}

View File

@@ -3654,6 +3654,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
cst_api_fill_to_wire_send_destination(apiObj.destination, wireObj.destination);
wireObj.fees_sat = cst_encode_u_64(apiObj.feesSat);
cst_api_fill_to_wire_ln_url_pay_request_data(apiObj.data, wireObj.data);
cst_api_fill_to_wire_pay_amount(apiObj.amount, wireObj.amount);
wireObj.comment = cst_encode_opt_String(apiObj.comment);
wireObj.success_action = cst_encode_opt_box_autoadd_success_action(apiObj.successAction);
}
@@ -3734,6 +3735,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
wire_cst_prepare_send_response wireObj,
) {
cst_api_fill_to_wire_send_destination(apiObj.destination, wireObj.destination);
wireObj.amount = cst_encode_opt_box_autoadd_pay_amount(apiObj.amount);
wireObj.fees_sat = cst_encode_opt_box_autoadd_u_64(apiObj.feesSat);
wireObj.estimated_asset_fees = cst_encode_opt_box_autoadd_f_64(apiObj.estimatedAssetFees);
}
@@ -6701,6 +6703,33 @@ final class wire_cst_ln_url_pay_request_data extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> ln_address;
}
final class wire_cst_PayAmount_Bitcoin extends ffi.Struct {
@ffi.Uint64()
external int receiver_amount_sat;
}
final class wire_cst_PayAmount_Asset extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> asset_id;
@ffi.Double()
external double receiver_amount;
external ffi.Pointer<ffi.Bool> estimate_asset_fees;
}
final class PayAmountKind extends ffi.Union {
external wire_cst_PayAmount_Bitcoin Bitcoin;
external wire_cst_PayAmount_Asset Asset;
}
final class wire_cst_pay_amount extends ffi.Struct {
@ffi.Int32()
external int tag;
external PayAmountKind kind;
}
final class wire_cst_aes_success_action_data extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> description;
@@ -6757,6 +6786,8 @@ final class wire_cst_prepare_ln_url_pay_response extends ffi.Struct {
external wire_cst_ln_url_pay_request_data data;
external wire_cst_pay_amount amount;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> comment;
external ffi.Pointer<wire_cst_success_action> success_action;
@@ -6814,33 +6845,6 @@ final class wire_cst_prepare_buy_bitcoin_request extends ffi.Struct {
external int amount_sat;
}
final class wire_cst_PayAmount_Bitcoin extends ffi.Struct {
@ffi.Uint64()
external int receiver_amount_sat;
}
final class wire_cst_PayAmount_Asset extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> asset_id;
@ffi.Double()
external double receiver_amount;
external ffi.Pointer<ffi.Bool> estimate_asset_fees;
}
final class PayAmountKind extends ffi.Union {
external wire_cst_PayAmount_Bitcoin Bitcoin;
external wire_cst_PayAmount_Asset Asset;
}
final class wire_cst_pay_amount extends ffi.Struct {
@ffi.Int32()
external int tag;
external PayAmountKind kind;
}
final class wire_cst_prepare_ln_url_pay_request extends ffi.Struct {
external wire_cst_ln_url_pay_request_data data;
@@ -6945,6 +6949,8 @@ final class wire_cst_restore_request extends ffi.Struct {
final class wire_cst_prepare_send_response extends ffi.Struct {
external wire_cst_send_destination destination;
external ffi.Pointer<wire_cst_pay_amount> amount;
external ffi.Pointer<ffi.Uint64> fees_sat;
external ffi.Pointer<ffi.Double> estimated_asset_fees;

View File

@@ -1187,6 +1187,9 @@ class PrepareLnUrlPayResponse {
/// The [LnUrlPayRequestData] returned by [parse]
final LnUrlPayRequestData data;
/// The amount to send
final PayAmount amount;
/// An optional comment for this payment
final String? comment;
@@ -1198,13 +1201,19 @@ class PrepareLnUrlPayResponse {
required this.destination,
required this.feesSat,
required this.data,
required this.amount,
this.comment,
this.successAction,
});
@override
int get hashCode =>
destination.hashCode ^ feesSat.hashCode ^ data.hashCode ^ comment.hashCode ^ successAction.hashCode;
destination.hashCode ^
feesSat.hashCode ^
data.hashCode ^
amount.hashCode ^
comment.hashCode ^
successAction.hashCode;
@override
bool operator ==(Object other) =>
@@ -1214,6 +1223,7 @@ class PrepareLnUrlPayResponse {
destination == other.destination &&
feesSat == other.feesSat &&
data == other.data &&
amount == other.amount &&
comment == other.comment &&
successAction == other.successAction;
}
@@ -1430,6 +1440,9 @@ class PrepareSendRequest {
class PrepareSendResponse {
final SendDestination destination;
/// The optional amount to be sent in either Bitcoin or another asset
final PayAmount? amount;
/// The optional estimated fee in satoshi. Is set when there is Bitcoin available
/// to pay fees. When not set, there are asset fees available to pay fees.
final BigInt? feesSat;
@@ -1439,10 +1452,10 @@ class PrepareSendResponse {
/// are funds available in this asset to pay fees.
final double? estimatedAssetFees;
const PrepareSendResponse({required this.destination, this.feesSat, this.estimatedAssetFees});
const PrepareSendResponse({required this.destination, this.amount, this.feesSat, this.estimatedAssetFees});
@override
int get hashCode => destination.hashCode ^ feesSat.hashCode ^ estimatedAssetFees.hashCode;
int get hashCode => destination.hashCode ^ amount.hashCode ^ feesSat.hashCode ^ estimatedAssetFees.hashCode;
@override
bool operator ==(Object other) =>
@@ -1450,6 +1463,7 @@ class PrepareSendResponse {
other is PrepareSendResponse &&
runtimeType == other.runtimeType &&
destination == other.destination &&
amount == other.amount &&
feesSat == other.feesSat &&
estimatedAssetFees == other.estimatedAssetFees;
}

View File

@@ -4550,6 +4550,33 @@ final class wire_cst_ln_url_pay_request_data extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> ln_address;
}
final class wire_cst_PayAmount_Bitcoin extends ffi.Struct {
@ffi.Uint64()
external int receiver_amount_sat;
}
final class wire_cst_PayAmount_Asset extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> asset_id;
@ffi.Double()
external double receiver_amount;
external ffi.Pointer<ffi.Bool> estimate_asset_fees;
}
final class PayAmountKind extends ffi.Union {
external wire_cst_PayAmount_Bitcoin Bitcoin;
external wire_cst_PayAmount_Asset Asset;
}
final class wire_cst_pay_amount extends ffi.Struct {
@ffi.Int32()
external int tag;
external PayAmountKind kind;
}
final class wire_cst_aes_success_action_data extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> description;
@@ -4606,6 +4633,8 @@ final class wire_cst_prepare_ln_url_pay_response extends ffi.Struct {
external wire_cst_ln_url_pay_request_data data;
external wire_cst_pay_amount amount;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> comment;
external ffi.Pointer<wire_cst_success_action> success_action;
@@ -4663,33 +4692,6 @@ final class wire_cst_prepare_buy_bitcoin_request extends ffi.Struct {
external int amount_sat;
}
final class wire_cst_PayAmount_Bitcoin extends ffi.Struct {
@ffi.Uint64()
external int receiver_amount_sat;
}
final class wire_cst_PayAmount_Asset extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> asset_id;
@ffi.Double()
external double receiver_amount;
external ffi.Pointer<ffi.Bool> estimate_asset_fees;
}
final class PayAmountKind extends ffi.Union {
external wire_cst_PayAmount_Bitcoin Bitcoin;
external wire_cst_PayAmount_Asset Asset;
}
final class wire_cst_pay_amount extends ffi.Struct {
@ffi.Int32()
external int tag;
external PayAmountKind kind;
}
final class wire_cst_prepare_ln_url_pay_request extends ffi.Struct {
external wire_cst_ln_url_pay_request_data data;
@@ -4794,6 +4796,8 @@ final class wire_cst_restore_request extends ffi.Struct {
final class wire_cst_prepare_send_response extends ffi.Struct {
external wire_cst_send_destination destination;
external ffi.Pointer<wire_cst_pay_amount> amount;
external ffi.Pointer<ffi.Uint64> fees_sat;
external ffi.Pointer<ffi.Double> estimated_asset_fees;

View File

@@ -2003,6 +2003,7 @@ fun asPrepareLnUrlPayResponse(prepareLnUrlPayResponse: ReadableMap): PrepareLnUr
"destination",
"feesSat",
"data",
"amount",
),
)
) {
@@ -2011,6 +2012,7 @@ fun asPrepareLnUrlPayResponse(prepareLnUrlPayResponse: ReadableMap): PrepareLnUr
val destination = prepareLnUrlPayResponse.getMap("destination")?.let { asSendDestination(it) }!!
val feesSat = prepareLnUrlPayResponse.getDouble("feesSat").toULong()
val data = prepareLnUrlPayResponse.getMap("data")?.let { asLnUrlPayRequestData(it) }!!
val amount = prepareLnUrlPayResponse.getMap("amount")?.let { asPayAmount(it) }!!
val comment = if (hasNonNullKey(prepareLnUrlPayResponse, "comment")) prepareLnUrlPayResponse.getString("comment") else null
val successAction =
if (hasNonNullKey(prepareLnUrlPayResponse, "successAction")) {
@@ -2020,7 +2022,7 @@ fun asPrepareLnUrlPayResponse(prepareLnUrlPayResponse: ReadableMap): PrepareLnUr
} else {
null
}
return PrepareLnUrlPayResponse(destination, feesSat, data, comment, successAction)
return PrepareLnUrlPayResponse(destination, feesSat, data, amount, comment, successAction)
}
fun readableMapOf(prepareLnUrlPayResponse: PrepareLnUrlPayResponse): ReadableMap =
@@ -2028,6 +2030,7 @@ fun readableMapOf(prepareLnUrlPayResponse: PrepareLnUrlPayResponse): ReadableMap
"destination" to readableMapOf(prepareLnUrlPayResponse.destination),
"feesSat" to prepareLnUrlPayResponse.feesSat,
"data" to readableMapOf(prepareLnUrlPayResponse.data),
"amount" to readableMapOf(prepareLnUrlPayResponse.amount),
"comment" to prepareLnUrlPayResponse.comment,
"successAction" to prepareLnUrlPayResponse.successAction?.let { readableMapOf(it) },
)
@@ -2357,6 +2360,7 @@ fun asPrepareSendResponse(prepareSendResponse: ReadableMap): PrepareSendResponse
return null
}
val destination = prepareSendResponse.getMap("destination")?.let { asSendDestination(it) }!!
val amount = if (hasNonNullKey(prepareSendResponse, "amount")) prepareSendResponse.getMap("amount")?.let { asPayAmount(it) } else null
val feesSat = if (hasNonNullKey(prepareSendResponse, "feesSat")) prepareSendResponse.getDouble("feesSat").toULong() else null
val estimatedAssetFees =
if (hasNonNullKey(
@@ -2368,12 +2372,13 @@ fun asPrepareSendResponse(prepareSendResponse: ReadableMap): PrepareSendResponse
} else {
null
}
return PrepareSendResponse(destination, feesSat, estimatedAssetFees)
return PrepareSendResponse(destination, amount, feesSat, estimatedAssetFees)
}
fun readableMapOf(prepareSendResponse: PrepareSendResponse): ReadableMap =
readableMapOf(
"destination" to readableMapOf(prepareSendResponse.destination),
"amount" to prepareSendResponse.amount?.let { readableMapOf(it) },
"feesSat" to prepareSendResponse.feesSat,
"estimatedAssetFees" to prepareSendResponse.estimatedAssetFees,
)

View File

@@ -2392,6 +2392,11 @@ enum BreezSDKLiquidMapper {
}
let data = try asLnUrlPayRequestData(lnUrlPayRequestData: dataTmp)
guard let amountTmp = prepareLnUrlPayResponse["amount"] as? [String: Any?] else {
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "amount", typeName: "PrepareLnUrlPayResponse"))
}
let amount = try asPayAmount(payAmount: amountTmp)
var comment: String?
if hasNonNilKey(data: prepareLnUrlPayResponse, key: "comment") {
guard let commentTmp = prepareLnUrlPayResponse["comment"] as? String else {
@@ -2404,7 +2409,7 @@ enum BreezSDKLiquidMapper {
successAction = try asSuccessAction(successAction: successActionTmp)
}
return PrepareLnUrlPayResponse(destination: destination, feesSat: feesSat, data: data, comment: comment, successAction: successAction)
return PrepareLnUrlPayResponse(destination: destination, feesSat: feesSat, data: data, amount: amount, comment: comment, successAction: successAction)
}
static func dictionaryOf(prepareLnUrlPayResponse: PrepareLnUrlPayResponse) -> [String: Any?] {
@@ -2412,6 +2417,7 @@ enum BreezSDKLiquidMapper {
"destination": dictionaryOf(sendDestination: prepareLnUrlPayResponse.destination),
"feesSat": prepareLnUrlPayResponse.feesSat,
"data": dictionaryOf(lnUrlPayRequestData: prepareLnUrlPayResponse.data),
"amount": dictionaryOf(payAmount: prepareLnUrlPayResponse.amount),
"comment": prepareLnUrlPayResponse.comment == nil ? nil : prepareLnUrlPayResponse.comment,
"successAction": prepareLnUrlPayResponse.successAction == nil ? nil : dictionaryOf(successAction: prepareLnUrlPayResponse.successAction!),
]
@@ -2743,6 +2749,11 @@ enum BreezSDKLiquidMapper {
}
let destination = try asSendDestination(sendDestination: destinationTmp)
var amount: PayAmount?
if let amountTmp = prepareSendResponse["amount"] as? [String: Any?] {
amount = try asPayAmount(payAmount: amountTmp)
}
var feesSat: UInt64?
if hasNonNilKey(data: prepareSendResponse, key: "feesSat") {
guard let feesSatTmp = prepareSendResponse["feesSat"] as? UInt64 else {
@@ -2758,12 +2769,13 @@ enum BreezSDKLiquidMapper {
estimatedAssetFees = estimatedAssetFeesTmp
}
return PrepareSendResponse(destination: destination, feesSat: feesSat, estimatedAssetFees: estimatedAssetFees)
return PrepareSendResponse(destination: destination, amount: amount, feesSat: feesSat, estimatedAssetFees: estimatedAssetFees)
}
static func dictionaryOf(prepareSendResponse: PrepareSendResponse) -> [String: Any?] {
return [
"destination": dictionaryOf(sendDestination: prepareSendResponse.destination),
"amount": prepareSendResponse.amount == nil ? nil : dictionaryOf(payAmount: prepareSendResponse.amount!),
"feesSat": prepareSendResponse.feesSat == nil ? nil : prepareSendResponse.feesSat,
"estimatedAssetFees": prepareSendResponse.estimatedAssetFees == nil ? nil : prepareSendResponse.estimatedAssetFees,
]

View File

@@ -356,6 +356,7 @@ export interface PrepareLnUrlPayResponse {
destination: SendDestination
feesSat: number
data: LnUrlPayRequestData
amount: PayAmount
comment?: string
successAction?: SuccessAction
}
@@ -404,6 +405,7 @@ export interface PrepareSendRequest {
export interface PrepareSendResponse {
destination: SendDestination
amount?: PayAmount
feesSat?: number
estimatedAssetFees?: number
}