Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ca8632d
Breez SDK token support
danielgranhao Sep 4, 2025
0a449a1
Use transaction timestamp for syncing token payments
danielgranhao Sep 9, 2025
33b6edf
Adjust frb mirrors
danielgranhao Sep 9, 2025
8b9c64c
Address review
danielgranhao Sep 9, 2025
c65b040
Sync using sparkscan
danielgranhao Sep 12, 2025
c0d5deb
Update flutter bindings
danielgranhao Sep 15, 2025
0a421d5
Small adjustments
danielgranhao Sep 15, 2025
cf0e07d
Use sparkscan client for to workaround CORS issue
danielgranhao Sep 16, 2025
086e03f
Use transfer id as id of outgoing lightning payments
danielgranhao Sep 16, 2025
be605ac
Use legacy address with sparkscan
danielgranhao Sep 16, 2025
9c70b27
Fix rebase issue
danielgranhao Oct 2, 2025
7016c8c
Revert to bitcoin/token scan
danielgranhao Oct 6, 2025
74abf2b
Extract sync implementation to service
danielgranhao Oct 6, 2025
01d46a6
Stop token syncing when we see an already stored finalized payment
danielgranhao Oct 6, 2025
506e218
Fix send token payment id forever pending
danielgranhao Oct 6, 2025
3544555
Sync to the last synced token payment id
danielgranhao Oct 6, 2025
3b45258
Do not group outputs and use hash:vout id format
danielgranhao Oct 6, 2025
dc5ab70
Remove sparkscan leftovers
danielgranhao Oct 6, 2025
8daae7d
Rollback payment status filtering in storage
danielgranhao Oct 6, 2025
bb6f26f
Rollback legacy hrp support
danielgranhao Oct 6, 2025
cb23f13
Rollback querying specific transfers
danielgranhao Oct 6, 2025
2d78b8a
Rollback unwanted bitcoin sync changes
danielgranhao Oct 6, 2025
a8eb52a
Address review
danielgranhao Oct 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions crates/breez-sdk/cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,14 @@ pub enum Command {
#[arg(short = 'r', long)]
payment_request: String,

/// Optional amount to pay in satoshis
/// Optional amount to pay. By default is denominated in sats.
/// If a token identifier is provided, the amount will be denominated in the token base units.
#[arg(short = 'a', long)]
amount: Option<u64>,

/// Optional token identifier. May only be provided if the payment request is a spark address.
#[arg(short = 't', long)]
token_identifier: Option<String>,
},

