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