Skip to content

Commit 662a070

Browse files
committed
fix: prevent duplicate loaded contracts
1 parent 971129c commit 662a070

6 files changed

Lines changed: 154 additions & 80 deletions

File tree

src/context/contract_token_db.rs

Lines changed: 78 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,71 @@ use crate::model::wallet::WalletSeedHash;
55
use crate::ui::tokens::tokens_screen::{IdentityTokenBalance, IdentityTokenIdentifier};
66
use bincode::config;
77
use dash_sdk::dpp::data_contract::TokenConfiguration;
8+
use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters;
89
use dash_sdk::platform::{DataContract, Identifier};
910
use dash_sdk::query_types::IndexMap;
1011
use rusqlite::Result;
1112
use std::sync::Arc;
1213
use std::sync::atomic::Ordering;
1314

1415
impl AppContext {
16+
/// Returns borrowed references to the cached system-contract `Arc`s alongside their aliases.
17+
///
18+
/// Use this for read-only lookups (id checks, membership tests, `Arc` cloning) to avoid
19+
/// the deep `DataContract` clones that `system_contracts()` performs.
20+
pub(crate) fn system_contract_arcs(&self) -> [(&Arc<DataContract>, &'static str); 5] {
21+
[
22+
(&self.dpns_contract, "dpns"),
23+
(&self.token_history_contract, "token_history"),
24+
(&self.withdraws_contract, "withdrawals"),
25+
(&self.keyword_search_contract, "keyword_search"),
26+
(&self.dashpay_contract, "dashpay"),
27+
]
28+
}
29+
30+
pub(crate) fn system_contracts(&self) -> [QualifiedContract; 5] {
31+
self.system_contract_arcs()
32+
.map(|(contract, alias)| QualifiedContract {
33+
contract: contract.as_ref().clone(),
34+
alias: Some(alias.to_string()),
35+
})
36+
}
37+
38+
pub(crate) fn system_contract_ids(&self) -> [Identifier; 5] {
39+
self.system_contract_arcs()
40+
.map(|(contract, _alias)| contract.id())
41+
}
42+
43+
pub(crate) fn system_contract_by_id(
44+
&self,
45+
contract_id: &Identifier,
46+
) -> Option<QualifiedContract> {
47+
self.system_contract_arcs()
48+
.into_iter()
49+
.find(|(contract, _alias)| contract.id() == *contract_id)
50+
.map(|(contract, alias)| QualifiedContract {
51+
contract: contract.as_ref().clone(),
52+
alias: Some(alias.to_string()),
53+
})
54+
}
55+
56+
/// Returns an `Arc::clone` of a cached system contract by id, without deep cloning.
57+
pub(crate) fn system_contract_arc_by_id(
58+
&self,
59+
contract_id: &Identifier,
60+
) -> Option<Arc<DataContract>> {
61+
self.system_contract_arcs()
62+
.into_iter()
63+
.find(|(contract, _alias)| contract.id() == *contract_id)
64+
.map(|(contract, _alias)| Arc::clone(contract))
65+
}
66+
67+
pub(crate) fn is_system_contract_id(&self, contract_id: &Identifier) -> bool {
68+
self.system_contract_arcs()
69+
.iter()
70+
.any(|(contract, _alias)| contract.id() == *contract_id)
71+
}
72+
1573
/// Retrieves all contracts from the database plus the system contracts from app context.
1674
pub fn get_contracts(
1775
&self,
@@ -21,50 +79,9 @@ impl AppContext {
2179
// Get contracts from the database
2280
let mut contracts = self.db.get_contracts(self, limit, offset)?;
2381

24-
// Add the DPNS contract to the list
25-
let dpns_contract = QualifiedContract {
26-
contract: Arc::clone(&self.dpns_contract).as_ref().clone(),
27-
alias: Some("dpns".to_string()),
28-
};
29-
30-
// Insert the DPNS contract at 0
31-
contracts.insert(0, dpns_contract);
32-
33-
// Add the token history contract to the list
34-
let token_history_contract = QualifiedContract {
35-
contract: Arc::clone(&self.token_history_contract).as_ref().clone(),
36-
alias: Some("token_history".to_string()),
37-
};
38-
39-
// Insert the token history contract at 1
40-
contracts.insert(1, token_history_contract);
41-
42-
// Add the withdrawal contract to the list
43-
let withdraws_contract = QualifiedContract {
44-
contract: Arc::clone(&self.withdraws_contract).as_ref().clone(),
45-
alias: Some("withdrawals".to_string()),
46-
};
47-
48-
// Insert the withdrawal contract at 2
49-
contracts.insert(2, withdraws_contract);
50-
51-
// Add the keyword search contract to the list
52-
let keyword_search_contract = QualifiedContract {
53-
contract: Arc::clone(&self.keyword_search_contract).as_ref().clone(),
54-
alias: Some("keyword_search".to_string()),
55-
};
56-
57-
// Insert the keyword search contract at 3
58-
contracts.insert(3, keyword_search_contract);
59-
60-
// Add the DashPay contract to the list
61-
let dashpay_contract = QualifiedContract {
62-
contract: Arc::clone(&self.dashpay_contract).as_ref().clone(),
63-
alias: Some("dashpay".to_string()),
64-
};
65-
66-
// Insert the DashPay contract at 4
67-
contracts.insert(4, dashpay_contract);
82+
for (index, system_contract) in self.system_contracts().into_iter().enumerate() {
83+
contracts.insert(index, system_contract);
84+
}
6885

6986
Ok(contracts)
7087
}
@@ -73,6 +90,10 @@ impl AppContext {
7390
&self,
7491
contract_id: &Identifier,
7592
) -> Result<Option<QualifiedContract>> {
93+
if let Some(system_contract) = self.system_contract_by_id(contract_id) {
94+
return Ok(Some(system_contract));
95+
}
96+
7697
// Get the contract from the database
7798
self.db.get_contract_by_id(*contract_id, self)
7899
}
@@ -81,12 +102,20 @@ impl AppContext {
81102
&self,
82103
contract_id: &Identifier,
83104
) -> Result<Option<DataContract>> {
105+
if let Some(system_contract) = self.system_contract_by_id(contract_id) {
106+
return Ok(Some(system_contract.contract));
107+
}
108+
84109
// Get the contract from the database
85110
self.db.get_unqualified_contract_by_id(*contract_id, self)
86111
}
87112

88113
// Remove contract from the database by ID
89114
pub fn remove_contract(&self, contract_id: &Identifier) -> Result<()> {
115+
if self.is_system_contract_id(contract_id) {
116+
return Ok(());
117+
}
118+
90119
self.db.remove_contract(contract_id.as_bytes(), self)
91120
}
92121

@@ -95,6 +124,10 @@ impl AppContext {
95124
contract_id: Identifier,
96125
new_contract: &DataContract,
97126
) -> Result<()> {
127+
if self.is_system_contract_id(&contract_id) {
128+
return Ok(());
129+
}
130+
98131
self.db.replace_contract(contract_id, new_contract, self)
99132
}
100133

@@ -183,6 +216,6 @@ impl AppContext {
183216
let Some(contract_id) = self.db.get_contract_id_by_token_id(token_id, self)? else {
184217
return Ok(None);
185218
};
186-
self.db.get_contract_by_id(contract_id, self)
219+
self.get_contract_by_id(&contract_id)
187220
}
188221
}

src/context_provider.rs

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use crate::context::AppContext;
44
use crate::database::Database;
55
use dash_sdk::core::LowLevelDashCoreClient as CoreClient;
66
use dash_sdk::dpp::dashcore::Network;
7-
use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters;
87
use dash_sdk::dpp::version::PlatformVersion;
98
use dash_sdk::error::ContextProviderError;
109
use dash_sdk::platform::{ContextProvider, DataContract, Identifier};
@@ -15,36 +14,14 @@ use std::sync::{Arc, Mutex};
1514
// Shared contract/token resolution used by both RPC and SPV providers.
1615
// ---------------------------------------------------------------------------
1716

18-
/// Number of system contracts cached on [`AppContext`].
19-
/// Update this when adding a new system contract field.
20-
///
21-
/// The typed array size in [`resolve_data_contract`] must match — the compiler
22-
/// will reject a mismatch, catching forgotten additions at build time.
23-
pub(crate) const SYSTEM_CONTRACT_COUNT: usize = 5;
24-
2517
/// Resolve a data contract by ID: check cached system contracts first, then DB.
26-
///
27-
/// All system contracts are listed in `cached` — adding a new one is a single
28-
/// array edit, which prevents the two providers from drifting out of sync.
29-
/// The array size is tied to [`SYSTEM_CONTRACT_COUNT`] so the compiler enforces
30-
/// completeness.
3118
pub(crate) fn resolve_data_contract(
3219
app_ctx: &AppContext,
3320
db: &Database,
3421
data_contract_id: &Identifier,
3522
) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
36-
let cached: [&Arc<DataContract>; SYSTEM_CONTRACT_COUNT] = [
37-
&app_ctx.dpns_contract,
38-
&app_ctx.dashpay_contract,
39-
&app_ctx.token_history_contract,
40-
&app_ctx.withdraws_contract,
41-
&app_ctx.keyword_search_contract,
42-
];
43-
44-
for contract in &cached {
45-
if data_contract_id == &contract.id() {
46-
return Ok(Some(Arc::clone(contract)));
47-
}
23+
if let Some(system_contract) = app_ctx.system_contract_arc_by_id(data_contract_id) {
24+
return Ok(Some(system_contract));
4825
}
4926

5027
// DB fallback for user-added / non-system contracts

src/database/contracts.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ impl Database {
3131
insert_tokens_too: InsertTokensToo,
3232
app_context: &AppContext,
3333
) -> Result<()> {
34+
if app_context.is_system_contract_id(&data_contract.id()) {
35+
return Ok(());
36+
}
37+
3438
// Serialize the contract
3539
let contract_bytes = data_contract
3640
.serialize_to_bytes_with_platform_version(app_context.platform_version())
@@ -260,8 +264,15 @@ impl Database {
260264
) -> Result<Vec<QualifiedContract>> {
261265
let network = app_context.network.to_string();
262266

267+
let excluded_contract_ids = app_context
268+
.system_contract_ids()
269+
.map(|contract_id| contract_id.to_vec());
270+
let excluded_contract_placeholders = vec!["?"; excluded_contract_ids.len()].join(", ");
271+
263272
// Build the SQL query with optional limit and offset
264-
let mut query = String::from("SELECT contract, alias FROM contract WHERE network = ?");
273+
let mut query = format!(
274+
"SELECT contract, alias FROM contract WHERE network = ? AND contract_id NOT IN ({excluded_contract_placeholders})"
275+
);
265276
if limit.is_some() {
266277
query.push_str(" LIMIT ?");
267278
}
@@ -278,6 +289,9 @@ impl Database {
278289

279290
// Collect parameters for query execution
280291
let mut params: Vec<&dyn rusqlite::ToSql> = vec![&network];
292+
for contract_id in &excluded_contract_ids {
293+
params.push(contract_id);
294+
}
281295
if let Some(l) = limit {
282296
limit_value = l;
283297
params.push(&limit_value); // Now `limit_value` lives long enough

src/ui/components/contract_chooser_panel.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -516,10 +516,7 @@ pub fn add_contract_chooser_panel(
516516
// Right‐aligned Remove button
517517
ui.horizontal(|ui| {
518518
ui.add_space(8.0);
519-
if contract.alias != Some("dpns".to_string())
520-
&& contract.alias != Some("token_history".to_string())
521-
&& contract.alias != Some("withdrawals".to_string())
522-
&& contract.alias != Some("keyword_search".to_string())
519+
if !app_context.is_system_contract_id(&contract.contract.id())
523520
&& ui.add(
524521
egui::Button::new("Remove")
525522
.min_size(egui::Vec2::new(60.0, 20.0))

src/ui/contracts_documents/add_contracts_screen.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,63 @@ impl AddContractsScreen {
8080
fn add_contracts_clicked(&mut self) -> AppAction {
8181
match self.parse_identifiers() {
8282
Ok(identifiers) => {
83+
let duplicate_input =
84+
identifiers
85+
.iter()
86+
.enumerate()
87+
.find_map(|(index, identifier)| {
88+
identifiers[..index]
89+
.contains(identifier)
90+
.then_some(identifier)
91+
});
92+
if let Some(identifier) = duplicate_input {
93+
let message = format!(
94+
"Contract {} was entered more than once. Remove the duplicate entry before adding contracts.",
95+
identifier.to_string(Encoding::Base58)
96+
);
97+
self.add_contracts_status = AddContractsStatus::Error;
98+
MessageBanner::set_global(
99+
self.app_context.egui_ctx(),
100+
&message,
101+
MessageType::Error,
102+
);
103+
return AppAction::None;
104+
}
105+
106+
let existing_contract_ids = match self.app_context.get_contracts(None, None) {
107+
Ok(contracts) => contracts
108+
.into_iter()
109+
.map(|contract| contract.contract.id())
110+
.collect::<Vec<_>>(),
111+
Err(e) => {
112+
tracing::warn!("Failed to check loaded contracts before adding: {e}");
113+
self.add_contracts_status = AddContractsStatus::Error;
114+
MessageBanner::set_global(
115+
self.app_context.egui_ctx(),
116+
"Unable to check whether those contracts are already loaded. Please try again.",
117+
MessageType::Error,
118+
);
119+
return AppAction::None;
120+
}
121+
};
122+
123+
let already_loaded = identifiers
124+
.iter()
125+
.find(|identifier| existing_contract_ids.contains(identifier));
126+
if let Some(identifier) = already_loaded {
127+
let message = format!(
128+
"Contract {} is already loaded. Select it from the existing contracts list or enter a different contract ID.",
129+
identifier.to_string(Encoding::Base58)
130+
);
131+
self.add_contracts_status = AddContractsStatus::Error;
132+
MessageBanner::set_global(
133+
self.app_context.egui_ctx(),
134+
&message,
135+
MessageType::Error,
136+
);
137+
return AppAction::None;
138+
}
139+
83140
self.add_banner.take_and_clear();
84141
let handle = MessageBanner::set_global(
85142
self.app_context.egui_ctx(),

src/ui/contracts_documents/update_contract_screen.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,11 @@ impl UpdateDataContractScreen {
7777
None
7878
};
7979

80-
let excluded_aliases = ["dpns", "keyword_search", "token_history", "withdrawals"];
8180
let known_contracts = app_context
8281
.get_contracts(None, None)
8382
.expect("Failed to load contracts")
8483
.into_iter()
85-
.filter(|c| match &c.alias {
86-
Some(alias) => !excluded_aliases.contains(&alias.as_str()),
87-
None => true,
88-
})
84+
.filter(|contract| !app_context.is_system_contract_id(&contract.contract.id()))
8985
.collect::<Vec<_>>();
9086

9187
let selected_key = selected_qualified_identity.as_ref().and_then(|identity| {

0 commit comments

Comments
 (0)