Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- **Wallet data no longer lives inside the deletable network-cache folder**:
each network's wallet database used to sit inside the same folder as the
temporary blockchain sync cache, so clearing or losing that cache folder
could take real wallet, identity, and key data down with it. Each network's
wallet database now lives in its own file alongside the app's other
permanent data, completely separate from the disposable cache.

- **Funding an identity you don't own could silently corrupt wallet data, or
misdirect a later top-up**: paying Platform credits into an identity that
belongs to a different wallet on this device could register that identity
under the paying wallet by mistake. Restarting the app afterward could then
fail to open a wallet with "Saved wallet data appears damaged and cannot be
loaded," and — separately — a later top-up of the paying wallet's own
identity at the same position could be misdirected to the wrong identity
entirely. Funding another wallet's identity now completes without touching
the paying wallet's own identity records.

- **One damaged payment record no longer makes every wallet unopenable**: all
wallets are kept in a single file, and one unreadable payment record in it
stopped that whole file from opening — every wallet it held, funded ones
Expand Down
48 changes: 24 additions & 24 deletions docs/kv-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Three backing stores exist:
| Store | Path | Contents |
|-------|------|----------|
| `det-app.sqlite` | `<data_dir>/det-app.sqlite` | Cross-network settings, wallet metadata, migration sentinel, single-key metadata |
| `platform-wallet.sqlite` | `<data_dir>/spv/<net>/platform-wallet.sqlite` | Per-network identities, tokens, contracts, DashPay overlays, platform addresses, selected wallet |
| `det-<net>.sqlite` | `<data_dir>/det-<net>.sqlite` | Per-network identities, tokens, contracts, DashPay overlays, platform addresses, selected wallet |
| `SecretStore` | `<data_dir>/secrets/det-secrets.*` | Encrypted HD-wallet seed envelopes and imported single-key private bytes |

