1use chrono::NaiveDate;
2use futures::{future, StreamExt};
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5use trustchain_core::utils::get_did_from_suffix;
6
7use crate::{
8 utils::{
9 block_height_range_on_date, locate_transaction, query_mongodb_on_interval, transaction,
10 },
11 TrustchainBitcoinError, TrustchainMongodbError, ION_TEST_METHOD, MONGO_FILTER_DID_SUFFIX,
12 MONGO_FILTER_TXN_TIME,
13};
14
15#[derive(Error, Debug)]
17pub enum TrustchainRootError {
18 #[error("Bitcoin RPC error while processing root event date.")]
20 BitcoinRpcError(TrustchainBitcoinError),
21 #[error("Mongo DB error while processing root event date.")]
23 MongoDbError(TrustchainMongodbError),
24 #[error("No unique root DID on date: {0}")]
26 NoUniqueRootEvent(NaiveDate),
27 #[error("Invalid date: {0}-{1}-{2}")]
29 InvalidDate(i32, u32, u32),
30 #[error("Failed to parse block height: {0}")]
32 FailedToParseBlockHeight(String),
33}
34
35impl From<TrustchainBitcoinError> for TrustchainRootError {
36 fn from(err: TrustchainBitcoinError) -> Self {
37 TrustchainRootError::BitcoinRpcError(err)
38 }
39}
40
41impl From<TrustchainMongodbError> for TrustchainRootError {
42 fn from(err: TrustchainMongodbError) -> Self {
43 TrustchainRootError::MongoDbError(err)
44 }
45}
46
47#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, PartialOrd, Eq, Ord)]
49#[serde(rename_all = "camelCase")]
50pub struct RootCandidate {
51 pub did: String,
52 pub txid: String,
53 pub block_height: u64,
54}
55
56pub async fn root_did_candidates(
61 date: NaiveDate,
62) -> Result<Vec<RootCandidate>, TrustchainRootError> {
63 let block_height_range = block_height_range_on_date(date, None, None)?;
64 let cursor =
65 query_mongodb_on_interval(block_height_range.0 as u32, block_height_range.1 as u32).await?;
66
67 let rpc_client = &crate::utils::rpc_client();
84 let vec = cursor
85 .filter(|x| future::ready(x.is_ok()))
86 .map(|x| x.unwrap())
87 .filter_map(|doc| async move {
88 if doc.get_str(MONGO_FILTER_DID_SUFFIX).is_err() {
89 return None;
90 }
91 let did_suffix = doc.get_str(MONGO_FILTER_DID_SUFFIX).unwrap();
92 let did = get_did_from_suffix(did_suffix, ION_TEST_METHOD);
94 let tx_locator = locate_transaction(&did, rpc_client).await;
95 if tx_locator.is_err() {
96 return None;
97 }
98 let (block_hash, tx_index) = tx_locator.unwrap();
99 let tx = transaction(&block_hash, tx_index, Some(rpc_client));
100 if tx.is_err() {
101 return None;
102 }
103 let txid = tx.unwrap().txid().to_string();
104
105 let block_height = doc
106 .get_i32(MONGO_FILTER_TXN_TIME)
107 .unwrap()
108 .try_into()
109 .unwrap();
110 Some(RootCandidate {
111 did,
112 txid,
113 block_height,
114 })
115 })
116 .collect::<Vec<RootCandidate>>()
117 .await;
118 Ok(vec)
119}
120
121#[cfg(test)]
122mod tests {
123 use itertools::Itertools;
124
125 use super::*;
126
127 #[tokio::test]
128 #[ignore = "Integration test requires Bitcoin & MongoDB"]
129 async fn test_root_did_candidates() {
130 let date = NaiveDate::from_ymd_opt(2022, 10, 20).unwrap();
131 let result = root_did_candidates(date)
132 .await
133 .unwrap()
134 .into_iter()
135 .sorted()
136 .collect_vec();
137
138 assert_eq!(result.len(), 38);
143
144 assert_eq!(
145 result[0].did,
146 "did:ion:test:EiA6m4-V4fW_l1xEu3jH9xvXt1JyynmO7I_rkBpFulEAuQ"
147 );
148 assert_eq!(
149 result[0].txid,
150 "b698c0919a91a161bc141cd395788296edb85d19415a6d29a13a220a8f2249e0"
151 );
152 assert_eq!(result[0].block_height, 2377410);
153
154 assert_eq!(
156 result[26].did,
157 "did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg"
158 );
159 assert_eq!(
160 result[26].txid,
161 "9dc43cca950d923442445340c2e30bc57761a62ef3eaf2417ec5c75784ea9c2c"
162 );
163 assert_eq!(result[26].block_height, 2377445);
164
165 assert_eq!(
166 result[37].did,
167 "did:ion:test:EiDz_zvUa2FUIgLUvBia9wUJakhrrW889nDdGlr1-RTAWw"
168 );
169 assert_eq!(
170 result[37].txid,
171 "c369dd566a0dd5c2f381c1ab9c8e96b4f6b4fd323f5c1ed68dbb2a1bfb9cb48f"
172 );
173 assert_eq!(result[37].block_height, 2377416);
174 }
175}