Skip to main content

cdk_bdk/storage/
receive.rs

1use uuid::Uuid;
2
3use super::{
4    finalized_receive_intent_by_quote_namespace, outpoint_to_key, BdkStorage,
5    FinalizedReceiveIntentRecord, BDK_NAMESPACE, FINALIZED_RECEIVE_INTENT_NAMESPACE,
6    FINALIZED_RECEIVE_INTENT_OUTPOINT_NAMESPACE, RECEIVE_ADDRESS_QUOTE_ID_NAMESPACE,
7    RECEIVE_INTENT_NAMESPACE, RECEIVE_INTENT_OUTPOINT_NAMESPACE,
8};
9use crate::error::Error;
10use crate::receive::receive_intent::record::ReceiveIntentRecord;
11
12impl BdkStorage {
13    // ── Receive address index storage ────────────────────────────────
14
15    /// Track a generated receive address by quote ID.
16    ///
17    /// Returns `true` when the address was reserved for `quote_id` (or was
18    /// already tracked for that same quote). Returns `false` when the
19    /// address is already tracked for a different quote; the caller must
20    /// derive a fresh address and retry.
21    pub async fn track_receive_address(
22        &self,
23        address: &str,
24        quote_id: &str,
25    ) -> Result<bool, Error> {
26        let mut tx = self
27            .kv_store
28            .begin_transaction()
29            .await
30            .map_err(Error::from)?;
31
32        // Reserve the address atomically: an address must never be remapped
33        // to a different quote once tracked.
34        let reserved = tx
35            .kv_write_if_absent(
36                BDK_NAMESPACE,
37                RECEIVE_ADDRESS_QUOTE_ID_NAMESPACE,
38                address,
39                quote_id.as_bytes(),
40            )
41            .await
42            .map_err(Error::from)?;
43
44        if !reserved {
45            let existing = tx
46                .kv_read(BDK_NAMESPACE, RECEIVE_ADDRESS_QUOTE_ID_NAMESPACE, address)
47                .await
48                .map_err(Error::from)?;
49            let same_quote = existing.as_deref() == Some(quote_id.as_bytes());
50            tx.rollback().await.map_err(Error::from)?;
51            return Ok(same_quote);
52        }
53
54        tx.commit().await.map_err(Error::from)?;
55        Ok(true)
56    }
57
58    /// Get the quote ID for a tracked receive address.
59    pub async fn get_quote_id_by_receive_address(
60        &self,
61        address: &str,
62    ) -> Result<Option<String>, Error> {
63        let quote_id_bytes = self
64            .kv_store
65            .kv_read(BDK_NAMESPACE, RECEIVE_ADDRESS_QUOTE_ID_NAMESPACE, address)
66            .await
67            .map_err(Error::from)?;
68
69        let Some(quote_id_bytes) = quote_id_bytes else {
70            return Ok(None);
71        };
72
73        let quote_id = String::from_utf8(quote_id_bytes)
74            .map_err(|e| Error::Wallet(format!("Invalid quote-id index entry: {}", e)))?;
75        Ok(Some(quote_id))
76    }
77
78    /// Get all tracked receive addresses.
79    pub async fn get_tracked_receive_addresses(&self) -> Result<Vec<String>, Error> {
80        self.kv_store
81            .kv_list(BDK_NAMESPACE, RECEIVE_ADDRESS_QUOTE_ID_NAMESPACE)
82            .await
83            .map_err(Error::from)
84    }
85
86    // ── Receive Intent storage ───────────────────────────────────────
87
88    /// Store a new receive intent if no intent already tracks the same outpoint.
89    ///
90    /// Uses the outpoint as a secondary index key to ensure idempotent
91    /// detection. Returns `true` if the intent was created, `false` if a
92    /// duplicate outpoint was found (silently skipped).
93    pub async fn create_receive_intent_if_absent(
94        &self,
95        intent: &ReceiveIntentRecord,
96    ) -> Result<bool, Error> {
97        let outpoint = match &intent.state {
98            crate::receive::receive_intent::record::ReceiveIntentState::Detected {
99                outpoint,
100                ..
101            } => outpoint.clone(),
102        };
103
104        let mut tx = self
105            .kv_store
106            .begin_transaction()
107            .await
108            .map_err(Error::from)?;
109
110        // Check outpoint index for duplicates (active and finalized)
111        let outpoint_key = outpoint_to_key(&outpoint);
112        let active = tx
113            .kv_read(
114                BDK_NAMESPACE,
115                RECEIVE_INTENT_OUTPOINT_NAMESPACE,
116                &outpoint_key,
117            )
118            .await
119            .map_err(Error::from)?;
120
121        if active.is_some() {
122            tx.rollback().await.map_err(Error::from)?;
123            return Ok(false);
124        }
125
126        let finalized = tx
127            .kv_read(
128                BDK_NAMESPACE,
129                FINALIZED_RECEIVE_INTENT_OUTPOINT_NAMESPACE,
130                &outpoint_key,
131            )
132            .await
133            .map_err(Error::from)?;
134
135        if finalized.is_some() {
136            tx.rollback().await.map_err(Error::from)?;
137            return Ok(false);
138        }
139
140        let serialized = serde_json::to_vec(intent)?;
141        tx.kv_write(
142            BDK_NAMESPACE,
143            RECEIVE_INTENT_NAMESPACE,
144            &intent.intent_id.to_string(),
145            &serialized,
146        )
147        .await
148        .map_err(Error::from)?;
149        tx.kv_write(
150            BDK_NAMESPACE,
151            RECEIVE_INTENT_OUTPOINT_NAMESPACE,
152            &outpoint_key,
153            intent.intent_id.to_string().as_bytes(),
154        )
155        .await
156        .map_err(Error::from)?;
157        tx.commit().await.map_err(Error::from)?;
158        Ok(true)
159    }
160
161    /// Get a receive intent by ID.
162    pub async fn get_receive_intent(
163        &self,
164        intent_id: &Uuid,
165    ) -> Result<Option<ReceiveIntentRecord>, Error> {
166        self.get_record::<ReceiveIntentRecord>(&intent_id.to_string())
167            .await
168    }
169
170    /// Get all active receive intents.
171    pub async fn get_all_receive_intents(&self) -> Result<Vec<ReceiveIntentRecord>, Error> {
172        self.list_records::<ReceiveIntentRecord>().await
173    }
174
175    /// Delete an active receive intent.
176    #[cfg(test)]
177    pub async fn delete_receive_intent(&self, intent_id: &Uuid) -> Result<(), Error> {
178        let Some(intent) = self.get_receive_intent(intent_id).await? else {
179            return Ok(());
180        };
181
182        let outpoint_key = match &intent.state {
183            crate::receive::receive_intent::record::ReceiveIntentState::Detected {
184                outpoint,
185                ..
186            } => outpoint_to_key(outpoint),
187        };
188
189        let mut tx = self
190            .kv_store
191            .begin_transaction()
192            .await
193            .map_err(Error::from)?;
194        tx.kv_remove(
195            BDK_NAMESPACE,
196            RECEIVE_INTENT_NAMESPACE,
197            &intent_id.to_string(),
198        )
199        .await
200        .map_err(Error::from)?;
201        tx.kv_remove(
202            BDK_NAMESPACE,
203            RECEIVE_INTENT_OUTPOINT_NAMESPACE,
204            &outpoint_key,
205        )
206        .await
207        .map_err(Error::from)?;
208        tx.commit().await.map_err(Error::from)?;
209        Ok(())
210    }
211
212    // ── Finalized Receive Intent storage (tombstones) ────────────────
213
214    /// Atomically finalize an active receive intent and create a tombstone.
215    pub async fn finalize_receive_intent(
216        &self,
217        intent_id: &Uuid,
218        record: &FinalizedReceiveIntentRecord,
219    ) -> Result<(), Error> {
220        let Some(intent) = self.get_receive_intent(intent_id).await? else {
221            return Err(Error::ReceiveIntentNotFound(*intent_id));
222        };
223
224        let outpoint_key = match &intent.state {
225            crate::receive::receive_intent::record::ReceiveIntentState::Detected {
226                outpoint,
227                ..
228            } => outpoint_to_key(outpoint),
229        };
230
231        let serialized = serde_json::to_vec(record)?;
232        let mut tx = self
233            .kv_store
234            .begin_transaction()
235            .await
236            .map_err(Error::from)?;
237
238        tx.kv_write(
239            BDK_NAMESPACE,
240            FINALIZED_RECEIVE_INTENT_NAMESPACE,
241            &record.intent_id.to_string(),
242            &serialized,
243        )
244        .await
245        .map_err(Error::from)?;
246        tx.kv_write(
247            BDK_NAMESPACE,
248            FINALIZED_RECEIVE_INTENT_OUTPOINT_NAMESPACE,
249            &outpoint_key,
250            record.intent_id.to_string().as_bytes(),
251        )
252        .await
253        .map_err(Error::from)?;
254
255        // Per-quote namespace, one key per intent; see
256        // FINALIZED_RECEIVE_INTENT_BY_QUOTE_NAMESPACE_PREFIX for rationale.
257        let quote_ns = finalized_receive_intent_by_quote_namespace(&record.quote_id);
258        tx.kv_write(
259            BDK_NAMESPACE,
260            &quote_ns,
261            &record.intent_id.to_string(),
262            record.intent_id.to_string().as_bytes(),
263        )
264        .await
265        .map_err(Error::from)?;
266
267        tx.kv_remove(
268            BDK_NAMESPACE,
269            RECEIVE_INTENT_NAMESPACE,
270            &intent_id.to_string(),
271        )
272        .await
273        .map_err(Error::from)?;
274        tx.kv_remove(
275            BDK_NAMESPACE,
276            RECEIVE_INTENT_OUTPOINT_NAMESPACE,
277            &outpoint_key,
278        )
279        .await
280        .map_err(Error::from)?;
281        tx.commit().await.map_err(Error::from)?;
282        Ok(())
283    }
284
285    /// Look up a finalized receive intent tombstone by intent ID.
286    #[cfg(test)]
287    pub async fn get_finalized_receive_intent(
288        &self,
289        intent_id: &Uuid,
290    ) -> Result<Option<FinalizedReceiveIntentRecord>, Error> {
291        self.get_record::<FinalizedReceiveIntentRecord>(&intent_id.to_string())
292            .await
293    }
294
295    /// Look up finalized receive intent tombstones by quote ID.
296    ///
297    /// The index is stored as one key per finalized intent under the
298    /// per-quote secondary namespace
299    /// `finalized_receive_intent_by_quote__<quote_id>`, so listing the
300    /// namespace yields all finalized intent IDs for this quote.
301    pub async fn get_finalized_receive_intents_by_quote_id(
302        &self,
303        quote_id: &str,
304    ) -> Result<Vec<FinalizedReceiveIntentRecord>, Error> {
305        let quote_ns = finalized_receive_intent_by_quote_namespace(quote_id);
306        let intent_id_keys = self
307            .kv_store
308            .kv_list(BDK_NAMESPACE, &quote_ns)
309            .await
310            .map_err(Error::from)?;
311
312        let mut results = Vec::new();
313        for intent_id_key in intent_id_keys {
314            if let Some(record) = self
315                .get_record::<FinalizedReceiveIntentRecord>(&intent_id_key)
316                .await?
317            {
318                results.push(record);
319            }
320        }
321        Ok(results)
322    }
323}