diff --git a/Cargo.lock b/Cargo.lock index 4233c8a334..f66b7e7b00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1840,7 +1840,7 @@ dependencies = [ [[package]] name = "consensus_tests" -version = "0.15.0" +version = "0.15.1" dependencies = [ "fern", "futures 0.3.31", @@ -2628,7 +2628,7 @@ dependencies = [ [[package]] name = "db_inspector" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "axum 0.8.4", @@ -3780,7 +3780,7 @@ checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" [[package]] name = "generate_ristretto_value_lookup" -version = "0.15.0" +version = "0.15.1" dependencies = [ "clap 3.2.25", "futures 0.3.31", @@ -5030,7 +5030,7 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "integration_tests" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "config", @@ -5809,7 +5809,7 @@ dependencies = [ [[package]] name = "libp2p-messaging" -version = "0.15.0" +version = "0.15.1" dependencies = [ "async-trait", "futures-bounded", @@ -5988,7 +5988,7 @@ dependencies = [ [[package]] name = "libp2p-substream" -version = "0.15.0" +version = "0.15.1" dependencies = [ "libp2p", "prometheus-client", @@ -8514,7 +8514,7 @@ dependencies = [ [[package]] name = "proto_builder" -version = "0.15.0" +version = "0.15.1" dependencies = [ "prost-build 0.14.1", "sha2", @@ -10348,7 +10348,7 @@ dependencies = [ [[package]] name = "sqlite_message_logger" -version = "0.15.0" +version = "0.15.1" dependencies = [ "chrono", "diesel", @@ -10389,7 +10389,7 @@ checksum = "e7386b49cb287f6fafbfd3bd604914bccb99fb8d53483f40e1ecfda5d45f3370" [[package]] name = "state_store_tests" -version = "0.15.0" +version = "0.15.1" dependencies = [ "env_logger 0.11.8", "indexmap 2.11.4", @@ -10778,7 +10778,7 @@ dependencies = [ [[package]] name = "tari_base_node_client" -version = "0.15.0" +version = "0.15.1" dependencies = [ "log", "minotari_app_grpc", @@ -11002,7 +11002,7 @@ dependencies = [ [[package]] name = "tari_consensus" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "indexmap 2.11.4", @@ -11026,7 +11026,7 @@ dependencies = [ [[package]] name = "tari_consensus_types" -version = "0.15.0" +version = "0.15.1" dependencies = [ "borsh", "serde", @@ -11128,7 +11128,7 @@ dependencies = [ [[package]] name = "tari_engine" -version = "0.15.0" +version = "0.15.1" dependencies = [ "blake2", "cargo_toml 0.22.3", @@ -11157,7 +11157,7 @@ dependencies = [ [[package]] name = "tari_engine_types" -version = "0.15.0" +version = "0.15.1" dependencies = [ "base64 0.21.7", "bincode 2.0.1", @@ -11184,7 +11184,7 @@ dependencies = [ [[package]] name = "tari_epoch_manager" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "log", @@ -11206,12 +11206,13 @@ dependencies = [ [[package]] name = "tari_epoch_oracles" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "blake2", "log", "serde", + "serde_json", "tari_base_node_client", "tari_common_types", "tari_engine_types", @@ -11248,7 +11249,7 @@ dependencies = [ [[package]] name = "tari_indexer" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "async-graphql", @@ -11312,7 +11313,7 @@ dependencies = [ [[package]] name = "tari_indexer_client" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "bounded-vec", @@ -11342,7 +11343,7 @@ dependencies = [ [[package]] name = "tari_indexer_lib" -version = "0.15.0" +version = "0.15.1" dependencies = [ "log", "serde", @@ -11418,7 +11419,7 @@ dependencies = [ [[package]] name = "tari_networking" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "async-trait", @@ -11455,7 +11456,7 @@ dependencies = [ [[package]] name = "tari_ootle_address" -version = "0.15.0" +version = "0.15.1" dependencies = [ "bech32", "bincode 2.0.1", @@ -11471,7 +11472,7 @@ dependencies = [ [[package]] name = "tari_ootle_app_utilities" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "bincode 2.0.1", @@ -11510,7 +11511,7 @@ dependencies = [ [[package]] name = "tari_ootle_common_types" -version = "0.15.0" +version = "0.15.1" dependencies = [ "blake2", "borsh", @@ -11538,7 +11539,7 @@ dependencies = [ [[package]] name = "tari_ootle_p2p" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "prost 0.14.1", @@ -11560,7 +11561,7 @@ dependencies = [ [[package]] name = "tari_ootle_storage" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "bitflags 2.9.2", @@ -11585,7 +11586,7 @@ dependencies = [ [[package]] name = "tari_ootle_storage_sqlite" -version = "0.15.0" +version = "0.15.1" dependencies = [ "diesel", "diesel_migrations", @@ -11605,7 +11606,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_cli" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -11634,7 +11635,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_crypto" -version = "0.15.0" +version = "0.15.1" dependencies = [ "argon2 0.5.3", "blake2", @@ -11658,7 +11659,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_sdk" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "blake2", @@ -11693,7 +11694,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_sdk_services" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "futures 0.3.31", @@ -11718,7 +11719,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_storage_sqlite" -version = "0.15.0" +version = "0.15.1" dependencies = [ "bigdecimal", "diesel", @@ -11742,7 +11743,7 @@ dependencies = [ [[package]] name = "tari_ootle_walletd" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "async-trait", @@ -11827,7 +11828,7 @@ dependencies = [ [[package]] name = "tari_rpc_framework" -version = "0.15.0" +version = "0.15.1" dependencies = [ "async-trait", "bitflags 2.9.2", @@ -11851,7 +11852,7 @@ dependencies = [ [[package]] name = "tari_rpc_macros" -version = "0.15.0" +version = "0.15.1" dependencies = [ "proc-macro2", "quote", @@ -11860,7 +11861,7 @@ dependencies = [ [[package]] name = "tari_rpc_state_sync" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "futures 0.3.31", @@ -11880,7 +11881,7 @@ dependencies = [ [[package]] name = "tari_scaffolder" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "clap 3.2.25", @@ -11952,7 +11953,7 @@ dependencies = [ [[package]] name = "tari_signaling_server" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "axum 0.8.4", @@ -11978,7 +11979,7 @@ dependencies = [ [[package]] name = "tari_state_store_rocksdb" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "bincode 2.0.1", @@ -12004,7 +12005,7 @@ dependencies = [ [[package]] name = "tari_state_tree" -version = "0.15.0" +version = "0.15.1" dependencies = [ "indexmap 2.11.4", "log", @@ -12031,7 +12032,7 @@ dependencies = [ [[package]] name = "tari_swarm" -version = "0.15.0" +version = "0.15.1" dependencies = [ "libp2p", "libp2p-messaging", @@ -12041,7 +12042,7 @@ dependencies = [ [[package]] name = "tari_swarm_daemon" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "async-trait", @@ -12097,7 +12098,7 @@ dependencies = [ [[package]] name = "tari_template_builtin" -version = "0.15.0" +version = "0.15.1" dependencies = [ "tari_engine_types", "tari_template_lib", @@ -12149,7 +12150,7 @@ dependencies = [ [[package]] name = "tari_template_manager" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "bytes 1.10.1", @@ -12215,7 +12216,7 @@ dependencies = [ [[package]] name = "tari_transaction" -version = "0.15.0" +version = "0.15.1" dependencies = [ "borsh", "hex", @@ -12303,7 +12304,7 @@ dependencies = [ [[package]] name = "tari_transaction_manifest" -version = "0.15.0" +version = "0.15.1" dependencies = [ "proc-macro2", "serde_json", @@ -12337,7 +12338,7 @@ dependencies = [ [[package]] name = "tari_validator_node" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "axum 0.8.4", @@ -12399,7 +12400,7 @@ dependencies = [ [[package]] name = "tari_validator_node_cli" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "clap 3.2.25", @@ -12427,7 +12428,7 @@ dependencies = [ [[package]] name = "tari_validator_node_client" -version = "0.15.0" +version = "0.15.1" dependencies = [ "indexmap 2.11.4", "multiaddr 0.18.1", @@ -12450,7 +12451,7 @@ dependencies = [ [[package]] name = "tari_validator_node_rpc" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "prost 0.14.1", @@ -12472,7 +12473,7 @@ dependencies = [ [[package]] name = "tari_wallet_daemon_client" -version = "0.15.0" +version = "0.15.1" dependencies = [ "reqwest 0.11.27", "serde", @@ -12493,7 +12494,7 @@ dependencies = [ [[package]] name = "tari_watcher" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "clap 3.2.25", @@ -12521,7 +12522,7 @@ dependencies = [ [[package]] name = "tariswap_bench" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "clap 4.5.48", @@ -13180,7 +13181,7 @@ dependencies = [ [[package]] name = "traffic-sim" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "clap 4.5.48", @@ -13202,7 +13203,7 @@ dependencies = [ [[package]] name = "transaction_generator" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "bincode 2.0.1", @@ -13222,7 +13223,7 @@ dependencies = [ [[package]] name = "transaction_submitter" -version = "0.15.0" +version = "0.15.1" dependencies = [ "anyhow", "clap 4.5.48", diff --git a/Cargo.toml b/Cargo.toml index 1caf4f6666..8ff3cf3ede 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ # NOTE: When editing this version, also edit the versions in template_built_in/templates/account and account_nft [workspace.package] -version = "0.15.0" +version = "0.15.1" edition = "2021" authors = ["The Tari Development Community"] repository = "https://github.com/tari-project/tari-ootle" diff --git a/applications/tari_walletd/src/cli.rs b/applications/tari_walletd/src/cli.rs index d0c91fc967..e24417f0b0 100644 --- a/applications/tari_walletd/src/cli.rs +++ b/applications/tari_walletd/src/cli.rs @@ -49,6 +49,8 @@ pub struct Cli { pub json_rpc_address: Option, #[clap(long, env = "TARI_WALLET_WEB_UI_JSON_RPC_PUBLIC_URL")] pub web_ui_public_json_rpc_url: Option, + #[clap(short = 'w', long, env = "TARI_WALLET_WEB_UI_JSON_RPC_PUBLIC_URL")] + pub web_ui_listen_addr: Option, #[clap(long, env = "SIGNALING_SERVER_ADDRESS")] pub signaling_server_address: Option, #[clap(long, short = 'i', alias = "indexer-url")] @@ -118,7 +120,12 @@ impl ConfigOverrideProvider for Cli { file.display().to_string(), )); } - + if let Some(ref listen_addr) = self.web_ui_listen_addr { + overrides.push(( + "ootle_wallet_daemon.web_ui_address".to_string(), + listen_addr.to_string(), + )); + } overrides } } diff --git a/applications/tari_walletd/src/handlers/accounts.rs b/applications/tari_walletd/src/handlers/accounts.rs index a22c9872f1..cd7c895b52 100644 --- a/applications/tari_walletd/src/handlers/accounts.rs +++ b/applications/tari_walletd/src/handlers/accounts.rs @@ -1,7 +1,7 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::{collections::HashSet, iter}; +use std::{collections::HashSet, iter, time::Duration}; use anyhow::{anyhow, Context}; use axum_extra::headers::authorization::Bearer; @@ -30,7 +30,7 @@ use tari_ootle_wallet_sdk::{ stealth_transfer::{StealthTransferParams, TransferOutput}, substate::ValidatorScanResult, }, - models::{BranchAndKeyId, KeyBranch, KeyId, NewAccountData, WalletLockDropGuard}, + models::{BranchAndKeyId, KeyBranch, KeyId, NewAccountData}, }; use tari_ootle_wallet_sdk_services::events::TransactionSubmittedEvent; use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS; @@ -1005,7 +1005,7 @@ pub async fn handle_stealth_transfer( // Spawn here is to prevent the async block from being aborted if the caller aborts the request early as this can // cause funds to remain locked indefinitely. task::spawn(async move { - let transfer = sdk.stealth_transfer_api().transfer(owner_account, params).await?; + let (lock, transfer) = sdk.stealth_transfer_api().transfer(owner_account, params).await?; let transaction = transfer.transaction.authorized_sealed_signer(); let main_pk = transfer.main_signer.public_key().to_byte_type(); @@ -1031,13 +1031,7 @@ pub async fn handle_stealth_transfer( 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 - if let Err(err) = sdk.stealth_outputs_api().release_lock(transfer.lock_id) { - error!( - target: LOG_TARGET, - "Failed to release locked outputs for dry run : {}", - err - ); - } + lock.release(); let result = transaction_service.submit_dry_run_transaction(transaction).await; return match result { Ok(res) => Ok(StealthTransferResponse { @@ -1047,15 +1041,15 @@ pub async fn handle_stealth_transfer( }; } - 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, - ) + let tx_id = transaction_service + .submit_transaction_with_opts(transaction, None, Some(lock.id())) + .await .context("Transaction failed to submit")?; + + // Transaction submitted, we're home free, make sure to allow the lock to persist past this call. + // The wallet will monitor the transaction and release the lock when it's finalized. + lock.keep_locked(); + notifier.notify(TransactionSubmittedEvent { transaction_id: tx_id, new_account: None, @@ -1089,8 +1083,7 @@ pub async fn handle_create_stealth_transfer_statement( } let mut required_signers = HashSet::new(); - let lock_id = sdk.stealth_outputs_api().create_lock()?; - let lock_guard = WalletLockDropGuard::new(lock_id, sdk.store().clone()); + let lock = sdk.locks_api().create_lock_with_timeout(Duration::from_secs(5 * 60))?; let mut statements = Vec::with_capacity(req.requests.len()); for req in req.requests { let sender_account = get_account(&req.sender_account, &sdk.accounts_api())?; @@ -1120,7 +1113,7 @@ pub async fn handle_create_stealth_transfer_statement( .as_selection() .map(|sel| { sdk.stealth_transfer_api().lock_inputs_for_transfer( - lock_id, + lock.id(), sender_account.component_address(), req.resource_address, amount_to_spend, @@ -1181,7 +1174,7 @@ pub async fn handle_create_stealth_transfer_statement( } // Return without unlocking the outputs - lock_guard.disarm(); + let lock_id = lock.keep_locked(); Ok(AccountsCreateStealthTransferStatementResponse { statements, diff --git a/applications/tari_walletd/src/handlers/confidential.rs b/applications/tari_walletd/src/handlers/confidential.rs index 72cac3330d..cf07045d1d 100644 --- a/applications/tari_walletd/src/handlers/confidential.rs +++ b/applications/tari_walletd/src/handlers/confidential.rs @@ -1,7 +1,7 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::fs; +use std::{fs, time::Duration}; use anyhow::anyhow; use axum_extra::headers::authorization::Bearer; @@ -10,8 +10,12 @@ use log::*; use rand::rngs::OsRng; use serde_json::json; use tari_crypto::{commitment::HomomorphicCommitmentFactory, keys::PublicKey as _, ristretto::RistrettoPublicKey}; -use tari_engine_types::{crypto::get_commitment_factory, ToByteType}; -use tari_ootle_wallet_crypto::{AlwaysMissLookupTable, IoReaderValueLookup, UnblindedOutputWitness}; +use tari_engine_types::{ + crypto::{get_commitment_factory, ValueLookupTable}, + ToByteType, +}; +use tari_ootle_common_types::{displayable::Displayable, optional::Optional}; +use tari_ootle_wallet_crypto::{GenerateValueLookup, IoReaderValueLookup, UnblindedOutputWitness}; use tari_ootle_wallet_sdk::models::{ConfidentialOutputModel, KeyBranch, OutputStatus}; use tari_template_lib::types::Amount; use tari_wallet_daemon_client::{ @@ -23,6 +27,8 @@ use tari_wallet_daemon_client::{ ConfidentialViewVaultBalanceResponse, ProofsCancelRequest, ProofsCancelResponse, + ProofsFinalizeRequest, + ProofsFinalizeResponse, ProofsGenerateRequest, ProofsGenerateResponse, }, @@ -59,7 +65,7 @@ pub async fn handle_create_transfer_proof( let vault = sdk .accounts_api() .get_vault_by_resource(account.component_address(), &req.resource_address)?; - let lock_id = sdk.confidential_outputs_api().create_lock()?; + let lock = sdk.locks_api().create_lock_with_timeout(Duration::from_secs(5 * 60))?; let amount_to_transfer = req.amount.checked_add_positive(req.reveal_amount).ok_or_else(|| { invalid_request(format!( @@ -70,13 +76,13 @@ pub async fn handle_create_transfer_proof( // Lock inputs we're going to spend let (inputs, total_input_value) = sdk.confidential_outputs_api() - .lock_outputs_by_amount(lock_id, &vault.id, amount_to_transfer)?; + .lock_outputs_by_amount(lock.id(), &vault.id, amount_to_transfer)?; info!( target: LOG_TARGET, "Locked {} inputs for proof {} worth {} µT", inputs.len(), - lock_id, + lock.id(), total_input_value ); @@ -165,7 +171,7 @@ pub async fn handle_create_transfer_proof( memo: None, public_asset_tag: None, status: OutputStatus::LockedUnconfirmed, - lock_id: Some(lock_id), + lock_id: Some(lock.id()), })?; Some(UnblindedOutputWitness { @@ -192,6 +198,8 @@ pub async fn handle_create_transfer_proof( Amount::zero(), )?; + let lock_id = lock.keep_locked(); + Ok(ProofsGenerateResponse { proof_id: lock_id, proof, @@ -201,15 +209,54 @@ pub async fn handle_create_transfer_proof( pub async fn handle_finalize_transfer( context: &HandlerContext, token: Option<&Bearer>, - req: ProofsCancelRequest, -) -> Result { + req: ProofsFinalizeRequest, +) -> Result { let sdk = context.wallet_sdk(); context.check_auth(token, &[JrpcPermission::Admin])?; + let transaction = sdk + .transaction_api() + .get(req.transaction_id) + .optional()? + .ok_or_else(|| { + invalid_params( + "transaction_id", + Some("No such transaction in wallet to finalize proof for"), + ) + })?; + let lock_id = sdk + .locks_api() + .get_lock_by_transaction_id(req.transaction_id) + .optional()?; + if lock_id != Some(req.lock_id) { + return Err(invalid_params( + "lock_id", + Some("Lock not associated with this transaction"), + )); + } - sdk.confidential_outputs_api() - .finalize_locked_revealed_funds(req.proof_id)?; - sdk.confidential_outputs_api().finalize_outputs_for_lock(req.proof_id)?; - Ok(ProofsCancelResponse {}) + match transaction.finalized_diff() { + Some(diff) => { + info!( + target: LOG_TARGET, + "Finalizing locked proof {} for transaction {}", + req.lock_id, + req.transaction_id + ); + sdk.locks_api().finalize_lock(req.lock_id, diff)?; + }, + None => { + return Err(invalid_params( + "transaction_id", + Some(format!( + "Transaction is not finalized (status = {}, reason = {})", + transaction.status, + transaction.failure_reason_as_string().display() + )), + )); + }, + } + + Ok(ProofsFinalizeResponse {}) } pub async fn handle_cancel_transfer( @@ -219,8 +266,7 @@ pub async fn handle_cancel_transfer( ) -> Result { let sdk = context.wallet_sdk(); context.check_auth(token, &[JrpcPermission::Admin])?; - sdk.confidential_outputs_api().release_revealed_funds(req.proof_id)?; - sdk.confidential_outputs_api().release_locked_outputs(req.proof_id)?; + sdk.locks_api().release_lock(req.proof_id)?; Ok(ProofsCancelResponse {}) } @@ -296,7 +342,14 @@ pub async fn handle_view_vault_balance( Some(file) => { let mut file = fs::File::open(file) .map_err(|e| anyhow!("Unable to load value lookup file '{}': {e}", file.display()))?; - let mut lookup = IoReaderValueLookup::load(&mut file)?; + let mut is_logged = false; + let mut lookup = IoReaderValueLookup::load(&mut file)?.with_fallback(move |v| { + if !is_logged { + is_logged = true; + warn!("Using value lookup fallback. This will likely result in very slow lookups."); + } + GenerateValueLookup.lookup(v) + }); block_in_place(|| { sdk.viewable_balance_api().try_brute_force_commitment_balances( @@ -307,14 +360,20 @@ pub async fn handle_view_vault_balance( ) })? }, - None => block_in_place(|| { - sdk.viewable_balance_api().try_brute_force_commitment_balances( - &view_key.key, - commitments.values().filter_map(|o| o.viewable_balance.as_ref()), - value_range, - &mut AlwaysMissLookupTable, - ) - })?, + None => { + warn!( + target: LOG_TARGET, + "No value lookup table configured. This will likely result in very slow lookups." + ); + block_in_place(|| { + sdk.viewable_balance_api().try_brute_force_commitment_balances( + &view_key.key, + commitments.values().filter_map(|o| o.viewable_balance.as_ref()), + value_range, + &mut GenerateValueLookup, + ) + })? + }, }; info!(target: LOG_TARGET, "Brute force balance lookup took {:.2?}", timer.elapsed()); diff --git a/applications/tari_walletd/src/handlers/stealth_utxos.rs b/applications/tari_walletd/src/handlers/stealth_utxos.rs index 8496f78d69..1c4e939710 100644 --- a/applications/tari_walletd/src/handlers/stealth_utxos.rs +++ b/applications/tari_walletd/src/handlers/stealth_utxos.rs @@ -7,7 +7,8 @@ use anyhow::anyhow; use axum_extra::headers::authorization::Bearer; use indexmap::IndexMap; use log::{info, warn}; -use tari_ootle_wallet_crypto::{AlwaysMissLookupTable, IoReaderValueLookup}; +use tari_engine_types::crypto::ValueLookupTable; +use tari_ootle_wallet_crypto::{GenerateValueLookup, IoReaderValueLookup}; use tari_template_lib::models::UtxoAddress; use tari_wallet_daemon_client::{ permissions::JrpcPermission, @@ -112,7 +113,8 @@ pub async fn handle_decrypt_value( Some(path) => spawn_blocking(move || { let mut file = fs::File::open(&path) .map_err(|e| anyhow!("Unable to load value lookup file '{}': {e}", path.display()))?; - let mut lookup = IoReaderValueLookup::load(&mut file)?; + let lookup = IoReaderValueLookup::load(&mut file)?; + info!( target: LOG_TARGET, "Using value lookup table from file '{}' ({}-{}) for brute force balance lookup", @@ -135,6 +137,15 @@ pub async fn handle_decrypt_value( ); } + let mut is_logged = false; + let mut lookup = lookup.with_fallback(move |v| { + if !is_logged { + is_logged = true; + warn!("Using value lookup fallback. This will likely result in very slow lookups."); + } + GenerateValueLookup.lookup(v) + }); + let balance = sdk.viewable_balance_api().try_brute_force_commitment_balances( &view_key.key, elgamal_proofs.iter(), @@ -147,15 +158,15 @@ pub async fn handle_decrypt_value( None => { warn!( target: LOG_TARGET, - "No value lookup table file configured. Generating a temporary lookup table that always misses. \ - This will make the brute force balance lookup very slow for high-value outputs." + "No value lookup table file configured. Using a generated value lookup fallback. \ + Brute-force may still be slow for very high-value outputs." ); spawn_blocking(move || { let balances = sdk.viewable_balance_api().try_brute_force_commitment_balances( &view_key.key, elgamal_proofs.iter(), value_range, - &mut AlwaysMissLookupTable, + &mut GenerateValueLookup, )?; anyhow::Ok(balances) }) diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx index bb44f811b5..b638500ec5 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx @@ -41,6 +41,7 @@ import FormStep, { FormError, SendMoneyFormState } from "../steps/FormStep"; import ConfirmationStep from "../steps/ConfirmationStep"; import ResultStep, { TransferResult } from "../steps/ResultStep"; import PopupTitle from "@/components/PopupTitle"; +import { XTR_CURRENCY } from "@utils/constants"; export interface SendMoneyDialogProps { open: boolean; @@ -136,15 +137,20 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { if (parts.length > 2) { processedValue = parts[0] + "." + parts.slice(1).join(""); } + } else if (name === "fee" && value) { + // let parsed = parseInt(transferFormState.fee); + // if (!isNaN(parsed)) { + // processedValue = parsed.toString(); // (parsed / XTR_CURRENCY.DIVISOR).toString(); + // } } // Clear fee when amount or publicKey changes to trigger re-estimation - const shouldClearFee = (name === "amount" || name === "address") && transferFormState.fee; + // const shouldClearFee = (name === "amount" || name === "address") && transferFormState.fee; setTransferFormState({ ...transferFormState, [name]: processedValue, - ...(shouldClearFee ? { fee: "" } : {}), + // ...(shouldClearFee ? { fee: "" } : {}), }); } diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx index 336e3f0c55..3731e14c7e 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx @@ -32,7 +32,7 @@ import { Divider, InputLabel, Stack, InputAdornment, Typography } from "@mui/mat import { SelectChangeEvent } from "@mui/material/Select/Select"; import { ResourceType, ResourceAddress, validateOotleAddress } from "@tari-project/typescript-bindings"; import { formatDisplayCurrency } from "@utils/helpers"; -import { CURRENCY } from "@utils/constants"; +import { XTR_CURRENCY } from "@utils/constants"; export interface SendMoneyFormState { address: string; @@ -239,19 +239,15 @@ export default function FormStep({ {token_symbol} : null, + !isEstimatingFee && token_symbol ? ( + µ{XTR_CURRENCY.SYMBOL} + ) : null, }} /> diff --git a/applications/tari_walletd/web_ui/src/routes/Settings/Components/DecryptUtxoBalance.tsx b/applications/tari_walletd/web_ui/src/routes/Settings/Components/DecryptUtxoBalance.tsx index 0d6dd0dc5d..1eacf3e682 100644 --- a/applications/tari_walletd/web_ui/src/routes/Settings/Components/DecryptUtxoBalance.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Settings/Components/DecryptUtxoBalance.tsx @@ -27,7 +27,7 @@ import Button from "@mui/material/Button"; import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; import { Divider } from "@mui/material"; -import { stealthDecryptUtxoBalance } from "../../../utils/json_rpc"; +import { stealthDecryptUtxoBalance } from "@utils/json_rpc"; import { StealthUtxosDecryptValueRequest, StealthUtxosDecryptValueResponse } from "@tari-project/typescript-bindings"; function DecryptUtxoBalanceForm() { @@ -38,7 +38,7 @@ function DecryptUtxoBalanceForm() { maximumExpectedValue: 100000000, keyId: 0, }); - const [balance, setBalance] = useState(null); + const [balance, setBalance] = useState(null); const onViewBalanceClicked = async () => { const resp = await stealthDecryptUtxoBalance({ diff --git a/applications/tari_walletd/web_ui/src/utils/constants.ts b/applications/tari_walletd/web_ui/src/utils/constants.ts index 61e425f8a2..b5296b36a4 100644 --- a/applications/tari_walletd/web_ui/src/utils/constants.ts +++ b/applications/tari_walletd/web_ui/src/utils/constants.ts @@ -20,7 +20,8 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -export const CURRENCY = { +export const XTR_CURRENCY = { + // TODO: only tXTR for testnet SYMBOL: "tXTR", DECIMALS: 6, DIVISOR: 1_000_000, diff --git a/applications/tari_walletd/web_ui/src/utils/helpers.tsx b/applications/tari_walletd/web_ui/src/utils/helpers.tsx index d4c4a0993f..9d0cbcaaad 100644 --- a/applications/tari_walletd/web_ui/src/utils/helpers.tsx +++ b/applications/tari_walletd/web_ui/src/utils/helpers.tsx @@ -22,7 +22,7 @@ import { ChangeEvent } from "react"; import type { Amount, SubstateId, NonFungibleId } from "@tari-project/typescript-bindings"; -import { CURRENCY } from "@utils/constants"; +import { XTR_CURRENCY } from "@utils/constants"; import useCurrencyStore from "@store/currencyStore"; export const renderJson = (json: any) => { @@ -256,31 +256,31 @@ export const formatCurrency = (amount: number | bigint | Amount): string => { const { currencySymbol } = useCurrencyStore.getState(); if (typeof amount === "bigint") { - const divisor = BigInt(CURRENCY.DIVISOR); + const divisor = BigInt(XTR_CURRENCY.DIVISOR); const integerPart = amount / divisor; const remainder = amount % divisor; - const fractionalPart = remainder.toString().padStart(CURRENCY.DECIMALS, "0"); + const fractionalPart = remainder.toString().padStart(XTR_CURRENCY.DECIMALS, "0"); return `${Number(integerPart).toLocaleString("en-US")}.${fractionalPart} ${currencySymbol}`; } else if (typeof amount === "number") { if (isNaN(amount)) { return `0 ${currencySymbol}`; } - const convertedAmount = amount / CURRENCY.DIVISOR; + const convertedAmount = amount / XTR_CURRENCY.DIVISOR; return `${convertedAmount.toLocaleString("en-US", { minimumFractionDigits: 0, - maximumFractionDigits: CURRENCY.DECIMALS, + maximumFractionDigits: XTR_CURRENCY.DECIMALS, })} ${currencySymbol}`; } else if (typeof amount === "string") { // Handle Amount type try { const numericAmount = BigInt(amount); - const divisor = BigInt(CURRENCY.DIVISOR); + const divisor = BigInt(XTR_CURRENCY.DIVISOR); const integerPart = numericAmount / divisor; const remainder = numericAmount % divisor; - const fractionalPart = remainder.toString().padStart(CURRENCY.DECIMALS, "0"); + const fractionalPart = remainder.toString().padStart(XTR_CURRENCY.DECIMALS, "0"); return `${Number(integerPart).toLocaleString("en-US")}.${fractionalPart} ${currencySymbol}`; } catch (error) { @@ -292,11 +292,11 @@ export const formatCurrency = (amount: number | bigint | Amount): string => { try { const stringValue = String(amount); const numericAmount = BigInt(stringValue); - const divisor = BigInt(CURRENCY.DIVISOR); + const divisor = BigInt(XTR_CURRENCY.DIVISOR); const integerPart = numericAmount / divisor; const remainder = numericAmount % divisor; - const fractionalPart = remainder.toString().padStart(CURRENCY.DECIMALS, "0"); + const fractionalPart = remainder.toString().padStart(XTR_CURRENCY.DECIMALS, "0"); return `${Number(integerPart).toLocaleString("en-US")}.${fractionalPart} ${currencySymbol}`; } catch (error) { diff --git a/bindings/package.json b/bindings/package.json index bbe206af50..9dc83f6463 100644 --- a/bindings/package.json +++ b/bindings/package.json @@ -1,7 +1,5 @@ { "name": "@tari-project/typescript-bindings", - "version": "1.20.0", - "description": "TypeScript types synchronized to the Tari Ootle Rust codebase", "homepage": "https://github.com/tari-project/tari-ootle#readme", "bugs": { "url": "https://github.com/tari-project/tari-ootle/issues" diff --git a/bindings/src/types/StealthTransferStatement.ts b/bindings/src/types/StealthTransferStatement.ts index 2fc8f2a503..10409a48b4 100644 --- a/bindings/src/types/StealthTransferStatement.ts +++ b/bindings/src/types/StealthTransferStatement.ts @@ -1,4 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SchnorrSignatureBytes } from "./SchnorrSignatureBytes"; import type { StealthInputsStatement } from "./StealthInputsStatement"; import type { StealthOutputsStatement } from "./StealthOutputsStatement"; @@ -9,5 +10,5 @@ export type StealthTransferStatement = { * Balance proof that proves that no coins were created or destroyed during the transfer (assuming the range proof * is valid). This may be None, if and only if, the transfer is revealed-only (i.e. no stealth inputs or outputs). */ - balance_proof: { public_nonce: string; signature: string } | null; + balance_proof: SchnorrSignatureBytes | null; }; diff --git a/bindings/src/types/wallet-daemon-client/ProofsFinalizeRequest.ts b/bindings/src/types/wallet-daemon-client/ProofsFinalizeRequest.ts index bf88cf9665..011a02c98d 100644 --- a/bindings/src/types/wallet-daemon-client/ProofsFinalizeRequest.ts +++ b/bindings/src/types/wallet-daemon-client/ProofsFinalizeRequest.ts @@ -1,3 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { TransactionId } from "../TransactionId"; -export type ProofsFinalizeRequest = { proof_id: number }; +export type ProofsFinalizeRequest = { lock_id: number; transaction_id: TransactionId }; diff --git a/clients/wallet_daemon_client/src/types.rs b/clients/wallet_daemon_client/src/types.rs index f53b941caf..ec2d8a23ba 100644 --- a/clients/wallet_daemon_client/src/types.rs +++ b/clients/wallet_daemon_client/src/types.rs @@ -511,8 +511,8 @@ pub struct ProofsGenerateResponse { #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct ProofsFinalizeRequest { - #[cfg_attr(feature = "ts", ts(type = "number"))] - pub proof_id: WalletLockId, + pub lock_id: WalletLockId, + pub transaction_id: TransactionId, } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/crates/engine/src/runtime/working_state.rs b/crates/engine/src/runtime/working_state.rs index d8199b9e5f..b792de9034 100644 --- a/crates/engine/src/runtime/working_state.rs +++ b/crates/engine/src/runtime/working_state.rs @@ -725,7 +725,7 @@ impl WorkingState { }); } - if !vault_mut.resource_type().is_fungible() && !vault_mut.resource_type().is_stealth() { + if !vault_mut.resource_type().is_public_fungible() && !vault_mut.resource_type().is_stealth() { return Err(RuntimeError::InvalidArgument { argument: "resource", reason: format!( diff --git a/crates/engine/tests/access_rules.rs b/crates/engine/tests/access_rules.rs index 295c1839da..83f0474e9d 100644 --- a/crates/engine/tests/access_rules.rs +++ b/crates/engine/tests/access_rules.rs @@ -581,7 +581,7 @@ mod resource_access_rules { .unwrap() .up_iter() .filter_map(|(addr, s)| s.substate_value().as_resource().map(|r| (addr, r))) - .filter(|(_, r)| r.resource_type().is_fungible()) + .filter(|(_, r)| r.resource_type().is_public_fungible()) .map(|(addr, _)| addr.as_resource_address().unwrap()) .next() .unwrap(); @@ -867,7 +867,7 @@ mod resource_access_rules { .unwrap() .up_iter() .filter_map(|(addr, s)| s.substate_value().as_resource().map(|r| (addr, r))) - .filter(|(_, r)| r.resource_type().is_fungible()) + .filter(|(_, r)| r.resource_type().is_public_fungible()) .map(|(addr, _)| addr.as_resource_address().unwrap()) .next() .unwrap(); diff --git a/crates/engine/tests/confidential.rs b/crates/engine/tests/confidential.rs index 0b11bb3803..cfc8697e50 100644 --- a/crates/engine/tests/confidential.rs +++ b/crates/engine/tests/confidential.rs @@ -30,7 +30,7 @@ use tari_template_test_tooling::{ generate_withdraw_proof_with_inputs, generate_withdraw_proof_with_view_key, }, - AlwaysMissLookupTable, + GenerateValueLookup, }, TemplateTest, }; @@ -529,7 +529,7 @@ fn mint_with_view_key() { faucet_vault.get_confidential_commitments().unwrap(), &view_key_secret, 0..=200, - &mut AlwaysMissLookupTable, + &mut GenerateValueLookup, ) .unwrap(); assert_eq!(total_balance, Some(223 - 55)); @@ -544,7 +544,7 @@ fn mint_with_view_key() { user_vault.get_confidential_commitments().unwrap(), &view_key_secret, 0..=200, - &mut AlwaysMissLookupTable, + &mut GenerateValueLookup, ) .unwrap(); assert_eq!(total_balance, Some(55)); diff --git a/crates/engine/tests/stealth.rs b/crates/engine/tests/stealth.rs index 9b3c1daf51..6acdb6da0c 100644 --- a/crates/engine/tests/stealth.rs +++ b/crates/engine/tests/stealth.rs @@ -27,7 +27,7 @@ use tari_template_test_tooling::{ assert_error::assert_reject_reason, stealth, stealth::StealthUnblindedTransferData, - AlwaysMissLookupTable, + GenerateValueLookup, }, wallet_crypto::MaskAndValue, TemplateTest, @@ -487,7 +487,7 @@ fn mint_with_view_key() { .collect(); let total_balance = - try_brute_force_stealth_balance(&utxos, &view_key_secret, 0..=200, &mut AlwaysMissLookupTable).unwrap(); + try_brute_force_stealth_balance(&utxos, &view_key_secret, 0..=200, &mut GenerateValueLookup).unwrap(); assert_eq!(total_balance, Some(1000)); } diff --git a/crates/engine/tests/templates/tariswap/src/lib.rs b/crates/engine/tests/templates/tariswap/src/lib.rs index 6f3dd6e6c9..e18ceebb1f 100644 --- a/crates/engine/tests/templates/tariswap/src/lib.rs +++ b/crates/engine/tests/templates/tariswap/src/lib.rs @@ -228,8 +228,11 @@ mod tariswap { fn check_resource_is_fungible(resource: ResourceAddress) { let resource_type = ResourceManager::get(resource).resource_type(); assert!( - matches!(resource_type, ResourceType::Fungible | ResourceType::Confidential), - "Resource {} is not fungible nor confidential", + matches!( + resource_type, + ResourceType::Fungible | ResourceType::Confidential | ResourceType::Stealth + ), + "Resource {} is not fungible", resource ); } diff --git a/crates/engine_types/src/crypto/elgamal.rs b/crates/engine_types/src/crypto/elgamal.rs index e13b0a004e..f727938d67 100644 --- a/crates/engine_types/src/crypto/elgamal.rs +++ b/crates/engine_types/src/crypto/elgamal.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use borsh::BorshSerialize; -use log::{info, warn}; +use log::info; use tari_bor::{Deserialize, Serialize}; use tari_common_types::types::PrivateKey; use tari_crypto::{ @@ -210,22 +210,12 @@ impl ElgamalVerifiableBalance { .collect::>(); let mut results = vec![None; balances.len()]; - let mut warning_logged = false; for v in value_range { - let value = lookup_table.lookup(v)?.unwrap_or_else(|| { - if !warning_logged { - warning_logged = true; - warn!( - target: LOG_TARGET, - "Value lookup table missing entry for value {}, falling back to slow lookup method", - v - ); - } - // Fallback to slow lookup method if the lookup table does not contain a key for the value - let pk = RistrettoPublicKey::from_secret_key(&RistrettoSecretKey::from(v)); - copy_fixed(pk.as_bytes()) - }); + let Some(value) = lookup_table.lookup(v)? else { + log::debug!(target: LOG_TARGET, "Value {} not found in lookup table", v); + break; + }; while let Some(pos) = balances.iter().position(|(_, balance)| value == balance.as_bytes()) { let (order, _) = balances.swap_remove(pos); @@ -269,12 +259,6 @@ impl ToByteType for ElgamalVerifiableBalance { } } -fn copy_fixed(src: &[u8]) -> [u8; 32] { - let mut buf = [0u8; 32]; - buf.copy_from_slice(src); - buf -} - #[cfg(test)] mod tests { use std::convert::Infallible; @@ -284,6 +268,12 @@ mod tests { use super::*; + fn copy_fixed(src: &[u8]) -> [u8; 32] { + let mut buf = [0u8; 32]; + buf.copy_from_slice(src); + buf + } + #[derive(Default)] pub struct TestLookupTable; @@ -291,7 +281,6 @@ mod tests { type Error = Infallible; fn lookup(&mut self, value: u64) -> Result, Self::Error> { - // This would be a sequential lookup in a real implementation Ok(Some(copy_fixed( RistrettoPublicKey::from_secret_key(&RistrettoSecretKey::from(value)).as_bytes(), ))) diff --git a/crates/engine_types/src/crypto/value_lookup_table.rs b/crates/engine_types/src/crypto/value_lookup_table.rs index 2c25e13043..9a16e8ee4a 100644 --- a/crates/engine_types/src/crypto/value_lookup_table.rs +++ b/crates/engine_types/src/crypto/value_lookup_table.rs @@ -5,3 +5,66 @@ pub trait ValueLookupTable { type Error: std::error::Error; fn lookup(&mut self, value: u64) -> Result, Self::Error>; } + +impl ValueLookupTable for F +where + F: FnMut(u64) -> Result, E>, + E: std::error::Error, +{ + type Error = E; + + fn lookup(&mut self, value: u64) -> Result, Self::Error> { + (self)(value) + } +} + +pub struct AndThenLookup { + first: T1, + fallback: T2, +} + +impl AndThenLookup { + pub fn new(first: T1, fallback: T2) -> Self { + Self { first, fallback } + } +} + +impl ValueLookupTable for AndThenLookup +where + T1: ValueLookupTable, + T2: ValueLookupTable, +{ + type Error = T1::Error; + + fn lookup(&mut self, value: u64) -> Result, Self::Error> { + if let Some(val) = self.first.lookup(value)? { + Ok(Some(val)) + } else { + self.fallback.lookup(value) + } + } +} + +pub struct MapErrLookup { + inner: T, + map_err: F, +} + +impl MapErrLookup { + pub fn new(inner: T, map_err: F) -> Self { + Self { inner, map_err } + } +} + +impl ValueLookupTable for MapErrLookup +where + T: ValueLookupTable, + F: FnMut(T::Error) -> E + 'static, + E: std::error::Error + 'static, +{ + type Error = E; + + fn lookup(&mut self, value: u64) -> Result, Self::Error> { + self.inner.lookup(value).map_err(|e| (self.map_err)(e)) + } +} diff --git a/crates/epoch_oracles/Cargo.toml b/crates/epoch_oracles/Cargo.toml index 42bda9b031..b982ba1495 100644 --- a/crates/epoch_oracles/Cargo.toml +++ b/crates/epoch_oracles/Cargo.toml @@ -31,7 +31,7 @@ url = { workspace = true, optional = true } [dev-dependencies] tokio = { workspace = true, features = ["time", "macros", "rt"] } - +serde_json = { workspace = true } [features] base_layer = [ diff --git a/crates/epoch_oracles/src/configured/real_time_ticker.rs b/crates/epoch_oracles/src/configured/real_time_ticker.rs index a282db4e6a..dc875b0c62 100644 --- a/crates/epoch_oracles/src/configured/real_time_ticker.rs +++ b/crates/epoch_oracles/src/configured/real_time_ticker.rs @@ -75,14 +75,14 @@ impl EpochTicker for RealTimeEpochTicker { } // Emit quickly to catch up to the calculated epoch - if calculated_epoch > self.epoch { + if calculated_epoch >= self.epoch { let epoch = self.increment_epoch(); let epoch_hash = calc_static_epoch_hash(epoch); return Poll::Ready(Some(EpochTickerData { epoch, epoch_hash, // Catching up - done_for_now: false, + done_for_now: calculated_epoch == epoch, })); } @@ -91,13 +91,14 @@ impl EpochTicker for RealTimeEpochTicker { // Every tick, check if we need to emit a new epoch ready!(interval_mut.poll_tick(cx)); - if self.epoch <= calculated_epoch { + if calculated_epoch >= self.epoch { let epoch = self.increment_epoch(); let epoch_hash = calc_static_epoch_hash(epoch); return Poll::Ready(Some(EpochTickerData { epoch, epoch_hash, - done_for_now: true, + // Catching up + done_for_now: calculated_epoch == epoch, })); } } @@ -161,6 +162,25 @@ mod tests { } } + mod config_test { + use super::*; + use crate::configured::Config; + + #[tokio::test] + #[ignore = "Not for testing, use to simulate the result of a real config file."] + async fn check_config_file() { + let mut file = std::fs::File::open("../../data/ec.json").unwrap(); + let config = serde_json::from_reader::<_, Config>(&mut file).unwrap(); + let mut ticker = RealTimeEpochTicker::new(config.initial_epoch, config.base_time, Epoch(1965)) + .with_epoch_time_secs((config.epoch_time.unwrap().as_secs() / 4).try_into().unwrap()); + + loop { + let res = poll_fn(|cx| ticker.poll_tick(cx)).await; + eprintln!("res: {:?}", res); + } + } + } + #[tokio::test] async fn it_stays_on_the_current_epoch_of_base_time_in_future() { let base_time = time::OffsetDateTime::now_utc() + time::Duration::seconds(1); diff --git a/crates/template_lib/src/models/stealth.rs b/crates/template_lib/src/models/stealth.rs index ce500e56b6..caacd73c05 100644 --- a/crates/template_lib/src/models/stealth.rs +++ b/crates/template_lib/src/models/stealth.rs @@ -93,7 +93,6 @@ pub struct StealthTransferStatement { pub outputs_statement: StealthOutputsStatement, /// Balance proof that proves that no coins were created or destroyed during the transfer (assuming the range proof /// is valid). This may be None, if and only if, the transfer is revealed-only (i.e. no stealth inputs or outputs). - #[cfg_attr(feature = "ts", ts(type = "{public_nonce: string, signature: string} | null"))] pub balance_proof: Option, } diff --git a/crates/template_lib/src/models/vault.rs b/crates/template_lib/src/models/vault.rs index 759ae08462..f2f082f27e 100644 --- a/crates/template_lib/src/models/vault.rs +++ b/crates/template_lib/src/models/vault.rs @@ -355,9 +355,14 @@ impl Vault { .expect("GetResourceAddress returned invalid resource address") } + /// Returns the [ResourceManager] for the resource that this vault holds. + pub fn to_resource_manager(&self) -> ResourceManager { + ResourceManager::get(self.resource_address()) + } + /// Returns the the type of resource that this vault holds. pub fn resource_type(&self) -> ResourceType { - ResourceManager::get(self.resource_address()).resource_type() + self.to_resource_manager().resource_type() } /// Pay a transaction fee with revealed funds present in the vault. diff --git a/crates/template_lib_types/src/resource_type.rs b/crates/template_lib_types/src/resource_type.rs index 5c088ef218..3d0d82711b 100644 --- a/crates/template_lib_types/src/resource_type.rs +++ b/crates/template_lib_types/src/resource_type.rs @@ -21,6 +21,7 @@ use tari_template_abi::rust::{fmt, str::FromStr}; pub enum ResourceType { /// Fungible tokens do not have individual identity, making them interchangeable. /// Examples include monetary units, liquidity pool tokens, or tokenized shares. + // TODO: rename to PublicFungible Fungible, /// A resource (i.e., collection) of non-fungible tokens. /// Each NFT is uniquely identifiable within the parent resource and indivisible. @@ -35,7 +36,7 @@ pub enum ResourceType { impl ResourceType { /// Returns `true` if the resource type is fungible, otherwise `false`. - pub fn is_fungible(&self) -> bool { + pub fn is_public_fungible(&self) -> bool { matches!(self, Self::Fungible) } diff --git a/crates/template_test_tooling/src/support/mod.rs b/crates/template_test_tooling/src/support/mod.rs index 2f7f542079..36021c3fef 100644 --- a/crates/template_test_tooling/src/support/mod.rs +++ b/crates/template_test_tooling/src/support/mod.rs @@ -5,4 +5,4 @@ pub mod assert_error; pub mod confidential; pub mod stealth; -pub use tari_ootle_wallet_crypto::AlwaysMissLookupTable; +pub use tari_ootle_wallet_crypto::GenerateValueLookup; diff --git a/crates/wallet/crypto/src/value_lookup/generate_lookup.rs b/crates/wallet/crypto/src/value_lookup/generate_lookup.rs new file mode 100644 index 0000000000..b975a50351 --- /dev/null +++ b/crates/wallet/crypto/src/value_lookup/generate_lookup.rs @@ -0,0 +1,26 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::convert::Infallible; + +use tari_crypto::{ + keys::PublicKey, + ristretto::{RistrettoPublicKey, RistrettoSecretKey}, +}; +use tari_engine_types::crypto::ValueLookupTable; +use tari_ootle_common_types::array_utils::copy_fixed_checked; +use tari_utilities::ByteArray; + +#[derive(Clone)] +pub struct GenerateValueLookup; + +impl ValueLookupTable for GenerateValueLookup { + type Error = Infallible; + + fn lookup(&mut self, value: u64) -> Result, Self::Error> { + let pk = RistrettoPublicKey::from_secret_key(&RistrettoSecretKey::from(value)); + Ok(Some( + copy_fixed_checked(pk.as_bytes()).expect("Ristretto public key is always 32 bytes"), + )) + } +} diff --git a/crates/wallet/crypto/src/value_lookup/io_reader_value_lookup.rs b/crates/wallet/crypto/src/value_lookup/io_reader_value_lookup.rs index b02611446f..2d4e10a5de 100644 --- a/crates/wallet/crypto/src/value_lookup/io_reader_value_lookup.rs +++ b/crates/wallet/crypto/src/value_lookup/io_reader_value_lookup.rs @@ -7,7 +7,7 @@ use std::{ ops::RangeInclusive, }; -use tari_engine_types::crypto::ValueLookupTable; +use tari_engine_types::crypto::{AndThenLookup, MapErrLookup, ValueLookupTable}; use crate::value_lookup::header::LookupHeader; @@ -34,6 +34,15 @@ impl<'a, R: Read + Seek> IoReaderValueLookup<'a, R> { }) } + pub fn with_fallback(self, fallback: T) -> impl ValueLookupTable + 'a { + AndThenLookup::new( + self, + MapErrLookup::new(fallback, |err| { + io::Error::other(format!("Lookup fallback error: {err}")) + }), + ) + } + fn seek_and_buffer_to_value(&mut self, value: u64) -> io::Result<()> { // Seek to the position of the value. Value must be in range. assert!(self.header.is_in_range(value)); diff --git a/crates/wallet/crypto/src/value_lookup/mod.rs b/crates/wallet/crypto/src/value_lookup/mod.rs index 29d34ac227..a6f0d68b86 100644 --- a/crates/wallet/crypto/src/value_lookup/mod.rs +++ b/crates/wallet/crypto/src/value_lookup/mod.rs @@ -4,19 +4,9 @@ mod header; pub use header::*; +mod generate_lookup; mod io_reader_value_lookup; -use std::convert::Infallible; +pub use generate_lookup::*; pub use io_reader_value_lookup::*; pub use tari_engine_types::crypto::ValueLookupTable; - -#[derive(Clone)] -pub struct AlwaysMissLookupTable; - -impl ValueLookupTable for AlwaysMissLookupTable { - type Error = Infallible; - - fn lookup(&mut self, _value: u64) -> Result, Self::Error> { - Ok(None) - } -} diff --git a/crates/wallet/crypto/tests/viewable_balance_proof.rs b/crates/wallet/crypto/tests/viewable_balance_proof.rs index bba7bb2a26..b818606f20 100644 --- a/crates/wallet/crypto/tests/viewable_balance_proof.rs +++ b/crates/wallet/crypto/tests/viewable_balance_proof.rs @@ -9,7 +9,7 @@ use tari_crypto::{ ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey, RistrettoSecretKey}, }; use tari_engine_types::crypto::validate_elgamal_verifiable_balance_proof; -use tari_ootle_wallet_crypto::{confidential, AlwaysMissLookupTable, UnblindedOutputWitness}; +use tari_ootle_wallet_crypto::{confidential, GenerateValueLookup, UnblindedOutputWitness}; use tari_template_lib::{ template_dependencies::{decode_exact, encode_with_len}, types::{Amount, EncryptedData}, @@ -81,7 +81,7 @@ fn it_generates_a_valid_proof() { let timer = Instant::now(); let balance = proof - .brute_force_balance(&view_key_secret, 0..=1000, &mut AlwaysMissLookupTable) + .brute_force_balance(&view_key_secret, 0..=1000, &mut GenerateValueLookup) .unwrap(); let brute_force_time = timer.elapsed(); assert_eq!(balance, Some(123)); diff --git a/crates/wallet/sdk/Cargo.toml b/crates/wallet/sdk/Cargo.toml index 7b3a4c4943..c4a58eec76 100644 --- a/crates/wallet/sdk/Cargo.toml +++ b/crates/wallet/sdk/Cargo.toml @@ -35,7 +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"] } +tokio = { workspace = true, default-features = false, features = ["sync", "rt-multi-thread"] } [dev-dependencies] tari_ootle_wallet_storage_sqlite = { workspace = true } diff --git a/crates/wallet/sdk/src/apis/confidential_outputs.rs b/crates/wallet/sdk/src/apis/confidential_outputs.rs index bb9f647214..35f0e24ed3 100644 --- a/crates/wallet/sdk/src/apis/confidential_outputs.rs +++ b/crates/wallet/sdk/src/apis/confidential_outputs.rs @@ -116,54 +116,6 @@ where TStore: WalletStore Ok(()) } - pub fn create_lock(&self) -> Result { - let lock_id = self.store.with_write_tx(|tx| tx.locks_create())?; - Ok(lock_id) - } - - pub fn lock_vault_revealed_funds( - &self, - lock_id: WalletLockId, - vault_id: &VaultId, - amount: Amount, - ) -> Result<(), ConfidentialOutputsApiError> { - self.store - .with_write_tx(|tx| tx.vaults_lock_revealed_funds(lock_id, vault_id, amount))?; - Ok(()) - } - - pub fn release_locked_outputs(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { - let mut tx = self.store.create_write_tx()?; - tx.confidential_outputs_release_by_lock_id(lock_id)?; - tx.locks_delete(lock_id)?; - tx.commit()?; - Ok(()) - } - - pub fn release_revealed_funds(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { - let mut tx = self.store.create_write_tx()?; - tx.vaults_release_lock_revealed_funds(lock_id)?; - tx.commit()?; - - Ok(()) - } - - pub fn finalize_outputs_for_lock(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { - let mut tx = self.store.create_write_tx()?; - tx.confidential_outputs_finalize_by_lock_id(lock_id)?; - tx.locks_delete(lock_id)?; - tx.commit()?; - Ok(()) - } - - pub fn finalize_locked_revealed_funds(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { - let mut tx = self.store.create_write_tx()?; - tx.vaults_finalized_locked_revealed_funds(lock_id)?; - tx.commit()?; - - Ok(()) - } - pub fn resolve_output_masks( &self, outputs: Vec, diff --git a/crates/wallet/sdk/src/apis/confidential_transfer.rs b/crates/wallet/sdk/src/apis/confidential_transfer.rs index e11dada28e..b84ecb42fe 100644 --- a/crates/wallet/sdk/src/apis/confidential_transfer.rs +++ b/crates/wallet/sdk/src/apis/confidential_transfer.rs @@ -24,6 +24,7 @@ use crate::{ confidential_outputs::{ConfidentialOutputsApi, ConfidentialOutputsApiError}, config::{ConfigApi, ConfigApiError}, key_manager::{KeyManagerApi, KeyManagerApiError}, + locks::{LocksApi, LocksApiError}, substate::{SubstateApiError, SubstatesApi}, transaction::{TransactionApi, TransactionApiError}, }, @@ -36,6 +37,7 @@ const LOG_TARGET: &str = "tari::ootle::wallet_sdk::apis::confidential_transfers" pub struct ConfidentialTransferApi<'a, TStore, TNetworkInterface> { key_manager_api: KeyManagerApi<'a, TStore>, + locks_api: LocksApi<'a, TStore>, accounts_api: AccountsApi<'a, TStore, TNetworkInterface>, confidential_outputs_api: ConfidentialOutputsApi<'a, TStore>, transaction_api: TransactionApi<'a, TStore, TNetworkInterface>, @@ -53,6 +55,7 @@ where pub fn new( key_manager_api: KeyManagerApi<'a, TStore>, accounts_api: AccountsApi<'a, TStore, TNetworkInterface>, + locks_api: LocksApi<'a, TStore>, confidential_outputs_api: ConfidentialOutputsApi<'a, TStore>, substate_api: SubstatesApi<'a, TStore, TNetworkInterface>, transaction_api: TransactionApi<'a, TStore, TNetworkInterface>, @@ -61,6 +64,7 @@ where ) -> Self { Self { key_manager_api, + locks_api, accounts_api, confidential_outputs_api, substate_api, @@ -103,7 +107,6 @@ where Ok(InputsToSpend { confidential: confidential_inputs, - lock_id, revealed: Amount::zero(), }) }, @@ -112,8 +115,8 @@ where return Err(ConfidentialTransferApiError::InsufficientFunds); } - self.confidential_outputs_api - .lock_vault_revealed_funds(lock_id, &src_vault.id, spend_amount)?; + self.locks_api + .lock_funds_in_vault(lock_id, &src_vault.id, spend_amount)?; info!( target: LOG_TARGET, @@ -124,7 +127,6 @@ where Ok(InputsToSpend { confidential: vec![], - lock_id, revealed: spend_amount, }) }, @@ -139,15 +141,11 @@ where src_vault.id, ); - self.confidential_outputs_api.lock_vault_revealed_funds( - lock_id, - &src_vault.id, - revealed_to_spend, - )?; + self.locks_api + .lock_funds_in_vault(lock_id, &src_vault.id, revealed_to_spend)?; return Ok(InputsToSpend { confidential: vec![], - lock_id, revealed: revealed_to_spend, }); } @@ -163,8 +161,8 @@ where let total_confidential_spent = confidential_inputs.iter().map(|i| i.value).sum::(); - self.confidential_outputs_api - .lock_vault_revealed_funds(lock_id, &src_vault.id, revealed_to_spend)?; + self.locks_api + .lock_funds_in_vault(lock_id, &src_vault.id, revealed_to_spend)?; info!( target: LOG_TARGET, @@ -179,7 +177,6 @@ where Ok(InputsToSpend { confidential: confidential_inputs, - lock_id, revealed: revealed_to_spend, }) }, @@ -196,8 +193,8 @@ where return Err(ConfidentialTransferApiError::InsufficientFunds); } - self.confidential_outputs_api - .lock_vault_revealed_funds(lock_id, &src_vault.id, revealed_to_spend)?; + self.locks_api + .lock_funds_in_vault(lock_id, &src_vault.id, revealed_to_spend)?; let confidential_inputs = self .confidential_outputs_api @@ -205,7 +202,6 @@ where Ok(InputsToSpend { confidential: confidential_inputs, - lock_id, revealed: revealed_to_spend, }) }, @@ -282,9 +278,9 @@ where let account_public_key = PublicKey::from_secret_key(&account_key.secret); // Reserve and lock input funds - let lock_id = self.confidential_outputs_api.create_lock()?; + let lock = self.locks_api.create_lock()?; let inputs_to_spend = match self.resolved_inputs_for_transfer( - lock_id, + lock.id(), params.from_account, params.resource_address, params.amount, @@ -363,7 +359,7 @@ where public_asset_tag: None, memo: None, status: OutputStatus::LockedUnconfirmed, - lock_id: Some(inputs_to_spend.lock_id), + lock_id: Some(lock.id()), })?; } @@ -420,12 +416,13 @@ where .build_and_seal(&account_key.secret); let tx_id = transaction.calculate_id(); - self.transaction_api - .locks_set_transaction_id(inputs_to_spend.lock_id, tx_id)?; + self.transaction_api.locks_set_transaction_id(lock.id(), tx_id)?; + + let lock_id = lock.keep_locked(); Ok(TransferOutput { transaction, - transaction_proof_id: inputs_to_spend.lock_id, + transaction_proof_id: lock_id, }) } @@ -538,7 +535,6 @@ pub enum UtxoInputSelection { #[derive(Debug)] pub struct InputsToSpend { pub confidential: Vec, - pub lock_id: WalletLockId, pub revealed: Amount, } @@ -578,6 +574,8 @@ pub enum ConfidentialTransferApiError { AmountOverflow { param: &'static str, details: String }, #[error("Transaction API error: {0}")] TransactionApiError(#[from] TransactionApiError), + #[error("Lock error: {0}")] + LocksApiError(#[from] LocksApiError), } impl IsNotFoundError for ConfidentialTransferApiError { diff --git a/crates/wallet/sdk/src/apis/locks.rs b/crates/wallet/sdk/src/apis/locks.rs new file mode 100644 index 0000000000..743a021357 --- /dev/null +++ b/crates/wallet/sdk/src/apis/locks.rs @@ -0,0 +1,85 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::time::Duration; + +use tari_engine_types::substate::SubstateDiff; +use tari_ootle_common_types::optional::IsNotFoundError; +use tari_template_lib::{models::VaultId, prelude::Amount}; +use tari_transaction::TransactionId; + +use crate::{ + models::{WalletLockDropGuard, WalletLockId}, + storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, +}; + +#[derive(Clone)] +pub struct LocksApi<'a, TStore> { + store: &'a TStore, +} + +impl<'a, TStore: WalletStore> LocksApi<'a, TStore> { + pub(crate) fn new(store: &'a TStore) -> Self { + Self { store } + } + + pub fn create_lock(&self) -> Result, LocksApiError> { + let lock_id = self.store.with_write_tx(|tx| tx.locks_create(None))?; + Ok(WalletLockDropGuard::new(lock_id, self.store)) + } + + pub fn create_lock_with_timeout( + &self, + timeout: Duration, + ) -> Result, LocksApiError> { + let lock_id = self.store.with_write_tx(|tx| tx.locks_create(Some(timeout)))?; + Ok(WalletLockDropGuard::new(lock_id, self.store)) + } + + pub fn release_lock(&self, lock_id: WalletLockId) -> Result<(), LocksApiError> { + self.store.with_write_tx(|tx| tx.locks_release(lock_id))?; + Ok(()) + } + + pub fn finalize_lock(&self, lock_id: WalletLockId, diff: &SubstateDiff) -> Result<(), LocksApiError> { + self.store + .with_write_tx(|tx| tx.locks_unlock_finalized(lock_id, diff))?; + Ok(()) + } + + pub fn lock_funds_in_vault>( + &self, + lock_id: WalletLockId, + vault_id: &VaultId, + amount_to_lock: A, + ) -> Result<(), LocksApiError> { + self.store + .with_write_tx(|tx| tx.vaults_lock_revealed_funds(lock_id, vault_id, amount_to_lock.into()))?; + + Ok(()) + } + + pub fn clear_stale_locks(&self) -> Result { + let num = self.store.with_write_tx(|tx| tx.locks_release_stale())?; + Ok(num) + } + + pub fn get_lock_by_transaction_id(&self, transaction_id: TransactionId) -> Result { + let lock_id = self + .store + .with_read_tx(|tx| tx.locks_get_by_transaction_id(transaction_id))?; + Ok(lock_id) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum LocksApiError { + #[error("Store error: {0}")] + StoreError(#[from] WalletStorageError), +} + +impl IsNotFoundError for LocksApiError { + fn is_not_found_error(&self) -> bool { + matches!(self, LocksApiError::StoreError(e) if e.is_not_found_error()) + } +} diff --git a/crates/wallet/sdk/src/apis/mod.rs b/crates/wallet/sdk/src/apis/mod.rs index 203d89146b..0317c08043 100644 --- a/crates/wallet/sdk/src/apis/mod.rs +++ b/crates/wallet/sdk/src/apis/mod.rs @@ -7,6 +7,7 @@ pub mod confidential_outputs; pub mod confidential_transfer; pub mod config; pub mod key_manager; +pub mod locks; pub mod non_fungible_tokens; pub mod password_manager; pub mod resources; diff --git a/crates/wallet/sdk/src/apis/stealth_outputs.rs b/crates/wallet/sdk/src/apis/stealth_outputs.rs index 81dd1b4fb3..ffbeb31feb 100644 --- a/crates/wallet/sdk/src/apis/stealth_outputs.rs +++ b/crates/wallet/sdk/src/apis/stealth_outputs.rs @@ -9,7 +9,6 @@ use tari_crypto::{ }; use tari_engine_types::{ component::derive_component_address_from_public_key, - substate::SubstateDiff, FromByteType, ToByteType, Utxo, @@ -29,7 +28,7 @@ use tari_ootle_wallet_crypto::{ }; use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS; use tari_template_lib::{ - models::{ComponentAddress, ResourceAddress, StealthTransferStatement, UtxoAddress, VaultId}, + models::{ComponentAddress, ResourceAddress, StealthTransferStatement, UtxoAddress}, prelude::{PedersenCommitmentBytes, RistrettoPublicKeyBytes}, types::{Amount, EncryptedData}, }; @@ -37,7 +36,6 @@ use tari_template_lib::{ use crate::{ apis::{ accounts::AccountsApiError, - confidential_outputs::ConfidentialOutputsApiError, config::{ConfigApi, ConfigApiError}, key_manager::{KeyManagerApi, KeyManagerApiError}, stealth_crypto::{StealthCryptoApi, StealthCryptoApiError}, @@ -168,46 +166,6 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { Ok(()) } - pub fn lock_funds_in_vault>( - &self, - lock_id: WalletLockId, - vault_id: &VaultId, - amount_to_lock: A, - ) -> Result<(), StealthOutputsApiError> { - self.store - .with_write_tx(|tx| tx.vaults_lock_revealed_funds(lock_id, vault_id, amount_to_lock.into()))?; - Ok(()) - } - - // TODO: move into a lock api - pub fn create_lock(&self) -> Result { - let lock_id = self.store.with_write_tx(|tx| tx.locks_create())?; - Ok(lock_id) - } - - pub fn release_lock(&self, lock_id: WalletLockId) -> Result<(), StealthOutputsApiError> { - self.store.with_write_tx(|tx| tx.locks_release(lock_id))?; - Ok(()) - } - - 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 lock_revealed_funds>( - &self, - lock_id: WalletLockId, - vault_id: &VaultId, - amount_to_lock: A, - ) -> Result<(), StealthOutputsApiError> { - self.store - .with_write_tx(|tx| tx.vaults_lock_revealed_funds(lock_id, vault_id, amount_to_lock.into()))?; - - Ok(()) - } - fn resolve_output_masks_for_spending( &self, spend_key_branch: KeyBranch, diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/api.rs b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs index 7cd3827fbc..81a769438a 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/api.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs @@ -1,7 +1,7 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::{cmp, collections::HashSet}; +use std::{cmp, collections::HashSet, time::Duration}; use log::*; use tari_crypto::ristretto::RistrettoPublicKey; @@ -42,10 +42,19 @@ use crate::{ confidential_transfer::UtxoInputSelection, config::ConfigApi, key_manager::KeyManagerApi, + locks::LocksApi, stealth_outputs::{StealthOutputsApi, TransferStatementParams}, substate::{SubstatesApi, ValidatorScanResult}, }, - models::{AccountWithAddress, KeyBranch, KeyId, OutputStatus, StealthOutputModel, WalletLockId}, + models::{ + AccountWithAddress, + KeyBranch, + KeyId, + OutputStatus, + StealthOutputModel, + WalletLockDropGuard, + WalletLockId, + }, network::WalletNetworkInterface, storage::WalletStore, }; @@ -55,6 +64,7 @@ const LOG_TARGET: &str = "tari::ootle::wallet_sdk::apis::stealth_transfers"; pub struct StealthTransferApi<'a, TStore, TNetworkInterface> { accounts_api: AccountsApi<'a, TStore, TNetworkInterface>, outputs_api: StealthOutputsApi<'a, TStore>, + locks_api: LocksApi<'a, TStore>, substate_api: SubstatesApi<'a, TStore, TNetworkInterface>, key_manager_api: KeyManagerApi<'a, TStore>, config_api: ConfigApi<'a, TStore>, @@ -70,6 +80,7 @@ where pub fn new( accounts_api: AccountsApi<'a, TStore, TNetworkInterface>, outputs_api: StealthOutputsApi<'a, TStore>, + locks_api: LocksApi<'a, TStore>, substate_api: SubstatesApi<'a, TStore, TNetworkInterface>, key_manager_api: KeyManagerApi<'a, TStore>, config_api: ConfigApi<'a, TStore>, @@ -77,6 +88,7 @@ where Self { accounts_api, outputs_api, + locks_api, substate_api, key_manager_api, config_api, @@ -150,7 +162,7 @@ where ), })?; - self.outputs_api + self.locks_api .lock_funds_in_vault(lock_id, &src_vault.id, spend_amount)?; info!( @@ -178,7 +190,7 @@ where src_vault.id ); - self.outputs_api + self.locks_api .lock_funds_in_vault(lock_id, &src_vault.id, revealed_to_spend)?; return Ok(InputsToSpend { @@ -211,8 +223,8 @@ where .expect("BUG: an unblinded input amount was negative"); if let Some(ref src_vault) = maybe_src_vault { - self.outputs_api - .lock_revealed_funds(lock_id, &src_vault.id, revealed_to_spend)?; + self.locks_api + .lock_funds_in_vault(lock_id, &src_vault.id, revealed_to_spend)?; } info!( @@ -232,7 +244,6 @@ where }) }, UtxoInputSelection::PreferConfidential => { - let lock_id = self.outputs_api.create_lock()?; let (inputs, blinded_amount_locked) = self.outputs_api.lock_outputs_until_partial_amount( owner_account_component_address, &resource_address, @@ -264,8 +275,8 @@ where ), })?; - self.outputs_api - .lock_revealed_funds(lock_id, &vault.id, revealed_to_spend)?; + self.locks_api + .lock_funds_in_vault(lock_id, &vault.id, revealed_to_spend)?; } Ok(InputsToSpend { @@ -297,7 +308,7 @@ where &self, owner_account: AccountWithAddress, params: StealthTransferParams, - ) -> Result { + ) -> Result<(WalletLockDropGuard<'a, TStore>, StealthTransferOutput), StealthTransferApiError> { let network = self.config_api.get_network()?; params.validate(network)?; @@ -353,13 +364,12 @@ where let _permit = self.semaphore.acquire().await.expect("semaphore is never closed"); block_in_place(|| { - let lock_id = self.outputs_api.create_lock()?; + // Create a lock with a timeout, the lock timeout will be removed if the lock is assigned a transaction + let lock = self.locks_api.create_lock_with_timeout(Duration::from_secs(5 * 60))?; // 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, params.max_fee, params.fee_input_selection), - )?; + let fee_inputs_to_spend = + self.lock_fee_inputs(lock.id(), &owner_account, params.max_fee, params.fee_input_selection)?; let fee_stealth_change_amt = fee_inputs_to_spend .total_stealth_input_amount() @@ -379,8 +389,7 @@ where 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))?; + let next_index = self.key_manager_api.next_derived_key_index(KeyBranch::Nonce)?; (KeyBranch::Nonce, KeyId::derived(next_index)) }; let required_signer = self @@ -390,21 +399,18 @@ where let fee_signer = required_signer; // Generate fee transfer statement - let fee_transfer_statement = self.unlock_on_failure( - lock_id, - self.outputs_api.generate_transfer_statement(TransferStatementParams { - spend_key_branch: KeyBranch::Account, - spend_key_id: owner_key_id, - view_only_key_id: owner_account.view_only_key_id(), - resource_address: ¶ms.resource_address, - resource_view_key: None, - inputs: &fee_inputs_to_spend.inputs, - input_revealed_amount: fee_inputs_to_spend.revealed, - outputs: fee_change_output, - output_revealed_amount: Amount::from(params.max_fee), - required_signer: required_signer_pk, - }), - )?; + let fee_transfer_statement = self.outputs_api.generate_transfer_statement(TransferStatementParams { + spend_key_branch: KeyBranch::Account, + spend_key_id: owner_key_id, + view_only_key_id: owner_account.view_only_key_id(), + resource_address: ¶ms.resource_address, + resource_view_key: None, + inputs: &fee_inputs_to_spend.inputs, + input_revealed_amount: fee_inputs_to_spend.revealed, + outputs: fee_change_output, + output_revealed_amount: Amount::from(params.max_fee), + required_signer: required_signer_pk, + })?; // Add the unconfirmed fee change output to the wallet store if let Some(output) = fee_transfer_statement.outputs_statement.outputs.first() { @@ -415,30 +421,24 @@ where fee_stealth_change_amt, owner_account.component_address() ); - self.unlock_on_failure( - lock_id, - self.add_unconfirmed_output_from_statement( - lock_id, - &owner_account, - XTR, - output, - fee_stealth_change_amt, - None, - ), + self.add_unconfirmed_output_from_statement( + lock.id(), + &owner_account, + XTR, + output, + fee_stealth_change_amt, + None, )?; } // 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, - ), + let inputs_to_spend = 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 @@ -446,8 +446,7 @@ where 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))?; + let next_index = 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 { @@ -509,24 +508,21 @@ where .map(TryInto::try_into) .collect::, StealthTransferApiError>>()?; - let transfer_statement = self.unlock_on_failure( - lock_id, - self.outputs_api.generate_transfer_statement(TransferStatementParams { - spend_key_branch: KeyBranch::Account, - spend_key_id: owner_key_id, - view_only_key_id: owner_account.view_only_key_id(), - resource_address: ¶ms.resource_address, - resource_view_key, - inputs: &inputs_to_spend.inputs, - input_revealed_amount: inputs_to_spend.revealed, - outputs: outputs_to_create - .into_iter() - .chain(change_output) - .filter(|o| o.amount.is_positive()), - output_revealed_amount: params.total_revealed_output_amount(), - required_signer: required_signer_pk, - }), - )?; + let transfer_statement = self.outputs_api.generate_transfer_statement(TransferStatementParams { + spend_key_branch: KeyBranch::Account, + spend_key_id: owner_key_id, + view_only_key_id: owner_account.view_only_key_id(), + resource_address: ¶ms.resource_address, + resource_view_key, + inputs: &inputs_to_spend.inputs, + input_revealed_amount: inputs_to_spend.revealed, + outputs: outputs_to_create + .into_iter() + .chain(change_output) + .filter(|o| o.amount.is_positive()), + output_revealed_amount: params.total_revealed_output_amount(), + required_signer: required_signer_pk, + })?; // 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 @@ -540,16 +536,13 @@ where 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, - ), + self.add_unconfirmed_output_from_statement( + lock.id(), + &owner_account, + params.resource_address, + output, + change_amount, + None, )?; } } @@ -588,27 +581,23 @@ where substate_inputs.push(SubstateRequirement::unversioned(badge_vault.id)); } - let transaction = self.unlock_on_failure( - lock_id, - self.generate_transfer_transaction( - network, - &owner_account, - params, - substate_inputs, - fee_transfer_statement, - transfer_statement, - &accounts_to_create, - ), + let transaction = self.generate_transfer_transaction( + network, + &owner_account, + params, + substate_inputs, + fee_transfer_statement, + transfer_statement, + &accounts_to_create, )?; - Ok(StealthTransferOutput { + Ok((lock, StealthTransferOutput { transaction, - lock_id, fee_inputs: fee_inputs_to_spend, transfer_inputs: inputs_to_spend, additional_signer: main_signer, main_signer: fee_signer, - }) + })) }) } @@ -701,18 +690,6 @@ where } } - pub fn unlock_on_failure(&self, lock_id: WalletLockId, result: Result) -> Result { - match result { - Ok(value) => Ok(value), - Err(e) => { - if let Err(err) = self.outputs_api.release_lock(lock_id) { - error!(target: LOG_TARGET, "Failed to release inputs lock after error: {}", err); - } - Err(e) - }, - } - } - #[allow(clippy::too_many_lines)] fn generate_transfer_transaction( &self, diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/error.rs b/crates/wallet/sdk/src/apis/stealth_transfer/error.rs index 8405f08923..f53907f0a0 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/error.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/error.rs @@ -9,6 +9,7 @@ use crate::{ accounts::AccountsApiError, config::ConfigApiError, key_manager::KeyManagerApiError, + locks::LocksApiError, stealth_crypto::StealthCryptoApiError, stealth_outputs::StealthOutputsApiError, substate::SubstateApiError, @@ -50,6 +51,8 @@ pub enum StealthTransferApiError { InsufficientRevealedFunds { details: String }, #[error("Invariant violation: {details}")] InvariantViolation { details: String }, + #[error("Locks API error: {0}")] + LocksApiError(#[from] LocksApiError), } impl IsNotFoundError for StealthTransferApiError { diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/types.rs b/crates/wallet/sdk/src/apis/stealth_transfer/types.rs index 2585bb39b1..1de76c98ee 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/types.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/types.rs @@ -9,11 +9,10 @@ use tari_template_lib::{ }; use tari_transaction::UnsignedTransaction; -use crate::models::{InputSpendData, WalletLockId, WalletPublicKey}; +use crate::models::{InputSpendData, WalletPublicKey}; pub struct StealthTransferOutput { pub transaction: UnsignedTransaction, - pub lock_id: WalletLockId, pub fee_inputs: InputsToSpend, pub transfer_inputs: InputsToSpend, pub additional_signer: Option, diff --git a/crates/wallet/sdk/src/models/lock_guard.rs b/crates/wallet/sdk/src/models/lock_guard.rs index cc1601622f..0b56cff79b 100644 --- a/crates/wallet/sdk/src/models/lock_guard.rs +++ b/crates/wallet/sdk/src/models/lock_guard.rs @@ -8,31 +8,37 @@ use crate::{ const LOG_TARGET: &str = "tari::ootle::wallet::models::lock_guard"; -pub struct WalletLockDropGuard { +pub struct WalletLockDropGuard<'a, TStore: WriteableWalletStore> { lock_id: WalletLockId, - store: Option, + store: Option<&'a TStore>, } -impl WalletLockDropGuard +impl<'a, TStore> WalletLockDropGuard<'a, TStore> where TStore: WriteableWalletStore { - pub fn new(lock_id: WalletLockId, store: TStore) -> Self { + pub fn new(lock_id: WalletLockId, store: &'a TStore) -> Self { Self { lock_id, store: Some(store), } } - pub fn lock_id(&self) -> &WalletLockId { - &self.lock_id + pub fn id(&self) -> WalletLockId { + self.lock_id } - pub fn disarm(mut self) { + pub fn keep_locked(mut self) -> WalletLockId { self.store = None; + self.lock_id + } + + pub fn release(self) -> WalletLockId { + self.lock_id + // Drop will be called here, releasing the lock } } -impl Drop for WalletLockDropGuard +impl Drop for WalletLockDropGuard<'_, TStore> where TStore: WriteableWalletStore { fn drop(&mut self) { diff --git a/crates/wallet/sdk/src/models/wallet_transaction.rs b/crates/wallet/sdk/src/models/wallet_transaction.rs index 0c685b3855..395db1b369 100644 --- a/crates/wallet/sdk/src/models/wallet_transaction.rs +++ b/crates/wallet/sdk/src/models/wallet_transaction.rs @@ -6,7 +6,7 @@ use std::{fmt::Display, str::FromStr, time::Duration}; use anyhow::anyhow; use serde::{Deserialize, Serialize}; use tari_consensus_types::ProposalCertificate; -use tari_engine_types::commit_result::FinalizeResult; +use tari_engine_types::{commit_result::FinalizeResult, substate::SubstateDiff}; use tari_transaction::{Transaction, TransactionId}; use time::PrimitiveDateTime; @@ -33,6 +33,26 @@ pub struct WalletTransaction { pub last_update_time: PrimitiveDateTime, } +impl WalletTransaction { + pub fn is_accepted(&self) -> bool { + self.status.is_accepted() + } + + pub fn finalized_diff(&self) -> Option<&SubstateDiff> { + self.finalize.as_ref().and_then(|f| f.any_accept()) + } + + pub fn failure_reason_as_string(&self) -> Option { + if let Some(reason) = &self.invalid_reason { + return Some(reason.clone()); + } + self.finalize + .as_ref() + .and_then(|finalize| finalize.any_reject()) + .map(|reject| reject.to_string()) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub enum TransactionStatus { diff --git a/crates/wallet/sdk/src/sdk.rs b/crates/wallet/sdk/src/sdk.rs index 8490f6a529..08eb8a3c92 100644 --- a/crates/wallet/sdk/src/sdk.rs +++ b/crates/wallet/sdk/src/sdk.rs @@ -24,6 +24,7 @@ use crate::{ confidential_transfer::ConfidentialTransferApi, config::{ConfigApi, ConfigApiError, ConfigKey}, key_manager::{KeyManagerApi, KeyManagerApiError}, + locks::LocksApi, non_fungible_tokens::NonFungibleTokensApi, password_manager::{PasswordManagerApi, PasswordManagerApiError}, resources::ResourcesApi, @@ -194,6 +195,10 @@ where &self.network_interface } + pub fn locks_api(&self) -> LocksApi<'_, TStore> { + LocksApi::new(&self.store) + } + /// Returns the KeyManager API for the wallet. pub fn key_manager_api(&self) -> KeyManagerApi<'_, TStore> { let network = self.config.network; @@ -250,6 +255,7 @@ where ConfidentialTransferApi::new( self.key_manager_api(), self.accounts_api(), + self.locks_api(), self.confidential_outputs_api(), self.substate_api(), self.transaction_api(), @@ -266,6 +272,7 @@ where StealthTransferApi::new( self.accounts_api(), self.stealth_outputs_api(), + self.locks_api(), self.substate_api(), self.key_manager_api(), self.config_api(), diff --git a/crates/wallet/sdk/src/storage.rs b/crates/wallet/sdk/src/storage.rs index 748fa69d43..ae6f1267ba 100644 --- a/crates/wallet/sdk/src/storage.rs +++ b/crates/wallet/sdk/src/storage.rs @@ -4,6 +4,7 @@ use std::{ collections::{HashMap, HashSet}, ops::{Deref, DerefMut}, + time::Duration, }; use tari_engine_types::{ @@ -466,7 +467,7 @@ pub trait WalletStoreWriter: CommitableStore { ) -> Result<(), WalletStorageError>; // Locks - fn locks_create(&mut self) -> Result; + fn locks_create(&mut self, timeout: Option) -> Result; fn locks_delete(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError>; fn locks_link_transaction( @@ -475,6 +476,8 @@ pub trait WalletStoreWriter: CommitableStore { transaction_id: TransactionId, ) -> Result<(), WalletStorageError>; + fn locks_release_stale(&mut self) -> Result; + /// 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>; diff --git a/crates/wallet/sdk/tests/confidential_output_api.rs b/crates/wallet/sdk/tests/confidential_output_api.rs index 8c80e8fd5b..f9d14e352e 100644 --- a/crates/wallet/sdk/tests/confidential_output_api.rs +++ b/crates/wallet/sdk/tests/confidential_output_api.rs @@ -4,7 +4,7 @@ mod support; use tari_crypto::commitment::HomomorphicCommitmentFactory; -use tari_engine_types::{crypto::get_commitment_factory, ToByteType}; +use tari_engine_types::{crypto::get_commitment_factory, substate::SubstateDiff, ToByteType}; use tari_ootle_wallet_sdk::{ models::{ConfidentialOutputModel, KeyId, OutputStatus}, storage::{ReadableWalletStore, WalletStoreReader}, @@ -21,7 +21,7 @@ fn outputs_locked_and_released() { let commitment_49 = test.add_unspent_output(49); let _commitment_100 = test.add_unspent_output(100); - let lock_id = test.new_lock(); + let lock_id = test.new_lock().keep_locked(); let (inputs, total_value) = test .sdk() .confidential_outputs_api() @@ -39,10 +39,7 @@ fn outputs_locked_and_released() { assert!(locked.iter().any(|l| l.commitment == commitment_49)); assert_eq!(locked.len(), 2); - test.sdk() - .confidential_outputs_api() - .release_locked_outputs(lock_id) - .unwrap(); + test.sdk().locks_api().release_lock(lock_id).unwrap(); let locked = test .store() @@ -60,17 +57,18 @@ fn outputs_locked_and_finalized() { let commitment_100 = test.add_unspent_output(100); let outputs_api = test.sdk().confidential_outputs_api(); - let proof_id = test.new_lock(); + let locks_api = test.sdk().locks_api(); + let lock_id = test.new_lock().keep_locked(); let (inputs, total_value) = outputs_api - .lock_outputs_by_amount(proof_id, &Test::test_vault_address(), 50) + .lock_outputs_by_amount(lock_id, &Test::test_vault_address(), 50) .unwrap(); assert_eq!(total_value, 74); assert_eq!(inputs.len(), 2); let locked = test .store() - .with_read_tx(|tx| tx.confidential_outputs_get_locked_by_lock_id(proof_id)) + .with_read_tx(|tx| tx.confidential_outputs_get_locked_by_lock_id(lock_id)) .unwrap(); assert!(locked.iter().any(|l| l.commitment == commitment_25)); @@ -94,18 +92,19 @@ fn outputs_locked_and_finalized() { public_asset_tag: None, memo: None, status: OutputStatus::LockedUnconfirmed, - lock_id: Some(proof_id), + lock_id: Some(lock_id), }) .unwrap(); let balance = test.get_unspent_balance(); assert_eq!(balance, 100); - outputs_api.finalize_outputs_for_lock(proof_id).unwrap(); + let diff = SubstateDiff::new(); + locks_api.finalize_lock(lock_id, &diff).unwrap(); { let mut tx = test.store().create_read_tx().unwrap(); - let locked = tx.confidential_outputs_get_locked_by_lock_id(proof_id).unwrap(); + let locked = tx.confidential_outputs_get_locked_by_lock_id(lock_id).unwrap(); assert_eq!(locked.len(), 0); let unspent = tx diff --git a/crates/wallet/sdk/tests/support/harness.rs b/crates/wallet/sdk/tests/support/harness.rs index dffa3dd067..0aedfb42e7 100644 --- a/crates/wallet/sdk/tests/support/harness.rs +++ b/crates/wallet/sdk/tests/support/harness.rs @@ -13,7 +13,7 @@ use tari_engine_types::{ use tari_ootle_common_types::{optional::Optional, shard::Shard, Network, StateVersion}; use tari_ootle_wallet_sdk::{ cipher_seed::CipherSeedRestore, - models::{ConfidentialOutputModel, KeyId, OutputStatus, UtxoUpdateSet, WalletLockId}, + models::{ConfidentialOutputModel, KeyId, OutputStatus, UtxoUpdateSet, WalletLockDropGuard, WalletLockId}, network::{SubstateQueryResult, TransactionQueryResult, UtxoUpdateStream, WalletNetworkInterface}, storage::TagAndPublicNoncePair, WalletSdk, @@ -115,8 +115,8 @@ impl Test { commitment } - pub fn new_lock(&self) -> WalletLockId { - self.sdk.confidential_outputs_api().create_lock().unwrap() + pub fn new_lock(&self) -> WalletLockDropGuard<'_, SqliteWalletStore> { + self.sdk.locks_api().create_lock().unwrap() } pub fn get_unspent_balance(&self) -> Amount { diff --git a/crates/wallet/sdk_services/src/transaction_service/error.rs b/crates/wallet/sdk_services/src/transaction_service/error.rs index 8179e7075c..0f5a643095 100644 --- a/crates/wallet/sdk_services/src/transaction_service/error.rs +++ b/crates/wallet/sdk_services/src/transaction_service/error.rs @@ -1,7 +1,7 @@ // Copyright 2024 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_ootle_wallet_sdk::apis::transaction::TransactionApiError; +use tari_ootle_wallet_sdk::apis::{locks::LocksApiError, transaction::TransactionApiError}; #[derive(Debug, thiserror::Error)] pub enum TransactionServiceError { @@ -11,4 +11,6 @@ pub enum TransactionServiceError { TransactionApiError(#[from] TransactionApiError), #[error("Dry run transaction failed: {details}")] DryRunTransactionFailed { details: String }, + #[error("Lock API error: {0}")] + LockApiError(#[from] LocksApiError), } diff --git a/crates/wallet/sdk_services/src/transaction_service/service.rs b/crates/wallet/sdk_services/src/transaction_service/service.rs index 9881eef39b..fbfce29d9a 100644 --- a/crates/wallet/sdk_services/src/transaction_service/service.rs +++ b/crates/wallet/sdk_services/src/transaction_service/service.rs @@ -197,12 +197,33 @@ where if let Err(err) = Self::check_pending_transactions(&wallet_sdk, ¬ify).await { error!(target: LOG_TARGET, "Error checking pending transactions: {}", err); } + if let Err(err) = Self::clear_stale_locks(&wallet_sdk) { + error!(target: LOG_TARGET, "Error clearing stale locks: {}", err); + } drop(permit); }); Ok(()) } + fn clear_stale_locks(wallet_sdk: &WalletSdk) -> Result<(), TransactionServiceError> { + let transaction_api = wallet_sdk.locks_api(); + let num_cleared = transaction_api.clear_stale_locks()?; + if num_cleared > 0 { + info!( + target: LOG_TARGET, + "Cleared {} stale wallet lock(s)", + num_cleared + ); + } else { + debug!( + target: LOG_TARGET, + "No stale wallet locks to clear", + ); + } + Ok(()) + } + async fn resubmit_new_transactions( wallet_sdk: &WalletSdk, notify: &Notify, diff --git a/crates/wallet/storage_sqlite/migrations/2025-10-29-122554_add_timeout_at_to_locks/down.sql b/crates/wallet/storage_sqlite/migrations/2025-10-29-122554_add_timeout_at_to_locks/down.sql new file mode 100644 index 0000000000..d9a93fe9a1 --- /dev/null +++ b/crates/wallet/storage_sqlite/migrations/2025-10-29-122554_add_timeout_at_to_locks/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/crates/wallet/storage_sqlite/migrations/2025-10-29-122554_add_timeout_at_to_locks/up.sql b/crates/wallet/storage_sqlite/migrations/2025-10-29-122554_add_timeout_at_to_locks/up.sql new file mode 100644 index 0000000000..13073afeae --- /dev/null +++ b/crates/wallet/storage_sqlite/migrations/2025-10-29-122554_add_timeout_at_to_locks/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE locks + ADD COLUMN timeout_at DATETIME NULL; \ No newline at end of file diff --git a/crates/wallet/storage_sqlite/src/schema.rs b/crates/wallet/storage_sqlite/src/schema.rs index aa888d286f..95a5e9b048 100644 --- a/crates/wallet/storage_sqlite/src/schema.rs +++ b/crates/wallet/storage_sqlite/src/schema.rs @@ -97,6 +97,7 @@ diesel::table! { id -> Integer, transaction_id -> Nullable, created_at -> Timestamp, + timeout_at -> Nullable, } } diff --git a/crates/wallet/storage_sqlite/src/writer.rs b/crates/wallet/storage_sqlite/src/writer.rs index ae19adfce7..a753e98897 100644 --- a/crates/wallet/storage_sqlite/src/writer.rs +++ b/crates/wallet/storage_sqlite/src/writer.rs @@ -7,6 +7,7 @@ use std::{ ops::{Add, Deref, DerefMut, Sub}, str::FromStr, sync::MutexGuard, + time::Duration, }; use diesel::{ @@ -1345,14 +1346,22 @@ impl WalletStoreWriter for WriteTransaction<'_> { } // locks - fn locks_create(&mut self) -> Result { + fn locks_create(&mut self, timeout: Option) -> Result { const OPERATION: &str = "locks_create"; use crate::schema::locks; - diesel::insert_into(locks::table) - .default_values() - .execute(self.connection()) - .map_err(|e| WalletStorageError::general(OPERATION, e))?; + if let Some(timeout) = timeout { + let timeout_seconds = i32::try_from(timeout.as_secs()).unwrap_or(i32::MAX); + diesel::insert_into(locks::table) + .values(locks::timeout_at.eq(dsl::sql(&format!("datetime('now', '+{} seconds')", timeout_seconds)))) + .execute(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + } else { + diesel::insert_into(locks::table) + .default_values() + .execute(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + } // TODO: See if we can upgrade libSQLite 0.35 let lock_id = locks::table .select(locks::id) @@ -1383,13 +1392,34 @@ impl WalletStoreWriter for WriteTransaction<'_> { use crate::schema::locks; diesel::update(locks::table.filter(locks::id.eq(lock_id))) - .set(locks::transaction_id.eq(serialize_hex(transaction_id))) + .set(( + locks::transaction_id.eq(serialize_hex(transaction_id)), + locks::timeout_at.eq(None::), + )) .execute(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; Ok(()) } + fn locks_release_stale(&mut self) -> Result { + const OPERATION: &str = "locks_release_stale"; + use crate::schema::locks; + + let stale_locks = locks::table + .select(locks::id) + .filter(locks::timeout_at.is_not_null()) + .filter(locks::timeout_at.le(dsl::now)) + .get_results::(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + let num_stale = stale_locks.len(); + for lock_id in stale_locks { + self.locks_release(lock_id)?; + } + + Ok(num_stale) + } + 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)?; diff --git a/utilities/generate_ristretto_value_lookup/src/main.rs b/utilities/generate_ristretto_value_lookup/src/main.rs index 19adf16d99..6841531a76 100644 --- a/utilities/generate_ristretto_value_lookup/src/main.rs +++ b/utilities/generate_ristretto_value_lookup/src/main.rs @@ -15,7 +15,7 @@ use tari_crypto::{ ristretto::{RistrettoPublicKey, RistrettoSecretKey}, tari_utilities::ByteArray, }; -use tari_ootle_wallet_crypto::ValueLookupTable; +use tari_ootle_wallet_crypto::{LookupHeader, ValueLookupTable}; use crate::cli::Cli; mod cli; @@ -32,8 +32,28 @@ async fn main() -> io::Result<()> { dest_file.display() ); let timer = Instant::now(); + let metadata = fs::metadata(&dest_file)?; let mut file = fs::File::open(&dest_file)?; let mut lookup = tari_ootle_wallet_crypto::IoReaderValueLookup::load(&mut file)?; + + let expected_size = (lookup.range().end() - lookup.range().start() + 1) * 32 + LookupHeader::SIZE as u64; + if metadata.len() != expected_size { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "File size mismatch. Expected {} bytes but found {} bytes.", + expected_size, + metadata.len() + ), + )); + } + + println!( + "✅ File size OK - header range {} to {}.", + lookup.range().start(), + lookup.range().end() + ); + for v in lookup.range() { let pk_bytes = lookup.lookup(v)?.ok_or_else(|| { io::Error::new( @@ -57,7 +77,7 @@ async fn main() -> io::Result<()> { return Ok(()); } - let file_size = (cli.max - cli.min + 1) * 32 + 20; + let file_size = (cli.max - cli.min + 1) * 32 + LookupHeader::SIZE as u64; println!( "Generating Ristretto value lookup table from {} to {} and writing to {} ({}).", cli.min, diff --git a/utilities/traffic-sim/src/sim.rs b/utilities/traffic-sim/src/sim.rs index c295b0bfed..cce4268bb0 100644 --- a/utilities/traffic-sim/src/sim.rs +++ b/utilities/traffic-sim/src/sim.rs @@ -15,7 +15,7 @@ use tari_ootle_common_types::{displayable::Displayable, optional::Optional, Netw use tari_ootle_wallet_sdk::{ apis::{confidential_transfer::UtxoInputSelection, stealth_transfer::TransferOutput}, crypto::memo::Memo, - models::{AccountWithAddress, BranchAndKeyId, KeyBranch}, + models::{AccountWithAddress, BranchAndKeyId}, }; use tari_template_lib::{ constants::XTR, @@ -349,7 +349,7 @@ impl TrafficSim { .client .submit_transaction(TransactionSubmitRequest { transaction, - seal_signer: BranchAndKeyId::new(KeyBranch::Account, exchange_account_key_id), + seal_signer: BranchAndKeyId::for_account(exchange_account_key_id), other_signers: transfer_resp.signing_keys, detect_inputs: true, detect_inputs_use_unversioned: true, @@ -416,8 +416,8 @@ impl TrafficSim { .await { Ok(_) => { - let delay = rand::thread_rng().gen_range(1..=5); - sleep(Duration::from_secs(delay)).await; + // let delay = rand::thread_rng().gen_range(1..=5); + // sleep(Duration::from_secs(delay)).await; }, Err(e) => { log::info!("Transaction failed: {:?}", e);