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        receiver_outputs: Vec<spark_wallet::ReceiverTokenOutput>,
138    ) -> Result<(), TransferObserverError> {
139        Ok(self
140            .inner
141            .before_send(
142                receiver_outputs
143                    .into_iter()
144                    .enumerate()
145                    .map(|(index, output)| ProvisionalPayment {
146                        payment_id: format!("{partial_tx_id}:{index}"),
147                        amount: output.amount,
148                        details: ProvisionalPaymentDetails::Token {
149                            token_id: output.token_id,
150                            pay_request: output.pay_request,
151                        },
152                    })
153                    .collect(),
154            )
155            .await?)
156    }
157
158    async fn before_send_transfer(
159        &self,
160        transfer_id: &TransferId,
161        receiver_address: &str,
162        amount_sats: u64,
163    ) -> Result<(), TransferObserverError> {
164        Ok(self
165            .inner
166            .before_send(vec![ProvisionalPayment {
167                payment_id: transfer_id.to_string(),
168                amount: u128::from(amount_sats),
169                details: ProvisionalPaymentDetails::Spark {
170                    pay_request: receiver_address.to_string(),
171                },
172            }])
173            .await?)
174    }
175
176    async fn after_send_token(
177        &self,
178        partial_tx_id: &str,
179        final_tx_id: &str,
180        receiver_output_count: usize,
181    ) -> Result<(), TransferObserverError> {
182        // Pair each provisional id minted by before_send_token with its final id. The receiver
183        // outputs keep their order (and vout) across the partial and final transaction, so index i
184        // maps to vout i.
185        let updates = (0..receiver_output_count)
186            .map(|i| PaymentIdUpdate {
187                provisional_payment_id: format!("{partial_tx_id}:{i}"),
188                final_payment_id: format!("{final_tx_id}:{i}"),
189            })
190            .collect();
191        Ok(self.inner.after_send(updates).await?)
192    }
193}