Receive: Switch payment to pending state when lockup is in the mempool (#301)

* feat: switch to pending state when receive lockup is in the mempool

* rebasing

* fix: move socket update logic to sub-crate

* Update payments query, to avoid duplicate Receive Swaps

This can happen if the app is stopped before the temporary lockup tx is removed from the DB. The Receive Swap would then forever result in two payments in list_payments.

* Add comments to clarify use of temp lockup txid

* Re-generate flutter bridge bindings

* feat: set Payment `tx_id` as optional and change `list_payments` logic

* fix: debug typo

* fix: undo `remove_temporary_tx` changes

* fix: switch to full join rather than manual filtering

* fix: bindings

* fix: improve error handling when tx data is not present

* fix: RN bindings

* fix: exclude Created receives from the list

* fix: fixing nits

* Re-generate FRB bindings

---------

Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
This commit is contained in:
yse
2024-06-12 22:44:31 +02:00
committed by GitHub
parent 31e2ab44af
commit 2b64708e36
21 changed files with 194 additions and 136 deletions

2
cli/Cargo.lock generated
View File

@@ -381,7 +381,7 @@ checksum = "829a082bd3761fde7476dc2ed85ca56c11628948460ece621e4f56fef5046567"
[[package]]
name = "boltz-client"
version = "0.1.3"
source = "git+https://github.com/ok300/boltz-rust?branch=ok300-breez-latest-06-05#9a0b5fdcb71271d804aa755870fc079e9605e2c4"
source = "git+https://github.com/hydra-yse/boltz-rust?branch=yse-breez-latest#66cdf65ba889a25a5274af3d27f5f52a2d4e3cc9"
dependencies = [
"bip39",
"bitcoin 0.31.2",

2
lib/Cargo.lock generated
View File

@@ -501,7 +501,7 @@ checksum = "829a082bd3761fde7476dc2ed85ca56c11628948460ece621e4f56fef5046567"
[[package]]
name = "boltz-client"
version = "0.1.3"
source = "git+https://github.com/ok300/boltz-rust?branch=ok300-breez-latest-06-05#9a0b5fdcb71271d804aa755870fc079e9605e2c4"
source = "git+https://github.com/hydra-yse/boltz-rust?branch=yse-breez-latest#66cdf65ba889a25a5274af3d27f5f52a2d4e3cc9"
dependencies = [
"bip39",
"bitcoin 0.31.2",

View File

@@ -14,12 +14,6 @@ void store_dart_post_cobject(DartPostCObjectFnType ptr);
// EXTRA END
typedef struct _Dart_Handle* Dart_Handle;
/**
* Claim tx feerate, in sats per vbyte.
* Since the Liquid blocks are consistently empty for now, we hardcode the minimum feerate.
*/
#define LIQUID_CLAIM_TX_FEERATE_MSAT 100.0
typedef struct wire_cst_list_prim_u_8_strict {
uint8_t *ptr;
int32_t len;

View File

@@ -117,7 +117,7 @@ dictionary LNInvoice {
};
dictionary Payment {
string tx_id;
string? tx_id = null;
string? swap_id = null;
u32 timestamp;
u64 amount_sat;

View File

@@ -15,7 +15,7 @@ frb = ["dep:flutter_rust_bridge"]
anyhow = { workspace = true }
bip39 = { version = "2.0.0", features = ["serde"] }
#boltz-client = { git = "https://github.com/SatoshiPortal/boltz-rust", rev = "a05731cc33030ada9ae14afcafe0cded22842ba6" }
boltz-client = { git = "https://github.com/ok300/boltz-rust", branch = "ok300-breez-latest-06-05" }
boltz-client = { git = "https://github.com/hydra-yse/boltz-rust", branch = "yse-breez-latest" }
chrono = "0.4"
env_logger = "0.11"
flutter_rust_bridge = { version = "=2.0.0-dev.38", features = ["chrono"], optional = true }

View File

@@ -1028,7 +1028,7 @@ impl SseDecode for Option<u64> {
impl SseDecode for crate::model::Payment {
// 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_txId = <String>::sse_decode(deserializer);
let mut var_txId = <Option<String>>::sse_decode(deserializer);
let mut var_swapId = <Option<String>>::sse_decode(deserializer);
let mut var_timestamp = <u32>::sse_decode(deserializer);
let mut var_amountSat = <u64>::sse_decode(deserializer);
@@ -2091,7 +2091,7 @@ impl SseEncode for Option<u64> {
impl SseEncode for crate::model::Payment {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<String>::sse_encode(self.tx_id, serializer);
<Option<String>>::sse_encode(self.tx_id, serializer);
<Option<String>>::sse_encode(self.swap_id, serializer);
<u32>::sse_encode(self.timestamp, serializer);
<u64>::sse_encode(self.amount_sat, serializer);

View File

@@ -288,6 +288,8 @@ pub(crate) struct ReceiveSwap {
pub(crate) claim_fees_sat: u64,
/// Persisted as soon as a claim tx is broadcast
pub(crate) claim_tx_id: Option<String>,
/// Until the lockup tx is seen in the mempool, it contains the swap creation time.
/// Afterwards, it shows the lockup tx creation time.
pub(crate) created_at: u32,
pub(crate) state: PaymentState,
}
@@ -530,8 +532,7 @@ pub struct PaymentSwapData {
/// By default, this is an onchain tx. It may represent a swap, if swap metadata is available.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Payment {
/// The tx ID of the onchain transaction
pub tx_id: String,
pub tx_id: Option<String>,
/// The swap ID, if any swap is associated with this payment
pub swap_id: Option<String>,
@@ -582,9 +583,29 @@ pub struct Payment {
pub status: PaymentState,
}
impl Payment {
pub(crate) fn from(tx: PaymentTxData, swap: Option<PaymentSwapData>) -> Payment {
pub(crate) fn from_pending_swap(swap: PaymentSwapData, payment_type: PaymentType) -> Payment {
let amount_sat = match payment_type {
PaymentType::Receive => swap.receiver_amount_sat,
PaymentType::Send => swap.payer_amount_sat,
};
Payment {
tx_id: tx.tx_id,
tx_id: None,
swap_id: Some(swap.swap_id),
timestamp: swap.created_at,
amount_sat,
fees_sat: swap.payer_amount_sat - swap.receiver_amount_sat,
preimage: swap.preimage,
refund_tx_id: swap.refund_tx_id,
refund_tx_amount_sat: swap.refund_tx_amount_sat,
payment_type,
status: swap.status,
}
}
pub(crate) fn from_tx_data(tx: PaymentTxData, swap: Option<PaymentSwapData>) -> Payment {
Payment {
tx_id: Some(tx.tx_id),
swap_id: swap.as_ref().map(|s| s.swap_id.clone()),
timestamp: match swap {
Some(ref swap) => swap.created_at,

View File

@@ -11,6 +11,7 @@ pub(crate) fn current_migrations() -> Vec<&'static str> {
created_at INTEGER NOT NULL,
claim_fees_sat INTEGER NOT NULL,
claim_tx_id TEXT,
lockup_tx_id TEXT,
state INTEGER NOT NULL
) STRICT;",
"CREATE TABLE IF NOT EXISTS send_swaps (

View File

@@ -3,7 +3,7 @@ mod migrations;
pub(crate) mod receive;
pub(crate) mod send;
use std::{collections::HashMap, fs::create_dir_all, path::PathBuf, str::FromStr};
use std::{fs::create_dir_all, path::PathBuf, str::FromStr};
use anyhow::Result;
use migrations::current_migrations;
@@ -118,7 +118,10 @@ impl Persister {
ss.state,
rtx.amount_sat
FROM payment_tx_data AS ptx -- Payment tx (each tx results in a Payment)
LEFT JOIN receive_swaps AS rs -- Receive Swap data
FULL JOIN (
SELECT * FROM receive_swaps
WHERE claim_tx_id IS NOT NULL OR lockup_tx_id IS NOT NULL
) rs -- Receive Swap data (by claim)
ON ptx.tx_id = rs.claim_tx_id
LEFT JOIN send_swaps AS ss -- Send Swap data
ON ptx.tx_id = ss.lockup_tx_id
@@ -133,13 +136,17 @@ impl Persister {
}
fn sql_row_to_payment(&self, row: &Row) -> Result<Payment, rusqlite::Error> {
let tx = PaymentTxData {
tx_id: row.get(0)?,
let maybe_tx_tx_id: Result<String, rusqlite::Error> = row.get(0);
let tx = match maybe_tx_tx_id {
Ok(ref tx_id) => Some(PaymentTxData {
tx_id: tx_id.to_string(),
timestamp: row.get(1)?,
amount_sat: row.get(2)?,
fees_sat: row.get(3)?,
payment_type: row.get(4)?,
is_confirmed: row.get(5)?,
}),
_ => None,
};
let maybe_receive_swap_id: Option<String> = row.get(6)?;
@@ -157,8 +164,9 @@ impl Persister {
let maybe_send_swap_state: Option<PaymentState> = row.get(17)?;
let maybe_send_swap_refund_tx_amount_sat: Option<u64> = row.get(18)?;
let swap = match maybe_receive_swap_id {
Some(receive_swap_id) => Some(PaymentSwapData {
let (swap, payment_type) = match maybe_receive_swap_id {
Some(receive_swap_id) => (
Some(PaymentSwapData {
swap_id: receive_swap_id,
created_at: maybe_receive_swap_created_at.unwrap_or(utils::now()),
preimage: None,
@@ -168,7 +176,10 @@ impl Persister {
refund_tx_amount_sat: None,
status: maybe_receive_swap_receiver_state.unwrap_or(PaymentState::Created),
}),
None => maybe_send_swap_id.map(|send_swap_id| PaymentSwapData {
PaymentType::Receive,
),
None => (
maybe_send_swap_id.map(|send_swap_id| PaymentSwapData {
swap_id: send_swap_id,
created_at: maybe_send_swap_created_at.unwrap_or(utils::now()),
preimage: maybe_send_swap_preimage,
@@ -178,9 +189,16 @@ impl Persister {
refund_tx_amount_sat: maybe_send_swap_refund_tx_amount_sat,
status: maybe_send_swap_state.unwrap_or(PaymentState::Created),
}),
PaymentType::Send,
),
};
Ok(Payment::from(tx, swap))
match (tx, swap.clone()) {
(None, None) => Err(maybe_tx_tx_id.err().unwrap()),
(None, Some(swap)) => Ok(Payment::from_pending_swap(swap, payment_type)),
(Some(tx), None) => Ok(Payment::from_tx_data(tx, None)),
(Some(tx), Some(swap)) => Ok(Payment::from_tx_data(tx, Some(swap))),
}
}
pub fn get_payment(&self, id: String) -> Result<Option<Payment>> {
@@ -194,18 +212,15 @@ impl Persister {
.optional()?)
}
pub fn get_payments(&self) -> Result<HashMap<String, Payment>> {
pub fn get_payments(&self) -> Result<Vec<Payment>> {
let con = self.get_connection()?;
// Assumes there is no swap chaining (send swap lockup tx = receive swap claim tx)
let mut stmt = con.prepare(&self.select_payment_query(None))?;
let data = stmt
.query_map(params![], |row| {
self.sql_row_to_payment(row).map(|p| (p.tx_id.clone(), p))
})?
let payments: Vec<Payment> = stmt
.query_map(params![], |row| self.sql_row_to_payment(row))?
.map(|i| i.unwrap())
.collect();
Ok(data)
Ok(payments)
}
}

View File

@@ -178,8 +178,9 @@ impl Persister {
swap_id: &str,
to_state: PaymentState,
claim_tx_id: Option<&str>,
lockup_tx_id: Option<&str>,
) -> Result<(), PaymentError> {
// Do not overwrite claim_tx_id
// Do not overwrite claim_tx_id or lockup_tx_id
let con: Connection = self.get_connection()?;
con.execute(
"UPDATE receive_swaps
@@ -189,12 +190,17 @@ impl Persister {
WHEN claim_tx_id IS NULL THEN :claim_tx_id
ELSE claim_tx_id
END,
lockup_tx_id =
CASE
WHEN lockup_tx_id IS NULL THEN :lockup_tx_id
ELSE lockup_tx_id
END,
state = :state
WHERE
id = :id",
named_params! {
":id": swap_id,
":lockup_tx_id": lockup_tx_id,
":claim_tx_id": claim_tx_id,
":state": to_state,
},

View File

@@ -2,6 +2,7 @@ use std::{str::FromStr, sync::Arc};
use anyhow::{anyhow, Result};
use boltz_client::swaps::boltz::RevSwapStates;
use boltz_client::swaps::boltzv2;
use log::{debug, error, info, warn};
use tokio::sync::broadcast;
@@ -40,7 +41,10 @@ impl ReceiveSwapStateHandler {
}
/// Handles status updates from Boltz for Receive swaps
pub(crate) async fn on_new_status(&self, swap_state: &str, id: &str) -> Result<()> {
pub(crate) async fn on_new_status(&self, update: &boltzv2::Update) -> Result<()> {
let id = update.id();
let swap_state = update.status();
let receive_swap = self
.persister
.fetch_receive_swap(id)?
@@ -49,33 +53,41 @@ impl ReceiveSwapStateHandler {
info!("Handling Receive Swap transition to {swap_state:?} for swap {id}");
match RevSwapStates::from_str(swap_state) {
Ok(RevSwapStates::SwapExpired
Ok(
RevSwapStates::SwapExpired
| RevSwapStates::InvoiceExpired
| RevSwapStates::TransactionFailed
| RevSwapStates::TransactionRefunded) => {
| RevSwapStates::TransactionRefunded,
) => {
error!("Swap {id} entered into an unrecoverable state: {swap_state:?}");
self.update_swap_info(id, Failed, None).await?;
self.update_swap_info(id, Failed, None, None).await?;
Ok(())
}
// The lockup tx is in the mempool and we accept 0-conf => try to claim
// TODO Add 0-conf preconditions check: https://github.com/breez/breez-liquid-sdk/issues/187
Ok(RevSwapStates::TransactionMempool
// The lockup tx is confirmed => try to claim
| RevSwapStates::TransactionConfirmed) => {
Ok(RevSwapStates::TransactionMempool) => {
let boltzv2::Update::TransactionMempool { transaction, .. } = update else {
return Err(anyhow!("Unexpected payload from Boltz status stream"));
};
let lockup_tx_id = &transaction.id;
self.update_swap_info(id, Pending, None, Some(lockup_tx_id))
.await?;
Ok(())
}
Ok(RevSwapStates::TransactionConfirmed) => {
match receive_swap.claim_tx_id {
Some(claim_tx_id) => {
warn!("Claim tx for Receive Swap {id} was already broadcast: txid {claim_tx_id}")
}
None => {
self.update_swap_info(&receive_swap.id, Pending, None)
self.update_swap_info(&receive_swap.id, Pending, None, None)
.await?;
match self.claim(&receive_swap).await {
Ok(_) => {}
Err(err) => match err {
PaymentError::AlreadyClaimed => warn!("Funds already claimed for Receive Swap {id}"),
_ => error!("Claim for Receive Swap {id} failed: {err}")
PaymentError::AlreadyClaimed => {
warn!("Funds already claimed for Receive Swap {id}")
}
_ => error!("Claim for Receive Swap {id} failed: {err}"),
},
}
}
}
@@ -85,9 +97,11 @@ impl ReceiveSwapStateHandler {
Ok(_) => {
debug!("Unhandled state for Receive Swap {id}: {swap_state}");
Ok(())
},
}
_ => Err(anyhow!("Invalid RevSwapState for Receive Swap {id}: {swap_state}")),
_ => Err(anyhow!(
"Invalid RevSwapState for Receive Swap {id}: {swap_state}"
)),
}
}
@@ -97,9 +111,10 @@ impl ReceiveSwapStateHandler {
swap_id: &str,
to_state: PaymentState,
claim_tx_id: Option<&str>,
lockup_tx_id: Option<&str>,
) -> Result<(), PaymentError> {
info!(
"Transitioning Receive swap {swap_id} to {to_state:?} (claim_tx_id = {claim_tx_id:?})"
"Transitioning Receive swap {swap_id} to {to_state:?} (claim_tx_id = {claim_tx_id:?}, lockup_tx_id = {lockup_tx_id:?})"
);
let swap = self
@@ -109,11 +124,18 @@ impl ReceiveSwapStateHandler {
.ok_or(PaymentError::Generic {
err: format!("Receive Swap not found {swap_id}"),
})?;
let payment_id = claim_tx_id.map(|c| c.to_string()).or(swap.claim_tx_id);
let payment_id = claim_tx_id
.or(lockup_tx_id)
.map(|id| id.to_string())
.or(swap.claim_tx_id);
Self::validate_state_transition(swap.state, to_state)?;
self.persister
.try_handle_receive_swap_update(swap_id, to_state, claim_tx_id)?;
self.persister.try_handle_receive_swap_update(
swap_id,
to_state,
claim_tx_id,
lockup_tx_id,
)?;
if let Some(payment_id) = payment_id {
let _ = self.subscription_notifier.send(payment_id);
@@ -143,7 +165,7 @@ impl ReceiveSwapStateHandler {
is_confirmed: false,
})?;
self.update_swap_info(swap_id, Pending, Some(&claim_tx_id))
self.update_swap_info(swap_id, Pending, Some(&claim_tx_id), None)
.await?;
Ok(())

View File

@@ -11,7 +11,6 @@ use std::{
use anyhow::Result;
use async_trait::async_trait;
use boltz_client::lightning_invoice::Bolt11InvoiceDescription;
use boltz_client::swaps::boltzv2;
use boltz_client::ToHex;
use boltz_client::{swaps::boltzv2::*, util::secrets::Preimage, Amount, Bolt11Invoice};
use futures_util::stream::select_all;
@@ -41,10 +40,6 @@ use crate::{
utils,
};
/// Claim tx feerate, in sats per vbyte.
/// Since the Liquid blocks are consistently empty for now, we hardcode the minimum feerate.
pub const LIQUID_CLAIM_TX_FEERATE_MSAT: f32 = 100.0;
pub const DEFAULT_DATA_DIR: &str = ".data";
pub(crate) trait ChainService: Send + Sync + BlockchainBackend {}
@@ -224,19 +219,20 @@ impl LiquidSdk {
}
}
update = updates_stream.recv() => match update {
Ok(boltzv2::Update { id, status }) => {
Ok(update) => {
let _ = cloned.sync().await;
match cloned.persister.fetch_send_swap_by_id(&id) {
let id = update.id();
match cloned.persister.fetch_send_swap_by_id(id) {
Ok(Some(_)) => {
match cloned.send_swap_state_handler.on_new_status(&status, &id).await {
match cloned.send_swap_state_handler.on_new_status(&update).await {
Ok(_) => info!("Successfully handled Send Swap {id} update"),
Err(e) => error!("Failed to handle Send Swap {id} update: {e}")
}
}
_ => {
match cloned.persister.fetch_receive_swap(&id) {
match cloned.persister.fetch_receive_swap(id) {
Ok(Some(_)) => {
match cloned.receive_swap_state_handler.on_new_status(&status, &id).await {
match cloned.receive_swap_state_handler.on_new_status(&update).await {
Ok(_) => info!("Successfully handled Receive Swap {id} update"),
Err(e) => error!("Failed to handle Receive Swap {id} update: {e}")
}
@@ -685,7 +681,7 @@ impl LiquidSdk {
self.emit_payment_updated(Some(tx_id)).await?; // Emit Pending event
Ok(SendPaymentResponse {
payment: Payment::from(tx_data, None),
payment: Payment::from_tx_data(tx_data, None),
})
}
@@ -958,7 +954,10 @@ impl LiquidSdk {
.list_payments()
.await?
.into_iter()
.map(|p| (p.tx_id.clone(), p))
.filter_map(|payment| {
let tx_id = payment.tx_id.clone();
tx_id.map(|tx_id| (tx_id, payment))
})
.collect();
if with_scan {
self.onchain_wallet.full_scan().await?;
@@ -989,7 +988,7 @@ impl LiquidSdk {
if let Some(swap) = pending_receive_swaps_by_claim_tx_id.get(&tx_id) {
if is_tx_confirmed {
self.receive_swap_state_handler
.update_swap_info(&swap.id, Complete, None)
.update_swap_info(&swap.id, Complete, None, None)
.await?;
}
} else if let Some(swap) = pending_send_swaps_by_refund_tx_id.get(&tx_id) {
@@ -1027,7 +1026,7 @@ impl LiquidSdk {
pub async fn list_payments(&self) -> Result<Vec<Payment>, PaymentError> {
self.ensure_is_started().await?;
let mut payments: Vec<Payment> = self.persister.get_payments()?.values().cloned().collect();
let mut payments: Vec<Payment> = self.persister.get_payments()?;
payments.sort_by_key(|p| p.timestamp);
Ok(payments)
}

View File

@@ -1,6 +1,7 @@
use std::{str::FromStr, sync::Arc};
use anyhow::{anyhow, Result};
use boltz_client::swaps::boltzv2;
use boltz_client::swaps::{boltz::SubSwapStates, boltzv2::CreateSubmarineResponse};
use boltz_client::util::secrets::Preimage;
use boltz_client::{Amount, Bolt11Invoice, ToHex};
@@ -55,7 +56,9 @@ impl SendSwapStateHandler {
}
/// Handles status updates from Boltz for Send swaps
pub(crate) async fn on_new_status(&self, swap_state: &str, id: &str) -> Result<()> {
pub(crate) async fn on_new_status(&self, update: &boltzv2::Update) -> Result<()> {
let id = update.id();
let swap_state = update.status();
let swap = self
.persister
.fetch_send_swap_by_id(id)?

View File

@@ -1,10 +1,9 @@
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::Result;
use lwk_wollet::elements::{LockTime, LockTime::*};
use crate::error::PaymentError;
use anyhow::Result;
use lwk_wollet::elements::LockTime::{self, *};
pub(crate) fn now() -> u32 {
SystemTime::now()

View File

@@ -849,7 +849,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final arr = raw as List<dynamic>;
if (arr.length != 10) throw Exception('unexpected arr length: expect 10 but see ${arr.length}');
return Payment(
txId: dco_decode_String(arr[0]),
txId: dco_decode_opt_String(arr[0]),
swapId: dco_decode_opt_String(arr[1]),
timestamp: dco_decode_u_32(arr[2]),
amountSat: dco_decode_u_64(arr[3]),
@@ -1401,7 +1401,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@protected
Payment sse_decode_payment(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_txId = sse_decode_String(deserializer);
var var_txId = sse_decode_opt_String(deserializer);
var var_swapId = sse_decode_opt_String(deserializer);
var var_timestamp = sse_decode_u_32(deserializer);
var var_amountSat = sse_decode_u_64(deserializer);
@@ -1968,7 +1968,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@protected
void sse_encode_payment(Payment self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String(self.txId, serializer);
sse_encode_opt_String(self.txId, serializer);
sse_encode_opt_String(self.swapId, serializer);
sse_encode_u_32(self.timestamp, serializer);
sse_encode_u_64(self.amountSat, serializer);

View File

@@ -714,7 +714,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void cst_api_fill_to_wire_payment(Payment apiObj, wire_cst_payment wireObj) {
wireObj.tx_id = cst_encode_String(apiObj.txId);
wireObj.tx_id = cst_encode_opt_String(apiObj.txId);
wireObj.swap_id = cst_encode_opt_String(apiObj.swapId);
wireObj.timestamp = cst_encode_u_32(apiObj.timestamp);
wireObj.amount_sat = cst_encode_u_64(apiObj.amountSat);
@@ -1935,5 +1935,3 @@ final class wire_cst_receive_payment_response extends ffi.Struct {
final class wire_cst_send_payment_response extends ffi.Struct {
external wire_cst_payment payment;
}
const double LIQUID_CLAIM_TX_FEERATE_MSAT = 100.0;

View File

@@ -245,8 +245,7 @@ enum Network {
///
/// By default, this is an onchain tx. It may represent a swap, if swap metadata is available.
class Payment {
/// The tx ID of the onchain transaction
final String txId;
final String? txId;
/// The swap ID, if any swap is associated with this payment
final String? swapId;
@@ -296,7 +295,7 @@ class Payment {
final PaymentState status;
const Payment({
required this.txId,
this.txId,
this.swapId,
required this.timestamp,
required this.amountSat,

View File

@@ -898,5 +898,3 @@ final class wire_cst_send_payment_response extends ffi.Struct {
/// EXTRA BEGIN
typedef WireSyncRust2DartDco = ffi.Pointer<DartCObject>;
const double LIQUID_CLAIM_TX_FEERATE_MSAT = 100.0;

View File

@@ -274,7 +274,6 @@ fun asPayment(payment: ReadableMap): Payment? {
if (!validateMandatoryFields(
payment,
arrayOf(
"txId",
"timestamp",
"amountSat",
"feesSat",
@@ -285,7 +284,7 @@ fun asPayment(payment: ReadableMap): Payment? {
) {
return null
}
val txId = payment.getString("txId")!!
val txId = if (hasNonNullKey(payment, "txId")) payment.getString("txId") else null
val swapId = if (hasNonNullKey(payment, "swapId")) payment.getString("swapId") else null
val timestamp = payment.getInt("timestamp").toUInt()
val amountSat = payment.getDouble("amountSat").toULong()

View File

@@ -324,8 +324,12 @@ enum BreezLiquidSDKMapper {
}
static func asPayment(payment: [String: Any?]) throws -> Payment {
guard let txId = payment["txId"] as? String else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "txId", typeName: "Payment"))
var txId: String?
if hasNonNilKey(data: payment, key: "txId") {
guard let txIdTmp = payment["txId"] as? String else {
throw LiquidSdkError.Generic(message: errUnexpectedValue(fieldName: "txId"))
}
txId = txIdTmp
}
var swapId: String?
if hasNonNilKey(data: payment, key: "swapId") {
@@ -390,7 +394,7 @@ enum BreezLiquidSDKMapper {
static func dictionaryOf(payment: Payment) -> [String: Any?] {
return [
"txId": payment.txId,
"txId": payment.txId == nil ? nil : payment.txId,
"swapId": payment.swapId == nil ? nil : payment.swapId,
"timestamp": payment.timestamp,
"amountSat": payment.amountSat,

View File

@@ -64,7 +64,7 @@ export interface LogEntry {
}
export interface Payment {
txId: string
txId?: string
swapId?: string
timestamp: number
amountSat: number