From e4cfbb311ec8d9a09936721bc47d05081ee427a3 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Wed, 22 Oct 2025 17:43:18 +0400 Subject: [PATCH] fix(wallet)!: support multiple vault locks, handle partial commit case --- Cargo.lock | 1 + applications/tari_indexer/src/lib.rs | 2 +- .../src/network_state_sync/worker.rs | 9 +- .../web_ui/src/routes/Substates/Substates.tsx | 3 +- .../tari_walletd/src/handlers/accounts.rs | 79 ++-- .../tari_walletd/src/handlers/auth/jwt.rs | 8 +- .../tari_walletd/src/services/webauthn.rs | 8 +- .../StealthUtxoList/StealthUtxoList.tsx | 11 +- crates/engine_types/src/commit_result.rs | 11 + crates/engine_types/src/resource_container.rs | 4 +- crates/wallet/crypto/src/memo.rs | 7 - crates/wallet/sdk/Cargo.toml | 1 + crates/wallet/sdk/src/apis/accounts.rs | 2 +- .../sdk/src/apis/confidential_outputs.rs | 2 +- crates/wallet/sdk/src/apis/config.rs | 2 +- crates/wallet/sdk/src/apis/key_manager.rs | 7 +- .../sdk/src/apis/non_fungible_tokens.rs | 2 +- crates/wallet/sdk/src/apis/stealth_outputs.rs | 55 +-- .../wallet/sdk/src/apis/stealth_transfer.rs | 151 ++++++-- crates/wallet/sdk/src/apis/transaction.rs | 95 ++--- crates/wallet/sdk/src/models/key.rs | 17 +- crates/wallet/sdk/src/sdk.rs | 39 ++ crates/wallet/sdk/src/storage.rs | 54 ++- .../sdk/tests/confidential_output_api.rs | 2 +- .../src/account_monitor/monitor.rs | 16 +- .../src/account_monitor/scanner.rs | 27 +- .../sdk_services/src/indexer_rest_api.rs | 2 + .../src/transaction_service/handle.rs | 7 +- .../src/transaction_service/service.rs | 14 +- .../src/utxo_scanner/scanner_round.rs | 2 +- .../src/utxo_scanner/utxo_recovery.rs | 44 ++- .../2023-02-08-122514_initial/up.sql | 38 +- crates/wallet/storage_sqlite/src/lib.rs | 9 +- .../wallet/storage_sqlite/src/models/vault.rs | 11 +- crates/wallet/storage_sqlite/src/reader.rs | 109 ++++-- crates/wallet/storage_sqlite/src/schema.rs | 16 +- crates/wallet/storage_sqlite/src/writer.rs | 351 +++++++++++------- .../wallet/storage_sqlite/tests/accounts.rs | 3 +- crates/wallet/storage_sqlite/tests/config.rs | 3 +- .../storage_sqlite/tests/key_manager_state.rs | 3 +- .../wallet/storage_sqlite/tests/substates.rs | 3 +- .../storage_sqlite/tests/transaction.rs | 3 +- lints.toml | 3 +- .../db_inspector/src/webserver/server.rs | 14 +- 44 files changed, 793 insertions(+), 457 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ecb5ef96e..993d2b71a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11636,6 +11636,7 @@ dependencies = [ "tempfile", "thiserror 2.0.17", "time", + "tokio", "ts-rs", "webauthn-rs", "zeroize", diff --git a/applications/tari_indexer/src/lib.rs b/applications/tari_indexer/src/lib.rs index 8c27970aa4..94e5b5e1a9 100644 --- a/applications/tari_indexer/src/lib.rs +++ b/applications/tari_indexer/src/lib.rs @@ -190,7 +190,7 @@ pub async fn run_indexer(config: ApplicationConfig, mut shutdown_signal: Shutdow }, _ = shutdown_signal.wait() => { - dbg!("Shutting down run_substate_polling"); + debug!(target: LOG_TARGET, "Shutting down run_substate_polling"); break; }, } diff --git a/applications/tari_indexer/src/network_state_sync/worker.rs b/applications/tari_indexer/src/network_state_sync/worker.rs index 601ce8a55e..121301932c 100644 --- a/applications/tari_indexer/src/network_state_sync/worker.rs +++ b/applications/tari_indexer/src/network_state_sync/worker.rs @@ -503,12 +503,17 @@ fn extend_bufs_from_substate_update( value: create.substate.value().clone(), }); }, - Some(_) => {}, + Some(_) => { + warn!(target: LOG_TARGET, "⚠️ NEVER HAPPEN: Received unexpected substate value for created substate: {}", create.substate.substate_id()); + }, None => { let id = create.substate.substate_id(); - if id.is_template() || id.is_transaction_receipt() || id.is_utxo() { + if id.is_template() || id.is_transaction_receipt() { warn!(target: LOG_TARGET, "⚠️ NEVER HAPPEN: Received substate {id} update with no value"); } + if let Some(addr) = id.as_utxo_address() { + debug!(target: LOG_TARGET, "🌍️ Received UTXO substate {addr} creation with no value. Ignoring as this means it is spent later."); + } }, }, SubstateUpdateProof::Destroy(destroy) => match &destroy.substate_id { diff --git a/applications/tari_indexer/web_ui/src/routes/Substates/Substates.tsx b/applications/tari_indexer/web_ui/src/routes/Substates/Substates.tsx index 882501864c..fe529219cd 100644 --- a/applications/tari_indexer/web_ui/src/routes/Substates/Substates.tsx +++ b/applications/tari_indexer/web_ui/src/routes/Substates/Substates.tsx @@ -59,6 +59,7 @@ const SUBSTATE_TYPES = [ "TransactionReceipt", "ValidatorFeePool", "Template", + "Utxo", ] as const; type ExtendedSubstateItem = ListSubstateItem & { id: string; show?: boolean }; @@ -90,7 +91,7 @@ function SubstatesLayout() { const extendedSubstates = useMemo( () => substates.map((substate) => ({ ...substate, id: substateIdToString(substate.substate_id) })), - [substates] + [substates], ); const visibleSubstates = filteredSubstates.filter((substate) => substate.show !== false); diff --git a/applications/tari_walletd/src/handlers/accounts.rs b/applications/tari_walletd/src/handlers/accounts.rs index 90230ac987..a1cf2495c9 100644 --- a/applications/tari_walletd/src/handlers/accounts.rs +++ b/applications/tari_walletd/src/handlers/accounts.rs @@ -3,6 +3,7 @@ use std::{array, collections::HashSet}; +use anyhow::Context; use axum_extra::headers::authorization::Bearer; use indexmap::IndexMap; use log::*; @@ -36,7 +37,7 @@ use tari_template_lib::{ constants::{STEALTH_TARI_RESOURCE_ADDRESS, XTR, XTR_FAUCET_COMPONENT_ADDRESS, XTR_FAUCET_VAULT_ADDRESS}, types::{Amount, ResourceType}, }; -use tari_transaction::args; +use tari_transaction::{args, TransactionSignature}; use tari_wallet_daemon_client::{ permissions::JrpcPermission, types::{ @@ -284,7 +285,7 @@ pub async fn handle_get_balances( let vaults = sdk.accounts_api().get_vaults_by_account(account.component_address())?; let stealth_outputs = sdk .stealth_outputs_api() - .get_unspent_outputs_by_account(account.component_address())?; + .get_unspent_outputs_by_account(account.component_address(), false)?; let mut balances = Vec::with_capacity(vaults.len()); let mut vaulted_resources = HashSet::new(); @@ -683,6 +684,7 @@ pub async fn handle_create_free_test_coins( account.is_confirmed_on_chain().then(|| NewAccountData { address: *account.component_address(), }), + None, ) .await?; @@ -987,13 +989,27 @@ pub async fn handle_stealth_transfer( task::spawn(async move { let transfer = sdk.stealth_transfer_api().transfer(owner_account, params).await?; - let transaction = transfer.transaction.authorized_sealed_signer().build(); + let transaction = transfer.transaction.authorized_sealed_signer(); + let main_pk = transfer.main_signer.public_key().to_byte_type(); + // Add additional signature if needed + let additional_sig = transfer + .additional_signer + .as_ref() + .map(|s| { + sdk.local_signer_api() + .get_signature(s.branch, s.key_id, &main_pk, &transaction) + }) + .transpose()? + .map(|sig| TransactionSignature::new(sig.public_key.to_byte_type(), sig.signature.to_byte_type())); + + let transaction = transaction.build_with_signatures(additional_sig.into_iter().collect()); + + // Sign and seal the final transaction let transaction = sdk.local_signer_api() - .sign(transfer.signing_key_branch, transfer.signing_key_id, transaction)?; + .sign(transfer.main_signer.branch, transfer.main_signer.key_id, transaction)?; - // TODO: if submitting fails we need to unlock the inputs again if req.dry_run { // Release the lock immediately as dry run does not submit the transaction // TODO: maybe transfer() should not lock the outputs if it's a dry run @@ -1009,46 +1025,25 @@ pub async fn handle_stealth_transfer( Ok(res) => Ok(StealthTransferResponse { transaction_id: res.finalize.transaction_hash.into(), }), - Err(e) => { - if let Err(err) = sdk.stealth_outputs_api().release_lock(transfer.lock_id) { - error!( - target: LOG_TARGET, - "Failed to release locked outputs after dry run failure: {}", - err - ); - } - - Err(anyhow::anyhow!("Dry run transaction failed: {}", e)) - }, + Err(e) => Err(anyhow::anyhow!("Dry run transaction failed: {}", e)), }; } - // Associate lock with transaction - sdk.stealth_outputs_api() - .locks_set_transaction_id(transfer.lock_id, transaction.calculate_id())?; - - let result = transaction_service.submit_transaction(transaction).await; - match result { - Ok(tx_id) => { - notifier.notify(TransactionSubmittedEvent { - transaction_id: tx_id, - new_account: None, - }); - - Ok(StealthTransferResponse { transaction_id: tx_id }) - }, - Err(e) => { - if let Err(err) = sdk.stealth_outputs_api().release_lock(transfer.lock_id) { - error!( - target: LOG_TARGET, - "Failed to release locked outputs after submission failure: {}", - err - ); - } - - Err(anyhow::anyhow!("Transaction submission failed: {}", e)) - }, - } + let tx_id = sdk + .stealth_transfer_api() + .unlock_on_failure( + transfer.lock_id, + transaction_service + .submit_transaction_with_opts(transaction, None, Some(transfer.lock_id)) + .await, + ) + .context("Transaction failed to submit")?; + notifier.notify(TransactionSubmittedEvent { + transaction_id: tx_id, + new_account: None, + }); + + Ok(StealthTransferResponse { transaction_id: tx_id }) }) .await? } diff --git a/applications/tari_walletd/src/handlers/auth/jwt.rs b/applications/tari_walletd/src/handlers/auth/jwt.rs index d88d74bd22..aff94c2397 100644 --- a/applications/tari_walletd/src/handlers/auth/jwt.rs +++ b/applications/tari_walletd/src/handlers/auth/jwt.rs @@ -7,7 +7,13 @@ use axum_extra::headers::authorization::Bearer; use jsonwebtoken::{errors, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; use tari_crypto::tari_utilities::SafePassword; -use tari_ootle_wallet_sdk::storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}; +use tari_ootle_wallet_sdk::storage::{ + CommitableStore, + WalletStorageError, + WalletStore, + WalletStoreReader, + WalletStoreWriter, +}; use tari_wallet_daemon_client::{ permissions::{Claims, JrpcPermission, JrpcPermissions}, types::EncodedJwtString, diff --git a/applications/tari_walletd/src/services/webauthn.rs b/applications/tari_walletd/src/services/webauthn.rs index baeba8ee46..d14db6f8d8 100644 --- a/applications/tari_walletd/src/services/webauthn.rs +++ b/applications/tari_walletd/src/services/webauthn.rs @@ -3,7 +3,13 @@ use std::time::{Duration, Instant}; -use tari_ootle_wallet_sdk::storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}; +use tari_ootle_wallet_sdk::storage::{ + CommitableStore, + WalletStorageError, + WalletStore, + WalletStoreReader, + WalletStoreWriter, +}; use thiserror::Error; use webauthn_rs::prelude::{Passkey, PasskeyAuthentication, PasskeyRegistration}; diff --git a/applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx b/applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx index d329c33e9c..1fb116dce3 100644 --- a/applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx +++ b/applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx @@ -64,13 +64,12 @@ function StealthUtxoList({ account }: { account: Account }) { ); const columnWidths = { - 1: "10%", - 2: "15%", - 3: "20%", - 4: "25%", + 1: "15%", + 2: "20%", + 3: "15%", + 4: "30%", 5: "10%", 6: "10%", - 7: "10%", }; return ( @@ -93,7 +92,6 @@ function StealthUtxoList({ account }: { account: Account }) { Memo Burnt Frozen - On Chain @@ -116,7 +114,6 @@ function StealthUtxoList({ account }: { account: Account }) { {utxo.is_burnt ? "Yes" : "No"} {utxo.is_frozen ? "Yes" : "No"} - {utxo.is_on_chain ? "Yes" : "No"} ))} {emptyRows(page, rowsPerPage, data.utxos) > 0 && ( diff --git a/crates/engine_types/src/commit_result.rs b/crates/engine_types/src/commit_result.rs index 052089ac2a..df78dbb724 100644 --- a/crates/engine_types/src/commit_result.rs +++ b/crates/engine_types/src/commit_result.rs @@ -197,6 +197,10 @@ impl FinalizeResult { self.result.any_reject() } + pub fn reject(&self) -> Option<&RejectReason> { + self.result.reject() + } + pub fn fee_accept_transaction_reject(&self) -> Option<(&SubstateDiff, &RejectReason)> { self.result.fee_accept_transaction_reject() } @@ -288,6 +292,13 @@ impl TransactionResult { } } + pub fn reject(&self) -> Option<&RejectReason> { + match self { + Self::Reject(reject_result) => Some(reject_result), + _ => None, + } + } + pub fn expect(self, msg: &str) -> SubstateDiff { match self { Self::Accept(substate_diff) => substate_diff, diff --git a/crates/engine_types/src/resource_container.rs b/crates/engine_types/src/resource_container.rs index 79a177cd76..abee2eb87e 100644 --- a/crates/engine_types/src/resource_container.rs +++ b/crates/engine_types/src/resource_container.rs @@ -330,7 +330,7 @@ impl ResourceContainer { if withdraw_amt > *revealed_amount { return Err(ResourceError::InsufficientBalance { details: format!( - "Bucket contained insufficient revealed funds. Required: {}, Available: {}", + "Bucket or vault contained insufficient revealed funds. Required: {}, Available: {}", withdraw_amt, revealed_amount ), }); @@ -342,7 +342,7 @@ impl ResourceContainer { if withdraw_amt > *revealed_amount { return Err(ResourceError::InsufficientBalance { details: format!( - "Bucket contained insufficient revealed funds. Required: {}, Available: {}", + "Bucket or vault contained insufficient revealed funds. Required: {}, Available: {}", withdraw_amt, revealed_amount ), }); diff --git a/crates/wallet/crypto/src/memo.rs b/crates/wallet/crypto/src/memo.rs index 6f7acf6e48..21d5e12ea4 100644 --- a/crates/wallet/crypto/src/memo.rs +++ b/crates/wallet/crypto/src/memo.rs @@ -51,13 +51,6 @@ impl Memo { Some(Self::Bytes(b)) } - pub fn as_bytes(&self) -> &[u8] { - match self { - Memo::Message(s) => s.as_bytes(), - Memo::Bytes(b) => b.as_ref(), - } - } - pub fn len(&self) -> usize { match self { Memo::Message(s) => s.len(), diff --git a/crates/wallet/sdk/Cargo.toml b/crates/wallet/sdk/Cargo.toml index 4f54e5f4fc..7b3a4c4943 100644 --- a/crates/wallet/sdk/Cargo.toml +++ b/crates/wallet/sdk/Cargo.toml @@ -35,6 +35,7 @@ webauthn-rs = { workspace = true } keyring = { version = "3.6.3", features = ["apple-native", "windows-native", "sync-secret-service"] } passwords = "3.1.16" zeroize = { workspace = true, features = ["serde", "simd"] } +tokio = { workspace = true, default-features = false, features = ["sync"] } [dev-dependencies] tari_ootle_wallet_storage_sqlite = { workspace = true } diff --git a/crates/wallet/sdk/src/apis/accounts.rs b/crates/wallet/sdk/src/apis/accounts.rs index e6900094e8..6769e88f92 100644 --- a/crates/wallet/sdk/src/apis/accounts.rs +++ b/crates/wallet/sdk/src/apis/accounts.rs @@ -39,7 +39,7 @@ use crate::{ WalletOotleAddressWithKeyIds, }, network::WalletNetworkInterface, - storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, + storage::{CommitableStore, WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, }; pub struct AccountsApi<'a, TStore, TNetworkInterface> { diff --git a/crates/wallet/sdk/src/apis/confidential_outputs.rs b/crates/wallet/sdk/src/apis/confidential_outputs.rs index 998991048d..18734e7828 100644 --- a/crates/wallet/sdk/src/apis/confidential_outputs.rs +++ b/crates/wallet/sdk/src/apis/confidential_outputs.rs @@ -16,7 +16,7 @@ use crate::{ key_manager::{KeyManagerApi, KeyManagerApiError}, }, models::{Account, ConfidentialOutputModel, OutputStatus, WalletLockId, WalletSecretKey}, - storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, + storage::{CommitableStore, WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, }; const LOG_TARGET: &str = "tari::ootle::wallet_sdk::apis::confidential_outputs"; diff --git a/crates/wallet/sdk/src/apis/config.rs b/crates/wallet/sdk/src/apis/config.rs index d8888e8c05..921337de03 100644 --- a/crates/wallet/sdk/src/apis/config.rs +++ b/crates/wallet/sdk/src/apis/config.rs @@ -6,7 +6,7 @@ use std::{str::FromStr, sync::OnceLock}; use serde::{de::DeserializeOwned, Serialize}; use tari_ootle_common_types::{optional::IsNotFoundError, Network}; -use crate::storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}; +use crate::storage::{CommitableStore, WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}; #[derive(Debug, Clone)] pub struct ConfigApi<'a, TStore> { diff --git a/crates/wallet/sdk/src/apis/key_manager.rs b/crates/wallet/sdk/src/apis/key_manager.rs index a2baaf91b2..729b52e543 100644 --- a/crates/wallet/sdk/src/apis/key_manager.rs +++ b/crates/wallet/sdk/src/apis/key_manager.rs @@ -34,7 +34,7 @@ use crate::{ WalletPublicKey, WalletSecretKey, }, - storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, + storage::{CommitableStore, WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, }; pub type WalletKeyManager = TariKeyManager>; @@ -76,6 +76,7 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { key_id: KeyId::derived(index), public_key: pk, secret_key: key, + branch, is_active: active, }); } @@ -137,6 +138,7 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { let imported_key = self.get_imported_key(local_key_id)?; Ok(WalletPublicKey { public_key: imported_key.to_public_key(), + branch, key_id, }) }, @@ -144,6 +146,7 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { let derived_key = self.derive_key(branch, index)?; Ok(WalletPublicKey { public_key: derived_key.to_public_key(), + branch, key_id, }) }, @@ -168,6 +171,7 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { .map_err(|e| KeyManagerApiError::KeyStoreError { source: e.into() })?; Ok(DerivedWalletKey { key: secret, + branch, key_index: index, }) } @@ -254,6 +258,7 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { let key = self.derive_key(branch, next_key_id)?; Ok(WalletPublicKey { public_key: key.to_public_key(), + branch, key_id: key.as_key_id(), }) } diff --git a/crates/wallet/sdk/src/apis/non_fungible_tokens.rs b/crates/wallet/sdk/src/apis/non_fungible_tokens.rs index 196c1feb34..1eb18a8e15 100644 --- a/crates/wallet/sdk/src/apis/non_fungible_tokens.rs +++ b/crates/wallet/sdk/src/apis/non_fungible_tokens.rs @@ -12,7 +12,7 @@ use thiserror::Error; use crate::{ models::NonFungibleToken, - storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, + storage::{CommitableStore, WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, }; pub struct NonFungibleTokensApi<'a, TStore> { diff --git a/crates/wallet/sdk/src/apis/stealth_outputs.rs b/crates/wallet/sdk/src/apis/stealth_outputs.rs index 7d0fe25023..3a4454dd84 100644 --- a/crates/wallet/sdk/src/apis/stealth_outputs.rs +++ b/crates/wallet/sdk/src/apis/stealth_outputs.rs @@ -9,6 +9,7 @@ use tari_crypto::{ }; use tari_engine_types::{ component::derive_component_address_from_public_key, + substate::SubstateDiff, FromByteType, ToByteType, Utxo, @@ -32,7 +33,6 @@ use tari_template_lib::{ prelude::{PedersenCommitmentBytes, RistrettoPublicKeyBytes}, types::{Amount, EncryptedData}, }; -use tari_transaction::TransactionId; use crate::{ apis::{ @@ -53,7 +53,7 @@ use crate::{ StealthOutputModel, WalletLockId, }, - storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, + storage::{CommitableStore, WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, }; const LOG_TARGET: &str = "tari::ootle::wallet::apis::stealth_outputs"; @@ -108,16 +108,6 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { }) } - pub fn locks_set_transaction_id( - &self, - lock_id: WalletLockId, - transaction_id: TransactionId, - ) -> Result<(), StealthOutputsApiError> { - self.store - .with_write_tx(|tx| tx.locks_link_transaction(lock_id, transaction_id))?; - Ok(()) - } - /// Locks as many outputs required to reach at least the specified amount. If there are insufficient funds, all /// available outputs will be locked and returned along with the total amount locked. pub fn lock_outputs_until_partial_amount( @@ -195,31 +185,16 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { } pub fn release_lock(&self, lock_id: WalletLockId) -> Result<(), StealthOutputsApiError> { - self.store.with_write_tx(|tx| { - tx.stealth_outputs_release_by_lock_id(lock_id)?; - tx.vaults_release_lock_revealed_funds(lock_id).optional()?; - tx.locks_delete(lock_id) - })?; + self.store.with_write_tx(|tx| tx.locks_release(lock_id))?; Ok(()) } - pub fn finalize_lock(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { - let mut tx = self.store.create_write_tx()?; - tx.stealth_outputs_finalize_by_lock_id(lock_id)?; - tx.locks_delete(lock_id)?; - tx.commit()?; + pub fn finalize_lock(&self, lock_id: WalletLockId, diff: &SubstateDiff) -> Result<(), ConfidentialOutputsApiError> { + self.store + .with_write_tx(|tx| tx.locks_unlock_finalized(lock_id, diff))?; Ok(()) } - pub fn finalize_outputs(&self, lock_id: WalletLockId) -> Result<(), StealthOutputsApiError> { - self.store.with_write_tx(|tx| { - tx.stealth_outputs_finalize_by_lock_id(lock_id)?; - tx.vaults_finalized_locked_revealed_funds(lock_id).optional()?; - tx.locks_delete(lock_id)?; - Ok(()) - }) - } - pub fn lock_revealed_funds>( &self, lock_id: WalletLockId, @@ -283,10 +258,11 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { pub fn get_unspent_outputs_by_account( &self, account_address: &ComponentAddress, + exclude_locked: bool, ) -> Result, StealthOutputsApiError> { let balance = self .store - .with_read_tx(|tx| tx.stealth_outputs_get_unspent_by_account(account_address))?; + .with_read_tx(|tx| tx.stealth_outputs_get_unspent_by_account(account_address, exclude_locked))?; Ok(balance) } @@ -314,13 +290,18 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { pub fn upsert_utxo(&self, utxo: &StealthOutputModel) -> Result<(), StealthOutputsApiError> { self.store.with_write_tx(|tx| { // TODO(perf): consider a dedicated exists query - let exists = tx + let maybe_utxo = tx .stealth_outputs_get_by_commitment(&utxo.resource_address, &utxo.commitment) - .optional()? - .is_some(); - if exists { + .optional()?; + if let Some(prev_utxo) = maybe_utxo { + let new_status = match prev_utxo.status { + OutputStatus::Unspent => Some(utxo.status), + // If not unspent, don't allow status to be changed. + // EDGE-CASE: scanning picks up a local UTXO that we know was spent + _ => None, + }; let address = utxo.to_utxo_address(); - tx.stealth_outputs_update(&address, Some(utxo.is_burnt), Some(utxo.status), Some(utxo.is_frozen)) + tx.stealth_outputs_update(&address, Some(utxo.is_burnt), new_status, Some(utxo.is_frozen)) } else { tx.stealth_outputs_insert(utxo) } diff --git a/crates/wallet/sdk/src/apis/stealth_transfer.rs b/crates/wallet/sdk/src/apis/stealth_transfer.rs index cd5bca3db4..ef5538a0c2 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer.rs @@ -28,6 +28,7 @@ use tari_template_lib::{ types::Amount, }; use tari_transaction::{args, Transaction, UnsignedTransaction}; +use tokio::sync::Semaphore; use crate::{ apis::{ @@ -39,7 +40,16 @@ use crate::{ stealth_outputs::{StealthOutputsApi, StealthOutputsApiError, TransferStatementParams}, substate::{SubstateApiError, SubstatesApi, ValidatorScanResult}, }, - models::{AccountWithAddress, InputSpendData, KeyBranch, KeyId, OutputStatus, StealthOutputModel, WalletLockId}, + models::{ + AccountWithAddress, + InputSpendData, + KeyBranch, + KeyId, + OutputStatus, + StealthOutputModel, + WalletLockId, + WalletPublicKey, + }, network::WalletNetworkInterface, storage::{WalletStorageError, WalletStore}, }; @@ -52,6 +62,7 @@ pub struct StealthTransferApi<'a, TStore, TNetworkInterface> { substate_api: SubstatesApi<'a, TStore, TNetworkInterface>, key_manager_api: KeyManagerApi<'a, TStore>, config_api: ConfigApi<'a, TStore>, + semaphore: Semaphore, } impl<'a, TStore, TNetworkInterface> StealthTransferApi<'a, TStore, TNetworkInterface> @@ -73,24 +84,10 @@ where substate_api, key_manager_api, config_api, + semaphore: Semaphore::new(1), } } - fn lock_fee_inputs( - &self, - lock_id: WalletLockId, - owner_account: &AccountWithAddress, - params: &StealthTransferParams, - ) -> Result { - self.lock_inputs_for_transfer( - lock_id, - owner_account.account().component_address(), - XTR, - params.max_fee.into(), - params.input_selection, - ) - } - #[allow(clippy::too_many_lines)] pub fn lock_inputs_for_transfer( &self, @@ -275,6 +272,22 @@ where } } + fn lock_fee_inputs>( + &self, + lock_id: WalletLockId, + owner_account: &AccountWithAddress, + max_fee: A, + input_selection: ConfidentialTransferInputSelection, + ) -> Result { + self.lock_inputs_for_transfer( + lock_id, + owner_account.account().component_address(), + XTR, + max_fee.into(), + input_selection, + ) + } + #[allow(clippy::too_many_lines)] pub async fn transfer( &self, @@ -335,7 +348,7 @@ where }, None => { // TODO: we're just determining if the account exists - symptom of a larger problem/missing - // feature where account is created as needed by the execution layer instead of having to be + // feature: the account should be created as needed by the execution layer, instead of having to be // determined by the client side let to_account_substate = self .substate_api @@ -406,21 +419,15 @@ where .try_from_byte_type() .expect("already validated"); + // Critical section + let _permit = self.semaphore.acquire().await.expect("semaphore is never closed"); + let lock_id = self.outputs_api.create_lock()?; // Lock up funds for fees and transfer - let fee_inputs_to_spend = - self.unlock_on_failure(lock_id, self.lock_fee_inputs(lock_id, &owner_account, ¶ms))?; - - let inputs_to_spend = self.unlock_on_failure( + let fee_inputs_to_spend = self.unlock_on_failure( lock_id, - self.lock_inputs_for_transfer( - lock_id, - owner_account.account().component_address(), - params.resource_address, - params.total_output_amount(), - params.input_selection, - ), + self.lock_fee_inputs(lock_id, &owner_account, params.max_fee, params.input_selection), )?; // TODO: use single db transaction across calls @@ -440,8 +447,7 @@ where // Figure out which signing key to use - if there are no revealed funds, which necessitate using a account // withdraw auth signature, then we can use a nonce key. - let must_sign_with_account_key = - fee_inputs_to_spend.revealed.is_positive() || inputs_to_spend.revealed.is_positive(); + let must_sign_with_account_key = fee_inputs_to_spend.revealed.is_positive(); let (signing_key_branch, signing_key_id) = if must_sign_with_account_key { (KeyBranch::Account, owner_key_id) } else { @@ -453,6 +459,7 @@ where .key_manager_api .get_public_key(signing_key_branch, signing_key_id)?; let required_signer_pk = required_signer.public_key.to_byte_type(); + let fee_signer = required_signer; // Generate fee transfer statement let fee_transfer_statement = self.unlock_on_failure( @@ -473,12 +480,19 @@ where // Add the unconfirmed fee change output to the wallet store if let Some(output) = fee_transfer_statement.outputs_statement.outputs.first() { + debug!( + target: LOG_TARGET, + "Adding FEE unconfirmed output with commitment {} for amount {} to account {}", + output.output.commitment, + fee_stealth_change_amt, + owner_account.component_address() + ); self.unlock_on_failure( lock_id, self.add_unconfirmed_output_from_statement( lock_id, &owner_account, - params.resource_address, + XTR, output, fee_stealth_change_amt, None, @@ -486,6 +500,38 @@ where )?; } + // NOTE: important to add this after we add the fee change, because this allows us to spend the fee change + // UTXO (XTR case) + let inputs_to_spend = self.unlock_on_failure( + lock_id, + self.lock_inputs_for_transfer( + lock_id, + owner_account.account().component_address(), + params.resource_address, + params.total_output_amount(), + params.input_selection, + ), + )?; + + // Signing key for main transfer intent + let must_sign_with_account_key = inputs_to_spend.revealed.is_positive(); + let (signing_key_branch, signing_key_id) = if must_sign_with_account_key { + (KeyBranch::Account, owner_key_id) + } else { + let next_index = + self.unlock_on_failure(lock_id, self.key_manager_api.next_derived_key_index(KeyBranch::Nonce))?; + (KeyBranch::Nonce, KeyId::derived(next_index)) + }; + let main_signer = if signing_key_branch == fee_signer.branch && signing_key_id == fee_signer.key_id { + None + } else { + let required_signer = self + .key_manager_api + .get_public_key(signing_key_branch, signing_key_id)?; + Some(required_signer) + }; + let required_signer_pk = main_signer.as_ref().unwrap_or(&fee_signer).public_key().to_byte_type(); + // If we're spending from the owner account, add the inputs if inputs_to_spend.revealed.is_positive() || fee_inputs_to_spend.revealed.is_positive() { substate_inputs.push(SubstateRequirement::unversioned(*owner_account.component_address())); @@ -553,12 +599,43 @@ where }), )?; + // Add the unconfirmed change output to the wallet store + // NOTE: we can get the nth element because outputs are guaranteed to be in the order we pass them to + // generate_transfer_statement + let index = if params.blinded_output_amount.is_positive() { + // Change output is second element + 1 + } else { + // otherwise, it's the first element + 0 + }; + if let Some(output) = transfer_statement.outputs_statement.outputs.get(index) { + debug!( + target: LOG_TARGET, + "Adding TRANSFER unconfirmed output with commitment {} for amount {} to account {}", + output.output.commitment, + change_amount, + owner_account.component_address() + ); + self.unlock_on_failure( + lock_id, + self.add_unconfirmed_output_from_statement( + lock_id, + &owner_account, + params.resource_address, + output, + change_amount, + None, + ), + )?; + } + // Add all input UTXO substates to transaction inputs substate_inputs.extend( fee_inputs_to_spend .inputs .iter() - // If spending XTR, we may lock the fee change UTXO for spending, however since this does not exist yet we do not include it as a tx input + // If spending XTR, we may lock the fee change UTXO for spending, however since this does not exist yet, we do not include it as a tx input .filter(|i| i.is_on_chain) .map(|i| &i.commitment) .map(|commitment| UtxoAddress::new(XTR, (*commitment).into())) @@ -592,12 +669,12 @@ where lock_id, fee_inputs: fee_inputs_to_spend, transfer_inputs: inputs_to_spend, - signing_key_branch, - signing_key_id, + additional_signer: main_signer, + main_signer: fee_signer, }) } - fn unlock_on_failure(&self, lock_id: WalletLockId, result: Result) -> Result { + pub fn unlock_on_failure(&self, lock_id: WalletLockId, result: Result) -> Result { match result { Ok(value) => Ok(value), Err(e) => { @@ -716,8 +793,8 @@ pub struct TransferOutput { pub lock_id: WalletLockId, pub fee_inputs: InputsToSpend, pub transfer_inputs: InputsToSpend, - pub signing_key_branch: KeyBranch, - pub signing_key_id: KeyId, + pub additional_signer: Option, + pub main_signer: WalletPublicKey, } #[derive(Debug)] diff --git a/crates/wallet/sdk/src/apis/transaction.rs b/crates/wallet/sdk/src/apis/transaction.rs index e0ccb34069..fa5a28a48f 100644 --- a/crates/wallet/sdk/src/apis/transaction.rs +++ b/crates/wallet/sdk/src/apis/transaction.rs @@ -19,9 +19,9 @@ use tari_template_lib::{ use tari_transaction::{Transaction, TransactionId}; use crate::{ - models::{NewAccountData, TransactionStatus, WalletTransaction, WalletTransactionUpdate}, + models::{NewAccountData, TransactionStatus, WalletLockId, WalletTransaction, WalletTransactionUpdate}, network::{StatusResponseError, TransactionFinalizedResult, WalletNetworkInterface, WalletQueryErrorStatus}, - storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, + storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter, WriteableWalletStore}, }; const LOG_TARGET: &str = "tari::ootle::wallet_sdk::apis::transaction"; @@ -89,21 +89,21 @@ where ) })?; }, - Err(err) => match err.get_status() { - WalletQueryErrorStatus::TransactionRejected { message } => { - warn!(target: LOG_TARGET, "Invalid transaction submission: {transaction_id} {message}"); - self.store.with_write_tx(|tx| { - tx.transactions_update( - WalletTransactionUpdate::new(transaction_id) - .with_new_status(TransactionStatus::InvalidTransaction) - .with_invalid_reason(&message), - ) - })?; - return Ok(false); - }, - _ => { - return Err(err.into()); - }, + Err(err) => { + return match err.get_status() { + WalletQueryErrorStatus::TransactionRejected { message } => { + warn!(target: LOG_TARGET, "Invalid transaction submission: {transaction_id} {message}"); + self.store.with_write_tx(|tx| { + tx.transactions_update( + WalletTransactionUpdate::new(transaction_id) + .with_new_status(TransactionStatus::InvalidTransaction) + .with_invalid_reason(&message), + ) + })?; + Ok(false) + }, + _ => Err(err.into()), + } }, } @@ -256,27 +256,25 @@ where .with_finalized_time(finalized_time), )?; - // if the transaction being processed is confidential, - // we should make sure that the account's locked outputs - // are either set to spent or released, depending if the + + // Make sure that any locked outputs are either set to spent or released, depending on if the // transaction was finalized or rejected. Always release for dry runs. - if transaction.is_dry_run || - !matches!( - new_status, - TransactionStatus::Accepted | TransactionStatus::OnlyFeeAccepted - ) - { + if transaction.is_dry_run { self.release_all_locks_for_transaction_internal(tx, transaction_id)?; } else { - // TODO: it becomes more complicated if the transaction is Fee accepted, we'll need to finalize - // spends relating to fees and release the rest - let lock_ids = tx.locks_get_by_transaction_id(transaction_id)?; - info!(target: LOG_TARGET, "Finalizing locked outputs for transaction {}: {:?}", transaction_id, lock_ids); - for lock_id in lock_ids { - tx.confidential_outputs_finalize_by_lock_id(lock_id)?; - tx.stealth_outputs_finalize_by_lock_id(lock_id)?; - tx.vaults_finalized_locked_revealed_funds(lock_id).optional()?; - tx.locks_delete(lock_id)?; + let maybe_diff = execution_result + .as_ref() + .and_then(|e| e.finalize.result.any_accept()); + match maybe_diff { + Some(diff) => { + if let Some(lock_id) = tx.locks_get_by_transaction_id(transaction_id).optional()? { + info!(target: LOG_TARGET, "Finalizing locked outputs for transaction {}: {}", transaction_id, lock_id); + tx.locks_unlock_finalized(lock_id, diff)?; + } + } + None => { + self.release_all_locks_for_transaction_internal(tx, transaction_id)?; + } } } @@ -294,21 +292,24 @@ where .with_write_tx(|tx| self.release_all_locks_for_transaction_internal(tx, transaction_id)) } + pub fn locks_set_transaction_id( + &self, + lock_id: WalletLockId, + transaction_id: TransactionId, + ) -> Result<(), TransactionApiError> { + self.store + .with_write_tx(|tx| tx.locks_link_transaction(lock_id, transaction_id))?; + Ok(()) + } + fn release_all_locks_for_transaction_internal( &self, - tx: &mut ::WriteTransaction<'_>, + tx: &mut ::WriteTransaction<'_>, transaction_id: TransactionId, ) -> Result<(), TransactionApiError> { - let lock_ids = tx.locks_get_by_transaction_id(transaction_id)?; - - debug!(target: LOG_TARGET, "Releasing {} locks (and associated outputs) for transaction {} that was not committed", lock_ids.len(), transaction_id); - for lock_id in lock_ids { - // Lock could be for confidential outputs or stealth outputs - tx.confidential_outputs_release_by_lock_id(lock_id)?; - tx.stealth_outputs_release_by_lock_id(lock_id)?; - // If the lock locks a vault, we need to release the revealed funds - tx.vaults_release_lock_revealed_funds(lock_id).optional()?; - tx.locks_delete(lock_id)?; + if let Some(lock_id) = tx.locks_get_by_transaction_id(transaction_id).optional()? { + debug!(target: LOG_TARGET, "Releasing lock {} (and associated outputs) for transaction {} that was not committed", lock_id, transaction_id); + tx.locks_release(lock_id)?; } Ok(()) @@ -352,7 +353,7 @@ where )?; for owned_id in indexed.referenced_substates() { - if let Some(pos) = other_substates.iter().position(|(addr, _)| addr == &owned_id) { + if let Some(pos) = other_substates.iter().position(|(addr, _)| *addr == owned_id) { let (_, child) = other_substates.swap_remove(pos); // If there was a previous parent for this substate, we keep it as is. let parent = downed_substates_with_parents diff --git a/crates/wallet/sdk/src/models/key.rs b/crates/wallet/sdk/src/models/key.rs index 174186fa46..6c5ff07a29 100644 --- a/crates/wallet/sdk/src/models/key.rs +++ b/crates/wallet/sdk/src/models/key.rs @@ -58,6 +58,7 @@ pub struct WalletKeyRecord { pub(crate) key_id: KeyId, pub(crate) public_key: RistrettoPublicKey, pub(crate) secret_key: RistrettoSecretKey, + pub(crate) branch: KeyBranch, pub(crate) is_active: bool, } @@ -73,6 +74,10 @@ impl WalletKeyRecord { pub fn public_key(&self) -> &RistrettoPublicKey { &self.public_key } + + pub fn branch(&self) -> KeyBranch { + self.branch + } } #[derive(Clone, serde::Serialize, serde::Deserialize)] @@ -101,6 +106,7 @@ impl ImportedWalletKey { #[derive(Clone)] pub struct DerivedWalletKey { pub key: RistrettoSecretKey, + pub branch: KeyBranch, pub key_index: DerivedKeyIndex, } @@ -114,18 +120,10 @@ impl DerivedWalletKey { } } -impl From for DerivedWalletKey { - fn from(key: tari_transaction_components::key_manager::tari_key_manager::DerivedKey) -> Self { - Self { - key: key.key, - key_index: key.key_index, - } - } -} - #[derive(Clone)] pub struct WalletPublicKey { pub public_key: RistrettoPublicKey, + pub branch: KeyBranch, pub key_id: KeyId, } @@ -143,6 +141,7 @@ impl From for WalletPublicKey { fn from(derived: DerivedWalletKey) -> Self { Self { key_id: derived.as_key_id(), + branch: derived.branch, public_key: derived.to_public_key(), } } diff --git a/crates/wallet/sdk/src/sdk.rs b/crates/wallet/sdk/src/sdk.rs index 074b904a99..15bce85617 100644 --- a/crates/wallet/sdk/src/sdk.rs +++ b/crates/wallet/sdk/src/sdk.rs @@ -95,6 +95,45 @@ where }) } + // pub fn create_read_context(&self) -> Result, WalletSdkError> { + // let read_tx = self.store.create_read_tx()?; + // Ok(SdkReadContext::new(read_tx)) + // } + // + // pub fn with_read_context(&self, f: F) -> Result + // where + // F: FnOnce(&mut SdkReadContext) -> Result, + // E: From, + // { + // let mut ctx = self.create_read_context()?; + // let ret = f(&mut ctx)?; + // Ok(ret) + // } + // + // pub fn create_write_context(&self) -> Result, WalletSdkError> { + // let write_tx = self.store.create_write_tx()?; + // Ok(SdkWriteContext::new(write_tx)) + // } + // + // pub fn with_write_context(&self, f: F) -> Result + // where + // F: FnOnce(&mut SdkWriteContext) -> Result, + // E: From, + // { + // let mut ctx = self.create_write_context()?; + // match f(&mut ctx) { + // Ok(r) => { + // ctx.commit()?; + // Ok(r) + // }, + // Err(e) => { + // warn!(target: LOG_TARGET, "Transaction failed! rollback"); + // ctx.rollback()?; + // Err(e.into()) + // }, + // } + // } + pub fn get_store_network(store: &TStore) -> Result, WalletSdkError> { let config_api = ConfigApi::new(store); let network = config_api.get(ConfigKey::Network).optional()?; diff --git a/crates/wallet/sdk/src/storage.rs b/crates/wallet/sdk/src/storage.rs index 1368d2cfc3..748fa69d43 100644 --- a/crates/wallet/sdk/src/storage.rs +++ b/crates/wallet/sdk/src/storage.rs @@ -6,7 +6,10 @@ use std::{ ops::{Deref, DerefMut}, }; -use tari_engine_types::{resource::Resource, substate::SubstateId}; +use tari_engine_types::{ + resource::Resource, + substate::{SubstateDiff, SubstateId}, +}; use tari_ootle_common_types::{ optional::IsNotFoundError, shard::Shard, @@ -53,13 +56,24 @@ use crate::models::{ WalletTransactionUpdate, }; -pub trait WalletStore { +pub trait ReadableWalletStore { type ReadTransaction<'a>: WalletStoreReader where Self: 'a; + + fn create_read_tx(&self) -> Result, WalletStorageError>; + + fn with_read_tx) -> Result, R, E>(&self, f: F) -> Result + where E: From { + let mut tx = self.create_read_tx()?; + let ret = f(&mut tx)?; + Ok(ret) + } +} + +pub trait WriteableWalletStore: ReadableWalletStore { type WriteTransaction<'a>: WalletStoreWriter + Deref> + DerefMut where Self: 'a; - fn create_read_tx(&self) -> Result, WalletStorageError>; fn create_write_tx(&self) -> Result, WalletStorageError>; fn with_write_tx) -> Result, R, E>(&self, f: F) -> Result @@ -78,15 +92,12 @@ pub trait WalletStore { }, } } - - fn with_read_tx) -> Result, R, E>(&self, f: F) -> Result - where E: From { - let mut tx = self.create_read_tx()?; - let ret = f(&mut tx)?; - Ok(ret) - } } +pub trait WalletStore: ReadableWalletStore + WriteableWalletStore {} + +impl WalletStore for T where T: ReadableWalletStore + WriteableWalletStore {} + #[derive(Debug, thiserror::Error)] pub enum WalletStorageError { #[error("General database failure for operation {operation}: {details}")] @@ -242,6 +253,7 @@ pub trait WalletStoreReader { fn stealth_outputs_get_unspent_by_account( &mut self, account_addr: &ComponentAddress, + exclude_locked: bool, ) -> Result, WalletStorageError>; fn stealth_outputs_get_locked_by_lock_id( @@ -265,7 +277,7 @@ pub trait WalletStoreReader { fn locks_get_by_transaction_id( &mut self, transaction_id: TransactionId, - ) -> Result, WalletStorageError>; + ) -> Result; // Non fungible tokens fn non_fungible_token_get_by_nft_id( @@ -320,10 +332,12 @@ pub trait WalletStoreReader { pub type TagAndPublicNoncePair = (UtxoTag, RistrettoPublicKeyBytes); -pub trait WalletStoreWriter { - fn commit(self) -> Result<(), WalletStorageError>; - fn rollback(self) -> Result<(), WalletStorageError>; +pub trait CommitableStore { + fn commit(&mut self) -> Result<(), WalletStorageError>; + fn rollback(&mut self) -> Result<(), WalletStorageError>; +} +pub trait WalletStoreWriter: CommitableStore { // JWT fn jwt_add_empty_token(&mut self) -> Result; fn jwt_store_decision(&mut self, id: u64, permissions_token: Option<&str>) -> Result<(), WalletStorageError>; @@ -443,10 +457,6 @@ pub trait WalletStoreWriter { resource_address: &ResourceAddress, id: &UtxoId, ) -> Result<(), WalletStorageError>; - /// Mark outputs locked by this lock id as finalized - fn stealth_outputs_finalize_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError>; - /// Release outputs that were locked and remove pending unconfirmed outputs for this lock - fn stealth_outputs_release_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError>; fn stealth_outputs_update( &mut self, address: &UtxoAddress, @@ -465,6 +475,14 @@ pub trait WalletStoreWriter { transaction_id: TransactionId, ) -> Result<(), WalletStorageError>; + /// Release the lock including all outputs and vaults that were locked. Release is used when a transaction is + /// aborted. + fn locks_release(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError>; + /// Finalize the lock according to the provided diff. Any outputs and vaults locked by this lock and included in the + /// diff are finalised (marked as unspent/funds removed/added as necessary). Any objects not included in the diff + /// are reverted and released from the lock. This is used when a transaction is committed. + fn locks_unlock_finalized(&mut self, lock_id: WalletLockId, diff: &SubstateDiff) -> Result<(), WalletStorageError>; + // Non fungible tokens fn non_fungible_token_upsert(&mut self, non_fungible_token: &NonFungibleToken) -> Result<(), WalletStorageError>; fn non_fungible_token_remove( diff --git a/crates/wallet/sdk/tests/confidential_output_api.rs b/crates/wallet/sdk/tests/confidential_output_api.rs index 9fd887ba9f..8c80e8fd5b 100644 --- a/crates/wallet/sdk/tests/confidential_output_api.rs +++ b/crates/wallet/sdk/tests/confidential_output_api.rs @@ -7,7 +7,7 @@ use tari_crypto::commitment::HomomorphicCommitmentFactory; use tari_engine_types::{crypto::get_commitment_factory, ToByteType}; use tari_ootle_wallet_sdk::{ models::{ConfidentialOutputModel, KeyId, OutputStatus}, - storage::{WalletStore, WalletStoreReader}, + storage::{ReadableWalletStore, WalletStoreReader}, }; use tari_template_lib::types::EncryptedData; diff --git a/crates/wallet/sdk_services/src/account_monitor/monitor.rs b/crates/wallet/sdk_services/src/account_monitor/monitor.rs index 381198d9bf..8589bc3323 100644 --- a/crates/wallet/sdk_services/src/account_monitor/monitor.rs +++ b/crates/wallet/sdk_services/src/account_monitor/monitor.rs @@ -95,14 +95,14 @@ where } pub async fn run(mut self) -> Result<(), anyhow::Error> { - info!(target: LOG_TARGET, "👁️‍🗨️ Account monitor started"); + info!(target: LOG_TARGET, "🏦 Account monitor started"); let mut poll_interval = time::interval(self.periodic_scan_interval); poll_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); loop { tokio::select! { _ = self.shutdown_signal.wait() => { - info!(target: LOG_TARGET, "👁️‍🗨️ Account monitor shutting down"); + info!(target: LOG_TARGET, "🏦 Account monitor shutting down"); break Ok(()); } @@ -125,7 +125,7 @@ where } async fn handle_request(&self, req: AccountMonitorRequest) { - debug!(target: LOG_TARGET, "👁️‍🗨️ Account monitor received request: {:?}", req); + debug!(target: LOG_TARGET, "🏦 Account monitor received request: {:?}", req); match req { AccountMonitorRequest::RefreshAccount { account, @@ -189,12 +189,12 @@ where if is_updated { info!( target: LOG_TARGET, - "👁️‍🗨️ Account {} has been updated", account + "🏦 Account {} has been updated", account ); } else { info!( target: LOG_TARGET, - "👁️‍🗨️ Account {} is up to date", account + "🏦 Account {} is up to date", account ); } } @@ -219,12 +219,12 @@ where if is_updated { info!( target: LOG_TARGET, - "👁️‍🗨️ Account {} updated", account_address + "🏦 Account {} updated", account_address ); } else { info!( target: LOG_TARGET, - "👁️‍🗨️ Account {} is up to date", account_address + "🏦 Account {} is up to date", account_address ); } Ok(is_updated) @@ -238,7 +238,7 @@ where info!( target: LOG_TARGET, - "👁️‍🗨️ Requesting UTXO scan for account {} for {} stealth resource(s)", + "🏦 Requesting UTXO scan for account {} for {} stealth resource(s)", account_address, associated_resources.len() ); diff --git a/crates/wallet/sdk_services/src/account_monitor/scanner.rs b/crates/wallet/sdk_services/src/account_monitor/scanner.rs index 49c480089b..00b5961145 100644 --- a/crates/wallet/sdk_services/src/account_monitor/scanner.rs +++ b/crates/wallet/sdk_services/src/account_monitor/scanner.rs @@ -35,6 +35,7 @@ use crate::{ const LOG_TARGET: &str = "tari::ootle::wallet_services::account_monitor"; +#[derive(Debug, Clone)] pub struct AccountScanner { notify: Notify, wallet_sdk: WalletSdk, @@ -53,7 +54,7 @@ where pub async fn refresh_account(&self, account_address: ComponentAddress) -> Result { info!( target: LOG_TARGET, - "👁️‍🗨️ Refreshing account {}", account_address + "🏦 Refreshing account {}", account_address ); let substate_api = self.wallet_sdk.substate_api(); let accounts_api = self.wallet_sdk.accounts_api(); @@ -410,7 +411,7 @@ where #[allow(clippy::too_many_lines)] pub async fn process_result( - &mut self, + &self, tx_id: TransactionId, diff: &SubstateDiff, new_account_data: Option, @@ -428,7 +429,7 @@ where new_account = existing_account; debug!( target: LOG_TARGET, - "👁️‍🗨️ New account {} created in transaction {}", + "🏦 New account {} created in transaction {}", account.address, tx_id ); @@ -443,7 +444,7 @@ where } else { info!( target: LOG_TARGET, - "👁️‍🗨️ Account {} already exists and on-chain (processing transaction result {})", + "🏦 Account {} already exists and on-chain (processing transaction result {})", account.address, tx_id ); @@ -468,7 +469,7 @@ where Err(e) => { error!( target: LOG_TARGET, - "👁️‍🗨️ Failed to parse account substate {} in tx {}: {}", a, tx_id, e + "🏦 Failed to parse account substate {} in tx {}: {}", a, tx_id, e ); None }, @@ -521,7 +522,7 @@ where for (vault_id, substate) in vaults { let vault_addr = SubstateId::Vault(vault_id); let SubstateValue::Vault(vault) = substate.substate_value() else { - error!(target: LOG_TARGET, "👁️‍🗨️ Substate {} is not a vault. This should be impossible.", vault_addr); + error!(target: LOG_TARGET, "🏦 Substate {} is not a vault. This should be impossible.", vault_addr); continue; }; @@ -529,13 +530,13 @@ where let maybe_vault_substate = substate_api.get_substate(&vault_addr).optional()?; let Some(vault_substate) = maybe_vault_substate else { // This should be impossible. - error!(target: LOG_TARGET, "👁️‍🗨️ Vault {} is not a known substate.", vault_addr); + error!(target: LOG_TARGET, "🏦 Vault {} is not a known substate.", vault_addr); continue; }; let Some(account_addr) = vault_substate.parent_address else { // Happens if this is someone else's vault - debug!(target: LOG_TARGET, "👁️‍🗨️ Vault {} has no parent component.", vault_addr); + debug!(target: LOG_TARGET, "🏦 Vault {} has no parent component.", vault_addr); continue; }; let account_addr = account_addr.as_component_address().unwrap_or_else(|| { @@ -549,7 +550,7 @@ where if accounts_api.get_account_by_address(&account_addr).optional()?.is_none() { info!( target: LOG_TARGET, - "👁️‍🗨️ Vault {} not in any known account", + "🏦 Vault {} not in any known account", vault_addr, ); continue; @@ -595,7 +596,7 @@ where if let Some(account) = new_account { debug!( target: LOG_TARGET, - "👁️‍🗨️ Notifying account created for tx {}: {}", + "🏦 Notifying account created for tx {}: {}", tx_id, account ); @@ -607,7 +608,7 @@ where if !updated_accounts.is_empty() { debug!( target: LOG_TARGET, - "👁️‍🗨️ Notifying {} account(s) changed for tx {}", + "🏦 Notifying {} account(s) changed for tx {}", updated_accounts.len(), tx_id ); @@ -655,7 +656,7 @@ where Err(e) => { warn!( target: LOG_TARGET, - "👁️‍🗨️ Failed to scan vault {} from VN: {}", + "🏦 Failed to scan vault {} from VN: {}", vault_id, e ); @@ -670,7 +671,7 @@ where info!( target: LOG_TARGET, - "👁️‍🗨️ New {} in account {}", + "🏦 New {} in account {}", vault_id, account_addr ); diff --git a/crates/wallet/sdk_services/src/indexer_rest_api.rs b/crates/wallet/sdk_services/src/indexer_rest_api.rs index acf3e3f831..d9f5f2eb1f 100644 --- a/crates/wallet/sdk_services/src/indexer_rest_api.rs +++ b/crates/wallet/sdk_services/src/indexer_rest_api.rs @@ -252,6 +252,8 @@ impl WalletNetworkInterface for IndexerRestApiNetworkInterface { tag_and_nonce_pairs: Vec<(UtxoTag, RistrettoPublicKeyBytes)>, ) -> Result, Self::Error> { let mut client = self.get_client()?; + // TODO: Given the potential size of substates protobuf, json + hex encoding may be too inefficient. Consider + // supporting the application/x-protobuf content type in the indexer REST API. let resp = client .get_unspent_utxos(GetUnspentUtxosRequest { resource_address, diff --git a/crates/wallet/sdk_services/src/transaction_service/handle.rs b/crates/wallet/sdk_services/src/transaction_service/handle.rs index 50d2e2f8a1..ef230e81c6 100644 --- a/crates/wallet/sdk_services/src/transaction_service/handle.rs +++ b/crates/wallet/sdk_services/src/transaction_service/handle.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_engine_types::commit_result::ExecuteResult; -use tari_ootle_wallet_sdk::models::NewAccountData; +use tari_ootle_wallet_sdk::models::{NewAccountData, WalletLockId}; use tari_transaction::{Transaction, TransactionId}; use tokio::sync::{mpsc, oneshot}; @@ -14,6 +14,7 @@ pub(super) enum TransactionServiceRequest { SubmitTransaction { transaction: Transaction, new_account_info: Option, + lock_id: Option, reply: Reply>, }, @@ -36,7 +37,7 @@ impl TransactionServiceHandle { impl TransactionServiceHandle { pub async fn submit_transaction(&self, transaction: Transaction) -> Result { - self.submit_transaction_with_opts(transaction, None).await + self.submit_transaction_with_opts(transaction, None, None).await } pub async fn submit_dry_run_transaction( @@ -58,12 +59,14 @@ impl TransactionServiceHandle { &self, transaction: Transaction, new_account_info: Option, + lock_id: Option, ) -> Result { let (reply_tx, reply_rx) = oneshot::channel(); self.sender .send(TransactionServiceRequest::SubmitTransaction { transaction, new_account_info, + lock_id, reply: reply_tx, }) .await diff --git a/crates/wallet/sdk_services/src/transaction_service/service.rs b/crates/wallet/sdk_services/src/transaction_service/service.rs index cefe27fcd6..9881eef39b 100644 --- a/crates/wallet/sdk_services/src/transaction_service/service.rs +++ b/crates/wallet/sdk_services/src/transaction_service/service.rs @@ -7,7 +7,7 @@ use log::*; use tari_engine_types::commit_result::ExecuteResult; use tari_ootle_common_types::optional::IsNotFoundError; use tari_ootle_wallet_sdk::{ - models::{NewAccountData, TransactionStatus}, + models::{NewAccountData, TransactionStatus, WalletLockId}, network::{StatusResponseError, WalletNetworkInterface}, storage::WalletStore, WalletSdk, @@ -109,10 +109,14 @@ where TransactionServiceRequest::SubmitTransaction { transaction, new_account_info, + lock_id, reply, } => { reply - .send(self.handle_submit_transaction(transaction, new_account_info).await) + .send( + self.handle_submit_transaction(transaction, new_account_info, lock_id) + .await, + ) .map_err(|_| TransactionServiceError::ServiceShutdown)?; }, TransactionServiceRequest::SubmitDryRunTransaction { transaction, reply } => { @@ -149,9 +153,15 @@ where &self, transaction: Transaction, new_account_info: Option, + lock_id: Option, ) -> Result { let transaction_api = self.wallet_sdk.transaction_api(); let transaction_id = transaction_api.insert_new_transaction(transaction, new_account_info.clone(), false)?; + + if let Some(lock_id) = lock_id { + transaction_api.locks_set_transaction_id(lock_id, transaction_id)?; + } + if transaction_api.submit_transaction(transaction_id).await? { self.notify.notify(TransactionSubmittedEvent { transaction_id, diff --git a/crates/wallet/sdk_services/src/utxo_scanner/scanner_round.rs b/crates/wallet/sdk_services/src/utxo_scanner/scanner_round.rs index 74d898cb45..8947e5807d 100644 --- a/crates/wallet/sdk_services/src/utxo_scanner/scanner_round.rs +++ b/crates/wallet/sdk_services/src/utxo_scanner/scanner_round.rs @@ -148,7 +148,7 @@ where // Commit the previous progress and start with the next shard if let Some(sos) = sos.take() { if num_received > 0 { - info!( + debug!( target: LOG_TARGET, "🔍️ Scan complete for account {}: No more stealth outputs found in shard {} (max state version {})", self.account.component_address(), diff --git a/crates/wallet/sdk_services/src/utxo_scanner/utxo_recovery.rs b/crates/wallet/sdk_services/src/utxo_scanner/utxo_recovery.rs index 68a03e3f19..07fc6d558f 100644 --- a/crates/wallet/sdk_services/src/utxo_scanner/utxo_recovery.rs +++ b/crates/wallet/sdk_services/src/utxo_scanner/utxo_recovery.rs @@ -12,7 +12,7 @@ use tari_ootle_common_types::{ use tari_ootle_wallet_sdk::{ models::AccountAndViewKeys, network::{StatusResponseError, WalletNetworkInterface}, - storage::{WalletStore, WalletStoreReader, WalletStoreWriter}, + storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, WalletSdk, }; use tari_template_lib::models::{ComponentAddress, ResourceAddress, UtxoAddress, UtxoId}; @@ -90,6 +90,7 @@ where } } + #[allow(clippy::too_many_lines)] pub async fn process_utxo_validation_queue(&mut self) -> Result<(), StealthScannerApiError> { let mut start_event_published = false; let mut num_recovered = 0; @@ -130,6 +131,10 @@ where } let tag_and_nonce_pairs = tag_and_nonce_to_view_key_map.keys().copied().collect(); + // TODO(perf): we should check if any UTXOs are already known (by tag and nonce?) because we + // created/spent them locally, and skip querying those. This would also avoid having to deal with this + // downstream (previous bug caused spent to be marked as unspent). + // max 3.3kB per request (excl underlying protocol overhead *cough* json + hex) let utxos = self .sdk @@ -138,19 +143,32 @@ where .await .map_err(|e| StealthScannerApiError::NetworkInterfaceError(e.into()))?; + self.sdk.store().with_write_tx(|tx| { + // We're trusting that the indexer is up to date and accurate. If any UTXOs we asked for are not + // returned, remove them from the queue to avoid retrying forever because they are presumably spent. + let missing_utxos = tag_and_nonce_to_view_key_map + .keys() + .filter(|(tag, nonce)| { + !utxos.iter().any(|(_, utxo)| { + utxo.output.as_ref().is_some_and(|output| utxo.tag() == Some(*tag) && output.output.public_nonce == *nonce) + }) + }); + + let mut count = 0usize; + for (tag, nonce) in missing_utxos { + count += 1; + tx.utxo_process_queue_remove_item(*resource_addr, *tag, *nonce)?; + } + if count > 0 { + debug!(target: LOG_TARGET, "❓️ Removed {} missing UTXOs from the processing queue for resource {} as they were not returned by the indexer.", count, resource_addr); + } + Ok::<_, WalletStorageError>(()) + })?; + if utxos.is_empty() { - // We asked for some UTXOs but got none back. This should never happen because UTXO recovery is - // 'fed' by UTXO scanning, which should only give recovery tasks if the network - // has UTXOs. This could indicate a bug in the indexer (assuming that NetworkInterface impl is - // used). To prevent this case causing fast spinning, return an error that will - // sleep and retry. - return Err(StealthScannerApiError::UnexpectedResponse { - details: format!( - "{} UTXOs requested but network returned an empty set for resource {}.", - tag_and_nonce_to_view_key_map.len(), - resource_addr - ), - }); + // We asked for some UTXOs but got none back. This could happen if the UTXOs were all spent later. + // Ignore this and continue syncing. + continue; } if utxos.len() != tag_and_nonce_to_view_key_map.len() { diff --git a/crates/wallet/storage_sqlite/migrations/2023-02-08-122514_initial/up.sql b/crates/wallet/storage_sqlite/migrations/2023-02-08-122514_initial/up.sql index 74e52c7dfe..9a9b1aa9ca 100644 --- a/crates/wallet/storage_sqlite/migrations/2023-02-08-122514_initial/up.sql +++ b/crates/wallet/storage_sqlite/migrations/2023-02-08-122514_initial/up.sql @@ -101,23 +101,33 @@ CREATE UNIQUE INDEX accounts_uniq_name ON accounts (name) WHERE name IS NOT NULL -- Vaults CREATE TABLE vaults ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - account_id INTEGER NOT NULL REFERENCES accounts (id), - address TEXT NOT NULL, - resource_address TEXT NOT NULL, - resource_type TEXT NOT NULL, - revealed_balance BIGINT NOT NULL DEFAULT 0, - confidential_balance BIGINT NOT NULL DEFAULT 0, - locked_revealed_balance BIGINT NOT NULL DEFAULT 0, - token_symbol TEXT NULL, - divisibility INTEGER NOT NULL DEFAULT 0, - locked_by INTEGER NULL REFERENCES locks (id) ON DELETE SET NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES accounts (id), + address TEXT NOT NULL, + resource_address TEXT NOT NULL, + resource_type TEXT NOT NULL, + revealed_balance BIGINT NOT NULL DEFAULT 0, + confidential_balance BIGINT NOT NULL DEFAULT 0, + token_symbol TEXT NULL, + divisibility INTEGER NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX vaults_uniq_address ON vaults (address); +-- Vault locks +CREATE TABLE vault_locks +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + vault_id INTEGER NOT NULL REFERENCES vaults (id) ON DELETE CASCADE, + lock_id INTEGER NOT NULL REFERENCES locks (id) ON DELETE CASCADE, + amount BIGINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX vault_locks_uniq_vault_lock ON vault_locks (vault_id, lock_id); + -- Resources CREATE TABLE resources ( @@ -173,6 +183,8 @@ CREATE TABLE locks created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); +CREATE UNIQUE INDEX locks_uniq_transaction_id ON locks (transaction_id) WHERE transaction_id IS NOT NULL; + -- Auth token, we don't store the auth token, the token in this table is the jwt token that is granted when user accepts the auth login request. CREATE TABLE auth_status ( diff --git a/crates/wallet/storage_sqlite/src/lib.rs b/crates/wallet/storage_sqlite/src/lib.rs index e2284793a9..83140a7fcc 100644 --- a/crates/wallet/storage_sqlite/src/lib.rs +++ b/crates/wallet/storage_sqlite/src/lib.rs @@ -18,7 +18,7 @@ use std::{ use diesel::{sql_query, Connection, RunQueryDsl, SqliteConnection}; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; -use tari_ootle_wallet_sdk::storage::{WalletStorageError, WalletStore}; +use tari_ootle_wallet_sdk::storage::{ReadableWalletStore, WalletStorageError, WriteableWalletStore}; use crate::{reader::ReadTransaction, writer::WriteTransaction}; @@ -54,9 +54,8 @@ impl SqliteWalletStore { } } -impl WalletStore for SqliteWalletStore { +impl ReadableWalletStore for SqliteWalletStore { type ReadTransaction<'a> = ReadTransaction<'a>; - type WriteTransaction<'a> = WriteTransaction<'a>; fn create_read_tx(&self) -> Result, WalletStorageError> { let mut lock = self.connection.lock().unwrap(); @@ -65,6 +64,10 @@ impl WalletStore for SqliteWalletStore { .map_err(|e| WalletStorageError::general("BEGIN transaction", e))?; Ok(ReadTransaction::new(lock)) } +} + +impl WriteableWalletStore for SqliteWalletStore { + type WriteTransaction<'a> = WriteTransaction<'a>; fn create_write_tx(&self) -> Result, WalletStorageError> { let mut lock = self.connection.lock().unwrap(); diff --git a/crates/wallet/storage_sqlite/src/models/vault.rs b/crates/wallet/storage_sqlite/src/models/vault.rs index 35054b3cfa..44528ce1d4 100644 --- a/crates/wallet/storage_sqlite/src/models/vault.rs +++ b/crates/wallet/storage_sqlite/src/models/vault.rs @@ -3,6 +3,7 @@ use std::str::FromStr; +use bigdecimal::{BigDecimal, ToPrimitive}; use diesel::{Identifiable, Queryable}; use tari_ootle_wallet_sdk::storage::WalletStorageError; use tari_template_lib::{ @@ -23,10 +24,8 @@ pub struct Vault { pub resource_type: String, pub revealed_balance: i64, pub confidential_balance: i64, - pub locked_revealed_balance: i64, pub token_symbol: Option, pub divisibility: i32, - pub _locked_by: Option, pub created_at: PrimitiveDateTime, pub updated_at: PrimitiveDateTime, } @@ -35,6 +34,7 @@ impl Vault { pub(crate) fn try_into_vault( self, account_address: ComponentAddress, + locked_revealed_balance: BigDecimal, ) -> Result { Ok(tari_ootle_wallet_sdk::models::VaultModel { account_address, @@ -59,7 +59,12 @@ impl Vault { })?, token_symbol: self.token_symbol, revealed_balance: Amount::from(self.revealed_balance), - locked_revealed_balance: Amount::from(self.locked_revealed_balance), + locked_revealed_balance: Amount::from( + locked_revealed_balance + .to_u128() + // Should be impossible because sqlite is limited to i64 + .expect("locked more than u128::MAX funds"), + ), confidential_balance: Amount::from(self.confidential_balance), divisibility: u8::try_from(self.divisibility as u32).map_err(|e| WalletStorageError::DecodingError { operation: "try_into_vault", diff --git a/crates/wallet/storage_sqlite/src/reader.rs b/crates/wallet/storage_sqlite/src/reader.rs index 07a46902bc..2a34141f44 100644 --- a/crates/wallet/storage_sqlite/src/reader.rs +++ b/crates/wallet/storage_sqlite/src/reader.rs @@ -88,7 +88,7 @@ impl<'a> ReadTransaction<'a> { } /// Internal commit - pub(super) fn commit(&mut self) -> Result<(), WalletStorageError> { + pub(super) fn commit_internal(&mut self) -> Result<(), WalletStorageError> { sql_query("COMMIT") .execute(self.connection()) .map_err(|e| WalletStorageError::general("commit", e))?; @@ -97,7 +97,7 @@ impl<'a> ReadTransaction<'a> { } /// Internal rollback - pub(super) fn rollback(&mut self) -> Result<(), WalletStorageError> { + pub(super) fn rollback_internal(&mut self) -> Result<(), WalletStorageError> { sql_query("ROLLBACK") .execute(self.connection()) .map_err(|e| WalletStorageError::general("rollback", e))?; @@ -512,7 +512,7 @@ impl WalletStoreReader for ReadTransaction<'_> { // -------------------------------- Vaults -------------------------------- // fn vaults_get(&mut self, vault_id: &VaultId) -> Result { - use crate::schema::{accounts, vaults}; + use crate::schema::{accounts, vault_locks, vaults}; let row = vaults::table .filter(vaults::address.eq(vault_id.to_string())) @@ -531,13 +531,20 @@ impl WalletStoreReader for ReadTransaction<'_> { .first::(self.connection()) .map_err(|e| WalletStorageError::general("vaults_get", e))?; - let vault = row.try_into_vault(ComponentAddress::from_str(&account_address).map_err(|e| { - WalletStorageError::DecodingError { + let locked_revealed_balance = vault_locks::table + .select(dsl::sum(vault_locks::amount)) + .filter(vault_locks::vault_id.eq(row.id)) + .first::>(self.connection()) + .map_err(|e| WalletStorageError::general("vaults_get", e))? + .unwrap_or_default(); + + let component_addr = + ComponentAddress::from_str(&account_address).map_err(|e| WalletStorageError::DecodingError { operation: "vaults_get", item: "vault", details: e.to_string(), - } - })?)?; + })?; + let vault = row.try_into_vault(component_addr, locked_revealed_balance)?; Ok(vault) } @@ -558,16 +565,16 @@ impl WalletStoreReader for ReadTransaction<'_> { account_addr: &ComponentAddress, resource_address: &ResourceAddress, ) -> Result { - use crate::schema::{accounts, vaults}; - - let account_id = accounts::table - .filter(accounts::address.eq(account_addr.to_string())) - .select(accounts::id) - .first::(self.connection()) - .map_err(|e| WalletStorageError::general("vaults_get_by_resource", e))?; + use crate::schema::{accounts, vault_locks, vaults}; let row = vaults::table - .filter(vaults::account_id.eq(account_id)) + .filter( + vaults::account_id.eq(accounts::table + .filter(accounts::address.eq(account_addr.to_string())) + .select(accounts::id) + .single_value() + .assume_not_null()), + ) .filter(vaults::resource_address.eq(resource_address.to_string())) .first::(self.connection()) .optional() @@ -578,8 +585,15 @@ impl WalletStoreReader for ReadTransaction<'_> { key: resource_address.to_string(), })?; + let locked_revealed_balance = vault_locks::table + .select(dsl::sum(vault_locks::amount)) + .filter(vault_locks::vault_id.eq(row.id)) + .first::>(self.connection()) + .map_err(|e| WalletStorageError::general("vaults_get", e))? + .unwrap_or_default(); + let vault = row - .try_into_vault(*account_addr) + .try_into_vault(*account_addr, locked_revealed_balance) .map_err(|e| WalletStorageError::DecodingError { operation: "vaults_get_by_resource", item: "vault", @@ -592,16 +606,17 @@ impl WalletStoreReader for ReadTransaction<'_> { &mut self, account_addr: &ComponentAddress, ) -> Result, WalletStorageError> { - use crate::schema::{accounts, vaults}; + const OPERATION: &str = "vaults_get_by_account"; + use crate::schema::{accounts, vault_locks, vaults}; let account_id = accounts::table .filter(accounts::address.eq(account_addr.to_string())) .select(accounts::id) .first::(self.connection()) .optional() - .map_err(|e| WalletStorageError::general("vaults_get_by_account", e))? + .map_err(|e| WalletStorageError::general(OPERATION, e))? .ok_or_else(|| WalletStorageError::NotFound { - operation: "vaults_get_by_account", + operation: OPERATION, entity: "account".to_string(), key: account_addr.to_string(), })?; @@ -609,11 +624,27 @@ impl WalletStoreReader for ReadTransaction<'_> { let rows = vaults::table .filter(vaults::account_id.eq(account_id)) .load::(self.connection()) - .map_err(|e| WalletStorageError::general("vaults_get_by_account", e))?; + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + + let locked_revealed_balances = vault_locks::table + .group_by(vault_locks::vault_id) + .select((vault_locks::vault_id, dsl::sum(vault_locks::amount))) + .filter(vault_locks::vault_id.eq_any(rows.iter().map(|r| r.id))) + .load_iter::<(i32, Option), _>(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + + let mut locked_balance_map = HashMap::new(); + for res in locked_revealed_balances { + let (id, balance) = res.map_err(|e| WalletStorageError::general(OPERATION, e))?; + locked_balance_map.insert(id, balance.unwrap_or_default()); + } let vaults = rows .into_iter() - .map(|row| row.try_into_vault(*account_addr)) + .map(|row| { + let locked_revealed_balance = locked_balance_map.remove(&row.id).unwrap_or_default(); + row.try_into_vault(*account_addr, locked_revealed_balance) + }) .collect::>()?; Ok(vaults) @@ -888,11 +919,12 @@ impl WalletStoreReader for ReadTransaction<'_> { fn stealth_outputs_get_unspent_by_account( &mut self, account_addr: &ComponentAddress, + exclude_locked: bool, ) -> Result, WalletStorageError> { const OPERATION: &str = "stealth_outputs_get_all_by_account"; use crate::schema::{accounts, stealth_outputs}; - let rows = stealth_outputs::table + let mut query = stealth_outputs::table .filter( stealth_outputs::owner_account_id.eq(accounts::table .select(accounts::id) @@ -902,10 +934,21 @@ impl WalletStoreReader for ReadTransaction<'_> { .assume_not_null()), ) .filter(stealth_outputs::status.eq(OutputStatus::Unspent.as_key_str())) - .get_results::(self.connection()) + .into_boxed(); + + if exclude_locked { + query = query.filter(stealth_outputs::lock_id.is_null()); + } + + let rows = query + .load_iter::(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; - rows.into_iter().map(|row| row.try_convert(*account_addr)).collect() + rows.map(|row| { + row.map_err(|e| WalletStorageError::general(OPERATION, e)) + .and_then(|row| row.try_convert(*account_addr)) + }) + .collect() } fn stealth_outputs_get_locked_by_lock_id( @@ -1029,16 +1072,22 @@ impl WalletStoreReader for ReadTransaction<'_> { fn locks_get_by_transaction_id( &mut self, transaction_id: TransactionId, - ) -> Result, WalletStorageError> { + ) -> Result { use crate::schema::locks; - let lock_ids = locks::table + let lock_id = locks::table .filter(locks::transaction_id.eq(serialize_hex(transaction_id))) .select(locks::id) - .get_results::(self.connection()) - .map_err(|e| WalletStorageError::general("locks_get_by_transaction_id", e))?; + .first::(self.connection()) + .optional() + .map_err(|e| WalletStorageError::general("locks_get_by_transaction_id", e))? + .ok_or_else(|| WalletStorageError::NotFound { + operation: "locks_get_by_transaction_id", + entity: "locks".to_string(), + key: serialize_hex(transaction_id), + })?; - Ok(lock_ids) + Ok(lock_id) } fn non_fungible_token_get_by_nft_id( @@ -1346,7 +1395,7 @@ impl WalletStoreReader for ReadTransaction<'_> { impl Drop for ReadTransaction<'_> { fn drop(&mut self) { if !self.is_done { - if let Err(err) = self.rollback() { + if let Err(err) = self.rollback_internal() { error!(target: LOG_TARGET, "Failed to rollback transaction: {}", err); } } diff --git a/crates/wallet/storage_sqlite/src/schema.rs b/crates/wallet/storage_sqlite/src/schema.rs index ac65adb2e0..aa888d286f 100644 --- a/crates/wallet/storage_sqlite/src/schema.rs +++ b/crates/wallet/storage_sqlite/src/schema.rs @@ -215,6 +215,16 @@ diesel::table! { } } +diesel::table! { + vault_locks (id) { + id -> Integer, + vault_id -> Integer, + lock_id -> Integer, + amount -> BigInt, + created_at -> Timestamp, + } +} + diesel::table! { vaults (id) { id -> Integer, @@ -224,10 +234,8 @@ diesel::table! { resource_type -> Text, revealed_balance -> BigInt, confidential_balance -> BigInt, - locked_revealed_balance -> BigInt, token_symbol -> Nullable, divisibility -> Integer, - locked_by -> Nullable, created_at -> Timestamp, updated_at -> Timestamp, } @@ -259,8 +267,9 @@ diesel::joinable!(shard_state_versions -> accounts (account_id)); diesel::joinable!(shard_state_versions -> resources (resource_id)); diesel::joinable!(stealth_outputs -> accounts (owner_account_id)); diesel::joinable!(utxo_process_queue -> accounts (account_id)); +diesel::joinable!(vault_locks -> locks (lock_id)); +diesel::joinable!(vault_locks -> vaults (vault_id)); diesel::joinable!(vaults -> accounts (account_id)); -diesel::joinable!(vaults -> locks (locked_by)); diesel::joinable!(webauthn_registration_passkeys -> webauthn_registrations (registration_id)); diesel::allow_tables_to_appear_in_same_query!( @@ -279,6 +288,7 @@ diesel::allow_tables_to_appear_in_same_query!( substates, transactions, utxo_process_queue, + vault_locks, vaults, webauthn_registration_passkeys, webauthn_registrations, diff --git a/crates/wallet/storage_sqlite/src/writer.rs b/crates/wallet/storage_sqlite/src/writer.rs index b4cf14139e..ae19adfce7 100644 --- a/crates/wallet/storage_sqlite/src/writer.rs +++ b/crates/wallet/storage_sqlite/src/writer.rs @@ -4,7 +4,7 @@ use std::{ collections::HashSet, iter, - ops::{Deref, DerefMut, Sub}, + ops::{Add, Deref, DerefMut, Sub}, str::FromStr, sync::MutexGuard, }; @@ -21,8 +21,11 @@ use diesel::{ use log::*; use serde::Serialize; use tari_bor::json_encoding::CborValueJsonSerializeWrapper; -use tari_engine_types::{resource::Resource, substate::SubstateId}; -use tari_ootle_common_types::{shard::Shard, StateVersion, VersionedSubstateIdRef}; +use tari_engine_types::{ + resource::Resource, + substate::{SubstateDiff, SubstateId}, +}; +use tari_ootle_common_types::{optional::Optional, shard::Shard, StateVersion, VersionedSubstateIdRef}; use tari_ootle_wallet_sdk::{ models::{ AccountUpdate, @@ -42,7 +45,7 @@ use tari_ootle_wallet_sdk::{ WalletLockId, WalletTransactionUpdate, }, - storage::{WalletStorageError, WalletStoreReader, WalletStoreWriter}, + storage::{CommitableStore, WalletStorageError, WalletStoreReader, WalletStoreWriter}, }; use tari_template_lib::{ models::{ComponentAddress, NonFungibleId, ResourceAddress, UtxoAddress, UtxoId, VaultId}, @@ -63,8 +66,7 @@ use crate::{ models, models::StealthOutputUpdate, reader::ReadTransaction, - schema::accounts, - serialization::{deserialize_json, serialize_hex, serialize_json}, + serialization::{deserialize_hex_try_from, deserialize_json, serialize_hex, serialize_json}, }; const LOG_TARGET: &str = "auth::tari::dan::wallet_sdk::storage_sqlite::writer"; @@ -100,19 +102,148 @@ impl<'a> WriteTransaction<'a> { } Ok(()) } + + fn stealth_outputs_release_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { + const OPERATION: &str = "stealth_outputs_release_by_lock_id"; + use crate::schema::stealth_outputs; + + // Unlock locked unspent stealth_outputs + diesel::update(stealth_outputs::table) + .filter(stealth_outputs::lock_id.eq(lock_id)) + .filter(stealth_outputs::status.eq(OutputStatus::LockedForSpend.as_key_str())) + .filter(stealth_outputs::is_on_chain.eq(true)) + .set(( + stealth_outputs::status.eq(OutputStatus::Unspent.as_key_str()), + stealth_outputs::lock_id.eq::>(None), + stealth_outputs::locked_at.eq::>(None), + )) + .execute(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + + // Remove stealth_outputs that were created by this lock + diesel::delete(stealth_outputs::table) + .filter(stealth_outputs::lock_id.eq(lock_id)) + .filter( + stealth_outputs::status + .eq(OutputStatus::LockedUnconfirmed.as_key_str()) + .or(stealth_outputs::status + .eq(OutputStatus::LockedForSpend.as_key_str()) + .and(stealth_outputs::is_on_chain.eq(false))), + ) + .execute(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + + Ok(()) + } + + fn stealth_outputs_finalize_by_lock_id( + &mut self, + lock_id: WalletLockId, + diff: &SubstateDiff, + ) -> Result<(), WalletStorageError> { + const OPERATION: &str = "stealth_outputs_finalize_by_lock_id"; + use crate::schema::stealth_outputs; + + // Fetch the outputs locked by this lock_id + let locked_outputs = stealth_outputs::table + .select(( + stealth_outputs::id, + stealth_outputs::resource_address, + stealth_outputs::commitment, + )) + .filter(stealth_outputs::lock_id.eq(lock_id)) + .load_iter::<(i32, String, String), _>(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + + let up_id_index = diff + .up_iter() + .filter_map(|(id, _)| id.as_utxo_address()) + .collect::>(); + let down_id_index = diff + .down_iter() + .filter_map(|(id, _)| id.as_utxo_address()) + .collect::>(); + let mut to_confirm = vec![]; + let mut to_spend = vec![]; + + for res in locked_outputs { + let (id, resx, commitment) = res.map_err(|e| WalletStorageError::general(OPERATION, e))?; + let resource_address = resx.parse().map_err(|_| WalletStorageError::DecodingError { + operation: "try_to_substate_id", + item: "output", + details: format!("Corrupt db: invalid resource address '{resx}' for id {id}"), + })?; + let commitment: PedersenCommitmentBytes = + deserialize_hex_try_from(&commitment).map_err(|_| WalletStorageError::DecodingError { + operation: "try_into_output", + item: "output commitment", + details: "Corrupt db: invalid hex representation".to_string(), + })?; + + let addr = UtxoAddress::new(resource_address, commitment.into()); + let is_downed = down_id_index.contains(&addr); + let is_upped = up_id_index.contains(&addr); + + if is_upped { + to_confirm.push(id); + } else if is_downed { + to_spend.push(id); + } else { + // Lock will be released (i.e. LockedUnconfirmed outputs deleted, LockedForSpend -> Unspent) + } + } + + if !to_confirm.is_empty() { + // Unlock locked unconfirmed stealth_outputs + diesel::update(stealth_outputs::table) + .filter(stealth_outputs::lock_id.eq(lock_id)) + .filter(stealth_outputs::status.eq(OutputStatus::LockedUnconfirmed.as_key_str())) + .filter(stealth_outputs::id.eq_any(to_confirm)) + .set(( + stealth_outputs::status.eq(OutputStatus::Unspent.as_key_str()), + stealth_outputs::lock_id.eq::>(None), + stealth_outputs::locked_at.eq::>(None), + stealth_outputs::is_on_chain.eq(true), + )) + .execute(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + } + + if !to_spend.is_empty() { + // Mark locked outputs as spent + diesel::update(stealth_outputs::table) + .filter(stealth_outputs::lock_id.eq(lock_id)) + .filter(stealth_outputs::status.eq(OutputStatus::LockedForSpend.as_key_str())) + .filter(stealth_outputs::id.eq_any(to_spend)) + .set(( + stealth_outputs::status.eq(OutputStatus::Spent.as_key_str()), + stealth_outputs::lock_id.eq::>(None), + stealth_outputs::locked_at.eq::>(None), + )) + .execute(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + } + + // Any outputs that were not confirmed or spent are released + self.stealth_outputs_release_by_lock_id(lock_id)?; + + Ok(()) + } } -impl WalletStoreWriter for WriteTransaction<'_> { - fn commit(mut self) -> Result<(), WalletStorageError> { - self.transaction.commit()?; +impl CommitableStore for WriteTransaction<'_> { + fn commit(&mut self) -> Result<(), WalletStorageError> { + self.transaction.commit_internal()?; Ok(()) } - fn rollback(mut self) -> Result<(), WalletStorageError> { - self.transaction.rollback()?; + fn rollback(&mut self) -> Result<(), WalletStorageError> { + self.transaction.rollback_internal()?; Ok(()) } +} +impl WalletStoreWriter for WriteTransaction<'_> { fn jwt_add_empty_token(&mut self) -> Result { use crate::schema::auth_status; @@ -701,7 +832,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { amount_to_lock: Amount, ) -> Result<(), WalletStorageError> { const OPERATION: &str = "vaults_lock_revealed_funds"; - use crate::schema::vaults; + use crate::schema::{vault_locks, vaults}; if amount_to_lock.is_zero() { // No-op @@ -717,19 +848,25 @@ impl WalletStoreWriter for WriteTransaction<'_> { self.ensure_lock_exists(lock_id)?; let vault_str = vault_id.to_string(); - let (db_id, existing_lock_id) = vaults::table - .select((vaults::id, vaults::locked_by)) + let vault_db_id = vaults::table + .select(vaults::id) .filter(vaults::address.eq(&vault_str)) - .first::<(i32, Option)>(self.connection()) - .map_err(|e| WalletStorageError::general(OPERATION, e))?; - - // Only one lock per vault (for simplicity, but unlikely to be a real limitation) - if existing_lock_id.is_some_and(|l| l != lock_id) { - return Err(WalletStorageError::BadQuery { + .first::(self.connection()) + .optional() + .map_err(|e| WalletStorageError::general(OPERATION, e))? + .ok_or_else(|| WalletStorageError::NotFound { operation: OPERATION, - details: format!("Vault {} is already locked by another lock", vault_id), - }); - } + entity: "vault".to_string(), + key: vault_str.clone(), + })?; + + let existing_lock = vault_locks::table + .select(vault_locks::lock_id) + .filter(vault_locks::vault_id.eq(vault_db_id)) + .filter(vault_locks::lock_id.eq(lock_id)) + .count() + .get_result::(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; // TODO: we're limited to i64::MAX. Could be an issue with resources that have a high // divisibility. e.g. i64::MAX < 10 ETH. Values could be represented as a string @@ -740,41 +877,54 @@ impl WalletStoreWriter for WriteTransaction<'_> { WalletStorageError::bad_query(OPERATION, "amount to lock is too large, must be less than i64::MAX") })?; - let changeset = ( - vaults::locked_revealed_balance.eq(amount_to_lock), - vaults::locked_by.eq(lock_id), - ); - - let num_rows = diesel::update(vaults::table) - .set(changeset) - .filter(vaults::id.eq(db_id)) - .execute(self.connection()) - .map_err(|e| WalletStorageError::general(OPERATION, e))?; - - if num_rows == 0 { - return Err(WalletStorageError::NotFound { - operation: OPERATION, - entity: "vault".to_string(), - key: vault_id.to_string(), - }); + if existing_lock > 0 { + // Add to the existing lock + diesel::update(vault_locks::table) + .set(vault_locks::amount.eq(vault_locks::amount.add(amount_to_lock))) + .filter(vault_locks::lock_id.eq(lock_id)) + .filter(vault_locks::vault_id.eq(vault_db_id)) + .execute(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + } else { + diesel::insert_into(vault_locks::table) + .values(( + vault_locks::vault_id.eq(vault_db_id), + vault_locks::lock_id.eq(lock_id), + vault_locks::amount.eq(amount_to_lock), + )) + .execute(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; } Ok(()) } fn vaults_finalized_locked_revealed_funds(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { - const OPERATION: &str = "vaults_finalized_locked_funds"; - use crate::schema::vaults; + const OPERATION: &str = "vaults_finalized_locked_revealed_funds"; + use crate::schema::{vault_locks, vaults}; + + // Fetch the vault locked by this lock_id + let (vault_id, amount) = vault_locks::table + .select((vault_locks::vault_id, vault_locks::amount)) + .filter(vault_locks::lock_id.eq(lock_id)) + .first::<(i32, i64)>(self.connection()) + .optional() + .map_err(|e| WalletStorageError::general(OPERATION, e))? + .ok_or_else(|| WalletStorageError::NotFound { + operation: OPERATION, + entity: "vault lock".to_string(), + key: lock_id.to_string(), + })?; - let changeset = ( - vaults::revealed_balance.eq(vaults::revealed_balance.sub(vaults::locked_revealed_balance)), - vaults::locked_revealed_balance.eq(0), - vaults::locked_by.eq(None::), - ); + // Delete the lock record + diesel::delete(vault_locks::table) + .filter(vault_locks::lock_id.eq(lock_id)) + .execute(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; let num_rows = diesel::update(vaults::table) - .set(changeset) - .filter(vaults::locked_by.eq(lock_id)) + .set(vaults::revealed_balance.eq(vaults::revealed_balance.sub(amount))) + .filter(vaults::id.eq(vault_id)) .execute(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; @@ -791,22 +941,13 @@ impl WalletStoreWriter for WriteTransaction<'_> { fn vaults_release_lock_revealed_funds(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { const OPERATION: &str = "vaults_unlock_revealed_funds"; - use crate::schema::vaults; + use crate::schema::vault_locks; - let num_rows = diesel::update(vaults::table) - .set((vaults::locked_revealed_balance.eq(0), vaults::locked_by.eq(None::))) - .filter(vaults::locked_by.eq(lock_id)) + diesel::delete(vault_locks::table) + .filter(vault_locks::lock_id.eq(lock_id)) .execute(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; - if num_rows == 0 { - return Err(WalletStorageError::NotFound { - operation: OPERATION, - entity: "lock on vault".to_string(), - key: lock_id.to_string(), - }); - } - Ok(()) } @@ -1052,7 +1193,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { lock_id: WalletLockId, ) -> Result { const OPERATION: &str = "stealth_outputs_lock_smallest_amount"; - use crate::schema::stealth_outputs; + use crate::schema::{accounts, stealth_outputs}; self.ensure_lock_exists(lock_id)?; @@ -1071,7 +1212,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { // Allow locking a UTXO created within the transaction .or(stealth_outputs::status .eq(OutputStatus::LockedUnconfirmed.as_key_str()) - .and(stealth_outputs::lock_id.eq(lock_id ))), + .and(stealth_outputs::lock_id.eq(lock_id))), ) // We have the key to spend .filter(stealth_outputs::owner_key_id.is_not_null()) @@ -1169,71 +1310,6 @@ impl WalletStoreWriter for WriteTransaction<'_> { Ok(()) } - fn stealth_outputs_finalize_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { - const OPERATION: &str = "stealth_outputs_finalize_by_lock_id"; - use crate::schema::stealth_outputs; - - // Unlock locked unconfirmed stealth_outputs - diesel::update(stealth_outputs::table) - .filter(stealth_outputs::lock_id.eq(lock_id)) - .filter(stealth_outputs::status.eq(OutputStatus::LockedUnconfirmed.as_key_str())) - .set(( - stealth_outputs::status.eq(OutputStatus::Unspent.as_key_str()), - stealth_outputs::lock_id.eq::>(None), - stealth_outputs::locked_at.eq::>(None), - stealth_outputs::is_on_chain.eq(true), - )) - .execute(self.connection()) - .map_err(|e| WalletStorageError::general(OPERATION, e))?; - - // Mark locked confidential_outputs as spent - diesel::update(stealth_outputs::table) - .filter(stealth_outputs::lock_id.eq(lock_id)) - .filter(stealth_outputs::status.eq(OutputStatus::LockedForSpend.as_key_str())) - .set(( - stealth_outputs::status.eq(OutputStatus::Spent.as_key_str()), - stealth_outputs::lock_id.eq::>(None), - stealth_outputs::locked_at.eq::>(None), - )) - .execute(self.connection()) - .map_err(|e| WalletStorageError::general(OPERATION, e))?; - - Ok(()) - } - - fn stealth_outputs_release_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { - const OPERATION: &str = "stealth_outputs_release_by_lock_id"; - use crate::schema::stealth_outputs; - - // Unlock locked unspent stealth_outputs - diesel::update(stealth_outputs::table) - .filter(stealth_outputs::lock_id.eq(lock_id)) - .filter(stealth_outputs::status.eq(OutputStatus::LockedForSpend.as_key_str())) - .filter(stealth_outputs::is_on_chain.eq(true)) - .set(( - stealth_outputs::status.eq(OutputStatus::Unspent.as_key_str()), - stealth_outputs::lock_id.eq::>(None), - stealth_outputs::locked_at.eq::>(None), - )) - .execute(self.connection()) - .map_err(|e| WalletStorageError::general(OPERATION, e))?; - - // Remove stealth_outputs that were created by this lock - diesel::delete(stealth_outputs::table) - .filter(stealth_outputs::lock_id.eq(lock_id)) - .filter( - stealth_outputs::status - .eq(OutputStatus::LockedUnconfirmed.as_key_str()) - .or(stealth_outputs::status - .eq(OutputStatus::LockedForSpend.as_key_str()) - .and(stealth_outputs::is_on_chain.eq(false))), - ) - .execute(self.connection()) - .map_err(|e| WalletStorageError::general(OPERATION, e))?; - - Ok(()) - } - fn stealth_outputs_update( &mut self, address: &UtxoAddress, @@ -1314,6 +1390,23 @@ impl WalletStoreWriter for WriteTransaction<'_> { Ok(()) } + fn locks_unlock_finalized(&mut self, lock_id: WalletLockId, diff: &SubstateDiff) -> Result<(), WalletStorageError> { + self.stealth_outputs_finalize_by_lock_id(lock_id, diff)?; + self.confidential_outputs_finalize_by_lock_id(lock_id)?; + self.vaults_finalized_locked_revealed_funds(lock_id).optional()?; + self.locks_delete(lock_id)?; + Ok(()) + } + + fn locks_release(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { + self.confidential_outputs_release_by_lock_id(lock_id)?; + self.stealth_outputs_release_by_lock_id(lock_id)?; + self.vaults_release_lock_revealed_funds(lock_id)?; + self.locks_delete(lock_id)?; + + Ok(()) + } + // -------------------------------- Non fungible tokens -------------------------------- // fn non_fungible_token_upsert(&mut self, non_fungible_token: &NonFungibleToken) -> Result<(), WalletStorageError> { use crate::schema::{non_fungible_tokens, vaults}; @@ -1547,7 +1640,7 @@ impl Drop for WriteTransaction<'_> { fn drop(&mut self) { if !self.transaction.is_done() { warn!(target: LOG_TARGET, "WriteTransaction was not committed or rolled back"); - if let Err(err) = self.transaction.rollback() { + if let Err(err) = self.transaction.rollback_internal() { warn!(target: LOG_TARGET, "Failed to rollback WriteTransaction: {}", err); } } diff --git a/crates/wallet/storage_sqlite/tests/accounts.rs b/crates/wallet/storage_sqlite/tests/accounts.rs index 4534867d58..8b5d109ec0 100644 --- a/crates/wallet/storage_sqlite/tests/accounts.rs +++ b/crates/wallet/storage_sqlite/tests/accounts.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use tari_ootle_wallet_sdk::{ models::{AccountUpdate, KeyId}, - storage::{WalletStore, WalletStoreReader, WalletStoreWriter}, + storage::{CommitableStore, WalletStoreReader, WalletStoreWriter, WriteableWalletStore}, }; use tari_ootle_wallet_storage_sqlite::SqliteWalletStore; use tari_template_lib::{models::ComponentAddress, prelude::RistrettoPublicKeyBytes}; @@ -36,7 +36,6 @@ fn update_account() { .unwrap(); tx.commit().unwrap(); - let mut tx = db.create_read_tx().unwrap(); let account = tx.accounts_get_by_name("foo").unwrap(); assert_eq!(account.name.as_deref(), Some("foo")); } diff --git a/crates/wallet/storage_sqlite/tests/config.rs b/crates/wallet/storage_sqlite/tests/config.rs index 1373afb97b..24a1ad8cb3 100644 --- a/crates/wallet/storage_sqlite/tests/config.rs +++ b/crates/wallet/storage_sqlite/tests/config.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_ootle_common_types::optional::Optional; -use tari_ootle_wallet_sdk::storage::{WalletStore, WalletStoreReader, WalletStoreWriter}; +use tari_ootle_wallet_sdk::storage::{CommitableStore, WalletStoreReader, WalletStoreWriter, WriteableWalletStore}; use tari_ootle_wallet_storage_sqlite::SqliteWalletStore; #[test] @@ -15,7 +15,6 @@ fn get_and_set_value() { tx.config_set("dummy", &123u32, false).unwrap(); tx.commit().unwrap(); - let mut tx = db.create_read_tx().unwrap(); let rec = tx.config_get::("dummy").unwrap(); assert_eq!(rec.value, 123); } diff --git a/crates/wallet/storage_sqlite/tests/key_manager_state.rs b/crates/wallet/storage_sqlite/tests/key_manager_state.rs index 7334e18ff8..91f64713ec 100644 --- a/crates/wallet/storage_sqlite/tests/key_manager_state.rs +++ b/crates/wallet/storage_sqlite/tests/key_manager_state.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_ootle_common_types::optional::Optional; -use tari_ootle_wallet_sdk::storage::{WalletStore, WalletStoreReader, WalletStoreWriter}; +use tari_ootle_wallet_sdk::storage::{CommitableStore, WalletStoreReader, WalletStoreWriter, WriteableWalletStore}; use tari_ootle_wallet_storage_sqlite::SqliteWalletStore; #[test] @@ -22,7 +22,6 @@ fn get_and_set_branch_index() { tx.key_manager_set_active_index("another", 2).unwrap(); tx.commit().unwrap(); - let mut tx = db.create_read_tx().unwrap(); let index = tx.key_manager_get_active_index("").unwrap(); assert_eq!(index, 123); let index = tx.key_manager_get_active_index("another").unwrap(); diff --git a/crates/wallet/storage_sqlite/tests/substates.rs b/crates/wallet/storage_sqlite/tests/substates.rs index 19954bac3d..b254365695 100644 --- a/crates/wallet/storage_sqlite/tests/substates.rs +++ b/crates/wallet/storage_sqlite/tests/substates.rs @@ -5,7 +5,7 @@ use std::{collections::HashSet, str::FromStr}; use tari_engine_types::substate::SubstateId; use tari_ootle_common_types::{optional::Optional, VersionedSubstateIdRef}; -use tari_ootle_wallet_sdk::storage::{WalletStore, WalletStoreReader, WalletStoreWriter}; +use tari_ootle_wallet_sdk::storage::{CommitableStore, WalletStoreReader, WalletStoreWriter, WriteableWalletStore}; use tari_ootle_wallet_storage_sqlite::SqliteWalletStore; #[test] @@ -31,7 +31,6 @@ fn get_and_insert_substates() { tx.commit().unwrap(); - let mut tx = db.create_read_tx().unwrap(); let returned = tx.substates_get(&address).unwrap(); assert!(returned.parent_address.is_none()); assert_eq!(*returned.substate_id.substate_id(), address); diff --git a/crates/wallet/storage_sqlite/tests/transaction.rs b/crates/wallet/storage_sqlite/tests/transaction.rs index bce807a8dd..ead47aea8d 100644 --- a/crates/wallet/storage_sqlite/tests/transaction.rs +++ b/crates/wallet/storage_sqlite/tests/transaction.rs @@ -5,7 +5,7 @@ use tari_crypto::ristretto::RistrettoSecretKey; use tari_ootle_common_types::optional::Optional; use tari_ootle_wallet_sdk::{ models::TransactionStatus, - storage::{WalletStore, WalletStoreReader, WalletStoreWriter}, + storage::{CommitableStore, WalletStoreReader, WalletStoreWriter, WriteableWalletStore}, }; use tari_ootle_wallet_storage_sqlite::SqliteWalletStore; use tari_transaction::{args, Transaction, TransactionId}; @@ -34,7 +34,6 @@ fn get_and_insert_transaction() { tx.transactions_insert(&transaction, None, false).unwrap(); tx.commit().unwrap(); - let mut tx = db.create_read_tx().unwrap(); let returned = tx.transactions_get(tx_id).unwrap(); // Transaction was not malleated in the database assert!(returned.transaction.verify_all_signatures()); diff --git a/lints.toml b/lints.toml index f6d3a47d40..500b5118d5 100644 --- a/lints.toml +++ b/lints.toml @@ -22,8 +22,7 @@ deny = [ 'unreachable_patterns', 'clippy::cloned_instead_of_copied', 'clippy::create_dir', - # TODO: re-add this and remove dbg! - # 'clippy::dbg_macro', + 'clippy::dbg_macro', 'clippy::else_if_without_else', 'clippy::inline_always', 'let_underscore_drop', diff --git a/utilities/db_inspector/src/webserver/server.rs b/utilities/db_inspector/src/webserver/server.rs index cf915bd280..f35b99bd25 100644 --- a/utilities/db_inspector/src/webserver/server.rs +++ b/utilities/db_inspector/src/webserver/server.rs @@ -22,7 +22,7 @@ const LOG_TARGET: &str = "tari::ootle::swarm::webserver"; macro_rules! add_cf_route { ($api:expr, $cf:expr) => { $api = $api.route( - &format!("/databases/:db_name/column-families/{}", $cf.as_name()), + &format!("/databases/{{db_name}}/column-families/{}", $cf.as_name()), get(handlers::tables::list(|| $cf)), ); }; @@ -39,28 +39,28 @@ pub async fn run(context: HandlerContext) -> anyhow::Result<()> { let mut api = Router::new() .route("/databases", get(handlers::databases::list)) .route( - "/databases/:db_name/column-families", + "/databases/{db_name}/column-families", get(handlers::column_families::list), ) // Special cases .route( - "/databases/:db_name/column-families/blocks", + "/databases/{db_name}/column-families/blocks", get(handlers::blocks::list), ) .route( - "/databases/:db_name/column-families/state_transitions", + "/databases/{db_name}/column-families/state_transitions", get(handlers::state_transitions::list), ) .route( - "/databases/:db_name/column-families/block_diff", + "/databases/{db_name}/column-families/block_diff", get(handlers::block_diff::list), ) .route( - "/databases/:db_name/column-families/bookkeeping", + "/databases/{db_name}/column-families/bookkeeping", get(handlers::bookkeeping::list), ) .route( - "/databases/:db_name/column-families/foreign_substate_pledges", + "/databases/{db_name}/column-families/foreign_substate_pledges", get(handlers::foreign_substate_pledges::list), );