Skip to content

Commit 33ba2f6

Browse files
feat: wire masternode engine methods to MasternodeListEngine
- Add `masternode_engine()` async accessor on `DashSpvClient` that returns `Option<Arc<RwLock<MasternodeListEngine>>>` (clone of field) - Implement `SpvClient::get_masternode_count()`: reads the latest masternode list from the engine and returns the entry count; returns 0 when masternodes are disabled or no list received yet - Implement `SpvClient::get_masternodes()`: iterates the latest masternode list and maps `MasternodeListEntry` fields to `MasternodeInfo` (pro_reg_tx_hash → pro_tx_hash, service_address → address, is_valid → "Enabled"/"PoSeBanned"); pose_penalty/last_paid_height/ registered_height default to 0 as they are not in the SML diff - Replace stub bridge tests with no-engine and empty-engine variants - Add DashSpvClient-level tests for masternode_engine() accessor and a populated-engine scenario using a manually constructed MasternodeList Closes #29 Co-authored-by: Kevin Rombach <xdustinface@users.noreply.github.com>
1 parent 0a29c2a commit 33ba2f6

3 files changed

Lines changed: 222 additions & 14 deletions

File tree

dash-spv/src/bridge/mod.rs

Lines changed: 98 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -610,20 +610,65 @@ pub struct GovernanceProposal {
610610
impl SpvClient {
611611
/// Returns the number of masternodes in the current masternode list.
612612
///
613-
/// TODO: wire up to `MasternodeListEngine` once the engine is accessible
614-
/// from `DashSpvClient`.
613+
/// Reads the latest masternode list from the engine and returns its entry
614+
/// count. Returns `0` when masternodes are disabled or no list has been
615+
/// received yet.
615616
pub async fn get_masternode_count(&self) -> u32 {
616-
// TODO: return self.inner.masternode_list_engine()?.read().await.count()
617-
0
617+
let Some(engine) = self.inner.masternode_engine().await else {
618+
return 0;
619+
};
620+
let guard = engine.read().await;
621+
guard
622+
.latest_masternode_list()
623+
.map(|list| list.masternodes.len() as u32)
624+
.unwrap_or(0)
618625
}
619626

620627
/// Returns all masternodes from the current masternode list.
621628
///
622-
/// TODO: wire up to `MasternodeListEngine` once the engine is accessible
623-
/// from `DashSpvClient`.
629+
/// Iterates the latest masternode list from the engine and maps each
630+
/// [`dashcore::sml::masternode_list_entry::MasternodeListEntry`] to a
631+
/// [`MasternodeInfo`] record. Returns an empty `Vec` when masternodes are
632+
/// disabled or no list has been received yet.
633+
///
634+
/// # Field mapping
635+
///
636+
/// | `MasternodeListEntry` field | `MasternodeInfo` field |
637+
/// |---|---|
638+
/// | `pro_reg_tx_hash` | `pro_tx_hash` |
639+
/// | `service_address` | `address` |
640+
/// | `is_valid` | `status` (`"Enabled"` / `"PoSeBanned"`) |
641+
/// | — | `pose_penalty` (always `0`; not in SML diff) |
642+
/// | — | `last_paid_height` (always `0`; not in SML diff) |
643+
/// | — | `registered_height` (always `0`; not in SML diff) |
624644
pub async fn get_masternodes(&self) -> Vec<MasternodeInfo> {
625-
// TODO: map MasternodeListEntry fields to MasternodeInfo
626-
vec![]
645+
let Some(engine) = self.inner.masternode_engine().await else {
646+
return vec![];
647+
};
648+
let guard = engine.read().await;
649+
let Some(list) = guard.latest_masternode_list() else {
650+
return vec![];
651+
};
652+
list.masternodes
653+
.values()
654+
.map(|entry| {
655+
let mn = &entry.masternode_list_entry;
656+
MasternodeInfo {
657+
pro_tx_hash: mn.pro_reg_tx_hash.to_string(),
658+
address: mn.service_address.to_string(),
659+
status: if mn.is_valid {
660+
"Enabled".to_string()
661+
} else {
662+
"PoSeBanned".to_string()
663+
},
664+
// The SML diff does not carry PoSe penalty, last-paid height, or
665+
// registered height — default to 0 until richer data sources are wired up.
666+
pose_penalty: 0,
667+
last_paid_height: 0,
668+
registered_height: 0,
669+
}
670+
})
671+
.collect()
627672
}
628673
}
629674

@@ -1237,28 +1282,68 @@ mod tests {
12371282
assert_eq!(proposal.abstain_count, 1);
12381283
}
12391284

1285+
/// `get_masternode_count` returns 0 when masternodes are disabled (no engine).
12401286
#[tokio::test]
1241-
async fn test_get_masternode_count_stub() {
1287+
async fn test_get_masternode_count_no_engine() {
12421288
let temp_dir = TempDir::new().expect("Failed to create temp dir");
12431289
let config = ClientConfig::regtest()
12441290
.without_filters()
12451291
.without_masternodes()
12461292
.with_storage_path(temp_dir.path());
12471293

12481294
let client = SpvClient::new(config).await.expect("SpvClient construction must succeed");
1249-
assert_eq!(client.get_masternode_count().await, 0, "stub should return 0");
1295+
assert_eq!(
1296+
client.get_masternode_count().await,
1297+
0,
1298+
"should return 0 when engine is None"
1299+
);
12501300
}
12511301

1302+
/// `get_masternodes` returns an empty vec when masternodes are disabled (no engine).
12521303
#[tokio::test]
1253-
async fn test_get_masternodes_stub() {
1304+
async fn test_get_masternodes_no_engine() {
12541305
let temp_dir = TempDir::new().expect("Failed to create temp dir");
12551306
let config = ClientConfig::regtest()
12561307
.without_filters()
12571308
.without_masternodes()
12581309
.with_storage_path(temp_dir.path());
12591310

12601311
let client = SpvClient::new(config).await.expect("SpvClient construction must succeed");
1261-
let masternodes = client.get_masternodes().await;
1262-
assert!(masternodes.is_empty(), "stub should return empty vec");
1312+
assert!(
1313+
client.get_masternodes().await.is_empty(),
1314+
"should return empty vec when engine is None"
1315+
);
1316+
}
1317+
1318+
/// `get_masternode_count` returns 0 when masternodes are enabled but no list
1319+
/// has been received yet (engine is Some but empty).
1320+
#[tokio::test]
1321+
async fn test_get_masternode_count_empty_engine() {
1322+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
1323+
// Default regtest config has enable_masternodes = true
1324+
let config =
1325+
ClientConfig::regtest().without_filters().with_storage_path(temp_dir.path());
1326+
1327+
let client = SpvClient::new(config).await.expect("SpvClient construction must succeed");
1328+
assert_eq!(
1329+
client.get_masternode_count().await,
1330+
0,
1331+
"should return 0 when engine has no list yet"
1332+
);
1333+
}
1334+
1335+
/// `get_masternodes` returns an empty vec when masternodes are enabled but no
1336+
/// list has been received yet (engine is Some but empty).
1337+
#[tokio::test]
1338+
async fn test_get_masternodes_empty_engine() {
1339+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
1340+
let config =
1341+
ClientConfig::regtest().without_filters().with_storage_path(temp_dir.path());
1342+
1343+
let client = SpvClient::new(config).await.expect("SpvClient construction must succeed");
1344+
assert!(
1345+
client.get_masternodes().await.is_empty(),
1346+
"should return empty vec when engine has no list yet"
1347+
);
12631348
}
12641349
}

dash-spv/src/client/mod.rs

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,19 @@ mod tests {
4949
use crate::client::config::MempoolStrategy;
5050
use crate::storage::DiskStorageManager;
5151
use crate::{test_utils::MockNetworkManager, types::UnconfirmedTransaction};
52-
use dashcore::{Address, Amount, Transaction, TxOut};
52+
use dashcore::sml::masternode_list::MasternodeList;
53+
use dashcore::sml::masternode_list_entry::{
54+
EntryMasternodeType, MasternodeListEntry,
55+
qualified_masternode_list_entry::QualifiedMasternodeListEntry,
56+
};
57+
use dashcore::sml::masternode_list_engine::MasternodeListEngine;
58+
use dashcore::{Address, Amount, BlockHash, ProTxHash, Transaction, TxOut};
59+
use dashcore_hashes::Hash;
5360
use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo;
5461
use key_wallet_manager::wallet_manager::WalletManager;
62+
use std::collections::BTreeMap;
63+
use std::net::SocketAddr;
64+
use std::str::FromStr;
5565
use std::sync::Arc;
5666
use tempfile::TempDir;
5767
use tokio::sync::RwLock;
@@ -170,4 +180,112 @@ mod tests {
170180
"InstantSend balance should be 10 Dash"
171181
);
172182
}
183+
184+
// ============ masternode_engine() accessor tests ============
185+
186+
/// `masternode_engine()` returns `None` when masternodes are disabled.
187+
#[tokio::test]
188+
async fn test_masternode_engine_none_when_disabled() {
189+
let config = ClientConfig::testnet()
190+
.without_filters()
191+
.without_masternodes()
192+
.with_storage_path(TempDir::new().unwrap().path());
193+
194+
let network_manager = MockNetworkManager::new();
195+
let storage = DiskStorageManager::new(&config).await.expect("storage");
196+
let wallet = Arc::new(RwLock::new(WalletManager::<ManagedWalletInfo>::new(config.network)));
197+
198+
let client = DashSpvClient::new(config, network_manager, storage, wallet)
199+
.await
200+
.expect("client construction must succeed");
201+
202+
assert!(
203+
client.masternode_engine().await.is_none(),
204+
"masternode_engine() should be None when masternodes are disabled"
205+
);
206+
}
207+
208+
/// `masternode_engine()` returns `Some` when masternodes are enabled.
209+
#[tokio::test]
210+
async fn test_masternode_engine_some_when_enabled() {
211+
let config = ClientConfig::testnet()
212+
.without_filters()
213+
.with_storage_path(TempDir::new().unwrap().path());
214+
215+
let network_manager = MockNetworkManager::new();
216+
let storage = DiskStorageManager::new(&config).await.expect("storage");
217+
let wallet = Arc::new(RwLock::new(WalletManager::<ManagedWalletInfo>::new(config.network)));
218+
219+
let client = DashSpvClient::new(config, network_manager, storage, wallet)
220+
.await
221+
.expect("client construction must succeed");
222+
223+
assert!(
224+
client.masternode_engine().await.is_some(),
225+
"masternode_engine() should be Some when masternodes are enabled"
226+
);
227+
}
228+
229+
/// Verifies that after manually inserting a masternode list into the engine
230+
/// the count and entries are visible via the engine handle.
231+
#[tokio::test]
232+
async fn test_masternode_engine_populated_reflects_entries() {
233+
let config = ClientConfig::testnet()
234+
.without_filters()
235+
.with_storage_path(TempDir::new().unwrap().path());
236+
let network = config.network;
237+
238+
let network_manager = MockNetworkManager::new();
239+
let storage = DiskStorageManager::new(&config).await.expect("storage");
240+
let wallet = Arc::new(RwLock::new(WalletManager::<ManagedWalletInfo>::new(network)));
241+
242+
let client = DashSpvClient::new(config, network_manager, storage, wallet)
243+
.await
244+
.expect("client construction must succeed");
245+
246+
let engine_arc = client
247+
.masternode_engine()
248+
.await
249+
.expect("engine should be Some");
250+
251+
// Build a minimal MasternodeListEntry for testing.
252+
let pro_reg_tx_hash = ProTxHash::all_zeros();
253+
let entry = MasternodeListEntry {
254+
version: 1,
255+
pro_reg_tx_hash,
256+
confirmed_hash: None,
257+
service_address: SocketAddr::from_str("1.2.3.4:9999").unwrap(),
258+
operator_public_key: dashcore::bls_sig_utils::BLSPublicKey::from([0u8; 48]),
259+
key_id_voting: dashcore::PubkeyHash::all_zeros(),
260+
is_valid: true,
261+
mn_type: EntryMasternodeType::Regular,
262+
};
263+
let qualified: QualifiedMasternodeListEntry = entry.into();
264+
265+
// Insert the entry into a MasternodeList and load it into the engine.
266+
let block_hash = BlockHash::all_zeros();
267+
let masternodes = BTreeMap::from([(pro_reg_tx_hash, qualified)]);
268+
let list = MasternodeList::build(masternodes, BTreeMap::new(), block_hash, 100).build();
269+
270+
{
271+
let mut engine = engine_arc.write().await;
272+
engine.masternode_lists.insert(100, list);
273+
}
274+
275+
// Now read back via the accessor.
276+
let engine_arc2 = client
277+
.masternode_engine()
278+
.await
279+
.expect("engine should still be Some");
280+
let engine = engine_arc2.read().await;
281+
let latest = engine.latest_masternode_list().expect("list should be present");
282+
283+
assert_eq!(latest.masternodes.len(), 1, "should have 1 masternode");
284+
let mn = latest.masternodes.values().next().unwrap();
285+
assert!(mn.masternode_list_entry.is_valid, "masternode should be valid (Enabled)");
286+
assert_eq!(
287+
mn.masternode_list_entry.service_address.to_string(),
288+
"1.2.3.4:9999"
289+
);
290+
}
173291
}

dash-spv/src/client/queries.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,
5252
}
5353
}
5454

55+
/// Returns a clone of the masternode engine handle, or `None` if masternodes are disabled.
56+
pub async fn masternode_engine(&self) -> Option<Arc<RwLock<MasternodeListEngine>>> {
57+
self.masternode_engine.clone()
58+
}
59+
5560
/// Get a quorum entry by type and hash at a specific block height.
5661
/// Returns `SpvError::QuorumLookupError` if the quorum is not found.
5762
pub async fn get_quorum_at_height(

0 commit comments

Comments
 (0)