Skip to main content

dash_sdk/core/
transaction.rs

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    transactions_with_proofs_request, transactions_with_proofs_response, GetTransactionRequest,
7    GetTransactionResponse, TransactionsWithProofsRequest, TransactionsWithProofsResponse,
8};
9use dpp::dashcore::consensus::Decodable;
10use dpp::dashcore::{Address, InstantLock, MerkleBlock, OutPoint, Transaction, Txid};
11use dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof;
12use dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof;
13use dpp::prelude::AssetLockProof;
14
15use dapi_grpc::tonic::Code;
16use rs_dapi_client::transport::TransportError;
17use rs_dapi_client::{DapiClientError, DapiRequestExecutor, IntoInner, RequestSettings};
18use std::time::Duration;
19use tokio::time::{sleep, timeout};
20
21/// A Core transaction fetched by id, plus the finality metadata needed to
22/// reconstruct an asset-lock proof from it (an InstantSend proof when the
23/// InstantLock is known, otherwise a ChainLock proof once chain-locked).
24#[derive(Clone, Debug)]
25pub struct FetchedCoreTransaction {
26    /// The decoded transaction.
27    pub transaction: Transaction,
28    /// Height of the block the transaction was mined in (0 if unconfirmed).
29    pub height: u32,
30    /// Whether the transaction's block is ChainLocked.
31    pub is_chain_locked: bool,
32    /// Whether the transaction is InstantSend-locked. Deliberately surfaced but
33    /// not required by the invitation claim: the proof carries the islock from
34    /// the link, and consensus re-verifies it — this flag is informational.
35    pub is_instant_locked: bool,
36}
37
38/// Whether an SDK error is a gRPC `NOT_FOUND` (the requested tx is unknown to
39/// the node), as opposed to a transient/transport failure. Used to distinguish
40/// "retry with a reversed txid" from "surface the error".
41fn error_is_not_found(err: &Error) -> bool {
42    match err {
43        Error::DapiClientError(DapiClientError::Transport(TransportError::Grpc(status))) => {
44            status.code() == Code::NotFound
45        }
46        Error::NoAvailableAddressesToRetry(inner) => error_is_not_found(inner),
47        _ => false,
48    }
49}
50
51impl Sdk {
52    /// Fetch a Core transaction by its id via DAPI `getTransaction`.
53    ///
54    /// `txid` is the transaction id as a hex string (big-endian display form).
55    /// Returns `Ok(Some(..))` with the decoded transaction plus its
56    /// confirmation/lock metadata; `Ok(None)` when the node does not know the tx
57    /// (empty response or gRPC `NOT_FOUND`) so the caller can retry with the id
58    /// byte-reversed; and `Err` for a transient/transport failure that must not
59    /// be masked by a doomed reversed-id retry.
60    pub async fn get_transaction(
61        &self,
62        txid: &str,
63    ) -> Result<Option<FetchedCoreTransaction>, Error> {
64        let response = match self
65            .execute(
66                GetTransactionRequest {
67                    id: txid.to_string(),
68                },
69                RequestSettings::default(),
70            )
71            .await
72            .into_inner()
73        {
74            Ok(response) => response,
75            Err(e) => {
76                let err: Error = e.into();
77                return if error_is_not_found(&err) {
78                    Ok(None)
79                } else {
80                    Err(err)
81                };
82            }
83        };
84
85        let GetTransactionResponse {
86            transaction,
87            height,
88            is_chain_locked,
89            is_instant_locked,
90            ..
91        } = response;
92
93        if transaction.is_empty() {
94            return Ok(None);
95        }
96
97        let transaction = Transaction::consensus_decode(&mut transaction.as_slice())
98            .map_err(|e| Error::CoreError(e.into()))?;
99
100        Ok(Some(FetchedCoreTransaction {
101            transaction,
102            height,
103            is_chain_locked,
104            is_instant_locked,
105        }))
106    }
107
108    /// Starts the stream to listen for instant send lock messages
109    pub async fn start_instant_send_lock_stream(
110        &self,
111        from_block_hash: Vec<u8>,
112        address: &Address,
113    ) -> Result<dapi_grpc::tonic::Streaming<TransactionsWithProofsResponse>, Error> {
114        let address_bytes = address.as_unchecked().payload_to_vec();
115
116        // create the bloom filter
117        let bloom_filter = BloomFilter::builder(1, 0.001)
118            .expect("this FP rate allows up to 10000 items")
119            .add_element(&address_bytes)
120            .build();
121
122        let bloom_filter_proto = {
123            let BloomFilterData {
124                v_data,
125                n_hash_funcs,
126                n_tweak,
127                n_flags,
128            } = bloom_filter.into();
129            dapi_grpc::core::v0::BloomFilter {
130                v_data,
131                n_hash_funcs,
132                n_tweak,
133                n_flags,
134            }
135        };
136
137        let core_transactions_stream = TransactionsWithProofsRequest {
138            bloom_filter: Some(bloom_filter_proto),
139            count: 0, // Subscribing to new transactions as well
140            send_transaction_hashes: true,
141            from_block: Some(transactions_with_proofs_request::FromBlock::FromBlockHash(
142                from_block_hash,
143            )),
144        };
145        self.execute(core_transactions_stream, RequestSettings::default())
146            .await
147            .into_inner()
148            .map_err(|e| e.into())
149    }
150
151    /// Waits for a response for the asset lock proof
152    pub async fn wait_for_asset_lock_proof_for_transaction(
153        &self,
154        mut stream: dapi_grpc::tonic::Streaming<TransactionsWithProofsResponse>,
155        transaction: &Transaction,
156        time_out: Option<Duration>,
157    ) -> Result<AssetLockProof, Error> {
158        let transaction_id = transaction.txid();
159
160        let _span = tracing::debug_span!(
161            "wait_for_asset_lock_proof_for_transaction",
162            transaction_id = transaction_id.to_string(),
163        )
164        .entered();
165
166        tracing::debug!("waiting for messages from stream");
167
168        // Define an inner async block to handle the stream processing.
169        let stream_processing = async {
170            loop {
171                // TODO: We should retry if Err is returned
172                let message = stream
173                    .message()
174                    .await
175                    .map_err(|e| Error::Generic(format!("can't receive message: {e}")))?;
176
177                let Some(TransactionsWithProofsResponse { responses }) = message else {
178                    return Err(Error::Generic("stream closed unexpectedly".to_string()));
179                };
180
181                match responses {
182                    Some(
183                        transactions_with_proofs_response::Responses::InstantSendLockMessages(
184                            instant_send_lock_messages,
185                        ),
186                    ) => {
187                        tracing::debug!(
188                            "received {} instant lock message(s)",
189                            instant_send_lock_messages.messages.len()
190                        );
191
192                        for instant_lock_bytes in instant_send_lock_messages.messages {
193                            let instant_lock =
194                                InstantLock::consensus_decode(&mut instant_lock_bytes.as_slice())
195                                    .map_err(|e| {
196                                    tracing::error!("invalid asset lock: {}", e);
197
198                                    Error::CoreError(e.into())
199                                })?;
200
201                            if instant_lock.txid == transaction_id {
202                                let asset_lock_proof =
203                                    AssetLockProof::Instant(InstantAssetLockProof {
204                                        instant_lock,
205                                        transaction: transaction.clone(),
206                                        output_index: 0,
207                                    });
208
209                                tracing::debug!(
210                                    ?asset_lock_proof,
211                                    "instant lock is matching to the broadcasted transaction, returning instant asset lock proof"
212                                );
213
214                                return Ok(asset_lock_proof);
215                            } else {
216                                tracing::debug!(
217                                    "instant lock is not matching, waiting for the next message"
218                                );
219                            }
220                        }
221                    }
222                    Some(transactions_with_proofs_response::Responses::RawMerkleBlock(
223                        raw_merkle_block,
224                    )) => {
225                        tracing::debug!("received merkle block");
226
227                        let merkle_block =
228                            MerkleBlock::consensus_decode(&mut raw_merkle_block.as_slice())
229                                .map_err(|e| {
230                                    tracing::error!("can't decode merkle block: {}", e);
231
232                                    Error::CoreError(e.into())
233                                })?;
234
235                        let mut matches: Vec<Txid> = vec![];
236                        let mut index: Vec<u32> = vec![];
237
238                        merkle_block.extract_matches(&mut matches, &mut index)?;
239
240                        // Continue receiving messages until we find the transaction
241                        if !matches.contains(&transaction_id) {
242                            tracing::debug!(
243                                "merkle block doesn't contain the transaction, waiting for the next message"
244                            );
245
246                            continue;
247                        }
248
249                        tracing::debug!(
250                            "merkle block contains the transaction, obtaining core chain locked height"
251                        );
252
253                        // TODO: This a temporary implementation until we have headers stream running in background
254                        //  so we can always get actual height and chain locks
255
256                        // Wait until the block is chainlocked
257                        let mut core_chain_locked_height;
258                        loop {
259                            let GetTransactionResponse {
260                                height,
261                                is_chain_locked,
262                                ..
263                            } = self
264                                .execute(
265                                    GetTransactionRequest {
266                                        id: transaction_id.to_string(),
267                                    },
268                                    RequestSettings::default(),
269                                )
270                                .await // TODO: We need better way to handle execution errors
271                                .into_inner()?;
272
273                            core_chain_locked_height = height;
274
275                            if is_chain_locked {
276                                break;
277                            }
278
279                            tracing::trace!("the transaction is on height {} but not chainlocked. try again in 1 sec", height);
280
281                            sleep(Duration::from_secs(1)).await;
282                        }
283
284                        tracing::debug!(
285                            "the transaction is chainlocked on height {}, waiting platform for reaching the same core height",
286                            core_chain_locked_height
287                        );
288
289                        // Wait until platform chain is on the block's chain locked height
290                        loop {
291                            let (_epoch, metadata) =
292                                Epoch::fetch_current_with_metadata(self).await?;
293
294                            if metadata.core_chain_locked_height >= core_chain_locked_height {
295                                break;
296                            }
297
298                            tracing::trace!(
299                                "platform chain locked core height {} but we need {}. try again in 1 sec",
300                                metadata.core_chain_locked_height,
301                                core_chain_locked_height,
302                            );
303
304                            sleep(Duration::from_secs(1)).await;
305                        }
306
307                        let asset_lock_proof = AssetLockProof::Chain(ChainAssetLockProof {
308                            core_chain_locked_height,
309                            out_point: OutPoint {
310                                txid: transaction.txid(),
311                                vout: 0,
312                            },
313                        });
314
315                        tracing::debug!(
316                                ?asset_lock_proof,
317                                "merkle block contains the broadcasted transaction, returning chain asset lock proof"
318                            );
319
320                        return Ok(asset_lock_proof);
321                    }
322                    Some(transactions_with_proofs_response::Responses::RawTransactions(_)) => {
323                        tracing::trace!("received transaction(s), ignoring")
324                    }
325                    None => tracing::trace!(
326                        "received empty response as a workaround for the bug in tonic, ignoring"
327                    ),
328                }
329            }
330        };
331
332        // Apply the timeout if `time_out_ms` is Some, otherwise just await the processing.
333        match time_out {
334            Some(duration) => timeout(duration, stream_processing).await.map_err(|_| {
335                Error::TimeoutReached(duration, String::from("receiving asset lock proof"))
336            })?,
337            None => stream_processing.await,
338        }
339    }
340}