/// Pay using LNURL
Expand Down Expand Up @@ -285,11 +290,13 @@ pub(crate) async fn execute_command(
Command::Pay {
payment_request,
amount,
token_identifier,
} => {
let prepared_payment = sdk
.prepare_send_payment(PrepareSendPaymentRequest {
payment_request,
amount_sats: amount,
amount,
token_identifier,
})
.await;

Expand Down
1 change: 1 addition & 0 deletions crates/breez-sdk/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod models;
mod persist;
mod sdk;
mod sdk_builder;
mod sync;
mod utils;

#[cfg(feature = "uniffi")]
Expand Down
6 changes: 3 additions & 3 deletions crates/breez-sdk/core/src/lnurl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ impl LnurlServerClient for ReqwestLnurlServerClient {
&self,
) -> Result<Option<RecoverLnurlPayResponse>, LnurlServerError> {
// Get the pubkey from the wallet
let spark_address = self.wallet.get_spark_address().await.map_err(|e| {
let spark_address = self.wallet.get_spark_address().map_err(|e| {
LnurlServerError::SigningError(format!("Failed to get spark address: {e}"))
})?;
let pubkey = spark_address.identity_public_key;
Expand Down Expand Up @@ -183,7 +183,7 @@ impl LnurlServerClient for ReqwestLnurlServerClient {
request: &RegisterLightningAddressRequest,
) -> Result<RegisterLnurlPayResponse, LnurlServerError> {
// Get the pubkey from the wallet
let spark_address = self.wallet.get_spark_address().await.map_err(|e| {
let spark_address = self.wallet.get_spark_address().map_err(|e| {
LnurlServerError::SigningError(format!("Failed to get spark address: {e}"))
})?;
let pubkey = spark_address.identity_public_key;
Expand Down Expand Up @@ -236,7 +236,7 @@ impl LnurlServerClient for ReqwestLnurlServerClient {
request: &UnregisterLightningAddressRequest,
) -> Result<(), LnurlServerError> {
// Get the pubkey from the wallet
let spark_address = self.wallet.get_spark_address().await.map_err(|e| {
let spark_address = self.wallet.get_spark_address().map_err(|e| {
LnurlServerError::SigningError(format!("Failed to get spark address: {e}"))
})?;
let pubkey = spark_address.identity_public_key;
Expand Down
317 changes: 317 additions & 0 deletions crates/breez-sdk/core/src/models/adaptors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
use std::time::UNIX_EPOCH;

use breez_sdk_common::input;
use spark_wallet::{
CoopExitFeeQuote, CoopExitSpeedFeeQuote, ExitSpeed, LightningSendPayment, LightningSendStatus,
Network as SparkNetwork, SspUserRequest, TokenTransactionStatus, TransferDirection,
TransferStatus, TransferType, WalletTransfer,
};

use crate::{
Fee, Network, OnchainConfirmationSpeed, Payment, PaymentDetails, PaymentMethod, PaymentStatus,
PaymentType, SdkError, SendOnchainFeeQuote, SendOnchainSpeedFeeQuote, TokenBalance,
TokenMetadata,
};

impl From<TransferType> for PaymentMethod {
fn from(value: TransferType) -> Self {
match value {
TransferType::PreimageSwap => PaymentMethod::Lightning,
TransferType::CooperativeExit => PaymentMethod::Withdraw,
TransferType::Transfer => PaymentMethod::Spark,
TransferType::UtxoSwap => PaymentMethod::Deposit,
_ => PaymentMethod::Unknown,
}
}
}

impl TryFrom<SspUserRequest> for PaymentDetails {
type Error = SdkError;
fn try_from(user_request: SspUserRequest) -> Result<Self, Self::Error> {
let details = match user_request {
SspUserRequest::CoopExitRequest(request) => PaymentDetails::Withdraw {
tx_id: request.coop_exit_txid,
},
SspUserRequest::LeavesSwapRequest(_) => PaymentDetails::Spark,
SspUserRequest::LightningReceiveRequest(request) => {
let invoice_details = input::parse_invoice(&request.invoice.encoded_invoice)
.ok_or(SdkError::Generic(
"Invalid invoice in SspUserRequest::LightningReceiveRequest".to_string(),
))?;
PaymentDetails::Lightning {
description: invoice_details.description,
preimage: request.lightning_receive_payment_preimage,
invoice: request.invoice.encoded_invoice,
payment_hash: request.invoice.payment_hash,
destination_pubkey: invoice_details.payee_pubkey,
lnurl_pay_info: None,
}
}
SspUserRequest::LightningSendRequest(request) => {
let invoice_details =
input::parse_invoice(&request.encoded_invoice).ok_or(SdkError::Generic(
"Invalid invoice in SspUserRequest::LightningSendRequest".to_string(),
))?;
PaymentDetails::Lightning {
description: invoice_details.description,
preimage: request.lightning_send_payment_preimage,
invoice: request.encoded_invoice,
payment_hash: invoice_details.payment_hash,
destination_pubkey: invoice_details.payee_pubkey,
lnurl_pay_info: None,
}
}
SspUserRequest::ClaimStaticDeposit(request) => PaymentDetails::Deposit {
tx_id: request.transaction_id,
},
};
Ok(details)
}
}

impl TryFrom<WalletTransfer> for Payment {
type Error = SdkError;
fn try_from(transfer: WalletTransfer) -> Result<Self, Self::Error> {
let payment_type = match transfer.direction {
TransferDirection::Incoming => PaymentType::Receive,
TransferDirection::Outgoing => PaymentType::Send,
};
let mut status = match transfer.status {
TransferStatus::Completed => PaymentStatus::Completed,
TransferStatus::SenderKeyTweaked
if transfer.direction == TransferDirection::Outgoing =>
{
PaymentStatus::Completed
}
TransferStatus::Expired | TransferStatus::Returned => PaymentStatus::Failed,
_ => PaymentStatus::Pending,
};
let (fees_sat, mut amount_sat): (u64, u64) = match transfer.clone().user_request {
Some(user_request) => match user_request {
SspUserRequest::LightningSendRequest(r) => {
// TODO: if we have the preimage it is not pending. This is a workaround
// until spark will implement incremental syncing based on updated time.
if r.lightning_send_payment_preimage.is_some() {
status = PaymentStatus::Completed;
}
let fee_sat = r.fee.as_sats().unwrap_or(0);
(fee_sat, transfer.total_value_sat.saturating_sub(fee_sat))
}
SspUserRequest::CoopExitRequest(r) => {
let fee_sat = r
.fee
.as_sats()
.unwrap_or(0)
.saturating_add(r.l1_broadcast_fee.as_sats().unwrap_or(0));
(fee_sat, transfer.total_value_sat.saturating_sub(fee_sat))
}
SspUserRequest::ClaimStaticDeposit(r) => {
let fee_sat = r.max_fee.as_sats().unwrap_or(0);
(fee_sat, transfer.total_value_sat)
}
_ => (0, transfer.total_value_sat),
},
None => (0, transfer.total_value_sat),
};

let details: Option<PaymentDetails> = if let Some(user_request) = transfer.user_request {
Some(user_request.try_into()?)
} else {
// in case we have a completed status without user object we want
// to keep syncing this payment
if status == PaymentStatus::Completed
&& [
TransferType::CooperativeExit,
TransferType::PreimageSwap,
TransferType::UtxoSwap,
]
.contains(&transfer.transfer_type)
{
status = PaymentStatus::Pending;
}
amount_sat = transfer.total_value_sat;
None
};

Ok(Payment {
id: transfer.id.to_string(),
payment_type,
status,
amount: amount_sat,
fees: fees_sat,
timestamp: match transfer.created_at.map(|t| t.duration_since(UNIX_EPOCH)) {
Some(Ok(duration)) => duration.as_secs(),
_ => 0,
},
method: transfer.transfer_type.into(),
details,
})
}
}

impl Payment {
pub fn from_lightning(
payment: LightningSendPayment,
amount_sat: u64,
transfer_id: String,
) -> Result<Self, SdkError> {
let mut status = match payment.status {
LightningSendStatus::LightningPaymentSucceeded => PaymentStatus::Completed,
LightningSendStatus::LightningPaymentFailed
| LightningSendStatus::TransferFailed
| LightningSendStatus::PreimageProvidingFailed
| LightningSendStatus::UserSwapReturnFailed
| LightningSendStatus::UserSwapReturned => PaymentStatus::Failed,
_ => PaymentStatus::Pending,
};
if payment.payment_preimage.is_some() {
status = PaymentStatus::Completed;
}

let invoice_details = input::parse_invoice(&payment.encoded_invoice).ok_or(
SdkError::Generic("Invalid invoice in LightnintSendPayment".to_string()),
)?;
let details = PaymentDetails::Lightning {
description: invoice_details.description,
preimage: payment.payment_preimage,
invoice: payment.encoded_invoice,
payment_hash: invoice_details.payment_hash,
destination_pubkey: invoice_details.payee_pubkey,
lnurl_pay_info: None,
};

Ok(Payment {
id: transfer_id,
payment_type: PaymentType::Send,
status,
amount: amount_sat,
fees: payment.fee_sat,
timestamp: payment.created_at.cast_unsigned(),
method: PaymentMethod::Lightning,
details: Some(details),
})
}
}

impl From<Network> for SparkNetwork {
fn from(network: Network) -> Self {
match network {
Network::Mainnet => SparkNetwork::Mainnet,
Network::Regtest => SparkNetwork::Regtest,
}
}
}

impl From<Fee> for spark_wallet::Fee {
fn from(fee: Fee) -> Self {
match fee {
Fee::Fixed { amount } => spark_wallet::Fee::Fixed { amount },
Fee::Rate { sat_per_vbyte } => spark_wallet::Fee::Rate { sat_per_vbyte },
}
}
}

impl From<spark_wallet::TokenBalance> for TokenBalance {
fn from(value: spark_wallet::TokenBalance) -> Self {
Self {
balance: value.balance.try_into().unwrap_or_default(), // balance will be changed to u128 or similar
token_metadata: value.token_metadata.into(),
}
}
}

impl From<spark_wallet::TokenMetadata> for TokenMetadata {
fn from(value: spark_wallet::TokenMetadata) -> Self {
Self {
identifier: value.identifier,
issuer_public_key: hex::encode(value.issuer_public_key.serialize()),
name: value.name,
ticker: value.ticker,
decimals: value.decimals,
max_supply: value.max_supply.try_into().unwrap_or_default(), // max_supply will be changed to u128 or similar
is_freezable: value.is_freezable,
}
}
}

impl From<CoopExitFeeQuote> for SendOnchainFeeQuote {
fn from(value: CoopExitFeeQuote) -> Self {
Self {
id: value.id,
expires_at: value.expires_at,
speed_fast: value.speed_fast.into(),
speed_medium: value.speed_medium.into(),
speed_slow: value.speed_slow.into(),
}
}
}

impl From<SendOnchainFeeQuote> for CoopExitFeeQuote {
fn from(value: SendOnchainFeeQuote) -> Self {
Self {
id: value.id,
expires_at: value.expires_at,
speed_fast: value.speed_fast.into(),
speed_medium: value.speed_medium.into(),
speed_slow: value.speed_slow.into(),
}
}
}

impl From<CoopExitSpeedFeeQuote> for SendOnchainSpeedFeeQuote {
fn from(value: CoopExitSpeedFeeQuote) -> Self {
Self {
user_fee_sat: value.user_fee_sat,
l1_broadcast_fee_sat: value.l1_broadcast_fee_sat,
}
}
}

impl From<SendOnchainSpeedFeeQuote> for CoopExitSpeedFeeQuote {
fn from(value: SendOnchainSpeedFeeQuote) -> Self {
Self {
user_fee_sat: value.user_fee_sat,
l1_broadcast_fee_sat: value.l1_broadcast_fee_sat,
}
}
}

impl From<OnchainConfirmationSpeed> for ExitSpeed {
fn from(speed: OnchainConfirmationSpeed) -> Self {
match speed {
OnchainConfirmationSpeed::Fast => ExitSpeed::Fast,
OnchainConfirmationSpeed::Medium => ExitSpeed::Medium,
OnchainConfirmationSpeed::Slow => ExitSpeed::Slow,
}
}
}

impl From<ExitSpeed> for OnchainConfirmationSpeed {
fn from(speed: ExitSpeed) -> Self {
match speed {
ExitSpeed::Fast => OnchainConfirmationSpeed::Fast,
ExitSpeed::Medium => OnchainConfirmationSpeed::Medium,
ExitSpeed::Slow => OnchainConfirmationSpeed::Slow,
}
}
}

impl PaymentStatus {
pub(crate) fn from_token_transaction_status(
status: TokenTransactionStatus,
is_transfer_transaction: bool,
) -> Self {
match status {
TokenTransactionStatus::Started
| TokenTransactionStatus::Revealed
| TokenTransactionStatus::Unknown => PaymentStatus::Pending,
TokenTransactionStatus::Signed if is_transfer_transaction => PaymentStatus::Pending,
TokenTransactionStatus::Finalized | TokenTransactionStatus::Signed => {
PaymentStatus::Completed
}
TokenTransactionStatus::StartedCancelled | TokenTransactionStatus::SignedCancelled => {
PaymentStatus::Failed
}
}
}
}
Loading
Loading