Repository navigation
fix(platform-wallet-storage): persist sentinel-scoped token balances of wallet-owned identities #5613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v5.1-dev
Are you sure you want to change the base?
fix(platform-wallet-storage): persist sentinel-scoped token balances of wallet-owned identities #5613
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,13 +8,14 @@ | |||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| use crate::common; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| use common::{ensure_wallet_meta, fresh_persister, wid}; | ||||||||||||||||||||||||||||||||||||||||||||
| use common::{ensure_identity, ensure_wallet_meta, fresh_persister, wid}; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| use dashcore::{Address, Network, OutPoint, TxOut, Txid}; | ||||||||||||||||||||||||||||||||||||||||||||
| use dpp::prelude::Identifier; | ||||||||||||||||||||||||||||||||||||||||||||
| use key_wallet::Utxo; | ||||||||||||||||||||||||||||||||||||||||||||
| use platform_wallet::changeset::{ | ||||||||||||||||||||||||||||||||||||||||||||
| CoreChangeSet, PlatformAddressBalanceEntry, PlatformAddressChangeSet, PlatformWalletChangeSet, | ||||||||||||||||||||||||||||||||||||||||||||
| PlatformWalletPersistence, | ||||||||||||||||||||||||||||||||||||||||||||
| PlatformWalletPersistence, TokenBalanceChangeSet, | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
| use platform_wallet::wallet::platform_wallet::WalletId; | ||||||||||||||||||||||||||||||||||||||||||||
| use platform_wallet_storage::WalletStorageError; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -97,6 +98,112 @@ fn platform_addr_mixed_wallet_rejected() { | |||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /// `IdentitySyncManager` has no wallet linkage, so it stores token | ||||||||||||||||||||||||||||||||||||||||||||
| /// balances under the all-zero sentinel scope (`token_balances` rows are | ||||||||||||||||||||||||||||||||||||||||||||
| /// keyed by `(identity_id, token_id)` alone). A wallet-owned identity's | ||||||||||||||||||||||||||||||||||||||||||||
| /// balance must persist under that scope rather than fail the flush with | ||||||||||||||||||||||||||||||||||||||||||||
| /// `WalletIdMismatch` on every sync pass. | ||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||
| fn sentinel_scoped_token_balance_of_wallet_owned_identity_persists() { | ||||||||||||||||||||||||||||||||||||||||||||
| let (persister, _tmp, _path) = fresh_persister(); | ||||||||||||||||||||||||||||||||||||||||||||
| let owner_wallet = wid(0xB3); | ||||||||||||||||||||||||||||||||||||||||||||
| ensure_wallet_meta(&persister, &owner_wallet); | ||||||||||||||||||||||||||||||||||||||||||||
| let identity = [0x21u8; 32]; | ||||||||||||||||||||||||||||||||||||||||||||
| ensure_identity(&persister, &identity, Some(&owner_wallet)); | ||||||||||||||||||||||||||||||||||||||||||||
| let token = [0x22u8; 32]; | ||||||||||||||||||||||||||||||||||||||||||||
| let pair = (Identifier::from(identity), Identifier::from(token)); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let mut balances = TokenBalanceChangeSet::default(); | ||||||||||||||||||||||||||||||||||||||||||||
| balances.balances.insert(pair, 7); | ||||||||||||||||||||||||||||||||||||||||||||
| persister | ||||||||||||||||||||||||||||||||||||||||||||
| .store(WalletId::default(), balances.into()) | ||||||||||||||||||||||||||||||||||||||||||||
| .expect("sentinel-scoped token balance of a wallet-owned identity must persist"); | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+107
to
+120
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Cover the previously valid sentinel-scoped orphan-identity case The new tests cover the newly required wallet-owned identity case, missing-identity FK rejection, and real-wallet cross-ownership rejection, but they do not preserve the sentinel behavior that already worked before this change: a token balance for an existing identity whose source: |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let stored: i64 = persister | ||||||||||||||||||||||||||||||||||||||||||||
| .lock_conn_for_test() | ||||||||||||||||||||||||||||||||||||||||||||
| .query_row( | ||||||||||||||||||||||||||||||||||||||||||||
| "SELECT balance FROM token_balances WHERE identity_id = ?1 AND token_id = ?2", | ||||||||||||||||||||||||||||||||||||||||||||
| params![identity.as_slice(), token.as_slice()], | ||||||||||||||||||||||||||||||||||||||||||||
| |row| row.get(0), | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| .expect("token balance row written"); | ||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(stored, 7); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let mut removal = TokenBalanceChangeSet::default(); | ||||||||||||||||||||||||||||||||||||||||||||
| removal.removed_balances.insert(pair); | ||||||||||||||||||||||||||||||||||||||||||||
| persister | ||||||||||||||||||||||||||||||||||||||||||||
| .store(WalletId::default(), removal.into()) | ||||||||||||||||||||||||||||||||||||||||||||
| .expect("sentinel-scoped token balance removal must persist"); | ||||||||||||||||||||||||||||||||||||||||||||
| let remaining: i64 = persister | ||||||||||||||||||||||||||||||||||||||||||||
| .lock_conn_for_test() | ||||||||||||||||||||||||||||||||||||||||||||
| .query_row( | ||||||||||||||||||||||||||||||||||||||||||||
| "SELECT COUNT(*) FROM token_balances WHERE identity_id = ?1", | ||||||||||||||||||||||||||||||||||||||||||||
| params![identity.as_slice()], | ||||||||||||||||||||||||||||||||||||||||||||
| |row| row.get(0), | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| .expect("count token balance rows"); | ||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(remaining, 0); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /// The sentinel scope skips only the ownership check: a balance for an | ||||||||||||||||||||||||||||||||||||||||||||
| /// identity with no `identities` row is still rejected by the FK. | ||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||
| fn sentinel_scoped_token_balance_of_missing_identity_rejected() { | ||||||||||||||||||||||||||||||||||||||||||||
| let (persister, _tmp, _path) = fresh_persister(); | ||||||||||||||||||||||||||||||||||||||||||||
| let missing_identity = [0x25u8; 32]; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let mut balances = TokenBalanceChangeSet::default(); | ||||||||||||||||||||||||||||||||||||||||||||
| balances.balances.insert( | ||||||||||||||||||||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||||||||||||||||||||
| Identifier::from(missing_identity), | ||||||||||||||||||||||||||||||||||||||||||||
| Identifier::from([0x26u8; 32]), | ||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||
| 7, | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| let err = persister | ||||||||||||||||||||||||||||||||||||||||||||
| .store(WalletId::default(), balances.into()) | ||||||||||||||||||||||||||||||||||||||||||||
| .expect_err("a balance for a missing identity must be rejected"); | ||||||||||||||||||||||||||||||||||||||||||||
| assert!( | ||||||||||||||||||||||||||||||||||||||||||||
| format!("{err:?}").contains("FOREIGN KEY"), | ||||||||||||||||||||||||||||||||||||||||||||
| "expected FOREIGN KEY failure, got `{err:?}`" | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| let rows: i64 = persister | ||||||||||||||||||||||||||||||||||||||||||||
| .lock_conn_for_test() | ||||||||||||||||||||||||||||||||||||||||||||
| .query_row( | ||||||||||||||||||||||||||||||||||||||||||||
| "SELECT COUNT(*) FROM token_balances WHERE identity_id = ?1", | ||||||||||||||||||||||||||||||||||||||||||||
| params![missing_identity.as_slice()], | ||||||||||||||||||||||||||||||||||||||||||||
| |row| row.get(0), | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| .expect("count token balance rows"); | ||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(rows, 0); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /// A real wallet scope still may not write token balances for another | ||||||||||||||||||||||||||||||||||||||||||||
| /// wallet's identity. | ||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||
| fn token_balance_of_another_wallets_identity_rejected() { | ||||||||||||||||||||||||||||||||||||||||||||
| let (persister, _tmp, _path) = fresh_persister(); | ||||||||||||||||||||||||||||||||||||||||||||
| let owner_wallet = wid(0xB4); | ||||||||||||||||||||||||||||||||||||||||||||
| let other_wallet = wid(0xB5); | ||||||||||||||||||||||||||||||||||||||||||||
| ensure_wallet_meta(&persister, &owner_wallet); | ||||||||||||||||||||||||||||||||||||||||||||
| ensure_wallet_meta(&persister, &other_wallet); | ||||||||||||||||||||||||||||||||||||||||||||
| let identity = [0x23u8; 32]; | ||||||||||||||||||||||||||||||||||||||||||||
| ensure_identity(&persister, &identity, Some(&owner_wallet)); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let mut balances = TokenBalanceChangeSet::default(); | ||||||||||||||||||||||||||||||||||||||||||||
| balances.balances.insert( | ||||||||||||||||||||||||||||||||||||||||||||
| (Identifier::from(identity), Identifier::from([0x24u8; 32])), | ||||||||||||||||||||||||||||||||||||||||||||
| 7, | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| let err = persister | ||||||||||||||||||||||||||||||||||||||||||||
| .store(other_wallet, balances.into()) | ||||||||||||||||||||||||||||||||||||||||||||
| .expect_err("another wallet's scope must be rejected"); | ||||||||||||||||||||||||||||||||||||||||||||
| assert!( | ||||||||||||||||||||||||||||||||||||||||||||
| format!("{err}").contains("wallet id mismatch"), | ||||||||||||||||||||||||||||||||||||||||||||
| "expected wallet id mismatch, got `{err}`" | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+198
to
+204
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Assert the typed WalletIdMismatch payload The cross-wallet test currently checks the formatted error text. Since this path produces the typed
Suggested change
source: |
||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| fn p2pkh(byte: u8) -> Address { | ||||||||||||||||||||||||||||||||||||||||||||
| use dashcore::address::Payload; | ||||||||||||||||||||||||||||||||||||||||||||
| use dashcore::hashes::Hash; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Suggestion: Reuse the existing sentinel-scope helper instead of duplicating the predicate
This adds another inline definition of the all-zero sentinel scope.
schema::wallet_id_to_paramalready centralizes the same sentinel encoding and documents its meaning, so this check can usesuper::wallet_id_to_param(wallet_id).is_none(). Reusing that helper keeps the storage spelling consistent and avoids another call site that would need updating if the sentinel representation changes.source:
glm-5.3-flash(phase1-reviewer: general, rust-quality)