1use crate::platform::fetch_current_no_parameters::FetchCurrent;
2use crate::platform::types::epoch::Epoch;
3use crate::{Error, Sdk};
4use bip37_bloom_filter::{BloomFilter, BloomFilterData};
5use dapi_grpc::core::v0::{
6 get_block_request, transactions_with_proofs_request, transactions_with_proofs_response,
7 GetBlockRequest, GetTransactionRequest, GetTransactionResponse, TransactionsWithProofsRequest,
8 TransactionsWithProofsResponse,
9};
10use dpp::dashcore::consensus::Decodable;
11use dpp::dashcore::hashes::Hash;
12use dpp::dashcore::{
13 Address, Block, BlockHash, InstantLock, MerkleBlock, OutPoint, Transaction, Txid,
14};
15use dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof;
16use dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof;
17use dpp::prelude::AssetLockProof;
18
19use dapi_grpc::tonic::Code;
20use rs_dapi_client::transport::TransportError;
21use rs_dapi_client::{DapiClientError, DapiRequestExecutor, IntoInner, RequestSettings};
22use std::time::Duration;
23use tokio::time::{sleep, timeout};
24
25#[derive(Clone, Debug)]
29pub struct FetchedCoreTransaction {
30 pub transaction: Transaction,
32 pub height: u32,
34 pub is_chain_locked: bool,
36 pub is_instant_locked: bool,
40}
41
42#[derive(Clone, Debug)]
48#[non_exhaustive]
49pub struct CoreTransactionPlacement {
50 pub transaction: Transaction,
52 pub height: u32,
54 pub block_hash: Option<BlockHash>,
57 pub is_chain_locked: bool,
59 pub is_instant_locked: bool,
61}
62
63fn error_is_not_found(err: &Error) -> bool {
67 match err {
68 Error::DapiClientError(DapiClientError::Transport(TransportError::Grpc(status))) => {
69 status.code() == Code::NotFound
70 }
71 Error::NoAvailableAddressesToRetry(inner) => error_is_not_found(inner),
72 _ => false,
73 }
74}
75
76fn block_hash_from_display_bytes(bytes: &[u8]) -> Option<BlockHash> {
80 let mut hash: [u8; 32] = bytes.try_into().ok()?;
81 hash.reverse();
82 Some(BlockHash::from_byte_array(hash))
83}
84
85fn decode_transaction(bytes: &[u8]) -> Result<Transaction, Error> {
87 Transaction::consensus_decode(&mut &bytes[..]).map_err(|e| Error::CoreError(e.into()))
88}
89
90fn decode_block(bytes: &[u8]) -> Result<Block, Error> {
92 Block::consensus_decode(&mut &bytes[..]).map_err(|e| Error::CoreError(e.into()))
93}
94
95impl Sdk {
96 pub async fn get_transaction(
105 &self,
106 txid: &str,
107 ) -> Result<Option<FetchedCoreTransaction>, Error> {
108 let Some(response) = self
109 .fetch_core_transaction(txid, RequestSettings::default())
110 .await?
111 else {
112 return Ok(None);
113 };
114
115 Ok(Some(FetchedCoreTransaction {
116 transaction: decode_transaction(&response.transaction)?,
117 height: response.height,
118 is_chain_locked: response.is_chain_locked,
119 is_instant_locked: response.is_instant_locked,
120 }))
121 }
122
123 pub async fn get_transaction_placement(
131 &self,
132 txid: &str,
133 settings: RequestSettings,
134 ) -> Result<Option<CoreTransactionPlacement>, Error> {
135 let Some(response) = self.fetch_core_transaction(txid, settings).await? else {
136 return Ok(None);
137 };
138
139 Ok(Some(CoreTransactionPlacement {
140 transaction: decode_transaction(&response.transaction)?,
141 height: response.height,
142 block_hash: block_hash_from_display_bytes(&response.block_hash),
143 is_chain_locked: response.is_chain_locked,
144 is_instant_locked: response.is_instant_locked,
145 }))
146 }
147
148 pub async fn get_block_by_hash(
156 &self,
157 hash: &BlockHash,
158 settings: RequestSettings,
159 ) -> Result<Option<Block>, Error> {
160 let response = match self
161 .execute(
162 GetBlockRequest {
163 block: Some(get_block_request::Block::Hash(hash.to_string())),
165 },
166 settings,
167 )
168 .await
169 .into_inner()
170 {
171 Ok(response) => response,
172 Err(e) => {
173 let err: Error = e.into();
174 return if error_is_not_found(&err) {
175 Ok(None)
176 } else {
177 Err(err)
178 };
179 }
180 };
181
182 if response.block.is_empty() {
183 return Ok(None);
184 }
185 decode_block(&response.block).map(Some)
186 }
187
188 async fn fetch_core_transaction(
191 &self,
192 txid: &str,
193 settings: RequestSettings,
194 ) -> Result<Option<GetTransactionResponse>, Error> {
195 let response = match self
196 .execute(
197 GetTransactionRequest {
198 id: txid.to_string(),
199 },
200 settings,
201 )
202 .await
203 .into_inner()
204 {
205 Ok(response) => response,
206 Err(e) => {
207 let err: Error = e.into();
208 return if error_is_not_found(&err) {
209 Ok(None)
210 } else {
211 Err(err)
212 };
213 }
214 };
215
216 if response.transaction.is_empty() {
217 return Ok(None);
218 }
219 Ok(Some(response))
220 }
221
222 pub async fn start_instant_send_lock_stream(
224 &self,
225 from_block_hash: Vec<u8>,
226 address: &Address,
227 ) -> Result<dapi_grpc::tonic::Streaming<TransactionsWithProofsResponse>, Error> {
228 let address_bytes = address.as_unchecked().payload_to_vec();
229
230 let bloom_filter = BloomFilter::builder(1, 0.001)
232 .expect("this FP rate allows up to 10000 items")
233 .add_element(&address_bytes)
234 .build();
235
236 let bloom_filter_proto = {
237 let BloomFilterData {
238 v_data,
239 n_hash_funcs,
240 n_tweak,
241 n_flags,
242 } = bloom_filter.into();
243 dapi_grpc::core::v0::BloomFilter {
244 v_data,
245 n_hash_funcs,
246 n_tweak,
247 n_flags,
248 }
249 };
250
251 let core_transactions_stream = TransactionsWithProofsRequest {
252 bloom_filter: Some(bloom_filter_proto),
253 count: 0, send_transaction_hashes: true,
255 from_block: Some(transactions_with_proofs_request::FromBlock::FromBlockHash(
256 from_block_hash,
257 )),
258 };
259 self.execute(core_transactions_stream, RequestSettings::default())
260 .await
261 .into_inner()
262 .map_err(|e| e.into())
263 }
264
265 pub async fn wait_for_asset_lock_proof_for_transaction(
267 &self,
268 mut stream: dapi_grpc::tonic::Streaming<TransactionsWithProofsResponse>,
269 transaction: &Transaction,
270 time_out: Option<Duration>,
271 ) -> Result<AssetLockProof, Error> {
272 let transaction_id = transaction.txid();
273
274 let _span = tracing::debug_span!(
275 "wait_for_asset_lock_proof_for_transaction",
276 transaction_id = transaction_id.to_string(),
277 )
278 .entered();
279
280 tracing::debug!("waiting for messages from stream");
281
282 let stream_processing = async {
284 loop {
285 let message = stream
287 .message()
288 .await
289 .map_err(|e| Error::Generic(format!("can't receive message: {e}")))?;
290
291 let Some(TransactionsWithProofsResponse { responses }) = message else {
292 return Err(Error::Generic("stream closed unexpectedly".to_string()));
293 };
294
295 match responses {
296 Some(
297 transactions_with_proofs_response::Responses::InstantSendLockMessages(
298 instant_send_lock_messages,
299 ),
300 ) => {
301 tracing::debug!(
302 "received {} instant lock message(s)",
303 instant_send_lock_messages.messages.len()
304 );
305
306 for instant_lock_bytes in instant_send_lock_messages.messages {
307 let instant_lock =
308 InstantLock::consensus_decode(&mut instant_lock_bytes.as_slice())
309 .map_err(|e| {
310 tracing::error!("invalid asset lock: {}", e);
311
312 Error::CoreError(e.into())
313 })?;
314
315 if instant_lock.txid == transaction_id {
316 let asset_lock_proof =
317 AssetLockProof::Instant(InstantAssetLockProof {
318 instant_lock,
319 transaction: transaction.clone(),
320 output_index: 0,
321 });
322
323 tracing::debug!(
324 ?asset_lock_proof,
325 "instant lock is matching to the broadcasted transaction, returning instant asset lock proof"
326 );
327
328 return Ok(asset_lock_proof);
329 } else {
330 tracing::debug!(
331 "instant lock is not matching, waiting for the next message"
332 );
333 }
334 }
335 }
336 Some(transactions_with_proofs_response::Responses::RawMerkleBlock(
337 raw_merkle_block,
338 )) => {
339 tracing::debug!("received merkle block");
340
341 let merkle_block =
342 MerkleBlock::consensus_decode(&mut raw_merkle_block.as_slice())
343 .map_err(|e| {
344 tracing::error!("can't decode merkle block: {}", e);
345
346 Error::CoreError(e.into())
347 })?;
348
349 let mut matches: Vec<Txid> = vec![];
350 let mut index: Vec<u32> = vec![];
351
352 merkle_block.extract_matches(&mut matches, &mut index)?;
353
354 if !matches.contains(&transaction_id) {
356 tracing::debug!(
357 "merkle block doesn't contain the transaction, waiting for the next message"
358 );
359
360 continue;
361 }
362
363 tracing::debug!(
364 "merkle block contains the transaction, obtaining core chain locked height"
365 );
366
367 let mut core_chain_locked_height;
372 loop {
373 let GetTransactionResponse {
374 height,
375 is_chain_locked,
376 ..
377 } = self
378 .execute(
379 GetTransactionRequest {
380 id: transaction_id.to_string(),
381 },
382 RequestSettings::default(),
383 )
384 .await .into_inner()?;
386
387 core_chain_locked_height = height;
388
389 if is_chain_locked {
390 break;
391 }
392
393 tracing::trace!("the transaction is on height {} but not chainlocked. try again in 1 sec", height);
394
395 sleep(Duration::from_secs(1)).await;
396 }
397
398 tracing::debug!(
399 "the transaction is chainlocked on height {}, waiting platform for reaching the same core height",
400 core_chain_locked_height
401 );
402
403 loop {
405 let (_epoch, metadata) =
406 Epoch::fetch_current_with_metadata(self).await?;
407
408 if metadata.core_chain_locked_height >= core_chain_locked_height {
409 break;
410 }
411
412 tracing::trace!(
413 "platform chain locked core height {} but we need {}. try again in 1 sec",
414 metadata.core_chain_locked_height,
415 core_chain_locked_height,
416 );
417
418 sleep(Duration::from_secs(1)).await;
419 }
420
421 let asset_lock_proof = AssetLockProof::Chain(ChainAssetLockProof {
422 core_chain_locked_height,
423 out_point: OutPoint {
424 txid: transaction.txid(),
425 vout: 0,
426 },
427 });
428
429 tracing::debug!(
430 ?asset_lock_proof,
431 "merkle block contains the broadcasted transaction, returning chain asset lock proof"
432 );
433
434 return Ok(asset_lock_proof);
435 }
436 Some(transactions_with_proofs_response::Responses::RawTransactions(_)) => {
437 tracing::trace!("received transaction(s), ignoring")
438 }
439 None => tracing::trace!(
440 "received empty response as a workaround for the bug in tonic, ignoring"
441 ),
442 }
443 }
444 };
445
446 match time_out {
448 Some(duration) => timeout(duration, stream_processing).await.map_err(|_| {
449 Error::TimeoutReached(duration, String::from("receiving asset lock proof"))
450 })?,
451 None => stream_processing.await,
452 }
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
462 fn block_hash_from_display_bytes_reverses_the_bytes() {
463 let mut display = [0u8; 32];
464 display[0] = 0xaa;
465 display[31] = 0x01;
466 let hash = block_hash_from_display_bytes(&display).expect("32 bytes");
467 let internal = hash.to_byte_array();
468 assert_eq!(internal[0], 0x01);
469 assert_eq!(internal[31], 0xaa);
470 }
471
472 #[test]
475 fn block_hash_from_display_bytes_rejects_wrong_lengths() {
476 assert!(block_hash_from_display_bytes(&[]).is_none());
477 assert!(block_hash_from_display_bytes(&[0u8; 31]).is_none());
478 assert!(block_hash_from_display_bytes(&[0u8; 33]).is_none());
479 }
480
481 const CORE_V24_BLOCK: &str = "00000020624025135b0936675af8abbe364e7d21c8e1e42eab3f2b6f6528d6ea875dad51e2664e33e00b3bf6b05a67603cddadf9daa4149e9130c7e5180e38426d20a695d5a6c76affff7f20010000000203000500010000000000000000000000000000000000000000000000000000000000000000ffffffff016affffffff036de1f505000000001976a91456208a8adff9433262bffee3c4894d2a160c21bd88ac209db40600000000016a26072d0b000000001976a91456208a8adff9433262bffee3c4894d2a160c21bd88ac00000000cf04002a3b0000db318d0bad75d80a6e14088f71657af817056edee711360d25c729905f33ed502692b21d078be4bc3103156612ba6bdcf387efdd6320a50a97985f5cdce4ead8008df9d40906f1c7c36c545b06daec45a84ffd43b5caf4045c9c9a39a146a7771de6cbdd11f5d16b22e9e358f5c483e35b12a5f3cab190026c5445dec34e632c36a0ba36a1f87bb411d7d292c5fd55cd36128877c707747286e40abec1aee7e4812eb72a56c407000000000000000000000000000000000000000000000000000000000000000000000300010001d02a36181466f1f3ec0f61a73114bdf31fa7f137fa880441d7e5e7b672493c04010000006a473044022004f9bb946ee7fab7d823a812cfb27b576f2c0cda4e597a8c92be26d8af33f155022034d05190a9b7a9aaae89fc89148639af1eb7f0601d9c959232b7a159cb2ca991012102c861a156ca6c6abecdeea95eab3ec7c93c6a37d63a5a6f62368ac94a424af15ffeffffff0200e87648170000001976a914fc52fb4219926f353f5475da7885b213c384a24188ac4d4fbfb88b0000001976a914c39aa109a7f20cefa69079374c7324e01998a12488ac00000000d10300000000000000000000000000000000000000000000000000000000000000000000000000000000000100c1dad84fdab5738de910b64d3a9ae7baf6afbd69806b8889b4ed6ccecefe7861fc3d69b7598f6ab9621fe2568403f94a1c5df398891433cdf301c67fda82f80bfe7f321465e088b8b1360c62946afcbea403cf927d5bfe230000011976a91431389f57b2f32565dcf713d2b2420914885bedcd88ac1027000000000000000000000000004f8f8fe9fd4ad5565cd6c4887bc54e2910a340bb7583c0d8d6a92d52b324095700";
485
486 #[test]
489 fn decode_block_reads_a_core_v24_block() {
490 let bytes = hex::decode(CORE_V24_BLOCK).expect("vector is hex");
491 let block = decode_block(&bytes).expect("a Core v24 block decodes");
492 assert!(block.check_merkle_root());
493 assert_eq!(block.txdata.len(), 2);
494 }
495}