-
Notifications
You must be signed in to change notification settings - Fork 57
Expand file tree
/
Copy pathwallet.rs
More file actions
386 lines (360 loc) · 15.9 KB
/
Copy pathwallet.rs
File metadata and controls
386 lines (360 loc) · 15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
//! Platform address wallet for DIP-17 platform payment addresses.
use std::sync::Arc;
use dpp::address_funds::PlatformAddress;
use dpp::fee::Credits;
use tokio::sync::RwLock;
use crate::broadcaster::SpvBroadcaster;
use crate::error::PlatformWalletError;
use crate::wallet::asset_lock::manager::AssetLockManager;
use crate::wallet::platform_wallet::{PlatformWalletInfo, WalletId};
use key_wallet_manager::WalletManager;
use crate::wallet::persister::WalletPersister;
use super::provider::PlatformPaymentAddressProvider;
/// Platform address wallet providing DIP-17 platform payment address functionality.
#[derive(Clone)]
pub struct PlatformAddressWallet {
pub(crate) sdk: Arc<dash_sdk::Sdk>,
/// The shared wallet manager lock for all mutable wallet state.
pub(crate) wallet_manager: Arc<RwLock<WalletManager<PlatformWalletInfo>>>,
/// Identifies which wallet within the manager this sub-wallet operates on.
pub(crate) wallet_id: WalletId,
/// Single provider covering every platform payment account on the
/// wallet. `None` until [`initialize`] runs so that no-account
/// wallets don't allocate empty state. Sync takes a `write` lock;
/// transfer/withdraw paths take `read` for key_source lookups.
pub(crate) provider: Arc<RwLock<Option<PlatformPaymentAddressProvider>>>,
/// Shared asset-lock manager. Threaded in so the orchestrated
/// `fund_from_asset_lock` path can drive
/// build → IS-or-CL wait → consume on the same tracked locks
/// every other sub-wallet sees. Cloned `Arc`, not owned.
pub(crate) asset_locks: Arc<AssetLockManager<SpvBroadcaster>>,
/// Per-wallet persistence handle for queuing changesets.
pub(crate) persister: WalletPersister,
}
impl PlatformAddressWallet {
/// Create a new PlatformAddressWallet without initializing the provider.
///
/// Call [`initialize`] afterwards to build the unified provider.
pub(crate) fn new(
sdk: Arc<dash_sdk::Sdk>,
wallet_manager: Arc<RwLock<WalletManager<PlatformWalletInfo>>>,
wallet_id: WalletId,
asset_locks: Arc<AssetLockManager<SpvBroadcaster>>,
persister: WalletPersister,
) -> Self {
Self {
sdk,
wallet_manager,
wallet_id,
provider: Arc::new(RwLock::new(None)),
asset_locks,
persister,
}
}
/// Build (or rebuild) the unified address provider covering every
/// platform payment account on the wallet.
///
/// Safe to call multiple times — later invocations re-scan the
/// current account set from the wallet manager, picking up any
/// accounts added since the last call. Sync state (watermark,
/// `found`, `known_balances`) is **not** preserved across a
/// rebuild; callers that need to preserve it should use
/// [`restore_sync_state`] on the fresh provider.
pub async fn initialize(&self) {
match PlatformPaymentAddressProvider::from_wallets(
Arc::clone(&self.wallet_manager),
[self.wallet_id],
)
.await
{
Ok(provider) => {
let mut guard = self.provider.write().await;
*guard = Some(provider);
}
Err(e) => {
tracing::warn!(
"Failed to create platform address provider for wallet {}: {}",
hex::encode(self.wallet_id),
e
);
}
}
}
/// Rebuild the provider from persisted state. Used on startup
/// when a persister returned a non-empty
/// [`PlatformAddressSyncStartState`](crate::PlatformAddressSyncStartState)
/// — delegates to
/// [`PlatformPaymentAddressProvider::from_persisted`] so xpubs,
/// `found`, and `absent` are restored verbatim while `addresses`
/// and `pending` are rebuilt from the live `AddressPool`.
///
/// Also pushes each persisted balance back onto the matching
/// `ManagedPlatformAccount` via `set_address_credit_balance` so
/// the transfer/withdrawal `auto_select_inputs` paths see a
/// non-zero balance immediately after restore — without this,
/// they'd report "available 0 credits" until a fresh BLAST sync
/// round fired `on_address_found` for every known address.
/// Mirrors the `set_address_credit_balance(.., None)` shape in
/// [apply.rs](crate::wallet::apply): `None` for the key-source
/// argument because the gap-limit pool is already restored from
/// `account_state.addresses` inside `from_persisted`.
// TODO(CMT-004): no direct regression test for balance hydration via
// initialize_from_persisted; future refactor could silently regress
// restart visibility.
pub async fn initialize_from_persisted(
&self,
persisted: crate::PlatformAddressSyncStartState,
) -> Result<(), PlatformWalletError> {
// Hydrate `account.address_credit_balance` BEFORE constructing
// the provider. `from_persisted` holds a read lock on
// `wallet_manager` for its duration, and Tokio's `RwLock` has
// no read→write upgrade — doing the write-lock dance first
// keeps both paths simple and avoids exposing a new public
// accessor on the provider.
//
// Required by spend paths that enumerate funded addresses
// (e.g. `shielded_shield_from_account`): without this, after
// a restart they read `available = 0` until the first BLAST
// sync repopulates the in-memory map, even though SwiftData
// reports a real balance to the UI.
{
let mut wm = self.wallet_manager.write().await;
if let Some(info) = wm.get_wallet_info_mut(&self.wallet_id) {
for (account_index, account_state) in &persisted.per_account {
if let Some(account) = info
.core_wallet
.platform_payment_managed_account_at_index_mut(*account_index)
{
for (p2pkh, funds) in account_state.found() {
account.set_address_credit_balance(*p2pkh, funds.balance, None);
}
}
}
}
}
let mut per_wallet = std::collections::BTreeMap::new();
per_wallet.insert(self.wallet_id, persisted.per_account);
let provider = PlatformPaymentAddressProvider::from_persisted(
Arc::clone(&self.wallet_manager),
per_wallet,
persisted.sync_height,
persisted.sync_timestamp,
persisted.last_known_recent_block,
)
.await?;
let mut guard = self.provider.write().await;
*guard = Some(provider);
Ok(())
}
/// Get the network from the SDK.
pub fn network(&self) -> key_wallet::Network {
self.sdk.network
}
/// Wallet id this `PlatformAddressWallet` operates on. Exposed so
/// FFI callers that build a `MnemonicResolverCoreSigner` on demand
/// can thread the wallet id through to the resolver callback.
/// Mirrors [`AssetLockManager::wallet_id`].
pub fn wallet_id(&self) -> WalletId {
self.wallet_id
}
/// Rebuild the provider so it covers a newly added account.
///
/// Equivalent to [`initialize`]: the unified provider is rebuilt
/// from the current account set in the wallet manager. The name
/// is kept for API continuity with call sites that used to add
/// per-account providers.
pub async fn add_provider(&self, _account_index: u32) -> Result<(), PlatformWalletError> {
self.initialize().await;
Ok(())
}
/// Restore the incremental-sync watermark on the unified provider.
///
/// Called during persisted-state replay so the next `sync_balances`
/// call resumes from where the previous session left off instead of
/// doing a full rescan. Zero-valued arguments are ignored (they mean
/// "no stored watermark" — the provider keeps its fresh-start state).
pub(crate) async fn apply_sync_state(
&self,
height: Option<u64>,
timestamp: Option<u64>,
last_known_recent_block: Option<u64>,
) {
if height.is_none() && timestamp.is_none() && last_known_recent_block.is_none() {
return;
}
let h = height.unwrap_or(0);
let t = timestamp.unwrap_or(0);
let r = last_known_recent_block.unwrap_or(0);
let mut guard = self.provider.write().await;
if let Some(provider) = guard.as_mut() {
provider.set_stored_sync_state(h, t, r);
}
}
/// Restore sync state from externally persisted values (e.g., SwiftData).
///
/// Call this after `initialize()` and before the first sync to resume
/// incremental mode instead of doing a full trunk/branch/compact rescan.
pub async fn restore_sync_state(
&self,
sync_height: u64,
sync_timestamp: u64,
last_known_recent_block: u64,
) {
self.apply_sync_state(
Some(sync_height),
Some(sync_timestamp),
Some(last_known_recent_block),
)
.await;
}
/// Reset the platform-address sync watermark and drop every cached
/// balance for this wallet, forcing a full trunk/branch/compact
/// rescan on the next `sync_balances`.
///
/// Backs the host's "Clear" flow. Clears BOTH in-memory balance
/// stores a resume would otherwise read from:
/// * the provider's incremental seed (`found`) + watermark — what
/// makes a resync "fast" (see
/// [`PlatformPaymentAddressProvider::reset_sync_state`]);
/// * each `ManagedPlatformAccount`'s `address_balances` map — what
/// [`addresses_with_balances`](Self::addresses_with_balances) /
/// `total_credits` and the transfer/withdraw spend paths read.
/// Without this the UI/spend paths would keep reporting stale
/// balances until the next full sync re-zeroed them via the
/// absent diff.
///
/// Does NOT route through [`apply_sync_state`] — that helper's
/// all-None early-return guard is meant for persisted-state replay
/// and is irrelevant here. The two locks are taken sequentially
/// (one released before the next is acquired), so there is no
/// nested-lock hazard; this mirrors the ordering rationale in
/// [`initialize_from_persisted`].
pub async fn reset_sync_state(&self) {
{
let mut wm = self.wallet_manager.write().await;
if let Some(info) = wm.get_wallet_info_mut(&self.wallet_id) {
for account in info.core_wallet.all_platform_payment_managed_accounts_mut() {
account.clear_balances();
}
}
}
let mut guard = self.provider.write().await;
if let Some(provider) = guard.as_mut() {
provider.reset_sync_state();
}
}
/// Internal accessor for the diagnostic snapshot path on
/// [`crate::manager::PlatformWalletManager`]. The provider lock is
/// otherwise crate-private — the manager-level snapshot needs to
/// `blocking_read` it, which requires re-exposing the `Arc`.
pub(crate) fn provider_for_diagnostics(
&self,
) -> Arc<RwLock<Option<super::provider::PlatformPaymentAddressProvider>>> {
Arc::clone(&self.provider)
}
}
impl PlatformAddressWallet {
/// Get the next unused platform payment receive address from the
/// HD address pool for the given account key. Generates a new
/// address if the pool is exhausted, maintaining the gap limit.
///
/// DIP-17 derivation: `m/9'/coin_type'/17'/account'/key_class'/index`
/// - `account_key.account` selects the HD account
/// - `account_key.key_class` selects the key purpose (0 = clear funds)
///
/// The address is derived from the wallet's public key material
/// via dashcore's `AddressPool::next_unused` — no seed access or
/// caller-side derivation needed.
pub async fn next_unused_receive_address(
&self,
account_key: key_wallet::account::account_collection::PlatformPaymentAccountKey,
) -> Result<PlatformAddress, PlatformWalletError> {
let mut wm = self.wallet_manager.write().await;
let (wallet, info) = wm
.get_wallet_mut_and_info_mut(&self.wallet_id)
.ok_or_else(|| {
PlatformWalletError::WalletNotFound(format!(
"Wallet {:?} not found",
hex::encode(self.wallet_id)
))
})?;
let managed_account = info
.core_wallet
.platform_payment_managed_account_at_index_mut(account_key.account)
.ok_or_else(|| {
PlatformWalletError::AddressSync(format!(
"No platform payment account at index {}",
account_key.account
))
})?;
let key_source = {
let xpub = wallet
.accounts
.platform_payment_accounts
.get(&account_key)
.map(|acct| acct.account_xpub)
.ok_or_else(|| {
PlatformWalletError::AddressSync(format!(
"No platform payment account key for {:?}",
account_key
))
})?;
key_wallet::KeySource::Public(xpub)
};
let address = managed_account
.addresses
.next_unused(&key_source, true)
.map_err(|e| PlatformWalletError::AddressSync(e.to_string()))?;
PlatformAddress::try_from(address).map_err(|e| {
PlatformWalletError::AddressSync(format!("Failed to convert to PlatformAddress: {e}"))
})
}
/// Get all platform addresses with their cached balances.
///
/// Returns the balances from the last call to [`sync_balances`](Self::sync_balances),
/// [`transfer`](Self::transfer), or [`withdraw`](Self::withdraw).
pub async fn addresses_with_balances(&self) -> Vec<(PlatformAddress, Credits)> {
let wm = self.wallet_manager.read().await;
wm.get_wallet_info(&self.wallet_id)
.and_then(|info| info.core_wallet.first_platform_payment_managed_account())
.map(|account| {
account
.address_balances
.iter()
.map(|(p2pkh, &bal)| (PlatformAddress::P2pkh(p2pkh.to_bytes()), bal))
.collect()
})
.unwrap_or_default()
}
/// Current incremental-sync watermark (`last_known_recent_block`)
/// from the unified platform-address provider.
///
/// Returns `None` when the provider hasn't been initialised yet or
/// when no incremental sync has produced a watermark. A zero-valued
/// watermark is reported as `None` to match the "no stored watermark"
/// convention used by [`Self::apply_sync_state`]. Intended for
/// progress checks where the precise "uninitialised vs. zero"
/// distinction is not material.
pub async fn sync_watermark(&self) -> Option<u64> {
let guard = self.provider.read().await;
let raw = guard.as_ref().map(|p| p.last_known_recent_block())?;
(raw > 0).then_some(raw)
}
/// Get total platform credits across all addresses.
///
/// Returns the sum of all cached balances.
pub async fn total_credits(&self) -> Credits {
let wm = self.wallet_manager.read().await;
wm.get_wallet_info(&self.wallet_id)
.and_then(|info| info.core_wallet.first_platform_payment_managed_account())
.map(|account| account.total_credit_balance())
.unwrap_or(0)
}
}
impl std::fmt::Debug for PlatformAddressWallet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PlatformAddressWallet")
.field("network", &self.sdk.network)
.finish()
}
}