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

tari-project / tari / 15534516141

09 Jun 2025 12:28PM UTC coverage: 72.299% (-0.06%) from 72.363%
15534516141

push

github

web-flow
test: add ffi cucumber wallet balance test (#7189)

Description
---
- Added cucumber test to verify wallet balance with recovery into FFI
wallet.
- Fixed broken `As a client I want to be able to restore my ffi wallet
from seed words` cucumber test.

Motivation and Context
---
Trying to solve various balance query inconsistencies.

How Has This Been Tested?
---
Cucumber test: `As a client I want to be able to check my balance from
restored wallets`

What process can a PR reviewer use to test or verify this change?
---
Code review

<!-- Checklist -->
<!-- 1. Is the title of your PR in the form that would make nice release
notes? The title, excluding the conventional commit
tag, will be included exactly as is in the CHANGELOG, so please think
about it carefully. -->


Breaking Changes
---

- [x] None
- [ ] Requires data directory on base node to be deleted
- [ ] Requires hard fork
- [ ] Other - Please specify

<!-- Does this include a breaking change? If so, include this line as a
footer -->
<!-- BREAKING CHANGE: Description what the user should do, e.g. delete a
database, resync the chain -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added the ability to restore FFI wallets from seed words and verify
balances in integration tests.
- Introduced support for storing and checking wallet balances within the
test environment.
- Enhanced wallet CLI test steps to support wallet recovery from seed
words and view/spend key export/import.
- Added detailed logging for transaction errors in the wallet gRPC
server.
- Extended wallet process and world state with new fields for base node
association, peer seeds, balances, and keys.

- **Bug Fixes**
- Improved type handling when collecting seed words during wallet
recovery.

- **Tests**
- Added critical scenarios to test wallet restoration and balance
checking for FFI wallets.
- Introduced new test steps for remembering and verifying walle... (continued)

81268 of 112405 relevant lines covered (72.3%)

242759.67 hits per line

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

20.63
/base_layer/core/src/base_node/comms_interface/local_interface.rs
1
// Copyright 2019. The Tari Project
2
//
3
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
4
// following conditions are met:
5
//
6
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
7
// disclaimer.
8
//
9
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
10
// following disclaimer in the documentation and/or other materials provided with the distribution.
11
//
12
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
13
// products derived from this software without specific prior written permission.
14
//
15
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
16
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
18
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
20
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
21
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22

23
use std::{ops::RangeInclusive, sync::Arc};
24

25
use tari_common_types::{
26
    chain_metadata::ChainMetadata,
27
    types::{BlockHash, CompressedCommitment, CompressedPublicKey, HashOutput, Signature},
28
};
29
use tari_service_framework::{reply_channel::SenderService, Service};
30
use tokio::sync::broadcast;
31

32
use crate::{
33
    base_node::comms_interface::{
34
        comms_request::GetNewBlockTemplateRequest,
35
        error::CommsInterfaceError,
36
        BlockEvent,
37
        NodeCommsRequest,
38
        NodeCommsResponse,
39
    },
40
    blocks::{Block, ChainHeader, HistoricalBlock, NewBlockTemplate},
41
    chain_storage::TemplateRegistrationEntry,
42
    proof_of_work::{Difficulty, PowAlgorithm},
43
    transactions::transaction_components::{TransactionKernel, TransactionOutput},
44
};
45

46
pub type BlockEventSender = broadcast::Sender<Arc<BlockEvent>>;
47
pub type BlockEventReceiver = broadcast::Receiver<Arc<BlockEvent>>;
48

49
/// The InboundNodeCommsInterface provides an interface to request information from the current local node by other
50
/// internal services.
51
#[derive(Clone)]
52
pub struct LocalNodeCommsInterface {
53
    request_sender: SenderService<NodeCommsRequest, Result<NodeCommsResponse, CommsInterfaceError>>,
54
    block_sender: SenderService<Block, Result<BlockHash, CommsInterfaceError>>,
55
    block_event_sender: BlockEventSender,
56
}
57

58
impl LocalNodeCommsInterface {
59
    /// Construct a new LocalNodeCommsInterface with the specified SenderService.
60
    pub fn new(
62✔
61
        request_sender: SenderService<NodeCommsRequest, Result<NodeCommsResponse, CommsInterfaceError>>,
62✔
62
        block_sender: SenderService<Block, Result<BlockHash, CommsInterfaceError>>,
62✔
63
        block_event_sender: BlockEventSender,
62✔
64
    ) -> Self {
62✔
65
        Self {
62✔
66
            request_sender,
62✔
67
            block_sender,
62✔
68
            block_event_sender,
62✔
69
        }
62✔
70
    }
62✔
71

72
    pub fn get_block_event_stream(&self) -> BlockEventReceiver {
107✔
73
        self.block_event_sender.subscribe()
107✔
74
    }
107✔
75

76
    /// Request metadata from the current local node.
77
    pub async fn get_metadata(&mut self) -> Result<ChainMetadata, CommsInterfaceError> {
82✔
78
        match self.request_sender.call(NodeCommsRequest::GetChainMetadata).await?? {
82✔
79
            NodeCommsResponse::ChainMetadata(metadata) => Ok(metadata),
82✔
80
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
81
        }
82
    }
82✔
83

84
    pub async fn get_target_difficulty_for_next_block(
×
85
        &mut self,
×
86
        algo: PowAlgorithm,
×
87
    ) -> Result<Difficulty, CommsInterfaceError> {
×
88
        match self
×
89
            .request_sender
×
90
            .call(NodeCommsRequest::GetTargetDifficultyNextBlock(algo))
×
91
            .await??
×
92
        {
93
            NodeCommsResponse::TargetDifficulty(target_difficulty) => Ok(target_difficulty),
×
94
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
95
        }
96
    }
×
97

98
    /// Request the block headers within the given range
99
    pub async fn get_blocks(
×
100
        &mut self,
×
101
        range: RangeInclusive<u64>,
×
102
        compact: bool,
×
103
    ) -> Result<Vec<HistoricalBlock>, CommsInterfaceError> {
×
104
        match self
×
105
            .request_sender
×
106
            .call(NodeCommsRequest::FetchMatchingBlocks { range, compact })
×
107
            .await??
×
108
        {
109
            NodeCommsResponse::HistoricalBlocks(blocks) => Ok(blocks),
×
110
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
111
        }
112
    }
×
113

114
    /// Request the block header at the given height
115
    pub async fn get_block(
×
116
        &mut self,
×
117
        height: u64,
×
118
        compact: bool,
×
119
    ) -> Result<Option<HistoricalBlock>, CommsInterfaceError> {
×
120
        match self
×
121
            .request_sender
×
122
            .call(NodeCommsRequest::FetchMatchingBlocks {
×
123
                range: height..=height,
×
124
                compact,
×
125
            })
×
126
            .await??
×
127
        {
128
            NodeCommsResponse::HistoricalBlocks(mut blocks) => Ok(blocks.pop()),
×
129
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
130
        }
131
    }
×
132

133
    /// Request the block headers with the given range of heights. The returned headers are ordered from lowest to
134
    /// highest block height
135
    pub async fn get_headers(&mut self, range: RangeInclusive<u64>) -> Result<Vec<ChainHeader>, CommsInterfaceError> {
×
136
        match self
×
137
            .request_sender
×
138
            .call(NodeCommsRequest::FetchHeaders(range))
×
139
            .await??
×
140
        {
141
            NodeCommsResponse::BlockHeaders(headers) => Ok(headers),
×
142
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
143
        }
144
    }
×
145

146
    /// Request the block header with the height.
147
    pub async fn get_header(&mut self, height: u64) -> Result<Option<ChainHeader>, CommsInterfaceError> {
×
148
        match self
×
149
            .request_sender
×
150
            .call(NodeCommsRequest::FetchHeaders(height..=height))
×
151
            .await??
×
152
        {
153
            NodeCommsResponse::BlockHeaders(mut headers) => Ok(headers.pop()),
×
154
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
155
        }
156
    }
×
157

158
    /// Request the construction of a new mineable block template from the base node service.
159
    pub async fn get_new_block_template(
3✔
160
        &mut self,
3✔
161
        pow_algorithm: PowAlgorithm,
3✔
162
        max_weight: u64,
3✔
163
    ) -> Result<NewBlockTemplate, CommsInterfaceError> {
3✔
164
        let request = GetNewBlockTemplateRequest {
3✔
165
            algo: pow_algorithm,
3✔
166
            max_weight,
3✔
167
        };
3✔
168
        match self
3✔
169
            .request_sender
3✔
170
            .call(NodeCommsRequest::GetNewBlockTemplate(request))
3✔
171
            .await??
3✔
172
        {
173
            NodeCommsResponse::NewBlockTemplate(new_block_template) => Ok(new_block_template),
3✔
174
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
175
        }
176
    }
3✔
177

178
    /// Request from base node service the construction of a block from a block template.
179
    pub async fn get_new_block(&mut self, block_template: NewBlockTemplate) -> Result<Block, CommsInterfaceError> {
3✔
180
        match self
3✔
181
            .request_sender
3✔
182
            .call(NodeCommsRequest::GetNewBlock(block_template))
3✔
183
            .await??
3✔
184
        {
185
            NodeCommsResponse::NewBlock { success, error, block } => {
3✔
186
                if success {
3✔
187
                    if let Some(block) = block {
3✔
188
                        Ok(block)
3✔
189
                    } else {
190
                        Err(CommsInterfaceError::UnexpectedApiResponse)
×
191
                    }
192
                } else {
193
                    Err(CommsInterfaceError::ApiError(
×
194
                        error.unwrap_or_else(|| "Unspecified error".to_string()),
×
195
                    ))
×
196
                }
197
            },
198
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
199
        }
200
    }
3✔
201

202
    /// Submit a block to the base node service.
203
    pub async fn submit_block(&mut self, block: Block) -> Result<BlockHash, CommsInterfaceError> {
12✔
204
        self.block_sender.call(block).await?
12✔
205
    }
12✔
206

207
    pub fn publish_block_event(&self, event: BlockEvent) -> usize {
×
208
        // If event send fails, that means that there are no receivers (i.e. it was sent to zero receivers)
×
209
        self.block_event_sender.send(Arc::new(event)).unwrap_or(0)
×
210
    }
×
211

212
    pub async fn fetch_matching_utxos(
×
213
        &mut self,
×
214
        hashes: Vec<HashOutput>,
×
215
    ) -> Result<Vec<TransactionOutput>, CommsInterfaceError> {
×
216
        match self
×
217
            .request_sender
×
218
            .call(NodeCommsRequest::FetchMatchingUtxos(hashes))
×
219
            .await??
×
220
        {
221
            NodeCommsResponse::TransactionOutputs(outputs) => Ok(outputs),
×
222
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
223
        }
224
    }
×
225

226
    /// Fetches the blocks with the specified utxo commitments
227
    pub async fn fetch_blocks_with_utxos(
×
228
        &mut self,
×
229
        commitments: Vec<CompressedCommitment>,
×
230
    ) -> Result<Vec<HistoricalBlock>, CommsInterfaceError> {
×
231
        match self
×
232
            .request_sender
×
233
            .call(NodeCommsRequest::FetchBlocksByUtxos(commitments))
×
234
            .await??
×
235
        {
236
            NodeCommsResponse::HistoricalBlocks(blocks) => Ok(blocks),
×
237
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
238
        }
239
    }
×
240

241
    /// Fetches the blocks with the specified kernel signatures commitments
242
    pub async fn get_blocks_with_kernels(
×
243
        &mut self,
×
244
        kernels: Vec<Signature>,
×
245
    ) -> Result<Vec<HistoricalBlock>, CommsInterfaceError> {
×
246
        match self
×
247
            .request_sender
×
248
            .call(NodeCommsRequest::FetchBlocksByKernelExcessSigs(kernels))
×
249
            .await??
×
250
        {
251
            NodeCommsResponse::HistoricalBlocks(blocks) => Ok(blocks),
×
252
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
253
        }
254
    }
×
255

256
    /// Return header matching the given hash. If the header cannot be found `Ok(None)` is returned.
257
    pub async fn get_header_by_hash(&mut self, hash: HashOutput) -> Result<Option<ChainHeader>, CommsInterfaceError> {
×
258
        match self
×
259
            .request_sender
×
260
            .call(NodeCommsRequest::GetHeaderByHash(hash))
×
261
            .await??
×
262
        {
263
            NodeCommsResponse::BlockHeader(header) => Ok(header),
×
264
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
265
        }
266
    }
×
267

268
    /// Return block matching the given hash. If the block cannot be found `Ok(None)` is returned.
269
    pub async fn get_block_by_hash(
×
270
        &mut self,
×
271
        hash: HashOutput,
×
272
    ) -> Result<Option<HistoricalBlock>, CommsInterfaceError> {
×
273
        match self
×
274
            .request_sender
×
275
            .call(NodeCommsRequest::GetBlockByHash(hash))
×
276
            .await??
×
277
        {
278
            NodeCommsResponse::HistoricalBlock(block) => Ok(*block),
×
279
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
280
        }
281
    }
×
282

283
    /// Searches for a kernel via the excess sig
284
    pub async fn get_kernel_by_excess_sig(
×
285
        &mut self,
×
286
        kernel: Signature,
×
287
    ) -> Result<Vec<TransactionKernel>, CommsInterfaceError> {
×
288
        match self
×
289
            .request_sender
×
290
            .call(NodeCommsRequest::FetchKernelByExcessSig(kernel))
×
291
            .await??
×
292
        {
293
            NodeCommsResponse::TransactionKernels(kernels) => Ok(kernels),
×
294
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
295
        }
296
    }
×
297

298
    pub async fn get_active_validator_nodes(
×
299
        &mut self,
×
300
        height: u64,
×
301
    ) -> Result<Vec<(CompressedPublicKey, [u8; 32])>, CommsInterfaceError> {
×
302
        match self
×
303
            .request_sender
×
304
            .call(NodeCommsRequest::FetchValidatorNodesKeys { height })
×
305
            .await??
×
306
        {
307
            NodeCommsResponse::FetchValidatorNodesKeysResponse(validator_node) => Ok(validator_node),
×
308
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
309
        }
310
    }
×
311

312
    pub async fn get_shard_key(
×
313
        &mut self,
×
314
        height: u64,
×
315
        public_key: CompressedPublicKey,
×
316
    ) -> Result<Option<[u8; 32]>, CommsInterfaceError> {
×
317
        match self
×
318
            .request_sender
×
319
            .call(NodeCommsRequest::GetShardKey { height, public_key })
×
320
            .await??
×
321
        {
322
            NodeCommsResponse::GetShardKeyResponse(shard_key) => Ok(shard_key),
×
323
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
324
        }
325
    }
×
326

327
    pub async fn get_template_registrations(
×
328
        &mut self,
×
329
        start_height: u64,
×
330
        end_height: u64,
×
331
    ) -> Result<Vec<TemplateRegistrationEntry>, CommsInterfaceError> {
×
332
        match self
×
333
            .request_sender
×
334
            .call(NodeCommsRequest::FetchTemplateRegistrations {
×
335
                start_height,
×
336
                end_height,
×
337
            })
×
338
            .await??
×
339
        {
340
            NodeCommsResponse::FetchTemplateRegistrationsResponse(template_registrations) => Ok(template_registrations),
×
341
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
342
        }
343
    }
×
344

345
    /// Fetches UTXOs that are not spent for the given block hash up to the current chain tip.
346
    pub async fn fetch_unspent_utxos_in_block(
×
347
        &mut self,
×
348
        block_hash: BlockHash,
×
349
    ) -> Result<Vec<TransactionOutput>, CommsInterfaceError> {
×
350
        match self
×
351
            .request_sender
×
352
            .call(NodeCommsRequest::FetchUnspentUtxosInBlock { block_hash })
×
353
            .await??
×
354
        {
355
            NodeCommsResponse::TransactionOutputs(outputs) => Ok(outputs),
×
356
            _ => Err(CommsInterfaceError::UnexpectedApiResponse),
×
357
        }
358
    }
×
359
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc