pub(crate) mod record;
pub(crate) mod state;
use uuid::Uuid;
use self::record::{ReceiveIntentRecord, ReceiveIntentState};
use self::state::Detected;
use crate::error::Error;
use crate::storage::{BdkStorage, FinalizedReceiveIntentRecord};
#[derive(Debug, Clone)]
pub(crate) struct ReceiveIntent<S> {
pub intent_id: Uuid,
pub state: S,
}
impl ReceiveIntent<Detected> {
pub async fn new(
storage: &BdkStorage,
address: String,
txid: String,
outpoint: String,
amount_sat: u64,
block_height: u32,
) -> Result<Option<Self>, Error> {
let intent_id = Uuid::new_v4();
let created_at = crate::util::unix_now();
let request = storage
.get_quote_id_by_receive_address(&address)
.await?
.ok_or_else(|| {
Error::Wallet(format!(
"No tracked receive address for address {}",
address
))
})?;
let quote_id = request;
let record = ReceiveIntentRecord {
intent_id,
quote_id: quote_id.clone(),
state: ReceiveIntentState::Detected {
address: address.clone(),
txid: txid.clone(),
outpoint: outpoint.clone(),
amount_sat,
block_height,
created_at,
},
};
let was_created = storage.create_receive_intent_if_absent(&record).await?;
if !was_created {
return Ok(None);
}
Ok(Some(Self {
intent_id,
state: Detected {
quote_id,
address,
txid,
outpoint,
amount_sat,
block_height,
},
}))
}
pub async fn finalize(self, storage: &BdkStorage) -> Result<(), Error> {
let tombstone = FinalizedReceiveIntentRecord {
intent_id: self.intent_id,
quote_id: self.state.quote_id.clone(),
address: self.state.address.clone(),
txid: self.state.txid.clone(),
outpoint: self.state.outpoint.clone(),
amount_sat: self.state.amount_sat,
finalized_at: crate::util::unix_now(),
};
storage
.finalize_receive_intent(&self.intent_id, &tombstone)
.await?;
Ok(())
}
}
pub(crate) fn from_record(record: &ReceiveIntentRecord) -> ReceiveIntentAny {
match &record.state {
ReceiveIntentState::Detected {
address,
txid,
outpoint,
amount_sat,
block_height,
..
} => ReceiveIntentAny::Detected(ReceiveIntent {
intent_id: record.intent_id,
state: Detected {
quote_id: record.quote_id.clone(),
address: address.clone(),
txid: txid.clone(),
outpoint: outpoint.clone(),
amount_sat: *amount_sat,
block_height: *block_height,
},
}),
}
}
pub(crate) enum ReceiveIntentAny {
Detected(ReceiveIntent<Detected>),
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use cdk_common::payment::{PaymentIdentifier, WaitPaymentResponse};
use cdk_common::{Amount, CurrencyUnit};
use super::*;
use crate::storage::BdkStorage;
async fn test_storage() -> BdkStorage {
let db = cdk_sqlite::mint::memory::empty()
.await
.expect("in-memory db");
BdkStorage::new(Arc::new(db))
}
#[tokio::test]
async fn test_detected_creation() {
let storage = test_storage().await;
let address = "bcrt1qaddr".to_string();
let quote_id = Uuid::new_v4().to_string();
storage
.track_receive_address(&address, "e_id)
.await
.expect("track address");
let intent = ReceiveIntent::new(
&storage,
address,
"txid_abc".to_string(),
"txid_abc:0".to_string(),
50_000,
100,
)
.await
.expect("create detected intent")
.expect("should not be duplicate");
assert_eq!(intent.state.address, "bcrt1qaddr");
assert_eq!(intent.state.quote_id, quote_id);
}
#[tokio::test]
async fn test_finalize_receive_intent_creates_tombstone() {
let storage = test_storage().await;
let address = "bcrt1qreceive".to_string();
let quote_id = Uuid::new_v4().to_string();
storage
.track_receive_address(&address, "e_id)
.await
.expect("track address");
let intent = ReceiveIntent::new(
&storage,
address.clone(),
"txid_receive".to_string(),
"txid_receive:0".to_string(),
75_000,
150,
)
.await
.expect("create detected intent")
.expect("should not be duplicate");
let intent_id = intent.intent_id;
let outpoint = intent.state.outpoint.clone();
let amount_sat = intent.state.amount_sat;
intent.finalize(&storage).await.expect("finalize");
let active = storage
.get_receive_intent(&intent_id)
.await
.expect("get active receive intent");
assert!(
active.is_none(),
"active receive intent should be deleted after finalization"
);
let tombstone = storage
.get_finalized_receive_intent(&intent_id)
.await
.expect("get finalized receive intent")
.expect("tombstone should exist");
assert_eq!(tombstone.address, address);
assert_eq!(tombstone.quote_id, quote_id);
assert_eq!(tombstone.outpoint, outpoint.clone());
assert_eq!(tombstone.amount_sat, amount_sat);
let response = WaitPaymentResponse {
payment_identifier: PaymentIdentifier::QuoteId(quote_id.parse().unwrap()),
payment_amount: Amount::new(tombstone.amount_sat, CurrencyUnit::Sat),
payment_id: tombstone.outpoint,
};
assert_eq!(
response.payment_identifier,
PaymentIdentifier::QuoteId(quote_id.parse().unwrap())
);
assert_eq!(
response.payment_amount,
Amount::new(75_000, CurrencyUnit::Sat)
);
assert_eq!(response.payment_id, outpoint);
}
#[tokio::test]
async fn test_multiple_finalized_receive_intents_can_share_quote_id() {
let storage = test_storage().await;
let quote_id = Uuid::new_v4().to_string();
let address_1 = "bcrt1qreceive1".to_string();
let address_2 = "bcrt1qreceive2".to_string();
storage
.track_receive_address(&address_1, "e_id)
.await
.expect("track first address");
storage
.track_receive_address(&address_2, "e_id)
.await
.expect("track second address");
let intent_1 = ReceiveIntent::new(
&storage,
address_1,
"txid_receive_1".to_string(),
"txid_receive_1:0".to_string(),
30_000,
150,
)
.await
.expect("create first detected intent")
.expect("first intent should not be duplicate");
let intent_2 = ReceiveIntent::new(
&storage,
address_2,
"txid_receive_2".to_string(),
"txid_receive_2:1".to_string(),
45_000,
151,
)
.await
.expect("create second detected intent")
.expect("second intent should not be duplicate");
intent_1.finalize(&storage).await.expect("finalize first");
intent_2.finalize(&storage).await.expect("finalize second");
let mut finalized = storage
.get_finalized_receive_intents_by_quote_id("e_id)
.await
.expect("get finalized receive intents by quote id");
finalized.sort_by_key(|record| record.amount_sat);
assert_eq!(finalized.len(), 2);
assert_eq!(finalized[0].quote_id, quote_id);
assert_eq!(finalized[0].amount_sat, 30_000);
assert_eq!(finalized[1].quote_id, quote_id);
assert_eq!(finalized[1].amount_sat, 45_000);
}
}