diff --git a/crates/breez-sdk/cli/src/commands.rs b/crates/breez-sdk/cli/src/commands.rs index 85424e0a..58d67b66 100644 --- a/crates/breez-sdk/cli/src/commands.rs +++ b/crates/breez-sdk/cli/src/commands.rs @@ -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, + + /// Optional token identifier. May only be provided if the payment request is a spark address. + #[arg(short = 't', long)] + token_identifier: Option, }, /// Pay using LNURL @@ -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; diff --git a/crates/breez-sdk/core/src/lib.rs b/crates/breez-sdk/core/src/lib.rs index 8d87ef43..196e258d 100644 --- a/crates/breez-sdk/core/src/lib.rs +++ b/crates/breez-sdk/core/src/lib.rs @@ -9,6 +9,7 @@ mod models; mod persist; mod sdk; mod sdk_builder; +mod sync; mod utils; #[cfg(feature = "uniffi")] diff --git a/crates/breez-sdk/core/src/lnurl.rs b/crates/breez-sdk/core/src/lnurl.rs index cea28f15..340173b5 100644 --- a/crates/breez-sdk/core/src/lnurl.rs +++ b/crates/breez-sdk/core/src/lnurl.rs @@ -134,7 +134,7 @@ impl LnurlServerClient for ReqwestLnurlServerClient { &self, ) -> 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; @@ -183,7 +183,7 @@ impl LnurlServerClient for ReqwestLnurlServerClient { request: &RegisterLightningAddressRequest, ) -> Result { // 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; @@ -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; diff --git a/crates/breez-sdk/core/src/models/adaptors.rs b/crates/breez-sdk/core/src/models/adaptors.rs new file mode 100644 index 00000000..8327c634 --- /dev/null +++ b/crates/breez-sdk/core/src/models/adaptors.rs @@ -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 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 for PaymentDetails { + type Error = SdkError; + fn try_from(user_request: SspUserRequest) -> Result { + 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 for Payment { + type Error = SdkError; + fn try_from(transfer: WalletTransfer) -> Result { + 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 = 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 { + 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 for SparkNetwork { + fn from(network: Network) -> Self { + match network { + Network::Mainnet => SparkNetwork::Mainnet, + Network::Regtest => SparkNetwork::Regtest, + } + } +} + +impl From 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 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 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 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 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 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 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 for ExitSpeed { + fn from(speed: OnchainConfirmationSpeed) -> Self { + match speed { + OnchainConfirmationSpeed::Fast => ExitSpeed::Fast, + OnchainConfirmationSpeed::Medium => ExitSpeed::Medium, + OnchainConfirmationSpeed::Slow => ExitSpeed::Slow, + } + } +} + +impl From 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 + } + } + } +} diff --git a/crates/breez-sdk/core/src/models.rs b/crates/breez-sdk/core/src/models/mod.rs similarity index 65% rename from crates/breez-sdk/core/src/models.rs rename to crates/breez-sdk/core/src/models/mod.rs index 330b313f..beb2f69c 100644 --- a/crates/breez-sdk/core/src/models.rs +++ b/crates/breez-sdk/core/src/models/mod.rs @@ -1,6 +1,8 @@ +pub(crate) mod adaptors; + use breez_sdk_common::{ fiat::{FiatCurrency, Rate}, - input::{self, BitcoinAddressDetails, Bolt11InvoiceDetails}, + input::{BitcoinAddressDetails, Bolt11InvoiceDetails}, lnurl::pay::{LnurlPayRequestDetails, SuccessAction, SuccessActionProcessed}, network::BitcoinNetwork, }; @@ -8,15 +10,10 @@ use core::fmt; use lnurl_models::RecoverLnurlPayResponse; use serde::{Deserialize, Serialize}; use serde_json::Value; -use spark_wallet::{ - CoopExitFeeQuote, CoopExitSpeedFeeQuote, ExitSpeed, LightningSendPayment, LightningSendStatus, - Network as SparkNetwork, SspUserRequest, TransferDirection, TransferStatus, TransferType, - WalletTransfer, -}; -use std::{fmt::Display, str::FromStr, time::UNIX_EPOCH}; +use std::{collections::HashMap, fmt::Display, str::FromStr}; +use crate::error::DepositClaimError; use crate::sdk_builder::Seed; -use crate::{SdkError, error::DepositClaimError}; #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct ConnectRequest { @@ -68,6 +65,13 @@ pub enum PaymentStatus { Failed, } +impl PaymentStatus { + /// Returns true if the payment status is final (either Completed or Failed) + pub fn is_final(&self) -> bool { + matches!(self, PaymentStatus::Completed | PaymentStatus::Failed) + } +} + impl fmt::Display for PaymentStatus { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -96,6 +100,7 @@ impl FromStr for PaymentStatus { pub enum PaymentMethod { Lightning, Spark, + Token, Deposit, Withdraw, Unknown, @@ -106,6 +111,7 @@ impl Display for PaymentMethod { match self { PaymentMethod::Lightning => write!(f, "lightning"), PaymentMethod::Spark => write!(f, "spark"), + PaymentMethod::Token => write!(f, "token"), PaymentMethod::Deposit => write!(f, "deposit"), PaymentMethod::Withdraw => write!(f, "withdraw"), PaymentMethod::Unknown => write!(f, "unknown"), @@ -120,6 +126,7 @@ impl FromStr for PaymentMethod { match s { "lightning" => Ok(PaymentMethod::Lightning), "spark" => Ok(PaymentMethod::Spark), + "token" => Ok(PaymentMethod::Token), "deposit" => Ok(PaymentMethod::Deposit), "withdraw" => Ok(PaymentMethod::Withdraw), "unknown" => Ok(PaymentMethod::Unknown), @@ -128,17 +135,6 @@ impl FromStr for PaymentMethod { } } -impl From 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, - } - } -} /// Represents a payment (sent or received) #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] @@ -169,6 +165,10 @@ pub struct Payment { #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] pub enum PaymentDetails { Spark, + Token { + metadata: TokenMetadata, + tx_hash: String, + }, Lightning { /// Represents the invoice description description: Option, @@ -196,174 +196,6 @@ pub enum PaymentDetails { }, } -impl TryFrom for PaymentDetails { - type Error = SdkError; - fn try_from(user_request: SspUserRequest) -> Result { - 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 for Payment { - type Error = SdkError; - fn try_from(transfer: WalletTransfer) -> Result { - 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 = 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 { - 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), - }) - } -} - #[derive(Debug, Clone, Copy)] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] pub enum Network { @@ -380,15 +212,6 @@ impl std::fmt::Display for Network { } } -impl From for SparkNetwork { - fn from(network: Network) -> Self { - match network { - Network::Mainnet => SparkNetwork::Mainnet, - Network::Regtest => SparkNetwork::Regtest, - } - } -} - impl From for BitcoinNetwork { fn from(network: Network) -> Self { match network { @@ -448,15 +271,6 @@ impl Fee { } } -impl From 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 }, - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct DepositInfo { @@ -537,6 +351,29 @@ pub struct GetInfoRequest { pub struct GetInfoResponse { /// The balance in satoshis pub balance_sats: u64, + /// The balances of the tokens in the wallet keyed by the token identifier + pub token_balances: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct TokenBalance { + pub balance: u64, + pub token_metadata: TokenMetadata, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct TokenMetadata { + pub identifier: String, + /// Hex representation of the issuer public key + pub issuer_public_key: String, + pub name: String, + pub ticker: String, + /// Number of decimals the token uses + pub decimals: u32, + pub max_supply: u64, + pub is_freezable: bool, } /// Request to sync the wallet with the Spark network @@ -574,7 +411,12 @@ pub enum SendPaymentMethod { }, // should be replaced with the parsed invoice SparkAddress { address: String, - fee_sats: u64, + /// Fee to pay for the transaction + /// Denominated in sats if token identifier is empty, otherwise in the token base units + fee: u64, + /// The presence of this field indicates that the payment is for a token + /// If empty, it is a Bitcoin payment + token_identifier: Option, }, } @@ -588,29 +430,6 @@ pub struct SendOnchainFeeQuote { pub speed_slow: SendOnchainSpeedFeeQuote, } -impl From 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 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(), - } - } -} #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[derive(Debug, Clone, Serialize)] pub struct SendOnchainSpeedFeeQuote { @@ -624,24 +443,6 @@ impl SendOnchainSpeedFeeQuote { } } -impl From 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 for CoopExitSpeedFeeQuote { - fn from(value: SendOnchainSpeedFeeQuote) -> Self { - Self { - user_fee_sat: value.user_fee_sat, - l1_broadcast_fee_sat: value.l1_broadcast_fee_sat, - } - } -} - #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct ReceivePaymentRequest { pub payment_method: ReceivePaymentMethod, @@ -732,38 +533,29 @@ pub enum OnchainConfirmationSpeed { Slow, } -impl From for ExitSpeed { - fn from(speed: OnchainConfirmationSpeed) -> Self { - match speed { - OnchainConfirmationSpeed::Fast => ExitSpeed::Fast, - OnchainConfirmationSpeed::Medium => ExitSpeed::Medium, - OnchainConfirmationSpeed::Slow => ExitSpeed::Slow, - } - } -} - -impl From for OnchainConfirmationSpeed { - fn from(speed: ExitSpeed) -> Self { - match speed { - ExitSpeed::Fast => OnchainConfirmationSpeed::Fast, - ExitSpeed::Medium => OnchainConfirmationSpeed::Medium, - ExitSpeed::Slow => OnchainConfirmationSpeed::Slow, - } - } -} - #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct PrepareSendPaymentRequest { pub payment_request: String, + /// Amount to send. By default is denominated in sats. + /// If a token identifier is provided, the amount will be denominated in the token base units. + #[cfg_attr(feature = "uniffi", uniffi(default=None))] + pub amount: Option, + /// If provided, the payment will be for a token + /// May only be provided if the payment request is a spark address #[cfg_attr(feature = "uniffi", uniffi(default=None))] - pub amount_sats: Option, + pub token_identifier: Option, } #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct PrepareSendPaymentResponse { pub payment_method: SendPaymentMethod, - pub amount_sats: u64, + /// Amount to send. By default is denominated in sats. + /// If a token identifier is provided, the amount will be denominated in the token base units. + pub amount: u64, + /// The presence of this field indicates that the payment is for a token + /// If empty, it is a Bitcoin payment + pub token_identifier: Option, } #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] diff --git a/crates/breez-sdk/core/src/persist/mod.rs b/crates/breez-sdk/core/src/persist/mod.rs index af60fc13..67e15f8e 100644 --- a/crates/breez-sdk/core/src/persist/mod.rs +++ b/crates/breez-sdk/core/src/persist/mod.rs @@ -1,13 +1,16 @@ #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] pub(crate) mod sqlite; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use macros::async_trait; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{DepositClaimError, DepositInfo, LightningAddressInfo, LnurlPayInfo, models::Payment}; +use crate::{ + DepositClaimError, DepositInfo, LightningAddressInfo, LnurlPayInfo, TokenBalance, + models::Payment, +}; const ACCOUNT_INFO_KEY: &str = "account_info"; const LIGHTNING_ADDRESS_KEY: &str = "lightning_address"; @@ -313,11 +316,14 @@ impl ObjectCacheRepository { #[derive(Serialize, Deserialize, Default)] pub(crate) struct CachedAccountInfo { pub(crate) balance_sats: u64, + #[serde(default)] + pub(crate) token_balances: HashMap, } #[derive(Serialize, Deserialize, Default)] pub(crate) struct CachedSyncInfo { pub(crate) offset: u64, + pub(crate) last_synced_token_payment_id: Option, } #[derive(Serialize, Deserialize, Default)] @@ -332,49 +338,305 @@ pub(crate) struct StaticDepositAddress { #[cfg(feature = "test-utils")] pub mod tests { + use chrono::Utc; + use crate::{ - DepositClaimError, Payment, PaymentDetails, PaymentMethod, PaymentStatus, PaymentType, - Storage, UpdateDepositPayload, + DepositClaimError, Payment, PaymentDetails, PaymentMetadata, PaymentMethod, PaymentStatus, + PaymentType, Storage, UpdateDepositPayload, }; - use chrono::Utc; + #[allow(clippy::too_many_lines)] pub async fn test_sqlite_storage(storage: Box) { - // Create test payment - let payment = Payment { - id: "pmt123".to_string(), + use crate::models::{LnurlPayInfo, TokenMetadata}; + + // Test 1: Spark payment + let spark_payment = Payment { + id: "spark_pmt123".to_string(), payment_type: PaymentType::Send, status: PaymentStatus::Completed, amount: 100_000, fees: 1000, - timestamp: Utc::now().timestamp().try_into().unwrap(), + timestamp: 5000, method: PaymentMethod::Spark, details: Some(PaymentDetails::Spark), }; - // Insert payment - storage.insert_payment(payment.clone()).await.unwrap(); + // Test 2: Token payment + let token_metadata = TokenMetadata { + identifier: "token123".to_string(), + issuer_public_key: + "02abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab".to_string(), + name: "Test Token".to_string(), + ticker: "TTK".to_string(), + decimals: 8, + max_supply: 21_000_000, + is_freezable: false, + }; + let token_payment = Payment { + id: "token_pmt456".to_string(), + payment_type: PaymentType::Receive, + status: PaymentStatus::Pending, + amount: 50_000, + fees: 500, + timestamp: Utc::now().timestamp().try_into().unwrap(), + method: PaymentMethod::Token, + details: Some(PaymentDetails::Token { + metadata: token_metadata.clone(), + tx_hash: "tx_hash".to_string(), + }), + }; - // List payments + // Test 3: Lightning payment with full details + let metadata = PaymentMetadata { + lnurl_pay_info: Some(LnurlPayInfo { + ln_address: Some("test@example.com".to_string()), + comment: Some("Test comment".to_string()), + domain: Some("example.com".to_string()), + metadata: Some("[[\"text/plain\", \"Test metadata\"]]".to_string()), + processed_success_action: None, + raw_success_action: None, + }), + lnurl_description: None, + }; + let lightning_payment = Payment { + id: "lightning_pmt789".to_string(), + payment_type: PaymentType::Send, + status: PaymentStatus::Completed, + amount: 25_000, + fees: 250, + timestamp: Utc::now().timestamp().try_into().unwrap(), + method: PaymentMethod::Lightning, + details: Some(PaymentDetails::Lightning { + description: Some("Test lightning payment".to_string()), + preimage: Some("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab".to_string()), + invoice: "lnbc250n1pjqxyz9pp5abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567890abcdefghijklmnopqrstuvwxyz".to_string(), + payment_hash: "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321".to_string(), + destination_pubkey: "03123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01".to_string(), + lnurl_pay_info: metadata.lnurl_pay_info.clone(), + }), + }; + + // Test 4: Lightning payment with minimal details + let lightning_minimal_payment = Payment { + id: "lightning_minimal_pmt012".to_string(), + payment_type: PaymentType::Receive, + status: PaymentStatus::Failed, + amount: 10_000, + fees: 100, + timestamp: Utc::now().timestamp().try_into().unwrap(), + method: PaymentMethod::Lightning, + details: Some(PaymentDetails::Lightning { + description: None, + preimage: None, + invoice: "lnbc100n1pjqxyz9pp5def456ghi789jkl012mno345pqr678stu901vwx234yz567890abcdefghijklmnopqrstuvwxyz".to_string(), + payment_hash: "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(), + destination_pubkey: "02987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba09".to_string(), + lnurl_pay_info: None, + }), + }; + + // Test 5: Withdraw payment + let withdraw_payment = Payment { + id: "withdraw_pmt345".to_string(), + payment_type: PaymentType::Send, + status: PaymentStatus::Completed, + amount: 200_000, + fees: 2000, + timestamp: Utc::now().timestamp().try_into().unwrap(), + method: PaymentMethod::Withdraw, + details: Some(PaymentDetails::Withdraw { + tx_id: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12" + .to_string(), + }), + }; + + // Test 6: Deposit payment + let deposit_payment = Payment { + id: "deposit_pmt678".to_string(), + payment_type: PaymentType::Receive, + status: PaymentStatus::Completed, + amount: 150_000, + fees: 1500, + timestamp: Utc::now().timestamp().try_into().unwrap(), + method: PaymentMethod::Deposit, + details: Some(PaymentDetails::Deposit { + tx_id: "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321fe" + .to_string(), + }), + }; + + // Test 7: Payment with no details + let no_details_payment = Payment { + id: "no_details_pmt901".to_string(), + payment_type: PaymentType::Send, + status: PaymentStatus::Pending, + amount: 75_000, + fees: 750, + timestamp: Utc::now().timestamp().try_into().unwrap(), + method: PaymentMethod::Unknown, + details: None, + }; + + let test_payments = vec![ + spark_payment.clone(), + token_payment.clone(), + lightning_payment.clone(), + lightning_minimal_payment.clone(), + withdraw_payment.clone(), + deposit_payment.clone(), + no_details_payment.clone(), + ]; + + // Insert all payments + for payment in &test_payments { + storage.insert_payment(payment.clone()).await.unwrap(); + } + storage + .set_payment_metadata(lightning_payment.id.clone(), metadata) + .await + .unwrap(); + + // List all payments let payments = storage.list_payments(Some(0), Some(10)).await.unwrap(); - assert_eq!(payments.len(), 1); - assert_eq!(payments[0].id, payment.id); - assert_eq!(payments[0].payment_type, payment.payment_type); - assert_eq!(payments[0].status, payment.status); - assert_eq!(payments[0].amount, payment.amount); - assert_eq!(payments[0].fees, payment.fees); - assert!(matches!(payments[0].details, Some(PaymentDetails::Spark))); - - // Get payment by ID - let retrieved_payment = storage.get_payment_by_id(payment.id.clone()).await.unwrap(); - assert_eq!(retrieved_payment.id, payment.id); - assert_eq!(retrieved_payment.payment_type, payment.payment_type); - assert_eq!(retrieved_payment.status, payment.status); - assert_eq!(retrieved_payment.amount, payment.amount); - assert_eq!(retrieved_payment.fees, payment.fees); - assert!(matches!( - retrieved_payment.details, - Some(PaymentDetails::Spark) - )); + assert_eq!(payments.len(), 7); + + // Test each payment type individually + for (i, expected_payment) in test_payments.iter().enumerate() { + let retrieved_payment = storage + .get_payment_by_id(expected_payment.id.clone()) + .await + .unwrap(); + + // Basic fields + assert_eq!(retrieved_payment.id, expected_payment.id); + assert_eq!( + retrieved_payment.payment_type, + expected_payment.payment_type + ); + assert_eq!(retrieved_payment.status, expected_payment.status); + assert_eq!(retrieved_payment.amount, expected_payment.amount); + assert_eq!(retrieved_payment.fees, expected_payment.fees); + assert_eq!(retrieved_payment.method, expected_payment.method); + + // Test payment details persistence + match (&retrieved_payment.details, &expected_payment.details) { + (Some(PaymentDetails::Spark), Some(PaymentDetails::Spark)) | (None, None) => {} + ( + Some(PaymentDetails::Token { + metadata: retrieved_metadata, + tx_hash: retrieved_tx_hash, + }), + Some(PaymentDetails::Token { + metadata: expected_metadata, + tx_hash: expected_tx_hash, + }), + ) => { + assert_eq!(retrieved_metadata.identifier, expected_metadata.identifier); + assert_eq!( + retrieved_metadata.issuer_public_key, + expected_metadata.issuer_public_key + ); + assert_eq!(retrieved_metadata.name, expected_metadata.name); + assert_eq!(retrieved_metadata.ticker, expected_metadata.ticker); + assert_eq!(retrieved_metadata.decimals, expected_metadata.decimals); + assert_eq!(retrieved_metadata.max_supply, expected_metadata.max_supply); + assert_eq!( + retrieved_metadata.is_freezable, + expected_metadata.is_freezable + ); + assert_eq!(retrieved_tx_hash, expected_tx_hash); + } + ( + Some(PaymentDetails::Lightning { + description: r_description, + preimage: r_preimage, + invoice: r_invoice, + payment_hash: r_hash, + destination_pubkey: r_dest_pubkey, + lnurl_pay_info: r_lnurl, + }), + Some(PaymentDetails::Lightning { + description: e_description, + preimage: e_preimage, + invoice: e_invoice, + payment_hash: e_hash, + destination_pubkey: e_dest_pubkey, + lnurl_pay_info: e_lnurl, + }), + ) => { + assert_eq!(r_description, e_description); + assert_eq!(r_preimage, e_preimage); + assert_eq!(r_invoice, e_invoice); + assert_eq!(r_hash, e_hash); + assert_eq!(r_dest_pubkey, e_dest_pubkey); + + // Test LNURL pay info if present + match (r_lnurl, e_lnurl) { + (Some(r_info), Some(e_info)) => { + assert_eq!(r_info.ln_address, e_info.ln_address); + assert_eq!(r_info.comment, e_info.comment); + assert_eq!(r_info.domain, e_info.domain); + assert_eq!(r_info.metadata, e_info.metadata); + } + (None, None) => {} + _ => panic!( + "LNURL pay info mismatch for payment {}", + expected_payment.id + ), + } + } + ( + Some(PaymentDetails::Withdraw { tx_id: r_tx_id }), + Some(PaymentDetails::Withdraw { tx_id: e_tx_id }), + ) + | ( + Some(PaymentDetails::Deposit { tx_id: r_tx_id }), + Some(PaymentDetails::Deposit { tx_id: e_tx_id }), + ) => { + assert_eq!(r_tx_id, e_tx_id); + } + _ => panic!( + "Payment details mismatch for payment {} (index {})", + expected_payment.id, i + ), + } + } + + // Test filtering by payment type + let send_payments = payments + .iter() + .filter(|p| p.payment_type == PaymentType::Send) + .count(); + let receive_payments = payments + .iter() + .filter(|p| p.payment_type == PaymentType::Receive) + .count(); + assert_eq!(send_payments, 4); // spark, lightning, withdraw, no_details + assert_eq!(receive_payments, 3); // token, lightning_minimal, deposit + + // Test filtering by status + let completed_payments = payments + .iter() + .filter(|p| p.status == PaymentStatus::Completed) + .count(); + let pending_payments = payments + .iter() + .filter(|p| p.status == PaymentStatus::Pending) + .count(); + let failed_payments = payments + .iter() + .filter(|p| p.status == PaymentStatus::Failed) + .count(); + assert_eq!(completed_payments, 4); // spark, lightning, withdraw, deposit + assert_eq!(pending_payments, 2); // token, no_details + assert_eq!(failed_payments, 1); // lightning_minimal + + // Test filtering by method + let lightning_count = payments + .iter() + .filter(|p| p.method == PaymentMethod::Lightning) + .count(); + assert_eq!(lightning_count, 2); // lightning and lightning_minimal } pub async fn test_unclaimed_deposits_crud(storage: Box) { diff --git a/crates/breez-sdk/core/src/persist/sqlite.rs b/crates/breez-sdk/core/src/persist/sqlite.rs index 9ab174c1..00282361 100644 --- a/crates/breez-sdk/core/src/persist/sqlite.rs +++ b/crates/breez-sdk/core/src/persist/sqlite.rs @@ -1,10 +1,11 @@ +use std::path::{Path, PathBuf}; + use macros::async_trait; use rusqlite::{ Connection, Row, ToSql, params, types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}, }; use rusqlite_migration::{M, Migrations, SchemaVersion}; -use std::path::{Path, PathBuf}; use crate::{ DepositInfo, LnurlPayInfo, PaymentDetails, PaymentMethod, @@ -162,6 +163,12 @@ impl SqliteStorage { CREATE INDEX idx_payment_details_lightning_invoice ON payment_details_lightning(invoice); ", + "CREATE TABLE payment_details_token ( + payment_id TEXT PRIMARY KEY, + metadata TEXT NOT NULL, + tx_hash TEXT NOT NULL, + FOREIGN KEY (payment_id) REFERENCES payments(id) ON DELETE CASCADE + );", ] } } @@ -204,8 +211,11 @@ impl Storage for SqliteStorage { , COALESCE(l.description, pm.lnurl_description) AS lightning_description , l.preimage AS lightning_preimage , pm.lnurl_pay_info + , t.metadata AS token_metadata + , t.tx_hash AS token_tx_hash FROM payments p LEFT JOIN payment_details_lightning l ON p.id = l.payment_id + LEFT JOIN payment_details_token t ON p.id = t.payment_id LEFT JOIN payment_metadata pm ON p.id = pm.payment_id ORDER BY p.timestamp DESC LIMIT {} OFFSET {}", @@ -256,6 +266,12 @@ impl Storage for SqliteStorage { params![payment.id], )?; } + Some(PaymentDetails::Token { metadata, tx_hash }) => { + tx.execute( + "INSERT OR REPLACE INTO payment_details_token (payment_id, metadata, tx_hash) VALUES (?, ?, ?)", + params![payment.id, serde_json::to_string(&metadata)?, tx_hash], + )?; + } Some(PaymentDetails::Lightning { invoice, payment_hash, @@ -355,8 +371,11 @@ impl Storage for SqliteStorage { , COALESCE(l.description, pm.lnurl_description) AS lightning_description , l.preimage AS lightning_preimage , pm.lnurl_pay_info + , t.metadata AS token_metadata + , t.tx_hash AS token_tx_hash FROM payments p LEFT JOIN payment_details_lightning l ON p.id = l.payment_id + LEFT JOIN payment_details_token t ON p.id = t.payment_id LEFT JOIN payment_metadata pm ON p.id = pm.payment_id WHERE p.id = ?", )?; @@ -388,8 +407,11 @@ impl Storage for SqliteStorage { , COALESCE(l.description, pm.lnurl_description) AS lightning_description , l.preimage AS lightning_preimage , pm.lnurl_pay_info + , t.metadata AS token_metadata + , t.tx_hash AS token_tx_hash FROM payments p LEFT JOIN payment_details_lightning l ON p.id = l.payment_id + LEFT JOIN payment_details_token t ON p.id = t.payment_id LEFT JOIN payment_metadata pm ON p.id = pm.payment_id WHERE l.invoice = ?", )?; @@ -480,8 +502,15 @@ fn map_payment(row: &Row<'_>) -> Result { let deposit_tx_id: Option = row.get(8)?; let spark: Option = row.get(9)?; let lightning_invoice: Option = row.get(10)?; - let details = match (lightning_invoice, withdraw_tx_id, deposit_tx_id, spark) { - (Some(invoice), _, _, _) => { + let token_metadata: Option = row.get(16)?; + let details = match ( + lightning_invoice, + withdraw_tx_id, + deposit_tx_id, + spark, + token_metadata, + ) { + (Some(invoice), _, _, _, _) => { let payment_hash: String = row.get(11)?; let destination_pubkey: String = row.get(12)?; let description: Option = row.get(13)?; @@ -497,9 +526,15 @@ fn map_payment(row: &Row<'_>) -> Result { lnurl_pay_info, }) } - (_, Some(tx_id), _, _) => Some(PaymentDetails::Withdraw { tx_id }), - (_, _, Some(tx_id), _) => Some(PaymentDetails::Deposit { tx_id }), - (_, _, _, Some(_)) => Some(PaymentDetails::Spark), + (_, Some(tx_id), _, _, _) => Some(PaymentDetails::Withdraw { tx_id }), + (_, _, Some(tx_id), _, _) => Some(PaymentDetails::Deposit { tx_id }), + (_, _, _, Some(_), _) => Some(PaymentDetails::Spark), + (_, _, _, _, Some(metadata)) => Some(PaymentDetails::Token { + metadata: serde_json::from_str(&metadata).map_err(|e| { + rusqlite::Error::FromSqlConversionFailure(16, rusqlite::types::Type::Text, e.into()) + })?, + tx_hash: row.get(17)?, + }), _ => None, }; Ok(Payment { diff --git a/crates/breez-sdk/core/src/sdk.rs b/crates/breez-sdk/core/src/sdk.rs index 3ad4235a..537f19b3 100644 --- a/crates/breez-sdk/core/src/sdk.rs +++ b/crates/breez-sdk/core/src/sdk.rs @@ -20,7 +20,7 @@ use breez_sdk_common::{ rest::RestClient, }; use spark_wallet::{ - ExitSpeed, InvoiceDescription, Order, PagingFilter, SparkAddress, SparkWallet, WalletEvent, + ExitSpeed, InvoiceDescription, SparkAddress, SparkWallet, TransferTokenOutput, WalletEvent, WalletTransfer, }; use std::{str::FromStr, sync::Arc}; @@ -56,12 +56,14 @@ use crate::{ SendPaymentResponse, SyncWalletRequest, SyncWalletResponse, }, persist::{ - CachedAccountInfo, CachedSyncInfo, ObjectCacheRepository, PaymentMetadata, - StaticDepositAddress, Storage, UpdateDepositPayload, + CachedAccountInfo, ObjectCacheRepository, PaymentMetadata, StaticDepositAddress, Storage, + UpdateDepositPayload, }, + sync::SparkSyncService, utils::{ deposit_chain_syncer::DepositChainSyncer, run_with_shutdown, + token::token_transaction_to_payments, utxo_fetcher::{CachedUtxoFetcher, DetailedUtxo}, }, }; @@ -370,88 +372,28 @@ impl BreezSdk { let start_time = Instant::now(); if let SyncType::Full = sync_type { // Sync with the Spark network - info!("sync_wallet_internal: Syncing with Spark network"); - self.spark_wallet.sync().await?; - info!("sync_wallet_internal: Synced with Spark network completed"); + if let Err(e) = self.spark_wallet.sync().await { + error!("sync_wallet_internal: Failed to sync with Spark network: {e:?}"); + } + } + if let Err(e) = self.sync_wallet_state_to_storage().await { + error!("sync_wallet_internal: Failed to sync wallet state to storage: {e:?}"); + } + if let Err(e) = self.check_and_claim_static_deposits().await { + error!("sync_wallet_internal: Failed to check and claim static deposits: {e:?}"); } - self.sync_payments_to_storage().await?; - info!("sync_wallet_internal: Synced payments to storage completed"); - self.check_and_claim_static_deposits().await?; - info!("sync_wallet_internal: Checked and claimed static deposits completed"); let elapsed = start_time.elapsed(); info!("sync_wallet_internal: Wallet sync completed in {elapsed:?}"); self.event_emitter.emit(&SdkEvent::Synced {}).await; Ok(()) } - /// Synchronizes payments from transfers to persistent storage - async fn sync_payments_to_storage(&self) -> Result<(), SdkError> { - const BATCH_SIZE: u64 = 50; - - // Sync balance - update_balance(self.spark_wallet.clone(), self.storage.clone()).await?; - let object_repository = ObjectCacheRepository::new(self.storage.clone()); - - // Get the last offset we processed from storage - let cached_sync_info = object_repository - .fetch_sync_info() - .await? - .unwrap_or_default(); - let current_offset = cached_sync_info.offset; - - // We'll keep querying in batches until we have all transfers - let mut next_filter = Some(PagingFilter { - offset: current_offset, - limit: BATCH_SIZE, - order: Order::Ascending, - }); - info!("Syncing payments to storage, offset = {}", current_offset); - let mut pending_payments: u64 = 0; - while let Some(filter) = next_filter { - // Get batch of transfers starting from current offset - let transfers_response = self - .spark_wallet - .list_transfers(Some(filter.clone())) - .await?; + /// Synchronizes wallet state to persistent storage, making sure we have the latest balances and payments. + async fn sync_wallet_state_to_storage(&self) -> Result<(), SdkError> { + update_balances(self.spark_wallet.clone(), self.storage.clone()).await?; - info!( - "Syncing payments to storage, offset = {}, transfers = {}", - filter.offset, - transfers_response.len() - ); - // Process transfers in this batch - for transfer in &transfers_response.items { - // Create a payment record - let payment: Payment = transfer.clone().try_into()?; - // Insert payment into storage - if let Err(err) = self.storage.insert_payment(payment.clone()).await { - error!("Failed to insert payment: {err:?}"); - } - if payment.status == PaymentStatus::Pending { - pending_payments = pending_payments.saturating_add(1); - } - info!("Inserted payment: {payment:?}"); - } - - // Check if we have more transfers to fetch - let cache_offset = filter - .offset - .saturating_add(u64::try_from(transfers_response.len())?); - - // Update our last processed offset in the storage. We should remove pending payments - // from the offset as they might be removed from the list later. - let save_res = object_repository - .save_sync_info(&CachedSyncInfo { - offset: cache_offset.saturating_sub(pending_payments), - }) - .await; - - if let Err(err) = save_res { - error!("Failed to update last sync offset: {err:?}"); - } - - next_filter = transfers_response.next; - } + let sync_service = SparkSyncService::new(self.spark_wallet.clone(), self.storage.clone()); + sync_service.sync_payments().await?; Ok(()) } @@ -645,6 +587,7 @@ impl BreezSdk { .unwrap_or_default(); Ok(GetInfoResponse { balance_sats: account_info.balance_sats, + token_balances: account_info.token_balances, }) } @@ -655,7 +598,7 @@ impl BreezSdk { match &request.payment_method { ReceivePaymentMethod::SparkAddress => Ok(ReceivePaymentResponse { fee_sats: 0, - payment_request: self.spark_wallet.get_spark_address().await?.to_string(), + payment_request: self.spark_wallet.get_spark_address()?.to_string(), }), ReceivePaymentMethod::BitcoinAddress => { // TODO: allow passing amount @@ -741,7 +684,8 @@ impl BreezSdk { let prepare_response = self .prepare_send_payment(PrepareSendPaymentRequest { payment_request: success_data.pr, - amount_sats: Some(request.amount_sats), + amount: Some(request.amount_sats), + token_identifier: None, }) .await?; @@ -776,7 +720,8 @@ impl BreezSdk { spark_transfer_fee_sats: None, lightning_fee_sats: request.prepare_response.fee_sats, }, - amount_sats: request.prepare_response.amount_sats, + amount: request.prepare_response.amount_sats, + token_identifier: None, }, options: None, }, @@ -828,22 +773,73 @@ impl BreezSdk { }) } + #[allow(clippy::too_many_lines)] pub async fn prepare_send_payment( &self, request: PrepareSendPaymentRequest, ) -> Result { // First check for spark address if let Ok(spark_address) = request.payment_request.parse::() { + let payment_request_amount = if let Some(invoice_fields) = + &spark_address.spark_invoice_fields + && let Some(payment_type) = &invoice_fields.payment_type + { + match payment_type { + spark_wallet::SparkAddressPaymentType::SatsPayment(sats_payment_details) => { + if request.token_identifier.is_some() { + return Err(SdkError::InvalidInput( + "Token identifier can't be provided for this payment request: spark sats payment".to_string(), + )); + } + if sats_payment_details.amount.is_some() && request.amount.is_some() { + return Err(SdkError::InvalidInput( + "Amount can't be provided for this payment request: spark invoice defines amount".to_string(), + )); + } + sats_payment_details.amount + } + spark_wallet::SparkAddressPaymentType::TokensPayment( + tokens_payment_details, + ) => { + if request.token_identifier.is_none() { + return Err(SdkError::InvalidInput( + "Token identifier is required for this payment request: spark tokens payment".to_string(), + )); + } + if tokens_payment_details.amount.is_some() && request.amount.is_some() { + return Err(SdkError::InvalidInput( + "Amount can't be provided for this payment request: spark invoice defines amount".to_string(), + )); + } + tokens_payment_details.amount + } + } + } else { + None + }; + return Ok(PrepareSendPaymentResponse { payment_method: SendPaymentMethod::SparkAddress { address: spark_address.to_string(), - fee_sats: 0, + fee: 0, + token_identifier: request.token_identifier.clone(), }, - amount_sats: request - .amount_sats + amount: payment_request_amount + .or(request.amount) .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?, + token_identifier: request.token_identifier, }); } + + if request.token_identifier.is_some() { + return Err(SdkError::InvalidInput( + "Token identifier can't be provided for this payment request: non-spark address" + .to_string(), + )); + } + + let amount_sats = request.amount; + // Then check for other types of inputs let parsed_input = parse(&request.payment_request).await?; match &parsed_input { @@ -860,10 +856,7 @@ impl BreezSdk { let lightning_fee_sats = self .spark_wallet - .fetch_lightning_send_fee_estimate( - &request.payment_request, - request.amount_sats, - ) + .fetch_lightning_send_fee_estimate(&request.payment_request, amount_sats) .await?; Ok(PrepareSendPaymentResponse { @@ -872,10 +865,10 @@ impl BreezSdk { spark_transfer_fee_sats, lightning_fee_sats, }, - amount_sats: request - .amount_sats + amount: amount_sats .or(detailed_bolt11_invoice.amount_msat.map(|msat| msat / 1000)) .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?, + token_identifier: None, }) } InputType::BitcoinAddress(withdrawal_address) => { @@ -884,8 +877,7 @@ impl BreezSdk { .fetch_coop_exit_fee_quote( &withdrawal_address.address, Some( - request - .amount_sats + amount_sats .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?, ), ) @@ -895,9 +887,9 @@ impl BreezSdk { address: withdrawal_address.clone(), fee_quote: fee_quote.into(), }, - amount_sats: request - .amount_sats + amount: amount_sats .ok_or(SdkError::InvalidInput("Amount is required".to_string()))?, + token_identifier: None, }) } _ => Err(SdkError::InvalidInput( @@ -919,8 +911,13 @@ impl BreezSdk { suppress_payment_event: bool, ) -> Result { let res = match &request.prepare_response.payment_method { - SendPaymentMethod::SparkAddress { address, .. } => { - self.send_spark_address(address, &request).await + SendPaymentMethod::SparkAddress { + address, + token_identifier, + .. + } => { + self.send_spark_address(address, token_identifier.clone(), &request) + .await } SendPaymentMethod::Bolt11Invoice { invoice_details, @@ -957,18 +954,29 @@ impl BreezSdk { async fn send_spark_address( &self, address: &str, + token_identifier: Option, request: &SendPaymentRequest, ) -> Result { let spark_address = address .parse::() .map_err(|_| SdkError::InvalidInput("Invalid spark address".to_string()))?; - let transfer = self - .spark_wallet - .transfer(request.prepare_response.amount_sats, &spark_address) - .await?; - Ok(SendPaymentResponse { - payment: transfer.try_into()?, - }) + + let payment = if let Some(identifier) = token_identifier { + self.send_spark_token_payment( + identifier, + request.prepare_response.amount.into(), + spark_address, + ) + .await? + } else { + let transfer = self + .spark_wallet + .transfer(request.prepare_response.amount, &spark_address) + .await?; + transfer.try_into()? + }; + + Ok(SendPaymentResponse { payment }) } async fn send_bolt11_invoice( @@ -982,7 +990,7 @@ impl BreezSdk { // We are not sending amount in case the invoice contains it. Some(_) => None, // We are sending amount for zero amount invoice - None => Some(request.prepare_response.amount_sats), + None => Some(request.prepare_response.amount), }; let (prefer_spark, completion_timeout_secs) = match request.options { Some(SendPaymentOptions::Bolt11Invoice { @@ -1010,7 +1018,7 @@ impl BreezSdk { let ssp_id = lightning_payment.id.clone(); let payment = Payment::from_lightning( lightning_payment, - request.prepare_response.amount_sats, + request.prepare_response.amount, payment_response.transfer.id.to_string(), )?; self.poll_lightning_send_payment(&payment, ssp_id); @@ -1059,7 +1067,7 @@ impl BreezSdk { .spark_wallet .withdraw( &address.address, - Some(request.prepare_response.amount_sats), + Some(request.prepare_response.amount), exit_speed, fee_quote.clone().into(), ) @@ -1406,6 +1414,37 @@ impl BreezSdk { } } +impl BreezSdk { + async fn send_spark_token_payment( + &self, + token_identifier: String, + amount: u128, + receiver_address: SparkAddress, + ) -> Result { + let token_transaction = self + .spark_wallet + .transfer_tokens(vec![TransferTokenOutput { + token_id: token_identifier, + amount, + receiver_address: receiver_address.clone(), + }]) + .await?; + + let payments = + token_transaction_to_payments(&self.spark_wallet, &token_transaction, true).await?; + for payment in &payments { + self.storage.insert_payment(payment.clone()).await?; + } + + payments + .first() + .ok_or(SdkError::Generic( + "No payment created from token transfer".to_string(), + )) + .cloned() + } +} + fn is_payment_match(payment: &Payment, request: &WaitForPaymentRequest) -> bool { match &request.identifier { WaitForPaymentIdentifier::PaymentId(payment_id) => payment.id == *payment_id, @@ -1416,6 +1455,7 @@ fn is_payment_match(payment: &Payment, request: &WaitForPaymentRequest) -> bool invoice.to_lowercase() == payment_request.to_lowercase() } PaymentDetails::Spark + | PaymentDetails::Token { .. } | PaymentDetails::Withdraw { tx_id: _ } | PaymentDetails::Deposit { tx_id: _ } => false, } @@ -1445,7 +1485,7 @@ impl EventListener for BalanceWatcher { async fn on_event(&self, event: SdkEvent) { match event { SdkEvent::PaymentSucceeded { .. } | SdkEvent::ClaimDepositsSucceeded { .. } => { - match update_balance(self.spark_wallet.clone(), self.storage.clone()).await { + match update_balances(self.spark_wallet.clone(), self.storage.clone()).await { Ok(()) => info!("Balance updated successfully"), Err(e) => error!("Failed to update balance: {e:?}"), } @@ -1455,15 +1495,23 @@ impl EventListener for BalanceWatcher { } } -async fn update_balance( +async fn update_balances( spark_wallet: Arc, storage: Arc, ) -> Result<(), SdkError> { - let balance = spark_wallet.get_balance().await?; + let balance_sats = spark_wallet.get_balance().await?; + let token_balances = spark_wallet + .get_token_balances() + .await? + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(); let object_repository = ObjectCacheRepository::new(storage.clone()); + object_repository .save_account_info(&CachedAccountInfo { - balance_sats: balance, + balance_sats, + token_balances, }) .await?; Ok(()) diff --git a/crates/breez-sdk/core/src/sync.rs b/crates/breez-sdk/core/src/sync.rs new file mode 100644 index 00000000..2532c522 --- /dev/null +++ b/crates/breez-sdk/core/src/sync.rs @@ -0,0 +1,275 @@ +use std::sync::Arc; + +use spark_wallet::{ListTokenTransactionsRequest, Order, PagingFilter, SparkWallet}; +use tracing::{error, info}; + +use crate::{ + Payment, PaymentStatus, SdkError, Storage, + persist::{CachedSyncInfo, ObjectCacheRepository}, + utils::token::token_transaction_to_payments, +}; + +const PAYMENT_SYNC_BATCH_SIZE: u64 = 50; + +pub(crate) struct SparkSyncService { + spark_wallet: Arc, + storage: Arc, +} + +impl SparkSyncService { + pub fn new(spark_wallet: Arc, storage: Arc) -> Self { + Self { + spark_wallet, + storage, + } + } + + pub async fn sync_payments(&self) -> Result<(), SdkError> { + let object_repository = ObjectCacheRepository::new(self.storage.clone()); + self.sync_bitcoin_payments_to_storage(&object_repository) + .await?; + self.sync_token_payments_to_storage(&object_repository) + .await + } + + async fn sync_bitcoin_payments_to_storage( + &self, + object_repository: &ObjectCacheRepository, + ) -> Result<(), SdkError> { + // Get the last offset we processed from storage + let cached_sync_info = object_repository + .fetch_sync_info() + .await? + .unwrap_or_default(); + let current_offset = cached_sync_info.offset; + let last_synced_token_payment_id = cached_sync_info.last_synced_token_payment_id; + + // We'll keep querying in batches until we have all transfers + let mut next_filter = Some(PagingFilter { + offset: current_offset, + limit: PAYMENT_SYNC_BATCH_SIZE, + order: Order::Ascending, + }); + info!("Syncing payments to storage, offset = {}", current_offset); + let mut pending_payments: u64 = 0; + while let Some(filter) = next_filter { + // Get batch of transfers starting from current offset + let transfers_response = self + .spark_wallet + .list_transfers(Some(filter.clone())) + .await?; + + info!( + "Syncing payments to storage, offset = {}, transfers = {}", + filter.offset, + transfers_response.len() + ); + // Process transfers in this batch + for transfer in &transfers_response.items { + // Create a payment record + let payment: Payment = transfer.clone().try_into()?; + // Insert payment into storage + if let Err(err) = self.storage.insert_payment(payment.clone()).await { + error!("Failed to insert payment: {err:?}"); + } + if payment.status == PaymentStatus::Pending { + pending_payments = pending_payments.saturating_add(1); + } + info!("Inserted payment: {payment:?}"); + } + + // Check if we have more transfers to fetch + let cache_offset = filter + .offset + .saturating_add(u64::try_from(transfers_response.len())?); + + // Update our last processed offset in the storage. We should remove pending payments + // from the offset as they might be removed from the list later. + let save_res = object_repository + .save_sync_info(&CachedSyncInfo { + offset: cache_offset.saturating_sub(pending_payments), + last_synced_token_payment_id: last_synced_token_payment_id.clone(), + }) + .await; + + if let Err(err) = save_res { + error!("Failed to update last sync offset: {err:?}"); + } + + next_filter = transfers_response.next; + } + + Ok(()) + } + + #[allow(clippy::too_many_lines)] + async fn sync_token_payments_to_storage( + &self, + object_repository: &ObjectCacheRepository, + ) -> Result<(), SdkError> { + info!("Syncing token payments to storage"); + // Get the last synced token payment id we processed from storage + let cached_sync_info = object_repository + .fetch_sync_info() + .await? + .unwrap_or_default(); + let last_synced_token_payment_id = cached_sync_info.last_synced_token_payment_id; + let our_public_key = self.spark_wallet.get_identity_public_key(); + + // We'll keep querying in batches until we have all token tranactions + let mut payments_to_sync = Vec::new(); + let mut next_offset = 0; + let mut has_more = true; + // We'll keep querying in pages until we already have a completed or failed payment stored + // or we have fetched all transfers + 'page_loop: while has_more { + info!("Fetching token transactions, offset = {next_offset}"); + // Get batch of token transactions starting from current offset + let Ok(token_transactions) = self + .spark_wallet + .list_token_transactions(ListTokenTransactionsRequest { + paging: Some(PagingFilter::new( + Some(next_offset), + Some(PAYMENT_SYNC_BATCH_SIZE), + None, + )), + ..Default::default() + }) + .await + else { + error!( + "Failed to fetch address transactions, stopping sync and processing {} payments", + payments_to_sync.len() + ); + break 'page_loop; + }; + // If no token transactions to sync + if token_transactions.is_empty() { + break 'page_loop; + } + + // Get prev out hashes of first input of each token transaction + // Assumes all inputs of a tx share the same owner public key + let token_transactions_prevout_hashes = token_transactions + .items + .iter() + .filter_map(|tx| match &tx.inputs { + spark_wallet::TokenInputs::Transfer(token_transfer_input) => { + token_transfer_input.outputs_to_spend.first().cloned() + } + spark_wallet::TokenInputs::Mint(..) | spark_wallet::TokenInputs::Create(..) => { + None + } + }) + .map(|output| output.prev_token_tx_hash) + .collect::>(); + + // Since we are trying to fetch at most 1 parent transaction per token transaction, + // we can fetch all in one go using same batch size + let Ok(parent_transactions) = self + .spark_wallet + .list_token_transactions(ListTokenTransactionsRequest { + paging: Some(PagingFilter::new(None, Some(PAYMENT_SYNC_BATCH_SIZE), None)), + owner_public_keys: Some(Vec::new()), + token_transaction_hashes: token_transactions_prevout_hashes, + ..Default::default() + }) + .await + else { + error!( + "Failed to fetch parent transactions, stopping sync and processing {} payments", + payments_to_sync.len() + ); + break 'page_loop; + }; + + info!( + "Syncing token payments to storage, offset = {next_offset}, transactions = {}", + token_transactions.len() + ); + // Process transfers in this page + for transaction in &token_transactions.items { + let tx_inputs_are_ours = match &transaction.inputs { + spark_wallet::TokenInputs::Transfer(token_transfer_input) => { + let first_input = token_transfer_input.outputs_to_spend.first().ok_or( + SdkError::Generic("No input in token transfer input".to_string()), + )?; + let parent_transaction = parent_transactions + .items + .iter() + .find(|tx| tx.hash == first_input.prev_token_tx_hash) + .ok_or(SdkError::Generic( + "Parent transaction not found".to_string(), + ))?; + let output = parent_transaction + .outputs + .get(first_input.prev_token_tx_vout as usize) + .ok_or(SdkError::Generic("Output not found".to_string()))?; + output.owner_public_key == our_public_key + } + spark_wallet::TokenInputs::Mint(_) | spark_wallet::TokenInputs::Create(_) => { + false + } + }; + + // Create payment records + let payments = token_transaction_to_payments( + &self.spark_wallet, + transaction, + tx_inputs_are_ours, + ) + .await?; + + for payment in payments { + if last_synced_token_payment_id + .as_ref() + .is_some_and(|id| payment.id == *id) + { + info!( + "Last synced token payment id found ({last_synced_token_payment_id:?}), stopping sync and processing {} payments", + payments_to_sync.len() + ); + has_more = false; + break 'page_loop; + } + payments_to_sync.push(payment); + } + } + + // Check if we have more transfers to fetch + next_offset = next_offset.saturating_add(u64::try_from(token_transactions.len())?); + has_more = token_transactions.len() as u64 == PAYMENT_SYNC_BATCH_SIZE; + } + + // Insert what synced payments we have into storage, oldest to newest + payments_to_sync.sort_by_key(|p| p.timestamp); + for payment in &payments_to_sync { + info!("Inserting token payment: {payment:?}"); + if let Err(e) = self.storage.insert_payment(payment.clone()).await { + error!("Failed to insert token payment: {e:?}"); + } + } + + // We have synced all token transactions or found the last synced payment id. + // If there was a failure to fetch transactions or no transactions exist, + // we won't update the last synced token payment id + if !has_more + && let Some(last_synced_token_payment_id) = payments_to_sync + .into_iter() + .filter(|p| p.status.is_final()) + .next_back() + .map(|p| p.id) + { + // Update last synced token payment id to the newest final payment we have processed + info!("Updating last synced token payment id to {last_synced_token_payment_id}"); + object_repository + .save_sync_info(&CachedSyncInfo { + offset: cached_sync_info.offset, + last_synced_token_payment_id: Some(last_synced_token_payment_id), + }) + .await?; + } + + Ok(()) + } +} diff --git a/crates/breez-sdk/core/src/utils/mod.rs b/crates/breez-sdk/core/src/utils/mod.rs index 51ba9820..b3309886 100644 --- a/crates/breez-sdk/core/src/utils/mod.rs +++ b/crates/breez-sdk/core/src/utils/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod deposit_chain_syncer; +pub(crate) mod token; pub(crate) mod utxo_fetcher; /// Runs a future until completion or until a shutdown signal is received. diff --git a/crates/breez-sdk/core/src/utils/token.rs b/crates/breez-sdk/core/src/utils/token.rs new file mode 100644 index 00000000..a0c40dd0 --- /dev/null +++ b/crates/breez-sdk/core/src/utils/token.rs @@ -0,0 +1,87 @@ +use std::time::UNIX_EPOCH; + +use spark_wallet::{SparkWallet, TokenMetadata}; + +use crate::{Payment, PaymentDetails, PaymentMethod, PaymentStatus, PaymentType, SdkError}; + +/// Converts a token transaction to payments +/// +/// Each resulting payment corresponds to a potential group of outputs that share the same owner public key. +/// The id of the payment is the id of the first output in the group. +/// +/// Assumptions: +/// - All outputs of a token transaction share the same token identifier +/// - All inputs of a token transaction share the same owner public key +#[allow(clippy::too_many_lines)] +pub async fn token_transaction_to_payments( + spark_wallet: &SparkWallet, + transaction: &spark_wallet::TokenTransaction, + tx_inputs_are_ours: bool, +) -> Result, SdkError> { + // Get token metadata for the first output (assuming all outputs have the same token) + let token_identifier = transaction + .outputs + .first() + .ok_or(SdkError::Generic( + "No outputs in token transaction".to_string(), + ))? + .token_identifier + .as_ref(); + let metadata: TokenMetadata = spark_wallet + .get_tokens_metadata(&[token_identifier]) + .await? + .first() + .ok_or(SdkError::Generic("Token metadata not found".to_string()))? + .clone(); + + let is_transfer_transaction = + matches!(&transaction.inputs, spark_wallet::TokenInputs::Transfer(..)); + + let timestamp = transaction + .created_timestamp + .duration_since(UNIX_EPOCH) + .map_err(|_| { + SdkError::Generic( + "Token transaction created timestamp is before UNIX_EPOCH".to_string(), + ) + })? + .as_secs(); + + let identity_public_key = spark_wallet.get_identity_public_key(); + + let mut payments = Vec::new(); + + for (vout, output) in transaction.outputs.iter().enumerate() { + let payment_type = if tx_inputs_are_ours && output.owner_public_key != identity_public_key { + // If inputs are ours and outputs are not ours, add an outgoing payment + PaymentType::Send + } else if !tx_inputs_are_ours && output.owner_public_key == identity_public_key { + // If inputs are not ours and outputs are ours, add an incoming payment + PaymentType::Receive + } else { + continue; + }; + + let id = format!("{}:{}", transaction.hash, vout); + + let payment = Payment { + id, + payment_type, + status: PaymentStatus::from_token_transaction_status( + transaction.status, + is_transfer_transaction, + ), + amount: output.token_amount.try_into().unwrap_or_default(), + fees: 0, // TODO: calculate actual fees when they start being charged + timestamp, + method: PaymentMethod::Token, + details: Some(PaymentDetails::Token { + metadata: metadata.clone().into(), + tx_hash: transaction.hash.clone(), + }), + }; + payments.push(payment); + } + + Ok(payments) +} diff --git a/crates/breez-sdk/wasm/js/node-storage/index.cjs b/crates/breez-sdk/wasm/js/node-storage/index.cjs index d3890379..377511d9 100644 --- a/crates/breez-sdk/wasm/js/node-storage/index.cjs +++ b/crates/breez-sdk/wasm/js/node-storage/index.cjs @@ -142,12 +142,15 @@ class SqliteStorage { , COALESCE(l.description, pm.lnurl_description) AS lightning_description , l.preimage AS lightning_preimage , pm.lnurl_pay_info + , t.metadata AS token_metadata + , t.tx_hash AS token_tx_hash FROM payments p LEFT JOIN payment_details_lightning l ON p.id = l.payment_id + LEFT JOIN payment_details_token t ON p.id = t.payment_id LEFT JOIN payment_metadata pm ON p.id = pm.payment_id - ORDER BY p.timestamp DESC + ORDER BY p.timestamp DESC LIMIT ? OFFSET ? - `); + `); const rows = stmt.all(actualLimit, actualOffset); return Promise.resolve(rows.map(this._rowToPayment.bind(this))); @@ -178,6 +181,11 @@ class SqliteStorage { (payment_id, invoice, payment_hash, destination_pubkey, description, preimage) VALUES (@id, @invoice, @paymentHash, @destinationPubkey, @description, @preimage)` ); + const tokenInsert = this.db.prepare( + `INSERT OR REPLACE INTO payment_details_token + (payment_id, metadata, tx_hash) + VALUES (@id, @metadata, @txHash)` + ); const transaction = this.db.transaction(() => { paymentInsert.run({ id: payment.id, @@ -187,12 +195,14 @@ class SqliteStorage { fees: payment.fees, timestamp: payment.timestamp, method: payment.method ? JSON.stringify(payment.method) : null, - withdrawTxId: payment.details?.type === 'withdraw' ? payment.details.txId : null, - depositTxId: payment.details?.type === 'deposit' ? payment.details.txId : null, - spark: payment.details?.type === 'spark' ? 1 : null, + withdrawTxId: + payment.details?.type === "withdraw" ? payment.details.txId : null, + depositTxId: + payment.details?.type === "deposit" ? payment.details.txId : null, + spark: payment.details?.type === "spark" ? 1 : null, }); - if (payment.details?.type === 'lightning') { + if (payment.details?.type === "lightning") { lightningInsert.run({ id: payment.id, invoice: payment.details.invoice, @@ -202,8 +212,16 @@ class SqliteStorage { preimage: payment.details.preimage, }); } + + if (payment.details?.type === "token") { + tokenInsert.run({ + id: payment.id, + metadata: JSON.stringify(payment.details.metadata), + txHash: payment.details.txHash, + }); + } }); - + transaction(); return Promise.resolve(); } catch (error) { @@ -241,8 +259,11 @@ class SqliteStorage { , COALESCE(l.description, pm.lnurl_description) AS lightning_description , l.preimage AS lightning_preimage , pm.lnurl_pay_info + , t.metadata AS token_metadata + , t.tx_hash AS token_tx_hash FROM payments p LEFT JOIN payment_details_lightning l ON p.id = l.payment_id + LEFT JOIN payment_details_token t ON p.id = t.payment_id LEFT JOIN payment_metadata pm ON p.id = pm.payment_id WHERE p.id = ? `); @@ -292,8 +313,11 @@ class SqliteStorage { , COALESCE(l.description, pm.lnurl_description) AS lightning_description , l.preimage AS lightning_preimage , pm.lnurl_pay_info + , t.metadata AS token_metadata + , t.tx_hash AS token_tx_hash FROM payments p LEFT JOIN payment_details_lightning l ON p.id = l.payment_id + LEFT JOIN payment_details_token t ON p.id = t.payment_id LEFT JOIN payment_metadata pm ON p.id = pm.payment_id WHERE l.invoice = ? `); @@ -444,7 +468,7 @@ class SqliteStorage { let details = null; if (row.lightning_invoice) { details = { - type: 'lightning', + type: "lightning", invoice: row.lightning_invoice, paymentHash: row.lightning_payment_hash, destinationPubkey: row.lightning_destination_pubkey, @@ -464,19 +488,25 @@ class SqliteStorage { } } else if (row.withdraw_tx_id) { details = { - type: 'withdraw', + type: "withdraw", txId: row.withdraw_tx_id, }; } else if (row.deposit_tx_id) { details = { - type: 'deposit', + type: "deposit", txId: row.deposit_tx_id, }; } else if (row.spark) { details = { - type: 'spark', + type: "spark", amount: row.spark, }; + } else if (row.token_metadata) { + details = { + type: "token", + metadata: JSON.parse(row.token_metadata), + txHash: row.token_tx_hash, + }; } let method = null; diff --git a/crates/breez-sdk/wasm/js/node-storage/migrations.cjs b/crates/breez-sdk/wasm/js/node-storage/migrations.cjs index 0b4fa70b..f05463d5 100644 --- a/crates/breez-sdk/wasm/js/node-storage/migrations.cjs +++ b/crates/breez-sdk/wasm/js/node-storage/migrations.cjs @@ -169,14 +169,25 @@ class MigrationManager { json_extract(details, '$.Lightning.destination_pubkey'), json_extract(details, '$.Lightning.description'), json_extract(details, '$.Lightning.preimage') FROM payments WHERE json_extract(details, '$.Lightning.invoice') IS NOT NULL`, - `UPDATE payments SET withdraw_tx_id = json_extract(details, '$.Withdraw.tx_id') + `UPDATE payments SET withdraw_tx_id = json_extract(details, '$.Withdraw.tx_id') WHERE json_extract(details, '$.Withdraw.tx_id') IS NOT NULL`, - `UPDATE payments SET deposit_tx_id = json_extract(details, '$.Deposit.tx_id') + `UPDATE payments SET deposit_tx_id = json_extract(details, '$.Deposit.tx_id') WHERE json_extract(details, '$.Deposit.tx_id') IS NOT NULL`, - `ALTER TABLE payments DROP COLUMN details`, - `CREATE INDEX idx_payment_details_lightning_invoice ON payment_details_lightning(invoice)`, - ] - } + `ALTER TABLE payments DROP COLUMN details`, + `CREATE INDEX idx_payment_details_lightning_invoice ON payment_details_lightning(invoice)`, + ], + }, + { + name: "Create payment_details_token table", + sql: [ + `CREATE TABLE IF NOT EXISTS payment_details_token ( + payment_id TEXT PRIMARY KEY, + metadata TEXT, + tx_hash TEXT, + FOREIGN KEY (payment_id) REFERENCES payments(id) ON DELETE CASCADE + )`, + ], + }, ]; } } diff --git a/crates/breez-sdk/wasm/js/web-storage/index.js b/crates/breez-sdk/wasm/js/web-storage/index.js index 49a0f24a..61987188 100644 --- a/crates/breez-sdk/wasm/js/web-storage/index.js +++ b/crates/breez-sdk/wasm/js/web-storage/index.js @@ -109,8 +109,8 @@ class MigrationManager { unique: false, }); } - } - } + }, + }, ]; } } @@ -355,7 +355,7 @@ class IndexedDBStorage { request.onerror = () => { reject( new StorageError( - `Failed to list payments: ${ + `Failed to list payments (offset: ${offset}, limit: ${limit}): ${ request.error?.message || "Unknown error" }`, request.error @@ -724,7 +724,12 @@ class IndexedDBStorage { } // If this is a Lightning payment and we have lnurl_pay_info, add it to details - if (metadata && metadata.lnurlPayInfo && details && details.type == 'lightning') { + if ( + metadata && + metadata.lnurlPayInfo && + details && + details.type == "lightning" + ) { try { details.lnurlPayInfo = JSON.parse(metadata.lnurlPayInfo); if (metadata.lnurlDescription && !details.description) { @@ -751,7 +756,10 @@ class IndexedDBStorage { } } -export async function createDefaultStorage(dbName = "BreezSdkSpark", logger = null) { +export async function createDefaultStorage( + dbName = "BreezSdkSpark", + logger = null +) { const storage = new IndexedDBStorage(dbName, logger); await storage.initialize(); return storage; diff --git a/crates/breez-sdk/wasm/src/models.rs b/crates/breez-sdk/wasm/src/models.rs index 343245f1..697e355d 100644 --- a/crates/breez-sdk/wasm/src/models.rs +++ b/crates/breez-sdk/wasm/src/models.rs @@ -1,3 +1,7 @@ +use std::collections::HashMap; + +use wasm_bindgen::prelude::wasm_bindgen; + #[allow(clippy::large_enum_variant)] #[macros::extern_wasm_bindgen(breez_sdk_spark::SdkEvent)] pub enum SdkEvent { @@ -367,6 +371,10 @@ pub struct Payment { #[macros::extern_wasm_bindgen(breez_sdk_spark::PaymentDetails)] pub enum PaymentDetails { Spark, + Token { + metadata: TokenMetadata, + tx_hash: String, + }, Lightning { description: Option, preimage: Option, @@ -387,6 +395,7 @@ pub enum PaymentDetails { pub enum PaymentMethod { Lightning, Spark, + Token, Deposit, Withdraw, Unknown, @@ -492,6 +501,27 @@ pub struct GetInfoRequest { #[macros::extern_wasm_bindgen(breez_sdk_spark::GetInfoResponse)] pub struct GetInfoResponse { pub balance_sats: u64, + pub token_balances: HashMap, +} + +#[macros::extern_wasm_bindgen(breez_sdk_spark::TokenBalance)] +pub struct TokenBalance { + pub balance: u64, + pub token_metadata: TokenMetadata, +} + +#[macros::extern_wasm_bindgen(breez_sdk_spark::TokenMetadata)] +pub struct TokenMetadata { + pub identifier: String, + /// Hex representation of the issuer public key + pub issuer_public_key: String, + pub name: String, + pub ticker: String, + /// Number of decimals the token uses + pub decimals: u32, + /// Decimal representation of the token max supply (unsigned 128-bit integer) + pub max_supply: u64, + pub is_freezable: bool, } #[macros::extern_wasm_bindgen(breez_sdk_spark::SyncWalletRequest)] @@ -538,7 +568,8 @@ pub enum SendPaymentMethod { }, // should be replaced with the parsed invoice SparkAddress { address: String, - fee_sats: u64, + fee: u64, + token_identifier: Option, }, } @@ -585,13 +616,15 @@ pub struct LnurlPayResponse { #[macros::extern_wasm_bindgen(breez_sdk_spark::PrepareSendPaymentRequest)] pub struct PrepareSendPaymentRequest { pub payment_request: String, - pub amount_sats: Option, + pub amount: Option, + pub token_identifier: Option, } #[macros::extern_wasm_bindgen(breez_sdk_spark::PrepareSendPaymentResponse)] pub struct PrepareSendPaymentResponse { pub payment_method: SendPaymentMethod, - pub amount_sats: u64, + pub amount: u64, + pub token_identifier: Option, } #[macros::extern_wasm_bindgen(breez_sdk_spark::OnchainConfirmationSpeed)] diff --git a/crates/internal/src/command/mod.rs b/crates/internal/src/command/mod.rs index 4d54f722..017e7f5f 100644 --- a/crates/internal/src/command/mod.rs +++ b/crates/internal/src/command/mod.rs @@ -88,7 +88,7 @@ pub(crate) async fn handle_command( println!("Signature: {signature}"); } Command::SparkAddress => { - let spark_address = wallet.get_spark_address().await?; + let spark_address = wallet.get_spark_address()?; println!("{spark_address}") } Command::Sync => { diff --git a/crates/internal/src/command/tokens.rs b/crates/internal/src/command/tokens.rs index 0abba887..e95de7d2 100644 --- a/crates/internal/src/command/tokens.rs +++ b/crates/internal/src/command/tokens.rs @@ -110,7 +110,7 @@ pub async fn handle_command( .map(|o| o.try_into()) .collect::, _>>()?; let transfer_id = wallet.transfer_tokens(outputs).await?; - println!("Transaction ID: {transfer_id}"); + println!("Transaction ID: {transfer_id:?}"); Ok(()) } TokensCommand::ListTransactions { limit, offset } => { diff --git a/crates/macros/src/wasm_bindgen.rs b/crates/macros/src/wasm_bindgen.rs index 90c121e1..e2f66067 100644 --- a/crates/macros/src/wasm_bindgen.rs +++ b/crates/macros/src/wasm_bindgen.rs @@ -251,9 +251,13 @@ fn get_struct_fields(fields: &Fields) -> Vec { let segment = &type_path.path.segments[0]; if segment.ident == "Vec" { quote! { #ident: val.#ident.into_iter().map(|i| i.into()).collect() } + } else if segment.ident == "HashMap" { + quote! { #ident: val.#ident.into_iter().map(|(k, v)| (k, v.into())).collect() } } else if segment.ident == "Option" { if get_path(&segment.arguments).is_some_and(|tp| tp.path.segments[0].ident == "Vec") { quote! { #ident: val.#ident.map(|i| i.into_iter().map(|a| a.into()).collect()) } + } else if get_path(&segment.arguments).is_some_and(|tp| tp.path.segments[0].ident == "HashMap") { + quote! { #ident: val.#ident.map(|i| i.into_iter().map(|(k, v)| (k, v.into())).collect()) } } else { quote! { #ident: val.#ident.map(|i| i.into()) } } @@ -280,9 +284,13 @@ fn get_enum_fields(fields: &Fields) -> Vec { let segment = &type_path.path.segments[0]; if segment.ident == "Vec" { quote! { #ident: #ident.into_iter().map(|i| i.into()).collect() } + } else if segment.ident == "HashMap" { + quote! { #ident: #ident.into_iter().map(|(k, v)| (k, v.into())).collect() } } else if segment.ident == "Option" { if get_path(&segment.arguments).is_some_and(|tp| tp.path.segments[0].ident == "Vec") { quote! { #ident: #ident.map(|i| i.into_iter().map(|a| a.into()).collect()) } + } else if get_path(&segment.arguments).is_some_and(|tp| tp.path.segments[0].ident == "HashMap") { + quote! { #ident: #ident.map(|i| i.into_iter().map(|(k, v)| (k, v.into())).collect()) } } else { quote! { #ident: #ident.map(|i| i.into()) } } @@ -308,9 +316,13 @@ fn get_enum_fields(fields: &Fields) -> Vec { let segment = &type_path.path.segments[0]; if segment.ident == "Vec" { quote! { #ident.into_iter().map(|i| i.into()).collect() } + } else if segment.ident == "HashMap" { + quote! { #ident.into_iter().map(|(k, v)| (k, v.into())).collect() } } else if segment.ident == "Option" { if get_path(&segment.arguments).is_some_and(|tp| tp.path.segments[0].ident == "Vec") { quote! { #ident.map(|i| i.into_iter().map(|a| a.into()).collect()) } + } else if get_path(&segment.arguments).is_some_and(|tp| tp.path.segments[0].ident == "HashMap") { + quote! { #ident.map(|i| i.into_iter().map(|(k, v)| (k, v.into())).collect()) } } else { quote! { #ident.map(|i| i.into()) } } diff --git a/crates/spark-wallet/src/lib.rs b/crates/spark-wallet/src/lib.rs index 5224b2cd..874b9a77 100644 --- a/crates/spark-wallet/src/lib.rs +++ b/crates/spark-wallet/src/lib.rs @@ -12,13 +12,13 @@ pub use model::*; pub use spark::operator::{OperatorConfig, OperatorError, OperatorPoolConfig}; pub use spark::{ Identifier, Network, - address::SparkAddress, + address::{SparkAddress, SparkAddressPaymentType}, operator::rpc::{ConnectionManager, DefaultConnectionManager}, services::TokensConfig, services::{ CoopExitFeeQuote, CoopExitSpeedFeeQuote, CpfpUtxo, ExitSpeed, Fee, InvoiceDescription, - LightningSendPayment, LightningSendStatus, TransferStatus, TransferTokenOutput, - TransferType, Utxo, + LightningSendPayment, LightningSendStatus, TokenInputs, TokenMetadata, TokenTransaction, + TokenTransactionStatus, TransferStatus, TransferTokenOutput, TransferType, Utxo, }, session_manager::*, signer::{DefaultSigner, DefaultSignerError, KeySet, KeySetType, Signer}, diff --git a/crates/spark-wallet/src/model.rs b/crates/spark-wallet/src/model.rs index fe020751..391c07bd 100644 --- a/crates/spark-wallet/src/model.rs +++ b/crates/spark-wallet/src/model.rs @@ -212,7 +212,8 @@ pub struct TokenBalance { #[derive(Default)] pub struct ListTokenTransactionsRequest { pub paging: Option, - pub owner_public_keys: Vec, + /// If not provided, will use our own public key + pub owner_public_keys: Option>, pub issuer_public_keys: Vec, pub token_transaction_hashes: Vec, pub token_ids: Vec, diff --git a/crates/spark-wallet/src/wallet.rs b/crates/spark-wallet/src/wallet.rs index 538971e0..ace667ef 100644 --- a/crates/spark-wallet/src/wallet.rs +++ b/crates/spark-wallet/src/wallet.rs @@ -19,12 +19,12 @@ use spark::{ CoopExitFeeQuote, CoopExitService, CpfpUtxo, DepositService, ExitSpeed, Fee, InvoiceDescription, LeafTxCpfpPsbts, LightningReceivePayment, LightningSendPayment, LightningService, QueryTokenTransactionsFilter, StaticDepositQuote, Swap, TimelockManager, - TokenService, TokenTransaction, Transfer, TransferService, TransferTokenOutput, - UnilateralExitService, Utxo, + TokenMetadata, TokenService, TokenTransaction, Transfer, TransferService, + TransferTokenOutput, UnilateralExitService, Utxo, }, session_manager::{InMemorySessionManager, SessionManager}, signer::Signer, - ssp::{ServiceProvider, SspTransfer}, + ssp::{ServiceProvider, SspTransfer, SspUserRequest}, tree::{ InMemoryTreeStore, LeavesReservation, SynchronousTreeService, TargetAmounts, TreeNode, TreeNodeId, TreeService, TreeStore, select_leaves_by_amounts, with_reserved_leaves, @@ -214,6 +214,10 @@ impl SparkWallet { } impl SparkWallet { + pub fn get_identity_public_key(&self) -> PublicKey { + self.identity_public_key + } + pub async fn list_leaves(&self) -> Result { let leaves = self.tree_service.list_leaves().await?; Ok(leaves.into()) @@ -535,7 +539,7 @@ impl SparkWallet { } } - pub async fn get_spark_address(&self) -> Result { + pub fn get_spark_address(&self) -> Result { Ok(SparkAddress::new( self.identity_public_key, self.config.network, @@ -569,6 +573,24 @@ impl SparkWallet { create_transfers(transfers, &self.ssp_client, our_pubkey).await } + /// Queries the SSP for user requests by their associated transfer IDs + /// and returns a map of transfer IDs to user requests + pub async fn query_ssp_user_requests( + &self, + transfer_ids: Vec, + ) -> Result, SparkWalletError> { + let transfers = self.ssp_client.get_transfers(transfer_ids).await?; + Ok(transfers + .into_iter() + .filter_map( + |transfer| match (transfer.spark_id, transfer.user_request) { + (Some(spark_id), Some(user_request)) => Some((spark_id, user_request)), + _ => None, + }, + ) + .collect()) + } + /// Signs a message with the identity key using ECDSA and returns the signature. /// /// If exposing this, consider adding a prefix to prevent mistakenly signing messages. @@ -763,9 +785,9 @@ impl SparkWallet { pub async fn transfer_tokens( &self, outputs: Vec, - ) -> Result { - let tx_hash = self.token_service.transfer_tokens(outputs).await?; - Ok(tx_hash) + ) -> Result { + let tx = self.token_service.transfer_tokens(outputs).await?; + Ok(tx) } pub async fn list_token_transactions( @@ -797,6 +819,16 @@ impl SparkWallet { ) .to_string()) } + + pub async fn get_tokens_metadata( + &self, + token_identifiers: &[&str], + ) -> Result, SparkWalletError> { + self.token_service + .get_tokens_metadata(token_identifiers) + .await + .map_err(Into::into) + } } async fn claim_pending_transfers( diff --git a/crates/spark/src/services/lightning.rs b/crates/spark/src/services/lightning.rs index 62f9f93a..8e22a023 100644 --- a/crates/spark/src/services/lightning.rs +++ b/crates/spark/src/services/lightning.rs @@ -350,7 +350,11 @@ impl LightningService { .transfer_service .deliver_transfer_package(&swap.transfer, &swap.leaves, Default::default()) .await?; - let lightning_send_payment = self.finalize_lightning_swap(&swap).await?; + let mut lightning_send_payment = self.finalize_lightning_swap(&swap).await?; + // If ssp doesn't return a transfer id, we use the transfer id from the transfer service + if lightning_send_payment.transfer_id.is_none() { + lightning_send_payment.transfer_id = Some(transfer.id.clone()); + } Ok(PayLightningResult { lightning_send_payment, transfer, diff --git a/crates/spark/src/services/models.rs b/crates/spark/src/services/models.rs index 90ec9c67..4947b674 100644 --- a/crates/spark/src/services/models.rs +++ b/crates/spark/src/services/models.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, HashMap}; use std::fmt::{Debug, Display}; use std::str::FromStr; +use std::time::SystemTime; use bitcoin::secp256k1::ecdsa::Signature; use bitcoin::{Transaction, TxOut, Txid}; @@ -16,7 +17,7 @@ use uuid::Uuid; use crate::address::SparkAddress; use crate::core::Network; use crate::operator::rpc as operator_rpc; -use crate::services::bech32m_encode_token_id; +use crate::services::{HashableTokenTransaction, bech32m_encode_token_id}; use crate::signer::{FrostSigningCommitmentsWithNonces, PrivateKeySource}; use crate::tree::{SigningKeyshare, TreeNode, TreeNodeId}; use crate::{ssp::BitcoinNetwork, utils::refund::SignedTx}; @@ -820,9 +821,54 @@ impl #[derive(Debug, Clone, Deserialize, Serialize)] pub struct TokenTransaction { + pub hash: String, pub inputs: TokenInputs, pub outputs: Vec, pub status: TokenTransactionStatus, + pub created_timestamp: SystemTime, +} + +impl TryFrom<(operator_rpc::spark_token::TokenTransaction, Network)> for TokenTransaction { + type Error = ServiceError; + + fn try_from( + (token_transaction, network): (operator_rpc::spark_token::TokenTransaction, Network), + ) -> Result { + let hash = hex::encode(token_transaction.compute_hash(false)?); + + let inputs = token_transaction + .token_inputs + .ok_or(ServiceError::Generic("Missing token inputs".to_string()))? + .try_into()?; + + let outputs = token_transaction + .token_outputs + .into_iter() + .map(|output| (output, network).try_into()) + .collect::, _>>()?; + + let status = TokenTransactionStatus::Unknown; + + // client_created_timestamp will always be filled for V2 transactions and V1 transactions will be discontinued soon + let created_timestamp = token_transaction + .client_created_timestamp + .map(|ts| { + std::time::UNIX_EPOCH + + std::time::Duration::from_secs(ts.seconds as u64) + + std::time::Duration::from_nanos(ts.nanos as u64) + }) + .ok_or(ServiceError::Generic( + "Missing client created timestamp. Could this be a V1 transaction?".to_string(), + ))?; + + Ok(TokenTransaction { + hash, + inputs, + outputs, + status, + created_timestamp, + }) + } } impl @@ -843,6 +889,8 @@ impl "Missing token transaction".to_string(), ))?; + let hash = hex::encode(transaction.token_transaction_hash); + let inputs = token_transaction .token_inputs .ok_or(ServiceError::Generic("Missing token inputs".to_string()))? @@ -859,10 +907,24 @@ impl .map_err(|_| ServiceError::Generic("Invalid token transaction status".to_string()))? .into(); + // client_created_timestamp will always be filled for V2 transactions and V1 transactions will be discontinued soon + let created_timestamp = token_transaction + .client_created_timestamp + .map(|ts| { + std::time::UNIX_EPOCH + + std::time::Duration::from_secs(ts.seconds as u64) + + std::time::Duration::from_nanos(ts.nanos as u64) + }) + .ok_or(ServiceError::Generic( + "Missing client created timestamp. Could this be a V1 transaction?".to_string(), + ))?; + Ok(TokenTransaction { + hash, inputs, outputs, status, + created_timestamp, }) } } @@ -934,8 +996,8 @@ impl TryFrom for TokenTransferInp #[derive(Debug, Clone, Deserialize, Serialize)] pub struct TokenOutputToSpend { - prev_token_tx_hash: String, - prev_token_tx_vout: u32, + pub prev_token_tx_hash: String, + pub prev_token_tx_vout: u32, } impl TryFrom for TokenOutputToSpend { @@ -1004,11 +1066,18 @@ impl TryFrom for TokenCreateInput { #[derive(Debug, Clone, Copy, Deserialize, Serialize)] pub enum TokenTransactionStatus { + /// Transaction was successfully constructed and validated by the Operator. Started, + /// If not transfer transaction, transaction was accepted by the Operator and outputs are spendable. + /// Else, transaction was accepted by the Operator, inputs are 'locked' until consensus or until transaction expiry (in the event that consensus is not reached) Signed, + /// Operator has shared its revocation secret shares with other operators and is waiting for the system to collect enough shares to finalize the transaction. Revealed, + /// Transaction has reached consensus across operators. Transaction is final. Finalized, + /// Transaction was cancelled and cannot be recovered. StartedCancelled, + /// Transaction was cancelled and cannot be recovered. SignedCancelled, Unknown, } @@ -1043,7 +1112,8 @@ impl From for TokenTransactio #[derive(Debug, Clone)] pub struct QueryTokenTransactionsFilter { - pub owner_public_keys: Vec, + /// If not provided, will use our own public key + pub owner_public_keys: Option>, pub issuer_public_keys: Vec, pub token_transaction_hashes: Vec, pub token_ids: Vec, diff --git a/crates/spark/src/services/tokens.rs b/crates/spark/src/services/tokens.rs index 618b1958..3cfab45f 100644 --- a/crates/spark/src/services/tokens.rs +++ b/crates/spark/src/services/tokens.rs @@ -116,16 +116,7 @@ impl TokenService { // Fetch metadata for owned tokens let token_identifiers = outputs_map.keys().cloned().collect(); - let metadata = self - .operator_pool - .get_coordinator() - .client - .query_token_metadata(QueryTokenMetadataRequest { - token_identifiers, - ..Default::default() - }) - .await? - .token_metadata; + let metadata = self.query_tokens_metadata_inner(token_identifiers).await?; if metadata.len() != outputs_map.keys().len() { return Err(ServiceError::Generic( @@ -154,6 +145,76 @@ impl TokenService { Ok(()) } + /// Returns the metadata for the given token identifiers. + /// + /// For token identifiers that are not found in the local cache, the metadata will be queried from the SE. + pub async fn get_tokens_metadata( + &self, + token_identifiers: &[&str], + ) -> Result, ServiceError> { + let cached_outputs = { self.tokens_outputs.lock().await.clone() }; + + // Separate token identifiers into cached and uncached + let mut cached_metadata = Vec::new(); + let mut uncached_identifiers = Vec::new(); + + for token_id in token_identifiers { + if let Some(outputs) = cached_outputs.get(*token_id) { + cached_metadata.push(outputs.metadata.clone()); + } else { + uncached_identifiers.push(*token_id); + } + } + + // Query metadata for uncached tokens + let mut queried_metadata = Vec::new(); + if !uncached_identifiers.is_empty() { + queried_metadata = self.query_tokens_metadata(&uncached_identifiers).await?; + } + + // Combine cached and queried metadata + let mut all_metadata = cached_metadata; + all_metadata.extend(queried_metadata); + + Ok(all_metadata) + } + + async fn query_tokens_metadata( + &self, + token_identifiers: &[&str], + ) -> Result, ServiceError> { + let token_identifiers = token_identifiers + .iter() + .map(|id| { + bech32m_decode_token_id(id, Some(self.network)) + .map_err(|_| ServiceError::Generic("Invalid token id".to_string())) + }) + .collect::>, _>>()?; + let metadata = self.query_tokens_metadata_inner(token_identifiers).await?; + let metadata = metadata + .into_iter() + .map(|m| (m, self.network).try_into()) + .collect::, _>>()?; + Ok(metadata) + } + + async fn query_tokens_metadata_inner( + &self, + token_identifiers: Vec>, + ) -> Result, ServiceError> { + let metadata = self + .operator_pool + .get_coordinator() + .client + .query_token_metadata(QueryTokenMetadataRequest { + token_identifiers, + ..Default::default() + }) + .await? + .token_metadata; + Ok(metadata) + } + /// Returns owned token outputs from the local cache. pub async fn get_tokens_outputs(&self) -> HashMap { self.tokens_outputs.lock().await.clone() @@ -164,14 +225,16 @@ impl TokenService { filter: QueryTokenTransactionsFilter, paging: PagingFilter, ) -> Result, ServiceError> { - let mut owner_public_keys = filter - .owner_public_keys - .iter() - .map(|k| k.serialize().to_vec()) - .collect::>(); - if owner_public_keys.is_empty() { - owner_public_keys.push(self.signer.get_identity_public_key()?.serialize().to_vec()); - } + let owner_public_keys = match filter.owner_public_keys { + Some(keys) => keys + .iter() + .map(|k| k.serialize().to_vec()) + .collect::>(), + None => vec![self.signer.get_identity_public_key()?.serialize().to_vec()], + }; + + // TODO: ask for ordering field to be added to QueryTokenTransactionsRequest + // until then, PagingFilter's order is not being respected let response = self .operator_pool .get_coordinator() @@ -237,7 +300,7 @@ impl TokenService { pub async fn transfer_tokens( &self, receiver_outputs: Vec, - ) -> Result { + ) -> Result { // Validate parameters if receiver_outputs.is_empty() { return Err(ServiceError::Generic( @@ -284,6 +347,7 @@ impl TokenService { let identity_public_key_bytes = self.signer.get_identity_public_key()?.serialize(); final_tx .token_outputs + .clone() .into_iter() .enumerate() .filter(|(_, o)| o.owner_public_key == identity_public_key_bytes) @@ -296,7 +360,7 @@ impl TokenService { Ok(()) })?; - Ok(txid) + (final_tx, self.network).try_into() } /// Selects tokens to match a given amount. @@ -768,7 +832,7 @@ pub(crate) fn bech32m_decode_token_id( const TOKEN_TRANSACTION_TRANSFER_TYPE: u32 = 3; -trait HashableTokenTransaction { +pub trait HashableTokenTransaction { fn compute_hash(&self, partial: bool) -> Result, ServiceError>; } diff --git a/crates/spark/src/ssp/graphql/models.rs b/crates/spark/src/ssp/graphql/models.rs index d0e912e9..ba7cad6a 100644 --- a/crates/spark/src/ssp/graphql/models.rs +++ b/crates/spark/src/ssp/graphql/models.rs @@ -349,6 +349,40 @@ pub enum SspUserRequest { LightningSendRequest(LightningSendRequest), } +impl SspUserRequest { + pub fn get_lightning_invoice(&self) -> Option { + let invoice = match self { + SspUserRequest::LightningReceiveRequest(request) => { + Some(request.invoice.encoded_invoice.clone()) + } + SspUserRequest::LightningSendRequest(request) => Some(request.encoded_invoice.clone()), + _ => None, + }; + invoice.map(|i| i.to_lowercase()) + } + + pub fn get_lightning_preimage(&self) -> Option { + match self { + SspUserRequest::LightningReceiveRequest(request) => { + request.lightning_receive_payment_preimage.clone() + } + SspUserRequest::LightningSendRequest(request) => { + request.lightning_send_payment_preimage.clone() + } + _ => None, + } + } + + pub fn get_total_fees_sats(&self) -> u64 { + match self { + SspUserRequest::LightningSendRequest(request) => request.fee.as_sats().unwrap_or(0), + SspUserRequest::CoopExitRequest(request) => request.get_total_fees_sats(), + SspUserRequest::ClaimStaticDeposit(request) => request.get_total_fees_sats(), + _ => 0, + } + } +} + #[macros::derive_from(TransfersClaimStaticDepositFragment)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct ClaimStaticDepositInfo { @@ -365,7 +399,13 @@ pub struct ClaimStaticDepositInfo { pub transfer_spark_id: Option, } -#[derive(Debug, Clone, Copy, Deserialize, Serialize, Eq, PartialEq)] +impl ClaimStaticDepositInfo { + pub fn get_total_fees_sats(&self) -> u64 { + self.max_fee.as_sats().unwrap_or(0) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum ClaimStaticDepositStatus { Created, @@ -494,6 +534,15 @@ pub struct CoopExitRequest { pub transfer: Option, } +impl CoopExitRequest { + pub fn get_total_fees_sats(&self) -> u64 { + self.fee + .as_sats() + .unwrap_or(0) + .saturating_add(self.l1_broadcast_fee.as_sats().unwrap_or(0)) + } +} + /// CoopExitFeeQuote structure #[derive(Debug, Clone, Deserialize)] #[macros::derive_from(CoopExitFeeQuoteCoopExitFeeQuoteQuote)] diff --git a/docs/breez-sdk/snippets/rust/src/send_payment.rs b/docs/breez-sdk/snippets/rust/src/send_payment.rs index 266661b7..572cb37a 100644 --- a/docs/breez-sdk/snippets/rust/src/send_payment.rs +++ b/docs/breez-sdk/snippets/rust/src/send_payment.rs @@ -10,7 +10,8 @@ async fn prepare_send_payment_lightning_bolt11(sdk: &BreezSdk) -> Result<()> { let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request, - amount_sats: optional_amount_sats, + amount: optional_amount_sats, + token_identifier: None, }) .await?; @@ -38,7 +39,8 @@ async fn prepare_send_payment_lightning_onchain(sdk: &BreezSdk) -> Result<()> { let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request, - amount_sats, + amount: amount_sats, + token_identifier: None, }) .await?; @@ -63,13 +65,14 @@ async fn prepare_send_payment_spark(sdk: &BreezSdk) -> Result<()> { let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request, - amount_sats, + amount: amount_sats, + token_identifier: None, }) .await?; // If the fees are acceptable, continue to create the Send Payment - if let SendPaymentMethod::SparkAddress { fee_sats, .. } = prepare_response.payment_method { - info!("Fees: {} sats", fee_sats); + if let SendPaymentMethod::SparkAddress { fee, .. } = prepare_response.payment_method { + info!("Fees: {} sats", fee); } // ANCHOR_END: prepare-send-payment-spark Ok(()) diff --git a/packages/flutter/rust/Cargo.lock b/packages/flutter/rust/Cargo.lock index 0b8b133a..bd193ee9 100644 --- a/packages/flutter/rust/Cargo.lock +++ b/packages/flutter/rust/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -84,12 +84,6 @@ dependencies = [ "backtrace", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_log-sys" version = "0.3.2" @@ -134,9 +128,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -169,9 +163,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arrayref" @@ -342,9 +336,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -352,7 +346,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link 0.2.0", ] [[package]] @@ -518,7 +512,7 @@ dependencies = [ "spark", "spark-wallet", "strum", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tonic", "tonic-build", @@ -553,7 +547,7 @@ dependencies = [ "shellwords", "spark-wallet", "tempdir", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio_with_wasm", "tracing", @@ -615,9 +609,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.35" +version = "1.2.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590f9024a68a8c40351881787f1934dc11afd69090f5edb6831464694d836ea3" +checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" dependencies = [ "find-msvc-tools", "shlex", @@ -631,17 +625,16 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -656,9 +649,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.47" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", @@ -666,9 +659,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.47" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", @@ -700,7 +693,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -845,9 +838,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ "darling_core", "darling_macro", @@ -855,9 +848,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", @@ -869,9 +862,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", @@ -946,12 +939,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -1162,12 +1155,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.1", ] [[package]] @@ -1225,9 +1218,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e178e4fba8a2726903f6ba98a6d221e76f9c12c650d5dc0e6afdc50677b49650" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" [[package]] name = "fixedbitset" @@ -1324,7 +1317,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "serdect", - "thiserror 2.0.16", + "thiserror 2.0.17", "thiserror-nostd-notrait", "visibility", "zeroize", @@ -1452,20 +1445,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generator" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1499,7 +1478,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.3+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] @@ -1514,9 +1493,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "graphql-introspection-query" @@ -1599,7 +1578,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.0", + "indexmap 2.11.4", "slab", "tokio", "tokio-util", @@ -1638,9 +1617,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "hashlink" @@ -1730,7 +1709,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustls-pki-types", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", "tinyvec", "tokio", @@ -1754,7 +1733,7 @@ dependencies = [ "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1893,9 +1872,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64", "bytes", @@ -1919,9 +1898,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2067,13 +2046,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "serde", + "serde_core", ] [[package]] @@ -2154,9 +2134,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -2184,9 +2164,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libm" @@ -2288,9 +2268,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -2327,19 +2307,6 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "macros" version = "0.1.0" @@ -2376,9 +2343,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -2414,20 +2381,19 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.10" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" dependencies = [ "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", - "loom", + "equivalent", "parking_lot", "portable-atomic", "rustc_version", "smallvec", "tagptr", - "thiserror 1.0.69", "uuid", ] @@ -2519,9 +2485,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -2591,9 +2557,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.5.2+3.5.2" +version = "300.5.3+3.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" +checksum = "dc6bad8cd0233b63971e232cc9c5e83039375b8586d2312f31fda85db8f888c2" dependencies = [ "cc", ] @@ -2687,7 +2653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.11.0", + "indexmap 2.11.4", ] [[package]] @@ -2900,9 +2866,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -3020,18 +2986,18 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", @@ -3040,9 +3006,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.2" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", @@ -3052,9 +3018,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", @@ -3118,9 +3084,9 @@ dependencies = [ [[package]] name = "resolv-conf" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" +checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" [[package]] name = "rfc6979" @@ -3196,22 +3162,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.1", ] [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "log", "once_cell", @@ -3231,7 +3197,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.3.0", + "security-framework 3.5.1", ] [[package]] @@ -3254,9 +3220,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "ring", "rustls-pki-types", @@ -3277,11 +3243,11 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -3308,12 +3274,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -3370,9 +3330,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.3.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags", "core-foundation 0.10.1", @@ -3383,9 +3343,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -3393,24 +3353,34 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -3419,14 +3389,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -3443,15 +3414,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" dependencies = [ "base64", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.0", + "indexmap 2.11.4", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -3463,9 +3434,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" dependencies = [ "darling", "proc-macro2", @@ -3479,7 +3450,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.11.0", + "indexmap 2.11.4", "itoa", "ryu", "serde", @@ -3604,7 +3575,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio_with_wasm", "tonic", @@ -3626,7 +3597,7 @@ dependencies = [ "hex", "serde", "spark", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio_with_wasm", "tracing", @@ -3772,15 +3743,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.21.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.1", ] [[package]] @@ -3794,11 +3765,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -3814,9 +3785,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -3863,11 +3834,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.43" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde", @@ -3957,9 +3929,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -4220,9 +4192,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "uncased" @@ -4235,9 +4207,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-normalization" @@ -4351,30 +4323,40 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.3+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", @@ -4386,9 +4368,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -4399,9 +4381,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4409,9 +4391,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", @@ -4422,9 +4404,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] @@ -4444,9 +4426,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" dependencies = [ "js-sys", "wasm-bindgen", @@ -4508,57 +4490,24 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-link", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core", -] - [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", + "windows-link 0.2.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" dependencies = [ "proc-macro2", "quote", @@ -4567,9 +4516,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" dependencies = [ "proc-macro2", "quote", @@ -4583,14 +4532,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] -name = "windows-numerics" +name = "windows-link" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core", - "windows-link", -] +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-registry" @@ -4598,9 +4543,9 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -4609,7 +4554,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -4618,7 +4572,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -4654,7 +4617,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.4", +] + +[[package]] +name = "windows-sys" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -4690,11 +4662,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" dependencies = [ - "windows-link", + "windows-link 0.2.0", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -4705,15 +4677,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4864,9 +4827,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -4923,18 +4886,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", @@ -4964,9 +4927,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] diff --git a/packages/flutter/rust/src/models.rs b/packages/flutter/rust/src/models.rs index ac2b6d85..53f6d8ad 100644 --- a/packages/flutter/rust/src/models.rs +++ b/packages/flutter/rust/src/models.rs @@ -5,6 +5,7 @@ pub use breez_sdk_common::lnurl::pay::*; pub use breez_sdk_common::network::BitcoinNetwork; pub use breez_sdk_spark::*; use flutter_rust_bridge::frb; +use std::collections::HashMap; #[frb(mirror(BitcoinAddressDetails))] pub struct _BitcoinAddressDetails { @@ -90,6 +91,24 @@ pub struct _GetInfoRequest { #[frb(mirror(GetInfoResponse))] pub struct _GetInfoResponse { pub balance_sats: u64, + pub token_balances: HashMap, +} + +#[frb(mirror(TokenBalance))] +pub struct _TokenBalance { + pub balance: u64, + pub token_metadata: TokenMetadata, +} + +#[frb(mirror(TokenMetadata))] +pub struct _TokenMetadata { + pub identifier: String, + pub issuer_public_key: String, + pub name: String, + pub ticker: String, + pub decimals: u32, + pub max_supply: u64, + pub is_freezable: bool, } #[frb(mirror(GetPaymentRequest))] @@ -187,13 +206,15 @@ pub struct _PrepareLnurlPayResponse { #[frb(mirror(PrepareSendPaymentRequest))] pub struct _PrepareSendPaymentRequest { pub payment_request: String, - pub amount_sats: Option, + pub amount: Option, + pub token_identifier: Option, } #[frb(mirror(PrepareSendPaymentResponse))] pub struct _PrepareSendPaymentResponse { pub payment_method: SendPaymentMethod, - pub amount_sats: u64, + pub amount: u64, + pub token_identifier: Option, } #[frb(mirror(ReceivePaymentMethod))] @@ -259,7 +280,8 @@ pub enum _SendPaymentMethod { }, SparkAddress { address: String, - fee_sats: u64, + fee: u64, + token_identifier: Option, }, } @@ -357,6 +379,10 @@ pub struct _Payment { #[frb(mirror(PaymentDetails))] pub enum _PaymentDetails { Spark, + Token { + metadata: TokenMetadata, + tx_hash: String, + }, Lightning { description: Option, preimage: Option, @@ -383,6 +409,7 @@ pub struct _PaymentMetadata { pub enum _PaymentMethod { Lightning, Spark, + Token, Deposit, Withdraw, Unknown, diff --git a/packages/flutter/test/helper.dart b/packages/flutter/test/helper.dart index 477ccd2d..46c7b9e4 100644 --- a/packages/flutter/test/helper.dart +++ b/packages/flutter/test/helper.dart @@ -6,12 +6,16 @@ extension ConfigCopyWith on Config { Network? network, int? syncIntervalSecs, Fee? maxDepositClaimFee, + String? lnurlDomain, + bool? preferSparkOverLightning, }) { return Config( apiKey: apiKey ?? this.apiKey, network: network ?? this.network, syncIntervalSecs: syncIntervalSecs ?? this.syncIntervalSecs, maxDepositClaimFee: maxDepositClaimFee ?? this.maxDepositClaimFee, + lnurlDomain: lnurlDomain ?? this.lnurlDomain, + preferSparkOverLightning: preferSparkOverLightning ?? this.preferSparkOverLightning, ); } } diff --git a/packages/wasm/examples/web/src/main.js b/packages/wasm/examples/web/src/main.js index d9ab8608..b52fbfd1 100644 --- a/packages/wasm/examples/web/src/main.js +++ b/packages/wasm/examples/web/src/main.js @@ -201,12 +201,12 @@ async function initializeSdk() { } if (!CONFIG.apiKey) { - throw new Error('API key is required') + throw new Error("API key is required"); } // Get default config - const config = defaultConfig(CONFIG.network) - config.apiKey = CONFIG.apiKey + const config = defaultConfig(CONFIG.network); + config.apiKey = CONFIG.apiKey; // Create SDK builder let storage = await defaultStorage("BreezSDK"); @@ -304,11 +304,12 @@ function displayPayments(payments) {
${direction}: ${amount.toLocaleString()} sats
${description}
${new Date( - payment.timestamp * 1000 - ).toLocaleString()}
+ payment.timestamp * 1000 + ).toLocaleString()} -
${payment.status - }
+
${ + payment.status + }
`; elements.paymentsList.appendChild(paymentDiv);