In the per-domain tables below, a `Scope` of `None` denotes `DetScope::Global`.
Expand Down Expand Up @@ -72,7 +72,7 @@ Source: `src/backend_task/migration/finish_unwire.rs` (`sentinel_key_for`, `SENT

| Key | Scope | Store | Value type | Fields |
|-----|-------|-------|------------|--------|
| `det:selected_wallet:v1` | `None` | `platform-wallet.sqlite` | `SelectedWallet` | `hd_wallet_hash: Option<[u8;32]>`, `single_key_hash: Option<[u8;32]>` |
| `det:selected_wallet:v1` | `None` | `det-<net>.sqlite` | `SelectedWallet` | `hd_wallet_hash: Option<[u8;32]>`, `single_key_hash: Option<[u8;32]>` |

Source: `src/model/selected_wallet.rs`, `src/wallet_backend/mod.rs`

Expand All @@ -84,10 +84,10 @@ The identity blob and top-up history are **identity-scoped** (`DetScope::Identit

| Key | Scope | Store | Value type | Notes |
|-----|-------|-------|------------|-------|
| `det:identity:v1` | `DetScope::Identity(&id)` | `platform-wallet.sqlite` | `StoredQualifiedIdentity` | Fields: `qi_bytes` (inner bincode, redacted in `Debug`), `status: u8`, `identity_type: String`, `wallet_hash: Option<[u8;32]>`, `wallet_index: Option<u32>` |
| `det:identity_index:v1` | `None` | `platform-wallet.sqlite` | `Vec<[u8;32]>` | Complete enumeration index of stored identity ids |
| `det:identity_order:v1` | `None` | `platform-wallet.sqlite` | `Vec<[u8;32]>` | User-chosen display ordering of identity ID raw bytes |
| `det:top_ups:v1` | `DetScope::Identity(&id)` | `platform-wallet.sqlite` | `BTreeMap<u32, u64>` | Top-up history: account index → credits |
| `det:identity:v1` | `DetScope::Identity(&id)` | `det-<net>.sqlite` | `StoredQualifiedIdentity` | Fields: `qi_bytes` (inner bincode, redacted in `Debug`), `status: u8`, `identity_type: String`, `wallet_hash: Option<[u8;32]>`, `wallet_index: Option<u32>` |
| `det:identity_index:v1` | `None` | `det-<net>.sqlite` | `Vec<[u8;32]>` | Complete enumeration index of stored identity ids |
| `det:identity_order:v1` | `None` | `det-<net>.sqlite` | `Vec<[u8;32]>` | User-chosen display ordering of identity ID raw bytes |
| `det:top_ups:v1` | `DetScope::Identity(&id)` | `det-<net>.sqlite` | `BTreeMap<u32, u64>` | Top-up history: account index → credits |

Source: `src/context/identity_db.rs`

Expand All @@ -99,8 +99,8 @@ Scheduled votes are **voter-scoped** (`DetScope::Identity(&voter_id)`); the cont

| Key | Scope | Store | Value type | Notes |
|-----|-------|-------|------------|-------|
| `det:scheduled_vote:<contested_name>` | `DetScope::Identity(&voter_id)` | `platform-wallet.sqlite` | `StoredScheduledVote` | Fields: `voter_id: [u8;32]`, `contested_name: String`, `choice: StoredVoteChoice`, `unix_timestamp: u64`, `executed_successfully: bool` |
| `det:scheduled_vote_voters:v1` | `None` | `platform-wallet.sqlite` | `Vec<[u8;32]>` | Enumeration index of voter ids with scheduled votes |
| `det:scheduled_vote:<contested_name>` | `DetScope::Identity(&voter_id)` | `det-<net>.sqlite` | `StoredScheduledVote` | Fields: `voter_id: [u8;32]`, `contested_name: String`, `choice: StoredVoteChoice`, `unix_timestamp: u64`, `executed_successfully: bool` |
| `det:scheduled_vote_voters:v1` | `None` | `det-<net>.sqlite` | `Vec<[u8;32]>` | Enumeration index of voter ids with scheduled votes |

Source: `src/context/identity_db.rs`

Expand All @@ -110,7 +110,7 @@ Source: `src/context/identity_db.rs`

| Key | Scope | Store | Value type | Notes |
|-----|-------|-------|------------|-------|
| `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>` |
| `det:contested_name:<normalized_name>` | `None` | `det-<net>.sqlite` | `StoredContestedName` | Fields: `normalized_contested_name`, `locked_votes`, `abstain_votes`, `awarded_to`, `end_time`, `locked`, `last_updated`, `contestants: Vec<StoredContestant>` |

`StoredContestant` fields: `id: [u8;32]`, `name`, `info`, `votes: u32`, `created_at`, `created_at_block_height`, `created_at_core_block_height`, `document_id: [u8;32]`.

Expand All @@ -122,7 +122,7 @@ Source: `src/context/contested_names_db.rs`

| Key | Scope | Store | Value type | Notes |
|-----|-------|-------|------------|-------|
| `det:contract:<base58_contract_id>` | `None` | `platform-wallet.sqlite` | `StoredContract` | Fields: `contract_bytes: Vec<u8>` (platform-serialized), `alias: Option<String>` |
| `det:contract:<base58_contract_id>` | `None` | `det-<net>.sqlite` | `StoredContract` | Fields: `contract_bytes: Vec<u8>` (platform-serialized), `alias: Option<String>` |

Source: `src/context/contract_token_db.rs`

Expand All @@ -132,8 +132,8 @@ Source: `src/context/contract_token_db.rs`

| Key | Scope | Store | Value type | Notes |
|-----|-------|-------|------------|-------|
| `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` |
| `det:token_order:v1` | `None` | `platform-wallet.sqlite` | `Vec<([u8;32],[u8;32])>` | Ordered `(token_id, identity_id)` pairs for My Tokens screen |
| `det:token:<base58_token_id>` | `None` | `det-<net>.sqlite` | `StoredToken` | Fields: `config_bytes: Vec<u8>` (bincode `TokenConfiguration`), `alias: String`, `data_contract_id: [u8;32]`, `position: u16` |
| `det:token_order:v1` | `None` | `det-<net>.sqlite` | `Vec<([u8;32],[u8;32])>` | Ordered `(token_id, identity_id)` pairs for My Tokens screen |

Per-`(identity, token)` balances are no longer cached by DET. They are read live from the upstream `IdentitySyncManager` through the `TokenBalanceView` seam (`src/wallet_backend/token_balance.rs`), which is fed a lock-free snapshot refreshed off the UI thread.

Expand All @@ -147,27 +147,27 @@ Both keys use **per-wallet scope** (`DetScope::Wallet(&seed_hash)`) so entries c

| Key | Scope | Store | Value type | Notes |
|-----|-------|-------|------------|-------|
| `det:platform_addr:<canonical_address>` | `DetScope::Wallet(&seed_hash)` | `platform-wallet.sqlite` | `StoredPlatformAddressInfo` | Fields: `balance: u64`, `nonce: u32` |
| `det:platform_sync:v1` | `DetScope::Wallet(&seed_hash)` | `platform-wallet.sqlite` | `StoredPlatformSyncInfo` | Fields: `last_sync_timestamp: u64`, `sync_height: u64` |
| `det:platform_addr:<canonical_address>` | `DetScope::Wallet(&seed_hash)` | `det-<net>.sqlite` | `StoredPlatformAddressInfo` | Fields: `balance: u64`, `nonce: u32` |
| `det:platform_sync:v1` | `DetScope::Wallet(&seed_hash)` | `det-<net>.sqlite` | `StoredPlatformSyncInfo` | Fields: `last_sync_timestamp: u64`, `sync_height: u64` |

Source: `src/context/platform_address_db.rs`, `src/wallet_backend/platform_address.rs`

---

## DashPay sidecar

The per-network `platform-wallet.sqlite` already partitions DashPay data by network, so no `<network>:` prefix is needed within a key. Owner-specific decisions and recovery state use `DetScope::Identity(&owner)`; the owner id is carried by the scope and the upstream soft-cascade reaps those values when the owner identity row is deleted.
The per-network `det-<net>.sqlite` already partitions DashPay data by network, so no `<network>:` prefix is needed within a key. Owner-specific decisions and recovery state use `DetScope::Identity(&owner)`; the owner id is carried by the scope and the upstream soft-cascade reaps those values when the owner identity row is deleted.

| Key | Scope | Store | Value type | Notes |
|-----|-------|-------|------------|-------|
| `det:dashpay:blocked:<base58_contact_id>` | `DetScope::Identity(&owner)` | `platform-wallet.sqlite` | `()` | Presence-only flag: contact is blocked |
| `det:dashpay:declined:<base58_counterparty_id>` | `DetScope::Identity(&owner)` | `platform-wallet.sqlite` | `()` | Presence-only flag: incoming contact request declined |
| `det:dashpay:withdrawn:<base58_counterparty_id>` | `DetScope::Identity(&owner)` | `platform-wallet.sqlite` | `()` | Presence-only flag: outgoing contact request withdrawn |
| `det:dashpay:request_action:<decline|cancel>:<base58_request_id>` | `DetScope::Identity(&owner)` | `platform-wallet.sqlite` | `ContactRequestActionPhase` | Durable recovery phase for a paid hide/corrective-unhide followed by a local marker write |
| `det:dashpay:timestamps:<base58_entity_id>` | `None` | `platform-wallet.sqlite` | `(i64, i64)` | DET-local `(created_at_ms, updated_at_ms)` |
| `det:dashpay:private:<base58_contact>` | `DetScope::Identity(&owner)` | `platform-wallet.sqlite` | `ContactPrivateInfo` | Fields: `nickname: String`, `notes: String`, `is_hidden: bool` |
| `det:dashpay:address_index:<base58_contact>` | `DetScope::Identity(&owner)` | `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` |
| `det:dashpay:addr_map:<base58_owner>:<address>` | `None` | `platform-wallet.sqlite` | `([u8;32], u32)` | Reverse map: wallet address → `(contact_id_bytes, index)` |
| `det:dashpay:blocked:<base58_contact_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `()` | Presence-only flag: contact is blocked |
| `det:dashpay:declined:<base58_counterparty_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `()` | Presence-only flag: incoming contact request declined |
| `det:dashpay:withdrawn:<base58_counterparty_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `()` | Presence-only flag: outgoing contact request withdrawn |
| `det:dashpay:request_action:<decline|cancel>:<base58_request_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `ContactRequestActionPhase` | Durable recovery phase for a paid hide/corrective-unhide followed by a local marker write |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Escape the | inside the key name to fix the broken table row.

The unescaped | in <decline|cancel> splits this row into 6 cells instead of 5, as flagged by markdownlint. Escape it as \| so the table renders correctly.

🐛 Proposed fix
-| `det:dashpay:request_action:<decline|cancel>:<base58_request_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `ContactRequestActionPhase` | Durable recovery phase for a paid hide/corrective-unhide followed by a local marker write |
+| `det:dashpay:request_action:<decline\|cancel>:<base58_request_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `ContactRequestActionPhase` | Durable recovery phase for a paid hide/corrective-unhide followed by a local marker write |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `det:dashpay:request_action:<decline|cancel>:<base58_request_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `ContactRequestActionPhase` | Durable recovery phase for a paid hide/corrective-unhide followed by a local marker write |
| `det:dashpay:request_action:<decline\|cancel>:<base58_request_id>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `ContactRequestActionPhase` | Durable recovery phase for a paid hide/corrective-unhide followed by a local marker write |
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 166-166: Table column count
Expected: 5; Actual: 6; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/kv-keys.md` at line 166, Update the key name in the documentation table
row to escape the pipe within the `<decline|cancel>` placeholder as `\|`,
keeping the row’s five-column Markdown structure intact.

Source: Linters/SAST tools

| `det:dashpay:timestamps:<base58_entity_id>` | `None` | `det-<net>.sqlite` | `(i64, i64)` | DET-local `(created_at_ms, updated_at_ms)` |
| `det:dashpay:private:<base58_contact>` | `DetScope::Identity(&owner)` | `det-<net>.sqlite` | `ContactPrivateInfo` | Fields: `nickname: String`, `notes: String`, `is_hidden: bool` |
| `det:dashpay:address_index:<base58_contact>` | `DetScope::Identity(&owner)` | `det-<net>.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` |
| `det:dashpay:addr_map:<base58_owner>:<address>` | `None` | `det-<net>.sqlite` | `([u8;32], u32)` | Reverse map: wallet address → `(contact_id_bytes, index)` |

Source: `src/wallet_backend/dashpay.rs`, `src/model/dashpay.rs`

Expand Down Expand Up @@ -204,7 +204,7 @@ Source: `src/wallet_backend/single_key.rs` (`SINGLE_KEY_PRIV_LABEL_PREFIX`, `SIN
| Store | Key count |
|-------|-----------|
| `det-app.sqlite` | 4 (settings, wallet-meta sidecar, single-key-meta sidecar, migration sentinel) |
| `platform-wallet.sqlite` | 21 (across 8 domains) |
| `det-<net>.sqlite` | 21 (across 8 domains) |
| `SecretStore` | 2 label patterns (seed envelopes, imported-key private bytes) |
| **Total** | **27** |

Expand Down
2 changes: 2 additions & 0 deletions docs/user-stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,8 @@ As a user, I want to add credits to my identity so that I can continue performin

- Top up from wallet or Platform addresses.
- Amount selection with credit cost display.
- Any loaded wallet can pay, including for an identity another wallet owns; only the paying wallet's funds move and its own identity records are left untouched.
- A saved funding transaction can only pay for an identity of the wallet it was created in; paying for another wallet's identity uses the wallet balance instead.

### IDN-005: Withdraw credits to Core address [Implemented]
**Persona:** Priya, Jordan
Expand Down
16 changes: 16 additions & 0 deletions src/backend_task/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,22 @@ pub enum TaskError {
identity_id: dash_sdk::platform::Identifier,
},

/// A resumed funding lock was already spent by an earlier operation, so it
/// cannot fund this top-up.
#[error(
"This saved funding transaction has already been used. Choose a different one, or fund the top-up from your wallet balance instead."
)]
AssetLockAlreadyUsed,

/// A resumed funding lock is bound to a role that cannot pay for an
/// identity outside this wallet — a registration slot of this wallet's own
/// identity, an invitation voucher whose key the invitee holds, or a lock
/// this wallet does not track at all.
#[error(
"This saved funding transaction cannot pay for an identity outside this wallet. Fund the top-up from your wallet balance instead."
)]
AssetLockNotEligibleForTopUp,

/// The asset-lock proof finalization (InstantSend → ChainLock fallback)
/// timed out without producing a usable proof for Platform.
#[error(
Expand Down
2 changes: 1 addition & 1 deletion src/backend_task/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@ impl AppContext {
.load_identity_by_dpns_name(sdk, dpns_name, wallet_seed_hash)
.await?),
IdentityTask::TopUpIdentity(top_up_info) => {
Ok(self.top_up_identity(top_up_info).await?)
Ok(self.top_up_identity(sdk, top_up_info).await?)
}
IdentityTask::TopUpIdentityFromPlatformAddresses {
identity,
Expand Down
Loading
Loading