Skip to content

Commit b0fecac

Browse files
authored
Merge pull request #861 from dashpay/feat/unwire-deferred-domains
refactor: complete data.db unwire — shielded + DashPay (stacked on #860)
2 parents 6aa9c39 + 2136403 commit b0fecac

27 files changed

Lines changed: 2984 additions & 2507 deletions

docs/ai-design/2026-05-28-migration-tool/notes.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,21 @@ Other shielded artefacts investigated and resolved:
5555
consistent.
5656
- **Sync cursor**: already migrated as part of the per-wallet k/v cursor commit (`ed6ea588`).
5757

58-
### DashPay (deferred in C9)
58+
### DashPay (deferred in C9, completed in D1–D4d)
59+
60+
**Status update (2026-05-29):** the DashPay deferral closed in commits S1 (shielded retire)
61+
and D1–D4d (DashPay unwire) on branch `feat/unwire-deferred-domains` stacked on PR #860.
62+
Upstream `ManagedIdentity` now owns contacts / requests / profiles / payments, and a
63+
per-network DET k/v sidecar owns DashPay overlays (private memo, blocked / rejected
64+
markers, timestamps, address index, address mapping). The DET tables
65+
(`dashpay_profiles`, `dashpay_contacts`, `dashpay_contact_requests`,
66+
`dashpay_payments`, `dashpay_contact_address_indices`, `dashpay_address_mappings`,
67+
`contact_private_info`) are no longer created on fresh installs and have no live readers
68+
or writers in DET code. Pre-D4d installs keep the dormant rows; the migration tool drains
69+
them at its leisure.
5970

60-
DashPay was investigated during C9 and found to be a ~15K-LOC UI + backend re-platforming
61-
project, not a 1:1 unwire. The prerequisites identified during that investigation:
71+
The original C9 investigation notes (kept for the migration-tool author — DET DashPay state
72+
is still readable at SHA `35eb07bf67b48a74f14de2f1cd2a907412cc0b9a`):
6273

6374
1. **SecretStore is not wired into AppContext.** DET does not currently consume `SecretStore`
6475
directly anywhere — Stage-B uses upstream `SqlitePersister`'s `secrets_backend` config, not
@@ -229,8 +240,17 @@ Tables: `dashpay_profiles`, `dashpay_contacts`, `dashpay_contact_requests`,
229240
- **Gotchas:** Some DET-only address-index tables (e.g., `dashpay_contact_address_indices`,
230241
`dashpay_address_mappings`) may have no upstream equivalent — confirm during audit. Do not
231242
assume 1:1 column parity; DET and upstream evolved independently.
232-
- **Status:** DEFERRED — see "Domains deferred" section above. DashPay is a full re-platform,
233-
not a 1:1 unwire. Prerequisites listed above must land first.
243+
- **Status:** DONE (D1–D4d unwire on `feat/unwire-deferred-domains`, stacked on PR #860).
244+
S1 retired the shielded data.db code path; D1 introduced the `DashpayView` adapter; D2
245+
wired sidecar reads/writes for DET-only overlays; D3 added blocked/rejected/timestamp
246+
markers; D4a–D4c migrated every DashPay read and write off the DET tables; D4d deletes
247+
`src/database/dashpay.rs` (894 LOC) and `src/database/contacts.rs` (356 LOC), drops all
248+
`CREATE TABLE` entries from `database/initialization.rs`, collapses 3 UI dual-writes to
249+
sidecar-only writes, and extends `AppContext::clear_network_database` with a
250+
`det:dashpay:` prefix sweep on the per-network k/v sidecar. The migration tool reads
251+
DET DashPay rows at SHA `35eb07bf67b48a74f14de2f1cd2a907412cc0b9a` (pre-unwire) and
252+
writes upstream-owned state into `ManagedIdentity` plus DET overlays into the
253+
`det:dashpay:*` k/v namespace.
234254

235255
---
236256

docs/kv-keys.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# DET k/v key reference
2+
3+
`DetKv` wraps the upstream `platform_wallet_storage::KvStore`. Values are encoded as `[ schema_version (1 byte) | bincode(payload) ]` using `bincode::config::standard()`. Keys are colon-separated namespaces. Every `DetKv` call takes an `Option<&WalletId>` scope: `None` = global slot, `Some(&id)` = per-wallet slot (cascades on wallet delete).
4+
5+
Two backing stores exist:
6+
7+
| Store | Path | Contents |
8+
|-------|------|----------|
9+
| `det-app.sqlite` | `<data_dir>/det-app.sqlite` | Cross-network settings |
10+
| `platform-wallet.sqlite` | `<data_dir>/spv/<net>/platform-wallet.sqlite` | Everything else (per-network) |
11+
12+
---
13+
14+
## Settings
15+
16+
| Key | Scope | Store | Value type | Fields |
17+
|-----|-------|-------|------------|--------|
18+
| `det:settings:v1` | `None` | `det-app.sqlite` | `AppSettings` via `AppSettingsWire` | `network`, `root_screen_type`, `dash_qt_path`, `overwrite_dash_conf`, `disable_zmq`, `theme_mode`, `core_backend_mode`, `onboarding_completed`, `show_evonode_tools`, `user_mode`, `close_dash_qt_on_exit`, `auto_start_spv` |
19+
20+
Source: `src/model/settings.rs`, `src/context/settings_db.rs`
21+
22+
---
23+
24+
## Wallet selection
25+
26+
| Key | Scope | Store | Value type | Fields |
27+
|-----|-------|-------|------------|--------|
28+
| `det:selected_wallet:v1` | `None` | `platform-wallet.sqlite` | `SelectedWallet` | `hd_wallet_hash: Option<[u8;32]>`, `single_key_hash: Option<[u8;32]>` |
29+
30+
Source: `src/model/selected_wallet.rs`, `src/wallet_backend/mod.rs`
31+
32+
---
33+
34+
## Identities
35+
36+
| Key | Scope | Store | Value type | Notes |
37+
|-----|-------|-------|------------|-------|
38+
| `det:identity:<base58_identity_id>` | `None` | `platform-wallet.sqlite` | `StoredQualifiedIdentity` | Fields: `qi_bytes` (inner bincode), `status: u8`, `identity_type: String`, `wallet_hash: Option<[u8;32]>`, `wallet_index: Option<u32>` |
39+
| `det:identity_order:v1` | `None` | `platform-wallet.sqlite` | `Vec<[u8;32]>` | Ordered list of identity ID raw bytes |
40+
| `det:top_ups:<base58_identity_id>` | `None` | `platform-wallet.sqlite` | `BTreeMap<u32, u64>` | Top-up history: account index → credits |
41+
42+
Source: `src/context/identity_db.rs`
43+
44+
---
45+
46+
## Scheduled votes
47+
48+
| Key | Scope | Store | Value type | Notes |
49+
|-----|-------|-------|------------|-------|
50+
| `det:scheduled_vote:<base58_voter_id>:<contested_name>` | `None` | `platform-wallet.sqlite` | `StoredScheduledVote` | Fields: `voter_id: [u8;32]`, `contested_name: String`, `choice: StoredVoteChoice`, `unix_timestamp: u64`, `executed_successfully: bool` |
51+
52+
Source: `src/context/identity_db.rs`
53+
54+
---
55+
56+
## Contested names (DPNS)
57+
58+
| Key | Scope | Store | Value type | Notes |
59+
|-----|-------|-------|------------|-------|
60+
| `det:contested_name:<normalized_name>` | `None` | `platform-wallet.sqlite` | `StoredContestedName` | Fields: `normalized_contested_name`, `locked_votes`, `abstain_votes`, `awarded_to`, `end_time`, `locked`, `last_updated`, `contestants: Vec<StoredContestant>` |
61+
62+
`StoredContestant` fields: `id: [u8;32]`, `name`, `info`, `votes: u32`, `created_at`, `created_at_block_height`, `created_at_core_block_height`, `document_id: [u8;32]`.
63+
64+
Source: `src/context/contested_names_db.rs`
65+
66+
---
67+
68+
## Contracts
69+
70+
| Key | Scope | Store | Value type | Notes |
71+
|-----|-------|-------|------------|-------|
72+
| `det:contract:<base58_contract_id>` | `None` | `platform-wallet.sqlite` | `StoredContract` | Fields: `contract_bytes: Vec<u8>` (platform-serialized), `alias: Option<String>` |
73+
74+
Source: `src/context/contract_token_db.rs`
75+
76+
---
77+
78+
## Tokens
79+
80+
| Key | Scope | Store | Value type | Notes |
81+
|-----|-------|-------|------------|-------|
82+
| `det:token:<base58_token_id>` | `None` | `platform-wallet.sqlite` | `StoredToken` | Fields: `config_bytes: Vec<u8>` (bincode `TokenConfiguration`), `alias: String`, `data_contract_id: [u8;32]`, `position: u16` |
83+
| `det:token_balance:<base58_identity_id>:<base58_token_id>` | `None` | `platform-wallet.sqlite` | `u64` | Raw balance in token base units |
84+
| `det:token_order:v1` | `None` | `platform-wallet.sqlite` | `Vec<([u8;32],[u8;32])>` | Ordered `(token_id, identity_id)` pairs for My Tokens screen |
85+
86+
Source: `src/context/contract_token_db.rs`
87+
88+
---
89+
90+
## Platform addresses
91+
92+
Both keys use **per-wallet scope** (`Some(&seed_hash)`) so entries cascade on wallet removal.
93+
94+
| Key | Scope | Store | Value type | Notes |
95+
|-----|-------|-------|------------|-------|
96+
| `det:platform_addr:<canonical_address>` | `Some(&wallet_seed_hash)` | `platform-wallet.sqlite` | `StoredPlatformAddressInfo` | Fields: `balance: u64`, `nonce: u32` |
97+
| `det:platform_sync:v1` | `Some(&wallet_seed_hash)` | `platform-wallet.sqlite` | `StoredPlatformSyncInfo` | Fields: `last_sync_timestamp: u64`, `sync_height: u64` |
98+
99+
Source: `src/context/platform_address_db.rs`
100+
101+
---
102+
103+
## DashPay sidecar
104+
105+
All sidecar keys use **global scope** (`None`). The per-network `platform-wallet.sqlite` already partitions by network, so no `<network>:` prefix is needed within the key.
106+
107+
| Key | Scope | Store | Value type | Notes |
108+
|-----|-------|-------|------------|-------|
109+
| `det:dashpay:blocked:<base58_contact_id>` | `None` | `platform-wallet.sqlite` | `()` | Presence-only flag: contact is blocked |
110+
| `det:dashpay:rejected:<base58_counterparty_id>` | `None` | `platform-wallet.sqlite` | `()` | Presence-only flag: contact request rejected |
111+
| `det:dashpay:timestamps:<base58_entity_id>` | `None` | `platform-wallet.sqlite` | `(i64, i64)` | DET-local `(created_at_ms, updated_at_ms)` |
112+
| `det:dashpay:private:<base58_owner>:<base58_contact>` | `None` | `platform-wallet.sqlite` | `ContactPrivateInfo` | Fields: `nickname: String`, `notes: String`, `is_hidden: bool` |
113+
| `det:dashpay:address_index:<base58_owner>:<base58_contact>` | `None` | `platform-wallet.sqlite` | `ContactAddressIndex` | Fields: `owner_identity_id: Vec<u8>`, `contact_identity_id: Vec<u8>`, `next_send_index: u32`, `highest_receive_index: u32`, `bloom_registered_count: u32` |
114+
| `det:dashpay:addr_map:<base58_owner>:<address>` | `None` | `platform-wallet.sqlite` | `([u8;32], u32)` | Reverse map: wallet address → `(contact_id_bytes, index)` |
115+
116+
Source: `src/wallet_backend/dashpay.rs`, `src/model/dashpay.rs`
117+
118+
---
119+
120+
## Summary counts
121+
122+
| Store | Key count |
123+
|-------|-----------|
124+
| `det-app.sqlite` | 1 |
125+
| `platform-wallet.sqlite` | 17 (across 8 domains) |
126+
| **Total** | **18** |
127+
128+
Prefixed/templated keys (e.g. `det:identity:<id>`) are counted once per prefix, not per instance.

src/backend_task/dashpay.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,18 @@ impl AppContext {
171171
),
172172
DashPayTask::LoadPaymentHistory { identity } => {
173173
let identity_id = identity.identity.id();
174+
// Refresh-style action: kick upstream before reading so the
175+
// view sees the latest contact / payment state.
176+
if let Ok(backend) = self.wallet_backend()
177+
&& let Err(e) = backend.dashpay_sync(&identity_id).await
178+
{
179+
tracing::debug!(
180+
identity = %identity_id,
181+
error = ?e,
182+
"LoadPaymentHistory: dashpay_sync degraded; reading cached state"
183+
);
184+
}
185+
174186
let records = payments::load_payment_history(self, &identity_id, None)
175187
.await
176188
.map_err(
@@ -179,11 +191,14 @@ impl AppContext {
179191
},
180192
)?;
181193

182-
let network_str = self.network.to_string();
183-
let contacts = self
184-
.db
185-
.load_dashpay_contacts(&identity_id, &network_str)
186-
.unwrap_or_default();
194+
// Post-D4c: the WalletBackend DashPay adapter is the sole
195+
// source of truth for contacts. Pre-wire (e.g. cold start)
196+
// we surface an empty list rather than reading from DET —
197+
// a missing backend simply means "not loaded yet".
198+
let contacts = match self.wallet_backend() {
199+
Ok(backend) => backend.dashpay_view().contacts(&identity_id).await,
200+
Err(_) => Vec::new(),
201+
};
187202

188203
let results: Vec<_> = records
189204
.into_iter()

src/backend_task/dashpay/contact_requests.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,24 @@ pub async fn reject_contact_request(
717717
)
718718
.await?;
719719

720+
// Mirror the rejection into the DET-local sidecar so `DashpayView`
721+
// surfaces the request as "rejected" until a fresh outgoing/incoming
722+
// pair establishes a contact. DashPay has no on-chain "rejected" flag,
723+
// so the sidecar is the source of truth here.
724+
//
725+
// The reader keys on the counterparty's identity id (see
726+
// `DashpayView::contact_requests`), so we use the original sender
727+
// identity, not the request document id.
728+
if let Ok(backend) = app_context.wallet_backend()
729+
&& let Err(e) = backend.dashpay_mark_rejected(&from_identity_id)
730+
{
731+
tracing::debug!(
732+
from = %from_identity_id.to_string(Encoding::Base58),
733+
error = ?e,
734+
"DashPay rejection sidecar write failed; request will still display as pending"
735+
);
736+
}
737+
720738
Ok(BackendTaskSuccessResult::DashPayContactRequestRejected(
721739
request_id,
722740
))

0 commit comments

Comments
 (0)