Skip to content

Commit aea3bd9

Browse files
Breez SDK token support (#199)
* Breez SDK token support * Use transaction timestamp for syncing token payments * Adjust frb mirrors * Address review * Sync using sparkscan * Update flutter bindings * Small adjustments * Use sparkscan client for to workaround CORS issue * Use transfer id as id of outgoing lightning payments * Use legacy address with sparkscan * Fix rebase issue * Revert to bitcoin/token scan * Extract sync implementation to service * Stop token syncing when we see an already stored finalized payment * Fix send token payment id forever pending * Sync to the last synced token payment id * Do not group outputs and use hash:vout id format * Remove sparkscan leftovers * Rollback payment status filtering in storage * Rollback legacy hrp support * Rollback querying specific transfers * Rollback unwanted bitcoin sync changes * Address review
1 parent aa7b458 commit aea3bd9

File tree

30 files changed

+1962
-757
lines changed

30 files changed

+1962
-757
lines changed

crates/breez-sdk/cli/src/commands.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,14 @@ pub enum Command {
6363
#[arg(short = 'r', long)]
6464
payment_request: String,
6565

66-
/// Optional amount to pay in satoshis
66+
/// Optional amount to pay. By default is denominated in sats.
67+
/// If a token identifier is provided, the amount will be denominated in the token base units.
6768
#[arg(short = 'a', long)]
6869
amount: Option<u64>,
70+
71+
/// Optional token identifier. May only be provided if the payment request is a spark address.
72+
#[arg(short = 't', long)]
73+
token_identifier: Option<String>,
6974
},
7075

7176
/// Pay using LNURL
@@ -285,11 +290,13 @@ pub(crate) async fn execute_command(
285290
Command::Pay {
286291
payment_request,
287292
amount,
293+
token_identifier,
288294
} => {
289295
let prepared_payment = sdk
290296
.prepare_send_payment(PrepareSendPaymentRequest {
291297
payment_request,
292-
amount_sats: amount,
298+
amount,
299+
token_identifier,
293300
})
294301
.await;
295302

crates/breez-sdk/core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod models;
99
mod persist;
1010
mod sdk;
1111
mod sdk_builder;
12+
mod sync;
1213
mod utils;
1314

1415
#[cfg(feature = "uniffi")]

crates/breez-sdk/core/src/lnurl.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ impl LnurlServerClient for ReqwestLnurlServerClient {
134134
&self,
135135
) -> Result<Option<RecoverLnurlPayResponse>, LnurlServerError> {
136136
// Get the pubkey from the wallet
137-
let spark_address = self.wallet.get_spark_address().await.map_err(|e| {
137+
let spark_address = self.wallet.get_spark_address().map_err(|e| {
138138
LnurlServerError::SigningError(format!("Failed to get spark address: {e}"))
139139
})?;
140140
let pubkey = spark_address.identity_public_key;
@@ -183,7 +183,7 @@ impl LnurlServerClient for ReqwestLnurlServerClient {
183183
request: &RegisterLightningAddressRequest,
184184
) -> Result<RegisterLnurlPayResponse, LnurlServerError> {
185185
// Get the pubkey from the wallet
186-
let spark_address = self.wallet.get_spark_address().await.map_err(|e| {
186+
let spark_address = self.wallet.get_spark_address().map_err(|e| {
187187
LnurlServerError::SigningError(format!("Failed to get spark address: {e}"))
188188
})?;
189189
let pubkey = spark_address.identity_public_key;
@@ -236,7 +236,7 @@ impl LnurlServerClient for ReqwestLnurlServerClient {
236236
request: &UnregisterLightningAddressRequest,
237237
) -> Result<(), LnurlServerError> {
238238
// Get the pubkey from the wallet
239-
let spark_address = self.wallet.get_spark_address().await.map_err(|e| {
239+
let spark_address = self.wallet.get_spark_address().map_err(|e| {
240240
LnurlServerError::SigningError(format!("Failed to get spark address: {e}"))
241241
})?;
242242
let pubkey = spark_address.identity_public_key;
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
use std::time::UNIX_EPOCH;
2+
3+
use breez_sdk_common::input;
4+
use spark_wallet::{
5+
CoopExitFeeQuote, CoopExitSpeedFeeQuote, ExitSpeed, LightningSendPayment, LightningSendStatus,
6+
Network as SparkNetwork, SspUserRequest, TokenTransactionStatus, TransferDirection,
7+
TransferStatus, TransferType, WalletTransfer,
8+
};
9+
10+
use crate::{
11+
Fee, Network, OnchainConfirmationSpeed, Payment, PaymentDetails, PaymentMethod, PaymentStatus,
12+
PaymentType, SdkError, SendOnchainFeeQuote, SendOnchainSpeedFeeQuote, TokenBalance,
13+
TokenMetadata,
14+
};
15+
16+
impl From<TransferType> for PaymentMethod {
17+
fn from(value: TransferType) -> Self {
18+
match value {
19+
TransferType::PreimageSwap => PaymentMethod::Lightning,
20+
TransferType::CooperativeExit => PaymentMethod::Withdraw,
21+
TransferType::Transfer => PaymentMethod::Spark,
22+
TransferType::UtxoSwap => PaymentMethod::Deposit,
23+
_ => PaymentMethod::Unknown,
24+
}
25+
}
26+
}
27+
28+
impl TryFrom<SspUserRequest> for PaymentDetails {
29+
type Error = SdkError;
30+
fn try_from(user_request: SspUserRequest) -> Result<Self, Self::Error> {
31+
let details = match user_request {
32+
SspUserRequest::CoopExitRequest(request) => PaymentDetails::Withdraw {
33+
tx_id: request.coop_exit_txid,
34+
},
35+
SspUserRequest::LeavesSwapRequest(_) => PaymentDetails::Spark,
36+
SspUserRequest::LightningReceiveRequest(request) => {
37+
let invoice_details = input::parse_invoice(&request.invoice.encoded_invoice)
38+
.ok_or(SdkError::Generic(
39+
"Invalid invoice in SspUserRequest::LightningReceiveRequest".to_string(),
40+
))?;
41+
PaymentDetails::Lightning {
42+
description: invoice_details.description,
43+
preimage: request.lightning_receive_payment_preimage,
44+
invoice: request.invoice.encoded_invoice,
45+
payment_hash: request.invoice.payment_hash,
46+
destination_pubkey: invoice_details.payee_pubkey,
47+
lnurl_pay_info: None,
48+
}
49+
}
50+
SspUserRequest::LightningSendRequest(request) => {
51+
let invoice_details =
52+
input::parse_invoice(&request.encoded_invoice).ok_or(SdkError::Generic(
53+
"Invalid invoice in SspUserRequest::LightningSendRequest".to_string(),
54+
))?;
55+
PaymentDetails::Lightning {
56+
description: invoice_details.description,
57+
preimage: request.lightning_send_payment_preimage,
58+
invoice: request.encoded_invoice,
59+
payment_hash: invoice_details.payment_hash,
60+
destination_pubkey: invoice_details.payee_pubkey,
61+
lnurl_pay_info: None,
62+
}
63+
}
64+
SspUserRequest::ClaimStaticDeposit(request) => PaymentDetails::Deposit {
65+
tx_id: request.transaction_id,
66+
},
67+
};
68+
Ok(details)
69+
}
70+
}
71+
72+
impl TryFrom<WalletTransfer> for Payment {
73+
type Error = SdkError;
74+
fn try_from(transfer: WalletTransfer) -> Result<Self, Self::Error> {
75+
let payment_type = match transfer.direction {
76+
TransferDirection::Incoming => PaymentType::Receive,
77+
TransferDirection::Outgoing => PaymentType::Send,
78+
};
79+
let mut status = match transfer.status {
80+
TransferStatus::Completed => PaymentStatus::Completed,
81+
TransferStatus::SenderKeyTweaked
82+
if transfer.direction == TransferDirection::Outgoing =>
83+
{
84+
PaymentStatus::Completed
85+
}
86+
TransferStatus::Expired | TransferStatus::Returned => PaymentStatus::Failed,
87+
_ => PaymentStatus::Pending,
88+
};
89+
let (fees_sat, mut amount_sat): (u64, u64) = match transfer.clone().user_request {
90+
Some(user_request) => match user_request {
91+
SspUserRequest::LightningSendRequest(r) => {
92+
// TODO: if we have the preimage it is not pending. This is a workaround
93+
// until spark will implement incremental syncing based on updated time.
94+
if r.lightning_send_payment_preimage.is_some() {
95+
status = PaymentStatus::Completed;
96+
}
97+
let fee_sat = r.fee.as_sats().unwrap_or(0);
98+
(fee_sat, transfer.total_value_sat.saturating_sub(fee_sat))
99+
}
100+
SspUserRequest::CoopExitRequest(r) => {
101+
let fee_sat = r
102+
.fee
103+
.as_sats()
104+
.unwrap_or(0)
105+
.saturating_add(r.l1_broadcast_fee.as_sats().unwrap_or(0));
106+
(fee_sat, transfer.total_value_sat.saturating_sub(fee_sat))
107+
}
108+
SspUserRequest::ClaimStaticDeposit(r) => {
109+
let fee_sat = r.max_fee.as_sats().unwrap_or(0);
110+
(fee_sat, transfer.total_value_sat)
111+
}
112+
_ => (0, transfer.total_value_sat),
113+
},
114+
None => (0, transfer.total_value_sat),
115+
};
116+
117+
let details: Option<PaymentDetails> = if let Some(user_request) = transfer.user_request {
118+
Some(user_request.try_into()?)
119+
} else {
120+
// in case we have a completed status without user object we want
121+
// to keep syncing this payment
122+
if status == PaymentStatus::Completed
123+
&& [
124+
TransferType::CooperativeExit,
125+
TransferType::PreimageSwap,
126+
TransferType::UtxoSwap,
127+
]
128+
.contains(&transfer.transfer_type)
129+
{
130+
status = PaymentStatus::Pending;
131+
}
132+
amount_sat = transfer.total_value_sat;
133+
None
134+
};
135+
136+
Ok(Payment {
137+
id: transfer.id.to_string(),
138+
payment_type,
139+
status,
140+
amount: amount_sat,
141+
fees: fees_sat,
142+
timestamp: match transfer.created_at.map(|t| t.duration_since(UNIX_EPOCH)) {
143+
Some(Ok(duration)) => duration.as_secs(),
144+
_ => 0,
145+
},
146+
method: transfer.transfer_type.into(),
147+
details,
148+
})
149+
}
150+
}
151+
152+
impl Payment {
153+
pub fn from_lightning(
154+
payment: LightningSendPayment,
155+
amount_sat: u64,
156+
transfer_id: String,
157+
) -> Result<Self, SdkError> {
158+
let mut status = match payment.status {
159+
LightningSendStatus::LightningPaymentSucceeded => PaymentStatus::Completed,
160+
LightningSendStatus::LightningPaymentFailed
161+
| LightningSendStatus::TransferFailed
162+
| LightningSendStatus::PreimageProvidingFailed
163+
| LightningSendStatus::UserSwapReturnFailed
164+
| LightningSendStatus::UserSwapReturned => PaymentStatus::Failed,
165+
_ => PaymentStatus::Pending,
166+
};
167+
if payment.payment_preimage.is_some() {
168+
status = PaymentStatus::Completed;
169+
}
170+
171+
let invoice_details = input::parse_invoice(&payment.encoded_invoice).ok_or(
172+
SdkError::Generic("Invalid invoice in LightnintSendPayment".to_string()),
173+
)?;
174+
let details = PaymentDetails::Lightning {
175+
description: invoice_details.description,
176+
preimage: payment.payment_preimage,
177+
invoice: payment.encoded_invoice,
178+
payment_hash: invoice_details.payment_hash,
179+
destination_pubkey: invoice_details.payee_pubkey,
180+
lnurl_pay_info: None,
181+
};
182+
183+
Ok(Payment {
184+
id: transfer_id,
185+
payment_type: PaymentType::Send,
186+
status,
187+
amount: amount_sat,
188+
fees: payment.fee_sat,
189+
timestamp: payment.created_at.cast_unsigned(),
190+
method: PaymentMethod::Lightning,
191+
details: Some(details),
192+
})
193+
}
194+
}
195+
196+
impl From<Network> for SparkNetwork {
197+
fn from(network: Network) -> Self {
198+
match network {
199+
Network::Mainnet => SparkNetwork::Mainnet,
200+
Network::Regtest => SparkNetwork::Regtest,
201+
}
202+
}
203+
}
204+
205+
impl From<Fee> for spark_wallet::Fee {
206+
fn from(fee: Fee) -> Self {
207+
match fee {
208+
Fee::Fixed { amount } => spark_wallet::Fee::Fixed { amount },
209+
Fee::Rate { sat_per_vbyte } => spark_wallet::Fee::Rate { sat_per_vbyte },
210+
}
211+
}
212+
}
213+
214+
impl From<spark_wallet::TokenBalance> for TokenBalance {
215+
fn from(value: spark_wallet::TokenBalance) -> Self {
216+
Self {
217+
balance: value.balance.try_into().unwrap_or_default(), // balance will be changed to u128 or similar
218+
token_metadata: value.token_metadata.into(),
219+
}
220+
}
221+
}
222+
223+
impl From<spark_wallet::TokenMetadata> for TokenMetadata {
224+
fn from(value: spark_wallet::TokenMetadata) -> Self {
225+
Self {
226+
identifier: value.identifier,
227+
issuer_public_key: hex::encode(value.issuer_public_key.serialize()),
228+
name: value.name,
229+
ticker: value.ticker,
230+
decimals: value.decimals,
231+
max_supply: value.max_supply.try_into().unwrap_or_default(), // max_supply will be changed to u128 or similar
232+
is_freezable: value.is_freezable,
233+
}
234+
}
235+
}
236+
237+
impl From<CoopExitFeeQuote> for SendOnchainFeeQuote {
238+
fn from(value: CoopExitFeeQuote) -> Self {
239+
Self {
240+
id: value.id,
241+
expires_at: value.expires_at,
242+
speed_fast: value.speed_fast.into(),
243+
speed_medium: value.speed_medium.into(),
244+
speed_slow: value.speed_slow.into(),
245+
}
246+
}
247+
}
248+
249+
impl From<SendOnchainFeeQuote> for CoopExitFeeQuote {
250+
fn from(value: SendOnchainFeeQuote) -> Self {
251+
Self {
252+
id: value.id,
253+
expires_at: value.expires_at,
254+
speed_fast: value.speed_fast.into(),
255+
speed_medium: value.speed_medium.into(),
256+
speed_slow: value.speed_slow.into(),
257+
}
258+
}
259+
}
260+
261+
impl From<CoopExitSpeedFeeQuote> for SendOnchainSpeedFeeQuote {
262+
fn from(value: CoopExitSpeedFeeQuote) -> Self {
263+
Self {
264+
user_fee_sat: value.user_fee_sat,
265+
l1_broadcast_fee_sat: value.l1_broadcast_fee_sat,
266+
}
267+
}
268+
}
269+
270+
impl From<SendOnchainSpeedFeeQuote> for CoopExitSpeedFeeQuote {
271+
fn from(value: SendOnchainSpeedFeeQuote) -> Self {
272+
Self {
273+
user_fee_sat: value.user_fee_sat,
274+
l1_broadcast_fee_sat: value.l1_broadcast_fee_sat,
275+
}
276+
}
277+
}
278+
279+
impl From<OnchainConfirmationSpeed> for ExitSpeed {
280+
fn from(speed: OnchainConfirmationSpeed) -> Self {
281+
match speed {
282+
OnchainConfirmationSpeed::Fast => ExitSpeed::Fast,
283+
OnchainConfirmationSpeed::Medium => ExitSpeed::Medium,
284+
OnchainConfirmationSpeed::Slow => ExitSpeed::Slow,
285+
}
286+
}
287+
}
288+
289+
impl From<ExitSpeed> for OnchainConfirmationSpeed {
290+
fn from(speed: ExitSpeed) -> Self {
291+
match speed {
292+
ExitSpeed::Fast => OnchainConfirmationSpeed::Fast,
293+
ExitSpeed::Medium => OnchainConfirmationSpeed::Medium,
294+
ExitSpeed::Slow => OnchainConfirmationSpeed::Slow,
295+
}
296+
}
297+
}
298+
299+
impl PaymentStatus {
300+
pub(crate) fn from_token_transaction_status(
301+
status: TokenTransactionStatus,
302+
is_transfer_transaction: bool,
303+
) -> Self {
304+
match status {
305+
TokenTransactionStatus::Started
306+
| TokenTransactionStatus::Revealed
307+
| TokenTransactionStatus::Unknown => PaymentStatus::Pending,
308+
TokenTransactionStatus::Signed if is_transfer_transaction => PaymentStatus::Pending,
309+
TokenTransactionStatus::Finalized | TokenTransactionStatus::Signed => {
310+
PaymentStatus::Completed
311+
}
312+
TokenTransactionStatus::StartedCancelled | TokenTransactionStatus::SignedCancelled => {
313+
PaymentStatus::Failed
314+
}
315+
}
316+
}
317+
}

0 commit comments

Comments
 (0)