• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tari-project / tari / 29030017546

09 Jul 2026 03:33PM UTC coverage: 61.821% (-0.1%) from 61.952%
29030017546

push

github

web-flow
chore: fix clippy (#7920)

Description
---
fixes clippy after rust upgrade

0 of 7 new or added lines in 1 file covered. (0.0%)

192 existing lines in 14 files now uncovered.

72410 of 117129 relevant lines covered (61.82%)

223278.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.1
/base_layer/core/src/base_node/rpc/query_service.rs
1
// Copyright 2025 The Tari Project
2
// SPDX-License-Identifier: BSD-3-Clause
3

4
use std::cmp;
5

6
use log::trace;
7
use serde_valid::{Validate, validation};
8
use tari_common_types::{
9
    types,
10
    types::{FixedHash, FixedHashSizeError},
11
};
12
use tari_transaction_components::{
13
    rpc::{
14
        models,
15
        models::{
16
            BlockUtxoInfo,
17
            GenerateKernelMerkleProofResponse,
18
            GetUtxosByBlockRequest,
19
            GetUtxosByBlockResponse,
20
            MinimalUtxoSyncInfo,
21
            SyncUtxosByBlockRequest,
22
            SyncUtxosByBlockResponseV0,
23
            SyncUtxosByBlockResponseV1,
24
            TipInfoResponse,
25
            TxLocation,
26
            TxQueryResponse,
27
        },
28
    },
29
    transaction_components::TransactionOutput,
30
};
31
use tari_utilities::{ByteArray, ByteArrayError, hex::Hex};
32
use thiserror::Error;
33

34
use crate::{
35
    base_node::{StateMachineHandle, rpc::BaseNodeWalletQueryService, state_machine_service::states::StateInfo},
36
    chain_storage::{BlockchainBackend, ChainStorageError, async_db::AsyncBlockchainDb},
37
    mempool::{MempoolServiceError, TxStorageResponse, service::MempoolHandle},
38
};
39

40
const LOG_TARGET: &str = "c::bn::rpc::query_service";
41
const SYNC_UTXOS_SPEND_TIP_SAFETY_LIMIT: u64 = 1000;
42
const WALLET_MAX_BLOCKS_PER_REQUEST: u64 = 100;
43
const MAX_UTXO_CHUNK_SIZE: usize = 2000;
44

45
#[derive(Debug, Error)]
46
pub enum Error {
47
    #[error("Failed to get chain metadata: {0}")]
48
    FailedToGetChainMetadata(#[from] ChainStorageError),
49
    #[error("Header not found at height: {height}")]
50
    HeaderNotFound { height: u64 },
51
    #[error("Signature conversion error: {0}")]
52
    SignatureConversion(ByteArrayError),
53
    #[error("Mempool service error: {0}")]
54
    MempoolService(#[from] MempoolServiceError),
55
    #[error("Serde validation error: {0}")]
56
    SerdeValidation(#[from] validation::Errors),
57
    #[error("Hash conversion error: {0}")]
58
    HashConversion(#[from] FixedHashSizeError),
59
    #[error("Start header hash not found")]
60
    StartHeaderHashNotFound,
61
    #[error("End header hash not found")]
62
    EndHeaderHashNotFound,
63
    #[error("Header hash not found")]
64
    HeaderHashNotFound,
65
    #[error("Start header height {start_height} cannot be greater than the end header height {end_height}")]
66
    HeaderHeightMismatch { start_height: u64, end_height: u64 },
67
    #[error("Output not found")]
68
    OutputNotFound,
69
    #[error("A general error occurred: {0}")]
70
    General(anyhow::Error),
71
}
72

73
impl Error {
74
    fn general(err: impl Into<anyhow::Error>) -> Self {
×
75
        Error::General(err.into())
×
76
    }
×
77
}
78

79
pub struct Service<B> {
80
    db: AsyncBlockchainDb<B>,
81
    state_machine: StateMachineHandle,
82
    mempool: MempoolHandle,
83
    max_utxo_chunk_size: usize,
84
}
85

86
impl<B: BlockchainBackend + 'static> Service<B> {
87
    pub fn new(db: AsyncBlockchainDb<B>, state_machine: StateMachineHandle, mempool: MempoolHandle) -> Self {
4✔
88
        Self {
4✔
89
            db,
4✔
90
            state_machine,
4✔
91
            mempool,
4✔
92
            max_utxo_chunk_size: MAX_UTXO_CHUNK_SIZE,
4✔
93
        }
4✔
94
    }
4✔
95

96
    fn state_machine(&self) -> StateMachineHandle {
×
97
        self.state_machine.clone()
×
98
    }
×
99

100
    fn db(&self) -> &AsyncBlockchainDb<B> {
13✔
101
        &self.db
13✔
102
    }
13✔
103

104
    fn mempool(&self) -> MempoolHandle {
×
105
        self.mempool.clone()
×
106
    }
×
107

108
    async fn fetch_kernel(&self, signature: types::CompressedSignature) -> Result<TxQueryResponse, Error> {
×
109
        let db = self.db();
×
110

111
        match db.fetch_kernel_by_excess_sig(signature.clone()).await? {
×
112
            None => (),
×
113
            Some((_, block_hash)) => match db.fetch_header_by_block_hash(block_hash).await? {
×
114
                None => (),
×
115
                Some(header) => {
×
116
                    let response = TxQueryResponse {
×
117
                        location: TxLocation::Mined,
×
118
                        mined_header_hash: Some(block_hash.to_vec()),
×
119
                        mined_height: Some(header.height),
×
120
                        mined_timestamp: Some(header.timestamp.as_u64()),
×
121
                    };
×
122
                    return Ok(response);
×
123
                },
124
            },
125
        };
126

127
        // If not in a block then check the mempool
128
        let mut mempool = self.mempool();
×
129
        let mempool_response = match mempool.get_tx_state_by_excess_sig(signature.clone()).await? {
×
130
            TxStorageResponse::UnconfirmedPool => TxQueryResponse {
×
131
                location: TxLocation::InMempool,
×
132
                mined_header_hash: None,
×
133
                mined_height: None,
×
134
                mined_timestamp: None,
×
135
            },
×
136
            TxStorageResponse::ReorgPool |
137
            TxStorageResponse::NotStoredOrphan |
138
            TxStorageResponse::NotStoredTimeLocked |
139
            TxStorageResponse::NotStoredAlreadySpent |
140
            TxStorageResponse::NotStoredConsensus(_) |
141
            TxStorageResponse::NotStored(_) |
142
            TxStorageResponse::NotStoredFeeTooLow |
143
            TxStorageResponse::NotStoredAlreadyMined => TxQueryResponse {
×
144
                location: TxLocation::NotStored,
×
145
                mined_timestamp: None,
×
146
                mined_height: None,
×
147
                mined_header_hash: None,
×
148
            },
×
149
        };
150

151
        Ok(mempool_response)
×
152
    }
×
153

154
    async fn fetch_utxos_by_block(&self, request: GetUtxosByBlockRequest) -> Result<GetUtxosByBlockResponse, Error> {
×
155
        request.validate()?;
×
156

157
        let hash = request.header_hash.clone().try_into()?;
×
158

159
        let header = self
×
160
            .db()
×
161
            .fetch_header_by_block_hash(hash)
×
162
            .await?
×
163
            .ok_or_else(|| Error::HeaderHashNotFound)?;
×
164

165
        // fetch utxos
166
        let outputs_with_statuses = self.db.fetch_outputs_in_block_with_spend_state(hash, None).await?;
×
167

168
        let outputs = outputs_with_statuses
×
169
            .into_iter()
×
170
            .map(|(output, _spent)| output)
×
171
            .collect::<Vec<TransactionOutput>>();
×
172

173
        // if its empty, we need to send an empty vec of outputs.
174
        let utxo_block_response = GetUtxosByBlockResponse {
×
175
            outputs,
×
176
            height: header.height,
×
177
            header_hash: hash.to_vec(),
×
178
            mined_timestamp: header.timestamp.as_u64(),
×
179
        };
×
180

181
        Ok(utxo_block_response)
×
182
    }
×
183

184
    #[allow(clippy::too_many_lines)]
185
    async fn fetch_utxos(&self, request: SyncUtxosByBlockRequest) -> Result<SyncUtxosByBlockResponseV0, Error> {
10✔
186
        // validate and fetch inputs
187
        request.validate()?;
10✔
188

189
        let hash = request.start_header_hash.clone().try_into()?;
10✔
190
        let start_header = self
10✔
191
            .db()
10✔
192
            .fetch_header_by_block_hash(hash)
10✔
193
            .await?
10✔
194
            .ok_or_else(|| Error::StartHeaderHashNotFound)?;
10✔
195

196
        let tip_header = self.db.fetch_tip_header().await?;
9✔
197
        // we only allow wallets to ask for a max of 100 blocks at a time and we want to cache the queries to ensure
198
        // they are in batch of 100 and we want to ensure they request goes to the nearest 100 block height so
199
        // we can cache all wallet's queries
200
        let increase = ((start_header.height + WALLET_MAX_BLOCKS_PER_REQUEST) / WALLET_MAX_BLOCKS_PER_REQUEST) *
9✔
201
            WALLET_MAX_BLOCKS_PER_REQUEST;
9✔
202
        let end_height = cmp::min(tip_header.header().height, increase);
9✔
203
        // pagination
204
        let start_header_height = start_header.height + (request.page * request.limit);
9✔
205
        if start_header_height > tip_header.header().height {
9✔
206
            return Err(Error::HeaderHeightMismatch {
1✔
207
                start_height: start_header.height,
1✔
208
                end_height: tip_header.header().height,
1✔
209
            });
1✔
210
        }
8✔
211
        let start_header = self
8✔
212
            .db
8✔
213
            .fetch_header(start_header_height)
8✔
214
            .await?
8✔
215
            .ok_or_else(|| Error::HeaderNotFound {
8✔
216
                height: start_header_height,
×
217
            })?;
×
218
        // fetch utxos
219
        let mut utxos = vec![];
8✔
220
        let next_page_start_height = start_header.height.saturating_add(request.limit);
8✔
221
        let mut current_header = start_header;
8✔
222
        let mut fetched_chunks = 0;
8✔
223
        let spending_end_header_hash = self
8✔
224
            .db
8✔
225
            .fetch_header(
8✔
226
                tip_header
8✔
227
                    .header()
8✔
228
                    .height
8✔
229
                    .saturating_sub(SYNC_UTXOS_SPEND_TIP_SAFETY_LIMIT),
8✔
230
            )
8✔
231
            .await?
8✔
232
            .ok_or_else(|| Error::HeaderNotFound {
8✔
233
                height: tip_header
×
234
                    .header()
×
235
                    .height
×
236
                    .saturating_sub(SYNC_UTXOS_SPEND_TIP_SAFETY_LIMIT),
×
237
            })?
×
238
            .hash();
8✔
239
        let next_header_to_request;
240
        let mut has_next_page = true;
8✔
241
        loop {
242
            let current_header_hash = current_header.hash();
20✔
243
            trace!(
20✔
244
                target: LOG_TARGET,
×
245
                "current header = {} ({})",
246
                current_header.height,
247
                current_header_hash.to_hex()
×
248
            );
249
            let outputs = if request.exclude_spent {
20✔
250
                self.db
×
251
                    .fetch_outputs_in_block_with_spend_state(current_header_hash, Some(spending_end_header_hash))
×
252
                    .await?
×
253
                    .into_iter()
×
254
                    .filter(|(_, spent)| !spent)
×
255
                    .map(|(output, _spent)| output)
×
256
                    .collect::<Vec<TransactionOutput>>()
×
257
            } else {
258
                self.db
20✔
259
                    .fetch_outputs_in_block_with_spend_state(current_header_hash, None)
20✔
260
                    .await?
20✔
261
                    .into_iter()
20✔
262
                    .map(|(output, _spent)| output)
20✔
263
                    .collect::<Vec<TransactionOutput>>()
20✔
264
            };
265
            let mut inputs = if request.exclude_inputs {
20✔
266
                Vec::new()
×
267
            } else {
268
                self.db
20✔
269
                    .fetch_inputs_in_block(current_header_hash)
20✔
270
                    .await?
20✔
271
                    .into_iter()
20✔
272
                    .map(|input| input.output_hash())
20✔
273
                    .collect::<Vec<FixedHash>>()
20✔
274
            };
275
            if outputs.is_empty() && inputs.is_empty() {
20✔
276
                // No outputs or inputs in this block, put empty placeholder here so wallet knows this height has been
3✔
277
                // scanned This can happen if all the outputs are spent and exclude_spent is true
3✔
278
                let block_response = BlockUtxoInfo {
3✔
279
                    outputs: Vec::new(),
3✔
280
                    inputs: Vec::new(),
3✔
281
                    height: current_header.height,
3✔
282
                    header_hash: current_header_hash.to_vec(),
3✔
283
                    mined_timestamp: current_header.timestamp.as_u64(),
3✔
284
                };
3✔
285
                utxos.push(block_response);
3✔
286
            }
17✔
287
            for output_chunk in outputs.chunks(self.max_utxo_chunk_size) {
20✔
288
                let inputs_to_send = if inputs.is_empty() {
17✔
289
                    Vec::new()
17✔
290
                } else {
UNCOV
291
                    let num_to_drain = inputs.len().min(self.max_utxo_chunk_size);
×
UNCOV
292
                    inputs.drain(..num_to_drain).map(|h| h.to_vec()).collect()
×
293
                };
294

295
                let output_block_response = BlockUtxoInfo {
17✔
296
                    outputs: output_chunk
17✔
297
                        .iter()
17✔
298
                        .map(|output| MinimalUtxoSyncInfo {
17✔
299
                            output_hash: output.hash().to_vec(),
17✔
300
                            commitment: output.commitment().to_vec(),
17✔
301
                            encrypted_data: output.encrypted_data().as_bytes().to_vec(),
17✔
302
                            sender_offset_public_key: output.sender_offset_public_key.to_vec(),
17✔
303
                        })
17✔
304
                        .collect(),
17✔
305
                    inputs: inputs_to_send,
17✔
306
                    height: current_header.height,
17✔
307
                    header_hash: current_header_hash.to_vec(),
17✔
308
                    mined_timestamp: current_header.timestamp.as_u64(),
17✔
309
                };
310
                utxos.push(output_block_response);
17✔
311
                fetched_chunks += 1;
17✔
312
            }
313
            // We might still have inputs left to send if they are more than the outputs
314
            for input_chunk in inputs.chunks(self.max_utxo_chunk_size) {
20✔
315
                let output_block_response = BlockUtxoInfo {
×
316
                    outputs: Vec::new(),
×
317
                    inputs: input_chunk.iter().map(|h| h.to_vec()).collect::<Vec<_>>().to_vec(),
×
318
                    height: current_header.height,
×
319
                    header_hash: current_header_hash.to_vec(),
×
320
                    mined_timestamp: current_header.timestamp.as_u64(),
×
321
                };
322
                utxos.push(output_block_response);
×
323
                fetched_chunks += 1;
×
324
            }
325

326
            if current_header.height >= tip_header.header().height {
20✔
327
                next_header_to_request = vec![];
2✔
328
                has_next_page = (end_height.saturating_sub(current_header.height)) > 0;
2✔
329
                break;
2✔
330
            }
18✔
331
            if fetched_chunks > request.limit {
18✔
UNCOV
332
                next_header_to_request = current_header_hash.to_vec();
×
333
                // This is a special edge case, our request has reached the page limit, but we are also not done with
334
                // the block. We also dont want to split up the block over two requests. So we need to ensure that we
335
                // remove the partial block we added so that it can be requested fully in the next request. We also dont
336
                // want to get in a loop where the block cannot fit into the page limit, so if the block is the same as
337
                // the first one, we just send it as is, partial. If not we remove it and let it be sent in the next
338
                // request.
UNCOV
339
                if utxos.first().ok_or(Error::General(anyhow::anyhow!("No utxos founds")))? // should never happen as we always add at least one block
×
340
                    .header_hash ==
UNCOV
341
                    current_header_hash.to_vec()
×
342
                {
343
                    // special edge case where the first block is also the last block we can send, so we just send it as
344
                    // is, partial
345
                    break;
×
UNCOV
346
                }
×
UNCOV
347
                while !utxos.is_empty() &&
×
UNCOV
348
                    utxos.last().ok_or(Error::General(anyhow::anyhow!("No utxos found")))? // should never happen as we always add at least one block
×
349
                    .header_hash ==
UNCOV
350
                        current_header_hash.to_vec()
×
UNCOV
351
                {
×
UNCOV
352
                    utxos.pop();
×
UNCOV
353
                }
×
UNCOV
354
                has_next_page = false;
×
UNCOV
355
                break;
×
356
            }
18✔
357
            if current_header.height + 1 > end_height {
18✔
358
                next_header_to_request = current_header.hash().to_vec();
×
359
                has_next_page = (end_height.saturating_sub(current_header.height)) > 0;
×
360
                break; // Stop if we reach the end height
×
361
            }
18✔
362
            current_header =
18✔
363
                self.db
18✔
364
                    .fetch_header(current_header.height + 1)
18✔
365
                    .await?
18✔
366
                    .ok_or_else(|| Error::HeaderNotFound {
18✔
367
                        height: current_header.height + 1,
×
368
                    })?;
×
369

370
            if current_header.height == next_page_start_height {
18✔
371
                // we are on the limit, stop here
372
                next_header_to_request = current_header.hash().to_vec();
6✔
373
                has_next_page = (end_height.saturating_sub(current_header.height)) > 0;
6✔
374
                break;
6✔
375
            }
12✔
376
        }
377
        Ok(SyncUtxosByBlockResponseV0 {
8✔
378
            blocks: utxos,
8✔
379
            has_next_page,
8✔
380
            next_header_to_scan: next_header_to_request,
8✔
381
        })
8✔
382
    }
10✔
383
}
384

385
#[async_trait::async_trait]
386
impl<B: BlockchainBackend + 'static> BaseNodeWalletQueryService for Service<B> {
387
    type Error = Error;
388

389
    async fn get_tip_info(&self) -> Result<TipInfoResponse, Self::Error> {
×
390
        let state_machine = self.state_machine();
391
        let status_watch = state_machine.get_status_info_watch();
392
        let is_synced = match status_watch.borrow().state_info {
393
            StateInfo::Listening(li) => li.is_synced(),
394
            _ => false,
395
        };
396

397
        let metadata = self.db.get_chain_metadata().await?;
398

399
        Ok(TipInfoResponse {
400
            metadata: Some(metadata),
401
            is_synced,
402
        })
403
    }
×
404

405
    async fn get_header_by_height(&self, height: u64) -> Result<models::BlockHeader, Self::Error> {
×
406
        let result = self
407
            .db
408
            .fetch_header(height)
409
            .await?
410
            .ok_or(Error::HeaderNotFound { height })?
411
            .into();
412
        Ok(result)
413
    }
×
414

415
    async fn get_height_at_time(&self, epoch_time: u64) -> Result<u64, Self::Error> {
×
416
        trace!(target: LOG_TARGET, "requested_epoch_time: {}", epoch_time);
417
        let tip_header = self.db.fetch_tip_header().await?;
418

419
        let mut left_height = 0u64;
420
        let mut right_height = tip_header.height();
421

422
        while left_height <= right_height {
423
            let mut mid_height = (left_height + right_height) / 2;
424

425
            if mid_height == 0 {
426
                return Ok(0u64);
427
            }
428
            // If the two bounds are adjacent then perform the test between the right and left sides
429
            if left_height == mid_height {
430
                mid_height = right_height;
431
            }
432

433
            let mid_header = self
434
                .db
435
                .fetch_header(mid_height)
436
                .await?
437
                .ok_or_else(|| Error::HeaderNotFound { height: mid_height })?;
×
438
            let before_mid_header = self
439
                .db
440
                .fetch_header(mid_height - 1)
441
                .await?
442
                .ok_or_else(|| Error::HeaderNotFound { height: mid_height - 1 })?;
×
443
            trace!(
444
                target: LOG_TARGET,
445
                "requested_epoch_time: {}, left: {}, mid: {}/{} ({}/{}), right: {}",
446
                epoch_time,
447
                left_height,
448
                mid_height,
449
                mid_height-1,
450
                mid_header.timestamp.as_u64(),
451
                before_mid_header.timestamp.as_u64(),
452
                right_height
453
            );
454
            if epoch_time < mid_header.timestamp.as_u64() && epoch_time >= before_mid_header.timestamp.as_u64() {
455
                trace!(
456
                    target: LOG_TARGET,
457
                    "requested_epoch_time: {}, selected height: {}",
458
                    epoch_time, before_mid_header.height
459
                );
460
                return Ok(before_mid_header.height);
461
            } else if mid_height == right_height {
462
                trace!(
463
                    target: LOG_TARGET,
464
                    "requested_epoch_time: {epoch_time}, selected height: {right_height}"
465
                );
466
                return Ok(right_height);
467
            } else if epoch_time <= mid_header.timestamp.as_u64() {
468
                right_height = mid_height;
469
            } else {
470
                left_height = mid_height;
471
            }
472
        }
473

474
        Ok(0u64)
475
    }
×
476

477
    async fn transaction_query(
478
        &self,
479
        signature: crate::base_node::rpc::models::Signature,
480
    ) -> Result<TxQueryResponse, Self::Error> {
×
481
        let signature = signature.try_into().map_err(Error::SignatureConversion)?;
482

483
        let response = self.fetch_kernel(signature).await?;
484

485
        Ok(response)
486
    }
×
487

488
    async fn sync_utxos_by_block_v0(
489
        &self,
490
        request: SyncUtxosByBlockRequest,
491
    ) -> Result<SyncUtxosByBlockResponseV0, Self::Error> {
×
492
        self.fetch_utxos(request).await
493
    }
×
494

495
    async fn sync_utxos_by_block_v1(
496
        &self,
497
        request: SyncUtxosByBlockRequest,
498
    ) -> Result<SyncUtxosByBlockResponseV1, Self::Error> {
×
499
        let v1 = self.fetch_utxos(request).await?;
500
        Ok(v1.into())
501
    }
×
502

503
    async fn get_utxos_by_block(
504
        &self,
505
        request: GetUtxosByBlockRequest,
506
    ) -> Result<GetUtxosByBlockResponse, Self::Error> {
×
507
        self.fetch_utxos_by_block(request).await
508
    }
×
509

510
    async fn get_utxos_mined_info(
511
        &self,
512
        request: models::GetUtxosMinedInfoRequest,
513
    ) -> Result<models::GetUtxosMinedInfoResponse, Self::Error> {
×
514
        request.validate()?;
515

516
        let mut utxos = vec![];
517
        let mut unmined_hashes = vec![];
518

519
        let tip_header = self.db().fetch_tip_header().await?;
520
        for hash in request.hashes {
521
            let hash: types::HashOutput = hash.try_into()?;
522
            let output = self.db().fetch_output(hash).await?;
523
            if let Some(output) = output {
524
                utxos.push(models::MinedUtxoInfo {
525
                    utxo_hash: hash.to_vec(),
526
                    mined_in_hash: output.header_hash.to_vec(),
527
                    mined_in_height: output.mined_height,
528
                    mined_in_timestamp: output.mined_timestamp,
529
                });
530
            } else {
531
                unmined_hashes.push(hash);
532
            }
533
        }
534

535
        // Version 2: also check mempool for unmined outputs
536
        let mempool_utxos = if request.version >= 2 && !unmined_hashes.is_empty() {
537
            let mut mempool = self.mempool();
538
            mempool
539
                .filter_outputs_in_mempool(unmined_hashes)
540
                .await?
541
                .into_iter()
542
                .map(|h| h.to_vec())
×
543
                .collect()
544
        } else {
545
            vec![]
546
        };
547

548
        Ok(models::GetUtxosMinedInfoResponse {
549
            utxos,
550
            best_block_hash: tip_header.hash().to_vec(),
551
            best_block_height: tip_header.height(),
552
            mempool_utxos,
553
        })
554
    }
×
555

556
    async fn get_utxos_deleted_info(
557
        &self,
558
        request: models::GetUtxosDeletedInfoRequest,
559
    ) -> Result<models::GetUtxosDeletedInfoResponse, Self::Error> {
×
560
        request.validate()?;
561

562
        let mut utxos = vec![];
563

564
        let must_include_header = request.must_include_header.clone().try_into()?;
565
        if self
566
            .db()
567
            .fetch_header_by_block_hash(must_include_header)
568
            .await?
569
            .is_none()
570
        {
571
            return Err(Error::HeaderHashNotFound);
572
        }
573

574
        let tip_header = self.db().fetch_tip_header().await?;
575
        for hash in request.hashes {
576
            let hash = hash.try_into()?;
577
            let output = self.db().fetch_output(hash).await?;
578

579
            if let Some(output) = output {
580
                // is it still unspent?
581
                let input = self.db().fetch_input(hash).await?;
582
                if let Some(i) = input {
583
                    utxos.push(models::DeletedUtxoInfo {
584
                        utxo_hash: hash.to_vec(),
585
                        found_in_header: Some((output.mined_height, output.header_hash.to_vec())),
586
                        spent_in_header: Some((i.spent_height, i.header_hash.to_vec())),
587
                    });
588
                } else {
589
                    utxos.push(models::DeletedUtxoInfo {
590
                        utxo_hash: hash.to_vec(),
591
                        found_in_header: Some((output.mined_height, output.header_hash.to_vec())),
592
                        spent_in_header: None,
593
                    });
594
                }
595
            } else {
596
                utxos.push(models::DeletedUtxoInfo {
597
                    utxo_hash: hash.to_vec(),
598
                    found_in_header: None,
599
                    spent_in_header: None,
600
                });
601
            }
602
        }
603

604
        Ok(models::GetUtxosDeletedInfoResponse {
605
            utxos,
606
            best_block_hash: tip_header.hash().to_vec(),
607
            best_block_height: tip_header.height(),
608
        })
609
    }
×
610

611
    async fn get_utxos_deleted_info_v1(
612
        &self,
613
        request: models::GetUtxosDeletedInfoRequest,
614
    ) -> Result<models::GetUtxosDeletedInfoResponseV1, Self::Error> {
×
615
        request.validate()?;
616

617
        let mut utxos = Vec::with_capacity(request.hashes.len());
618

619
        let must_include_header = request.must_include_header.clone().try_into()?;
620
        if self
621
            .db()
622
            .fetch_header_by_block_hash(must_include_header)
623
            .await?
624
            .is_none()
625
        {
626
            return Err(Error::HeaderHashNotFound);
627
        }
628

629
        let tip_header = self.db().fetch_tip_header().await?;
630
        for hash in request.hashes {
631
            let hash = hash.try_into()?;
632
            let output = self.db().fetch_output(hash).await?;
633

634
            let utxo_info = if let Some(output) = output {
635
                let input = self.db().fetch_input(hash).await?;
636
                models::DeletedUtxoInfoV1 {
637
                    utxo_hash: hash.to_vec(),
638
                    found_in_header: Some((output.mined_height, output.header_hash.to_vec())),
639
                    spent_in_header: input.as_ref().map(|i| (i.spent_height, i.header_hash.to_vec())),
×
640
                    spent_timestamp: input.as_ref().map(|i| i.spent_timestamp),
641
                }
642
            } else {
643
                models::DeletedUtxoInfoV1 {
644
                    utxo_hash: hash.to_vec(),
645
                    found_in_header: None,
646
                    spent_in_header: None,
647
                    spent_timestamp: None,
648
                }
649
            };
650
            utxos.push(utxo_info);
651
        }
652

653
        Ok(models::GetUtxosDeletedInfoResponseV1 {
654
            utxos,
655
            best_block_hash: tip_header.hash().to_vec(),
656
            best_block_height: tip_header.height(),
657
        })
658
    }
×
659

660
    async fn generate_kernel_merkle_proof(
661
        &self,
662
        excess_sig: types::CompressedSignature,
663
    ) -> Result<GenerateKernelMerkleProofResponse, Self::Error> {
×
664
        let proof = self.db().generate_kernel_merkle_proof(excess_sig).await?;
665
        Ok(GenerateKernelMerkleProofResponse {
666
            encoded_merkle_proof: bincode::serialize(&proof.merkle_proof).map_err(Error::general)?,
667
            block_hash: proof.block_hash,
668
            leaf_index: proof.leaf_index.value() as u64,
669
            block_height: Some(proof.block_height),
670
        })
671
    }
×
672

673
    async fn get_utxo(&self, request: models::GetUtxoRequest) -> Result<Option<TransactionOutput>, Self::Error> {
×
674
        let hash: FixedHash = request.output_hash.try_into().map_err(Error::general)?;
675
        let outputs = self.db().fetch_outputs_with_spend_status_at_tip(vec![hash]).await?;
676
        let output = match outputs.first() {
677
            Some(Some((output, _spent))) => Some(output.clone()),
678
            _ => return Err(Error::OutputNotFound),
679
        };
680
        Ok(output)
681
    }
×
682

683
    async fn get_mempool_fee_per_gram_stats(&self, count: usize) -> Result<Vec<models::FeePerGramStat>, Self::Error> {
×
684
        if count > 20 {
685
            return Err(Error::general(anyhow::anyhow!(
686
                "count must be less than or equal to 20"
687
            )));
688
        }
689

690
        let metadata = self.db.get_chain_metadata().await?;
691
        let stats = self
692
            .mempool()
693
            .get_fee_per_gram_stats(count, metadata.best_block_height())
694
            .await
695
            .map_err(Error::general)?;
696

697
        Ok(stats)
698
    }
×
699
}
700

701
#[cfg(test)]
702
mod tests {
703
    #![allow(clippy::indexing_slicing)]
704
    use tari_common::configuration::Network;
705
    use tari_shutdown::Shutdown;
706

707
    use super::*;
708
    use crate::test_helpers::blockchain::create_new_blockchain_with_network;
709
    fn make_state_machine_handle() -> StateMachineHandle {
4✔
710
        use tokio::sync::{broadcast, watch};
711
        let (state_tx, _state_rx) = broadcast::channel(10);
4✔
712
        let (_status_tx, status_rx) =
4✔
713
            watch::channel(crate::base_node::state_machine_service::states::StatusInfo::new());
4✔
714
        let shutdown = Shutdown::new();
4✔
715
        StateMachineHandle::new(state_tx, status_rx, shutdown.to_signal())
4✔
716
    }
4✔
717

718
    fn make_mempool_handle() -> MempoolHandle {
4✔
719
        use crate::mempool::test_utils::mock::create_mempool_service_mock;
720
        let (handle, _state) = create_mempool_service_mock();
4✔
721
        handle
4✔
722
    }
4✔
723

724
    async fn make_service() -> Service<crate::test_helpers::blockchain::TempDatabase> {
2✔
725
        let db = create_new_blockchain_with_network(Network::LocalNet);
2✔
726
        let adb = AsyncBlockchainDb::from(db);
2✔
727
        let state_machine = make_state_machine_handle();
2✔
728
        let mempool = make_mempool_handle();
2✔
729
        Service::new(adb, state_machine, mempool)
2✔
730
    }
2✔
731

732
    #[tokio::test]
733
    async fn fetch_utxos_start_header_not_found() {
1✔
734
        let service = make_service().await;
1✔
735
        let req = SyncUtxosByBlockRequest {
1✔
736
            start_header_hash: vec![0xAB; 32],
1✔
737
            limit: 4,
1✔
738
            page: 0,
1✔
739
            exclude_spent: false,
1✔
740
            exclude_inputs: false,
1✔
741
            version: 0,
1✔
742
        };
1✔
743
        let err = service.fetch_utxos(req).await.unwrap_err();
1✔
744
        match err {
1✔
745
            Error::StartHeaderHashNotFound => {},
1✔
746
            other => panic!("unexpected error: {other:?}"),
1✔
747
        }
1✔
748
    }
1✔
749

750
    #[tokio::test]
751
    async fn fetch_utxos_header_height_mismatch() {
1✔
752
        let service = make_service().await;
1✔
753
        let genesis = service.db().fetch_header(0).await.unwrap().unwrap();
1✔
754
        // page * limit moves start height beyond tip (0)
755
        let req = SyncUtxosByBlockRequest {
1✔
756
            start_header_hash: genesis.hash().to_vec(),
1✔
757
            limit: 1,
1✔
758
            page: 1,
1✔
759
            exclude_spent: false,
1✔
760
            exclude_inputs: false,
1✔
761
            version: 0,
1✔
762
        };
1✔
763
        let err = service.fetch_utxos(req).await.unwrap_err();
1✔
764
        match err {
1✔
765
            Error::HeaderHeightMismatch { .. } => {},
1✔
766
            other => panic!("unexpected error: {other:?}"),
1✔
767
        }
1✔
768
    }
1✔
769

770
    #[tokio::test]
771
    async fn fetch_utxos_paginates_results() {
1✔
772
        use crate::test_helpers::blockchain::create_main_chain;
773

774
        // Build a small chain: GB -> A -> B -> C
775
        let db = create_new_blockchain_with_network(Network::LocalNet);
1✔
776
        let (_names, chain) = create_main_chain(&db, block_specs!(["A->GB"], ["B->A"], ["C->B"], ["D->C"]));
1✔
777

778
        // Construct the service over this DB
779
        let adb = AsyncBlockchainDb::from(db);
1✔
780
        let state_machine = make_state_machine_handle();
1✔
781
        let mempool = make_mempool_handle();
1✔
782
        let service = Service::new(adb, state_machine, mempool);
1✔
783

784
        // Use genesis as the start header hash
785
        let genesis = service.db().fetch_header(0).await.unwrap().unwrap();
1✔
786
        let g_hash = genesis.hash().to_vec();
1✔
787

788
        // Page 0, limit 1: should return only A (height 1) and next header should be B
789
        // genesis block in local net has 0 inputs and outputs
790
        let resp0 = service
1✔
791
            .fetch_utxos(SyncUtxosByBlockRequest {
1✔
792
                start_header_hash: g_hash.clone(),
1✔
793
                limit: 1,
1✔
794
                page: 0,
1✔
795
                exclude_spent: false,
1✔
796
                exclude_inputs: false,
1✔
797
                version: 0,
1✔
798
            })
1✔
799
            .await
1✔
800
            .expect("fetch_utxos page 0 should succeed");
1✔
801

802
        assert_eq!(resp0.blocks.len(), 1, "expected exactly one block in first page");
1✔
803
        assert_eq!(resp0.blocks[0].height, 0, "first page should start at genesis height");
1✔
804

805
        // Expect next_header_to_scan to be the hash of block A (height 1)
806
        let a_hash = chain.get("A").unwrap().hash().to_vec();
1✔
807
        assert_eq!(resp0.next_header_to_scan, a_hash, "next header should point to A");
1✔
808

809
        // Page 1, limit 1: should return only block A (height 1) and next header should be B
810
        let resp1 = service
1✔
811
            .fetch_utxos(SyncUtxosByBlockRequest {
1✔
812
                start_header_hash: g_hash.clone(),
1✔
813
                limit: 1,
1✔
814
                page: 1,
1✔
815
                exclude_spent: false,
1✔
816
                exclude_inputs: false,
1✔
817
                version: 0,
1✔
818
            })
1✔
819
            .await
1✔
820
            .expect("fetch_utxos page 1 should succeed");
1✔
821

822
        assert_eq!(resp1.blocks.len(), 1, "expected exactly one block in second page");
1✔
823
        assert_eq!(resp1.blocks[0].height, 1, "second page should start at height 1 (A)");
1✔
824

825
        // Expect next_header_to_scan to be the hash of block B (height 2)
826
        let b_hash = chain.get("B").unwrap().hash().to_vec();
1✔
827
        assert_eq!(resp1.next_header_to_scan, b_hash, "next header should point to B");
1✔
828

829
        // Page 2, limit 1: should return only block B (height 2) and next header should be C
830
        let resp2 = service
1✔
831
            .fetch_utxos(SyncUtxosByBlockRequest {
1✔
832
                start_header_hash: g_hash.clone(),
1✔
833
                limit: 1,
1✔
834
                page: 2,
1✔
835
                exclude_spent: false,
1✔
836
                exclude_inputs: false,
1✔
837
                version: 0,
1✔
838
            })
1✔
839
            .await
1✔
840
            .expect("fetch_utxos page 2 should succeed");
1✔
841

842
        assert_eq!(resp2.blocks.len(), 1, "expected exactly one block in third page");
1✔
843
        assert_eq!(resp2.blocks[0].height, 2, "third page should start at height 2 (B)");
1✔
844
        let c_hash = chain.get("C").unwrap().hash().to_vec();
1✔
845
        assert_eq!(resp2.next_header_to_scan, c_hash, "next header should point to C");
1✔
846

847
        // Page 3, limit 1: should return only block C (height 3) and next header should be empty (tip reached)
848
        let resp3 = service
1✔
849
            .fetch_utxos(SyncUtxosByBlockRequest {
1✔
850
                start_header_hash: g_hash.clone(),
1✔
851
                limit: 1,
1✔
852
                page: 3,
1✔
853
                exclude_spent: false,
1✔
854
                exclude_inputs: false,
1✔
855
                version: 0,
1✔
856
            })
1✔
857
            .await
1✔
858
            .expect("fetch_utxos page 3 should succeed");
1✔
859

860
        assert_eq!(resp3.blocks.len(), 1, "expected exactly one block in fourth page");
1✔
861
        assert_eq!(resp3.blocks[0].height, 3, "fourth page should start at height 3 (C)");
1✔
862
        let d_hash = chain.get("D").unwrap().hash().to_vec();
1✔
863
        assert_eq!(resp3.next_header_to_scan, d_hash, "next header should point to D");
1✔
864

865
        // Page 4, limit 1: should return only block C (height 3) and next header should be empty (tip reached)
866
        let resp4 = service
1✔
867
            .fetch_utxos(SyncUtxosByBlockRequest {
1✔
868
                start_header_hash: g_hash.clone(),
1✔
869
                limit: 1,
1✔
870
                page: 4,
1✔
871
                exclude_spent: false,
1✔
872
                exclude_inputs: false,
1✔
873
                version: 0,
1✔
874
            })
1✔
875
            .await
1✔
876
            .expect("fetch_utxos page 3 should succeed");
1✔
877

878
        assert_eq!(resp4.blocks.len(), 1, "expected exactly one block in fourth page");
1✔
879
        assert_eq!(resp4.blocks[0].height, 4, "fourth page should start at height 4 (D)");
1✔
880
        assert!(resp4.next_header_to_scan.is_empty(), "no next header at tip");
1✔
881
        let resp5 = service
1✔
882
            .fetch_utxos(SyncUtxosByBlockRequest {
1✔
883
                start_header_hash: g_hash,
1✔
884
                limit: 2,
1✔
885
                page: 0,
1✔
886
                exclude_spent: false,
1✔
887
                exclude_inputs: false,
1✔
888
                version: 0,
1✔
889
            })
1✔
890
            .await
1✔
891
            .expect("fetch_utxos should succeed");
1✔
892

893
        assert_eq!(resp5.blocks.len(), 2, "expected 2 blocks");
1✔
894
        assert_eq!(resp5.blocks[0].height, 0, "Should be block (GB)");
1✔
895
        assert_eq!(resp5.blocks[1].height, 1, "Should be block (A)");
1✔
896
        let b_hash = chain.get("B").unwrap().hash().to_vec();
1✔
897
        assert_eq!(resp5.next_header_to_scan, b_hash, "next header should point to B");
1✔
898
        assert!(resp5.has_next_page, "Should have more pages");
1✔
899
    }
1✔
900

901
    #[tokio::test]
902
    async fn large_fetch_utxo_paginates_results() {
1✔
903
        use crate::test_helpers::blockchain::create_main_chain;
904

905
        // Build a small chain: GB -> A -> B -> C
906
        let db = create_new_blockchain_with_network(Network::LocalNet);
1✔
907
        let (_names, chain) = create_main_chain(
1✔
908
            &db,
1✔
909
            block_specs!(
1✔
910
                ["1->GB"],
1✔
911
                ["2->1"],
1✔
912
                ["3->2"],
1✔
913
                ["4->3"],
1✔
914
                ["5->4"],
1✔
915
                ["6->5"],
1✔
916
                ["7->6"],
1✔
917
                ["8->7"],
1✔
918
                ["9->8"],
1✔
919
                ["10->9"],
1✔
920
                ["11->10"],
1✔
921
                ["12->11"]
1✔
922
            ),
1✔
923
        );
1✔
924

925
        // Construct the service over this DB
926
        let adb = AsyncBlockchainDb::from(db);
1✔
927
        let state_machine = make_state_machine_handle();
1✔
928
        let mempool = make_mempool_handle();
1✔
929
        let service = Service::new(adb, state_machine, mempool);
1✔
930

931
        // Use genesis as the start header hash
932
        let genesis = service.db().fetch_header(0).await.unwrap().unwrap();
1✔
933
        let g_hash = genesis.hash().to_vec();
1✔
934

935
        let resp = service
1✔
936
            .fetch_utxos(SyncUtxosByBlockRequest {
1✔
937
                start_header_hash: g_hash.clone(),
1✔
938
                limit: 10,
1✔
939
                page: 0,
1✔
940
                exclude_spent: false,
1✔
941
                exclude_inputs: false,
1✔
942
                version: 0,
1✔
943
            })
1✔
944
            .await
1✔
945
            .expect("fetch_utxos should succeed");
1✔
946

947
        assert_eq!(resp.blocks.len(), 10, "expected 10 blocks");
1✔
948
        assert_eq!(resp.blocks[0].height, 0, "Should be block (0)");
1✔
949
        assert_eq!(resp.blocks[1].height, 1, "Should be block (1)");
1✔
950
        assert_eq!(resp.blocks[2].height, 2, "Should be block (2)");
1✔
951
        assert_eq!(resp.blocks[3].height, 3, "Should be block (3)");
1✔
952
        assert_eq!(resp.blocks[4].height, 4, "Should be block (4)");
1✔
953
        assert_eq!(resp.blocks[5].height, 5, "Should be block (5)");
1✔
954
        assert_eq!(resp.blocks[6].height, 6, "Should be block (6)");
1✔
955
        assert_eq!(resp.blocks[7].height, 7, "Should be block (7)");
1✔
956
        assert_eq!(resp.blocks[8].height, 8, "Should be block (8)");
1✔
957
        assert_eq!(resp.blocks[9].height, 9, "Should be block (9)");
1✔
958
        let next_hash = chain.get("10").unwrap().hash().to_vec();
1✔
959
        assert_eq!(resp.next_header_to_scan, next_hash, "next header should point to 10");
1✔
960
        assert!(resp.has_next_page, "Should have more pages");
1✔
961
        let resp = service
1✔
962
            .fetch_utxos(SyncUtxosByBlockRequest {
1✔
963
                start_header_hash: g_hash,
1✔
964
                limit: 10,
1✔
965
                page: 1,
1✔
966
                exclude_spent: false,
1✔
967
                exclude_inputs: false,
1✔
968
                version: 0,
1✔
969
            })
1✔
970
            .await
1✔
971
            .expect("fetch_utxos should succeed");
1✔
972
        assert_eq!(resp.blocks.len(), 3, "expected 3 blocks");
1✔
973
        assert_eq!(resp.blocks[0].height, 10, "Should be block (10)");
1✔
974
        assert_eq!(resp.blocks[1].height, 11, "Should be block (11)");
1✔
975
        assert_eq!(resp.blocks[2].height, 12, "Should be block (12)");
1✔
976
        assert!(resp.next_header_to_scan.is_empty(), "Should be empty");
1✔
977
        assert!(!resp.has_next_page, "Should not have more pages");
1✔
978
    }
1✔
979

980
    // this will only run and work in esmeralda
981
    #[cfg(tari_target_network_testnet)]
982
    #[tokio::test]
983
    async fn large_utxo_handled_correctly() {
1✔
984
        use crate::test_helpers::blockchain::create_main_chain;
985

986
        // Build a small chain: GB -> A -> B -> C
987
        let db = create_new_blockchain_with_network(Network::Esmeralda);
1✔
988
        let (_names, _chain) = create_main_chain(
1✔
989
            &db,
1✔
990
            block_specs!(
1✔
991
                ["1->GB"],
1✔
992
                ["2->1"],
1✔
993
                ["3->2"],
1✔
994
                ["4->3"],
1✔
995
                ["5->4"],
1✔
996
                ["6->5"],
1✔
997
                ["7->6"],
1✔
998
                ["8->7"],
1✔
999
                ["9->8"],
1✔
1000
                ["10->9"],
1✔
1001
            ),
1✔
1002
        );
1✔
1003

1004
        // Construct the service over this DB
1005
        let adb = AsyncBlockchainDb::from(db);
1✔
1006
        let state_machine = make_state_machine_handle();
1✔
1007
        let mempool = make_mempool_handle();
1✔
1008
        let mut service = Service::new(adb, state_machine, mempool);
1✔
1009
        service.max_utxo_chunk_size = 500; // set small chunk size for testing
1✔
1010

1011
        // Use genesis as the start header hash
1012
        let genesis = service.db().fetch_header(0).await.unwrap().unwrap();
1✔
UNCOV
1013
        let g_hash = genesis.hash().to_vec();
×
1014

UNCOV
1015
        let resp = service
×
UNCOV
1016
            .fetch_utxos(SyncUtxosByBlockRequest {
×
UNCOV
1017
                start_header_hash: g_hash.clone(),
×
UNCOV
1018
                limit: 5,
×
UNCOV
1019
                page: 0,
×
UNCOV
1020
                exclude_spent: false,
×
UNCOV
1021
                exclude_inputs: false,
×
UNCOV
1022
                version: 0,
×
UNCOV
1023
            })
×
UNCOV
1024
            .await
×
UNCOV
1025
            .expect("fetch_utxos should succeed");
×
1026

UNCOV
1027
        assert_eq!(resp.blocks.len(), 5, "expected 5 blocks");
×
UNCOV
1028
        assert_eq!(resp.blocks[0].height, 0, "Should be block (0)");
×
UNCOV
1029
        assert_eq!(resp.blocks[1].height, 0, "Should be block (0)");
×
UNCOV
1030
        assert_eq!(resp.blocks[2].height, 1, "Should be block (1)");
×
UNCOV
1031
        assert_eq!(resp.blocks[3].height, 2, "Should be block (2)");
×
UNCOV
1032
        assert_eq!(resp.blocks[4].height, 3, "Should be block (3)");
×
UNCOV
1033
        let header_4 = service.db().fetch_header(4).await.unwrap().unwrap();
×
UNCOV
1034
        assert_eq!(
×
UNCOV
1035
            header_4.hash().to_vec(),
×
1036
            resp.next_header_to_scan,
1037
            "next header should point to 4"
1038
        );
UNCOV
1039
        assert!(!resp.has_next_page, "Should have no more pages");
×
1040
    }
1✔
1041
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc