Skip to main content

breez_sdk_spark/models/
payment_observer.rs

1use std::sync::Arc;
2
3use spark_wallet::{TransferId, TransferObserverError};
4use thiserror::Error;
5
6#[derive(Debug, Clone)]
7#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
8pub struct ProvisionalPayment {
9    /// Unique identifier for the payment
10    pub payment_id: String,
11    /// Amount in satoshis or token base units
12    pub amount: u128,
13    /// Details of the payment
14    pub details: ProvisionalPaymentDetails,
15}
16
17#[derive(Debug, Clone)]
18#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
19pub enum ProvisionalPaymentDetails {
20    Bitcoin {
21        /// Onchain Bitcoin address
22        withdrawal_address: String,
23    },
24    Lightning {
25        /// BOLT11 invoice
26        invoice: String,
27    },
28    Spark {
29        /// Spark pay request being paid (either a Spark address or a Spark invoice)
30        pay_request: String,
31    },
32    Token {
33        /// Token identifier
34        token_id: String,
35        /// Spark pay request being paid (either a Spark address or a Spark invoice)
36        pay_request: String,
37    },
38}
39
40#[derive(Debug, Clone)]
41#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
42pub struct PaymentIdUpdate {
43    /// Provisional payment id reported by `before_send`, in the form `{partial_tx_id}:{index}`
44    pub provisional_payment_id: String,
45    /// Final payment id once the transaction is broadcast, in the form `{final_tx_id}:{vout}`
46    pub final_payment_id: String,
47}
48
49#[derive(Debug, Error, Clone)]
50#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
51pub enum PaymentObserverError {
52    #[error("Service connectivity: {0}")]
53    ServiceConnectivity(String),
54    #[error("Generic: {0}")]
55    Generic(String),
56}
57
58impl From<PaymentObserverError> for TransferObserverError {
59    fn from(error: PaymentObserverError) -> Self {
60        match error {
61            PaymentObserverError::ServiceConnectivity(msg) => {
62                TransferObserverError::ServiceConnectivity(msg)
63            }
64            PaymentObserverError::Generic(msg) => TransferObserverError::Generic(msg),
65        }
66    }
67}
68
69/// This interface is used to observe outgoing Lightning, Spark, onchain Bitcoin and token payments.
70///
71/// `before_send` is called before a payment is made; if the implementation returns an error the
72/// payment is cancelled. `after_send` is called after a token payment has been broadcast to report
73/// its final payment id; it cannot cancel the payment and any error it returns is ignored.
74#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
75#[macros::async_trait]
76pub trait PaymentObserver: Send + Sync {
77    /// Called before Lightning, Spark, onchain Bitcoin or token payments are made
78    async fn before_send(
79        &self,
80        payments: Vec<ProvisionalPayment>,
81    ) -> Result<(), PaymentObserverError>;
82    /// Called after a token payment has been broadcast, mapping each provisional payment id
83    /// reported by `before_send` to its final payment id
84    async fn after_send(&self, updates: Vec<PaymentIdUpdate>) -> Result<(), PaymentObserverError>;
85}
86
87pub(crate) struct SparkTransferObserver {
88    inner: Arc<dyn PaymentObserver>,
89}
90
91impl SparkTransferObserver {
92    pub fn new(inner: Arc<dyn PaymentObserver>) -> Self {
93        Self { inner }
94    }
95}
96
97#[macros::async_trait]
98impl spark_wallet::TransferObserver for SparkTransferObserver {
99    async fn before_coop_exit(
100        &self,
101        transfer_id: &TransferId,
102        withdrawal_address: &bitcoin::Address,
103        amount_sats: u64,
104    ) -> Result<(), TransferObserverError> {
105        Ok(self
106            .inner
107            .before_send(vec![ProvisionalPayment {
108                payment_id: transfer_id.to_string(),
109                amount: u128::from(amount_sats),
110                details: ProvisionalPaymentDetails::Bitcoin {
111                    withdrawal_address: withdrawal_address.to_string(),
112                },
113            }])
114            .await?)
115    }
116    async fn before_send_lightning_payment(
117        &self,
118        transfer_id: &TransferId,
119        invoice: &str,
120        amount_sats: u64,
121    ) -> Result<(), TransferObserverError> {
122        Ok(self
123            .inner
124            .before_send(vec![ProvisionalPayment {
125                payment_id: transfer_id.to_string(),
126                amount: u128::from(amount_sats),
127                details: ProvisionalPaymentDetails::Lightning {
128                    invoice: invoice.to_string(),
129                },
130            }])
131            .await?)
132    }
133
134    async fn before_send_token(
135        &self,
136        partial_tx_id: &str,
137        token_id: &str,
138        receiver_outputs: Vec<spark_wallet::ReceiverTokenOutput>,
139    ) -> Result<(), TransferObserverError> {
140        Ok(self
141            .inner
142            .before_send(
143                receiver_outputs
144                    .into_iter()
145                    .enumerate()
146                    .map(|(index, output)| ProvisionalPayment {
147                        payment_id: format!("{partial_tx_id}:{index}"),
148                        amount: output.amount,
149                        details: ProvisionalPaymentDetails::Token {
150                            token_id: token_id.to_string(),
151                            pay_request: output.pay_request,
152                        },
153                    })
154                    .collect(),
155            )
156            .await?)
157    }
158
159    async fn before_send_transfer(
160        &self,
161        transfer_id: &TransferId,
162        receiver_address: &str,
163        amount_sats: u64,
164    ) -> Result<(), TransferObserverError> {
165        Ok(self
166            .inner
167            .before_send(vec![ProvisionalPayment {
168                payment_id: transfer_id.to_string(),
169                amount: u128::from(amount_sats),
170                details: ProvisionalPaymentDetails::Spark {
171                    pay_request: receiver_address.to_string(),
172                },
173            }])
174            .await?)
175    }
176
177    async fn after_send_token(
178        &self,
179        partial_tx_id: &str,
180        final_tx_id: &str,
181        receiver_output_count: usize,
182    ) -> Result<(), TransferObserverError> {
183        // Pair each provisional id minted by before_send_token with its final id. The receiver
184        // outputs keep their order (and vout) across the partial and final transaction, so index i
185        // maps to vout i.
186        let updates = (0..receiver_output_count)
187            .map(|i| PaymentIdUpdate {
188                provisional_payment_id: format!("{partial_tx_id}:{i}"),
189                final_payment_id: format!("{final_tx_id}:{i}"),
190            })
191            .collect();
192        Ok(self.inner.after_send(updates).await?)
193    }
194}