use std::str::FromStr;
use uuid::Uuid;
use super::{
outpoint_to_key, BdkStorage, FailedSendAttemptRecord, FinalizedSendIntentRecord, BDK_NAMESPACE,
FINALIZED_INTENT_NAMESPACE, FINALIZED_SEND_INTENT_QUOTE_ID_NAMESPACE, SEND_INTENT_NAMESPACE,
SEND_INTENT_QUOTE_ID_NAMESPACE, SEND_OUTPOINT_QUOTE_ID_BACKFILL_KEY,
SEND_OUTPOINT_QUOTE_ID_NAMESPACE, STORAGE_MIGRATION_NAMESPACE,
};
use crate::error::Error;
use crate::send::batch_transaction::record::{SendBatchRecord, SendBatchState};
use crate::send::payment_intent::record::{SendIntentRecord, SendIntentState};
impl BdkStorage {
pub async fn create_send_intent_if_absent(
&self,
intent: &SendIntentRecord,
) -> Result<(), Error> {
let mut tx = self
.kv_store
.begin_transaction()
.await
.map_err(Error::from)?;
let active = tx
.kv_read(
BDK_NAMESPACE,
SEND_INTENT_QUOTE_ID_NAMESPACE,
&intent.quote_id,
)
.await
.map_err(Error::from)?;
if active.is_some() {
tx.rollback().await.map_err(Error::from)?;
return Err(Error::DuplicateQuoteId(intent.quote_id.clone()));
}
let finalized = tx
.kv_read(
BDK_NAMESPACE,
FINALIZED_SEND_INTENT_QUOTE_ID_NAMESPACE,
&intent.quote_id,
)
.await
.map_err(Error::from)?;
if finalized.is_some() {
tx.rollback().await.map_err(Error::from)?;
return Err(Error::DuplicateQuoteId(intent.quote_id.clone()));
}
let serialized = serde_json::to_vec(intent)?;
tx.kv_write(
BDK_NAMESPACE,
SEND_INTENT_NAMESPACE,
&intent.intent_id.to_string(),
&serialized,
)
.await
.map_err(Error::from)?;
let reserved = tx
.kv_write_if_absent(
BDK_NAMESPACE,
SEND_INTENT_QUOTE_ID_NAMESPACE,
&intent.quote_id,
intent.intent_id.to_string().as_bytes(),
)
.await
.map_err(Error::from)?;
if !reserved {
tx.rollback().await.map_err(Error::from)?;
return Err(Error::DuplicateQuoteId(intent.quote_id.clone()));
}
if let SendIntentState::AwaitingConfirmation { outpoint, .. } = &intent.state {
tx.kv_write(
BDK_NAMESPACE,
SEND_OUTPOINT_QUOTE_ID_NAMESPACE,
&outpoint_to_key(outpoint),
intent.quote_id.as_bytes(),
)
.await
.map_err(Error::from)?;
}
tx.commit().await.map_err(Error::from)?;
Ok(())
}
pub async fn create_or_retry_failed_send_intent(
&self,
intent: &SendIntentRecord,
) -> Result<SendIntentRecord, Error> {
let mut tx = self
.kv_store
.begin_transaction()
.await
.map_err(Error::from)?;
let finalized = tx
.kv_read(
BDK_NAMESPACE,
FINALIZED_SEND_INTENT_QUOTE_ID_NAMESPACE,
&intent.quote_id,
)
.await
.map_err(Error::from)?;
if finalized.is_some() {
tx.rollback().await.map_err(Error::from)?;
return Err(Error::DuplicateQuoteId(intent.quote_id.clone()));
}
let active = tx
.kv_read(
BDK_NAMESPACE,
SEND_INTENT_QUOTE_ID_NAMESPACE,
&intent.quote_id,
)
.await
.map_err(Error::from)?;
let record = if let Some(intent_id_bytes) = active {
let intent_id_str = std::str::from_utf8(&intent_id_bytes)
.map_err(|e| Error::Wallet(format!("Invalid quote-id index entry: {}", e)))?;
let intent_id = Uuid::from_str(intent_id_str)
.map_err(|e| Error::Wallet(format!("Invalid indexed intent id: {}", e)))?;
let intent_bytes = tx
.kv_read(BDK_NAMESPACE, SEND_INTENT_NAMESPACE, &intent_id.to_string())
.await
.map_err(Error::from)?
.ok_or(Error::SendIntentNotFound(intent_id))?;
let existing: SendIntentRecord = serde_json::from_slice(&intent_bytes)?;
if !matches!(existing.state, SendIntentState::Failed { .. }) {
tx.rollback().await.map_err(Error::from)?;
return Err(Error::DuplicateQuoteId(intent.quote_id.clone()));
}
SendIntentRecord {
intent_id,
quote_id: intent.quote_id.clone(),
address: intent.address.clone(),
amount_sat: intent.amount_sat,
max_fee_amount_sat: intent.max_fee_amount_sat,
tier: intent.tier,
metadata: intent.metadata.clone(),
state: intent.state.clone(),
}
} else {
let reserved = tx
.kv_write_if_absent(
BDK_NAMESPACE,
SEND_INTENT_QUOTE_ID_NAMESPACE,
&intent.quote_id,
intent.intent_id.to_string().as_bytes(),
)
.await
.map_err(Error::from)?;
if !reserved {
tx.rollback().await.map_err(Error::from)?;
return Err(Error::DuplicateQuoteId(intent.quote_id.clone()));
}
intent.clone()
};
let serialized = serde_json::to_vec(&record)?;
tx.kv_write(
BDK_NAMESPACE,
SEND_INTENT_NAMESPACE,
&record.intent_id.to_string(),
&serialized,
)
.await
.map_err(Error::from)?;
if let SendIntentState::AwaitingConfirmation { outpoint, .. } = &record.state {
tx.kv_write(
BDK_NAMESPACE,
SEND_OUTPOINT_QUOTE_ID_NAMESPACE,
&outpoint_to_key(outpoint),
record.quote_id.as_bytes(),
)
.await
.map_err(Error::from)?;
}
tx.commit().await.map_err(Error::from)?;
Ok(record)
}
pub async fn get_send_intent(
&self,
intent_id: &Uuid,
) -> Result<Option<SendIntentRecord>, Error> {
self.get_record::<SendIntentRecord>(&intent_id.to_string())
.await
}
pub async fn update_send_intent(
&self,
intent_id: &Uuid,
new_state: &SendIntentState,
) -> Result<(), Error> {
let Some(mut intent) = self.get_send_intent(intent_id).await? else {
return Err(Error::SendIntentNotFound(*intent_id));
};
let previous_outpoint = match &intent.state {
SendIntentState::AwaitingConfirmation { outpoint, .. } => Some(outpoint.clone()),
_ => None,
};
let new_outpoint = match new_state {
SendIntentState::AwaitingConfirmation { outpoint, .. } => Some(outpoint.clone()),
_ => None,
};
intent.state = new_state.clone();
let serialized = serde_json::to_vec(&intent)?;
let mut tx = self
.kv_store
.begin_transaction()
.await
.map_err(Error::from)?;
tx.kv_write(
BDK_NAMESPACE,
SEND_INTENT_NAMESPACE,
&intent_id.to_string(),
&serialized,
)
.await
.map_err(Error::from)?;
if let Some(outpoint) =
previous_outpoint.filter(|outpoint| Some(outpoint) != new_outpoint.as_ref())
{
tx.kv_remove(
BDK_NAMESPACE,
SEND_OUTPOINT_QUOTE_ID_NAMESPACE,
&outpoint_to_key(&outpoint),
)
.await
.map_err(Error::from)?;
}
if let Some(outpoint) = new_outpoint {
tx.kv_write(
BDK_NAMESPACE,
SEND_OUTPOINT_QUOTE_ID_NAMESPACE,
&outpoint_to_key(&outpoint),
intent.quote_id.as_bytes(),
)
.await
.map_err(Error::from)?;
}
tx.commit().await.map_err(Error::from)
}
pub async fn delete_send_intent(&self, intent_id: &Uuid) -> Result<(), Error> {
let Some(intent) = self.get_send_intent(intent_id).await? else {
return Ok(());
};
let mut tx = self
.kv_store
.begin_transaction()
.await
.map_err(Error::from)?;
tx.kv_remove(BDK_NAMESPACE, SEND_INTENT_NAMESPACE, &intent_id.to_string())
.await
.map_err(Error::from)?;
tx.kv_remove(
BDK_NAMESPACE,
SEND_INTENT_QUOTE_ID_NAMESPACE,
&intent.quote_id,
)
.await
.map_err(Error::from)?;
if let SendIntentState::AwaitingConfirmation { outpoint, .. } = intent.state {
tx.kv_remove(
BDK_NAMESPACE,
SEND_OUTPOINT_QUOTE_ID_NAMESPACE,
&outpoint_to_key(&outpoint),
)
.await
.map_err(Error::from)?;
}
tx.commit().await.map_err(Error::from)?;
Ok(())
}
pub async fn get_all_send_intents(&self) -> Result<Vec<SendIntentRecord>, Error> {
self.list_records::<SendIntentRecord>().await
}
pub async fn get_pending_send_intents(&self) -> Result<Vec<SendIntentRecord>, Error> {
let all = self.get_all_send_intents().await?;
Ok(all
.into_iter()
.filter(|i| matches!(i.state, SendIntentState::Pending { .. }))
.collect())
}
pub async fn add_failed_send_attempt(
&self,
record: &FailedSendAttemptRecord,
) -> Result<(), Error> {
self.put_record(record).await
}
pub async fn get_failed_send_attempts_by_quote_id(
&self,
quote_id: &str,
) -> Result<Vec<FailedSendAttemptRecord>, Error> {
let all = self.list_records::<FailedSendAttemptRecord>().await?;
Ok(all
.into_iter()
.filter(|record| record.quote_id == quote_id)
.collect())
}
pub async fn store_send_batch(&self, batch: &SendBatchRecord) -> Result<(), Error> {
self.put_record(batch).await
}
pub async fn get_send_batch(&self, batch_id: &Uuid) -> Result<Option<SendBatchRecord>, Error> {
self.get_record::<SendBatchRecord>(&batch_id.to_string())
.await
}
pub async fn update_send_batch(
&self,
batch_id: &Uuid,
new_state: &SendBatchState,
) -> Result<(), Error> {
let key = batch_id.to_string();
if self.get_send_batch(batch_id).await?.is_none() {
return Err(Error::SendBatchNotFound(*batch_id));
}
self.update_record_state::<SendBatchRecord, SendBatchState>(&key, new_state)
.await
}
pub async fn delete_send_batch(&self, batch_id: &Uuid) -> Result<(), Error> {
self.delete_record::<SendBatchRecord>(&batch_id.to_string())
.await
}
pub async fn get_all_send_batches(&self) -> Result<Vec<SendBatchRecord>, Error> {
self.list_records::<SendBatchRecord>().await
}
pub async fn get_finalized_intent(
&self,
intent_id: &Uuid,
) -> Result<Option<FinalizedSendIntentRecord>, Error> {
self.get_record::<FinalizedSendIntentRecord>(&intent_id.to_string())
.await
}
pub async fn get_all_finalized_send_intents(
&self,
) -> Result<Vec<FinalizedSendIntentRecord>, Error> {
self.list_records::<FinalizedSendIntentRecord>().await
}
pub async fn get_quote_id_by_send_outpoint(
&self,
outpoint: &str,
) -> Result<Option<String>, Error> {
let quote_id_bytes = self
.kv_store
.kv_read(
BDK_NAMESPACE,
SEND_OUTPOINT_QUOTE_ID_NAMESPACE,
&outpoint_to_key(outpoint),
)
.await
.map_err(Error::from)?;
match quote_id_bytes {
Some(quote_id_bytes) => String::from_utf8(quote_id_bytes)
.map(Some)
.map_err(|e| Error::Wallet(format!("Invalid quote-id index entry: {}", e))),
None => Ok(None),
}
}
pub(crate) async fn ensure_send_outpoint_quote_id_index(&self) -> Result<(), Error> {
if self
.kv_store
.kv_read(
BDK_NAMESPACE,
STORAGE_MIGRATION_NAMESPACE,
SEND_OUTPOINT_QUOTE_ID_BACKFILL_KEY,
)
.await
.map_err(Error::from)?
.is_some()
{
return Ok(());
}
let (send_intents, finalized_send_intents) = tokio::try_join!(
self.get_all_send_intents(),
self.get_all_finalized_send_intents(),
)?;
let mut tx = self
.kv_store
.begin_transaction()
.await
.map_err(Error::from)?;
for intent in send_intents {
if let SendIntentState::AwaitingConfirmation { outpoint, .. } = intent.state {
tx.kv_write(
BDK_NAMESPACE,
SEND_OUTPOINT_QUOTE_ID_NAMESPACE,
&outpoint_to_key(&outpoint),
intent.quote_id.as_bytes(),
)
.await
.map_err(Error::from)?;
}
}
for intent in finalized_send_intents {
tx.kv_write(
BDK_NAMESPACE,
SEND_OUTPOINT_QUOTE_ID_NAMESPACE,
&outpoint_to_key(&intent.outpoint),
intent.quote_id.as_bytes(),
)
.await
.map_err(Error::from)?;
}
tx.kv_write(
BDK_NAMESPACE,
STORAGE_MIGRATION_NAMESPACE,
SEND_OUTPOINT_QUOTE_ID_BACKFILL_KEY,
b"complete",
)
.await
.map_err(Error::from)?;
tx.commit().await.map_err(Error::from)
}
pub async fn get_finalized_intent_by_quote_id(
&self,
quote_id: &str,
) -> Result<Option<FinalizedSendIntentRecord>, Error> {
let Some(intent_id_bytes) = self
.kv_store
.kv_read(
BDK_NAMESPACE,
FINALIZED_SEND_INTENT_QUOTE_ID_NAMESPACE,
quote_id,
)
.await
.map_err(Error::from)?
else {
return Ok(None);
};
let intent_id_str = std::str::from_utf8(&intent_id_bytes)
.map_err(|e| Error::Wallet(format!("Invalid intent-id index entry: {}", e)))?;
let intent_id = Uuid::from_str(intent_id_str)
.map_err(|e| Error::Wallet(format!("Invalid indexed intent id: {}", e)))?;
self.get_record::<FinalizedSendIntentRecord>(&intent_id.to_string())
.await
}
pub async fn get_send_intent_by_quote_id(
&self,
quote_id: &str,
) -> Result<Option<SendIntentRecord>, Error> {
let Some(intent_id_bytes) = self
.kv_store
.kv_read(BDK_NAMESPACE, SEND_INTENT_QUOTE_ID_NAMESPACE, quote_id)
.await
.map_err(Error::from)?
else {
return Ok(None);
};
let intent_id = std::str::from_utf8(&intent_id_bytes)
.map_err(|e| Error::Wallet(format!("Invalid quote-id index entry: {}", e)))?;
let intent_id = Uuid::from_str(intent_id)
.map_err(|e| Error::Wallet(format!("Invalid indexed intent id: {}", e)))?;
self.get_send_intent(&intent_id).await
}
pub async fn finalize_send_intent(
&self,
intent_id: &Uuid,
record: &FinalizedSendIntentRecord,
) -> Result<(), Error> {
let Some(intent) = self.get_send_intent(intent_id).await? else {
return Err(Error::SendIntentNotFound(*intent_id));
};
let serialized = serde_json::to_vec(record)?;
let mut tx = self
.kv_store
.begin_transaction()
.await
.map_err(Error::from)?;
tx.kv_write(
BDK_NAMESPACE,
FINALIZED_INTENT_NAMESPACE,
&record.intent_id.to_string(),
&serialized,
)
.await
.map_err(Error::from)?;
tx.kv_write(
BDK_NAMESPACE,
FINALIZED_SEND_INTENT_QUOTE_ID_NAMESPACE,
&intent.quote_id,
record.intent_id.to_string().as_bytes(),
)
.await
.map_err(Error::from)?;
tx.kv_write(
BDK_NAMESPACE,
SEND_OUTPOINT_QUOTE_ID_NAMESPACE,
&outpoint_to_key(&record.outpoint),
record.quote_id.as_bytes(),
)
.await
.map_err(Error::from)?;
tx.kv_remove(BDK_NAMESPACE, SEND_INTENT_NAMESPACE, &intent_id.to_string())
.await
.map_err(Error::from)?;
tx.kv_remove(
BDK_NAMESPACE,
SEND_INTENT_QUOTE_ID_NAMESPACE,
&intent.quote_id,
)
.await
.map_err(Error::from)?;
tx.commit().await.map_err(Error::from)?;
Ok(())
}
}