From d990d6cdab210bc30c218d48cb6813cf628b4e61 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Mon, 15 Sep 2025 08:56:54 +0400 Subject: [PATCH 1/2] fix(wallet)!: private derived tag --- Cargo.lock | 8 + applications/tari_indexer/Cargo.toml | 2 + applications/tari_indexer/log4rs_sample.yml | 6 +- applications/tari_indexer/src/bootstrap.rs | 5 +- .../tari_indexer/src/event_manager.rs | 2 +- .../tari_indexer/src/json_rpc/handlers.rs | 92 +- .../tari_indexer/src/json_rpc/server.rs | 1 + .../src/network_state_sync/block_scanner.rs | 5 +- .../src/network_state_sync/worker.rs | 41 +- .../2023-02-16-145719_initial/up.sql | 11 +- .../tari_indexer/src/storage_sqlite/mod.rs | 7 +- .../src/storage_sqlite/models/events.rs | 2 +- .../src/storage_sqlite/models/mod.rs | 2 + .../src/storage_sqlite/models/substate.rs | 7 +- .../src/storage_sqlite/models/utxo.rs | 144 +- .../src/storage_sqlite/models}/utxo_update.rs | 20 +- .../tari_indexer/src/storage_sqlite/reader.rs | 452 +++++ .../tari_indexer/src/storage_sqlite/schema.rs | 7 +- .../src/storage_sqlite/serialization.rs | 19 + .../src/storage_sqlite/store_factory.rs | 778 +------- .../tari_indexer/src/storage_sqlite/writer.rs | 340 ++++ .../tari_indexer/src/substate_manager.rs | 54 +- .../src/transaction_manager/mod.rs | 2 +- applications/tari_indexer/web_ui/package.json | 3 +- .../tari_validator_node/log4rs_sample.yml | 6 +- .../tari_validator_node/web_ui/package.json | 3 +- .../tari_wallet_cli/src/command/account.rs | 47 +- applications/tari_walletd/log4rs_sample.yml | 4 +- .../tari_walletd/src/handlers/accounts.rs | 150 +- .../tari_walletd/src/handlers/confidential.rs | 4 +- .../tari_walletd/src/handlers/settings.rs | 4 +- applications/tari_walletd/src/jrpc_server.rs | 1 - applications/tari_walletd/src/lib.rs | 23 +- .../src/services/account_monitor.rs | 3 +- applications/tari_walletd/src/services/mod.rs | 8 + .../src/services/recovery_service.rs | 177 +- .../src/services/stealth_utxo_scanner.rs | 49 +- .../{Components => components}/Accordion.tsx | 0 .../AlertDialog.tsx | 0 .../Breadcrumbs.tsx | 0 .../{Components => components}/CodeBlock.tsx | 0 .../ConnectorLink/CheckMark.css | 0 .../ConnectorLink/CheckMark.tsx | 0 .../ConnectorLink/ConfirmTransaction.tsx | 0 .../ConnectorLink/ConnectorLink.css | 0 .../ConnectorLink/ConnectorLink.tsx | 0 .../ConnectorLink/ConnectorLogo.tsx | 0 .../ConnectorLink/Permissions.css | 0 .../ConnectorLink/Permissions.tsx | 0 .../ConnectorLink/index.ts | 0 .../CopyAddress.tsx | 0 .../CopyToClipboard.tsx | 0 .../{Components => components}/Copyright.tsx | 0 .../src/{Components => components}/Error.tsx | 0 .../FetchStatusCheck.tsx | 0 .../HeadingMenu.tsx | 0 .../JsonTooltip.tsx | 0 .../{Components => components}/Loading.tsx | 0 .../{Components => components}/MenuItems.tsx | 0 .../{Components => components}/NFTList.tsx | 0 .../PageHeading.tsx | 0 .../SearchFilter.tsx | 0 .../SecondaryHeading.tsx | 0 .../{Components => components}/StatusChip.tsx | 0 .../{Components => components}/Stepper.tsx | 0 .../StyledComponents.ts | 0 .../ThemeSwitcher.tsx | 0 .../src/{Components => components}/Title.tsx | 0 .../WalletConnectLink/CheckMark.css | 0 .../WalletConnectLink/CheckMark.tsx | 0 .../WalletConnectLink/ConfirmTransaction.tsx | 0 .../WalletConnectLink/ConnectorLink.css | 0 .../WalletConnectLink/ConnectorLogo.tsx | 0 .../WalletConnectLink/Permissions.css | 0 .../WalletConnectLink/Permissions.tsx | 0 .../WalletConnectLink/WalletConnectLink.tsx | 0 .../WalletConnectLink/index.ts | 0 .../Tokens/components/SendMoney.tsx | 17 +- .../tari_walletd/web_ui/src/utils/json_rpc.ts | 4 - bindings/src/helpers/helpers.ts | 2 +- bindings/src/index.ts | 8 +- bindings/src/tari-indexer-client.ts | 2 + bindings/src/types/StealthUnspentOutput.ts | 4 +- .../types/{UtxoTagByte.ts => UtxoBurnt.ts} | 3 +- bindings/src/types/UtxoOutput.ts | 4 +- bindings/src/types/UtxoQueryResponse.ts | 6 - bindings/src/types/UtxoSpent.ts | 6 +- bindings/src/types/UtxoStateUpdateSet.ts | 5 + bindings/src/types/UtxoTag.ts | 8 + bindings/src/types/UtxoUnspent.ts | 15 +- bindings/src/types/UtxoUpdateSet.ts | 9 + .../{UtxoUpdate.ts => WalletUtxoUpdate.ts} | 3 +- .../GetUnspentUtxosRequest.ts | 5 + .../GetUnspentUtxosResponse.ts | 5 + .../GetUtxoUpdatesRequest.ts | 2 - .../GetUtxoUpdatesResponse.ts | 12 +- .../types/wallet-daemon-client/KeyBranch.ts | 9 +- .../RevealFundsRequest.ts | 22 - .../RevealFundsResponse.ts | 5 - bindings/src/wallet-daemon-client.ts | 2 - .../wallet_daemon_client/package.json | 3 +- .../wallet_daemon_client/src/index.ts | 6 - clients/tari_indexer_client/Cargo.toml | 3 +- .../src/json_rpc_client.rs | 9 + clients/tari_indexer_client/src/types.rs | 30 +- clients/wallet_daemon_client/src/lib.rs | 9 - clients/wallet_daemon_client/src/types.rs | 24 - config/logs.yml | 2 +- crates/common_types/src/lib.rs | 2 - crates/engine/src/runtime/impl.rs | 4 +- crates/engine_types/src/stealth/outputs.rs | 4 +- crates/engine_types/src/utxo.rs | 15 +- crates/indexer_lib/src/lib.rs | 3 - crates/indexer_lib/src/types.rs | 12 - crates/p2p/src/conversions/transaction.rs | 10 +- .../template_lib/src/models/unspent_output.rs | 4 +- crates/template_lib_types/src/crypto/mod.rs | 4 +- .../template_lib_types/src/crypto/tag_byte.rs | 25 - .../template_lib_types/src/crypto/utxo_tag.rs | 28 + .../src/support/stealth.rs | 6 +- crates/wallet/crypto/src/encrypted_data.rs | 6 +- crates/wallet/crypto/src/kdfs.rs | 64 +- crates/wallet/crypto/src/stealth.rs | 4 +- .../wallet/crypto/src/unblinded_statement.rs | 4 +- .../wallet/crypto/tests/output_statement.rs | 4 +- .../sdk/src/apis/confidential_crypto.rs | 3 +- crates/wallet/sdk/src/apis/key_manager.rs | 18 +- crates/wallet/sdk/src/apis/mod.rs | 1 - crates/wallet/sdk/src/apis/stealth_crypto.rs | 28 +- crates/wallet/sdk/src/apis/stealth_outputs.rs | 37 +- crates/wallet/sdk/src/apis/stealth_scanner.rs | 258 --- .../wallet/sdk/src/apis/stealth_transfer.rs | 65 +- crates/wallet/sdk/src/models/mod.rs | 2 + .../wallet/sdk/src/models/stealth_output.rs | 4 +- crates/wallet/sdk/src/models/utxo_update.rs | 52 + crates/wallet/sdk/src/network.rs | 25 +- crates/wallet/sdk/src/sdk.rs | 22 +- crates/wallet/sdk/src/storage.rs | 30 +- .../sdk/tests/confidential_output_api.rs | 26 +- crates/wallet/sdk_services/Cargo.toml | 5 + .../sdk_services/src/indexer_jrpc_impl.rs | 63 +- crates/wallet/sdk_services/src/lib.rs | 1 + .../sdk_services/src/utxo_scanner/error.rs | 34 + .../sdk_services/src/utxo_scanner/mod.rs | 12 + .../sdk_services/src/utxo_scanner/scanner.rs | 46 + .../src/utxo_scanner/scanner_round.rs | 270 +++ .../src/utxo_scanner/utxo_recovery.rs | 222 +++ .../2023-02-08-122514_initial/up.sql | 23 +- crates/wallet/storage_sqlite/src/helpers.rs | 9 - crates/wallet/storage_sqlite/src/lib.rs | 1 - .../wallet/storage_sqlite/src/models/mod.rs | 14 +- .../storage_sqlite/src/models/output.rs | 4 +- .../src/models/stealth_output.rs | 17 +- .../src/models/utxo_process_queue.rs | 14 + crates/wallet/storage_sqlite/src/reader.rs | 74 +- crates/wallet/storage_sqlite/src/schema.rs | 56 +- crates/wallet/storage_sqlite/src/writer.rs | 181 +- integration_tests/src/wallet_daemon_cli.rs | 24 - integration_tests/tests/cucumber.rs | 5 - .../tests/features/wallet_daemon.feature | 2 +- integration_tests/tests/log4rs/cucumber.yml | 8 +- pnpm-lock.yaml | 1722 ++++++++++++----- scripts/build_webuis.sh | 1 + 163 files changed, 3846 insertions(+), 2465 deletions(-) rename {crates/common_types/src => applications/tari_indexer/src/storage_sqlite/models}/utxo_update.rs (52%) create mode 100644 applications/tari_indexer/src/storage_sqlite/reader.rs create mode 100644 applications/tari_indexer/src/storage_sqlite/writer.rs rename applications/tari_walletd/web_ui/src/{Components => components}/Accordion.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/AlertDialog.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/Breadcrumbs.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/CodeBlock.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/ConnectorLink/CheckMark.css (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/ConnectorLink/CheckMark.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/ConnectorLink/ConfirmTransaction.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/ConnectorLink/ConnectorLink.css (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/ConnectorLink/ConnectorLink.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/ConnectorLink/ConnectorLogo.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/ConnectorLink/Permissions.css (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/ConnectorLink/Permissions.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/ConnectorLink/index.ts (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/CopyAddress.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/CopyToClipboard.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/Copyright.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/Error.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/FetchStatusCheck.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/HeadingMenu.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/JsonTooltip.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/Loading.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/MenuItems.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/NFTList.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/PageHeading.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/SearchFilter.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/SecondaryHeading.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/StatusChip.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/Stepper.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/StyledComponents.ts (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/ThemeSwitcher.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/Title.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/WalletConnectLink/CheckMark.css (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/WalletConnectLink/CheckMark.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/WalletConnectLink/ConfirmTransaction.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/WalletConnectLink/ConnectorLink.css (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/WalletConnectLink/ConnectorLogo.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/WalletConnectLink/Permissions.css (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/WalletConnectLink/Permissions.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/WalletConnectLink/WalletConnectLink.tsx (100%) rename applications/tari_walletd/web_ui/src/{Components => components}/WalletConnectLink/index.ts (100%) rename bindings/src/types/{UtxoTagByte.ts => UtxoBurnt.ts} (53%) delete mode 100644 bindings/src/types/UtxoQueryResponse.ts create mode 100644 bindings/src/types/UtxoStateUpdateSet.ts create mode 100644 bindings/src/types/UtxoTag.ts create mode 100644 bindings/src/types/UtxoUpdateSet.ts rename bindings/src/types/{UtxoUpdate.ts => WalletUtxoUpdate.ts} (58%) create mode 100644 bindings/src/types/tari-indexer-client/GetUnspentUtxosRequest.ts create mode 100644 bindings/src/types/tari-indexer-client/GetUnspentUtxosResponse.ts delete mode 100644 bindings/src/types/wallet-daemon-client/RevealFundsRequest.ts delete mode 100644 bindings/src/types/wallet-daemon-client/RevealFundsResponse.ts delete mode 100644 crates/indexer_lib/src/types.rs delete mode 100644 crates/template_lib_types/src/crypto/tag_byte.rs create mode 100644 crates/template_lib_types/src/crypto/utxo_tag.rs delete mode 100644 crates/wallet/sdk/src/apis/stealth_scanner.rs create mode 100644 crates/wallet/sdk/src/models/utxo_update.rs create mode 100644 crates/wallet/sdk_services/src/utxo_scanner/error.rs create mode 100644 crates/wallet/sdk_services/src/utxo_scanner/mod.rs create mode 100644 crates/wallet/sdk_services/src/utxo_scanner/scanner.rs create mode 100644 crates/wallet/sdk_services/src/utxo_scanner/scanner_round.rs create mode 100644 crates/wallet/sdk_services/src/utxo_scanner/utxo_recovery.rs delete mode 100644 crates/wallet/storage_sqlite/src/helpers.rs create mode 100644 crates/wallet/storage_sqlite/src/models/utxo_process_queue.rs diff --git a/Cargo.lock b/Cargo.lock index 58b45a7520..a22475d4b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11038,6 +11038,7 @@ dependencies = [ "async-graphql-axum", "axum 0.6.20", "axum-jrpc", + "bincode 2.0.1", "cacache", "clap 3.2.25", "config", @@ -11073,6 +11074,7 @@ dependencies = [ "tari_ootle_p2p", "tari_ootle_storage", "tari_ootle_storage_sqlite", + "tari_ootle_wallet_sdk", "tari_rpc_framework", "tari_shutdown", "tari_template_lib", @@ -11100,6 +11102,7 @@ dependencies = [ "tari_engine_types", "tari_ootle_common_types", "tari_ootle_storage", + "tari_ootle_wallet_sdk", "tari_template_abi", "tari_template_lib_types", "tari_transaction", @@ -11432,7 +11435,10 @@ dependencies = [ name = "tari_ootle_wallet_sdk_services" version = "0.11.2" dependencies = [ + "anyhow", + "log", "reqwest 0.11.27", + "tari_crypto", "tari_engine_types", "tari_indexer_client", "tari_ootle_common_types", @@ -11440,7 +11446,9 @@ dependencies = [ "tari_template_abi", "tari_template_lib", "tari_transaction", + "tari_transaction_components", "thiserror 1.0.69", + "tokio", "url", ] diff --git a/applications/tari_indexer/Cargo.toml b/applications/tari_indexer/Cargo.toml index c1fd44d376..a4509eba60 100644 --- a/applications/tari_indexer/Cargo.toml +++ b/applications/tari_indexer/Cargo.toml @@ -34,12 +34,14 @@ tari_template_lib = { workspace = true } tari_template_manager = { workspace = true } tari_transaction = { workspace = true } tari_validator_node_rpc = { workspace = true } +tari_ootle_wallet_sdk = { workspace = true } anyhow = { workspace = true } axum = { workspace = true } async-graphql = { workspace = true } async-graphql-axum = { workspace = true } axum-jrpc = { workspace = true, features = ["anyhow_error"] } +bincode = { workspace = true, features = ["serde"] } clap = { workspace = true, features = ["derive", "env"] } config = { workspace = true } diesel = { workspace = true, default-features = false, features = [ diff --git a/applications/tari_indexer/log4rs_sample.yml b/applications/tari_indexer/log4rs_sample.yml index d96b296f0a..da5978ec22 100644 --- a/applications/tari_indexer/log4rs_sample.yml +++ b/applications/tari_indexer/log4rs_sample.yml @@ -36,7 +36,7 @@ appenders: count: 5 pattern: "{{log_dir}}/log/indexer/network.{}.log" encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [Thread:{I}] {l:5} {m}// {f}:{L}{n}" + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m}// {f}:{L}{n}" # An appender named "ootle" that writes to a file with a custom pattern encoder ootle: @@ -53,7 +53,7 @@ appenders: count: 5 pattern: "{{log_dir}}/log/indexer/ootle.{}.log" encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}" + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}" # An appender named "ootle" that writes to a file with a custom pattern encoder json_rpc: @@ -70,7 +70,7 @@ appenders: count: 5 pattern: "{{log_dir}}/log/indexer/json_rpc.{}.log" encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}" + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}" # An appender named "libp2p" that writes to a file with a custom pattern encoder libp2p: diff --git a/applications/tari_indexer/src/bootstrap.rs b/applications/tari_indexer/src/bootstrap.rs index 2f3cbb3d0a..5784c59c82 100644 --- a/applications/tari_indexer/src/bootstrap.rs +++ b/applications/tari_indexer/src/bootstrap.rs @@ -66,7 +66,10 @@ use crate::{ network_state_sync::NetworkWideStateSyncConfig, storage_sqlite::{ models::Key, - store_factory::{IndexerStore, IndexerStoreReadTransaction, IndexerStoreWriteTransaction, SqliteIndexerStore}, + IndexerStore, + IndexerStoreReadTransaction, + IndexerStoreWriteTransaction, + SqliteIndexerStore, }, ApplicationConfig, IndexerEpochManagerSpec, diff --git a/applications/tari_indexer/src/event_manager.rs b/applications/tari_indexer/src/event_manager.rs index f107124aae..1efbf0f323 100644 --- a/applications/tari_indexer/src/event_manager.rs +++ b/applications/tari_indexer/src/event_manager.rs @@ -26,7 +26,7 @@ use log::*; use tari_engine_types::{events::Event, substate::SubstateId}; use tari_template_lib::{models::Metadata, types::Hash}; -use crate::storage_sqlite::store_factory::{IndexerStore, IndexerStoreReadTransaction, SqliteIndexerStore}; +use crate::storage_sqlite::{IndexerStore, IndexerStoreReadTransaction, SqliteIndexerStore}; const LOG_TARGET: &str = "tari::indexer::event_manager"; diff --git a/applications/tari_indexer/src/json_rpc/handlers.rs b/applications/tari_indexer/src/json_rpc/handlers.rs index bbff335c98..1f95e9a67e 100644 --- a/applications/tari_indexer/src/json_rpc/handlers.rs +++ b/applications/tari_indexer/src/json_rpc/handlers.rs @@ -54,6 +54,8 @@ use tari_indexer_client::types::{ GetTemplateDefinitionResponse, GetTransactionResultRequest, GetTransactionResultResponse, + GetUnspentUtxosRequest, + GetUnspentUtxosResponse, GetUtxoUpdatesRequest, GetUtxoUpdatesResponse, IndexerReadyResponse, @@ -66,7 +68,6 @@ use tari_indexer_client::types::{ ListSubstatesResponse, ListTemplatesRequest, ListTemplatesResponse, - NonFungibleSubstate, SubmitTransactionRequest, SubmitTransactionResponse, TemplateMetadata, @@ -86,6 +87,7 @@ use tari_ootle_storage::{ time::{PrimitiveDateTime, UtcDateTime}, }; use tari_ootle_storage_sqlite::global::SqliteGlobalDbAdapter; +use tari_ootle_wallet_sdk::models::{UtxoStateUpdateSet, UtxoUpdateSet}; use tari_template_manager::{ implementation::TemplateManager, interface::{TemplateExecutable, TemplateManagerError}, @@ -97,7 +99,7 @@ use crate::{ dry_run::processor::DryRunTransactionProcessor, json_rpc::error::internal_error, network_client::NetworkClientError, - storage_sqlite::store_factory::SqliteIndexerStore, + storage_sqlite::SqliteIndexerStore, substate_manager::SubstateManager, transaction_manager::{error::TransactionManagerError, TransactionManager}, }; @@ -433,19 +435,12 @@ impl JsonRpcHandlers { ) })?; - let res = self + let non_fungibles = self .substate_manager .get_non_fungibles_by_resource_address(request.address, limit, offset) .map_err(|e| Self::internal_error(answer_id, format!("Error getting non fungibles: {}", e)))?; Ok(JsonRpcResponse::success(answer_id, GetNonFungiblesResponse { - non_fungibles: res - .into_iter() - .map(|v| NonFungibleSubstate { - address: v.address, - version: v.version, - substate: v.substate, - }) - .collect(), + non_fungibles, })) } @@ -496,18 +491,12 @@ impl JsonRpcHandlers { )); } - let mut utxo_updates = Vec::new(); + let mut utxo_updates = HashMap::new(); let mut per_shard_high_watermark = Vec::with_capacity(req.shard_state_versions.len()); for (shard, state_version) in req.shard_state_versions { - let (max_version, updates) = self + let (max_state_version, updates) = self .substate_manager - .get_utxo_updates( - req.resource_address, - shard, - state_version, - &req.filter_tag_bytes, - req.per_shard_limit, - ) + .get_utxo_updates(req.resource_address, shard, state_version, req.per_shard_limit) .map_err(|e| { Self::internal_error( answer_id, @@ -517,19 +506,72 @@ impl JsonRpcHandlers { ), ) })?; - if max_version.as_u64() > 0 { + + // TODO: this is on the hot path, figure out a better way to let the client know the max shard versions + // without sending it each time + let current_tip_version = self + .substate_manager + .get_max_state_version(&req.resource_address, shard) + .map_err(|e| { + Self::internal_error( + answer_id, + format!( + "Error getting max state version for resource_address {}, shard {}: {}", + req.resource_address, shard, e + ), + ) + })?; + if current_tip_version.as_u64() > 0 { // Save a little over the wire initially by not sending 0 watermarks - per_shard_high_watermark.push((shard, max_version)); + per_shard_high_watermark.push((shard, current_tip_version)); + } + if !updates.is_empty() { + utxo_updates.insert(shard, UtxoStateUpdateSet { + updates, + max_state_version, + }); } - utxo_updates.extend(updates); } Ok(JsonRpcResponse::success(answer_id, GetUtxoUpdatesResponse { - utxo_updates, - per_shard_high_watermark, + updates: UtxoUpdateSet { + shard_updates: utxo_updates, + per_shard_high_watermark, + }, })) } + pub async fn get_unspent_utxos(&self, value: JsonRpcExtractor) -> JrpcResult { + let answer_id = value.get_answer_id(); + let req: GetUnspentUtxosRequest = value.parse_params()?; + if req.tag_and_nonce_pairs.len() > 1000 { + return Err(JsonRpcResponse::error( + answer_id, + JsonRpcError::new( + JsonRpcErrorReason::InvalidParams, + "cannot query more than 1000 UTXOs".to_string(), + Value::Null, + ), + )); + } + let utxos = self + .substate_manager + .get_unspent_utxos(&req.resource_address, &req.tag_and_nonce_pairs) + .map_err(|e| { + Self::internal_error( + answer_id, + format!( + "Error getting UTXOs for resource_address {}, with {} tag/nonce pair(s): {}", + req.resource_address, + req.tag_and_nonce_pairs.len(), + e + ), + ) + })?; + + Ok(JsonRpcResponse::success(answer_id, GetUnspentUtxosResponse { utxos })) + } + pub async fn submit_transaction(&self, value: JsonRpcExtractor) -> JrpcResult { let answer_id = value.get_answer_id(); let request: SubmitTransactionRequest = value.parse_params()?; diff --git a/applications/tari_indexer/src/json_rpc/server.rs b/applications/tari_indexer/src/json_rpc/server.rs index 0fdbfa36fb..df3a77762f 100644 --- a/applications/tari_indexer/src/json_rpc/server.rs +++ b/applications/tari_indexer/src/json_rpc/server.rs @@ -78,6 +78,7 @@ async fn handler(Extension(handlers): Extension>, value: Js "inspect_substate" => handlers.inspect_substate(value).await, "get_non_fungibles" => handlers.get_non_fungibles(value).await, "get_utxo_updates" => handlers.get_utxo_updates(value).await, + "get_unspent_utxos" => handlers.get_unspent_utxos(value).await, // Transactions "submit_transaction" => handlers.submit_transaction(value).await, diff --git a/applications/tari_indexer/src/network_state_sync/block_scanner.rs b/applications/tari_indexer/src/network_state_sync/block_scanner.rs index 53228d5d87..c74fddf0d5 100644 --- a/applications/tari_indexer/src/network_state_sync/block_scanner.rs +++ b/applications/tari_indexer/src/network_state_sync/block_scanner.rs @@ -19,7 +19,10 @@ use crate::{ block_data::BlockData, storage_sqlite::{ models::{NewScannedBlockId, NewSubstate}, - store_factory::{IndexerStore, IndexerStoreReadTransaction, IndexerStoreWriteTransaction, SqliteIndexerStore}, + IndexerStore, + IndexerStoreReadTransaction, + IndexerStoreWriteTransaction, + SqliteIndexerStore, }, }; diff --git a/applications/tari_indexer/src/network_state_sync/worker.rs b/applications/tari_indexer/src/network_state_sync/worker.rs index f810219c62..dc39a9810f 100644 --- a/applications/tari_indexer/src/network_state_sync/worker.rs +++ b/applications/tari_indexer/src/network_state_sync/worker.rs @@ -18,9 +18,6 @@ use tari_ootle_common_types::{ PeerAddress, ShardGroup, StateVersion, - UtxoSpent, - UtxoUnspent, - UtxoUpdate, VotePower, }; use tari_ootle_p2p::{proto::rpc, TariMessagingSpec}; @@ -43,14 +40,12 @@ use crate::{ sync_progress::SyncProgress, }, storage_sqlite::{ - models::Key, - store_factory::{ - IndexerStore, - IndexerStoreReadTransaction, - IndexerStoreWriteTransaction, - SqliteIndexerStore, - SqliteStoreWriteTransaction, - }, + models::{Key, UtxoSpent, UtxoUnspent, UtxoUpdateRecord}, + IndexerStore, + IndexerStoreReadTransaction, + IndexerStoreWriteTransaction, + SqliteIndexerStore, + SqliteStoreWriteTransaction, }, }; @@ -327,7 +322,7 @@ impl NetworkWideStateSync { shard: Shard, sync_plan_mut: &mut SyncPlan, update_buf: &mut Vec<(Epoch, SubstateUpdateProof)>, - utxos_buf: &mut Vec, + utxos_buf: &mut Vec, transactions_buf: &mut Vec, shard_group: ShardGroup, session: &mut ValidatorRpcSession, @@ -452,20 +447,24 @@ fn extend_bufs_from_substate_update( msg_epoch: Epoch, update_buf: &mut Vec<(Epoch, SubstateUpdateProof)>, templates_buf: &mut Vec, - utxos_buf: &mut Vec, + utxos_buf: &mut Vec, transactions_buf: &mut Vec, ) -> Result<(), NetworkStateSyncError> { match &update { SubstateUpdateProof::Create(create) => match create.substate.value().value() { Some(SubstateValue::Utxo(utxo)) => { if let Some(address) = create.substate.substate_id().as_utxo_address() { - utxos_buf.push(UtxoUpdate::Unspent(UtxoUnspent { - address, - version: update.version(), - shard, - state_version, - utxo: utxo.clone(), - })); + let is_frozen = utxo.is_frozen(); + if let Some(ref output) = utxo.output { + utxos_buf.push(UtxoUpdateRecord::Unspent(UtxoUnspent { + address, + version: update.version(), + shard, + state_version, + utxo_output: output.clone(), + is_frozen, + })); + } } else { warn!(target: LOG_TARGET, "⚠️ NEVER HAPPEN: Received UTXO substate with invalid address: {}", create.substate.substate_id()); }; @@ -505,7 +504,7 @@ fn extend_bufs_from_substate_update( }); }, SubstateId::Utxo(address) => { - utxos_buf.push(UtxoUpdate::Spent(UtxoSpent { + utxos_buf.push(UtxoUpdateRecord::Spent(UtxoSpent { address: address.clone(), shard, version: update.version(), diff --git a/applications/tari_indexer/src/storage_sqlite/migrations/2023-02-16-145719_initial/up.sql b/applications/tari_indexer/src/storage_sqlite/migrations/2023-02-16-145719_initial/up.sql index 15efbb2157..f9a3626667 100644 --- a/applications/tari_indexer/src/storage_sqlite/migrations/2023-02-16-145719_initial/up.sql +++ b/applications/tari_indexer/src/storage_sqlite/migrations/2023-02-16-145719_initial/up.sql @@ -119,19 +119,20 @@ CREATE UNIQUE INDEX epoch_checkpoints_uniq_epoch_shard_group ON epoch_checkpoint create table utxos ( id integer not NULL primary key AUTOINCREMENT, - address text not NULL, + commitment text not NULL, + public_nonce text not NULL, version int not NULL, resource_address text not NULL, shard int not NULL, state_version bigint not NULL, - output text NULL, - utxo_tag_byte int NULL, + output blob NULL, + utxo_tag int not NULL, is_spent boolean not NULL, is_burnt boolean not NULL, is_frozen boolean not NULL, created_at timestamp not null default current_timestamp ); -CREATE INDEX utxos_shard_tag_resource_state_version_idx - ON utxos (shard, utxo_tag_byte, resource_address, state_version); +CREATE INDEX utxos_resource_state_version_shard_idx ON utxos (resource_address, state_version, shard); +CREATE UNIQUE INDEX utxos_resource_public_nonce_utxo_tag_uniq_partial ON utxos (resource_address, public_nonce, utxo_tag) WHERE is_spent = false; diff --git a/applications/tari_indexer/src/storage_sqlite/mod.rs b/applications/tari_indexer/src/storage_sqlite/mod.rs index be05734d31..6d93ddc3ba 100644 --- a/applications/tari_indexer/src/storage_sqlite/mod.rs +++ b/applications/tari_indexer/src/storage_sqlite/mod.rs @@ -21,6 +21,11 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pub mod models; +mod reader; mod schema; mod serialization; -pub mod store_factory; +mod store_factory; +mod writer; + +pub use store_factory::*; +pub use writer::*; diff --git a/applications/tari_indexer/src/storage_sqlite/models/events.rs b/applications/tari_indexer/src/storage_sqlite/models/events.rs index 383760202b..a0c2474f49 100644 --- a/applications/tari_indexer/src/storage_sqlite/models/events.rs +++ b/applications/tari_indexer/src/storage_sqlite/models/events.rs @@ -33,7 +33,7 @@ use crate::storage_sqlite::schema::*; #[derive(Debug, Identifiable, Queryable)] #[diesel(table_name = events)] -pub struct EventDb { +pub struct EventRecord { pub id: i32, pub template_address: String, pub tx_hash: String, diff --git a/applications/tari_indexer/src/storage_sqlite/models/mod.rs b/applications/tari_indexer/src/storage_sqlite/models/mod.rs index 31bcf6b5bd..3d8c73dd87 100644 --- a/applications/tari_indexer/src/storage_sqlite/models/mod.rs +++ b/applications/tari_indexer/src/storage_sqlite/models/mod.rs @@ -24,8 +24,10 @@ mod events; mod key_value; mod substate; mod utxo; +mod utxo_update; pub use events::*; pub use key_value::*; pub use substate::*; pub(crate) use utxo::*; +pub(crate) use utxo_update::*; diff --git a/applications/tari_indexer/src/storage_sqlite/models/substate.rs b/applications/tari_indexer/src/storage_sqlite/models/substate.rs index 7dde7b4b74..db895dc6a9 100644 --- a/applications/tari_indexer/src/storage_sqlite/models/substate.rs +++ b/applications/tari_indexer/src/storage_sqlite/models/substate.rs @@ -26,12 +26,13 @@ use std::convert::TryFrom; use tari_ootle_storage::time::PrimitiveDateTime; use crate::{ - storage_sqlite::{models::substate::Substate as SubstateRow, schema::substates}, + storage_sqlite::{models::substate::SubstateRecord as SubstateRow, schema::substates}, substate_manager::SubstateResponse, }; #[derive(Debug, Identifiable, Queryable)] -pub struct Substate { +#[diesel(table_name = substates)] +pub struct SubstateRecord { pub id: i32, pub address: String, pub version: i32, @@ -43,7 +44,7 @@ pub struct Substate { pub created_at: PrimitiveDateTime, } -impl TryFrom for SubstateResponse { +impl TryFrom for SubstateResponse { type Error = anyhow::Error; fn try_from(row: SubstateRow) -> Result { diff --git a/applications/tari_indexer/src/storage_sqlite/models/utxo.rs b/applications/tari_indexer/src/storage_sqlite/models/utxo.rs index f4b41b49b7..5e5cb1c3a8 100644 --- a/applications/tari_indexer/src/storage_sqlite/models/utxo.rs +++ b/applications/tari_indexer/src/storage_sqlite/models/utxo.rs @@ -3,18 +3,19 @@ use std::str::FromStr; -use diesel::internal::derives::multiconnection::time::PrimitiveDateTime; -use tari_engine_types::{Utxo, UtxoAddress}; -use tari_ootle_common_types::{shard::Shard, StateVersion, UtxoSpent, UtxoUnspent, UtxoUpdate}; -use tari_ootle_storage::StorageError; +use tari_engine_types::{Utxo, UtxoAddress, UtxoId, UtxoOutput}; +use tari_ootle_common_types::StateVersion; +use tari_ootle_storage::{time::PrimitiveDateTime, StorageError}; +use tari_ootle_wallet_sdk::models::{UtxoBurnt, UtxoSpent, UtxoUnspent, WalletUtxoUpdate}; +use tari_template_lib::prelude::{PedersenCommitmentBytes, ResourceAddress}; -use crate::storage_sqlite::{schema::utxos, serialization::deserialize_json}; +use crate::storage_sqlite::{schema::utxos, serialization::deserialize_bincode}; #[derive(AsChangeset, Default)] #[diesel(table_name = utxos)] pub(crate) struct UtxoRecordUpdate { pub version: Option, - pub output: Option>, + pub output: Option>>, pub state_version: Option, pub is_spent: Option, pub is_burnt: Option, @@ -24,13 +25,14 @@ pub(crate) struct UtxoRecordUpdate { #[derive(Insertable)] #[diesel(table_name = utxos)] pub(crate) struct UtxoRecordInsert { - pub address: String, + pub commitment: String, + pub public_nonce: String, pub version: i32, pub shard: i32, pub resource_address: String, pub state_version: i64, - pub output: Option, - pub utxo_tag_byte: Option, + pub output: Option>, + pub utxo_tag: i32, pub is_spent: bool, pub is_burnt: bool, pub is_frozen: bool, @@ -38,59 +40,97 @@ pub(crate) struct UtxoRecordInsert { #[derive(Queryable)] #[diesel(table_name = utxos)] pub(crate) struct UtxoRecord { - #[allow(dead_code)] - pub id: i32, - pub address: String, + pub _id: i32, + pub commitment: String, + pub _public_nonce: String, pub version: i32, - #[allow(dead_code)] pub resource_address: String, - pub shard: i32, + pub _shard: i32, pub state_version: i64, - pub output: Option, - #[allow(dead_code)] - pub utxo_tag_byte: Option, - pub is_spent: bool, - #[allow(dead_code)] + pub output: Option>, + pub _utxo_tag: i32, + pub _is_spent: bool, pub is_burnt: bool, pub is_frozen: bool, - #[allow(dead_code)] - pub created_at: PrimitiveDateTime, + pub _created_at: PrimitiveDateTime, } impl UtxoRecord { - pub fn try_convert(self) -> Result { - let address = UtxoAddress::from_str(&self.address).map_err(|e| StorageError::DecodingError { - operation: "UtxoRecord::try_convert", - item: "Utxo", - details: format!("Failed to parse SubstateId from string: {}", e), - })?; + pub fn try_convert_to_update(self) -> Result<(StateVersion, WalletUtxoUpdate), StorageError> { + let id = self.to_utxo_id()?; + match self.output { + None => { + // Spent or burnt + if self.is_burnt { + Ok(( + StateVersion::new(self.state_version as u64), + WalletUtxoUpdate::Burnt(UtxoBurnt { + id, + version: self.version as u32, + }), + )) + } else { + Ok(( + StateVersion::new(self.state_version as u64), + WalletUtxoUpdate::Spent(UtxoSpent { + id, + version: self.version as u32, + }), + )) + } + }, + Some(ref output) => { + let output = deserialize_bincode::(output).map_err(|e| StorageError::DecodingError { + operation: "UtxoRecord::try_convert", + item: "Utxo", + details: format!("Failed to parse Utxo from string: {}", e), + })?; + + Ok(( + StateVersion::new(self.state_version as u64), + WalletUtxoUpdate::Unspent(UtxoUnspent { + public_nonce: output.output.public_nonce, + tag: output.tag, + }), + )) + }, + } + } - let update = if self.is_spent { - UtxoUpdate::Spent(UtxoSpent { - address, - version: self.version as u32, - shard: Shard::from(self.shard as u32), - state_version: StateVersion::new(self.state_version as u64), - }) - } else { - UtxoUpdate::Unspent(UtxoUnspent { - address, - version: self.version as u32, - shard: Shard::from(self.shard as u32), - state_version: StateVersion::new(self.state_version as u64), - utxo: Utxo { - output: self.output.as_ref().map(deserialize_json).transpose().map_err(|e| { - StorageError::DecodingError { - operation: "UtxoRecord::try_convert", - item: "Utxo", - details: format!("Failed to parse Utxo from string: {}", e), - } - })?, - is_frozen: self.is_frozen, - }, - }) + pub fn try_convert_to_utxo(self) -> Result<(UtxoAddress, Utxo), StorageError> { + let address = self.to_address()?; + let utxo = Utxo { + output: self.output.as_ref().map(deserialize_bincode).transpose().map_err(|e| { + StorageError::DecodingError { + operation: "UtxoRecord::try_convert", + item: "Utxo", + details: format!("Failed to parse Utxo from string: {}", e), + } + })?, + is_frozen: self.is_frozen, }; - Ok(update) + Ok((address, utxo)) + } + + fn to_utxo_id(&self) -> Result { + let commitment = + PedersenCommitmentBytes::from_hex(&self.commitment).map_err(|e| StorageError::DecodingError { + operation: "UtxoRecord::to_address", + item: "UtxoAddress", + details: format!("Failed to parse Commitment from string: {}", e), + })?; + Ok(commitment.into()) + } + + fn to_address(&self) -> Result { + let resource_address = + ResourceAddress::from_str(&self.resource_address).map_err(|e| StorageError::DecodingError { + operation: "UtxoRecord::to_address", + item: "UtxoAddress", + details: format!("Failed to parse ResourceAddress from string: {}", e), + })?; + let id = self.to_utxo_id()?; + Ok(UtxoAddress::new(resource_address, id)) } } diff --git a/crates/common_types/src/utxo_update.rs b/applications/tari_indexer/src/storage_sqlite/models/utxo_update.rs similarity index 52% rename from crates/common_types/src/utxo_update.rs rename to applications/tari_indexer/src/storage_sqlite/models/utxo_update.rs index 2a7bdebe88..022481b93e 100644 --- a/crates/common_types/src/utxo_update.rs +++ b/applications/tari_indexer/src/storage_sqlite/models/utxo_update.rs @@ -2,39 +2,29 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_bor::{Deserialize, Serialize}; -use tari_engine_types::{Utxo, UtxoAddress}; - -use crate::{shard::Shard, StateVersion}; +use tari_engine_types::{UtxoAddress, UtxoOutput}; +use tari_ootle_common_types::{shard::Shard, StateVersion}; #[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub enum UtxoUpdate { +pub enum UtxoUpdateRecord { Unspent(UtxoUnspent), Spent(UtxoSpent), } #[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub struct UtxoUnspent { pub address: UtxoAddress, pub version: u32, pub shard: Shard, pub state_version: StateVersion, - pub utxo: Utxo, + pub utxo_output: UtxoOutput, + pub is_frozen: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub struct UtxoSpent { pub address: UtxoAddress, pub version: u32, pub shard: Shard, pub state_version: StateVersion, } - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct UtxoQueryResponse { - pub updates: Vec, - pub per_shard_high_watermark: Vec<(Shard, StateVersion)>, -} diff --git a/applications/tari_indexer/src/storage_sqlite/reader.rs b/applications/tari_indexer/src/storage_sqlite/reader.rs new file mode 100644 index 0000000000..3ae372ab55 --- /dev/null +++ b/applications/tari_indexer/src/storage_sqlite/reader.rs @@ -0,0 +1,452 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::{fmt::Write, str::FromStr}; + +use diesel::{ + dsl, + sql_types, + BoolExpressionMethods, + ExpressionMethods, + OptionalExtension, + QueryDsl, + RunQueryDsl, + SqliteConnection, + TextExpressionMethods, +}; +use log::info; +use serde::de::DeserializeOwned; +use tari_consensus_types::BlockId; +use tari_engine_types::{ + substate::{SubstateId, SubstateValue}, + Utxo, + UtxoId, +}; +use tari_indexer_client::types::{ListSubstateItem, NonFungibleSubstate, TransactionEntry}; +use tari_ootle_common_types::{shard::Shard, substate_type::SubstateType, Epoch, ShardGroup, StateVersion}; +use tari_ootle_storage::{time::PrimitiveDateTime, StorageError}; +use tari_ootle_storage_sqlite::SqliteTransaction; +use tari_ootle_wallet_sdk::models::WalletUtxoUpdate; +use tari_template_lib::{ + models::ResourceAddress, + prelude::{RistrettoPublicKeyBytes, TemplateAddress}, + types::crypto::UtxoTag, +}; +use tari_transaction::{Transaction, TransactionId}; + +use crate::storage_sqlite::{ + models, + models::{EventRecord, KeyValue, ScannedBlockId, SubstateRecord}, + serialization::{deserialize_hex_try_from, deserialize_json, serialize_hex}, + IndexerStoreReadTransaction, +}; + +const LOG_TARGET: &str = "tari::indexer::storage_sqlite::reader"; + +pub struct SqliteStoreReadTransaction<'a> { + pub(super) transaction: SqliteTransaction<'a>, +} + +impl<'a> SqliteStoreReadTransaction<'a> { + pub(super) fn new(transaction: SqliteTransaction<'a>) -> Self { + Self { transaction } + } + + pub(super) fn connection(&mut self) -> &mut SqliteConnection { + self.transaction.connection() + } +} + +impl IndexerStoreReadTransaction for SqliteStoreReadTransaction<'_> { + fn list_substates( + &mut self, + by_type: Option, + by_template_address: Option, + limit: Option, + offset: Option, + ) -> Result, StorageError> { + use crate::storage_sqlite::schema::substates; + + let mut query = substates::table.into_boxed(); + + if let Some(template_address) = by_template_address { + query = query.filter(substates::template_address.eq(template_address.to_string())); + } + + if let Some(substate_type) = by_type { + let address_like = format!("{}_%", substate_type.as_prefix_str()); + query = query.filter(substates::address.like(address_like)); + } + + if let Some(limit) = limit { + query = query.limit(limit as i64); + } + if let Some(offset) = offset { + query = query.offset(offset as i64); + } + + let substates: Vec = query + .order_by(substates::id.desc()) + .get_results(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("list_substates: {}", e), + })?; + + let items = substates + .into_iter() + .map(|s| { + let substate_id = SubstateId::from_str(&s.address)?; + let version = s.version as u32; + let template_address = s.template_address.map(|h| deserialize_hex_try_from(&h)).transpose()?; + let timestamp = s.timestamp; + Ok(ListSubstateItem { + substate_id, + module_name: s.module_name, + version, + template_address, + timestamp, + }) + }) + .collect::, anyhow::Error>>() + .map_err(|e| StorageError::QueryError { + reason: format!("list_substates: invalid substate items: {}", e), + })?; + + Ok(items) + } + + fn get_substate( + &mut self, + address: &SubstateId, + version: Option, + ) -> Result, StorageError> { + use crate::storage_sqlite::schema::substates; + + let mut substate_query = substates::table + .into_boxed() + .filter(substates::address.eq(address.to_string())); + if let Some(version) = version { + substate_query = substate_query.filter(substates::version.eq(version as i32)); + } else { + substate_query = substate_query.order_by(substates::version.desc()) + } + + substate_query + .limit(1) + .first(self.connection()) + .optional() + .map_err(|e| StorageError::QueryError { + reason: format!("get_substate: {}", e), + }) + } + + fn get_non_fungible_count(&mut self, resource_address: String) -> Result { + use crate::storage_sqlite::schema::non_fungible_indexes; + + let count = non_fungible_indexes::table + .filter(non_fungible_indexes::resource_address.eq(resource_address)) + .count() + .get_result::(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("get_non_fungible_count: {}", e), + })?; + + Ok(count) + } + + fn get_non_fungibles_by_resource_address( + &mut self, + resource_address: ResourceAddress, + limit: usize, + offset: usize, + ) -> Result, StorageError> { + use crate::storage_sqlite::schema::substates; + + let res = substates::table + .filter(substates::address.like(format!("nft_{}_%", resource_address.as_object_key()))) + .limit(limit as i64) + .offset(offset as i64) + .get_results::(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("get_non_fungibles_by_resource_address: {}", e), + })?; + + res.into_iter() + .map(|row| { + let value: SubstateValue = + serde_json::from_str(&row.data).map_err(|e| StorageError::DataInconsistency { + details: format!("Failed to parse substate data: {}", e), + })?; + + Ok(NonFungibleSubstate { + address: row.address.parse().map_err(|e| StorageError::DataInconsistency { + details: format!("Failed to parse address: {}", e), + })?, + version: row.version.try_into().map_err(|e| StorageError::DataInconsistency { + details: format!("Version overflow {}", e), + })?, + substate: value, + }) + }) + .collect() + } + + fn get_events( + &mut self, + substate_id_filter: Option, + topic_filter: Option, + offset: u32, + limit: u32, + ) -> Result, StorageError> { + // TODO: allow to query by payload as well, unifying all event methods into one + info!( + target: LOG_TARGET, + "Querying substate scanner database: get_events with substate_id_filter = {:?} and \ + topic_filter = {:?}", + substate_id_filter, + topic_filter + ); + use crate::storage_sqlite::schema::events; + + let mut query = events::table.into_boxed(); + + if let Some(substate_id) = substate_id_filter { + query = query.filter(events::substate_id.eq(substate_id.to_string())); + } + + if let Some(topic) = topic_filter { + query = query.filter(events::topic.eq(topic)); + } + + query = query.offset(offset.into()); + if limit > 0 { + query = query.limit(limit.into()); + } + + let events = query + .order(events::id.desc()) + .get_results::(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("get_events: {}", e), + })?; + + Ok(events) + } + + fn get_oldest_scanned_epoch(&mut self) -> Result, StorageError> { + use crate::storage_sqlite::schema::scanned_block_ids; + + let res: Option = scanned_block_ids::table + .select(diesel::dsl::min(scanned_block_ids::epoch)) + .first(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("get_oldest_scanned_epoch: {}", e), + })?; + + let oldest_epoch = res + .map(|r| { + let epoch_as_u64 = r as u64; + Ok::(Epoch(epoch_as_u64)) + }) + .transpose()?; + + Ok(oldest_epoch) + } + + fn get_last_scanned_block_id( + &mut self, + epoch: Epoch, + shard_group: ShardGroup, + ) -> Result, StorageError> { + use crate::storage_sqlite::schema::scanned_block_ids; + + let row: Option = scanned_block_ids::table + .filter( + scanned_block_ids::epoch + .eq(epoch.0 as i64) + .and(scanned_block_ids::shard_group.eq(shard_group.encode_as_u32() as i32)), + ) + .first(self.connection()) + .optional() + .map_err(|e| StorageError::QueryError { + reason: format!("get_last_scanned_block_id: {}", e), + })?; + + let block_id_option = row.map(|r| BlockId::try_from(r.last_block_id)).transpose()?; + + Ok(block_id_option) + } + + fn list_recent_transactions( + &mut self, + last_transaction_id: Option, + limit: usize, + ) -> Result, StorageError> { + use crate::storage_sqlite::schema::transactions; + + let start_id = if let Some(last_id) = last_transaction_id { + transactions::table + .select(transactions::id) + .filter(transactions::transaction_id.eq(serialize_hex(last_id))) + .first::(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("list_recent_transactions: {e}"), + })? + } else { + i32::MAX + }; + + let rows = transactions::table + .select((transactions::body, transactions::created_at)) + .filter(transactions::id.lt(start_id)) + .order_by(transactions::id.desc()) + .limit(limit as i64) + .load_iter(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("get_last_scanned_block_id: {}", e), + })?; + + rows.map(|r| { + r.map_err(|e| StorageError::QueryError { + reason: format!("get_last_scanned_block_id: {}", e), + }) + .and_then(|(body, created_at): (String, PrimitiveDateTime)| { + let transaction: Transaction = deserialize_json(&body)?; + Ok(TransactionEntry { + transaction_id: transaction.calculate_id(), + created_at, + transaction, + }) + }) + }) + .collect() + } + + // -------------------------------- KeyValues -------------------------------- // + fn key_value_get_value, T: DeserializeOwned>(&mut self, key: K) -> Result { + let key_value = self.key_value_get_raw(key)?; + deserialize_json(&key_value.value) + } + + fn key_value_get_raw>(&mut self, key: K) -> Result, StorageError> { + const OPERATION: &str = "key_value_get_json"; + use crate::storage_sqlite::schema::key_values; + + let key_value = key_values::table + .filter(key_values::key.eq(key.as_ref())) + .first::(self.connection()) + .optional() + .map_err(|e| StorageError::general(OPERATION, e))? + .ok_or_else(|| StorageError::NotFound { + item: "key_value", + key: key.as_ref().to_string(), + })?; + + Ok(key_value.into()) + } + + fn utxos_get_max_state_version( + &mut self, + resource_address: ResourceAddress, + shard: Shard, + ) -> Result { + const OPERATION: &str = "utxos_get_max_state_version"; + use crate::storage_sqlite::schema::utxos; + let max_version = utxos::table + .select(dsl::max(utxos::state_version)) + .filter(utxos::resource_address.eq(resource_address.to_string())) + .filter(utxos::shard.eq(shard.as_u32() as i32)) + .get_result::>(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("{OPERATION}: {}", e), + })? + .map(|v| StateVersion::new(v as u64)) + .unwrap_or_else(StateVersion::zero); + + Ok(max_version) + } + + fn utxos_get_updates( + &mut self, + resource_address: ResourceAddress, + shard: Shard, + from_state_version: StateVersion, + limit: u32, + ) -> Result<(StateVersion, Vec), StorageError> { + const OPERATION: &str = "get_utxo_updates"; + use crate::storage_sqlite::schema::utxos; + + let rows = utxos::table + .filter(utxos::resource_address.eq(resource_address.to_string())) + .filter(utxos::state_version.gt(from_state_version.as_u64() as i64)) + .filter(utxos::shard.eq(shard.as_u32() as i32)) + .limit(i64::from(limit)) + .order_by(utxos::state_version.asc()) + .load_iter::(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("{OPERATION}: {}", e), + })?; + + let mut updates = Vec::new(); + let mut max_state_version = StateVersion::zero(); + for row in rows { + let row = row.map_err(|e| StorageError::QueryError { + reason: format!("{OPERATION}: {}", e), + })?; + let (state_version, update) = row.try_convert_to_update()?; + max_state_version = max_state_version.max(state_version); + updates.push(update); + } + + Ok((max_state_version, updates)) + } + + fn utxos_get_unspent_by_public_nonce_and_tag( + &mut self, + resource_address: &ResourceAddress, + tag_and_nonce_pairs: &[(UtxoTag, RistrettoPublicKeyBytes)], + ) -> Result, StorageError> { + const OPERATION: &str = "utxos_get_unspent_by_public_nonce_and_tag"; + use crate::storage_sqlite::schema::utxos; + + // Diesel does not support tuple IN queries, so we have to use raw SQL here (https://github.com/diesel-rs/diesel/issues/3222#issuecomment-2088239186) + let mut tag_and_nonce_query = String::with_capacity(29 + tag_and_nonce_pairs.len() * 50); + tag_and_nonce_query.push_str("(utxo_tag,public_nonce) IN ("); + for (i, (tag, nonce)) in tag_and_nonce_pairs.iter().enumerate() { + // SQLINJECTION not possible as we control the types + write!( + &mut tag_and_nonce_query, + "({},'{}')", + tag.value() as i32, + serialize_hex(nonce) + ) + .expect("write to string is infallible"); + + if i < tag_and_nonce_pairs.len() - 1 { + tag_and_nonce_query.push(','); + } + } + tag_and_nonce_query.push(')'); + + let rows = utxos::table + .filter(utxos::resource_address.eq(resource_address.to_string())) + .filter(dsl::sql::(&tag_and_nonce_query)) + .filter(utxos::is_spent.eq(false)) + .filter(utxos::is_burnt.eq(false)) + .load_iter::(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("{OPERATION}: {}", e), + })?; + let mut utxos = Vec::new(); + for row in rows { + let row = row.map_err(|e| StorageError::QueryError { + reason: format!("{OPERATION}: {}", e), + })?; + + let (address, utxo) = row.try_convert_to_utxo()?; + utxos.push((*address.id(), utxo)); + } + + Ok(utxos) + } +} diff --git a/applications/tari_indexer/src/storage_sqlite/schema.rs b/applications/tari_indexer/src/storage_sqlite/schema.rs index 6645fc8776..65175c9c69 100644 --- a/applications/tari_indexer/src/storage_sqlite/schema.rs +++ b/applications/tari_indexer/src/storage_sqlite/schema.rs @@ -92,13 +92,14 @@ diesel::table! { diesel::table! { utxos (id) { id -> Integer, - address -> Text, + commitment -> Text, + public_nonce -> Text, version -> Integer, resource_address -> Text, shard -> Integer, state_version -> BigInt, - output -> Nullable, - utxo_tag_byte -> Nullable, + output -> Nullable, + utxo_tag -> Integer, is_spent -> Bool, is_burnt -> Bool, is_frozen -> Bool, diff --git a/applications/tari_indexer/src/storage_sqlite/serialization.rs b/applications/tari_indexer/src/storage_sqlite/serialization.rs index 04bfcd8b81..27cae613e7 100644 --- a/applications/tari_indexer/src/storage_sqlite/serialization.rs +++ b/applications/tari_indexer/src/storage_sqlite/serialization.rs @@ -6,6 +6,25 @@ use std::any::type_name; use serde::Serialize; use tari_ootle_storage::StorageError; +pub fn serialize_bincode(t: &T) -> Result, StorageError> { + bincode::serde::encode_to_vec(t, bincode::config::standard()).map_err(|e| StorageError::EncodingError { + operation: "serialize_bincode", + item: type_name::(), + details: e.to_string(), + }) +} + +pub fn deserialize_bincode>(s: S) -> Result { + let (t, _) = bincode::serde::decode_from_slice(s.as_ref(), bincode::config::standard()).map_err(|e| { + StorageError::DecodingError { + operation: "deserialize_bincode", + item: type_name::(), + details: e.to_string(), + } + })?; + Ok(t) +} + pub fn serialize_json(t: &T) -> Result { serde_json::to_string(t).map_err(|e| StorageError::EncodingError { operation: "serialize_json", diff --git a/applications/tari_indexer/src/storage_sqlite/store_factory.rs b/applications/tari_indexer/src/storage_sqlite/store_factory.rs index 005473cbcf..23a63558ed 100644 --- a/applications/tari_indexer/src/storage_sqlite/store_factory.rs +++ b/applications/tari_indexer/src/storage_sqlite/store_factory.rs @@ -1,66 +1,38 @@ -// Copyright 2022. The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// 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. +// Copyright 2022 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause use std::{ - collections::HashSet, fmt::Debug, fs::create_dir_all, ops::{Deref, DerefMut}, path::PathBuf, - str::FromStr, sync::{Arc, Mutex}, }; -use diesel::{dsl, prelude::*, sql_query, SqliteConnection}; +use diesel::{sql_query, Connection, RunQueryDsl, SqliteConnection}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness}; -use log::*; use serde::{de::DeserializeOwned, Serialize}; use tari_consensus_types::BlockId; -use tari_crypto::tari_utilities::hex::to_hex; -use tari_engine_types::{ - events::Event, - substate::{SubstateId, SubstateValue}, -}; -use tari_indexer_client::types::{ListSubstateItem, TransactionEntry}; -use tari_indexer_lib::NonFungibleSubstate; -use tari_ootle_common_types::{shard::Shard, substate_type::SubstateType, Epoch, ShardGroup, StateVersion, UtxoUpdate}; +use tari_engine_types::{events::Event, substate::SubstateId, Utxo, UtxoId}; +use tari_indexer_client::types::{ListSubstateItem, NonFungibleSubstate, TransactionEntry}; +use tari_ootle_common_types::{shard::Shard, substate_type::SubstateType, Epoch, ShardGroup, StateVersion}; use tari_ootle_storage::{ consensus_models::{EpochCheckpoint, SubstateUpdateProof}, - time::PrimitiveDateTime, StorageError, }; use tari_ootle_storage_sqlite::{error::SqliteStorageError, SqliteTransaction}; +use tari_ootle_wallet_sdk::models::WalletUtxoUpdate; use tari_template_lib::{ models::ResourceAddress, - types::{crypto::UtxoTagByte, TemplateAddress}, + prelude::{crypto::UtxoTag, RistrettoPublicKeyBytes}, + types::TemplateAddress, }; use tari_transaction::{Transaction, TransactionId}; -use super::models::{NewEvent, NewScannedBlockId, UtxoRecordInsert, UtxoRecordUpdate}; use crate::storage_sqlite::{ - models, - models::{EventDb, KeyValue, NewSubstate, ScannedBlockId, Substate}, - schema, - serialization::{deserialize_hex_try_from, deserialize_json, serialize_hex, serialize_json}, + models::{EventRecord, KeyValue, NewScannedBlockId, NewSubstate, SubstateRecord, UtxoUpdateRecord}, + reader::SqliteStoreReadTransaction, + writer::SqliteStoreWriteTransaction, }; const LOG_TARGET: &str = "tari::indexer::storage_sqlite"; @@ -147,20 +119,6 @@ impl IndexerStore for SqliteIndexerStore { } } -pub struct SqliteStoreReadTransaction<'a> { - transaction: SqliteTransaction<'a>, -} - -impl<'a> SqliteStoreReadTransaction<'a> { - fn new(transaction: SqliteTransaction<'a>) -> Self { - Self { transaction } - } - - fn connection(&mut self) -> &mut SqliteConnection { - self.transaction.connection() - } -} - pub trait IndexerStoreReadTransaction { fn list_substates( &mut self, @@ -169,7 +127,11 @@ pub trait IndexerStoreReadTransaction { limit: Option, offset: Option, ) -> Result, StorageError>; - fn get_substate(&mut self, address: &SubstateId, version: Option) -> Result, StorageError>; + fn get_substate( + &mut self, + address: &SubstateId, + version: Option, + ) -> Result, StorageError>; fn get_non_fungible_count(&mut self, resource_address: String) -> Result; fn get_non_fungibles_by_resource_address( &mut self, @@ -184,7 +146,7 @@ pub trait IndexerStoreReadTransaction { topic_filter: Option, offset: u32, limit: u32, - ) -> Result, StorageError>; + ) -> Result, StorageError>; fn get_oldest_scanned_epoch(&mut self) -> Result, StorageError>; fn get_last_scanned_block_id( &mut self, @@ -209,386 +171,23 @@ pub trait IndexerStoreReadTransaction { /// Get UTXO updates for a given resource address and shard, starting from a specific state version. /// - /// Returns a tuple containing the total count of matching UTXO updates (without the limit) and a vector of UTXO - /// updates limited by the specified limit. + /// Returns a tuple containing the maximum returned state version, and a vector of UTXO + /// updates. fn utxos_get_updates( &mut self, resource_address: ResourceAddress, shard: Shard, from_state_version: StateVersion, - filter_tag_bytes: &HashSet, - limit: u32, - ) -> Result, StorageError>; -} - -impl IndexerStoreReadTransaction for SqliteStoreReadTransaction<'_> { - fn list_substates( - &mut self, - by_type: Option, - by_template_address: Option, - limit: Option, - offset: Option, - ) -> Result, StorageError> { - use crate::storage_sqlite::schema::substates; - - let mut query = substates::table.into_boxed(); - - if let Some(template_address) = by_template_address { - query = query.filter(substates::template_address.eq(template_address.to_string())); - } - - if let Some(substate_type) = by_type { - let address_like = format!("{}_%", substate_type.as_prefix_str()); - query = query.filter(substates::address.like(address_like)); - } - - if let Some(limit) = limit { - query = query.limit(limit as i64); - } - if let Some(offset) = offset { - query = query.offset(offset as i64); - } - - let substates: Vec = query - .order_by(substates::id.desc()) - .get_results(self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("list_substates: {}", e), - })?; - - let items = substates - .into_iter() - .map(|s| { - let substate_id = SubstateId::from_str(&s.address)?; - let version = s.version as u32; - let template_address = s.template_address.map(|h| deserialize_hex_try_from(&h)).transpose()?; - let timestamp = s.timestamp; - Ok(ListSubstateItem { - substate_id, - module_name: s.module_name, - version, - template_address, - timestamp, - }) - }) - .collect::, anyhow::Error>>() - .map_err(|e| StorageError::QueryError { - reason: format!("list_substates: invalid substate items: {}", e), - })?; - - Ok(items) - } - - fn get_substate(&mut self, address: &SubstateId, version: Option) -> Result, StorageError> { - use crate::storage_sqlite::schema::substates; - - let mut substate_query = substates::table - .into_boxed() - .filter(substates::address.eq(address.to_string())); - if let Some(version) = version { - substate_query = substate_query.filter(substates::version.eq(version as i32)); - } else { - substate_query = substate_query.order_by(substates::version.desc()) - } - - substate_query - .limit(1) - .first(self.connection()) - .optional() - .map_err(|e| StorageError::QueryError { - reason: format!("get_substate: {}", e), - }) - } - - fn get_non_fungible_count(&mut self, resource_address: String) -> Result { - use crate::storage_sqlite::schema::non_fungible_indexes; - - let count = non_fungible_indexes::table - .filter(non_fungible_indexes::resource_address.eq(resource_address)) - .count() - .get_result::(self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("get_non_fungible_count: {}", e), - })?; - - Ok(count) - } - - fn get_non_fungibles_by_resource_address( - &mut self, - resource_address: ResourceAddress, - limit: usize, - offset: usize, - ) -> Result, StorageError> { - use crate::storage_sqlite::schema::substates; - - let res = substates::table - .filter(substates::address.like(format!("nft_{}_%", resource_address.as_object_key()))) - .limit(limit as i64) - .offset(offset as i64) - .get_results::(self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("get_non_fungibles_by_resource_address: {}", e), - })?; - - res.into_iter() - .map(|row| { - let value: SubstateValue = - serde_json::from_str(&row.data).map_err(|e| StorageError::DataInconsistency { - details: format!("Failed to parse substate data: {}", e), - })?; - - Ok(NonFungibleSubstate { - address: row.address.parse().map_err(|e| StorageError::DataInconsistency { - details: format!("Failed to parse address: {}", e), - })?, - version: row.version.try_into().map_err(|e| StorageError::DataInconsistency { - details: format!("Version overflow {}", e), - })?, - substate: value, - }) - }) - .collect() - } - - fn get_events( - &mut self, - substate_id_filter: Option, - topic_filter: Option, - offset: u32, limit: u32, - ) -> Result, StorageError> { - // TODO: allow to query by payload as well, unifying all event methods into one - info!( - target: LOG_TARGET, - "Querying substate scanner database: get_events with substate_id_filter = {:?} and \ - topic_filter = {:?}", - substate_id_filter, - topic_filter - ); - use crate::storage_sqlite::schema::events; - - let mut query = events::table.into_boxed(); - - if let Some(substate_id) = substate_id_filter { - query = query.filter(events::substate_id.eq(substate_id.to_string())); - } - - if let Some(topic) = topic_filter { - query = query.filter(events::topic.eq(topic)); - } - - query = query.offset(offset.into()); - if limit > 0 { - query = query.limit(limit.into()); - } - - let events = query - .order(events::id.desc()) - .get_results::(self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("get_events: {}", e), - })?; - - Ok(events) - } - - fn get_oldest_scanned_epoch(&mut self) -> Result, StorageError> { - use crate::storage_sqlite::schema::scanned_block_ids; - - let res: Option = scanned_block_ids::table - .select(diesel::dsl::min(scanned_block_ids::epoch)) - .first(self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("get_oldest_scanned_epoch: {}", e), - })?; - - let oldest_epoch = res - .map(|r| { - let epoch_as_u64 = r as u64; - Ok::(Epoch(epoch_as_u64)) - }) - .transpose()?; - - Ok(oldest_epoch) - } - - fn get_last_scanned_block_id( - &mut self, - epoch: Epoch, - shard_group: ShardGroup, - ) -> Result, StorageError> { - use crate::storage_sqlite::schema::scanned_block_ids; - - let row: Option = scanned_block_ids::table - .filter( - scanned_block_ids::epoch - .eq(epoch.0 as i64) - .and(scanned_block_ids::shard_group.eq(shard_group.encode_as_u32() as i32)), - ) - .first(self.connection()) - .optional() - .map_err(|e| StorageError::QueryError { - reason: format!("get_last_scanned_block_id: {}", e), - })?; + ) -> Result<(StateVersion, Vec), StorageError>; - let block_id_option = row.map(|r| BlockId::try_from(r.last_block_id)).transpose()?; - - Ok(block_id_option) - } - - fn list_recent_transactions( + fn utxos_get_unspent_by_public_nonce_and_tag( &mut self, - last_transaction_id: Option, - limit: usize, - ) -> Result, StorageError> { - use crate::storage_sqlite::schema::transactions; - - let start_id = if let Some(last_id) = last_transaction_id { - transactions::table - .select(transactions::id) - .filter(transactions::transaction_id.eq(serialize_hex(last_id))) - .first::(self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("list_recent_transactions: {e}"), - })? - } else { - i32::MAX - }; - - let rows = transactions::table - .select((transactions::body, transactions::created_at)) - .filter(transactions::id.lt(start_id)) - .order_by(transactions::id.desc()) - .limit(limit as i64) - .load_iter(self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("get_last_scanned_block_id: {}", e), - })?; - - rows.map(|r| { - r.map_err(|e| StorageError::QueryError { - reason: format!("get_last_scanned_block_id: {}", e), - }) - .and_then(|(body, created_at): (String, PrimitiveDateTime)| { - let transaction: Transaction = deserialize_json(&body)?; - Ok(TransactionEntry { - transaction_id: transaction.calculate_id(), - created_at, - transaction, - }) - }) - }) - .collect() - } - - // -------------------------------- KeyValues -------------------------------- // - fn key_value_get_value, T: DeserializeOwned>(&mut self, key: K) -> Result { - let key_value = self.key_value_get_raw(key)?; - deserialize_json(&key_value.value) - } - - fn key_value_get_raw>(&mut self, key: K) -> Result, StorageError> { - const OPERATION: &str = "key_value_get_json"; - use schema::key_values; - - let key_value = key_values::table - .filter(key_values::key.eq(key.as_ref())) - .first::(self.connection()) - .optional() - .map_err(|e| StorageError::general(OPERATION, e))? - .ok_or_else(|| StorageError::NotFound { - item: "key_value", - key: key.as_ref().to_string(), - })?; - - Ok(key_value.into()) - } - - fn utxos_get_max_state_version( - &mut self, - resource_address: ResourceAddress, - shard: Shard, - ) -> Result { - const OPERATION: &str = "utxos_get_max_state_version"; - use crate::storage_sqlite::schema::utxos; - let max_version = utxos::table - .select(dsl::max(utxos::state_version)) - .filter(utxos::resource_address.eq(resource_address.to_string())) - .filter(utxos::shard.eq(shard.as_u32() as i32)) - .get_result::>(self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("{OPERATION}: {}", e), - })? - .map(|v| StateVersion::new(v as u64)) - .unwrap_or_else(StateVersion::zero); - - Ok(max_version) - } - - fn utxos_get_updates( - &mut self, - resource_address: ResourceAddress, - shard: Shard, - from_state_version: StateVersion, - filter_tag_bytes: &HashSet, - limit: u32, - ) -> Result, StorageError> { - const OPERATION: &str = "get_utxo_updates"; - use crate::storage_sqlite::schema::utxos; - - let mut query = utxos::table - .filter(utxos::resource_address.eq(resource_address.to_string())) - .filter(utxos::state_version.gt(from_state_version.as_u64() as i64)) - .filter(utxos::shard.eq(shard.as_u32() as i32)) - .into_boxed(); - - if !filter_tag_bytes.is_empty() { - query = query.filter(utxos::utxo_tag_byte.eq_any(filter_tag_bytes.iter().map(|b| i32::from(b.as_byte())))); - } - - let rows = query - .limit(i64::from(limit)) - .order_by(utxos::state_version.asc()) - .load_iter::(self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("{OPERATION}: {}", e), - })?; - - // NOTE: I doubt the size hint > 0 - let (lower, upper) = rows.size_hint(); - let size_hint = upper.unwrap_or(lower); - let mut updates = Vec::with_capacity(size_hint); - for row in rows { - let row = row.map_err(|e| StorageError::QueryError { - reason: format!("{OPERATION}: {}", e), - })?; - - updates.push(row.try_convert()?); - } - - Ok(updates) - } + resource_address: &ResourceAddress, + public_nonce_and_tag: &[(UtxoTag, RistrettoPublicKeyBytes)], + ) -> Result, StorageError>; } -pub struct SqliteStoreWriteTransaction<'a> { - /// None indicates if the transaction has been explicitly committed/rolled back - transaction: Option>, -} - -impl<'a> SqliteStoreWriteTransaction<'a> { - pub fn new(transaction: SqliteTransaction<'a>) -> Self { - Self { - transaction: Some(SqliteStoreReadTransaction::new(transaction)), - } - } - - pub fn connection(&mut self) -> &mut SqliteConnection { - self.transaction.as_mut().unwrap().connection() - } -} - -// TODO: remove the allow dead_code attributes as these become used. pub trait IndexerStoreWriteTransaction { fn commit(self) -> Result<(), StorageError>; fn rollback(self) -> Result<(), StorageError>; @@ -599,8 +198,10 @@ pub trait IndexerStoreWriteTransaction { state_version: StateVersion, updates: I, ) -> Result<(), StorageError>; - fn batch_insert_utxo_updates>(&mut self, updates: I) - -> Result<(), StorageError>; + fn batch_insert_utxo_updates>( + &mut self, + updates: I, + ) -> Result<(), StorageError>; fn upsert_substate(&mut self, new_substate: NewSubstate) -> Result<(), StorageError>; fn batch_insert_events>(&mut self, events: I) -> Result<(), StorageError>; fn save_scanned_block_id(&mut self, new_scanned_block_id: NewScannedBlockId) -> Result<(), StorageError>; @@ -608,324 +209,3 @@ pub trait IndexerStoreWriteTransaction { fn insert_or_ignore_transaction(&mut self, transaction: &Transaction) -> Result<(), StorageError>; fn insert_or_ignore_epoch_checkpoint(&mut self, epoch_checkpoint: &EpochCheckpoint) -> Result<(), StorageError>; } - -impl IndexerStoreWriteTransaction for SqliteStoreWriteTransaction<'_> { - fn commit(mut self) -> Result<(), StorageError> { - self.transaction.take().unwrap().transaction.commit()?; - Ok(()) - } - - fn rollback(mut self) -> Result<(), StorageError> { - self.transaction.take().unwrap().transaction.rollback()?; - Ok(()) - } - - fn key_value_set, V: Serialize>(&mut self, key: K, value: V) -> Result<(), StorageError> { - const OPERATION: &str = "key_value_set"; - use schema::key_values; - let json = serialize_json(&value)?; - - diesel::insert_into(key_values::table) - .values((key_values::key.eq(key.as_ref()), key_values::value.eq(&json))) - .on_conflict(key_values::key) - .do_update() - .set(key_values::value.eq(&json)) - .execute(self.connection()) - .map_err(|e| StorageError::general(OPERATION, e))?; - - Ok(()) - } - - fn batch_insert_substate_transitions>( - &mut self, - shard: Shard, - state_version: StateVersion, - updates: I, - ) -> Result<(), StorageError> { - const OPERATION: &str = "batch_insert_substate_transitions"; - use schema::substate_transitions; - - diesel::insert_into(substate_transitions::table) - .values( - updates - .into_iter() - .map(|(epoch, proof)| { - ( - substate_transitions::shard.eq(shard.as_u32() as i32), - substate_transitions::state_version.eq(state_version.as_u64() as i64), - substate_transitions::epoch.eq(epoch.as_u64() as i64), - substate_transitions::substate_id.eq(proof.substate_id().to_string()), - substate_transitions::substate_type.eq(SubstateType::from(proof.substate_id()).to_string()), - substate_transitions::version.eq(proof.version() as i32), - substate_transitions::is_up.eq(proof.is_create()), - substate_transitions::value_hash.eq(proof - .as_create() - .map(|v| serialize_hex(v.substate.value.to_value_hash(proof.version())))), - // substate_transitions::utxo_tag_byte.eq(proof - ) - }) - .collect::>(), - ) - .execute(self.connection()) - .map_err(|e| StorageError::general(OPERATION, e))?; - - Ok(()) - } - - fn batch_insert_utxo_updates>( - &mut self, - updates: I, - ) -> Result<(), StorageError> { - const OPERATION: &str = "batch_insert_utxo_updates"; - use crate::storage_sqlite::schema::utxos; - - fn utxo_exists(connection: &mut SqliteConnection, address: &str) -> Result { - use crate::storage_sqlite::schema::utxos; - - let count = utxos::table - .filter(utxos::address.eq(address)) - .count() - .limit(1) - .get_result::(connection) - .map_err(|e| StorageError::QueryError { - reason: format!("utxo_exists: {}", e), - })?; - Ok(count > 0) - } - - let (inserts, updates) = - updates - .into_iter() - .try_fold((Vec::new(), Vec::new()), |(mut inserts, mut updates), update| { - match update { - UtxoUpdate::Unspent(unspent) => { - let address = unspent.address.to_string(); - if utxo_exists(self.connection(), &address)? { - // If the UTXO already exists, we update it instead of inserting a new one - let update = UtxoRecordUpdate { - version: Some(unspent.version as i32), - output: Some(unspent.utxo.output().map(serialize_json).transpose()?), - state_version: Some(unspent.state_version.as_u64() as i64), - is_spent: Some(false), - is_burnt: Some(unspent.utxo.is_burnt()), - is_frozen: Some(unspent.utxo.is_frozen()), - }; - updates.push((address, update)); - } else { - inserts.push(UtxoRecordInsert { - address, - version: unspent.version as i32, - output: unspent.utxo.output().map(serialize_json).transpose()?, - shard: unspent.shard.as_u32() as i32, - resource_address: unspent.address.resource_address().to_string(), - state_version: unspent.state_version.as_u64() as i64, - utxo_tag_byte: unspent.utxo.output().map(|o| i32::from(o.tag.as_byte())), - is_spent: false, - is_burnt: unspent.utxo.is_burnt(), - is_frozen: unspent.utxo.is_frozen(), - }); - } - }, - UtxoUpdate::Spent(spent) => { - let update = UtxoRecordUpdate { - version: Some(spent.version as i32), - // Prune the UTXO data for spent outputs - output: Some(None), - // Update to deleted state version - state_version: Some(spent.state_version.as_u64() as i64), - is_spent: Some(true), - ..Default::default() - }; - updates.push((spent.address.to_string(), update)); - }, - } - - Ok::<_, StorageError>((inserts, updates)) - })?; - - for insert in inserts { - // batch insert results in "cannot start a transaction within a transaction" error - diesel::insert_into(utxos::table) - .values(insert) - .execute(self.connection()) - .map_err(|e| StorageError::general(OPERATION, format!("insert error: {e}")))?; - } - - for (address, update) in updates { - diesel::update(utxos::table.filter(utxos::address.eq(address))) - .set(update) - .execute(self.connection()) - .map_err(|e| StorageError::general(OPERATION, format!("update error: {e}")))?; - } - - Ok(()) - } - - fn upsert_substate(&mut self, new_substate: NewSubstate) -> Result<(), StorageError> { - use crate::storage_sqlite::schema::substates; - - let address = &new_substate.address; - let current_substate = substates::table - .filter(substates::address.eq(address)) - .first::(self.connection()) - .optional() - .map_err(|e| StorageError::QueryError { - reason: format!("find_by_address: {}", e), - })?; - - match current_substate { - Some(_) => { - diesel::update(substates::table) - .set(&new_substate) - .filter(substates::address.eq(address)) - .execute(self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("Update leaf node: {}", e), - })?; - debug!( - target: LOG_TARGET, - "Updated substate {} version to {}", address, new_substate.version - ); - }, - None => { - diesel::insert_into(substates::table) - .values(&new_substate) - .execute(self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("Update substate error: {}", e), - })?; - info!( - target: LOG_TARGET, - "Added new substate {} with version {}", address, new_substate.version - ); - }, - }; - - Ok(()) - } - - fn batch_insert_events>(&mut self, events: I) -> Result<(), StorageError> { - const OPERATION: &str = "batch_insert_events"; - use crate::storage_sqlite::schema::events; - - let events = events.into_iter().map(|event| { - Ok::<_, StorageError>(NewEvent { - template_address: event.template_address().to_string(), - tx_hash: event.tx_hash().to_string(), - topic: event.topic(), - payload: serialize_json(event.payload())?, - substate_id: event.substate_id().map(|s| s.to_string()), - }) - }); - - for result in events { - let event = result?; - - diesel::insert_into(events::table) - .values(event) - .execute(self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("{OPERATION}: {}", e), - })?; - } - - Ok(()) - } - - fn save_scanned_block_id(&mut self, new: NewScannedBlockId) -> Result<(), StorageError> { - use crate::storage_sqlite::schema::scanned_block_ids; - - diesel::insert_into(scanned_block_ids::table) - .values(&new) - .on_conflict((scanned_block_ids::epoch, scanned_block_ids::shard_group)) - .do_update() - .set(new.clone()) - .execute(&mut *self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("save_scanned_block_id error: {}", e), - })?; - - debug!( - target: LOG_TARGET, - "Added new scanned block id {} for epoch {} and shard {:?}", to_hex(&new.last_block_id), new.epoch, new.shard_group - ); - - Ok(()) - } - - fn delete_scanned_epochs_older_than(&mut self, epoch: Epoch) -> Result<(), StorageError> { - use crate::storage_sqlite::schema::scanned_block_ids; - - diesel::delete(scanned_block_ids::table) - .filter(scanned_block_ids::epoch.lt(epoch.as_u64() as i64)) - .execute(&mut *self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("delete_scanned_epochs_older_than: {}", e), - })?; - - Ok(()) - } - - fn insert_or_ignore_transaction(&mut self, transaction: &Transaction) -> Result<(), StorageError> { - use crate::storage_sqlite::schema::transactions; - - diesel::insert_into(transactions::table) - .values(( - transactions::transaction_id.eq(serialize_hex(transaction.calculate_id())), - transactions::body.eq(serialize_json(transaction).unwrap()), - )) - .on_conflict_do_nothing() - .execute(self.connection()) - .map_err(|e| StorageError::QueryError { - reason: format!("insert_transaction: {e}"), - })?; - - Ok(()) - } - - fn insert_or_ignore_epoch_checkpoint(&mut self, epoch_checkpoint: &EpochCheckpoint) -> Result<(), StorageError> { - const OPERATION: &str = "insert_or_ignore_epoch_checkpoint"; - use crate::storage_sqlite::schema::epoch_checkpoints; - - diesel::insert_into(epoch_checkpoints::table) - .values(( - epoch_checkpoints::epoch.eq(epoch_checkpoint.epoch().as_u64() as i64), - epoch_checkpoints::shard_group.eq(epoch_checkpoint - .checked_shard_group() - .expect("shard group should be valid") - .to_parsable_string()), - epoch_checkpoints::json_data.eq(serialize_json(epoch_checkpoint)?), - )) - .on_conflict((epoch_checkpoints::epoch, epoch_checkpoints::shard_group)) - .do_nothing() - .execute(self.connection()) - .map_err(|e| StorageError::general(OPERATION, e))?; - - Ok(()) - } -} - -impl<'a> Deref for SqliteStoreWriteTransaction<'a> { - type Target = SqliteStoreReadTransaction<'a>; - - fn deref(&self) -> &Self::Target { - self.transaction.as_ref().unwrap() - } -} - -impl DerefMut for SqliteStoreWriteTransaction<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.transaction.as_mut().unwrap() - } -} - -impl Drop for SqliteStoreWriteTransaction<'_> { - fn drop(&mut self) { - if self.transaction.is_some() { - warn!( - target: LOG_TARGET, - "Substate store write transaction was not committed/rolled back" - ); - } - } -} diff --git a/applications/tari_indexer/src/storage_sqlite/writer.rs b/applications/tari_indexer/src/storage_sqlite/writer.rs new file mode 100644 index 0000000000..cfa3fb883c --- /dev/null +++ b/applications/tari_indexer/src/storage_sqlite/writer.rs @@ -0,0 +1,340 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::ops::{Deref, DerefMut}; + +use diesel::{OptionalExtension, QueryDsl, RunQueryDsl, SqliteConnection}; +use log::{debug, info, warn}; +use serde::Serialize; +use tari_engine_types::events::Event; +use tari_ootle_common_types::{shard::Shard, substate_type::SubstateType, Epoch, StateVersion}; +use tari_ootle_storage::{ + consensus_models::{EpochCheckpoint, SubstateUpdateProof}, + StorageError, +}; +use tari_ootle_storage_sqlite::SqliteTransaction; +use tari_transaction::Transaction; + +use crate::{ + diesel::ExpressionMethods, + storage_sqlite::{ + models::{ + NewEvent, + NewScannedBlockId, + NewSubstate, + SubstateRecord, + UtxoRecordInsert, + UtxoRecordUpdate, + UtxoUpdateRecord, + }, + reader::SqliteStoreReadTransaction, + serialization::{serialize_bincode, serialize_hex, serialize_json}, + IndexerStoreWriteTransaction, + }, +}; + +const LOG_TARGET: &str = "tari::indexer::storage_sqlite::writer"; + +pub struct SqliteStoreWriteTransaction<'a> { + /// None indicates if the transaction has been explicitly committed/rolled back + transaction: Option>, +} + +impl<'a> SqliteStoreWriteTransaction<'a> { + pub fn new(transaction: SqliteTransaction<'a>) -> Self { + Self { + transaction: Some(SqliteStoreReadTransaction::new(transaction)), + } + } + + fn connection(&mut self) -> &mut SqliteConnection { + self.transaction.as_mut().unwrap().connection() + } +} +impl IndexerStoreWriteTransaction for SqliteStoreWriteTransaction<'_> { + fn commit(mut self) -> Result<(), StorageError> { + self.transaction.take().unwrap().transaction.commit()?; + Ok(()) + } + + fn rollback(mut self) -> Result<(), StorageError> { + self.transaction.take().unwrap().transaction.rollback()?; + Ok(()) + } + + fn key_value_set, V: Serialize>(&mut self, key: K, value: V) -> Result<(), StorageError> { + const OPERATION: &str = "key_value_set"; + use crate::storage_sqlite::schema::key_values; + let json = serialize_json(&value)?; + + diesel::insert_into(key_values::table) + .values((key_values::key.eq(key.as_ref()), key_values::value.eq(&json))) + .on_conflict(key_values::key) + .do_update() + .set(key_values::value.eq(&json)) + .execute(self.connection()) + .map_err(|e| StorageError::general(OPERATION, e))?; + + Ok(()) + } + + fn batch_insert_substate_transitions>( + &mut self, + shard: Shard, + state_version: StateVersion, + updates: I, + ) -> Result<(), StorageError> { + const OPERATION: &str = "batch_insert_substate_transitions"; + use crate::storage_sqlite::schema::substate_transitions; + + diesel::insert_into(substate_transitions::table) + .values( + updates + .into_iter() + .map(|(epoch, proof)| { + ( + substate_transitions::shard.eq(shard.as_u32() as i32), + substate_transitions::state_version.eq(state_version.as_u64() as i64), + substate_transitions::epoch.eq(epoch.as_u64() as i64), + substate_transitions::substate_id.eq(proof.substate_id().to_string()), + substate_transitions::substate_type.eq(SubstateType::from(proof.substate_id()).to_string()), + substate_transitions::version.eq(proof.version() as i32), + substate_transitions::is_up.eq(proof.is_create()), + substate_transitions::value_hash.eq(proof + .as_create() + .map(|v| serialize_hex(v.substate.value.to_value_hash(proof.version())))), + ) + }) + .collect::>(), + ) + .execute(self.connection()) + .map_err(|e| StorageError::general(OPERATION, e))?; + + Ok(()) + } + + fn batch_insert_utxo_updates>( + &mut self, + updates: I, + ) -> Result<(), StorageError> { + const OPERATION: &str = "batch_insert_utxo_updates"; + use crate::storage_sqlite::schema::utxos; + + for update in updates { + match update { + UtxoUpdateRecord::Unspent(unspent) => { + let resource_address = unspent.address.resource_address().to_string(); + let commitment = unspent.address.id().to_commitment_hex_string(); + + let insert = UtxoRecordInsert { + commitment, + public_nonce: serialize_hex(unspent.utxo_output.output.public_nonce), + version: unspent.version as i32, + output: Some(serialize_bincode(&unspent.utxo_output)?), + shard: unspent.shard.as_u32() as i32, + resource_address, + state_version: unspent.state_version.as_u64() as i64, + utxo_tag: unspent.utxo_output.tag.value() as i32, + is_spent: false, + is_burnt: false, + is_frozen: unspent.is_frozen, + }; + // batch insert results in "cannot start a transaction within a transaction" error + diesel::insert_into(utxos::table) + .values(insert) + .execute(self.connection()) + .map_err(|e| StorageError::general(OPERATION, format!("insert error: {e}")))?; + }, + UtxoUpdateRecord::Spent(spent) => { + let resource_address = spent.address.resource_address().to_string(); + let commitment = spent.address.id().to_commitment_hex_string(); + let update = UtxoRecordUpdate { + version: Some(spent.version as i32), + // Prune the UTXO data for spent outputs + output: Some(None), + // Update to deleted state version + state_version: Some(spent.state_version.as_u64() as i64), + is_spent: Some(true), + ..Default::default() + }; + diesel::update(utxos::table) + .filter(utxos::resource_address.eq(resource_address)) + .filter(utxos::commitment.eq(commitment)) + .set(update) + .execute(self.connection()) + .map_err(|e| StorageError::general(OPERATION, format!("update error: {e}")))?; + }, + } + } + + Ok(()) + } + + fn upsert_substate(&mut self, new_substate: NewSubstate) -> Result<(), StorageError> { + use crate::storage_sqlite::schema::substates; + + let address = &new_substate.address; + let current_substate = substates::table + .filter(substates::address.eq(address)) + .first::(self.connection()) + .optional() + .map_err(|e| StorageError::QueryError { + reason: format!("find_by_address: {}", e), + })?; + + match current_substate { + Some(_) => { + diesel::update(substates::table) + .set(&new_substate) + .filter(substates::address.eq(address)) + .execute(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("Update leaf node: {}", e), + })?; + debug!( + target: LOG_TARGET, + "Updated substate {} version to {}", address, new_substate.version + ); + }, + None => { + diesel::insert_into(substates::table) + .values(&new_substate) + .execute(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("Update substate error: {}", e), + })?; + info!( + target: LOG_TARGET, + "Added new substate {} with version {}", address, new_substate.version + ); + }, + }; + + Ok(()) + } + + fn batch_insert_events>(&mut self, events: I) -> Result<(), StorageError> { + const OPERATION: &str = "batch_insert_events"; + use crate::storage_sqlite::schema::events; + + let events = events.into_iter().map(|event| { + Ok::<_, StorageError>(NewEvent { + template_address: event.template_address().to_string(), + tx_hash: event.tx_hash().to_string(), + topic: event.topic(), + payload: serialize_json(event.payload())?, + substate_id: event.substate_id().map(|s| s.to_string()), + }) + }); + + for result in events { + let event = result?; + + diesel::insert_into(events::table) + .values(event) + .execute(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("{OPERATION}: {}", e), + })?; + } + + Ok(()) + } + + fn save_scanned_block_id(&mut self, new: NewScannedBlockId) -> Result<(), StorageError> { + use crate::storage_sqlite::schema::scanned_block_ids; + + diesel::insert_into(scanned_block_ids::table) + .values(&new) + .on_conflict((scanned_block_ids::epoch, scanned_block_ids::shard_group)) + .do_update() + .set(new.clone()) + .execute(&mut *self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("save_scanned_block_id error: {}", e), + })?; + + debug!( + target: LOG_TARGET, + "Added new scanned block id {} for epoch {} and shard {:?}", hex::encode(&new.last_block_id), new.epoch, new.shard_group + ); + + Ok(()) + } + + fn delete_scanned_epochs_older_than(&mut self, epoch: Epoch) -> Result<(), StorageError> { + use crate::storage_sqlite::schema::scanned_block_ids; + + diesel::delete(scanned_block_ids::table) + .filter(scanned_block_ids::epoch.lt(epoch.as_u64() as i64)) + .execute(&mut *self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("delete_scanned_epochs_older_than: {}", e), + })?; + + Ok(()) + } + + fn insert_or_ignore_transaction(&mut self, transaction: &Transaction) -> Result<(), StorageError> { + use crate::storage_sqlite::schema::transactions; + + diesel::insert_into(transactions::table) + .values(( + transactions::transaction_id.eq(serialize_hex(transaction.calculate_id())), + transactions::body.eq(serialize_json(transaction).unwrap()), + )) + .on_conflict_do_nothing() + .execute(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("insert_transaction: {e}"), + })?; + + Ok(()) + } + + fn insert_or_ignore_epoch_checkpoint(&mut self, epoch_checkpoint: &EpochCheckpoint) -> Result<(), StorageError> { + const OPERATION: &str = "insert_or_ignore_epoch_checkpoint"; + use crate::storage_sqlite::schema::epoch_checkpoints; + + diesel::insert_into(epoch_checkpoints::table) + .values(( + epoch_checkpoints::epoch.eq(epoch_checkpoint.epoch().as_u64() as i64), + epoch_checkpoints::shard_group.eq(epoch_checkpoint + .checked_shard_group() + .expect("shard group should be valid") + .to_parsable_string()), + epoch_checkpoints::json_data.eq(serialize_json(epoch_checkpoint)?), + )) + .on_conflict((epoch_checkpoints::epoch, epoch_checkpoints::shard_group)) + .do_nothing() + .execute(self.connection()) + .map_err(|e| StorageError::general(OPERATION, e))?; + + Ok(()) + } +} + +impl<'a> Deref for SqliteStoreWriteTransaction<'a> { + type Target = SqliteStoreReadTransaction<'a>; + + fn deref(&self) -> &Self::Target { + self.transaction.as_ref().unwrap() + } +} + +impl DerefMut for SqliteStoreWriteTransaction<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.transaction.as_mut().unwrap() + } +} + +impl Drop for SqliteStoreWriteTransaction<'_> { + fn drop(&mut self) { + if self.transaction.is_some() { + warn!( + target: LOG_TARGET, + "Substate store write transaction was not committed/rolled back" + ); + } + } +} diff --git a/applications/tari_indexer/src/substate_manager.rs b/applications/tari_indexer/src/substate_manager.rs index b19d50e819..091d30a614 100644 --- a/applications/tari_indexer/src/substate_manager.rs +++ b/applications/tari_indexer/src/substate_manager.rs @@ -20,30 +20,35 @@ // 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. -use std::{collections::HashSet, convert::TryInto}; +use std::convert::TryInto; use serde::{Deserialize, Serialize}; use tari_common_types::types::FixedHash; -use tari_engine_types::substate::{SubstateId, SubstateValue}; +use tari_engine_types::{ + substate::{SubstateId, SubstateValue}, + Utxo, + UtxoId, +}; use tari_epoch_manager::service::EpochManagerHandle; -use tari_indexer_client::types::ListSubstateItem; -use tari_indexer_lib::{substate_scanner::SubstateScanner, NonFungibleSubstate}; +use tari_indexer_client::types::{ListSubstateItem, NonFungibleSubstate}; +use tari_indexer_lib::substate_scanner::SubstateScanner; use tari_ootle_common_types::{ shard::Shard, substate_type::SubstateType, PeerAddress, StateVersion, - UtxoUpdate, VersionedSubstateIdRef, }; +use tari_ootle_wallet_sdk::models::WalletUtxoUpdate; use tari_template_lib::{ models::ResourceAddress, - types::{crypto::UtxoTagByte, TemplateAddress}, + prelude::{crypto::UtxoTag, RistrettoPublicKeyBytes}, + types::TemplateAddress, }; use tari_validator_node_rpc::client::{SubstateResult, TariValidatorNodeRpcClientFactory}; use crate::{ - storage_sqlite::store_factory::{IndexerStore, IndexerStoreReadTransaction, SqliteIndexerStore}, + storage_sqlite::{IndexerStore, IndexerStoreReadTransaction, SqliteIndexerStore}, substate_file_cache::SubstateFileCache, }; @@ -100,15 +105,34 @@ impl SubstateManager { resource_address: ResourceAddress, shard: Shard, from_state_version: StateVersion, - filter_tag_bytes: &HashSet, limit: u32, - ) -> Result<(StateVersion, Vec), anyhow::Error> { - let (max_version, updates) = self.substate_store.with_read_tx(|tx| { - let updates = tx.utxos_get_updates(resource_address, shard, from_state_version, filter_tag_bytes, limit)?; - let max_state_version = tx.utxos_get_max_state_version(resource_address, shard)?; - Ok::<_, anyhow::Error>((max_state_version, updates)) - })?; - Ok((max_version, updates)) + ) -> Result<(StateVersion, Vec), anyhow::Error> { + let updates = self + .substate_store + .with_read_tx(|tx| tx.utxos_get_updates(resource_address, shard, from_state_version, limit))?; + Ok(updates) + } + + pub fn get_max_state_version( + &self, + resource_address: &ResourceAddress, + shard: Shard, + ) -> Result { + let max_version = self + .substate_store + .with_read_tx(|tx| tx.utxos_get_max_state_version(*resource_address, shard))?; + Ok(max_version) + } + + pub fn get_unspent_utxos( + &self, + resource_address: &ResourceAddress, + public_nonce_and_tag: &[(UtxoTag, RistrettoPublicKeyBytes)], + ) -> Result, anyhow::Error> { + let utxos = self + .substate_store + .with_read_tx(|tx| tx.utxos_get_unspent_by_public_nonce_and_tag(resource_address, public_nonce_and_tag))?; + Ok(utxos) } pub async fn get_substate( diff --git a/applications/tari_indexer/src/transaction_manager/mod.rs b/applications/tari_indexer/src/transaction_manager/mod.rs index 9404f1eb98..dfce09e845 100644 --- a/applications/tari_indexer/src/transaction_manager/mod.rs +++ b/applications/tari_indexer/src/transaction_manager/mod.rs @@ -40,7 +40,7 @@ use tari_validator_node_rpc::client::{ use crate::{ network_client::TariNetworkClient, - storage_sqlite::store_factory::{IndexerStore, IndexerStoreReadTransaction, IndexerStoreWriteTransaction}, + storage_sqlite::{IndexerStore, IndexerStoreReadTransaction, IndexerStoreWriteTransaction}, transaction_manager::error::TransactionManagerError, }; diff --git a/applications/tari_indexer/web_ui/package.json b/applications/tari_indexer/web_ui/package.json index a408d593c0..1695f3b28e 100644 --- a/applications/tari_indexer/web_ui/package.json +++ b/applications/tari_indexer/web_ui/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "clean-dist": "rm -rf dist && rm -f tsconfig.tsbuildinfo" }, "dependencies": { "@emotion/react": "^11.14.0", diff --git a/applications/tari_validator_node/log4rs_sample.yml b/applications/tari_validator_node/log4rs_sample.yml index a584bf0f5f..cf1f3bb656 100644 --- a/applications/tari_validator_node/log4rs_sample.yml +++ b/applications/tari_validator_node/log4rs_sample.yml @@ -53,7 +53,7 @@ appenders: count: 5 pattern: "{{log_dir}}/log/validator-node/ootle.{}.log" encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}" + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}" # An appender named "ootle" that writes to a file with a custom pattern encoder engine: @@ -70,7 +70,7 @@ appenders: count: 5 pattern: "{{log_dir}}/log/validator-node/engine.{}.log" encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}" + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}" # An appender named "ootle" that writes to a file with a custom pattern encoder consensus: @@ -87,7 +87,7 @@ appenders: count: 5 pattern: "{{log_dir}}/log/validator-node/consensus.{}.log" encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}" + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}" # An appender named "libp2p" that writes to a file with a custom pattern encoder libp2p: diff --git a/applications/tari_validator_node/web_ui/package.json b/applications/tari_validator_node/web_ui/package.json index abff7f7403..d8bbfd5325 100644 --- a/applications/tari_validator_node/web_ui/package.json +++ b/applications/tari_validator_node/web_ui/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "clean-dist": "rm -rf dist && rm -f tsconfig.tsbuildinfo" }, "engines": { "node": ">=18.0.0" diff --git a/applications/tari_wallet_cli/src/command/account.rs b/applications/tari_wallet_cli/src/command/account.rs index 89c0ded887..a837af5af9 100644 --- a/applications/tari_wallet_cli/src/command/account.rs +++ b/applications/tari_wallet_cli/src/command/account.rs @@ -29,13 +29,12 @@ use tari_wallet_daemon_client::{ AccountsCreateOrGetRequest, AccountsCreateRequest, AccountsGetBalancesRequest, - RevealFundsRequest, }, ComponentAddressOrName, WalletDaemonClient, }; -use crate::{command::transaction::summarize_finalize_result, table::Table, table_row}; +use crate::{table::Table, table_row}; #[derive(Debug, Subcommand, Clone)] pub enum AccountsSubcommand { @@ -46,8 +45,6 @@ pub enum AccountsSubcommand { List, Get(GetArgs), - #[clap(alias = "reveal")] - RevealFunds(RevealFundsArgs), #[clap(alias = "faucet")] CreateFreeTestCoins(CreateFreeTestCoinsArgs), #[clap(alias = "default")] @@ -78,21 +75,6 @@ pub struct GetArgs { pub name: ComponentAddressOrName, } -#[derive(Debug, Args, Clone)] -pub struct RevealFundsArgs { - /// Amount of funds to reveal - reveal_amount: u64, - /// The account name where the funds will be revealed - account: Option, - /// The fee to pay for the reveal transaction - #[clap(long, short = 'f')] - max_fee: Option, - /// If set, the fee will be paid from the revealed funds instead of from the account resulting in less revealed - /// funds than requested. - #[clap(long, default_value_t = true)] - pay_from_reveal: bool, -} - #[derive(Debug, Args, Clone)] pub struct CreateFreeTestCoinsArgs { pub account: Option, @@ -117,7 +99,6 @@ impl AccountsSubcommand { handle_list(&mut client).await?; }, AccountsSubcommand::Get(args) => handle_get(args, &mut client).await?, - AccountsSubcommand::RevealFunds(args) => handle_reveal_funds(args, &mut client).await?, AccountsSubcommand::CreateFreeTestCoins(args) => handle_create_free_test_coins(args, &mut client).await?, AccountsSubcommand::SetDefault(args) => handle_set_default(args, &mut client).await?, } @@ -258,29 +239,3 @@ async fn handle_get(args: GetArgs, client: &mut WalletDaemonClient) -> Result<() Ok(()) } - -pub async fn handle_reveal_funds(args: RevealFundsArgs, client: &mut WalletDaemonClient) -> Result<(), anyhow::Error> { - let RevealFundsArgs { - account, - reveal_amount, - max_fee, - pay_from_reveal, - } = args; - - println!("Submitting reveal transaction..."); - let resp = client - .accounts_reveal_funds(RevealFundsRequest { - account, - amount_to_reveal: reveal_amount.into(), - max_fee: max_fee.map(Into::into), - pay_fee_from_reveal: pay_from_reveal, - }) - .await?; - - println!("Transaction: {}", resp.transaction_id); - println!("Fee: {}", resp.fee); - println!(); - summarize_finalize_result(&resp.result); - - Ok(()) -} diff --git a/applications/tari_walletd/log4rs_sample.yml b/applications/tari_walletd/log4rs_sample.yml index b3e4a03c14..b612037c8d 100644 --- a/applications/tari_walletd/log4rs_sample.yml +++ b/applications/tari_walletd/log4rs_sample.yml @@ -35,7 +35,7 @@ appenders: count: 5 pattern: "{{log_dir}}/log/wallet-daemon/ootle.{}.log" encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}" + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}" json_rpc: kind: rolling_file @@ -51,7 +51,7 @@ appenders: count: 5 pattern: "{{log_dir}}/log/wallet-daemon/json_rpc.{}.log" encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}" + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}" root: level: warn diff --git a/applications/tari_walletd/src/handlers/accounts.rs b/applications/tari_walletd/src/handlers/accounts.rs index 986870c263..5777759519 100644 --- a/applications/tari_walletd/src/handlers/accounts.rs +++ b/applications/tari_walletd/src/handlers/accounts.rs @@ -70,8 +70,6 @@ use tari_wallet_daemon_client::{ ConfidentialTransferRequest, ConfidentialTransferResponse, ExtClaimBurnProof, - RevealFundsRequest, - RevealFundsResponse, StealthTransferRequest, StealthTransferResponse, }, @@ -367,145 +365,6 @@ pub async fn handle_get_default( }) } -#[allow(clippy::too_many_lines)] -pub async fn handle_reveal_funds( - context: &HandlerContext, - token: Option<&Bearer>, - req: RevealFundsRequest, -) -> Result { - context.check_auth(token, &[JrpcPermission::Admin])?; - let sdk = context.wallet_sdk().clone(); - let notifier = context.notifier().clone(); - let transaction_service = context.transaction_service().clone(); - - // If the caller aborts the request early, this async block would be aborted at any await point. To avoid this, we - // spawn a task that will continue running. - let ctx = context.clone(); - task::spawn(async move { - let account = get_account_or_default(req.account.as_ref(), &sdk.accounts_api())?; - - let max_fee = req.max_fee.unwrap_or(DEFAULT_FEE); - let amount_to_reveal = req.amount_to_reveal + - if req.pay_fee_from_reveal { - max_fee.into() - } else { - 0.into() - }; - - let lock_id = sdk.stealth_outputs_api().create_lock()?; - - let (inputs, input_amount) = sdk.stealth_outputs_api().lock_outputs_for_at_least_amount( - account.address(), - &STEALTH_TARI_RESOURCE_ADDRESS, - lock_id, - amount_to_reveal, - )?; - - let account_key = sdk.key_manager_api().derive_account_key(account.key_index())?; - let account_public_key = RistrettoPublicKey::from_secret_key(&account_key.key); - - let output_mask = sdk.key_manager_api().next_key(KeyBranch::StealthMasks)?; - - let (nonce, public_nonce) = RistrettoPublicKey::random_keypair(&mut OsRng); - let remaining_confidential_amount = input_amount - amount_to_reveal; - let encrypted_data = sdk.stealth_crypto_api().encrypt_value_and_mask( - remaining_confidential_amount.to_u64_checked().unwrap(), - &output_mask.key, - &public_nonce, - &account_key.key, - )?; - - let network = sdk.config_api().get_network()?; - let tag = sdk - .stealth_crypto_api() - .derive_stealth_output_tag(network, &account.owner_public_key); - let stealth_address = - sdk.stealth_crypto_api() - .derive_stealth_owner_public_key(network, &account_public_key, &nonce); - - let output_statement = UnblindedStealthOutputStatement { - statement: UnblindedOutputStatement { - amount: remaining_confidential_amount, - mask: output_mask.key, - sender_public_nonce: public_nonce, - minimum_value_promise: 0, - encrypted_data, - resource_view_key: None, - }, - output_owner_public_key: stealth_address, - tag, - }; - - let inputs = sdk - .stealth_outputs_api() - .resolve_output_masks_for_spending(account.account(), inputs)?; - - let transfer = sdk.stealth_crypto_api().generate_transfer_statement( - inputs.iter().map(|i| &i.statement), - Amount::zero(), - array::from_ref(&output_statement), - amount_to_reveal, - )?; - - info!( - target: LOG_TARGET, - "Locked {} inputs ({}) for reveal funds transaction on account: {}", - inputs.len(), - input_amount, - account, - ); - - let account_address = *account.address(); - - let mut builder = ctx.transaction_builder(); - if req.pay_fee_from_reveal { - builder = builder.with_fee_instructions_builder(|builder| { - builder - .stealth_transfer(STEALTH_TARI_RESOURCE_ADDRESS, transfer) - .put_last_instruction_output_on_workspace("revealed") - .call_method(account_address, "deposit", args![Workspace("revealed")]) - .call_method(account_address, "pay_fee", args![max_fee]) - }); - } else { - builder = builder - .fee_transaction_pay_from_component(account_address, max_fee) - .stealth_transfer(STEALTH_TARI_RESOURCE_ADDRESS, transfer) - .put_last_instruction_output_on_workspace("revealed") - .call_method(account_address, "deposit", args![Workspace("revealed")]); - } - - // Add the account component - let account_substate = sdk.substate_api().get_substate(&account.account.address.into())?; - // Add all versioned account child addresses as inputs - let child_addresses = sdk - .substate_api() - .load_dependent_substates(&[&account.account.address.into()])?; - let mut inputs = Vec::with_capacity(child_addresses.len() + 1); - inputs.push(SubstateRequirement::from(account_substate.substate_id)); - inputs.extend(child_addresses); - - let transaction = builder.with_inputs(inputs).build_and_seal(&account_key.key); - - sdk.stealth_outputs_api() - .locks_set_transaction_id(lock_id, transaction.calculate_id())?; - - let mut events = notifier.subscribe(); - let tx_id = transaction_service.submit_transaction(transaction).await?; - - let finalized = wait_for_result(&mut events, tx_id).await?; - if let Some(reason) = finalized.finalize.fee_reject() { - return Err(anyhow::anyhow!("Transaction failed: {}", reason)); - } - - Ok(RevealFundsResponse { - transaction_id: tx_id, - fee: finalized.final_fee, - result: finalized.finalize, - }) - }) - .await? -} - #[allow(clippy::too_many_lines)] pub async fn handle_claim_burn( context: &HandlerContext, @@ -629,9 +488,12 @@ pub async fn handle_claim_burn( &nonce, )?; - let tag = sdk - .stealth_crypto_api() - .derive_stealth_output_tag(network, &account.owner_public_key); + let tag = sdk.stealth_crypto_api().derive_stealth_output_tag_for_recipient( + network, + &nonce, + &account_owner_public_key, + &STEALTH_TARI_RESOURCE_ADDRESS, + ); // Create stealth address - used during spend time let stealth_output_owner_public_key = diff --git a/applications/tari_walletd/src/handlers/confidential.rs b/applications/tari_walletd/src/handlers/confidential.rs index ff23bd8d80..f2c5f9818a 100644 --- a/applications/tari_walletd/src/handlers/confidential.rs +++ b/applications/tari_walletd/src/handlers/confidential.rs @@ -281,7 +281,9 @@ pub async fn handle_view_vault_balance( .ok_or_else(|| invalid_params("vault_id", Some("Vault does not contain a confidential resource")))?; // Get view secret key - let view_key = sdk.key_manager_api().derive_key(KeyBranch::ViewKey, req.view_key_id)?; + let view_key = sdk + .key_manager_api() + .derive_key(KeyBranch::ElgamalEncryptionViewKey, req.view_key_id)?; let value_range = req.minimum_expected_value.unwrap_or(0)..=req.maximum_expected_value.unwrap_or(10_000_000_000); diff --git a/applications/tari_walletd/src/handlers/settings.rs b/applications/tari_walletd/src/handlers/settings.rs index d1e196e1ad..87af1e2869 100644 --- a/applications/tari_walletd/src/handlers/settings.rs +++ b/applications/tari_walletd/src/handlers/settings.rs @@ -39,9 +39,9 @@ pub async fn handle_set( token: Option<&Bearer>, req: SettingsSetRequest, ) -> Result { - let mut sdk = context.wallet_sdk().clone(); + let sdk = context.wallet_sdk(); context.check_auth(token, &[JrpcPermission::Admin])?; - sdk.get_network_interface_mut().set_endpoint(&req.indexer_url)?; + sdk.get_network_interface().set_endpoint(&req.indexer_url)?; sdk.config_api().set(ConfigKey::IndexerUrl, &req.indexer_url, false)?; Ok(SettingsSetResponse {}) } diff --git a/applications/tari_walletd/src/jrpc_server.rs b/applications/tari_walletd/src/jrpc_server.rs index 781222fe22..265c991a86 100644 --- a/applications/tari_walletd/src/jrpc_server.rs +++ b/applications/tari_walletd/src/jrpc_server.rs @@ -126,7 +126,6 @@ async fn handler( _ => Ok(value.method_not_found(&value.method)), }, Some(("accounts", method)) => match method { - "reveal_funds" => call_handler(context, value, token, accounts::handle_reveal_funds).await, "claim_burn" => call_handler(context, value, token, accounts::handle_claim_burn).await, "create" => call_handler(context, value, token, accounts::handle_create).await, "create_or_get" => call_handler(context, value, token, accounts::handle_create_or_get).await, diff --git a/applications/tari_walletd/src/lib.rs b/applications/tari_walletd/src/lib.rs index 8a6e72df13..28f6722e5c 100644 --- a/applications/tari_walletd/src/lib.rs +++ b/applications/tari_walletd/src/lib.rs @@ -30,7 +30,7 @@ mod notify; mod services; mod webrtc; -use std::{fs, panic, process}; +use std::{fs, panic, pin, process}; use log::*; use tari_common_types::seeds::seed_words::SeedWords; @@ -89,15 +89,18 @@ pub async fn run_tari_ootle_walletd( let notify = Notify::new(100); let services = spawn_services(shutdown_signal.clone(), notify.clone(), wallet_sdk.clone()); - // trigger resource scanning if needed + // trigger account scanning if needed if needs_seed_recovery { - let scanner = recovery_service::Service::new( + let scanner = recovery_service::AccountRecoveryService::new( wallet_sdk.clone(), services.account_monitor_handle.clone(), config.ootle_wallet_daemon.recovery_abandon_count, - shutdown_signal.clone(), ); - tokio::spawn(scanner.scan()); + let shutdown_signal = shutdown_signal.clone(); + tokio::spawn(async move { + let scan_pinned = pin::pin!(scanner.scan()); + shutdown_signal.select(scan_pinned).await; + }); } let jrpc_address = config.ootle_wallet_daemon.json_rpc_address.unwrap(); @@ -156,7 +159,15 @@ pub async fn run_tari_ootle_walletd( res??; }, res = services.services_fut => { - res?; + match res { + Ok(_) => { + info!(target: LOG_TARGET, "All services have shut down"); + }, + Err(err) => { + error!(target: LOG_TARGET, "🚨 A service has crashed: {}. Shutting down", err); + return Err(err); + }, + } }, } Ok(()) diff --git a/applications/tari_walletd/src/services/account_monitor.rs b/applications/tari_walletd/src/services/account_monitor.rs index 387e849230..e0ca904aa7 100644 --- a/applications/tari_walletd/src/services/account_monitor.rs +++ b/applications/tari_walletd/src/services/account_monitor.rs @@ -639,7 +639,8 @@ where }; let Some(account_addr) = vault_substate.parent_address else { - warn!(target: LOG_TARGET, "👁️‍🗨️ Vault {} has no parent component.", vault_addr); + // Happens if this is someone else's vault + debug!(target: LOG_TARGET, "👁️‍🗨️ Vault {} has no parent component.", vault_addr); continue; }; let account_addr = account_addr.as_component_address().unwrap_or_else(|| { diff --git a/applications/tari_walletd/src/services/mod.rs b/applications/tari_walletd/src/services/mod.rs index 45b22de9df..5c84a42faf 100644 --- a/applications/tari_walletd/src/services/mod.rs +++ b/applications/tari_walletd/src/services/mod.rs @@ -27,6 +27,7 @@ use tari_ootle_wallet_sdk::{ storage::WalletStore, WalletSdk, }; +use tari_ootle_wallet_sdk_services::utxo_scanner::UtxoRecovery; use tari_shutdown::ShutdownSignal; use tokio::{sync::oneshot, task::JoinHandle}; use transaction_service::TransactionService; @@ -63,6 +64,12 @@ where let utxo_scanner = StealthUtxoScannerWorker::new(wallet_sdk.clone()); let (utxo_scanner_join_handle, utxo_scanner_handle) = utxo_scanner.spawn(); + let utxo_recovery_join_handle = { + let sdk = wallet_sdk.clone(); + let notify_sub = utxo_scanner_handle.subscribe_notifications(); + tokio::spawn(UtxoRecovery::new(sdk).run(notify_sub)) + }; + let (account_monitor, account_monitor_handle) = AccountMonitor::new(notify, wallet_sdk, utxo_scanner_handle, shutdown_signal); let account_monitor_join_handle = tokio::spawn(account_monitor.run()); @@ -75,6 +82,7 @@ where account_monitor_join_handle, template_monitor_join_handle, utxo_scanner_join_handle, + utxo_recovery_join_handle, ]) .boxed(), } diff --git a/applications/tari_walletd/src/services/recovery_service.rs b/applications/tari_walletd/src/services/recovery_service.rs index 2bc8515f81..a578c9ab74 100644 --- a/applications/tari_walletd/src/services/recovery_service.rs +++ b/applications/tari_walletd/src/services/recovery_service.rs @@ -1,12 +1,13 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::{collections::HashMap, time::Duration}; +use std::time::Duration; -use log::{error, info, warn}; +use log::*; use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; use tari_engine_types::{component::derive_component_address_from_public_key, ToByteType}; use tari_ootle_common_types::{ + displayable::Displayable, optional::{IsNotFoundError, Optional}, substate_type::SubstateType, }; @@ -20,24 +21,22 @@ use tari_ootle_wallet_sdk::{ storage::{WalletStorageError, WalletStore}, WalletSdk, }; -use tari_shutdown::ShutdownSignal; use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS; -use tari_template_lib::prelude::RistrettoPublicKeyBytes; use tari_transaction_components::key_manager::tari_key_manager::DerivedKey; +use tokio::time; use crate::services::{account_monitor::AccountMonitorError, AccountMonitorHandle}; const LOG_TARGET: &str = "tari::ootle_wallet_daemon::resource_scanner"; /// Scans through all the substates to find related resources to current wallet. -pub struct Service { +pub struct AccountRecoveryService { wallet_sdk: WalletSdk, account_monitor_handle: AccountMonitorHandle, abandon_after_not_found: usize, - shutdown_signal: ShutdownSignal, } -impl Service +impl AccountRecoveryService where TStore: WalletStore, TNetworkInterface: WalletNetworkInterface, @@ -47,13 +46,11 @@ where wallet_sdk: WalletSdk, account_monitor_handle: AccountMonitorHandle, abandon_after_not_found: usize, - shutdown_signal: ShutdownSignal, ) -> Self { Self { wallet_sdk, account_monitor_handle, abandon_after_not_found, - shutdown_signal, } } @@ -70,16 +67,7 @@ where warn!(target: LOG_TARGET, "Indexer is not ready yet: {error:?}"); }, } - if self.shutdown_signal.is_triggered() { - info!(target: LOG_TARGET, "Shutdown signal received. Stopping scan."); - break; - } - tokio::time::sleep(Duration::from_secs(5)).await; - } - - if self.shutdown_signal.is_triggered() { - info!(target: LOG_TARGET, "Shutdown signal received. Stopping scan."); - return; + time::sleep(Duration::from_secs(5)).await; } info!(target: LOG_TARGET, "🔑 Attempting to recover accounts..."); @@ -88,7 +76,6 @@ where let key_manager_api = self.wallet_sdk.key_manager_api(); let mut not_found_accounts_count = 0; let mut found_accounts_count = 0; - let mut owner_key_cache = HashMap::new(); let initial_key_index = match key_manager_api.get_active_key(KeyBranch::Account) { Ok((key_index, _)) => key_index, Err(err) => { @@ -106,14 +93,14 @@ where }, }; info!(target: LOG_TARGET, "🔍️ Attempting to recover account with key index {}", key.key_index); - match self.try_recover_account(&key, &mut owner_key_cache).await.optional() { - Ok(Some(())) => { + match self.try_recover_account(&key).await { + Ok(true) => { last_found_key = Some(key.key_index); info!(target: LOG_TARGET, "✅ Account with key index {} found!", key.key_index); not_found_accounts_count = 0; found_accounts_count += 1; }, - Ok(None) => { + Ok(false) => { not_found_accounts_count += 1; }, Err(err) => { @@ -160,14 +147,10 @@ where info!(target: LOG_TARGET, "✅ Scanning accounts finished! {found_accounts_count} owned account(s) found!"); } - /// Attempt to recover an account by the provided public key. - async fn try_recover_account( - &self, - key: &DerivedKey, - owner_key_cache: &mut HashMap, - ) -> Result<(), AccountScannerError> { + /// Attempt to recover an account by the provided public key. Returning true if the account was found on-chain, + /// false if not. + async fn try_recover_account(&self, key: &DerivedKey) -> Result { let network_interface = self.wallet_sdk.get_network_interface(); - let accounts_api = self.wallet_sdk.accounts_api(); let public_key = RistrettoPublicKey::from_secret_key(&key.key).to_byte_type(); // Derive the account address from the public key @@ -179,64 +162,76 @@ where .query_substate(&account_addr.into(), None, false) .await .optional() - .map_err(|e| AccountScannerError::NetworkInterfaceError { details: e.to_string() })? - .ok_or(AccountScannerError::AccountNotFound)?; - - let component = result - .substate - .as_component() - .ok_or_else(|| AccountScannerError::InvariantError { - details: format!( - "Expected component substate for address {account_addr}, got {}", - SubstateType::from(&result.substate) - ), - })?; - - let Some(owner_public_key) = component.owner_key else { - info!(target: LOG_TARGET, "Account {} has no owner key", account_addr); - return Ok(()); - }; - - let key_index = component - .owner_key - .and_then(|owner_public_key| { - if public_key == owner_public_key { - // Cache this in case we have another account that uses this key - owner_key_cache.insert(owner_public_key, key.key_index); - Some(key.key_index) - } else { - Some(*owner_key_cache.get(&owner_public_key)?) - } - }) - .unwrap_or_else(|| { - warn!( + .map_err(|e| AccountScannerError::NetworkInterfaceError { details: e.to_string() })?; + + match result { + None => { + info!(target: LOG_TARGET, "🔑 Account {} not found on chain. It may have stealth UTXOs owned by its key", account_addr); + + // We cannot find this account on chain, however there could be UTXOs owned by this key which we'll need + // to scan for. + self.wallet_sdk.accounts_api().add_account( + Some(format!("recovered-account-{}", key.key_index).as_str()), + &account_addr, + key.key_index, + false, + // if this is the first account, set it as the default + key.key_index == 0, + )?; + + // Update UTXOs + self.account_monitor_handle.refresh_account(account_addr).await?; + // Count this as not found for the purposes of stopping the scan after N not founds + Ok(false) + }, + Some(result) => { + let component = result + .substate + .as_component() + .ok_or_else(|| AccountScannerError::InvalidResponse { + details: format!( + "Expected component substate for address {account_addr}, got {}", + SubstateType::from(&result.substate) + ), + })?; + + if component.owner_key.is_none() { + warn!(target: LOG_TARGET, "⚠️ Account {} has no owner key. This wallet may not be able tio sign for this account", account_addr); + }; + + if component.owner_key.is_some_and(|pk| pk != public_key) { + warn!( + target: LOG_TARGET, + "⚠️ Account {} has a different owner key {} than the one derived from the seed key {}. This wallet may not be able to sign for this account", + account_addr, + component.owner_key.unwrap_or_default(), + public_key + ); + }; + + // add account + info!( target: LOG_TARGET, - "⚠️ Account {} has a different owner key {} than the one derived from the seed key {}. Signing a transaction with this key index may not work.", + "🔑 Adding account {} with owner key {} and key index {}", account_addr, - owner_public_key, - public_key + component.owner_key.display(), + key.key_index ); - - // TODO: this key index may not actually be the correct owner key - there is no guaranteed way to - // find the key index from the public key. We'll need to find another way to do - // this. - key.key_index - }); - - // add account - let any_exist = accounts_api.any_accounts_exist()?; - self.wallet_sdk.accounts_api().add_account( - Some(format!("account-{}", key.key_index).as_str()), - &account_addr, - key_index, - true, - !any_exist, - )?; - - // Update vaults, confidential outputs, nfts etc - self.account_monitor_handle.refresh_account(account_addr).await?; - - Ok(()) + self.wallet_sdk.accounts_api().add_account( + Some(format!("recovered-account-{}", key.key_index).as_str()), + &account_addr, + key.key_index, + true, + // if this is the first account, set it as the default + key.key_index == 0, + )?; + + // Update vaults, UTXOs, nfts etc + self.account_monitor_handle.refresh_account(account_addr).await?; + + Ok(true) + }, + } } } @@ -250,16 +245,8 @@ pub enum AccountScannerError { AccountsApiError(#[from] AccountsApiError), #[error("Network interface error: {details}")] NetworkInterfaceError { details: String }, - #[error("Account not found")] - AccountNotFound, #[error("Account monitor error: {0}")] AccountMonitorError(#[from] AccountMonitorError), - #[error("Invariant error: {details}")] - InvariantError { details: String }, -} - -impl IsNotFoundError for AccountScannerError { - fn is_not_found_error(&self) -> bool { - matches!(self, AccountScannerError::AccountNotFound) - } + #[error("Invalid response: {details}")] + InvalidResponse { details: String }, } diff --git a/applications/tari_walletd/src/services/stealth_utxo_scanner.rs b/applications/tari_walletd/src/services/stealth_utxo_scanner.rs index 2f930746d9..d98c60efe5 100644 --- a/applications/tari_walletd/src/services/stealth_utxo_scanner.rs +++ b/applications/tari_walletd/src/services/stealth_utxo_scanner.rs @@ -16,7 +16,10 @@ use tari_ootle_wallet_sdk::{ WalletSdk, }; use tari_template_lib::{models::ComponentAddress, prelude::ResourceAddress}; -use tokio::{sync::mpsc, task::JoinHandle}; +use tokio::{ + sync::{mpsc, watch}, + task::JoinHandle, +}; const LOG_TARGET: &str = "tari::ootle::wallet_daemon::stealth_utxo_scanner"; @@ -24,6 +27,7 @@ const MAX_CONCURRENT_SCANS: usize = 10; pub struct UtxoScannerHandle { tx: mpsc::UnboundedSender, + notify_sub: watch::Receiver<()>, } impl UtxoScannerHandle { @@ -35,6 +39,10 @@ impl UtxoScannerHandle { warn!(target: LOG_TARGET, "❓️ NEVER HAPPEN: UTXO scan request channel disconnected: {}", e); } } + + pub(crate) fn subscribe_notifications(&self) -> watch::Receiver<()> { + self.notify_sub.clone() + } } pub struct StealthUtxoScannerWorker { @@ -49,12 +57,13 @@ where { pub fn new(sdk: WalletSdk) -> Self { Self { - scanner: StealthUtxoScanner::new(sdk), + scanner: StealthUtxoScanner::new(sdk.clone()), } } pub fn spawn(self) -> (JoinHandle>, UtxoScannerHandle) { let (tx, rx) = mpsc::unbounded_channel(); + let notify_sub = self.scanner.subscribe_notifications(); let handle = tokio::spawn(async move { let mut worker = self; @@ -62,7 +71,7 @@ where Ok(()) }); - (handle, UtxoScannerHandle { tx }) + (handle, UtxoScannerHandle { tx, notify_sub }) } async fn run(&mut self, mut work_queue: mpsc::UnboundedReceiver) { @@ -80,7 +89,7 @@ where }; }, _ = poll_fut => { - // Poll completed - continue + // All work completed - continue }, } } @@ -94,6 +103,7 @@ type ScanResult = anyhow::Result<()>; pub struct StealthUtxoScanner { in_progress_work: futures_bounded::FuturesMap, sdk: WalletSdk, + notify_tx: watch::Sender<()>, } impl StealthUtxoScanner @@ -103,12 +113,18 @@ where TNetworkInterface::Error: IsNotFoundError + StatusResponseError, { pub(self) fn new(sdk: WalletSdk) -> Self { + let (notify_tx, _) = watch::channel::<()>(()); Self { in_progress_work: futures_bounded::FuturesMap::new(Duration::from_secs(300), MAX_CONCURRENT_SCANS), sdk, + notify_tx, } } + pub fn subscribe_notifications(&self) -> watch::Receiver<()> { + self.notify_tx.subscribe() + } + pub(self) fn enqueue_work(&mut self, task: UtxoScanRequest) { info!(target: LOG_TARGET, "🔍️ Received scan request for {}", task); @@ -116,7 +132,10 @@ where info!(target: LOG_TARGET, "🔍️ Scan for {} is already in progress, ignoring request", task); return; } - match self.in_progress_work.try_push(task, do_work(self.sdk.clone(), task)) { + match self + .in_progress_work + .try_push(task, do_work(self.sdk.clone(), self.notify_tx.clone(), task)) + { Ok(()) => {}, Err(PushError::BeyondCapacity(_)) => { warn!( @@ -133,14 +152,26 @@ where pub(self) fn poll(&mut self, cx: &mut Context<'_>) -> Poll<()> { let (task, result) = ready!(self.in_progress_work.poll_unpin(cx)); - info!(target: LOG_TARGET, "🔍️ Completed scan for {}: {:?}", task, result); - + match result { + Ok(Ok(_)) => { + info!(target: LOG_TARGET, "🔍️ Completed scan for {}", task); + }, + Ok(Err(e)) => { + warn!(target: LOG_TARGET, "❓️ Error during UTXO scan for {}: {}", task, e); + }, + Err(_) => { + warn!(target: LOG_TARGET, "❓️ UTXO scan for {} timed out", task); + }, + } + // NOTE: do not return Ready here. The caller is polling in a loop, and if there is no work to do, the loop will + // spin. Poll::Pending } } async fn do_work( sdk: WalletSdk, + notify_tx: watch::Sender<()>, task: UtxoScanRequest, ) -> ScanResult where @@ -150,8 +181,8 @@ where { info!(target: LOG_TARGET, "🔍 Scanning for UTXOs for {}", task); let account = sdk.accounts_api().get_account_by_address(&task.account_address)?; - sdk.stealth_scanner_api() - .scan_and_recover_utxos(&account, &task.resource_address) + tari_ootle_wallet_sdk_services::utxo_scanner::UtxoScanner::new(sdk) + .scan_and_recover_utxos(&account, &task.resource_address, ¬ify_tx) .await?; Ok(()) diff --git a/applications/tari_walletd/web_ui/src/Components/Accordion.tsx b/applications/tari_walletd/web_ui/src/components/Accordion.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/Accordion.tsx rename to applications/tari_walletd/web_ui/src/components/Accordion.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/AlertDialog.tsx b/applications/tari_walletd/web_ui/src/components/AlertDialog.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/AlertDialog.tsx rename to applications/tari_walletd/web_ui/src/components/AlertDialog.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/Breadcrumbs.tsx b/applications/tari_walletd/web_ui/src/components/Breadcrumbs.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/Breadcrumbs.tsx rename to applications/tari_walletd/web_ui/src/components/Breadcrumbs.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/CodeBlock.tsx b/applications/tari_walletd/web_ui/src/components/CodeBlock.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/CodeBlock.tsx rename to applications/tari_walletd/web_ui/src/components/CodeBlock.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/ConnectorLink/CheckMark.css b/applications/tari_walletd/web_ui/src/components/ConnectorLink/CheckMark.css similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/ConnectorLink/CheckMark.css rename to applications/tari_walletd/web_ui/src/components/ConnectorLink/CheckMark.css diff --git a/applications/tari_walletd/web_ui/src/Components/ConnectorLink/CheckMark.tsx b/applications/tari_walletd/web_ui/src/components/ConnectorLink/CheckMark.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/ConnectorLink/CheckMark.tsx rename to applications/tari_walletd/web_ui/src/components/ConnectorLink/CheckMark.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/ConnectorLink/ConfirmTransaction.tsx b/applications/tari_walletd/web_ui/src/components/ConnectorLink/ConfirmTransaction.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/ConnectorLink/ConfirmTransaction.tsx rename to applications/tari_walletd/web_ui/src/components/ConnectorLink/ConfirmTransaction.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/ConnectorLink/ConnectorLink.css b/applications/tari_walletd/web_ui/src/components/ConnectorLink/ConnectorLink.css similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/ConnectorLink/ConnectorLink.css rename to applications/tari_walletd/web_ui/src/components/ConnectorLink/ConnectorLink.css diff --git a/applications/tari_walletd/web_ui/src/Components/ConnectorLink/ConnectorLink.tsx b/applications/tari_walletd/web_ui/src/components/ConnectorLink/ConnectorLink.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/ConnectorLink/ConnectorLink.tsx rename to applications/tari_walletd/web_ui/src/components/ConnectorLink/ConnectorLink.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/ConnectorLink/ConnectorLogo.tsx b/applications/tari_walletd/web_ui/src/components/ConnectorLink/ConnectorLogo.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/ConnectorLink/ConnectorLogo.tsx rename to applications/tari_walletd/web_ui/src/components/ConnectorLink/ConnectorLogo.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/ConnectorLink/Permissions.css b/applications/tari_walletd/web_ui/src/components/ConnectorLink/Permissions.css similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/ConnectorLink/Permissions.css rename to applications/tari_walletd/web_ui/src/components/ConnectorLink/Permissions.css diff --git a/applications/tari_walletd/web_ui/src/Components/ConnectorLink/Permissions.tsx b/applications/tari_walletd/web_ui/src/components/ConnectorLink/Permissions.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/ConnectorLink/Permissions.tsx rename to applications/tari_walletd/web_ui/src/components/ConnectorLink/Permissions.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/ConnectorLink/index.ts b/applications/tari_walletd/web_ui/src/components/ConnectorLink/index.ts similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/ConnectorLink/index.ts rename to applications/tari_walletd/web_ui/src/components/ConnectorLink/index.ts diff --git a/applications/tari_walletd/web_ui/src/Components/CopyAddress.tsx b/applications/tari_walletd/web_ui/src/components/CopyAddress.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/CopyAddress.tsx rename to applications/tari_walletd/web_ui/src/components/CopyAddress.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/CopyToClipboard.tsx b/applications/tari_walletd/web_ui/src/components/CopyToClipboard.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/CopyToClipboard.tsx rename to applications/tari_walletd/web_ui/src/components/CopyToClipboard.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/Copyright.tsx b/applications/tari_walletd/web_ui/src/components/Copyright.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/Copyright.tsx rename to applications/tari_walletd/web_ui/src/components/Copyright.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/Error.tsx b/applications/tari_walletd/web_ui/src/components/Error.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/Error.tsx rename to applications/tari_walletd/web_ui/src/components/Error.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/FetchStatusCheck.tsx b/applications/tari_walletd/web_ui/src/components/FetchStatusCheck.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/FetchStatusCheck.tsx rename to applications/tari_walletd/web_ui/src/components/FetchStatusCheck.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/HeadingMenu.tsx b/applications/tari_walletd/web_ui/src/components/HeadingMenu.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/HeadingMenu.tsx rename to applications/tari_walletd/web_ui/src/components/HeadingMenu.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/JsonTooltip.tsx b/applications/tari_walletd/web_ui/src/components/JsonTooltip.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/JsonTooltip.tsx rename to applications/tari_walletd/web_ui/src/components/JsonTooltip.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/Loading.tsx b/applications/tari_walletd/web_ui/src/components/Loading.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/Loading.tsx rename to applications/tari_walletd/web_ui/src/components/Loading.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/MenuItems.tsx b/applications/tari_walletd/web_ui/src/components/MenuItems.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/MenuItems.tsx rename to applications/tari_walletd/web_ui/src/components/MenuItems.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/NFTList.tsx b/applications/tari_walletd/web_ui/src/components/NFTList.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/NFTList.tsx rename to applications/tari_walletd/web_ui/src/components/NFTList.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/PageHeading.tsx b/applications/tari_walletd/web_ui/src/components/PageHeading.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/PageHeading.tsx rename to applications/tari_walletd/web_ui/src/components/PageHeading.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/SearchFilter.tsx b/applications/tari_walletd/web_ui/src/components/SearchFilter.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/SearchFilter.tsx rename to applications/tari_walletd/web_ui/src/components/SearchFilter.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/SecondaryHeading.tsx b/applications/tari_walletd/web_ui/src/components/SecondaryHeading.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/SecondaryHeading.tsx rename to applications/tari_walletd/web_ui/src/components/SecondaryHeading.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/StatusChip.tsx b/applications/tari_walletd/web_ui/src/components/StatusChip.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/StatusChip.tsx rename to applications/tari_walletd/web_ui/src/components/StatusChip.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/Stepper.tsx b/applications/tari_walletd/web_ui/src/components/Stepper.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/Stepper.tsx rename to applications/tari_walletd/web_ui/src/components/Stepper.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/StyledComponents.ts b/applications/tari_walletd/web_ui/src/components/StyledComponents.ts similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/StyledComponents.ts rename to applications/tari_walletd/web_ui/src/components/StyledComponents.ts diff --git a/applications/tari_walletd/web_ui/src/Components/ThemeSwitcher.tsx b/applications/tari_walletd/web_ui/src/components/ThemeSwitcher.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/ThemeSwitcher.tsx rename to applications/tari_walletd/web_ui/src/components/ThemeSwitcher.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/Title.tsx b/applications/tari_walletd/web_ui/src/components/Title.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/Title.tsx rename to applications/tari_walletd/web_ui/src/components/Title.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/WalletConnectLink/CheckMark.css b/applications/tari_walletd/web_ui/src/components/WalletConnectLink/CheckMark.css similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/WalletConnectLink/CheckMark.css rename to applications/tari_walletd/web_ui/src/components/WalletConnectLink/CheckMark.css diff --git a/applications/tari_walletd/web_ui/src/Components/WalletConnectLink/CheckMark.tsx b/applications/tari_walletd/web_ui/src/components/WalletConnectLink/CheckMark.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/WalletConnectLink/CheckMark.tsx rename to applications/tari_walletd/web_ui/src/components/WalletConnectLink/CheckMark.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/WalletConnectLink/ConfirmTransaction.tsx b/applications/tari_walletd/web_ui/src/components/WalletConnectLink/ConfirmTransaction.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/WalletConnectLink/ConfirmTransaction.tsx rename to applications/tari_walletd/web_ui/src/components/WalletConnectLink/ConfirmTransaction.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/WalletConnectLink/ConnectorLink.css b/applications/tari_walletd/web_ui/src/components/WalletConnectLink/ConnectorLink.css similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/WalletConnectLink/ConnectorLink.css rename to applications/tari_walletd/web_ui/src/components/WalletConnectLink/ConnectorLink.css diff --git a/applications/tari_walletd/web_ui/src/Components/WalletConnectLink/ConnectorLogo.tsx b/applications/tari_walletd/web_ui/src/components/WalletConnectLink/ConnectorLogo.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/WalletConnectLink/ConnectorLogo.tsx rename to applications/tari_walletd/web_ui/src/components/WalletConnectLink/ConnectorLogo.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/WalletConnectLink/Permissions.css b/applications/tari_walletd/web_ui/src/components/WalletConnectLink/Permissions.css similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/WalletConnectLink/Permissions.css rename to applications/tari_walletd/web_ui/src/components/WalletConnectLink/Permissions.css diff --git a/applications/tari_walletd/web_ui/src/Components/WalletConnectLink/Permissions.tsx b/applications/tari_walletd/web_ui/src/components/WalletConnectLink/Permissions.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/WalletConnectLink/Permissions.tsx rename to applications/tari_walletd/web_ui/src/components/WalletConnectLink/Permissions.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/WalletConnectLink/WalletConnectLink.tsx b/applications/tari_walletd/web_ui/src/components/WalletConnectLink/WalletConnectLink.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/WalletConnectLink/WalletConnectLink.tsx rename to applications/tari_walletd/web_ui/src/components/WalletConnectLink/WalletConnectLink.tsx diff --git a/applications/tari_walletd/web_ui/src/Components/WalletConnectLink/index.ts b/applications/tari_walletd/web_ui/src/components/WalletConnectLink/index.ts similarity index 100% rename from applications/tari_walletd/web_ui/src/Components/WalletConnectLink/index.ts rename to applications/tari_walletd/web_ui/src/components/WalletConnectLink/index.ts 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 ef83c6fef2..c528213c1f 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 @@ -145,7 +145,7 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { const transfer = { account: substateIdToString(account.address), amount: Math.floor((parseFloat(transferFormState.amount) || 0) * Math.pow(10, balanceEntry?.divisibility || 6)), - resource_address: props.resource_address || XTR, + resource_address: props.resource_address!, destination_public_key: transferFormState.publicKey, resourceType: props.resource_type, output_to_revealed: !transferFormState.outputToConfidential, @@ -312,21 +312,6 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { setActiveStep(activeStep - 1); }; - // Auto-estimate fee when user enters valid public key and amount - useEffect(() => { - const { publicKey, amount } = transferFormState; - if (publicKey.trim() && publicKey.match(/^[0-9a-fA-F]+$/) && amount.trim()) { - // Small delay to let state update, then estimate fee - const timeoutId = setTimeout(() => { - estimateFee().catch(() => { - // Fee estimation failed, but don't block the user - }); - }, 500); - - return () => clearTimeout(timeoutId); - } - }, [transferFormState.publicKey, transferFormState.amount]); - const renderStepContent = () => { switch (activeStep) { case 0: diff --git a/applications/tari_walletd/web_ui/src/utils/json_rpc.ts b/applications/tari_walletd/web_ui/src/utils/json_rpc.ts index f55115cc61..c69b7b74d3 100644 --- a/applications/tari_walletd/web_ui/src/utils/json_rpc.ts +++ b/applications/tari_walletd/web_ui/src/utils/json_rpc.ts @@ -65,8 +65,6 @@ import type { MintFaucetNftResponse, PublishTemplateRequest, PublishTemplateResponse, - RevealFundsRequest, - RevealFundsResponse, SettingsGetResponse, SettingsSetRequest, SettingsSetResponse, @@ -267,8 +265,6 @@ export const transactionsSubmitManifest = ( ): Promise => client().then((c) => c.submitTransactionManifest(request)); // accounts -export const accountsRevealFunds = (request: RevealFundsRequest): Promise => - client().then((c) => c.accountsRevealFunds(request)); export const accountsClaimBurn = (request: ClaimBurnRequest): Promise => client().then((c) => c.accountsClaimBurn(request)); export const accountsCreate = (request: AccountsCreateRequest): Promise => diff --git a/bindings/src/helpers/helpers.ts b/bindings/src/helpers/helpers.ts index 5595440a5a..8af9760ae8 100644 --- a/bindings/src/helpers/helpers.ts +++ b/bindings/src/helpers/helpers.ts @@ -60,7 +60,7 @@ export function stringToSubstateId(substateId: string): SubstateId { return { Resource: rest }; case "vault": return { Vault: rest }; - case "commitment": + case "tombstone": return { UnclaimedConfidentialOutput: rest }; case "txreceipt": return { TransactionReceipt: rest }; diff --git a/bindings/src/index.ts b/bindings/src/index.ts index 8c9f3b6b3d..0edb114951 100644 --- a/bindings/src/index.ts +++ b/bindings/src/index.ts @@ -140,14 +140,15 @@ export * from "./types/UnsignedTransactionV1"; export * from "./types/UnspentOutput"; export * from "./types/UtxoAddressContents"; export * from "./types/UtxoAddress"; +export * from "./types/UtxoBurnt"; export * from "./types/UtxoId"; export * from "./types/UtxoOutput"; -export * from "./types/UtxoQueryResponse"; export * from "./types/UtxoSpent"; -export * from "./types/UtxoTagByte"; +export * from "./types/UtxoStateUpdateSet"; +export * from "./types/UtxoTag"; export * from "./types/Utxo"; export * from "./types/UtxoUnspent"; -export * from "./types/UtxoUpdate"; +export * from "./types/UtxoUpdateSet"; export * from "./types/ValidatorFeePoolAddress"; export * from "./types/ValidatorFeePool"; export * from "./types/ValidatorFeeWithdrawal"; @@ -160,6 +161,7 @@ export * from "./types/VersionedSubstateId"; export * from "./types/ViewableBalanceProof"; export * from "./types/VotePower"; export * from "./types/WalletTransaction"; +export * from "./types/WalletUtxoUpdate"; export * from "./types/WorkspaceOffsetId"; export * from "./base-node-client"; export * from "./tari-indexer-client"; diff --git a/bindings/src/tari-indexer-client.ts b/bindings/src/tari-indexer-client.ts index 530fcc17a2..f1fa3ef40a 100644 --- a/bindings/src/tari-indexer-client.ts +++ b/bindings/src/tari-indexer-client.ts @@ -4,6 +4,7 @@ export * from "./types/tari-indexer-client/IndexerSubmitTransactionResponse"; export * from "./types/tari-indexer-client/GetTemplateDefinitionRequest"; export * from "./types/tari-indexer-client/TemplateMetadata"; +export * from "./types/tari-indexer-client/GetUnspentUtxosResponse"; export * from "./types/tari-indexer-client/GetUtxoUpdatesRequest"; export * from "./types/tari-indexer-client/ListSubstatesResponse"; export * from "./types/tari-indexer-client/IndexerAddPeerResponse"; @@ -32,6 +33,7 @@ export * from "./types/tari-indexer-client/NonFungibleSubstate"; export * from "./types/tari-indexer-client/ListTemplatesRequest"; export * from "./types/tari-indexer-client/IndexerGetTransactionResultResponse"; export * from "./types/tari-indexer-client/IndexerConnectionDirection"; +export * from "./types/tari-indexer-client/GetUnspentUtxosRequest"; export * from "./types/tari-indexer-client/IndexerGetIdentityResponse"; export * from "./types/tari-indexer-client/IndexerGetTransactionResultRequest"; export * from "./types/tari-indexer-client/IndexerReadyResponse"; diff --git a/bindings/src/types/StealthUnspentOutput.ts b/bindings/src/types/StealthUnspentOutput.ts index 7730ae10b7..2208dec2e0 100644 --- a/bindings/src/types/StealthUnspentOutput.ts +++ b/bindings/src/types/StealthUnspentOutput.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { RistrettoPublicKeyBytes } from "./RistrettoPublicKeyBytes"; import type { UnspentOutput } from "./UnspentOutput"; -import type { UtxoTagByte } from "./UtxoTagByte"; +import type { UtxoTag } from "./UtxoTag"; export type StealthUnspentOutput = { output: UnspentOutput; @@ -10,5 +10,5 @@ export type StealthUnspentOutput = { * selected by the client. */ owner_public_key: RistrettoPublicKeyBytes; - tag: UtxoTagByte; + tag: UtxoTag; }; diff --git a/bindings/src/types/UtxoTagByte.ts b/bindings/src/types/UtxoBurnt.ts similarity index 53% rename from bindings/src/types/UtxoTagByte.ts rename to bindings/src/types/UtxoBurnt.ts index 89e0d0fab2..38f7f6e792 100644 --- a/bindings/src/types/UtxoTagByte.ts +++ b/bindings/src/types/UtxoBurnt.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 { UtxoId } from "./UtxoId"; -export type UtxoTagByte = number; +export type UtxoBurnt = { id: UtxoId; version: number }; diff --git a/bindings/src/types/UtxoOutput.ts b/bindings/src/types/UtxoOutput.ts index a52d3a42ce..c7f61d1c19 100644 --- a/bindings/src/types/UtxoOutput.ts +++ b/bindings/src/types/UtxoOutput.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { PrivateOutput } from "./PrivateOutput"; import type { RistrettoPublicKeyBytes } from "./RistrettoPublicKeyBytes"; -import type { UtxoTagByte } from "./UtxoTagByte"; +import type { UtxoTag } from "./UtxoTag"; export type UtxoOutput = { output: PrivateOutput; @@ -10,5 +10,5 @@ export type UtxoOutput = { * selected by the client. */ owner_public_key: RistrettoPublicKeyBytes; - tag: UtxoTagByte; + tag: UtxoTag; }; diff --git a/bindings/src/types/UtxoQueryResponse.ts b/bindings/src/types/UtxoQueryResponse.ts deleted file mode 100644 index 7a7a1e6949..0000000000 --- a/bindings/src/types/UtxoQueryResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Shard } from "./Shard"; -import type { StateVersion } from "./StateVersion"; -import type { UtxoUpdate } from "./UtxoUpdate"; - -export type UtxoQueryResponse = { updates: Array; per_shard_high_watermark: Array<[Shard, StateVersion]> }; diff --git a/bindings/src/types/UtxoSpent.ts b/bindings/src/types/UtxoSpent.ts index cac65a4ff7..4882d180b4 100644 --- a/bindings/src/types/UtxoSpent.ts +++ b/bindings/src/types/UtxoSpent.ts @@ -1,6 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Shard } from "./Shard"; -import type { StateVersion } from "./StateVersion"; -import type { UtxoAddress } from "./UtxoAddress"; +import type { UtxoId } from "./UtxoId"; -export type UtxoSpent = { address: UtxoAddress; version: number; shard: Shard; state_version: StateVersion }; +export type UtxoSpent = { id: UtxoId; version: number }; diff --git a/bindings/src/types/UtxoStateUpdateSet.ts b/bindings/src/types/UtxoStateUpdateSet.ts new file mode 100644 index 0000000000..584386f920 --- /dev/null +++ b/bindings/src/types/UtxoStateUpdateSet.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { StateVersion } from "./StateVersion"; +import type { WalletUtxoUpdate } from "./WalletUtxoUpdate"; + +export type UtxoStateUpdateSet = { updates: Array; max_state_version: StateVersion }; diff --git a/bindings/src/types/UtxoTag.ts b/bindings/src/types/UtxoTag.ts new file mode 100644 index 0000000000..d73769c573 --- /dev/null +++ b/bindings/src/types/UtxoTag.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * A 4-byte tag associated with a UTXO. This can be used to more quickly identify _possible_ ownership of a UTXO + * without needing to fully download and verify all UTXOs. + * 4 bytes gives a reasonable trade-off between initial download size and utxo size. + */ +export type UtxoTag = number; diff --git a/bindings/src/types/UtxoUnspent.ts b/bindings/src/types/UtxoUnspent.ts index 5df12a12b6..161a2638e1 100644 --- a/bindings/src/types/UtxoUnspent.ts +++ b/bindings/src/types/UtxoUnspent.ts @@ -1,13 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Shard } from "./Shard"; -import type { StateVersion } from "./StateVersion"; -import type { Utxo } from "./Utxo"; -import type { UtxoAddress } from "./UtxoAddress"; +import type { RistrettoPublicKeyBytes } from "./RistrettoPublicKeyBytes"; +import type { UtxoId } from "./UtxoId"; +import type { UtxoTag } from "./UtxoTag"; -export type UtxoUnspent = { - address: UtxoAddress; - version: number; - shard: Shard; - state_version: StateVersion; - utxo: Utxo; -}; +export type UtxoUnspent = { id: UtxoId; version: number; tag: UtxoTag; public_nonce: RistrettoPublicKeyBytes }; diff --git a/bindings/src/types/UtxoUpdateSet.ts b/bindings/src/types/UtxoUpdateSet.ts new file mode 100644 index 0000000000..a883a2116b --- /dev/null +++ b/bindings/src/types/UtxoUpdateSet.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Shard } from "./Shard"; +import type { StateVersion } from "./StateVersion"; +import type { UtxoStateUpdateSet } from "./UtxoStateUpdateSet"; + +export type UtxoUpdateSet = { + shard_updates: { [key in Shard]?: UtxoStateUpdateSet }; + per_shard_high_watermark: Array<[Shard, StateVersion]>; +}; diff --git a/bindings/src/types/UtxoUpdate.ts b/bindings/src/types/WalletUtxoUpdate.ts similarity index 58% rename from bindings/src/types/UtxoUpdate.ts rename to bindings/src/types/WalletUtxoUpdate.ts index 8dda6381e3..b1d0fb0b60 100644 --- a/bindings/src/types/UtxoUpdate.ts +++ b/bindings/src/types/WalletUtxoUpdate.ts @@ -1,5 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { UtxoBurnt } from "./UtxoBurnt"; import type { UtxoSpent } from "./UtxoSpent"; import type { UtxoUnspent } from "./UtxoUnspent"; -export type UtxoUpdate = { Unspent: UtxoUnspent } | { Spent: UtxoSpent }; +export type WalletUtxoUpdate = { Unspent: UtxoUnspent } | { Spent: UtxoSpent } | { Burnt: UtxoBurnt }; diff --git a/bindings/src/types/tari-indexer-client/GetUnspentUtxosRequest.ts b/bindings/src/types/tari-indexer-client/GetUnspentUtxosRequest.ts new file mode 100644 index 0000000000..ffc9520d64 --- /dev/null +++ b/bindings/src/types/tari-indexer-client/GetUnspentUtxosRequest.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PedersenCommitmentBytes } from "../PedersenCommitmentBytes"; +import type { ResourceAddress } from "../ResourceAddress"; + +export type GetUnspentUtxosRequest = { commitments: Array; resource_address: ResourceAddress }; diff --git a/bindings/src/types/tari-indexer-client/GetUnspentUtxosResponse.ts b/bindings/src/types/tari-indexer-client/GetUnspentUtxosResponse.ts new file mode 100644 index 0000000000..bad0be52ae --- /dev/null +++ b/bindings/src/types/tari-indexer-client/GetUnspentUtxosResponse.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Utxo } from "../Utxo"; +import type { UtxoId } from "../UtxoId"; + +export type GetUnspentUtxosResponse = { utxos: Array<[UtxoId, Utxo]> }; diff --git a/bindings/src/types/tari-indexer-client/GetUtxoUpdatesRequest.ts b/bindings/src/types/tari-indexer-client/GetUtxoUpdatesRequest.ts index 638298ec15..63a5e39cae 100644 --- a/bindings/src/types/tari-indexer-client/GetUtxoUpdatesRequest.ts +++ b/bindings/src/types/tari-indexer-client/GetUtxoUpdatesRequest.ts @@ -2,11 +2,9 @@ import type { ResourceAddress } from "../ResourceAddress"; import type { Shard } from "../Shard"; import type { StateVersion } from "../StateVersion"; -import type { UtxoTagByte } from "../UtxoTagByte"; export type GetUtxoUpdatesRequest = { shard_state_versions: Array<[Shard, StateVersion]>; - filter_tag_bytes: Array; resource_address: ResourceAddress; per_shard_limit: number; }; diff --git a/bindings/src/types/tari-indexer-client/GetUtxoUpdatesResponse.ts b/bindings/src/types/tari-indexer-client/GetUtxoUpdatesResponse.ts index 7f76000d9c..21a80623ad 100644 --- a/bindings/src/types/tari-indexer-client/GetUtxoUpdatesResponse.ts +++ b/bindings/src/types/tari-indexer-client/GetUtxoUpdatesResponse.ts @@ -1,12 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Shard } from "../Shard"; -import type { StateVersion } from "../StateVersion"; -import type { UtxoUpdate } from "../UtxoUpdate"; +import type { UtxoUpdateSet } from "../UtxoUpdateSet"; -export type GetUtxoUpdatesResponse = { - utxo_updates: Array; - /** - * Highest observed state_version per shard (may equal the request’s) - */ - per_shard_high_watermark: Array<[Shard, StateVersion]>; -}; +export type GetUtxoUpdatesResponse = { updates: UtxoUpdateSet }; diff --git a/bindings/src/types/wallet-daemon-client/KeyBranch.ts b/bindings/src/types/wallet-daemon-client/KeyBranch.ts index d4a9f775b9..145d9ddc5e 100644 --- a/bindings/src/types/wallet-daemon-client/KeyBranch.ts +++ b/bindings/src/types/wallet-daemon-client/KeyBranch.ts @@ -1,3 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type KeyBranch = "account" | "transaction" | "view_key" | "stealth_masks" | "confidential_masks" | "nonce"; +export type KeyBranch = + | "account" + | "transaction" + | "elgamal_encryption_view_key" + | "stealth_masks" + | "confidential_masks" + | "nonce" + | "view_key"; diff --git a/bindings/src/types/wallet-daemon-client/RevealFundsRequest.ts b/bindings/src/types/wallet-daemon-client/RevealFundsRequest.ts deleted file mode 100644 index 70c27b062f..0000000000 --- a/bindings/src/types/wallet-daemon-client/RevealFundsRequest.ts +++ /dev/null @@ -1,22 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Amount } from "../Amount"; -import type { ComponentAddressOrName } from "./ComponentAddressOrName"; - -export type RevealFundsRequest = { - /** - * Account with funds to reveal - */ - account: ComponentAddressOrName | null; - /** - * Amount to reveal - */ - amount_to_reveal: Amount; - /** - * Pay fee from revealed funds. If false, previously revealed funds in the account are used. - */ - pay_fee_from_reveal: boolean; - /** - * The amount of fees to add to the transaction. Any fees not charged are refunded. - */ - max_fee: number | null; -}; diff --git a/bindings/src/types/wallet-daemon-client/RevealFundsResponse.ts b/bindings/src/types/wallet-daemon-client/RevealFundsResponse.ts deleted file mode 100644 index d35ad754d6..0000000000 --- a/bindings/src/types/wallet-daemon-client/RevealFundsResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FinalizeResult } from "../FinalizeResult"; -import type { TransactionId } from "../TransactionId"; - -export type RevealFundsResponse = { transaction_id: TransactionId; fee: number; result: FinalizeResult }; diff --git a/bindings/src/wallet-daemon-client.ts b/bindings/src/wallet-daemon-client.ts index 8ec286ca2b..cce2abafd8 100644 --- a/bindings/src/wallet-daemon-client.ts +++ b/bindings/src/wallet-daemon-client.ts @@ -25,7 +25,6 @@ export * from "./types/wallet-daemon-client/ClaimBurnProof"; export * from "./types/wallet-daemon-client/TransactionGetAllRequest"; export * from "./types/wallet-daemon-client/WebauthnAlreadyRegisteredRequest"; export * from "./types/wallet-daemon-client/AuthMethod"; -export * from "./types/wallet-daemon-client/RevealFundsRequest"; export * from "./types/wallet-daemon-client/AuthLoginDenyResponse"; export * from "./types/wallet-daemon-client/MintFaucetNftRequest"; export * from "./types/wallet-daemon-client/AuthLoginDenyRequest"; @@ -64,7 +63,6 @@ export * from "./types/wallet-daemon-client/SubstatesGetResponse"; export * from "./types/wallet-daemon-client/AuthRevokeTokenRequest"; export * from "./types/wallet-daemon-client/ProofsGenerateRequest"; export * from "./types/wallet-daemon-client/MintFaucetNftResponse"; -export * from "./types/wallet-daemon-client/RevealFundsResponse"; export * from "./types/wallet-daemon-client/TemplatesListAuthoredRequest"; export * from "./types/wallet-daemon-client/AccountsListResponse"; export * from "./types/wallet-daemon-client/WebRtcStartResponse"; diff --git a/clients/javascript/wallet_daemon_client/package.json b/clients/javascript/wallet_daemon_client/package.json index 951d4c77f3..ec0cefe30e 100644 --- a/clients/javascript/wallet_daemon_client/package.json +++ b/clients/javascript/wallet_daemon_client/package.json @@ -15,7 +15,8 @@ }, "scripts": { "build": "tsc --project tsconfig.prod.json", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "clean-dist": "rm -rf dist && rm -f tsconfig.tsbuildinfo" }, "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/clients/javascript/wallet_daemon_client/src/index.ts b/clients/javascript/wallet_daemon_client/src/index.ts index cb786a83a1..4aa8d304d4 100644 --- a/clients/javascript/wallet_daemon_client/src/index.ts +++ b/clients/javascript/wallet_daemon_client/src/index.ts @@ -48,8 +48,6 @@ import type { PublishTemplateRequest, PublishTemplateResponse, rejectReasonToString, - RevealFundsRequest, - RevealFundsResponse, SettingsGetResponse, SettingsSetRequest, SettingsSetResponse, StealthTransferRequest, StealthTransferResponse, @@ -178,10 +176,6 @@ export class WalletDaemonClient { return this.__invokeRpc("accounts.associate_stealth_resource", params); } - public accountsRevealFunds(params: RevealFundsRequest): Promise { - return this.__invokeRpc("accounts.reveal_funds", params); - } - public accountsGetBalances(params: AccountsGetBalancesRequest): Promise { return this.__invokeRpc("accounts.get_balances", params); } diff --git a/clients/tari_indexer_client/Cargo.toml b/clients/tari_indexer_client/Cargo.toml index 9a4cbbb69d..95d17fed6c 100644 --- a/clients/tari_indexer_client/Cargo.toml +++ b/clients/tari_indexer_client/Cargo.toml @@ -16,6 +16,7 @@ tari_ootle_storage = { workspace = true } tari_template_abi = { workspace = true } tari_template_lib_types = { workspace = true } tari_consensus_types = { workspace = true } +tari_ootle_wallet_sdk = { workspace = true } anyhow = { workspace = true, optional = true } reqwest = { workspace = true, features = ["json"], optional = true } @@ -28,5 +29,5 @@ ts-rs = { workspace = true, optional = true } [features] default = ["client"] -ts = ["ts-rs"] +ts = ["ts-rs", "tari_template_lib_types/ts", "tari_engine_types/ts", "tari_ootle_common_types/ts", "tari_ootle_wallet_sdk/ts"] client = ["dep:reqwest", "dep:anyhow", "dep:thiserror"] diff --git a/clients/tari_indexer_client/src/json_rpc_client.rs b/clients/tari_indexer_client/src/json_rpc_client.rs index a0477af38d..1d7d8ade7b 100644 --- a/clients/tari_indexer_client/src/json_rpc_client.rs +++ b/clients/tari_indexer_client/src/json_rpc_client.rs @@ -39,6 +39,8 @@ use crate::{ GetTemplateDefinitionResponse, GetTransactionResultRequest, GetTransactionResultResponse, + GetUnspentUtxosRequest, + GetUnspentUtxosResponse, GetUtxoUpdatesRequest, GetUtxoUpdatesResponse, IndexerReadyResponse, @@ -141,6 +143,13 @@ impl IndexerJsonRpcClient { self.send_request("get_utxo_updates", req).await } + pub async fn get_unspent_utxos( + &mut self, + req: GetUnspentUtxosRequest, + ) -> Result { + self.send_request("get_unspent_utxos", req).await + } + pub async fn wait_until_ready(&mut self) -> Result { self.send_request("wait_until_ready", ()).await } diff --git a/clients/tari_indexer_client/src/types.rs b/clients/tari_indexer_client/src/types.rs index e3064be002..8834989b63 100644 --- a/clients/tari_indexer_client/src/types.rs +++ b/clients/tari_indexer_client/src/types.rs @@ -1,10 +1,7 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::{ - collections::{HashMap, HashSet}, - time::Duration, -}; +use std::{collections::HashMap, time::Duration}; use multiaddr::Multiaddr; use serde::{Deserialize, Serialize}; @@ -15,12 +12,15 @@ use tari_engine_types::{ commit_result::ExecuteResult, substate::{SubstateId, SubstateValue}, template_lib_models::{NonFungibleAddress, ResourceAddress}, + Utxo, + UtxoId, }; -use tari_ootle_common_types::{shard::Shard, substate_type::SubstateType, Epoch, StateVersion, UtxoUpdate}; +use tari_ootle_common_types::{shard::Shard, substate_type::SubstateType, Epoch, StateVersion}; use tari_ootle_storage::time::PrimitiveDateTime; +use tari_ootle_wallet_sdk::models::UtxoUpdateSet; use tari_template_abi::TemplateDef; use tari_template_lib_types::{ - crypto::{RistrettoPublicKeyBytes, UtxoTagByte}, + crypto::{RistrettoPublicKeyBytes, UtxoTag}, TemplateAddress, }; use tari_transaction::{Transaction, TransactionId}; @@ -360,7 +360,6 @@ pub struct GetUtxoUpdatesRequest { #[cfg_attr(feature = "ts", ts(as = "Vec<(Shard, StateVersion)>"))] #[serde_as(as = "Seq<(_, _)>")] pub shard_state_versions: HashMap, - pub filter_tag_bytes: HashSet, pub resource_address: ResourceAddress, pub per_shard_limit: u32, } @@ -368,7 +367,18 @@ pub struct GetUtxoUpdatesRequest { #[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "tari-indexer-client/"))] pub struct GetUtxoUpdatesResponse { - pub utxo_updates: Vec, - /// Highest observed state_version per shard (may equal the request’s) - pub per_shard_high_watermark: Vec<(Shard, StateVersion)>, + pub updates: UtxoUpdateSet, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "tari-indexer-client/"))] +pub struct GetUnspentUtxosRequest { + pub tag_and_nonce_pairs: Vec<(UtxoTag, RistrettoPublicKeyBytes)>, + pub resource_address: ResourceAddress, +} + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "tari-indexer-client/"))] +pub struct GetUnspentUtxosResponse { + pub utxos: Vec<(UtxoId, Utxo)>, } diff --git a/clients/wallet_daemon_client/src/lib.rs b/clients/wallet_daemon_client/src/lib.rs index ee6e5f892f..a6e004bac4 100644 --- a/clients/wallet_daemon_client/src/lib.rs +++ b/clients/wallet_daemon_client/src/lib.rs @@ -109,8 +109,6 @@ use crate::{ KeysSetActiveResponse, PublishTemplateRequest, PublishTemplateResponse, - RevealFundsRequest, - RevealFundsResponse, SettingsGetResponse, StealthTransferRequest, StealthTransferResponse, @@ -346,13 +344,6 @@ impl WalletDaemonClient { self.send_request("accounts.claim_burn", req.borrow()).await } - pub async fn accounts_reveal_funds>( - &mut self, - req: T, - ) -> Result { - self.send_request("accounts.reveal_funds", req.borrow()).await - } - pub async fn create_transfer_proof>( &mut self, req: T, diff --git a/clients/wallet_daemon_client/src/types.rs b/clients/wallet_daemon_client/src/types.rs index 6eff04bf28..96278cf8a1 100644 --- a/clients/wallet_daemon_client/src/types.rs +++ b/clients/wallet_daemon_client/src/types.rs @@ -611,30 +611,6 @@ pub struct ClaimBurnResponse { #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct ProofsCancelResponse {} -#[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] -pub struct RevealFundsRequest { - /// Account with funds to reveal - #[serde(deserialize_with = "opt_string_or_struct")] - pub account: Option, - /// Amount to reveal - pub amount_to_reveal: Amount, - /// Pay fee from revealed funds. If false, previously revealed funds in the account are used. - pub pay_fee_from_reveal: bool, - /// The amount of fees to add to the transaction. Any fees not charged are refunded. - #[cfg_attr(feature = "ts", ts(type = "number | null"))] - pub max_fee: Option, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] -pub struct RevealFundsResponse { - pub transaction_id: TransactionId, - #[cfg_attr(feature = "ts", ts(type = "number"))] - pub fee: u64, - pub result: FinalizeResult, -} - #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct AccountsCreateFreeTestCoinsRequest { diff --git a/config/logs.yml b/config/logs.yml index 735fc29022..8f3b49b3a6 100644 --- a/config/logs.yml +++ b/config/logs.yml @@ -35,7 +35,7 @@ appenders: count: 5 pattern: "log/wallet-daemon/ootle.{}.log" encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}" + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}" root: level: warn diff --git a/crates/common_types/src/lib.rs b/crates/common_types/src/lib.rs index b79c6f5794..4896e7379d 100644 --- a/crates/common_types/src/lib.rs +++ b/crates/common_types/src/lib.rs @@ -29,7 +29,6 @@ mod state_version; mod substate_address; pub mod substate_type; pub mod uint; -mod utxo_update; mod validator_metadata; mod versioned_substate_id; mod vote_power; @@ -51,7 +50,6 @@ pub use shard_state_versions::*; pub use state_version::*; pub use substate_address::*; pub use tari_engine_types::serde_with; -pub use utxo_update::*; pub use validator_metadata::*; pub use versioned_substate_id::*; pub use vote_power::*; diff --git a/crates/engine/src/runtime/impl.rs b/crates/engine/src/runtime/impl.rs index 41555c000b..fa02f1e866 100644 --- a/crates/engine/src/runtime/impl.rs +++ b/crates/engine/src/runtime/impl.rs @@ -123,7 +123,7 @@ use tari_template_lib::{ prelude::{ResourceType, RistrettoPublicKeyBytes}, resource::{IMAGE_URL, TOKEN_SYMBOL}, template::BuiltinTemplate, - types::{crypto::UtxoTagByte, Amount, EntityId, TemplateAddress}, + types::{crypto::UtxoTag, Amount, EntityId, TemplateAddress}, }; use super::{working_state::WorkingState, Runtime}; @@ -2352,7 +2352,7 @@ impl> RuntimeInte viewable_balance: None, }, owner_public_key: self.transaction_signer_public_key, - tag: UtxoTagByte::new(0), + tag: UtxoTag::new(0), }); state_mut.new_substate(address, utxo)?; diff --git a/crates/engine_types/src/stealth/outputs.rs b/crates/engine_types/src/stealth/outputs.rs index 9715efd72e..58c4f7becf 100644 --- a/crates/engine_types/src/stealth/outputs.rs +++ b/crates/engine_types/src/stealth/outputs.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_crypto::ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey}; -use tari_template_lib::{models::StealthOutputsStatement, types::crypto::UtxoTagByte}; +use tari_template_lib::{models::StealthOutputsStatement, types::crypto::UtxoTag}; use crate::{ crypto::{range_proof::validate_bullet_proof, validate_elgamal_verifiable_balance_proof, ValidatedPrivateOutput}, @@ -16,7 +16,7 @@ use crate::{ pub struct ValidatedStealthOutput { pub output: ValidatedPrivateOutput, pub owner_public_key: RistrettoPublicKey, - pub tag: UtxoTagByte, + pub tag: UtxoTag, } impl ValidatedStealthOutput { diff --git a/crates/engine_types/src/utxo.rs b/crates/engine_types/src/utxo.rs index b7683ce412..e1a3c63da3 100644 --- a/crates/engine_types/src/utxo.rs +++ b/crates/engine_types/src/utxo.rs @@ -12,7 +12,7 @@ use tari_bor::{BorTag, Deserialize, Serialize}; use tari_template_lib::{ models::{BinaryTag, ResourceAddress}, prelude::{from_hex, serde_helpers, KeyParseError, PedersenCommitmentBytes, RistrettoPublicKeyBytes}, - types::{crypto::UtxoTagByte, hex::write_hex_fmt}, + types::{crypto::UtxoTag, hex::write_hex_fmt}, }; use crate::crypto::PrivateOutput; @@ -31,7 +31,7 @@ pub struct UtxoOutput { /// The public key that must prove ownership of this UTXO. This is typically a one time "stealth" public key but is /// selected by the client. pub owner_public_key: RistrettoPublicKeyBytes, - pub tag: UtxoTagByte, + pub tag: UtxoTag, } impl Utxo { @@ -46,6 +46,10 @@ impl Utxo { self.output.as_ref() } + pub fn into_output(self) -> Option { + self.output + } + pub fn owner_public_key(&self) -> Option<&RistrettoPublicKeyBytes> { self.output().map(|o| &o.owner_public_key) } @@ -68,7 +72,7 @@ impl Utxo { } /// Returns the UTXO’s tag byte if the UTXO has not been burnt. - pub fn tag(&self) -> Option { + pub fn tag(&self) -> Option { self.output.as_ref().map(|o| o.tag) } } @@ -142,6 +146,11 @@ impl UtxoId { pub fn into_commitment_bytes(self) -> PedersenCommitmentBytes { PedersenCommitmentBytes::from_array(self.0) } + + pub fn to_commitment_hex_string(&self) -> String { + // to_string happens to return the hex encoding of the commitment bytes. If that changes, so will this. + self.to_string() + } } impl From for UtxoId { diff --git a/crates/indexer_lib/src/lib.rs b/crates/indexer_lib/src/lib.rs index fac980d471..8e1eb409b2 100644 --- a/crates/indexer_lib/src/lib.rs +++ b/crates/indexer_lib/src/lib.rs @@ -5,6 +5,3 @@ pub mod error; pub mod substate_cache; pub mod substate_decoder; pub mod substate_scanner; - -mod types; -pub use types::*; diff --git a/crates/indexer_lib/src/types.rs b/crates/indexer_lib/src/types.rs deleted file mode 100644 index 066c2b5b20..0000000000 --- a/crates/indexer_lib/src/types.rs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2023 The Tari Project -// SPDX-License-Identifier: BSD-3-Clause - -use serde::{Deserialize, Serialize}; -use tari_engine_types::{substate::SubstateValue, template_lib_models::NonFungibleAddress}; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct NonFungibleSubstate { - pub version: u32, - pub address: NonFungibleAddress, - pub substate: SubstateValue, -} diff --git a/crates/p2p/src/conversions/transaction.rs b/crates/p2p/src/conversions/transaction.rs index f59c196be7..3493a8031d 100644 --- a/crates/p2p/src/conversions/transaction.rs +++ b/crates/p2p/src/conversions/transaction.rs @@ -54,7 +54,7 @@ use tari_template_lib::{ PedersenCommitmentBytes, RangeProofBytes, RistrettoPublicKeyBytes, - UtxoTagByte, + UtxoTag, }, ObjectKey, }, @@ -752,15 +752,11 @@ impl TryFrom for tari_template_lib::mo .try_into()?; let owner_public_key = RistrettoPublicKeyBytes::from_bytes(&val.owner_public_key) .map_err(|e| anyhow!("Invalid owner public key: {}", e.to_error_string()))?; - let tag_byte = val - .tag_byte - .try_into() - .map_err(|e| anyhow!("Invalid tag byte: {}", e))?; Ok(tari_template_lib::models::StealthUnspentOutput { output, owner_public_key, - tag: UtxoTagByte::new(tag_byte), + tag: UtxoTag::new(val.tag_byte), }) } } @@ -770,7 +766,7 @@ impl From for proto::transactio Self { output: Some(val.output.into()), owner_public_key: val.owner_public_key.as_bytes().to_vec(), - tag_byte: val.tag.as_byte().into(), + tag_byte: val.tag.value(), } } } diff --git a/crates/template_lib/src/models/unspent_output.rs b/crates/template_lib/src/models/unspent_output.rs index 1a034a134a..86478c7459 100644 --- a/crates/template_lib/src/models/unspent_output.rs +++ b/crates/template_lib/src/models/unspent_output.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_bor::{Deserialize, Serialize}; -use tari_template_lib_types::crypto::{PedersenCommitmentBytes, RistrettoPublicKeyBytes, UtxoTagByte}; +use tari_template_lib_types::crypto::{PedersenCommitmentBytes, RistrettoPublicKeyBytes, UtxoTag}; use crate::models::{EncryptedData, ViewableBalanceProof}; @@ -36,5 +36,5 @@ pub struct StealthUnspentOutput { /// The public key that must prove ownership of this UTXO. This is typically a one time "stealth" public key /// selected by the client. pub owner_public_key: RistrettoPublicKeyBytes, - pub tag: UtxoTagByte, + pub tag: UtxoTag, } diff --git a/crates/template_lib_types/src/crypto/mod.rs b/crates/template_lib_types/src/crypto/mod.rs index 359d802382..0144de58d0 100644 --- a/crates/template_lib_types/src/crypto/mod.rs +++ b/crates/template_lib_types/src/crypto/mod.rs @@ -10,7 +10,7 @@ mod range_proof; mod ristretto; mod scalar; mod schnorr; -mod tag_byte; +mod utxo_tag; pub use balance_proof::*; pub use commitment::*; @@ -19,6 +19,6 @@ pub use range_proof::*; pub use ristretto::*; pub use scalar::*; pub use schnorr::*; -pub use tag_byte::*; +pub use utxo_tag::*; pub use crate::error::*; diff --git a/crates/template_lib_types/src/crypto/tag_byte.rs b/crates/template_lib_types/src/crypto/tag_byte.rs deleted file mode 100644 index 754e48db8a..0000000000 --- a/crates/template_lib_types/src/crypto/tag_byte.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2025 The Tari Project -// SPDX-License-Identifier: BSD-3-Clause - -use serde::{Deserialize, Serialize}; -use tari_template_abi::rust::fmt; - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct UtxoTagByte(u8); - -impl UtxoTagByte { - pub const fn new(tag: u8) -> Self { - Self(tag) - } - - pub const fn as_byte(&self) -> u8 { - self.0 - } -} - -impl fmt::Display for UtxoTagByte { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "TagByte({})", self.0) - } -} diff --git a/crates/template_lib_types/src/crypto/utxo_tag.rs b/crates/template_lib_types/src/crypto/utxo_tag.rs new file mode 100644 index 0000000000..e86458cb2b --- /dev/null +++ b/crates/template_lib_types/src/crypto/utxo_tag.rs @@ -0,0 +1,28 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use serde::{Deserialize, Serialize}; +use tari_template_abi::rust::fmt; + +/// A 4-byte tag associated with a UTXO. This can be used to more quickly identify _possible_ ownership of a UTXO +/// without needing to fully download and verify all UTXOs. +/// 4 bytes gives a reasonable trade-off between initial download size and utxo size. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub struct UtxoTag(u32); + +impl UtxoTag { + pub const fn new(tag: u32) -> Self { + Self(tag) + } + + pub const fn value(&self) -> u32 { + self.0 + } +} + +impl fmt::Display for UtxoTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "UtxoTag({})", self.0) + } +} diff --git a/crates/template_test_tooling/src/support/stealth.rs b/crates/template_test_tooling/src/support/stealth.rs index 9722f48fc8..0f4b7c21fd 100644 --- a/crates/template_test_tooling/src/support/stealth.rs +++ b/crates/template_test_tooling/src/support/stealth.rs @@ -15,7 +15,7 @@ use tari_ootle_wallet_crypto::{ }; use tari_template_lib::{ models::{EncryptedData, StealthOutputsStatement, StealthTransferStatement}, - types::{crypto::UtxoTagByte, Amount}, + types::{crypto::UtxoTag, Amount}, }; pub fn generate_stealth_output_statement, A: Into>( @@ -87,7 +87,7 @@ fn generate_stealth_statement_internal( resource_view_key: view_key.clone(), }, output_owner_public_key: RistrettoPublicKey::from_secret_key(mask), - tag: UtxoTagByte::new(0), + tag: UtxoTag::new(0), }) .collect::>(); @@ -178,7 +178,7 @@ fn generate_transfer_data_internal, A: Into>( UnblindedStealthOutputStatement { statement, output_owner_public_key, - tag: UtxoTagByte::new(0), + tag: UtxoTag::new(0), } }) .collect::>(); diff --git a/crates/wallet/crypto/src/encrypted_data.rs b/crates/wallet/crypto/src/encrypted_data.rs index f37fce421b..1c5f64b6ab 100644 --- a/crates/wallet/crypto/src/encrypted_data.rs +++ b/crates/wallet/crypto/src/encrypted_data.rs @@ -50,12 +50,10 @@ pub fn unblind_output( pub fn encrypt_value_and_mask( amount: u64, mask: &RistrettoSecretKey, - public_nonce: &RistrettoPublicKey, - secret: &RistrettoSecretKey, + encryption_key: &RistrettoSecretKey, ) -> Result { - let key = kdfs::encrypted_data_dh_kdf_aead(secret, public_nonce); let commitment = get_commitment_factory().commit_value(mask, amount).to_byte_type(); - let encrypted_data = encrypt_data(&key, &commitment, amount, mask)?; + let encrypted_data = encrypt_data(encryption_key, &commitment, amount, mask)?; Ok(encrypted_data) } diff --git a/crates/wallet/crypto/src/kdfs.rs b/crates/wallet/crypto/src/kdfs.rs index 431ae3a27f..90515f89b0 100644 --- a/crates/wallet/crypto/src/kdfs.rs +++ b/crates/wallet/crypto/src/kdfs.rs @@ -9,11 +9,11 @@ use tari_crypto::{ ristretto::{RistrettoPublicKey, RistrettoSecretKey}, }; use tari_ootle_common_types::{base_layer_hashing::encrypted_data_hasher, Network}; -use tari_template_lib::{prelude::RistrettoPublicKeyBytes, types::crypto::UtxoTagByte}; +use tari_template_lib::{models::ResourceAddress, types::crypto::UtxoTag}; use tari_utilities::{hidden_type, safe_array::SafeArray, Hidden}; use zeroize::Zeroize; -use crate::hashers::stealth_owner_hasher64; +use crate::hashers::{stealth_output_tag_hasher64, stealth_owner_hasher64}; pub(crate) const AEAD_KEY_LEN: usize = size_of::(); @@ -21,12 +21,19 @@ pub(crate) const AEAD_KEY_LEN: usize = size_of::(); hidden_type!(EncryptedDataKey, SafeArray); hidden_type!(SafeKey64, SafeArray); +fn dh( + public_key: &RistrettoPublicKey, + private_key: &RistrettoSecretKey, +) -> DiffieHellmanSharedSecret { + DiffieHellmanSharedSecret::::new(private_key, public_key) +} + /// Generate a decryption key from a private key and nonce pub fn encrypted_data_dh_kdf_aead( private_key: &RistrettoSecretKey, public_nonce: &RistrettoPublicKey, ) -> RistrettoSecretKey { - let shared_secret = DiffieHellmanSharedSecret::::new(private_key, public_nonce); + let shared_secret = dh(public_nonce, private_key); let mut aead_key = SafeKey64::from(SafeArray::default()); // Must match base layer burn encrypted_data_hasher() @@ -42,42 +49,55 @@ pub fn owner_stealth_dh_secret( private_key: &RistrettoSecretKey, public_nonce: &RistrettoPublicKey, ) -> RistrettoSecretKey { - // c = H(k * r.G) - let shared_secret = DiffieHellmanSharedSecret::::new(private_key, public_nonce); - let result = stealth_owner_hasher64(network) - .chain(shared_secret.as_bytes()) - .finalize(); - - let c = RistrettoSecretKey::from_uniform_bytes(result.as_ref()) - .expect("key length != RistrettoSecretKey::WIDE_REDUCTION_LEN"); - + // c = H(r.G * k) + let c = stealth_owner_dh(network, public_nonce, private_key); // c + k c + private_key } -pub fn owner_stealth_dh_stealth_address( + +fn stealth_owner_dh( network: Network, public_key: &RistrettoPublicKey, secret_nonce: &RistrettoSecretKey, -) -> RistrettoPublicKey { - // c = H(r * k.G) - let shared_secret = DiffieHellmanSharedSecret::::new(secret_nonce, public_key); +) -> RistrettoSecretKey { + let shared_secret = dh(public_key, secret_nonce); let result = stealth_owner_hasher64(network) .chain(shared_secret.as_bytes()) .finalize(); - let c = RistrettoSecretKey::from_uniform_bytes(result.as_ref()) - .expect("key length != RistrettoSecretKey::WIDE_REDUCTION_LEN"); + RistrettoSecretKey::from_uniform_bytes(result.as_ref()) + .expect("key length != RistrettoSecretKey::WIDE_REDUCTION_LEN") +} +pub fn owner_stealth_dh_stealth_address( + network: Network, + public_key: &RistrettoPublicKey, + secret_nonce: &RistrettoSecretKey, +) -> RistrettoPublicKey { + // c = H(k.G * r) + let c = stealth_owner_dh(network, public_key, secret_nonce); + // C = c.G let c_g = RistrettoPublicKey::from_secret_key(&c); - // c.G + k.G c_g + public_key } -pub fn derive_stealth_output_tag(network: Network, owner_public_key: &RistrettoPublicKeyBytes) -> UtxoTagByte { - let result = stealth_owner_hasher64(network).chain(owner_public_key).finalize(); +pub fn utxo_tag_stealth_dh( + network: Network, + public_key: &RistrettoPublicKey, + secret_nonce: &RistrettoSecretKey, + resource_address: &ResourceAddress, +) -> UtxoTag { + let shared_secret = dh(public_key, secret_nonce); + let result = stealth_output_tag_hasher64(network) + .chain(shared_secret.as_bytes()) + .chain(resource_address) + .finalize(); - UtxoTagByte::new(result[0]) + let mut buf = [0u8; size_of::()]; + buf.copy_from_slice(&result[..size_of::()]); + let tag = u32::from_le_bytes(buf); + UtxoTag::new(tag) } #[cfg(test)] diff --git a/crates/wallet/crypto/src/stealth.rs b/crates/wallet/crypto/src/stealth.rs index 0ae6d21007..cf9403d466 100644 --- a/crates/wallet/crypto/src/stealth.rs +++ b/crates/wallet/crypto/src/stealth.rs @@ -163,7 +163,7 @@ mod tests { use tari_engine_types::stealth::validate_stealth_outputs_statement; use tari_template_lib::{ models::EncryptedData, - types::{crypto::UtxoTagByte, Amount}, + types::{crypto::UtxoTag, Amount}, }; use super::*; @@ -182,7 +182,7 @@ mod tests { resource_view_key: None, }, output_owner_public_key: RistrettoPublicKey::default(), - tag: UtxoTagByte::new(0), + tag: UtxoTag::new(0), }], Amount::zero(), ) diff --git a/crates/wallet/crypto/src/unblinded_statement.rs b/crates/wallet/crypto/src/unblinded_statement.rs index 25bd8a8f6a..e793693e1a 100644 --- a/crates/wallet/crypto/src/unblinded_statement.rs +++ b/crates/wallet/crypto/src/unblinded_statement.rs @@ -5,7 +5,7 @@ use tari_crypto::ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey, R use tari_engine_types::crypto::commit_amount_checked; use tari_template_lib::{ models::EncryptedData, - types::{crypto::UtxoTagByte, Amount}, + types::{crypto::UtxoTag, Amount}, }; #[derive(Debug, Clone)] @@ -28,7 +28,7 @@ impl UnblindedOutputStatement { pub struct UnblindedStealthOutputStatement { pub statement: UnblindedOutputStatement, pub output_owner_public_key: RistrettoPublicKey, - pub tag: UtxoTagByte, + pub tag: UtxoTag, } #[derive(Debug, Clone)] diff --git a/crates/wallet/crypto/tests/output_statement.rs b/crates/wallet/crypto/tests/output_statement.rs index c405ef7255..3b04a4f7b0 100644 --- a/crates/wallet/crypto/tests/output_statement.rs +++ b/crates/wallet/crypto/tests/output_statement.rs @@ -29,7 +29,7 @@ fn it_create_a_valid_revealed_only_proof() { } mod stealth_tests { - use tari_template_lib::types::crypto::UtxoTagByte; + use tari_template_lib::types::crypto::UtxoTag; use super::*; @@ -130,7 +130,7 @@ mod stealth_tests { UnblindedStealthOutputStatement { statement, output_owner_public_key, - tag: UtxoTagByte::new(0), + tag: UtxoTag::new(0), } }) .collect() diff --git a/crates/wallet/sdk/src/apis/confidential_crypto.rs b/crates/wallet/sdk/src/apis/confidential_crypto.rs index c8cda5e361..65c48cce4a 100644 --- a/crates/wallet/sdk/src/apis/confidential_crypto.rs +++ b/crates/wallet/sdk/src/apis/confidential_crypto.rs @@ -75,7 +75,8 @@ impl ConfidentialCryptoApi { public_nonce: &RistrettoPublicKey, secret: &PrivateKey, ) -> Result { - let data = encrypt_value_and_mask(amount, mask, public_nonce, secret)?; + let key = kdfs::encrypted_data_dh_kdf_aead(secret, public_nonce); + let data = encrypt_value_and_mask(amount, mask, &key)?; Ok(data) } diff --git a/crates/wallet/sdk/src/apis/key_manager.rs b/crates/wallet/sdk/src/apis/key_manager.rs index 1cbc6d5a68..42b7d9c7c6 100644 --- a/crates/wallet/sdk/src/apis/key_manager.rs +++ b/crates/wallet/sdk/src/apis/key_manager.rs @@ -28,14 +28,17 @@ pub enum KeyBranch { Account, /// The transaction key branch, used to sign transactions that do not need to be signed with the account key. Transaction, - /// The view key branch, used to derive a view key for resources. - ViewKey, + /// The Elgamal encryption view key branch, used to derive a view key for resources with "viewable balance" + /// enabled. + ElgamalEncryptionViewKey, /// The stealth masks branch, used to derive masks for stealth addresses. StealthMasks, /// The confidential masks branch, used to derive masks for confidential transactions. ConfidentialMasks, /// Used to generate nonces that need to be recreated later, e.g. to derive the DH secret for claim burn Nonce, + /// The view key branch, used to derive an encryption key for wallet recovery. + ViewKey, } impl KeyBranch { @@ -43,10 +46,11 @@ impl KeyBranch { match self { Self::Account => "account", Self::Transaction => "transactions", - Self::ViewKey => "view_key", + Self::ElgamalEncryptionViewKey => "elgamal_view_key", Self::StealthMasks => "stealth_masks", Self::ConfidentialMasks => "confidential_masks", Self::Nonce => "nonce", + Self::ViewKey => "view_key", } } } @@ -253,6 +257,14 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { } } +impl Clone for KeyManagerApi<'_, TStore> { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for KeyManagerApi<'_, TStore> {} + #[derive(Debug, thiserror::Error)] pub enum KeyManagerApiError { #[error("Store error: {0}")] diff --git a/crates/wallet/sdk/src/apis/mod.rs b/crates/wallet/sdk/src/apis/mod.rs index 77da292a16..9f391a3ff3 100644 --- a/crates/wallet/sdk/src/apis/mod.rs +++ b/crates/wallet/sdk/src/apis/mod.rs @@ -11,7 +11,6 @@ pub mod non_fungible_tokens; pub mod resources; pub mod stealth_crypto; pub mod stealth_outputs; -pub mod stealth_scanner; pub mod stealth_transfer; pub mod substate; pub mod template; diff --git a/crates/wallet/sdk/src/apis/stealth_crypto.rs b/crates/wallet/sdk/src/apis/stealth_crypto.rs index f496c04944..beed31eec5 100644 --- a/crates/wallet/sdk/src/apis/stealth_crypto.rs +++ b/crates/wallet/sdk/src/apis/stealth_crypto.rs @@ -26,13 +26,14 @@ use tari_ootle_wallet_crypto::{ WalletCryptoError, }; use tari_template_lib::{ - models::{ConfidentialOutputStatement, EncryptedData, StealthTransferStatement}, + models::{ConfidentialOutputStatement, EncryptedData, ResourceAddress, StealthTransferStatement}, prelude::{crypto::CommitmentSignatureBytes, PedersenCommitmentBytes, RistrettoPublicKeyBytes}, - types::{crypto::UtxoTagByte, Amount}, + types::{crypto::UtxoTag, Amount}, }; const LOG_TARGET: &str = "tari::ootle::wallet::sdk::stealth_crypto"; +#[derive(Debug, Clone, Copy)] pub struct StealthCryptoApi; impl StealthCryptoApi { @@ -75,12 +76,24 @@ impl StealthCryptoApi { Ok(stmt) } - pub fn derive_stealth_output_tag( + pub fn derive_stealth_output_tag_for_recipient( &self, network: Network, - dest_public_key: &RistrettoPublicKeyBytes, - ) -> UtxoTagByte { - kdfs::derive_stealth_output_tag(network, dest_public_key) + nonce_secret: &RistrettoSecretKey, + dest_public_key: &RistrettoPublicKey, + resource_address: &ResourceAddress, + ) -> UtxoTag { + kdfs::utxo_tag_stealth_dh(network, dest_public_key, nonce_secret, resource_address) + } + + pub fn derive_stealth_output_tag_from_sender( + &self, + network: Network, + secret_key: &RistrettoSecretKey, + public_nonce: &RistrettoPublicKey, + resource_address: &ResourceAddress, + ) -> UtxoTag { + kdfs::utxo_tag_stealth_dh(network, public_nonce, secret_key, resource_address) } pub fn derive_stealth_owner_public_key( @@ -108,7 +121,8 @@ impl StealthCryptoApi { public_nonce: &RistrettoPublicKey, secret: &RistrettoSecretKey, ) -> Result { - let data = encrypt_value_and_mask(amount, mask, public_nonce, secret)?; + let encryption_key = kdfs::encrypted_data_dh_kdf_aead(secret, public_nonce); + let data = encrypt_value_and_mask(amount, mask, &encryption_key)?; Ok(data) } diff --git a/crates/wallet/sdk/src/apis/stealth_outputs.rs b/crates/wallet/sdk/src/apis/stealth_outputs.rs index 739975070d..a20e63fa80 100644 --- a/crates/wallet/sdk/src/apis/stealth_outputs.rs +++ b/crates/wallet/sdk/src/apis/stealth_outputs.rs @@ -12,6 +12,7 @@ use tari_engine_types::{ ToByteType, Utxo, UtxoAddress, + UtxoOutput, }; use tari_ootle_common_types::{ optional::{IsNotFoundError, Optional}, @@ -307,14 +308,15 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { Ok(()) } - pub fn update_utxo_status_from_utxo( + pub fn update_utxo_status( &self, address: &UtxoAddress, - utxo: &Utxo, + is_burnt: Option, + status: Option, + is_frozen: Option, ) -> Result<(), StealthOutputsApiError> { - self.store.with_write_tx(|tx| { - tx.stealth_outputs_update(address, Some(utxo.is_burnt()), None, Some(utxo.is_frozen())) - })?; + self.store + .with_write_tx(|tx| tx.stealth_outputs_update(address, is_burnt, status, is_frozen))?; Ok(()) } @@ -385,7 +387,8 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { // need for this case? }, None => { - if utxo.is_burnt() { + let is_frozen = utxo.is_frozen(); + let Some(output) = utxo.output() else { debug!( target: LOG_TARGET, "Unknown Utxo output is burnt for commitment: {}. Skipping.", @@ -395,7 +398,14 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { }; // Output does not exist. Validate it and add it to the store - match self.validate_utxo(&all_used_account_keys, network, *resource_address, commitment, utxo) { + match self.validate_utxo( + &all_used_account_keys, + network, + *resource_address, + commitment, + output, + is_frozen, + ) { Ok(Some(output)) => { found_utxos_count += 1; tx.stealth_outputs_insert(&output)?; @@ -433,13 +443,15 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { Ok(()) } + #[allow(clippy::too_many_lines)] pub fn validate_utxo( &self, all_used_account_keys: &[KeyPair], network: Network, resource_address: ResourceAddress, commitment: PedersenCommitmentBytes, - utxo: &Utxo, + output: &UtxoOutput, + is_frozen: bool, ) -> Result, StealthOutputsApiError> { // Validate the commitment is well-formed. let _output_commitment = PedersenCommitment::try_from_byte_type(&commitment).map_err(|e| { @@ -448,12 +460,6 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { reason: format!("Invalid output commitment bytes: {}", e), } })?; - let is_burnt = utxo.is_burnt(); - let is_frozen = utxo.is_frozen(); - let Some(output) = utxo.output() else { - // We can't validate a burnt output - return Ok(None); - }; let output_stealth_public_nonce = RistrettoPublicKey::try_from_byte_type(&output.output.public_nonce).map_err(|e| { @@ -472,7 +478,6 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { output_stealth_public_nonce, ); - // TODO: limit accounts to those matching a tag for wallet_key in all_used_account_keys { let unblinded_result = self.crypto_api.unblind_output( &commitment, @@ -536,7 +541,7 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { encrypted_data: output.output.encrypted_data.clone(), tag_byte: output.tag, status, - is_burnt, + is_burnt: false, is_frozen, is_on_chain: true, lock_id: None, diff --git a/crates/wallet/sdk/src/apis/stealth_scanner.rs b/crates/wallet/sdk/src/apis/stealth_scanner.rs deleted file mode 100644 index ebe99b7942..0000000000 --- a/crates/wallet/sdk/src/apis/stealth_scanner.rs +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright 2025 The Tari Project -// SPDX-License-Identifier: BSD-3-Clause - -use std::{ - array, - collections::{HashMap, HashSet}, -}; - -use log::*; -use tari_ootle_common_types::{ - optional::{IsNotFoundError, Optional}, - Network, - NumPreshards, - StateVersion, - UtxoSpent, - UtxoUnspent, - UtxoUpdate, -}; -use tari_template_lib::models::ResourceAddress; - -use crate::{ - apis::{ - config::{ConfigApi, ConfigApiError}, - key_manager::{KeyManagerApi, KeyManagerApiError}, - stealth_crypto::StealthCryptoApi, - stealth_outputs::{StealthOutputsApi, StealthOutputsApiError}, - }, - models::AccountWithPublicKey, - network::WalletNetworkInterface, - storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, -}; - -const LOG_TARGET: &str = "tari::ootle::wallet::apis::stealth_outputs"; - -// TODO: either fetch num preshards from the network or we should hardcode it to a single value for all apps -const NUM_PRESHARDS: NumPreshards = NumPreshards::P256; - -pub struct StealthScannerApi<'a, TStore, TNetworkInterface> { - store: &'a TStore, - crypto_api: StealthCryptoApi, - key_manager_api: KeyManagerApi<'a, TStore>, - outputs_api: StealthOutputsApi<'a, TStore>, - config_api: ConfigApi<'a, TStore>, - network_interface: &'a TNetworkInterface, -} - -impl<'a, TStore: WalletStore, TNetworkInterface: WalletNetworkInterface> - StealthScannerApi<'a, TStore, TNetworkInterface> -{ - pub fn new( - store: &'a TStore, - stealth_crypto_api: StealthCryptoApi, - key_manager_api: KeyManagerApi<'a, TStore>, - outputs_api: StealthOutputsApi<'a, TStore>, - network_interface: &'a TNetworkInterface, - config_api: ConfigApi<'a, TStore>, - ) -> Self { - Self { - store, - crypto_api: stealth_crypto_api, - key_manager_api, - outputs_api, - network_interface, - config_api, - } - } - - pub async fn scan_and_recover_utxos( - &self, - account: &AccountWithPublicKey, - resource_address: &ResourceAddress, - ) -> Result<(), StealthScannerApiError> { - let network = self.config_api.get_network()?; - let tag = self - .crypto_api - .derive_stealth_output_tag(network, account.owner_public_key()); - - let mut shard_state_versions_to_set = HashMap::new(); - loop { - let mut shard_state_versions = self - .store - .with_read_tx(|tx| tx.shard_state_version_get(account.address(), resource_address))?; - - // Populate any missing shards with zero - for shard in NUM_PRESHARDS.all_shards_iter() { - shard_state_versions.entry(shard).or_insert_with(StateVersion::zero); - } - - info!( - target: LOG_TARGET, - "🔍️ Scanning for stealth outputs (account {}, resource {}, num shards {}, tag {})", - account.address(), - resource_address, - shard_state_versions.len(), - tag - ); - - let response = self - .network_interface - .query_stealth_utxo_updates(*resource_address, shard_state_versions, HashSet::from([tag])) - .await - .map_err(|e| StealthScannerApiError::NetworkInterfaceError(e.into()))?; - - // Keep going until there are no more - if response.updates.is_empty() { - info!( - target: LOG_TARGET, - "🔍️ Scan complete for account {}: No more stealth outputs found", - account.address() - ); - // Update state versions to avoid rescanning from previous versions that didnt contain any changes - self.store.with_write_tx(|tx| { - tx.shard_state_version_set_many( - account.address(), - resource_address, - response.per_shard_high_watermark, - ) - })?; - - break; - } - - let num_received = response.updates.len(); - let mut num_recovered = 0; - let mut num_spent = 0; - - for update in response.updates { - match update { - UtxoUpdate::Unspent(unspent) => { - shard_state_versions_to_set - .entry(unspent.shard) - .and_modify(|v| { - if unspent.state_version > *v { - *v = unspent.state_version - } - }) - .or_insert(unspent.state_version); - - if self.check_unspent_utxo_and_store(network, account.key_index(), resource_address, unspent)? { - num_recovered += 1; - } - }, - UtxoUpdate::Spent(spent) => { - shard_state_versions_to_set - .entry(spent.shard) - .and_modify(|v| { - if spent.state_version > *v { - *v = spent.state_version - } - }) - .or_insert(spent.state_version); - if self.spend(spent)? { - num_spent += 1; - } - }, - } - } - - self.store.with_write_tx(|tx| { - tx.shard_state_version_set_many( - account.address(), - resource_address, - shard_state_versions_to_set.drain(), - ) - })?; - - info!( - target: LOG_TARGET, - "Scan complete for account {}: Discovered {}/{} new stealth outputs, marked {} as spent", - account.address(), - num_recovered, - num_received, - num_spent - ); - } - - Ok(()) - } - - fn check_unspent_utxo_and_store( - &self, - network: Network, - account_key_index: u64, - resource_address: &ResourceAddress, - unspent: UtxoUnspent, - ) -> Result { - let address = unspent.address; - let commitment = address.id().into_commitment_bytes(); - let account_key = self.key_manager_api.derive_account_key_pair(account_key_index)?; - - let Some(output) = self.outputs_api.validate_utxo( - array::from_ref(&account_key), - network, - *resource_address, - commitment, - &unspent.utxo, - )? - else { - // The output could be burnt, frozen, or otherwise invalid. If we already have it in the db, update its - // status - self.outputs_api - .update_utxo_status_from_utxo(&address, &unspent.utxo) - .optional()?; - - return Ok(false); - }; - - self.outputs_api.upsert_utxo(&output)?; - info!(target: LOG_TARGET, "💰️ Recovered stealth output {} for account {}", address, account_key.public_key); - Ok(true) - } - - fn spend(&self, spent: UtxoSpent) -> Result { - match self - .store - .with_write_tx(|tx| tx.stealth_outputs_mark_as_spent(&spent.address)) - .optional()? - { - Some(_) => { - info!(target: LOG_TARGET, "Marked stealth output {} as spent", spent.address); - Ok(true) - }, - None => { - debug!( - target: LOG_TARGET, - "Spent UTXO {} not found in wallet store (likely not our output)", spent.address - ); - Ok(false) - }, - } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum StealthScannerApiError { - #[error("Config error: {0}")] - ConfigError(#[from] ConfigApiError), - #[error("Network interface error: {0}")] - NetworkInterfaceError(anyhow::Error), - #[error("Unexpected response: {details}")] - UnexpectedResponse { details: String }, - #[error("Store error: {0}")] - StoreError(#[from] WalletStorageError), - #[error("Stealth outputs error: {0}")] - StealthOutputsError(#[from] StealthOutputsApiError), - #[error("Key manager error: {0}")] - KeyManagerError(#[from] KeyManagerApiError), -} - -impl IsNotFoundError for StealthScannerApiError { - fn is_not_found_error(&self) -> bool { - match self { - Self::StoreError(e) => e.is_not_found_error(), - Self::StealthOutputsError(e) => e.is_not_found_error(), - _ => false, - } - } -} diff --git a/crates/wallet/sdk/src/apis/stealth_transfer.rs b/crates/wallet/sdk/src/apis/stealth_transfer.rs index f64186b64d..af02b22890 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer.rs @@ -430,7 +430,12 @@ where .blinded_output_amount .is_positive() .then(|| { - self.create_output_statement(&destination_pk, params.blinded_output_amount, resource_view_key.clone()) + self.create_output_statement( + &destination_pk, + params.blinded_output_amount, + ¶ms.resource_address, + resource_view_key.clone(), + ) }) .transpose()?; @@ -448,7 +453,14 @@ where // Generate fee change outputs if required let fee_change_output_statement = fee_change .is_positive() - .then(|| self.create_output_statement(&owner_account.to_ristretto_public_key(), fee_change, None)) + .then(|| { + self.create_output_statement( + &owner_account.to_ristretto_public_key(), + fee_change, + ¶ms.resource_address, + None, + ) + }) .transpose() .inspect_err(|e| { warn!(target: LOG_TARGET, "Unlocking fee fund locks after error: {}", e); @@ -618,7 +630,7 @@ where self.key_manager_api .derive_account_key(params.owner_account.key_index())? } else { - // Since we don't require account auth, use a throw away nonce to sign the transaction + // Since we don't require account auth, use a throwaway nonce to sign the transaction self.key_manager_api.next_key(KeyBranch::Nonce)? }; @@ -709,7 +721,8 @@ where } })?; - let change = self.create_output_statement(&change_public_key, change_amount, resource_view_key)?; + let change = + self.create_output_statement(&change_public_key, change_amount, &resource_address, resource_view_key)?; self.add_unconfirmed_output_from_statement(lock_id, account, resource_address, &change)?; @@ -753,26 +766,27 @@ where fn create_output_statement( &self, dest_public_key: &RistrettoPublicKey, - confidential_amount: Amount, + amount: Amount, + resource_address: &ResourceAddress, resource_view_key: Option, ) -> Result { - let network = self.config_api.get_network()?; - if !confidential_amount.is_positive() { + if !amount.is_positive() { return Err(StealthTransferApiError::InvalidParameter { - param: "confidential_amount", - reason: "Confidential amount must be positive".to_string(), + param: "amount", + reason: format!("Amount must be positive, got {}", amount), }); } + let network = self.config_api.get_network()?; let mask = self.key_manager_api.next_key(KeyBranch::StealthMasks)?; let (nonce_secret, public_nonce) = RistrettoPublicKey::random_keypair(&mut OsRng); let encrypted_data = self.crypto_api.encrypt_value_and_mask( - confidential_amount + amount .to_u64_checked() .ok_or_else(|| StealthTransferApiError::AmountOverflow { - param: "confidential_amount", - details: "Confidential amount exceeds u64. This is currently a limitation due to the format of \ + param: "amount", + details: "Stealth amount exceeds u64::MAX. This is currently a limitation due to the format of \ EncryptedData" .to_string(), })?, @@ -786,19 +800,24 @@ where self.crypto_api .derive_stealth_owner_public_key(network, dest_public_key, &nonce_secret); - let derived_tag = self - .crypto_api - .derive_stealth_output_tag(network, &dest_public_key.to_byte_type()); + let statement = UnblindedOutputStatement { + amount, + mask: mask.key, + sender_public_nonce: public_nonce, + encrypted_data, + minimum_value_promise: 0, + resource_view_key, + }; + + let derived_tag = self.crypto_api.derive_stealth_output_tag_for_recipient( + network, + &nonce_secret, + dest_public_key, + resource_address, + ); Ok(UnblindedStealthOutputStatement { - statement: UnblindedOutputStatement { - amount: confidential_amount, - mask: mask.key, - sender_public_nonce: public_nonce, - encrypted_data, - minimum_value_promise: 0, - resource_view_key, - }, + statement, output_owner_public_key, tag: derived_tag, }) diff --git a/crates/wallet/sdk/src/models/mod.rs b/crates/wallet/sdk/src/models/mod.rs index f85c0e737f..25f730370c 100644 --- a/crates/wallet/sdk/src/models/mod.rs +++ b/crates/wallet/sdk/src/models/mod.rs @@ -10,6 +10,7 @@ mod non_fungible_tokens; mod resource; mod stealth_output; mod substate; +mod utxo_update; mod vault; mod wallet_transaction; mod webauthn_registration; @@ -23,6 +24,7 @@ pub use non_fungible_tokens::*; pub use resource::*; pub use stealth_output::*; pub use substate::*; +pub use utxo_update::*; pub use vault::*; pub use wallet_transaction::*; pub use webauthn_registration::*; diff --git a/crates/wallet/sdk/src/models/stealth_output.rs b/crates/wallet/sdk/src/models/stealth_output.rs index e09c81231d..1891eb902d 100644 --- a/crates/wallet/sdk/src/models/stealth_output.rs +++ b/crates/wallet/sdk/src/models/stealth_output.rs @@ -5,7 +5,7 @@ use tari_engine_types::UtxoAddress; use tari_template_lib::{ models::{ComponentAddress, EncryptedData}, prelude::{PedersenCommitmentBytes, ResourceAddress, RistrettoPublicKeyBytes}, - types::{crypto::UtxoTagByte, Amount}, + types::{crypto::UtxoTag, Amount}, }; use crate::models::{OutputStatus, WalletLockId}; @@ -19,7 +19,7 @@ pub struct StealthOutputModel { pub sender_public_nonce: RistrettoPublicKeyBytes, pub encryption_secret_key_index: u64, pub encrypted_data: EncryptedData, - pub tag_byte: UtxoTagByte, + pub tag_byte: UtxoTag, pub status: OutputStatus, pub is_burnt: bool, pub is_frozen: bool, diff --git a/crates/wallet/sdk/src/models/utxo_update.rs b/crates/wallet/sdk/src/models/utxo_update.rs new file mode 100644 index 0000000000..5e53aa4cb7 --- /dev/null +++ b/crates/wallet/sdk/src/models/utxo_update.rs @@ -0,0 +1,52 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::collections::HashMap; + +use tari_bor::{Deserialize, Serialize}; +use tari_engine_types::UtxoId; +use tari_ootle_common_types::{shard::Shard, StateVersion}; +use tari_template_lib::prelude::{crypto::UtxoTag, RistrettoPublicKeyBytes}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub struct UtxoUpdateSet { + pub shard_updates: HashMap, + pub per_shard_high_watermark: Vec<(Shard, StateVersion)>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub struct UtxoStateUpdateSet { + pub updates: Vec, + pub max_state_version: StateVersion, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub enum WalletUtxoUpdate { + Unspent(UtxoUnspent), + Spent(UtxoSpent), + Burnt(UtxoBurnt), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub struct UtxoUnspent { + pub tag: UtxoTag, + pub public_nonce: RistrettoPublicKeyBytes, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub struct UtxoSpent { + pub id: UtxoId, + pub version: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub struct UtxoBurnt { + pub id: UtxoId, + pub version: u32, +} diff --git a/crates/wallet/sdk/src/network.rs b/crates/wallet/sdk/src/network.rs index e1d94d99ad..40ba86e34f 100644 --- a/crates/wallet/sdk/src/network.rs +++ b/crates/wallet/sdk/src/network.rs @@ -1,28 +1,28 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::{ - collections::{HashMap, HashSet}, - convert::Infallible, - future::Future, - time::Duration, -}; +use std::{collections::HashMap, convert::Infallible, future::Future, time::Duration}; use serde::{Deserialize, Serialize}; use tari_consensus_types::Decision; use tari_engine_types::{ commit_result::ExecuteResult, substate::{SubstateId, SubstateValue}, + Utxo, + UtxoId, }; -use tari_ootle_common_types::{shard::Shard, substate_type::SubstateType, StateVersion, UtxoQueryResponse}; +use tari_ootle_common_types::{shard::Shard, substate_type::SubstateType, StateVersion}; use tari_template_abi::TemplateDef; use tari_template_lib::{ models::ResourceAddress, - prelude::{crypto::UtxoTagByte, TemplateAddress}, + prelude::{RistrettoPublicKeyBytes, TemplateAddress}, + types::crypto::UtxoTag, }; use tari_transaction::{Transaction, TransactionId}; use time::PrimitiveDateTime; +use crate::models::UtxoUpdateSet; + pub trait WalletNetworkInterface { type Error: std::error::Error + Send + Sync + 'static; @@ -65,8 +65,13 @@ pub trait WalletNetworkInterface { &self, resource_address: ResourceAddress, shard_state_versions: HashMap, - filter_tag_bytes: HashSet, - ) -> impl Future> + Send; + ) -> impl Future> + Send; + + fn get_unspent_utxos( + &self, + resource_address: ResourceAddress, + tag_and_nonce_pairs: Vec<(UtxoTag, RistrettoPublicKeyBytes)>, + ) -> impl Future, Self::Error>> + Send; fn wait_until_ready(&self) -> impl Future> + Send; } diff --git a/crates/wallet/sdk/src/sdk.rs b/crates/wallet/sdk/src/sdk.rs index 4b3950f1db..1246fe14b0 100644 --- a/crates/wallet/sdk/src/sdk.rs +++ b/crates/wallet/sdk/src/sdk.rs @@ -32,7 +32,6 @@ use crate::{ resources::ResourcesApi, stealth_crypto::StealthCryptoApi, stealth_outputs::StealthOutputsApi, - stealth_scanner::StealthScannerApi, stealth_transfer::StealthTransferApi, substate::SubstatesApi, template::TemplateApi, @@ -128,16 +127,16 @@ where ConfigApi::new(&self.store) } - pub fn get_config(&self) -> &WalletSdkConfig { + pub fn sdk_config(&self) -> &WalletSdkConfig { &self.config } - pub fn get_network_interface(&self) -> &TNetworkInterface { - &self.network_interface + pub fn network(&self) -> Network { + self.config.network } - pub fn get_network_interface_mut(&mut self) -> &mut TNetworkInterface { - &mut self.network_interface + pub fn get_network_interface(&self) -> &TNetworkInterface { + &self.network_interface } /// Returns the KeyManager API for the wallet. @@ -213,17 +212,6 @@ where ) } - pub fn stealth_scanner_api(&self) -> StealthScannerApi<'_, TStore, TNetworkInterface> { - StealthScannerApi::new( - &self.store, - self.stealth_crypto_api(), - self.key_manager_api(), - self.stealth_outputs_api(), - &self.network_interface, - self.config_api(), - ) - } - pub fn non_fungible_api(&self) -> NonFungibleTokensApi<'_, TStore> { NonFungibleTokensApi::new(&self.store) } diff --git a/crates/wallet/sdk/src/storage.rs b/crates/wallet/sdk/src/storage.rs index ce16e49e7f..8f0931602f 100644 --- a/crates/wallet/sdk/src/storage.rs +++ b/crates/wallet/sdk/src/storage.rs @@ -6,7 +6,7 @@ use std::{ ops::{Deref, DerefMut}, }; -use tari_engine_types::{resource::Resource, substate::SubstateId, UtxoAddress}; +use tari_engine_types::{resource::Resource, substate::SubstateId, UtxoAddress, UtxoId}; use tari_ootle_common_types::{ optional::IsNotFoundError, shard::Shard, @@ -24,7 +24,7 @@ use tari_template_lib::{ ResourceType, RistrettoPublicKeyBytes, }, - types::{Amount, TemplateAddress}, + types::{crypto::UtxoTag, Amount, TemplateAddress}, }; use tari_transaction::{Transaction, TransactionId}; use webauthn_rs::prelude::Passkey; @@ -43,6 +43,7 @@ use crate::models::{ StealthOutputModel, SubstateModel, TransactionStatus, + UtxoUnspent, VaultModel, WalletLockId, WalletTransaction, @@ -288,8 +289,15 @@ pub trait WalletStoreReader { account: &ComponentAddress, resource: &ResourceAddress, ) -> Result, WalletStorageError>; + + fn utxo_process_queue_fetch_batch( + &mut self, + batch_size: usize, + ) -> Result>, WalletStorageError>; } +pub type TagAndPublicNoncePair = (UtxoTag, RistrettoPublicKeyBytes); + pub trait WalletStoreWriter { fn commit(self) -> Result<(), WalletStorageError>; fn rollback(self) -> Result<(), WalletStorageError>; @@ -399,7 +407,11 @@ pub trait WalletStoreWriter { lock_id: WalletLockId, ) -> Result; fn stealth_outputs_insert(&mut self, output: &StealthOutputModel) -> Result<(), WalletStorageError>; - fn stealth_outputs_mark_as_spent(&mut self, address: &UtxoAddress) -> Result<(), WalletStorageError>; + fn stealth_outputs_mark_as_spent( + &mut self, + 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 @@ -441,4 +453,16 @@ pub trait WalletStoreWriter { resource_address: &ResourceAddress, shard_state_versions: I, ) -> Result<(), WalletStorageError>; + + fn utxo_process_queue_extend>( + &mut self, + resource_address: &ResourceAddress, + items: I, + ) -> Result<(), WalletStorageError>; + fn utxo_process_queue_remove_item( + &mut self, + resource_address: ResourceAddress, + tag: UtxoTag, + public_nonce: RistrettoPublicKeyBytes, + ) -> Result<(), WalletStorageError>; } diff --git a/crates/wallet/sdk/tests/confidential_output_api.rs b/crates/wallet/sdk/tests/confidential_output_api.rs index 567fd8d585..19d537ef6d 100644 --- a/crates/wallet/sdk/tests/confidential_output_api.rs +++ b/crates/wallet/sdk/tests/confidential_output_api.rs @@ -1,23 +1,21 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::{ - collections::{HashMap, HashSet}, - convert::Infallible, - str::FromStr, -}; +use std::{collections::HashMap, convert::Infallible, str::FromStr}; use tari_crypto::{commitment::HomomorphicCommitmentFactory, tari_utilities::SafePassword}; use tari_engine_types::{ crypto::{commit_amount_checked, get_commitment_factory}, substate::SubstateId, ToByteType, + Utxo, + UtxoId, }; -use tari_ootle_common_types::{optional::Optional, shard::Shard, Network, StateVersion, UtxoQueryResponse}; +use tari_ootle_common_types::{optional::Optional, shard::Shard, Network, StateVersion}; use tari_ootle_wallet_sdk::{ - models::{ConfidentialOutputModel, OutputStatus, WalletLockId}, + models::{ConfidentialOutputModel, OutputStatus, UtxoUpdateSet, WalletLockId}, network::{SubstateQueryResult, TransactionQueryResult, WalletNetworkInterface}, - storage::{WalletStore, WalletStoreReader}, + storage::{TagAndPublicNoncePair, WalletStore, WalletStoreReader}, WalletSdk, WalletSdkConfig, }; @@ -26,7 +24,6 @@ use tari_template_abi::TemplateDef; use tari_template_lib::{ constants::STEALTH_TARI_RESOURCE_ADDRESS, models::{ComponentAddress, EncryptedData, ResourceAddress, VaultId}, - prelude::crypto::UtxoTagByte, resource::ResourceType, types::{crypto::PedersenCommitmentBytes, Amount, TemplateAddress}, }; @@ -296,8 +293,15 @@ impl WalletNetworkInterface for PanicNetworkInterface { &self, _resource_address: ResourceAddress, _shard_state_versions: HashMap, - _filter_tag_bytes: HashSet, - ) -> Result { + ) -> Result { panic!("PanicNetworkInterface called") } + + async fn get_unspent_utxos( + &self, + _resource_address: ResourceAddress, + _tag_and_nonce_pairs: Vec, + ) -> Result, Self::Error> { + panic!("PanicNetworkInterface get_unspent_utxos called") + } } diff --git a/crates/wallet/sdk_services/Cargo.toml b/crates/wallet/sdk_services/Cargo.toml index 22aefad8f8..1aea90b01a 100644 --- a/crates/wallet/sdk_services/Cargo.toml +++ b/crates/wallet/sdk_services/Cargo.toml @@ -15,7 +15,12 @@ tari_engine_types = { workspace = true } tari_template_lib = { workspace = true } tari_indexer_client = { workspace = true, features = ["client"] } tari_transaction = { workspace = true } +tari_transaction_components = { workspace = true } +tari_crypto = { workspace = true } +anyhow = { workspace = true } reqwest = { workspace = true } thiserror = { workspace = true } url = { workspace = true } +log = { workspace = true } +tokio = { workspace = true, features = ["sync"] } diff --git a/crates/wallet/sdk_services/src/indexer_jrpc_impl.rs b/crates/wallet/sdk_services/src/indexer_jrpc_impl.rs index 26dc82406f..af379915ab 100644 --- a/crates/wallet/sdk_services/src/indexer_jrpc_impl.rs +++ b/crates/wallet/sdk_services/src/indexer_jrpc_impl.rs @@ -2,44 +2,43 @@ // SPDX-License-Identifier: BSD-3-Clause use std::{ - collections::{HashMap, HashSet}, + collections::HashMap, sync::{Arc, Mutex}, }; use reqwest::{IntoUrl, Url}; -use tari_engine_types::substate::SubstateId; +use tari_engine_types::{substate::SubstateId, Utxo, UtxoId}; use tari_indexer_client::{ error::IndexerClientError, json_rpc_client::IndexerJsonRpcClient, types::{ GetSubstateRequest, GetTransactionResultRequest, + GetUnspentUtxosRequest, GetUtxoUpdatesRequest, IndexerTransactionFinalizedResult, ListSubstatesRequest, SubmitTransactionRequest, }, }; -use tari_ootle_common_types::{ - optional::IsNotFoundError, - shard::Shard, - substate_type::SubstateType, - StateVersion, - UtxoQueryResponse, -}; -use tari_ootle_wallet_sdk::network::{ - StatusResponseError, - SubstateListItem, - SubstateListResult, - SubstateQueryResult, - TransactionFinalizedResult, - TransactionQueryResult, - WalletNetworkInterface, - WalletQueryErrorStatus, +use tari_ootle_common_types::{optional::IsNotFoundError, shard::Shard, substate_type::SubstateType, StateVersion}; +use tari_ootle_wallet_sdk::{ + models::UtxoUpdateSet, + network::{ + StatusResponseError, + SubstateListItem, + SubstateListResult, + SubstateQueryResult, + TransactionFinalizedResult, + TransactionQueryResult, + WalletNetworkInterface, + WalletQueryErrorStatus, + }, }; use tari_template_lib::{ models::ResourceAddress, - types::{crypto::UtxoTagByte, TemplateAddress}, + prelude::RistrettoPublicKeyBytes, + types::{crypto::UtxoTag, TemplateAddress}, }; use tari_transaction::{Transaction, TransactionId}; use url::ParseError; @@ -67,7 +66,7 @@ impl IndexerJsonRpcNetworkInterface { Ok(client) } - pub fn set_endpoint(&mut self, endpoint: &str) -> Result<(), IndexerJrpcError> { + pub fn set_endpoint(&self, endpoint: &str) -> Result<(), IndexerJrpcError> { *self.indexer_jrpc_address.lock().unwrap() = Url::parse(endpoint)?; Ok(()) } @@ -191,21 +190,31 @@ impl WalletNetworkInterface for IndexerJsonRpcNetworkInterface { &self, resource_address: ResourceAddress, shard_state_versions: HashMap, - filter_tag_bytes: HashSet, - ) -> Result { + ) -> Result { let mut client = self.get_client()?; let resp = client .get_utxo_updates(GetUtxoUpdatesRequest { shard_state_versions, - filter_tag_bytes, resource_address, per_shard_limit: 100, }) .await?; - Ok(UtxoQueryResponse { - updates: resp.utxo_updates, - per_shard_high_watermark: resp.per_shard_high_watermark, - }) + Ok(resp.updates) + } + + async fn get_unspent_utxos( + &self, + resource_address: ResourceAddress, + tag_and_nonce_pairs: Vec<(UtxoTag, RistrettoPublicKeyBytes)>, + ) -> Result, Self::Error> { + let mut client = self.get_client()?; + let resp = client + .get_unspent_utxos(GetUnspentUtxosRequest { + resource_address, + tag_and_nonce_pairs, + }) + .await?; + Ok(resp.utxos) } async fn wait_until_ready(&self) -> Result<(), Self::Error> { diff --git a/crates/wallet/sdk_services/src/lib.rs b/crates/wallet/sdk_services/src/lib.rs index 170ad00952..2783b66995 100644 --- a/crates/wallet/sdk_services/src/lib.rs +++ b/crates/wallet/sdk_services/src/lib.rs @@ -2,5 +2,6 @@ // SPDX-License-Identifier: BSD-3-Clause pub mod indexer_jrpc_impl; +pub mod utxo_scanner; // TODO: move additional common wallet SDK services here taken out of walletd diff --git a/crates/wallet/sdk_services/src/utxo_scanner/error.rs b/crates/wallet/sdk_services/src/utxo_scanner/error.rs new file mode 100644 index 0000000000..d5ff6f018f --- /dev/null +++ b/crates/wallet/sdk_services/src/utxo_scanner/error.rs @@ -0,0 +1,34 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use tari_ootle_common_types::optional::IsNotFoundError; +use tari_ootle_wallet_sdk::{ + apis::{config::ConfigApiError, key_manager::KeyManagerApiError, stealth_outputs::StealthOutputsApiError}, + storage::WalletStorageError, +}; + +#[derive(Debug, thiserror::Error)] +pub enum StealthScannerApiError { + #[error("Config error: {0}")] + ConfigError(#[from] ConfigApiError), + #[error("Network interface error: {0}")] + NetworkInterfaceError(anyhow::Error), + #[error("Unexpected response: {details}")] + UnexpectedResponse { details: String }, + #[error("Store error: {0}")] + StoreError(#[from] WalletStorageError), + #[error("Stealth outputs error: {0}")] + StealthOutputsError(#[from] StealthOutputsApiError), + #[error("Key manager error: {0}")] + KeyManagerError(#[from] KeyManagerApiError), +} + +impl IsNotFoundError for StealthScannerApiError { + fn is_not_found_error(&self) -> bool { + match self { + Self::StoreError(e) => e.is_not_found_error(), + Self::StealthOutputsError(e) => e.is_not_found_error(), + _ => false, + } + } +} diff --git a/crates/wallet/sdk_services/src/utxo_scanner/mod.rs b/crates/wallet/sdk_services/src/utxo_scanner/mod.rs new file mode 100644 index 0000000000..e97d0aa485 --- /dev/null +++ b/crates/wallet/sdk_services/src/utxo_scanner/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +mod error; +mod scanner; +mod scanner_round; +mod utxo_recovery; + +pub use error::*; +pub use scanner::*; +pub(crate) use scanner_round::*; +pub use utxo_recovery::*; diff --git a/crates/wallet/sdk_services/src/utxo_scanner/scanner.rs b/crates/wallet/sdk_services/src/utxo_scanner/scanner.rs new file mode 100644 index 0000000000..1cd1d51872 --- /dev/null +++ b/crates/wallet/sdk_services/src/utxo_scanner/scanner.rs @@ -0,0 +1,46 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use tari_ootle_common_types::optional::IsNotFoundError; +use tari_ootle_wallet_sdk::{ + models::AccountWithPublicKey, + network::{StatusResponseError, WalletNetworkInterface}, + storage::WalletStore, + WalletSdk, +}; +use tari_template_lib::models::ResourceAddress; +use tokio::sync::watch; + +use crate::utxo_scanner::{StealthScannerApiError, UtxoScannerRound}; + +pub struct UtxoScanner { + sdk: WalletSdk, +} + +impl UtxoScanner +where + TStore: WalletStore, + TNetworkInterface: WalletNetworkInterface, + TNetworkInterface::Error: IsNotFoundError + StatusResponseError, +{ + pub fn new(sdk: WalletSdk) -> Self { + Self { sdk } + } + + pub async fn scan_and_recover_utxos( + &self, + account: &AccountWithPublicKey, + resource_address: &ResourceAddress, + notify_tx: &watch::Sender<()>, + ) -> Result<(), StealthScannerApiError> { + let network = self.sdk.config_api().get_network()?; + + let account_key = self.sdk.key_manager_api().derive_account_key(account.key_index())?; + + let mut scanner_round = + UtxoScannerRound::new(network, &self.sdk, notify_tx, account, &account_key, resource_address); + scanner_round.scan_for_utxo_updates().await?; + + Ok(()) + } +} diff --git a/crates/wallet/sdk_services/src/utxo_scanner/scanner_round.rs b/crates/wallet/sdk_services/src/utxo_scanner/scanner_round.rs new file mode 100644 index 0000000000..1fe8e3e44f --- /dev/null +++ b/crates/wallet/sdk_services/src/utxo_scanner/scanner_round.rs @@ -0,0 +1,270 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::collections::HashMap; + +use log::{debug, info, trace, warn}; +use tari_crypto::ristretto::RistrettoPublicKey; +use tari_engine_types::FromByteType; +use tari_ootle_common_types::{ + optional::{IsNotFoundError, Optional}, + shard::Shard, + Network, + NumPreshards, + StateVersion, +}; +use tari_ootle_wallet_sdk::{ + models::{AccountWithPublicKey, UtxoSpent, UtxoUnspent, WalletUtxoUpdate}, + network::{StatusResponseError, WalletNetworkInterface}, + storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, + WalletSdk, +}; +use tari_template_lib::models::ResourceAddress; +use tari_transaction_components::key_manager::tari_key_manager::DerivedKey; +use tokio::sync::watch; + +use crate::utxo_scanner::StealthScannerApiError; + +const LOG_TARGET: &str = "tari::ootle::wallet::scanner_round"; +// TODO: either fetch num preshards from the network or we should hardcode it to a single value for all apps +const NUM_PRESHARDS: NumPreshards = NumPreshards::P256; + +pub(crate) struct UtxoScannerRound<'a, TStore, TNetworkInterface> { + network: Network, + account: &'a AccountWithPublicKey, + account_key: &'a DerivedKey, + resource_address: &'a ResourceAddress, + + sdk: &'a WalletSdk, + notify_tx: &'a watch::Sender<()>, + + shard_state_versions_to_set: HashMap, + utxos_to_recover: Vec<(u64, UtxoUnspent)>, + utxos_to_spend: Vec, +} + +impl<'a, TStore, TNetworkInterface> UtxoScannerRound<'a, TStore, TNetworkInterface> +where + TStore: WalletStore, + TNetworkInterface: WalletNetworkInterface, + TNetworkInterface::Error: IsNotFoundError + StatusResponseError, +{ + pub fn new( + network: Network, + sdk: &'a WalletSdk, + notify_tx: &'a watch::Sender<()>, + account: &'a AccountWithPublicKey, + account_key: &'a DerivedKey, + resource_address: &'a ResourceAddress, + ) -> Self { + Self { + network, + sdk, + notify_tx, + account, + account_key, + resource_address, + shard_state_versions_to_set: HashMap::new(), + utxos_to_recover: Vec::new(), + utxos_to_spend: Vec::new(), + } + } + + pub async fn scan_for_utxo_updates(&mut self) -> Result<(), StealthScannerApiError> { + let mut any_found = false; + loop { + if !self.scan().await? { + break; + } + any_found = true; + } + + if any_found { + // Notify that there are new UTXOs to process + debug!(target: LOG_TARGET, "Notifying that new UTXOs are available for processing"); + let _ignore = self.notify_tx.send(()); + } + + Ok(()) + } + + async fn scan(&mut self) -> Result { + let mut shard_state_versions = self + .sdk + .store() + .with_read_tx(|tx| tx.shard_state_version_get(self.account.address(), self.resource_address))?; + + // Populate any missing shards with zero + for shard in NUM_PRESHARDS.all_shards_iter() { + shard_state_versions.entry(shard).or_insert_with(StateVersion::zero); + } + + info!( + target: LOG_TARGET, + "🔍️ Scanning for stealth outputs (account {}, resource {}, num shards {})", + self.account.address(), + self.resource_address, + shard_state_versions.len(), + ); + + let response = self + .sdk + .get_network_interface() + .query_stealth_utxo_updates(*self.resource_address, shard_state_versions) + .await + .map_err(|e| StealthScannerApiError::NetworkInterfaceError(e.into()))?; + + // Keep going until there are no more + if response.shard_updates.is_empty() { + info!( + target: LOG_TARGET, + "🔍️ Scan complete for account {}: No more stealth outputs found", + self.account.address() + ); + // Update state versions to avoid rescanning from previous versions that didnt contain any changes + self.sdk.store().with_write_tx(|tx| { + tx.shard_state_version_set_many( + self.account.address(), + self.resource_address, + response.per_shard_high_watermark, + ) + })?; + + return Ok(false); + } + + let num_received = response.shard_updates.len(); + let mut num_spent = 0; + + for (shard, update_set) in response.shard_updates { + self.shard_state_versions_to_set + .entry(shard) + .and_modify(|v| { + if update_set.max_state_version > *v { + *v = update_set.max_state_version + } + }) + .or_insert(update_set.max_state_version); + for update in update_set.updates { + match update { + WalletUtxoUpdate::Unspent(unspent) => { + if self.check_if_tag_matches(self.network, self.account_key, self.resource_address, &unspent)? { + debug!( + target: LOG_TARGET, + "🏷️ Stealth output tag {} matches. Queueing for recovery.", + unspent.tag + ); + self.utxos_to_recover.push((self.account.key_index(), unspent)); + } + }, + WalletUtxoUpdate::Spent(spent) => { + self.utxos_to_spend.push(spent); + }, + WalletUtxoUpdate::Burnt(burnt) => { + // NOTE: we treat burnt outputs the same as spent for now. In future, we may want to track them + // separately + self.utxos_to_spend.push(UtxoSpent { + id: burnt.id, + version: burnt.version, + }); + }, + } + } + } + + // Atomically persist all changes from this round + let num_recovered = self.utxos_to_recover.len(); + self.sdk.store().with_write_tx(|tx| { + // Mark UTXOs as spent (if they exist) + for spent in self.utxos_to_spend.drain(..) { + if Self::spend(tx, self.resource_address, spent)? { + num_spent += 1; + } + } + + // Queue up tag matching UTXOs for processing + tx.utxo_process_queue_extend(self.resource_address, self.utxos_to_recover.drain(..))?; + + // Update shard state versions + tx.shard_state_version_set_many( + self.account.address(), + self.resource_address, + self.shard_state_versions_to_set.drain(), + ) + })?; + + info!( + target: LOG_TARGET, + "Scan round complete for account {}: Validated the tag of {}/{} new stealth outputs, marked {} as spent", + self.account.address(), + num_recovered, + num_received, + num_spent + ); + + Ok(true) + } + + fn check_if_tag_matches( + &self, + network: Network, + account_key: &DerivedKey, + resource_address: &ResourceAddress, + unspent: &UtxoUnspent, + ) -> Result { + let Ok(public_nonce) = RistrettoPublicKey::try_from_byte_type(&unspent.public_nonce).inspect_err(|e| { + warn!( + target: LOG_TARGET, + "⚠️ Received a malformed public nonce while syncing output: {}. Ignoring output.", + e + ) + }) else { + return Ok(false); + }; + + let tag = self.sdk.stealth_crypto_api().derive_stealth_output_tag_from_sender( + network, + &account_key.key, + &public_nonce, + resource_address, + ); + + if tag != unspent.tag { + trace!( + target: LOG_TARGET, + "Stealth output tag {} does not match (likely not our output)", unspent.tag + ); + return Ok(false); + } + + debug!( + target: LOG_TARGET, + "Stealth output tag {} matches. Will request full output.", unspent.tag + ); + + Ok(true) + } + + fn spend( + tx: &mut TStore::WriteTransaction<'_>, + resource_address: &ResourceAddress, + spent: UtxoSpent, + ) -> Result { + match tx + .stealth_outputs_mark_as_spent(resource_address, &spent.id) + .optional()? + { + Some(_) => { + info!(target: LOG_TARGET, "Marked stealth output {} as spent", spent.id); + Ok(true) + }, + None => { + debug!( + target: LOG_TARGET, + "Spent UTXO {} not found in wallet store (likely not our output)", spent.id + ); + Ok(false) + }, + } + } +} diff --git a/crates/wallet/sdk_services/src/utxo_scanner/utxo_recovery.rs b/crates/wallet/sdk_services/src/utxo_scanner/utxo_recovery.rs new file mode 100644 index 0000000000..d22f2f7136 --- /dev/null +++ b/crates/wallet/sdk_services/src/utxo_scanner/utxo_recovery.rs @@ -0,0 +1,222 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::{array, time::Duration}; + +use log::*; +use tari_engine_types::{UtxoAddress, UtxoId, UtxoOutput}; +use tari_ootle_common_types::{ + optional::{IsNotFoundError, Optional}, + Network, +}; +use tari_ootle_wallet_sdk::{ + network::{StatusResponseError, WalletNetworkInterface}, + storage::{WalletStore, WalletStoreReader, WalletStoreWriter}, + WalletSdk, +}; +use tari_template_lib::models::ResourceAddress; +use tokio::sync::watch; + +use crate::utxo_scanner::StealthScannerApiError; + +const LOG_TARGET: &str = "tari::ootle::wallet::utxo_recovery"; + +pub struct UtxoRecovery { + sdk: WalletSdk, +} + +impl UtxoRecovery +where + TStore: WalletStore, + TNetworkInterface: WalletNetworkInterface, + TNetworkInterface::Error: IsNotFoundError + StatusResponseError, +{ + pub fn new(sdk: WalletSdk) -> Self { + Self { sdk } + } + + pub async fn run(mut self, mut waker: watch::Receiver<()>) -> anyhow::Result<()> { + info!(target: LOG_TARGET, "🚀 Starting UTXO recovery process"); + loop { + // Process the UTXO validation queue until empty. NOTE: this comes before waiting on the waker so that we + // check the queue on startup + if let Err(err) = self.process_utxo_validation_queue().await { + match &err { + // Perhaps intermittent network issue, just log and continue + StealthScannerApiError::NetworkInterfaceError(_) | + StealthScannerApiError::UnexpectedResponse { .. } => { + warn!(target: LOG_TARGET, "⚠️ UTXO recovery process encountered a recoverable error: {}", err); + // Sleep to prevent fast spinning + tokio::time::sleep(Duration::from_secs(5)).await; + // Continue processing the queue + continue; + }, + _ => { + // Non-recoverable error + error!(target: LOG_TARGET, "❌ UTXO recovery process encountered a non-recoverable error: {}", err); + // CRASH + return Err(err.into()); + }, + } + } + + if waker.changed().await.is_err() { + info!(target: LOG_TARGET, "🔕 UTXO recovery process terminating because the notification channel closed"); + break; + } + } + Ok(()) + } + + async fn process_utxo_validation_queue(&mut self) -> Result<(), StealthScannerApiError> { + loop { + let batch = self + .sdk + .store() + .with_read_tx(|tx| tx.utxo_process_queue_fetch_batch(100))?; + + if batch.is_empty() { + debug!(target: LOG_TARGET, "✅ No more UTXOs to process"); + return Ok(()); + } + + for (resource_addr, tag_and_nonce_to_account_key_map) in &batch { + if tag_and_nonce_to_account_key_map.is_empty() { + error!(target: LOG_TARGET, "❓️ NEVER HAPPEN: Asked indexer for zero UTXOs for resource {}.", resource_addr); + } + let tag_and_nonce_pairs = tag_and_nonce_to_account_key_map.keys().copied().collect(); + + // max 3.3kB per request (excl underlying protocol overhead *cough* json + hex) + let utxos = self + .sdk + .get_network_interface() + .get_unspent_utxos(*resource_addr, tag_and_nonce_pairs) + .await + .map_err(|e| StealthScannerApiError::NetworkInterfaceError(e.into()))?; + + 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 the will + // sleep and retry. + return Err(StealthScannerApiError::UnexpectedResponse { + details: format!( + "{} UTXOs requested but network returned an empty set for resource {}.", + tag_and_nonce_to_account_key_map.len(), + resource_addr + ), + }); + } + + if utxos.len() != tag_and_nonce_to_account_key_map.len() { + // We could error as above, but let's process what we got + warn!(target: LOG_TARGET, "⚠️ Mismatch in number of UTXOs queried ({}) vs returned by indexer ({}).", + tag_and_nonce_to_account_key_map.len(), + utxos.len(), + ); + } + + // Group by account key index + let utxos = utxos + .into_iter() + .filter_map(|(id, utxo)| { + let is_frozen = utxo.is_frozen; + let Some(output) = utxo.into_output() else { + warn!(target: LOG_TARGET, "❓️ NEVER HAPPEN: Indexer returned burnt UTXO {}. Ignoring", id); + return None; + }; + Some((id, output, is_frozen)) + }) + .filter_map(|(id, output, is_frozen)| { + let tag_and_nonce_pair = (output.tag, output.output.public_nonce); + let Some(account_key_index) = tag_and_nonce_to_account_key_map.get(&tag_and_nonce_pair).copied() else { + warn!(target: LOG_TARGET, "❓️ NEVER HAPPEN: Indexer returned UTXO with tag {}, nonce {} that we didn't request. Ignoring", output.tag, output.output.public_nonce); + return None; + }; + + Some(FoundUtxo { + account_key_index, + id, + output, + is_frozen, + }) + }) + .collect(); + + self.process_recovered_utxos(*resource_addr, utxos)?; + } + } + } + + fn process_recovered_utxos( + &self, + resource_address: ResourceAddress, + utxos_to_recover: Vec, + ) -> Result<(), StealthScannerApiError> { + let num_attempted = utxos_to_recover.len(); + let mut num_recovered = 0; + for utxo in utxos_to_recover { + if self.check_unspent_utxo_and_store(self.sdk.network(), resource_address, utxo)? { + num_recovered += 1; + } + } + if num_attempted > 0 { + info!( + target: LOG_TARGET, + "✅ Recovered {} out of {} applicable UTXOs", + num_recovered, + num_attempted, + ); + } + + Ok(()) + } + + fn check_unspent_utxo_and_store( + &self, + network: Network, + resource_address: ResourceAddress, + found: FoundUtxo, + ) -> Result { + let account_key = self + .sdk + .key_manager_api() + .derive_account_key_pair(found.account_key_index)?; + let outputs_api = self.sdk.stealth_outputs_api(); + + let address = UtxoAddress::new(resource_address, found.id); + let commitment = found.id.into_commitment_bytes(); + let Some(output) = outputs_api.validate_utxo( + array::from_ref(&account_key), + network, + resource_address, + commitment, + &found.output, + found.is_frozen, + )? + else { + // The output could be burnt, frozen, or otherwise invalid. If we already have it in the db, update its + // status, if not, do nothing. + outputs_api + .update_utxo_status(&address, None, None, Some(found.is_frozen)) + .optional()?; + + return Ok(false); + }; + + outputs_api.upsert_utxo(&output)?; + self.sdk.store().with_write_tx(|tx| { + tx.utxo_process_queue_remove_item(resource_address, found.output.tag, found.output.output.public_nonce) + })?; + info!(target: LOG_TARGET, "💰️ Recovered stealth output {} for account {}", address, account_key.public_key); + Ok(true) + } +} + +struct FoundUtxo { + account_key_index: u64, + output: UtxoOutput, + id: UtxoId, + is_frozen: bool, +} 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 fe5899ce99..7306474de5 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 @@ -125,8 +125,8 @@ CREATE TABLE resources CREATE UNIQUE INDEX resources_uniq_address ON resources (address); --- Outputs -CREATE TABLE outputs +-- Confidential Outputs +CREATE TABLE confidential_outputs ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, account_id INTEGER NOT NULL REFERENCES accounts (id), @@ -145,8 +145,8 @@ CREATE TABLE outputs updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE UNIQUE INDEX outputs_uniq_commitment ON outputs (commitment); -CREATE INDEX outputs_idx_account_status ON outputs (account_id, status); +CREATE UNIQUE INDEX confidential_outputs_uniq_commitment ON confidential_outputs (commitment); +CREATE INDEX confidential_outputs_idx_account_status ON confidential_outputs (account_id, status); -- Locks CREATE TABLE locks @@ -251,4 +251,17 @@ CREATE TABLE shard_state_versions ); CREATE UNIQUE INDEX shard_state_versions_account_resource_shard_uniq ON shard_state_versions (account_id, resource_id, shard); -CREATE INDEX shard_state_versions_account_resource_shard_state_version_idx ON shard_state_versions (account_id, resource_id, shard, state_version); \ No newline at end of file +CREATE INDEX shard_state_versions_account_resource_shard_state_version_idx ON shard_state_versions (account_id, resource_id, shard, state_version); + +-- UTXO process queue +CREATE TABLE utxo_process_queue +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_key_index BIGINT NOT NULL, + resource_address TEXT NOT NULL, + utxo_tag INT NOT NULL, + public_nonce TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX utxo_process_queue_resource_tag_public_nonce_uniq ON utxo_process_queue (resource_address, utxo_tag, public_nonce); \ No newline at end of file diff --git a/crates/wallet/storage_sqlite/src/helpers.rs b/crates/wallet/storage_sqlite/src/helpers.rs deleted file mode 100644 index ce2933bc2e..0000000000 --- a/crates/wallet/storage_sqlite/src/helpers.rs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2025 The Tari Project -// SPDX-License-Identifier: BSD-3-Clause - -use time::{PrimitiveDateTime, UtcDateTime}; - -pub fn now() -> PrimitiveDateTime { - let now = UtcDateTime::now(); - PrimitiveDateTime::new(now.date(), now.time()) -} diff --git a/crates/wallet/storage_sqlite/src/lib.rs b/crates/wallet/storage_sqlite/src/lib.rs index 520eec78ca..e2284793a9 100644 --- a/crates/wallet/storage_sqlite/src/lib.rs +++ b/crates/wallet/storage_sqlite/src/lib.rs @@ -3,7 +3,6 @@ #[macro_use] extern crate diesel; -mod helpers; mod models; mod reader; mod schema; diff --git a/crates/wallet/storage_sqlite/src/models/mod.rs b/crates/wallet/storage_sqlite/src/models/mod.rs index 1769874bb4..8d99f442c4 100644 --- a/crates/wallet/storage_sqlite/src/models/mod.rs +++ b/crates/wallet/storage_sqlite/src/models/mod.rs @@ -17,16 +17,18 @@ mod authored_template; mod non_fungible_tokens; mod resource; mod stealth_output; +mod utxo_process_queue; mod webauthn_registrations; -pub use account::Account; -pub use authored_template::AuthoredTemplate; -pub use config::Config; -pub use non_fungible_tokens::NonFungibleToken; -pub use output::ConfidentialOutput; +pub use account::*; +pub use authored_template::*; +pub use config::*; +pub use non_fungible_tokens::*; +pub use output::*; pub use resource::*; pub use stealth_output::*; pub use substate::Substate; -pub use transaction::TransactionRecord; +pub use transaction::*; +pub use utxo_process_queue::*; pub use vault::Vault; pub use webauthn_registrations::*; diff --git a/crates/wallet/storage_sqlite/src/models/output.rs b/crates/wallet/storage_sqlite/src/models/output.rs index 1f6111e890..67125f77cf 100644 --- a/crates/wallet/storage_sqlite/src/models/output.rs +++ b/crates/wallet/storage_sqlite/src/models/output.rs @@ -8,10 +8,10 @@ use tari_template_lib::{ }; use time::PrimitiveDateTime; -use crate::schema::outputs; +use crate::schema::confidential_outputs; #[derive(Debug, Clone, Identifiable, Queryable)] -#[diesel(table_name = outputs)] +#[diesel(table_name = confidential_outputs)] pub struct ConfidentialOutput { pub id: i32, pub account_id: i32, diff --git a/crates/wallet/storage_sqlite/src/models/stealth_output.rs b/crates/wallet/storage_sqlite/src/models/stealth_output.rs index 645c1980b6..15288bac5b 100644 --- a/crates/wallet/storage_sqlite/src/models/stealth_output.rs +++ b/crates/wallet/storage_sqlite/src/models/stealth_output.rs @@ -1,10 +1,11 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +use diesel::dsl; use tari_ootle_wallet_sdk::{models::StealthOutputModel, storage::WalletStorageError}; use tari_template_lib::{ models::{ComponentAddress, EncryptedData}, - prelude::crypto::UtxoTagByte, + prelude::crypto::UtxoTag, types::{amount, crypto::RistrettoPublicKeyBytes}, }; use time::PrimitiveDateTime; @@ -69,15 +70,7 @@ impl StealthOutput { details: format!("Corrupt db: invalid encrypted data length {len}"), } })?, - tag_byte: UtxoTagByte::new( - self.tag_byte - .try_into() - .map_err(|_| WalletStorageError::DecodingError { - operation: "try_into_output", - item: "output", - details: format!("Corrupt db: invalid tag byte '{}'", self.tag_byte), - })?, - ), + tag_byte: UtxoTag::new(self.tag_byte as u32), status: self.status.parse().map_err(|_| WalletStorageError::DecodingError { operation: "try_into_output", item: "output", @@ -91,11 +84,11 @@ impl StealthOutput { } } -#[derive(AsChangeset, Default)] +#[derive(AsChangeset)] #[diesel(table_name = stealth_outputs)] pub(crate) struct StealthOutputUpdate<'a> { pub status: Option<&'a str>, pub is_burnt: Option, pub is_frozen: Option, - pub updated_at: Option, + pub updated_at: dsl::now, } diff --git a/crates/wallet/storage_sqlite/src/models/utxo_process_queue.rs b/crates/wallet/storage_sqlite/src/models/utxo_process_queue.rs new file mode 100644 index 0000000000..3921df661d --- /dev/null +++ b/crates/wallet/storage_sqlite/src/models/utxo_process_queue.rs @@ -0,0 +1,14 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use diesel::Queryable; + +#[derive(Debug, Clone, Queryable)] +pub struct UtxoProcessQueue { + pub _id: i32, + pub account_key_index: i64, + pub resource_address: String, + pub utxo_tag: i32, + pub public_nonce: String, + pub _created_at: time::PrimitiveDateTime, +} diff --git a/crates/wallet/storage_sqlite/src/reader.rs b/crates/wallet/storage_sqlite/src/reader.rs index da85a6dc97..eb7c9ae683 100644 --- a/crates/wallet/storage_sqlite/src/reader.rs +++ b/crates/wallet/storage_sqlite/src/reader.rs @@ -46,13 +46,13 @@ use tari_ootle_wallet_sdk::{ WalletTransaction, WebauthnRegistrationPasskeyModel, }, - storage::{WalletStorageError, WalletStoreReader}, + storage::{TagAndPublicNoncePair, WalletStorageError, WalletStoreReader}, }; use tari_template_lib::{ models::{ResourceAddress, VaultId}, prelude::{ComponentAddress, NonFungibleId, PedersenCommitmentBytes, RistrettoPublicKeyBytes}, resource::ResourceType, - types::TemplateAddress, + types::{crypto::UtxoTag, TemplateAddress}, }; use tari_transaction::TransactionId; use webauthn_rs::prelude::Passkey; @@ -61,7 +61,7 @@ use crate::{ models, models::{AuthoredTemplate, WebauthnRegistrationPasskey}, schema::accounts, - serialization::{deserialize_json, serialize_hex}, + serialization::{deserialize_hex_try_from, deserialize_json, serialize_hex}, }; const LOG_TARGET: &str = "tari::ootle::wallet_sdk::storage_sqlite::reader"; @@ -639,7 +639,7 @@ impl WalletStoreReader for ReadTransaction<'_> { // -------------------------------- Outputs -------------------------------- // fn outputs_get_unspent_balance(&mut self, vault_address: &VaultId) -> Result { - use crate::schema::{outputs, vaults}; + use crate::schema::{confidential_outputs, vaults}; let vault_id = vaults::table .filter(vaults::address.eq(vault_address.to_string())) @@ -653,10 +653,10 @@ impl WalletStoreReader for ReadTransaction<'_> { key: vault_address.to_string(), })?; - let balance = outputs::table - .select(sum(outputs::value)) - .filter(outputs::vault_id.eq(vault_id)) - .filter(outputs::status.eq(OutputStatus::Unspent.as_key_str())) + let balance = confidential_outputs::table + .select(sum(confidential_outputs::value)) + .filter(confidential_outputs::vault_id.eq(vault_id)) + .filter(confidential_outputs::status.eq(OutputStatus::Unspent.as_key_str())) .first::>(self.connection()) .map_err(|e| WalletStorageError::general("outputs_get_unspent_balance", e))?; @@ -668,10 +668,10 @@ impl WalletStoreReader for ReadTransaction<'_> { lock_id: WalletLockId, ) -> Result, WalletStorageError> { const OPERATION: &str = "outputs_get_locked_by_lock_id"; - use crate::schema::{accounts, outputs, vaults}; + use crate::schema::{accounts, confidential_outputs, vaults}; - let rows = outputs::table - .filter(outputs::lock_id.eq(lock_id)) + let rows = confidential_outputs::table + .filter(confidential_outputs::lock_id.eq(lock_id)) .get_results::(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; @@ -702,7 +702,7 @@ impl WalletStoreReader for ReadTransaction<'_> { }) .transpose()?; - let outputs = rows + let confidential_outputs = rows .into_iter() .map(|row| { let vault_id = row.vault_id; @@ -712,7 +712,7 @@ impl WalletStoreReader for ReadTransaction<'_> { ) }) .collect::>()?; - Ok(outputs) + Ok(confidential_outputs) } fn outputs_get_by_commitment( @@ -720,18 +720,18 @@ impl WalletStoreReader for ReadTransaction<'_> { vault_id: &VaultId, commitment: &PedersenCommitmentBytes, ) -> Result { - use crate::schema::{accounts, outputs, vaults}; + use crate::schema::{accounts, confidential_outputs, vaults}; - let row = outputs::table + let row = confidential_outputs::table .filter( - outputs::vault_id.eq(vaults::table + confidential_outputs::vault_id.eq(vaults::table .select(vaults::id) .filter(vaults::address.eq(vault_id.to_string())) .limit(1) .single_value() .assume_not_null()), ) - .filter(outputs::commitment.eq(serialize_hex(commitment))) + .filter(confidential_outputs::commitment.eq(serialize_hex(commitment))) .first::(self.connection()) .optional() .map_err(|e| WalletStorageError::general("outputs_get_by_commitment", e))? @@ -762,7 +762,7 @@ impl WalletStoreReader for ReadTransaction<'_> { account_addr: &ComponentAddress, status: OutputStatus, ) -> Result, WalletStorageError> { - use crate::schema::{accounts, outputs, vaults}; + use crate::schema::{accounts, confidential_outputs, vaults}; let account_id = accounts::table .filter(accounts::address.eq(account_addr.to_string())) @@ -770,9 +770,9 @@ impl WalletStoreReader for ReadTransaction<'_> { .first::(self.connection()) .map_err(|e| WalletStorageError::general("outputs_get_by_account_and_status", e))?; - let rows = outputs::table - .filter(outputs::account_id.eq(account_id)) - .filter(outputs::status.eq(status.as_key_str())) + let rows = confidential_outputs::table + .filter(confidential_outputs::account_id.eq(account_id)) + .filter(confidential_outputs::status.eq(status.as_key_str())) .load::(self.connection()) .map_err(|e| WalletStorageError::general("outputs_get_by_account_and_status", e))?; @@ -1196,6 +1196,38 @@ impl WalletStoreReader for ReadTransaction<'_> { } Ok(versions) } + + fn utxo_process_queue_fetch_batch( + &mut self, + batch_size: usize, + ) -> Result>, WalletStorageError> { + const OPERATION: &str = "utxo_process_queue_fetch_batch"; + use crate::schema::utxo_process_queue; + + let rows = utxo_process_queue::table + .order(utxo_process_queue::id.asc()) + .limit(i64::try_from(batch_size).unwrap_or(i64::MAX)) + .get_results::(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + + let mut result = HashMap::new(); + for row in &rows { + let resource_address = + ResourceAddress::from_str(&row.resource_address).map_err(|e| WalletStorageError::DecodingError { + operation: OPERATION, + item: "resource_address", + details: format!("Corrupt db: invalid resource address '{}': {}", row.resource_address, e), + })?; + let tag = UtxoTag::new(row.utxo_tag as u32); + let public_nonce = deserialize_hex_try_from(&row.public_nonce)?; + result + .entry(resource_address) + .or_insert_with(HashMap::new) + .insert((tag, public_nonce), row.account_key_index as u64); + } + + Ok(result) + } } impl Drop for ReadTransaction<'_> { diff --git a/crates/wallet/storage_sqlite/src/schema.rs b/crates/wallet/storage_sqlite/src/schema.rs index 9cf4f9e2e2..cfb32d6346 100644 --- a/crates/wallet/storage_sqlite/src/schema.rs +++ b/crates/wallet/storage_sqlite/src/schema.rs @@ -37,6 +37,25 @@ diesel::table! { } } +diesel::table! { + confidential_outputs (id) { + id -> Integer, + account_id -> Integer, + vault_id -> Integer, + commitment -> Text, + value -> BigInt, + sender_public_nonce -> Nullable, + encryption_secret_key_index -> BigInt, + public_asset_tag -> Nullable, + status -> Text, + locked_at -> Nullable, + lock_id -> Nullable, + encrypted_data -> Binary, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + diesel::table! { config (id) { id -> Integer, @@ -81,25 +100,6 @@ diesel::table! { } } -diesel::table! { - outputs (id) { - id -> Integer, - account_id -> Integer, - vault_id -> Integer, - commitment -> Text, - value -> BigInt, - sender_public_nonce -> Nullable, - encryption_secret_key_index -> BigInt, - public_asset_tag -> Nullable, - status -> Text, - locked_at -> Nullable, - lock_id -> Nullable, - encrypted_data -> Binary, - created_at -> Timestamp, - updated_at -> Timestamp, - } -} - diesel::table! { resources (id) { id -> Integer, @@ -187,6 +187,17 @@ diesel::table! { } } +diesel::table! { + utxo_process_queue (id) { + id -> Integer, + account_key_index -> BigInt, + resource_address -> Text, + utxo_tag -> Integer, + public_nonce -> Text, + created_at -> Timestamp, + } +} + diesel::table! { vaults (id) { id -> Integer, @@ -224,9 +235,9 @@ diesel::table! { } } +diesel::joinable!(confidential_outputs -> accounts (account_id)); +diesel::joinable!(confidential_outputs -> vaults (vault_id)); diesel::joinable!(non_fungible_tokens -> vaults (vault_id)); -diesel::joinable!(outputs -> accounts (account_id)); -diesel::joinable!(outputs -> vaults (vault_id)); diesel::joinable!(shard_state_versions -> accounts (account_id)); diesel::joinable!(shard_state_versions -> resources (resource_id)); diesel::joinable!(vaults -> accounts (account_id)); @@ -237,16 +248,17 @@ diesel::allow_tables_to_appear_in_same_query!( accounts, auth_status, authored_templates, + confidential_outputs, config, key_manager_states, locks, non_fungible_tokens, - outputs, resources, shard_state_versions, stealth_outputs, substates, transactions, + utxo_process_queue, 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 b4d13483ce..5a9fb026ca 100644 --- a/crates/wallet/storage_sqlite/src/writer.rs +++ b/crates/wallet/storage_sqlite/src/writer.rs @@ -10,6 +10,7 @@ use std::{ }; use diesel::{ + dsl, BoolExpressionMethods, NullableExpressionMethods, OptionalExtension, @@ -20,7 +21,7 @@ use diesel::{ use log::*; use serde::Serialize; use tari_bor::json_encoding::CborValueJsonSerializeWrapper; -use tari_engine_types::{resource::Resource, substate::SubstateId, UtxoAddress}; +use tari_engine_types::{resource::Resource, substate::SubstateId, UtxoAddress, UtxoId}; use tari_ootle_common_types::{shard::Shard, StateVersion, VersionedSubstateIdRef}; use tari_ootle_wallet_sdk::{ models::{ @@ -33,6 +34,7 @@ use tari_ootle_wallet_sdk::{ StealthOutputModel, SubstateModel, TransactionStatus, + UtxoUnspent, VaultModel, WalletLockId, WalletTransactionUpdate, @@ -41,7 +43,7 @@ use tari_ootle_wallet_sdk::{ }; use tari_template_lib::{ models::{EncryptedData, NonFungibleId, ResourceAddress, VaultId}, - prelude::{ComponentAddress, PedersenCommitmentBytes, RistrettoPublicKeyBytes}, + prelude::{crypto::UtxoTag, ComponentAddress, PedersenCommitmentBytes, RistrettoPublicKeyBytes}, types::{Amount, TemplateAddress}, }; use tari_transaction::{Transaction, TransactionId}; @@ -51,7 +53,6 @@ use webauthn_rs::prelude::Passkey; use crate::{ diesel::ExpressionMethods, - helpers, models, models::StealthOutputUpdate, reader::ReadTransaction, @@ -813,7 +814,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { vault_id: &VaultId, lock_id: WalletLockId, ) -> Result { - use crate::schema::{accounts, outputs, vaults}; + use crate::schema::{accounts, confidential_outputs, vaults}; let vault_db_id = vaults::table .select(vaults::id) @@ -821,10 +822,10 @@ impl WalletStoreWriter for WriteTransaction<'_> { .first::(self.connection()) .map_err(|e| WalletStorageError::general("outputs_lock_smallest_amount", e))?; - let locked_output = outputs::table - .filter(outputs::vault_id.eq(vault_db_id)) - .filter(outputs::status.eq(OutputStatus::Unspent.as_key_str())) - .order_by(outputs::value.asc()) + let locked_output = confidential_outputs::table + .filter(confidential_outputs::vault_id.eq(vault_db_id)) + .filter(confidential_outputs::status.eq(OutputStatus::Unspent.as_key_str())) + .order_by(confidential_outputs::value.asc()) .first::(self.connection()) .optional() .map_err(|e| WalletStorageError::general("outputs_lock_smallest_amount", e))?; @@ -842,13 +843,13 @@ impl WalletStoreWriter for WriteTransaction<'_> { .map_err(|e| WalletStorageError::general("outputs_lock_smallest_amount", e))?; let changeset = ( - outputs::status.eq(OutputStatus::LockedForSpend.as_key_str()), - outputs::lock_id.eq(lock_id), - outputs::locked_at.eq(diesel::dsl::now), + confidential_outputs::status.eq(OutputStatus::LockedForSpend.as_key_str()), + confidential_outputs::lock_id.eq(lock_id), + confidential_outputs::locked_at.eq(diesel::dsl::now), ); - diesel::update(outputs::table) + diesel::update(confidential_outputs::table) .set(changeset) - .filter(outputs::id.eq(locked_output.id)) + .filter(confidential_outputs::id.eq(locked_output.id)) .execute(self.connection()) .map_err(|e| WalletStorageError::general("outputs_lock_smallest_amount", e))?; @@ -894,7 +895,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { } fn outputs_insert(&mut self, output: ConfidentialOutputModel) -> Result<(), WalletStorageError> { - use crate::schema::{accounts, outputs, vaults}; + use crate::schema::{accounts, confidential_outputs, vaults}; let account_id = accounts::table .select(accounts::id) @@ -908,18 +909,18 @@ impl WalletStoreWriter for WriteTransaction<'_> { .first::(self.connection()) .map_err(|e| WalletStorageError::general("outputs_insert", e))?; - diesel::insert_into(outputs::table) + diesel::insert_into(confidential_outputs::table) .values(( - outputs::account_id.eq(account_id), - outputs::vault_id.eq(vault_id), - outputs::commitment.eq(output.commitment.to_hex()), + confidential_outputs::account_id.eq(account_id), + confidential_outputs::vault_id.eq(vault_id), + confidential_outputs::commitment.eq(output.commitment.to_hex()), // TODO: allow arbitrary precision in wallet - outputs::value.eq(output.value.to_u64_checked().expect("value overflow u64") as i64), - outputs::sender_public_nonce.eq(output.sender_public_nonce.map(|pk| pk.to_hex())), - outputs::encryption_secret_key_index.eq(output.encryption_secret_key_index as i64), - outputs::encrypted_data.eq(output.encrypted_data.as_ref()), - outputs::status.eq(output.status.as_key_str()), - outputs::lock_id.eq(output.lock_id), + confidential_outputs::value.eq(output.value.to_u64_checked().expect("value overflow u64") as i64), + confidential_outputs::sender_public_nonce.eq(output.sender_public_nonce.map(|pk| pk.to_hex())), + confidential_outputs::encryption_secret_key_index.eq(output.encryption_secret_key_index as i64), + confidential_outputs::encrypted_data.eq(output.encrypted_data.as_ref()), + confidential_outputs::status.eq(output.status.as_key_str()), + confidential_outputs::lock_id.eq(output.lock_id), )) .execute(self.connection()) .map_err(|e| WalletStorageError::general("outputs_insert", e))?; @@ -928,28 +929,28 @@ impl WalletStoreWriter for WriteTransaction<'_> { } fn outputs_finalize_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { - use crate::schema::outputs; + use crate::schema::confidential_outputs; - // Unlock locked unconfirmed outputs - diesel::update(outputs::table) - .filter(outputs::lock_id.eq(lock_id)) - .filter(outputs::status.eq(OutputStatus::LockedUnconfirmed.as_key_str())) + // Unlock locked unconfirmed confidential_outputs + diesel::update(confidential_outputs::table) + .filter(confidential_outputs::lock_id.eq(lock_id)) + .filter(confidential_outputs::status.eq(OutputStatus::LockedUnconfirmed.as_key_str())) .set(( - outputs::status.eq(OutputStatus::Unspent.as_key_str()), - outputs::lock_id.eq::>(None), - outputs::locked_at.eq::>(None), + confidential_outputs::status.eq(OutputStatus::Unspent.as_key_str()), + confidential_outputs::lock_id.eq::>(None), + confidential_outputs::locked_at.eq::>(None), )) .execute(self.connection()) .map_err(|e| WalletStorageError::general("outputs_finalize_by_proof_id", e))?; - // Mark locked outputs as spent - diesel::update(outputs::table) - .filter(outputs::lock_id.eq(lock_id)) - .filter(outputs::status.eq(OutputStatus::LockedForSpend.as_key_str())) + // Mark locked confidential_outputs as spent + diesel::update(confidential_outputs::table) + .filter(confidential_outputs::lock_id.eq(lock_id)) + .filter(confidential_outputs::status.eq(OutputStatus::LockedForSpend.as_key_str())) .set(( - outputs::status.eq(OutputStatus::Spent.as_key_str()), - outputs::lock_id.eq::>(None), - outputs::locked_at.eq::>(None), + confidential_outputs::status.eq(OutputStatus::Spent.as_key_str()), + confidential_outputs::lock_id.eq::>(None), + confidential_outputs::locked_at.eq::>(None), )) .execute(self.connection()) .map_err(|e| WalletStorageError::general("outputs_finalize_by_proof_id", e))?; @@ -958,24 +959,24 @@ impl WalletStoreWriter for WriteTransaction<'_> { } fn outputs_release_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { - use crate::schema::outputs; + use crate::schema::confidential_outputs; - // Unlock locked unspent outputs - diesel::update(outputs::table) - .filter(outputs::lock_id.eq(lock_id)) - .filter(outputs::status.eq(OutputStatus::LockedForSpend.as_key_str())) + // Unlock locked unspent confidential_outputs + diesel::update(confidential_outputs::table) + .filter(confidential_outputs::lock_id.eq(lock_id)) + .filter(confidential_outputs::status.eq(OutputStatus::LockedForSpend.as_key_str())) .set(( - outputs::status.eq(OutputStatus::Unspent.as_key_str()), - outputs::lock_id.eq::>(None), - outputs::locked_at.eq::>(None), + confidential_outputs::status.eq(OutputStatus::Unspent.as_key_str()), + confidential_outputs::lock_id.eq::>(None), + confidential_outputs::locked_at.eq::>(None), )) .execute(self.connection()) .map_err(|e| WalletStorageError::general("outputs_unlock_by_proof_id", e))?; - // Remove outputs that were created by this lock - diesel::delete(outputs::table) - .filter(outputs::status.eq(OutputStatus::LockedUnconfirmed.as_key_str())) - .filter(outputs::lock_id.eq(lock_id)) + // Remove confidential_outputs that were created by this lock + diesel::delete(confidential_outputs::table) + .filter(confidential_outputs::status.eq(OutputStatus::LockedUnconfirmed.as_key_str())) + .filter(confidential_outputs::lock_id.eq(lock_id)) .execute(self.connection()) .map_err(|e| WalletStorageError::general("outputs_unlock_by_proof_id", e))?; @@ -1057,7 +1058,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { stealth_outputs::sender_public_nonce.eq(serialize_hex(output.sender_public_nonce)), stealth_outputs::encryption_secret_key_index.eq(output.encryption_secret_key_index as i64), stealth_outputs::encrypted_data.eq(output.encrypted_data.as_ref()), - stealth_outputs::tag_byte.eq(i32::from(output.tag_byte.as_byte())), + stealth_outputs::tag_byte.eq(output.tag_byte.value() as i32), stealth_outputs::is_on_chain.eq(output.is_on_chain), stealth_outputs::status.eq(output.status.as_key_str()), stealth_outputs::is_burnt.eq(output.is_burnt), @@ -1070,7 +1071,11 @@ impl WalletStoreWriter for WriteTransaction<'_> { Ok(()) } - fn stealth_outputs_mark_as_spent(&mut self, address: &UtxoAddress) -> Result<(), WalletStorageError> { + fn stealth_outputs_mark_as_spent( + &mut self, + resource_address: &ResourceAddress, + id: &UtxoId, + ) -> Result<(), WalletStorageError> { const OPERATION: &str = "stealth_outputs_mark_as_spent"; use crate::schema::stealth_outputs; @@ -1079,9 +1084,10 @@ impl WalletStoreWriter for WriteTransaction<'_> { stealth_outputs::status.eq(OutputStatus::Spent.as_key_str()), stealth_outputs::lock_id.eq::>(None), stealth_outputs::locked_at.eq::>(None), + stealth_outputs::updated_at.eq(dsl::now), )) - .filter(stealth_outputs::resource_address.eq(address.resource_address().to_string())) - .filter(stealth_outputs::commitment.eq(serialize_hex(address.id().into_commitment_bytes()))) + .filter(stealth_outputs::resource_address.eq(resource_address.to_string())) + .filter(stealth_outputs::commitment.eq(serialize_hex(id.into_commitment_bytes()))) .execute(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; @@ -1089,7 +1095,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { return Err(WalletStorageError::NotFound { operation: OPERATION, entity: "stealth_output".to_string(), - key: format!("address={address}"), + key: format!("resource_address={}, id={}", resource_address, id), }); } @@ -1113,7 +1119,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { .execute(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; - // Mark locked outputs as spent + // 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())) @@ -1174,7 +1180,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { is_burnt, is_frozen, status: status.map(|s| s.as_key_str()), - updated_at: Some(helpers::now()), + updated_at: dsl::now, }; let num_rows = diesel::update(stealth_outputs::table) @@ -1409,6 +1415,65 @@ impl WalletStoreWriter for WriteTransaction<'_> { Ok(()) } + + fn utxo_process_queue_extend>( + &mut self, + resource_address: &ResourceAddress, + items: I, + ) -> Result<(), WalletStorageError> { + const OPERATION: &str = "utxo_process_queue_extend"; + use crate::schema::utxo_process_queue; + + for (account_key_index, unspent) in items { + diesel::insert_into(utxo_process_queue::table) + .values(( + utxo_process_queue::account_key_index.eq(account_key_index as i64), + utxo_process_queue::utxo_tag.eq(unspent.tag.value() as i32), + utxo_process_queue::public_nonce.eq(serialize_hex(unspent.public_nonce)), + utxo_process_queue::resource_address.eq(resource_address.to_string()), + )) + .on_conflict(( + utxo_process_queue::resource_address, + utxo_process_queue::utxo_tag, + utxo_process_queue::public_nonce, + )) + .do_nothing() + .execute(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + } + + Ok(()) + } + + fn utxo_process_queue_remove_item( + &mut self, + resource_address: ResourceAddress, + tag: UtxoTag, + public_nonce: RistrettoPublicKeyBytes, + ) -> Result<(), WalletStorageError> { + const OPERATION: &str = "utxo_process_queue_remove_item"; + use crate::schema::utxo_process_queue; + + let num_affected = diesel::delete(utxo_process_queue::table) + .filter(utxo_process_queue::resource_address.eq(resource_address.to_string())) + .filter(utxo_process_queue::utxo_tag.eq(tag.value() as i32)) + .filter(utxo_process_queue::public_nonce.eq(serialize_hex(public_nonce))) + .execute(self.connection()) + .map_err(|e| WalletStorageError::general(OPERATION, e))?; + + if num_affected == 0 { + return Err(WalletStorageError::NotFound { + operation: OPERATION, + entity: "utxo_process_queue item".to_string(), + key: format!( + "resource_address={}, tag={}, public_nonce={}", + resource_address, tag, public_nonce + ), + }); + } + + Ok(()) + } } impl Drop for WriteTransaction<'_> { diff --git a/integration_tests/src/wallet_daemon_cli.rs b/integration_tests/src/wallet_daemon_cli.rs index a14bd6ba19..4128dd459a 100644 --- a/integration_tests/src/wallet_daemon_cli.rs +++ b/integration_tests/src/wallet_daemon_cli.rs @@ -60,7 +60,6 @@ use tari_wallet_daemon_client::{ ExtClaimBurnProof, ListNftsRequest, MintFaucetNftRequest, - RevealFundsRequest, StealthTransferRequest, TransactionSubmitRequest, TransactionWaitResultRequest, @@ -137,29 +136,6 @@ pub async fn claim_fees( client.claim_validator_fees(request).await } -pub async fn reveal_burned_funds(world: &mut TariWorld, account_name: String, amount: u64, wallet_daemon_name: String) { - let mut client = get_auth_wallet_daemon_client(world, &wallet_daemon_name).await; - - let request = RevealFundsRequest { - account: Some(ComponentAddressOrName::Name(account_name)), - amount_to_reveal: amount.into(), - max_fee: Some(5000), - pay_fee_from_reveal: true, - }; - - let resp = client - .accounts_reveal_funds(request) - .await - .expect("Failed to request reveal funds"); - - let wait_req = TransactionWaitResultRequest { - transaction_id: resp.transaction_id, - timeout_secs: Some(120), - }; - let wait_resp = client.wait_transaction_result(wait_req).await.unwrap(); - assert!(wait_resp.result.unwrap().result.is_accept()); -} - pub async fn transfer_confidential( world: &mut TariWorld, source_account_name: String, diff --git a/integration_tests/tests/cucumber.rs b/integration_tests/tests/cucumber.rs index 3209a05c5f..a280910fd4 100644 --- a/integration_tests/tests/cucumber.rs +++ b/integration_tests/tests/cucumber.rs @@ -591,11 +591,6 @@ async fn submit_manifest_with_inputs( validator_node_cli::submit_manifest(world, vn_name, outputs_name, manifest, inputs, key_name).await; } -#[when(expr = "account {word} reveals {int} burned tokens via wallet daemon {word}")] -async fn reveal_burned_funds(world: &mut TariWorld, account_name: String, amount: u64, wallet_daemon_name: String) { - wallet_daemon_cli::reveal_burned_funds(world, account_name, amount, wallet_daemon_name).await; -} - #[when(regex = r#"^I submit a transaction manifest via wallet daemon (\w+) with inputs "([^"]+)" named "(\w+)"$"#)] async fn submit_transaction_manifest_via_wallet_daemon( world: &mut TariWorld, diff --git a/integration_tests/tests/features/wallet_daemon.feature b/integration_tests/tests/features/wallet_daemon.feature index 6d85ecc61f..d73b2d44df 100644 --- a/integration_tests/tests/features/wallet_daemon.feature +++ b/integration_tests/tests/features/wallet_daemon.feature @@ -72,7 +72,7 @@ Feature: Wallet Daemon When I claim burn BURN_PROOF and spend it into account ACCOUNT_1 using wallet daemon WALLET_D When I print the cucumber world When I check the confidential balance of ACCOUNT_1 on wallet daemon WALLET_D the amount is at least 10000 - # When account ACCOUNT_1 reveals 100 burned tokens via wallet daemon WALLET_D + Then I make a confidential transfer with amount 5 from ACCOUNT_1 to ACCOUNT_2 creating output OUTPUT_TX1 via the wallet_daemon WALLET_D Scenario: Create and mint account NFT diff --git a/integration_tests/tests/log4rs/cucumber.yml b/integration_tests/tests/log4rs/cucumber.yml index 57a89f0ba5..1f2abd49ba 100644 --- a/integration_tests/tests/log4rs/cucumber.yml +++ b/integration_tests/tests/log4rs/cucumber.yml @@ -78,7 +78,7 @@ appenders: count: 10 pattern: "{{log_dir}}/base_layer.{}.log" encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}" + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}" console_wallet: kind: rolling_file @@ -94,7 +94,7 @@ appenders: count: 10 pattern: "{{log_dir}}/wallet.{}.log" encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}" + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}" wallet_daemon: kind: rolling_file @@ -110,7 +110,7 @@ appenders: count: 10 pattern: "log/validator-node/wallet_daemon.{}.log" encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}" + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}" # An appender named "other" that writes to a file with a custom pattern encoder other: @@ -127,7 +127,7 @@ appenders: count: 10 pattern: "log/validator-node/other.{}.log" encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}" + pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}" root: level: warn diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e276b9885f..b00e5993d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,35 +176,35 @@ importers: applications/tari_walletd/web_ui: dependencies: '@emotion/react': - specifier: ^11.10.6 - version: 11.14.0(@types/react@18.3.20)(react@19.1.0) + specifier: ^11.14.0 + version: 11.14.0(@types/react@18.3.24)(react@19.1.1) '@emotion/styled': - specifier: ^11.10.6 - version: 11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0) + specifier: ^11.14.1 + version: 11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1) '@mui/icons-material': - specifier: ^5.11.11 - version: 5.17.1(@mui/material@5.17.1(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@18.3.20)(react@19.1.0) + specifier: ^5.18.0 + version: 5.18.0(@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@18.3.24)(react@19.1.1) '@mui/material': - specifier: ^5.11.14 - version: 5.17.1(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^5.18.0 + version: 5.18.0(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@mui/x-data-grid': - specifier: ^6.0.2 - version: 6.20.4(@mui/material@5.17.1(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.0.2(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^6.20.4 + version: 6.20.4(@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.0.2(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@reown/walletkit': - specifier: ^1.2.8 - version: 1.2.8(typescript@5.8.3)(zod@3.25.67) + specifier: ^1.2.10 + version: 1.2.10(typescript@5.8.3)(zod@3.25.67) '@tanstack/react-query': - specifier: ^4.33.0 - version: 4.36.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^5.85.7 + version: 5.87.4(react@19.1.1) '@tanstack/react-query-devtools': - specifier: ^4.33.0 - version: 4.36.1(@tanstack/react-query@4.36.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^5.85.7 + version: 5.87.4(@tanstack/react-query@5.87.4(react@19.1.1))(react@19.1.1) '@tari-project/tari-extension-common': specifier: ^0.0.14 version: 0.0.14 '@tari-project/tari-extension-query-builder': specifier: ^0.0.15 - version: 0.0.15(@types/node@22.14.0)(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(acorn@8.14.1)(jiti@2.4.2)(lightningcss@1.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)(webextension-polyfill@0.12.0)(zustand@5.0.5(@types/react@18.3.20)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))) + version: 0.0.15(@types/node@22.14.0)(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(acorn@8.14.1)(jiti@2.4.2)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.8.3)(webextension-polyfill@0.12.0)(zustand@5.0.8(@types/react@18.3.24)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1))) '@tari-project/tari-permissions': specifier: 0.10.1 version: 0.10.1 @@ -215,11 +215,11 @@ importers: specifier: workspace:* version: link:../../../clients/javascript/wallet_daemon_client '@walletconnect/core': - specifier: ^2.21.4 - version: 2.21.4(typescript@5.8.3)(zod@3.25.67) + specifier: ^2.21.8 + version: 2.21.8(typescript@5.8.3)(zod@3.25.67) '@walletconnect/utils': - specifier: ^2.21.4 - version: 2.21.4(typescript@5.8.3)(zod@3.25.67) + specifier: ^2.21.8 + version: 2.21.8(typescript@5.8.3)(zod@3.25.67) buffer: specifier: ^6.0.3 version: 6.0.3 @@ -231,37 +231,37 @@ importers: version: 2.0.5 framer-motion: specifier: ^12.23.12 - version: 12.23.12(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 12.23.12(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) jwt-decode: specifier: ^4.0.0 version: 4.0.0 prism-react-renderer: specifier: ^2.4.1 - version: 2.4.1(react@19.1.0) + version: 2.4.1(react@19.1.1) react: - specifier: ^19.0.0 - version: 19.1.0 + specifier: ^19.1.1 + version: 19.1.1 react-dom: - specifier: ^19.0.0 - version: 19.1.0(react@19.1.0) + specifier: ^19.1.1 + version: 19.1.1(react@19.1.1) react-icons: - specifier: ^4.9.0 - version: 4.12.0(react@19.1.0) + specifier: ^4.12.0 + version: 4.12.0(react@19.1.1) react-router-dom: - specifier: ^6.9.0 - version: 6.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^6.30.1 + version: 6.30.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) use-file-picker: - specifier: ^2.1.2 - version: 2.1.2(react@19.1.0) + specifier: ^2.1.4 + version: 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react@19.1.1) use-react-router-breadcrumbs: specifier: ^4.0.1 - version: 4.0.1(react-router-dom@6.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + version: 4.0.1(react-router-dom@6.30.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) zustand: - specifier: ^5.0.3 - version: 5.0.5(@types/react@18.3.20)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + specifier: ^5.0.8 + version: 5.0.8(@types/react@18.3.24)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) zustand-persist: specifier: ^0.4.0 - version: 0.4.0(react@19.1.0)(zustand@5.0.5(@types/react@18.3.20)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))) + version: 0.4.0(react@19.1.1)(zustand@5.0.8(@types/react@18.3.24)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1))) devDependencies: '@types/file-saver': specifier: ^2.0.7 @@ -270,17 +270,17 @@ importers: specifier: 'catalog:' version: 22.14.0 '@types/react': - specifier: ^18.0.28 - version: 18.3.20 + specifier: ^18.3.24 + version: 18.3.24 '@types/react-dom': - specifier: ^18.0.11 - version: 18.3.7(@types/react@18.3.20) + specifier: ^18.3.7 + version: 18.3.7(@types/react@18.3.24) '@vitejs/plugin-react-swc': - specifier: ^3.8.0 - version: 3.9.0(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)) + specifier: ^3.11.0 + version: 3.11.0(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)) prettier: - specifier: ^3.3.2 - version: 3.5.3 + specifier: ^3.6.2 + version: 3.6.2 typescript: specifier: 'catalog:' version: 5.8.3 @@ -541,6 +541,16 @@ packages: '@types/react': optional: true + '@emotion/styled@11.14.1': + resolution: {integrity: sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + '@emotion/unitless@0.10.0': resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} @@ -946,6 +956,9 @@ packages: '@mui/core-downloads-tracker@5.17.1': resolution: {integrity: sha512-OcZj+cs6EfUD39IoPBOgN61zf1XFVY+imsGoBDwXeSq2UHJZE3N59zzBOVjclck91Ne3e9gudONOeILvHCIhUA==} + '@mui/core-downloads-tracker@5.18.0': + resolution: {integrity: sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA==} + '@mui/core-downloads-tracker@7.0.2': resolution: {integrity: sha512-TfeFU9TgN1N06hyb/pV/63FfO34nijZRMqgHk0TJ3gkl4Fbd+wZ73+ZtOd7jag6hMmzO9HSrBc6Vdn591nhkAg==} @@ -960,6 +973,17 @@ packages: '@types/react': optional: true + '@mui/icons-material@5.18.0': + resolution: {integrity: sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@mui/material': ^5.0.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@mui/material@5.17.1': resolution: {integrity: sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw==} engines: {node: '>=12.0.0'} @@ -977,6 +1001,23 @@ packages: '@types/react': optional: true + '@mui/material@5.18.0': + resolution: {integrity: sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + '@mui/material@7.0.2': resolution: {integrity: sha512-rjJlJ13+3LdLfobRplkXbjIFEIkn6LgpetgU/Cs3Xd8qINCCQK9qXQIjjQ6P0FXFTPFzEVMj0VgBR1mN+FhOcA==} engines: {node: '>=14.0.0'} @@ -1030,6 +1071,19 @@ packages: '@emotion/styled': optional: true + '@mui/styled-engine@5.18.0': + resolution: {integrity: sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@mui/styled-engine@7.0.2': resolution: {integrity: sha512-11Bt4YdHGlh7sB8P75S9mRCUxTlgv7HGbr0UKz6m6Z9KLeiw1Bm9y/t3iqLLVMvSHYB6zL8X8X+LmfTE++gyBw==} engines: {node: '>=14.0.0'} @@ -1059,6 +1113,22 @@ packages: '@types/react': optional: true + '@mui/system@5.18.0': + resolution: {integrity: sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + '@mui/system@7.0.2': resolution: {integrity: sha512-yFUraAWYWuKIISPPEVPSQ1NLeqmTT4qiQ+ktmyS8LO/KwHxB+NNVOacEZaIofh5x1NxY8rzphvU5X2heRZ/RDA==} engines: {node: '>=14.0.0'} @@ -1642,8 +1712,11 @@ packages: resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} engines: {node: '>=14.0.0'} - '@reown/walletkit@1.2.8': - resolution: {integrity: sha512-X3EO9P6+Dvc++h8OwpBtBhGmq+890UlG/o0Ilb98l5ByDr3QVcYOURRIPVcV6pkTJ9sE6sVDXW7RIRiYSnQp2g==} + '@reown/walletkit@1.2.10': + resolution: {integrity: sha512-EwoCt2eb+jg1ezhXGcY2CsTUVbz9T1/0o8YQr/Lx+OTqRt1f0+FyZs6+eJ8UsgpbHnLUJ5lDgJj0p3rU1XNUrw==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} '@rollup/rollup-android-arm-eabi@4.40.1': resolution: {integrity: sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==} @@ -1779,6 +1852,12 @@ packages: cpu: [arm64] os: [darwin] + '@swc/core-darwin-arm64@1.13.5': + resolution: {integrity: sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + '@swc/core-darwin-x64@1.11.18': resolution: {integrity: sha512-RCRvC6Q9M5BArTvj/IzUAAYGrgxYFbTTnAtf6UX7JFq2DAn+hEwYUjmC1m0gFso9HqFU0m5QZUGfZvVmACGWUw==} engines: {node: '>=10'} @@ -1791,6 +1870,12 @@ packages: cpu: [x64] os: [darwin] + '@swc/core-darwin-x64@1.13.5': + resolution: {integrity: sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + '@swc/core-linux-arm-gnueabihf@1.11.18': resolution: {integrity: sha512-wteAKf8YKb3jOnZFm3EzuIMzzCVXMuQOLHsz1IgEOc44/gdgNXKxaYTWAowZuej7t68tf/w0cRNMc7Le414v/g==} engines: {node: '>=10'} @@ -1803,6 +1888,12 @@ packages: cpu: [arm] os: [linux] + '@swc/core-linux-arm-gnueabihf@1.13.5': + resolution: {integrity: sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + '@swc/core-linux-arm64-gnu@1.11.18': resolution: {integrity: sha512-hY6jJYZ6PKHSBo5OATswfyKsUgsWu9+4nDcN8liYIRRgz3E0G9wk0VUTP4cFPivBFeHWTTAGz687/Nf2aQEIpw==} engines: {node: '>=10'} @@ -1815,6 +1906,12 @@ packages: cpu: [arm64] os: [linux] + '@swc/core-linux-arm64-gnu@1.13.5': + resolution: {integrity: sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + '@swc/core-linux-arm64-musl@1.11.18': resolution: {integrity: sha512-slu0mlP2nucvQalttnapfpqpD/LlM9NHx9g3ofgsLzjObyMEBiX4ZysQ3y65U8Mjw71RNqtLd/ZmvxI6OmLdiQ==} engines: {node: '>=10'} @@ -1827,6 +1924,12 @@ packages: cpu: [arm64] os: [linux] + '@swc/core-linux-arm64-musl@1.13.5': + resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + '@swc/core-linux-x64-gnu@1.11.18': resolution: {integrity: sha512-h9a/8PA25arMCQ9t8CE8rA1s0c77z4kCZZ7dUuUkD88yEXIrARMca1IKR7of+S3slfQrf1Zlq3Ac1Fb1HVJziQ==} engines: {node: '>=10'} @@ -1839,6 +1942,12 @@ packages: cpu: [x64] os: [linux] + '@swc/core-linux-x64-gnu@1.13.5': + resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + '@swc/core-linux-x64-musl@1.11.18': resolution: {integrity: sha512-0sMDJj5qUGK9QEw4lrxLxkTP/4AoKciqNzXvqbk+J9XuXN2aIv4BsR1Y7z3GwAeMFGsba2lbHLOtJlDsaqIsiA==} engines: {node: '>=10'} @@ -1851,6 +1960,12 @@ packages: cpu: [x64] os: [linux] + '@swc/core-linux-x64-musl@1.13.5': + resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + '@swc/core-win32-arm64-msvc@1.11.18': resolution: {integrity: sha512-zGv9HnfgBcKyt54MJRWdwRNu9BuYkAFM7bx+tWtKhd37Ef7ZX20QLs9xXl5wWDXCbsOdRxXIZgXs6PEL+Pzmrw==} engines: {node: '>=10'} @@ -1863,6 +1978,12 @@ packages: cpu: [arm64] os: [win32] + '@swc/core-win32-arm64-msvc@1.13.5': + resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + '@swc/core-win32-ia32-msvc@1.11.18': resolution: {integrity: sha512-uBKj0S1lYv/E2ZhxHZOxSiQwoegYmzbPRpjq6eHBZDv97mu7W3K27/lsnPbvAfQ6b6rnv8BI+EsmJ7VLQBAHBQ==} engines: {node: '>=10'} @@ -1875,6 +1996,12 @@ packages: cpu: [ia32] os: [win32] + '@swc/core-win32-ia32-msvc@1.13.5': + resolution: {integrity: sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + '@swc/core-win32-x64-msvc@1.11.18': resolution: {integrity: sha512-8USTRcdgeFMNBgvVXl8tz6n4+9s9m+zHsfDeBT4jPgwnq2bnLBlTUlwnPwzDxfg9nUJr6RFD4xeKfWyZZRosZg==} engines: {node: '>=10'} @@ -1887,6 +2014,12 @@ packages: cpu: [x64] os: [win32] + '@swc/core-win32-x64-msvc@1.13.5': + resolution: {integrity: sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + '@swc/core@1.11.18': resolution: {integrity: sha512-ORZxyCKKiqYt2iHdh1C7pfVR1GBjkuFOdwqZggQzaq0vt22DpGca+2JsUtkUoWQmWcct04v5+ScwgvsHuMObxA==} engines: {node: '>=10'} @@ -1905,12 +2038,24 @@ packages: '@swc/helpers': optional: true + '@swc/core@1.13.5': + resolution: {integrity: sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} '@swc/types@0.1.21': resolution: {integrity: sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==} + '@swc/types@0.1.25': + resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + '@tanstack/match-sorter-utils@8.19.4': resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} engines: {node: '>=12'} @@ -1921,9 +2066,15 @@ packages: '@tanstack/query-core@5.81.2': resolution: {integrity: sha512-QLYkPdrudoMATDFa3MiLEwRhNnAlzHWDf0LKaXUqJd0/+QxN8uTPi7bahRlxoAyH0UbLMBdeDbYzWALj7THOtw==} + '@tanstack/query-core@5.87.4': + resolution: {integrity: sha512-uNsg6zMxraEPDVO2Bn+F3/ctHi+Zsk+MMpcN8h6P7ozqD088F6mFY5TfGM7zuyIrL7HKpDyu6QHfLWiDxh3cuw==} + '@tanstack/query-devtools@5.84.0': resolution: {integrity: sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==} + '@tanstack/query-devtools@5.87.3': + resolution: {integrity: sha512-LkzxzSr2HS1ALHTgDmJH5eGAVsSQiuwz//VhFW5OqNk0OQ+Fsqba0Tsf+NzWRtXYvpgUqwQr4b2zdFZwxHcGvg==} + '@tanstack/react-query-devtools@4.36.1': resolution: {integrity: sha512-WYku83CKP3OevnYSG8Y/QO9g0rT75v1om5IvcWUwiUZJ4LanYGLVCZ8TdFG5jfsq4Ej/lu2wwDAULEUnRIMBSw==} peerDependencies: @@ -1937,6 +2088,12 @@ packages: '@tanstack/react-query': ^5.85.5 react: ^18 || ^19 + '@tanstack/react-query-devtools@5.87.4': + resolution: {integrity: sha512-JYcnVJBBW1DCPyNGM0S2CyrLpe6KFiL2gpYd/k9tAp62Du7+Y27zkzd+dKFyxpFadYaTxsx4kUA7YvnkMLVUoQ==} + peerDependencies: + '@tanstack/react-query': ^5.87.4 + react: ^18 || ^19 + '@tanstack/react-query@4.36.1': resolution: {integrity: sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==} peerDependencies: @@ -1954,6 +2111,11 @@ packages: peerDependencies: react: ^18 || ^19 + '@tanstack/react-query@5.87.4': + resolution: {integrity: sha512-T5GT/1ZaNsUXf5I3RhcYuT17I4CPlbZgyLxc/ZGv7ciS6esytlbjb3DgUFO6c8JWYMDpdjSWInyGZUErgzqhcA==} + peerDependencies: + react: ^18 || ^19 + '@tari-project/indexer-provider@0.11.0': resolution: {integrity: sha512-GEGHxgMODWbI91hrDcg3t3EciQpHqoHIqYwMEGoHew3uAeDrXPJ4hnAby/B//JuEw3UFCmmB7+QXImBjhdmDEA==} @@ -2104,6 +2266,9 @@ packages: '@types/react@18.3.20': resolution: {integrity: sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==} + '@types/react@18.3.24': + resolution: {integrity: sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==} + '@types/react@19.1.0': resolution: {integrity: sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==} @@ -2172,6 +2337,11 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitejs/plugin-react-swc@3.11.0': + resolution: {integrity: sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==} + peerDependencies: + vite: ^4 || ^5 || ^6 || ^7 + '@vitejs/plugin-react-swc@3.8.1': resolution: {integrity: sha512-aEUPCckHDcFyxpwFm0AIkbtv6PpUp3xTb9wYGFjtABynXjCYKkWoxX0AOK9NT9XCrdk6mBBUOeHQS+RKdcNO1A==} peerDependencies: @@ -2192,6 +2362,14 @@ packages: resolution: {integrity: sha512-XtwPUCj3bCNX/2yjYGlQyvcsn32wkzixCjyWOD4pdKFVk7opZPAdF4xr85rmo6nJ7AiBYxjV1IH0bemTPEdE6Q==} engines: {node: '>=18'} + '@walletconnect/core@2.21.7': + resolution: {integrity: sha512-q/Au5Ne3g4R+q4GvHR5cvRd3+ha00QZCZiCs058lmy+eDbiZd0YsautvTPJ5a2guD6UaS1k/w5e1JHgixdcgLA==} + engines: {node: '>=18'} + + '@walletconnect/core@2.21.8': + resolution: {integrity: sha512-MD1SY7KAeHWvufiBK8C1MwP9/pxxI7SnKi/rHYfjco2Xvke+M+Bbm2OzvuSN7dYZvwLTkZCiJmBccTNVPCpSUQ==} + engines: {node: '>=18'} + '@walletconnect/environment@1.0.1': resolution: {integrity: sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==} @@ -2249,18 +2427,33 @@ packages: '@walletconnect/sign-client@2.21.4': resolution: {integrity: sha512-v1OJ9IQCZAqaDEUYGFnGLe2fSp8DN9cv7j8tUYm5ngiFK7h6LjP4Ew3gGCca4AHWzMFkHuIRNQ+6Ypep1K/B7g==} + '@walletconnect/sign-client@2.21.7': + resolution: {integrity: sha512-9k/JEl9copR6nXRhqnmzWz2Zk1hiWysH+o6bp6Cqo8TgDUrZoMLBZMZ6qbo+2HLI54V02kKf0Vg8M81nNFOpjQ==} + '@walletconnect/time@1.0.2': resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} '@walletconnect/types@2.21.4': resolution: {integrity: sha512-6O61esDSW8FZNdFezgB4bX2S35HM6tCwBEjGHNB8JeoKCfpXG33hw2raU/SBgBL/jmM57QRW4M1aFH7v1u9z7g==} + '@walletconnect/types@2.21.7': + resolution: {integrity: sha512-kyGnFje4Iq+XGkZZcSoAIrJWBE4BeghVW4O7n9e1MhUyeOOtO55M/kcqceNGYrvwjHvdN+Kf+aoLnKC0zKlpbQ==} + + '@walletconnect/types@2.21.8': + resolution: {integrity: sha512-xuLIPrLxe6viMu8Uk28Nf0sgyMy+4oT0mroOjBe5Vqyft8GTiwUBKZXmrGU9uDzZsYVn1FXLO9CkuNHXda3ODA==} + '@walletconnect/universal-provider@2.21.4': resolution: {integrity: sha512-ZSYU5H7Zi/nEy3L21kw5l3ovMagrbXDRKBG8vjPpGQAkflQocRj6d0SesFOCBEdJS16nt+6dKI2f5blpOGzyTQ==} '@walletconnect/utils@2.21.4': resolution: {integrity: sha512-LuSyBXvRLiDqIu4uMFei5eJ/WPhkIkdW58fmDlRnryatIuBPCho3dlrNSbOjVSdsID+OvxjOlpPLi+5h4oTbaA==} + '@walletconnect/utils@2.21.7': + resolution: {integrity: sha512-qyaclTgcFf9AwVuoV8CLLg8wfH3nX7yZdpylNkDqCpS7wawQL9zmFFTaGgma8sQrCsd3Sd9jUIymcpRvCJnSTw==} + + '@walletconnect/utils@2.21.8': + resolution: {integrity: sha512-HtMraGJ9qXo55l4wGSM1aZvyz0XVv460iWhlRGAyRl9Yz8RQeKyXavDhwBfcTFha/6kwLxPExqQ+MURtKeVVXw==} + '@walletconnect/window-getters@1.0.1': resolution: {integrity: sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==} @@ -2920,9 +3113,9 @@ packages: file-saver@2.0.5: resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} - file-selector@0.2.4: - resolution: {integrity: sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==} - engines: {node: '>= 10'} + file-selector@2.1.2: + resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} + engines: {node: '>= 12'} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} @@ -3834,6 +4027,11 @@ packages: engines: {node: '>=14'} hasBin: true + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + pretty-bytes@6.1.1: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} @@ -3900,6 +4098,11 @@ packages: peerDependencies: react: ^19.1.0 + react-dom@19.1.1: + resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} + peerDependencies: + react: ^19.1.1 + react-hotkeys-hook@4.6.2: resolution: {integrity: sha512-FmP+ZriY3EG59Ug/lxNfrObCnW9xQShgk7Nb83+CkpfkcCpfS95ydv+E9JuXA5cp8KtskU7LGlIARpkc92X22Q==} peerDependencies: @@ -3958,6 +4161,13 @@ packages: react: '>=16.8' react-dom: '>=16.8' + react-router-dom@6.30.1: + resolution: {integrity: sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + react-router-dom@7.6.2: resolution: {integrity: sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==} engines: {node: '>=20.0.0'} @@ -3971,6 +4181,12 @@ packages: peerDependencies: react: '>=16.8' + react-router@6.30.1: + resolution: {integrity: sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-router@7.6.2: resolution: {integrity: sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==} engines: {node: '>=20.0.0'} @@ -4005,6 +4221,10 @@ packages: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} + react@19.1.1: + resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} + engines: {node: '>=0.10.0'} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -4489,10 +4709,12 @@ packages: '@types/react': optional: true - use-file-picker@2.1.2: - resolution: {integrity: sha512-ZEIzRi1wXeIXDWr5i55gRBVER8rTkSGskDUY94bciTTAZJHlBnOTRLL/LDYjgz6d+US3yELHnRvtBhLxFGtB0A==} + use-file-picker@2.1.4: + resolution: {integrity: sha512-b4lZiAWrXi/QNUjTv0Q+S0hVcSFXIC9c4EUcrnYtdPtgK3T6xfi01YLVamhoY0k9WM9Cg4KyxD1TtM1e8dzQAQ==} engines: {node: '>=12'} peerDependencies: + '@types/react': '>=16' + '@types/react-dom': '>=16' react: '>=16' use-react-router-breadcrumbs@4.0.1: @@ -4770,8 +4992,8 @@ packages: react: optional: true - zustand@5.0.5: - resolution: {integrity: sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==} + zustand@5.0.8: + resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -4995,6 +5217,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.27.0 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.1.1 + optionalDependencies: + '@types/react': 18.3.24 + transitivePeerDependencies: + - supports-color + '@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0)': dependencies: '@babel/runtime': 7.27.0 @@ -5066,6 +5304,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.27.6 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.3.1 + '@emotion/react': 11.14.0(@types/react@18.3.24)(react@19.1.1) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) + '@emotion/utils': 1.4.2 + react: 19.1.1 + optionalDependencies: + '@types/react': 18.3.24 + transitivePeerDependencies: + - supports-color + '@emotion/unitless@0.10.0': {} '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@18.3.1)': @@ -5076,6 +5329,10 @@ snapshots: dependencies: react: 19.1.0 + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.1.1)': + dependencies: + react: 19.1.1 + '@emotion/utils@1.4.2': {} '@emotion/weak-memoize@0.4.0': {} @@ -5232,11 +5489,11 @@ snapshots: '@floating-ui/core': 1.7.1 '@floating-ui/utils': 0.2.9 - '@floating-ui/react-dom@2.1.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@floating-ui/react-dom@2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@floating-ui/dom': 1.7.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) '@floating-ui/utils@0.2.9': {} @@ -5296,12 +5553,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@ladle/react-context@1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@ladle/react-context@1.0.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) - '@ladle/react@5.0.3(@types/node@22.14.0)(@types/react@18.3.20)(acorn@8.14.1)(jiti@2.4.2)(lightningcss@1.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)': + '@ladle/react@5.0.3(@types/node@22.14.0)(@types/react@18.3.24)(acorn@8.14.1)(jiti@2.4.2)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.8.3)': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.26.10 @@ -5310,11 +5567,11 @@ snapshots: '@babel/template': 7.27.0 '@babel/traverse': 7.27.0 '@babel/types': 7.27.0 - '@ladle/react-context': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@ladle/react-context': 1.0.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@mdx-js/mdx': 3.1.0(acorn@8.14.1) - '@mdx-js/react': 3.1.0(@types/react@18.3.20)(react@19.1.0) + '@mdx-js/react': 3.1.0(@types/react@18.3.24)(react@19.1.1) '@vitejs/plugin-react': 4.3.4(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)) - '@vitejs/plugin-react-swc': 3.9.0(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)) + '@vitejs/plugin-react-swc': 3.11.0(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)) axe-core: 4.10.3 boxen: 8.0.1 chokidar: 4.0.3 @@ -5330,13 +5587,13 @@ snapshots: lodash.merge: 4.6.2 msw: 2.10.2(@types/node@22.14.0)(typescript@5.8.3) open: 10.1.2 - prism-react-renderer: 2.4.1(react@19.1.0) + prism-react-renderer: 2.4.1(react@19.1.1) prop-types: 15.8.1 query-string: 9.2.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-hotkeys-hook: 4.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-inspector: 6.0.2(react@19.1.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-hotkeys-hook: 4.6.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react-inspector: 6.0.2(react@19.1.1) rehype-class-names: 2.0.0 rehype-raw: 7.0.0 remark-gfm: 4.0.1 @@ -5398,11 +5655,11 @@ snapshots: - acorn - supports-color - '@mdx-js/react@3.1.0(@types/react@18.3.20)(react@19.1.0)': + '@mdx-js/react@3.1.0(@types/react@18.3.24)(react@19.1.1)': dependencies: '@types/mdx': 2.0.13 - '@types/react': 18.3.20 - react: 19.1.0 + '@types/react': 18.3.24 + react: 19.1.1 '@metamask/json-rpc-engine@10.0.3': dependencies: @@ -5526,6 +5783,8 @@ snapshots: '@mui/core-downloads-tracker@5.17.1': {} + '@mui/core-downloads-tracker@5.18.0': {} + '@mui/core-downloads-tracker@7.0.2': {} '@mui/icons-material@5.17.1(@mui/material@5.17.1(@emotion/react@11.14.0(@types/react@18.3.20)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@18.3.1))(@types/react@18.3.20)(react@18.3.1))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.20)(react@18.3.1)': @@ -5552,6 +5811,14 @@ snapshots: optionalDependencies: '@types/react': 19.1.0 + '@mui/icons-material@5.18.0(@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@18.3.24)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/material': 5.18.0(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + optionalDependencies: + '@types/react': 18.3.24 + '@mui/material@5.17.1(@emotion/react@11.14.0(@types/react@18.3.20)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@18.3.1))(@types/react@18.3.20)(react@18.3.1))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.27.0 @@ -5594,6 +5861,27 @@ snapshots: '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0) '@types/react': 18.3.20 + '@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/core-downloads-tracker': 5.18.0 + '@mui/system': 5.18.0(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1) + '@mui/types': 7.2.24(@types/react@18.3.24) + '@mui/utils': 5.17.1(@types/react@18.3.24)(react@19.1.1) + '@popperjs/core': 2.11.8 + '@types/react-transition-group': 4.4.12(@types/react@18.3.24) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-is: 19.1.0 + react-transition-group: 4.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.24)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1) + '@types/react': 18.3.24 + '@mui/material@7.0.2(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.0 @@ -5633,6 +5921,15 @@ snapshots: optionalDependencies: '@types/react': 18.3.20 + '@mui/private-theming@5.17.1(@types/react@18.3.24)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.27.0 + '@mui/utils': 5.17.1(@types/react@18.3.24)(react@19.1.1) + prop-types: 15.8.1 + react: 19.1.1 + optionalDependencies: + '@types/react': 18.3.24 + '@mui/private-theming@7.0.2(@types/react@18.3.20)(react@18.3.1)': dependencies: '@babel/runtime': 7.27.0 @@ -5651,6 +5948,15 @@ snapshots: optionalDependencies: '@types/react': 18.3.20 + '@mui/private-theming@7.0.2(@types/react@18.3.24)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.27.0 + '@mui/utils': 7.0.2(@types/react@18.3.24)(react@19.1.1) + prop-types: 15.8.1 + react: 19.1.1 + optionalDependencies: + '@types/react': 18.3.24 + '@mui/private-theming@7.0.2(@types/react@19.1.0)(react@19.1.0)': dependencies: '@babel/runtime': 7.27.0 @@ -5682,6 +5988,18 @@ snapshots: '@emotion/react': 11.14.0(@types/react@18.3.20)(react@19.1.0) '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0) + '@mui/styled-engine@5.18.0(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(react@19.1.1)': + dependencies: + '@babel/runtime': 7.27.6 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.1.1 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.24)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1) + '@mui/styled-engine@7.0.2(@emotion/react@11.14.0(@types/react@18.3.20)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@18.3.1))(@types/react@18.3.20)(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.27.0 @@ -5708,6 +6026,19 @@ snapshots: '@emotion/react': 11.14.0(@types/react@18.3.20)(react@19.1.0) '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0) + '@mui/styled-engine@7.0.2(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(react@19.1.1)': + dependencies: + '@babel/runtime': 7.27.0 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.1.1 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.24)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1) + '@mui/styled-engine@7.0.2(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.0 @@ -5753,6 +6084,22 @@ snapshots: '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0) '@types/react': 18.3.20 + '@mui/system@5.18.0(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/private-theming': 5.17.1(@types/react@18.3.24)(react@19.1.1) + '@mui/styled-engine': 5.18.0(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(react@19.1.1) + '@mui/types': 7.2.24(@types/react@18.3.24) + '@mui/utils': 5.17.1(@types/react@18.3.24)(react@19.1.1) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.1.1 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.24)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1) + '@types/react': 18.3.24 + '@mui/system@7.0.2(@emotion/react@11.14.0(@types/react@18.3.20)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@18.3.1))(@types/react@18.3.20)(react@18.3.1))(@types/react@18.3.20)(react@18.3.1)': dependencies: '@babel/runtime': 7.27.0 @@ -5785,6 +6132,22 @@ snapshots: '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0) '@types/react': 18.3.20 + '@mui/system@7.0.2(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.27.0 + '@mui/private-theming': 7.0.2(@types/react@18.3.24)(react@19.1.1) + '@mui/styled-engine': 7.0.2(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(react@19.1.1) + '@mui/types': 7.4.1(@types/react@18.3.24) + '@mui/utils': 7.0.2(@types/react@18.3.24)(react@19.1.1) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.1.1 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.24)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1) + '@types/react': 18.3.24 + '@mui/system@7.0.2(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0)': dependencies: '@babel/runtime': 7.27.0 @@ -5805,12 +6168,22 @@ snapshots: optionalDependencies: '@types/react': 18.3.20 - '@mui/types@7.4.1(@types/react@18.3.20)': + '@mui/types@7.2.24(@types/react@18.3.24)': + optionalDependencies: + '@types/react': 18.3.24 + + '@mui/types@7.4.1(@types/react@18.3.20)': dependencies: '@babel/runtime': 7.27.0 optionalDependencies: '@types/react': 18.3.20 + '@mui/types@7.4.1(@types/react@18.3.24)': + dependencies: + '@babel/runtime': 7.27.0 + optionalDependencies: + '@types/react': 18.3.24 + '@mui/types@7.4.1(@types/react@19.1.0)': dependencies: '@babel/runtime': 7.27.0 @@ -5847,6 +6220,18 @@ snapshots: optionalDependencies: '@types/react': 18.3.20 + '@mui/utils@5.17.1(@types/react@18.3.24)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.27.0 + '@mui/types': 7.2.24(@types/react@18.3.24) + '@types/prop-types': 15.7.14 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.1.1 + react-is: 19.1.0 + optionalDependencies: + '@types/react': 18.3.24 + '@mui/utils@7.0.2(@types/react@18.3.20)(react@18.3.1)': dependencies: '@babel/runtime': 7.27.0 @@ -5871,6 +6256,18 @@ snapshots: optionalDependencies: '@types/react': 18.3.20 + '@mui/utils@7.0.2(@types/react@18.3.24)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.27.0 + '@mui/types': 7.4.1(@types/react@18.3.24) + '@types/prop-types': 15.7.14 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.1.1 + react-is: 19.1.0 + optionalDependencies: + '@types/react': 18.3.24 + '@mui/utils@7.0.2(@types/react@19.1.0)(react@19.1.0)': dependencies: '@babel/runtime': 7.27.0 @@ -5909,16 +6306,16 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@mui/x-data-grid@6.20.4(@mui/material@5.17.1(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.0.2(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/x-data-grid@6.20.4(@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.0.2(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@babel/runtime': 7.27.0 - '@mui/material': 5.17.1(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@mui/system': 7.0.2(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0))(@types/react@18.3.20)(react@19.1.0) - '@mui/utils': 5.17.1(@types/react@18.3.20)(react@19.1.0) + '@mui/material': 5.18.0(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@mui/system': 7.0.2(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1))(@types/react@18.3.24)(react@19.1.1) + '@mui/utils': 5.17.1(@types/react@18.3.24)(react@19.1.1) clsx: 2.1.1 prop-types: 15.8.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) reselect: 4.1.8 transitivePeerDependencies: - '@types/react' @@ -6029,416 +6426,416 @@ snapshots: '@radix-ui/primitive@1.1.2': {} - '@radix-ui/react-alert-dialog@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-alert-dialog@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-dialog': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-checkbox@1.3.2(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-checkbox@1.3.2(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.24)(react@19.1.1)': dependencies: - react: 19.1.0 + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-context@1.1.2(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-context@1.1.2(@types/react@18.3.24)(react@19.1.1)': dependencies: - react: 19.1.0 + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-dialog@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-dialog@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.20)(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@19.1.1) aria-hidden: 1.2.6 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-remove-scroll: 2.7.1(@types/react@18.3.20)(react@19.1.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-direction@1.1.1(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-direction@1.1.1(@types/react@18.3.24)(react@19.1.1)': dependencies: - react: 19.1.0 + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-menu': 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.24)(react@19.1.1)': dependencies: - react: 19.1.0 + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-icons@1.3.2(react@19.1.0)': + '@radix-ui/react-icons@1.3.2(react@19.1.1)': dependencies: - react: 19.1.0 + react: 19.1.1 - '@radix-ui/react-id@1.1.1(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-id@1.1.1(@types/react@18.3.24)(react@19.1.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-label@2.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-label@2.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.20)(react@19.1.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@19.1.1) aria-hidden: 1.2.6 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-remove-scroll: 2.7.1(@types/react@18.3.20)(react@19.1.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-popover@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-popover@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.20)(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@19.1.1) aria-hidden: 1.2.6 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-remove-scroll: 2.7.1(@types/react@18.3.20)(react@19.1.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) - - '@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@floating-ui/react-dom': 2.1.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.20)(react@19.1.0) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@floating-ui/react-dom': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@19.1.1) '@radix-ui/rect': 1.1.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-select@2.2.5(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-select@2.2.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) aria-hidden: 1.2.6 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-remove-scroll: 2.7.1(@types/react@18.3.20)(react@19.1.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-separator@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-separator@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-slot@1.2.3(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-slot@1.2.3(@types/react@18.3.24)(react@19.1.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-tooltip@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-tooltip@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.24)(react@19.1.1)': dependencies: - react: 19.1.0 + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.24)(react@19.1.1)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.24)(react@19.1.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.24)(react@19.1.1)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.24)(react@19.1.1)': dependencies: - react: 19.1.0 + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.24)(react@19.1.1)': dependencies: - react: 19.1.0 + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.24)(react@19.1.1)': dependencies: '@radix-ui/rect': 1.1.1 - react: 19.1.0 + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-use-size@1.1.1(@types/react@18.3.20)(react@19.1.0)': + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.24)(react@19.1.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@19.1.0) - react: 19.1.0 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@19.1.1) + react: 19.1.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.7(@types/react@18.3.20) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) '@radix-ui/rect@1.1.1': {} '@remix-run/router@1.23.0': {} - '@reown/walletkit@1.2.8(typescript@5.8.3)(zod@3.25.67)': + '@reown/walletkit@1.2.10(typescript@5.8.3)(zod@3.25.67)': dependencies: - '@walletconnect/core': 2.21.4(typescript@5.8.3)(zod@3.25.67) + '@walletconnect/core': 2.21.7(typescript@5.8.3)(zod@3.25.67) '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.4(typescript@5.8.3)(zod@3.25.67) - '@walletconnect/types': 2.21.4 - '@walletconnect/utils': 2.21.4(typescript@5.8.3)(zod@3.25.67) + '@walletconnect/sign-client': 2.21.7(typescript@5.8.3)(zod@3.25.67) + '@walletconnect/types': 2.21.7 + '@walletconnect/utils': 2.21.7(typescript@5.8.3)(zod@3.25.67) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -6463,6 +6860,8 @@ snapshots: - utf-8-validate - zod + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/rollup-android-arm-eabi@4.40.1': optional: true @@ -6557,60 +6956,90 @@ snapshots: '@swc/core-darwin-arm64@1.11.24': optional: true + '@swc/core-darwin-arm64@1.13.5': + optional: true + '@swc/core-darwin-x64@1.11.18': optional: true '@swc/core-darwin-x64@1.11.24': optional: true + '@swc/core-darwin-x64@1.13.5': + optional: true + '@swc/core-linux-arm-gnueabihf@1.11.18': optional: true '@swc/core-linux-arm-gnueabihf@1.11.24': optional: true + '@swc/core-linux-arm-gnueabihf@1.13.5': + optional: true + '@swc/core-linux-arm64-gnu@1.11.18': optional: true '@swc/core-linux-arm64-gnu@1.11.24': optional: true + '@swc/core-linux-arm64-gnu@1.13.5': + optional: true + '@swc/core-linux-arm64-musl@1.11.18': optional: true '@swc/core-linux-arm64-musl@1.11.24': optional: true + '@swc/core-linux-arm64-musl@1.13.5': + optional: true + '@swc/core-linux-x64-gnu@1.11.18': optional: true '@swc/core-linux-x64-gnu@1.11.24': optional: true + '@swc/core-linux-x64-gnu@1.13.5': + optional: true + '@swc/core-linux-x64-musl@1.11.18': optional: true '@swc/core-linux-x64-musl@1.11.24': optional: true + '@swc/core-linux-x64-musl@1.13.5': + optional: true + '@swc/core-win32-arm64-msvc@1.11.18': optional: true '@swc/core-win32-arm64-msvc@1.11.24': optional: true + '@swc/core-win32-arm64-msvc@1.13.5': + optional: true + '@swc/core-win32-ia32-msvc@1.11.18': optional: true '@swc/core-win32-ia32-msvc@1.11.24': optional: true + '@swc/core-win32-ia32-msvc@1.13.5': + optional: true + '@swc/core-win32-x64-msvc@1.11.18': optional: true '@swc/core-win32-x64-msvc@1.11.24': optional: true + '@swc/core-win32-x64-msvc@1.13.5': + optional: true + '@swc/core@1.11.18': dependencies: '@swc/counter': 0.1.3 @@ -6643,12 +7072,32 @@ snapshots: '@swc/core-win32-ia32-msvc': 1.11.24 '@swc/core-win32-x64-msvc': 1.11.24 + '@swc/core@1.13.5': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.25 + optionalDependencies: + '@swc/core-darwin-arm64': 1.13.5 + '@swc/core-darwin-x64': 1.13.5 + '@swc/core-linux-arm-gnueabihf': 1.13.5 + '@swc/core-linux-arm64-gnu': 1.13.5 + '@swc/core-linux-arm64-musl': 1.13.5 + '@swc/core-linux-x64-gnu': 1.13.5 + '@swc/core-linux-x64-musl': 1.13.5 + '@swc/core-win32-arm64-msvc': 1.13.5 + '@swc/core-win32-ia32-msvc': 1.13.5 + '@swc/core-win32-x64-msvc': 1.13.5 + '@swc/counter@0.1.3': {} '@swc/types@0.1.21': dependencies: '@swc/counter': 0.1.3 + '@swc/types@0.1.25': + dependencies: + '@swc/counter': 0.1.3 + '@tanstack/match-sorter-utils@8.19.4': dependencies: remove-accents: 0.5.0 @@ -6657,8 +7106,12 @@ snapshots: '@tanstack/query-core@5.81.2': {} + '@tanstack/query-core@5.87.4': {} + '@tanstack/query-devtools@5.84.0': {} + '@tanstack/query-devtools@5.87.3': {} + '@tanstack/react-query-devtools@4.36.1(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/match-sorter-utils': 8.19.4 @@ -6668,21 +7121,18 @@ snapshots: superjson: 1.13.3 use-sync-external-store: 1.5.0(react@18.3.1) - '@tanstack/react-query-devtools@4.36.1(@tanstack/react-query@4.36.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@tanstack/match-sorter-utils': 8.19.4 - '@tanstack/react-query': 4.36.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - superjson: 1.13.3 - use-sync-external-store: 1.5.0(react@19.1.0) - '@tanstack/react-query-devtools@5.85.5(@tanstack/react-query@5.81.2(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/query-devtools': 5.84.0 '@tanstack/react-query': 5.81.2(react@19.1.0) react: 19.1.0 + '@tanstack/react-query-devtools@5.87.4(@tanstack/react-query@5.87.4(react@19.1.1))(react@19.1.1)': + dependencies: + '@tanstack/query-devtools': 5.87.3 + '@tanstack/react-query': 5.87.4(react@19.1.1) + react: 19.1.1 + '@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/query-core': 4.36.1 @@ -6704,6 +7154,11 @@ snapshots: '@tanstack/query-core': 5.81.2 react: 19.1.0 + '@tanstack/react-query@5.87.4(react@19.1.1)': + dependencies: + '@tanstack/query-core': 5.87.4 + react: 19.1.1 + '@tari-project/indexer-provider@0.11.0': dependencies: '@tari-project/tari-permissions': 0.11.0 @@ -6726,34 +7181,34 @@ snapshots: '@tari-project/tari-extension-common@0.0.15': {} - '@tari-project/tari-extension-query-builder@0.0.15(@types/node@22.14.0)(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(acorn@8.14.1)(jiti@2.4.2)(lightningcss@1.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)(webextension-polyfill@0.12.0)(zustand@5.0.5(@types/react@18.3.20)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)))': - dependencies: - '@ladle/react': 5.0.3(@types/node@22.14.0)(@types/react@18.3.20)(acorn@8.14.1)(jiti@2.4.2)(lightningcss@1.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) - '@radix-ui/react-alert-dialog': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-checkbox': 1.3.2(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-dropdown-menu': 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-icons': 1.3.2(react@19.1.0) - '@radix-ui/react-label': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-popover': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-select': 2.2.5(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-separator': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.20)(react@19.1.0) - '@radix-ui/react-tooltip': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tari-project/tari-extension-query-builder@0.0.15(@types/node@22.14.0)(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(acorn@8.14.1)(jiti@2.4.2)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.8.3)(webextension-polyfill@0.12.0)(zustand@5.0.8(@types/react@18.3.24)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)))': + dependencies: + '@ladle/react': 5.0.3(@types/node@22.14.0)(@types/react@18.3.24)(acorn@8.14.1)(jiti@2.4.2)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.8.3) + '@radix-ui/react-alert-dialog': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-checkbox': 1.3.2(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-dropdown-menu': 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-icons': 1.3.2(react@19.1.1) + '@radix-ui/react-label': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-popover': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-select': 2.2.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@19.1.1) + '@radix-ui/react-tooltip': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@tari-project/tari-extension-common': 0.0.15 - '@tari-project/tarijs-all': 0.11.0(@types/react@18.3.20)(react@19.1.0)(typescript@5.8.3)(webextension-polyfill@0.12.0)(zod@3.25.67) - '@xyflow/react': 12.7.1(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tari-project/tarijs-all': 0.11.0(@types/react@18.3.24)(react@19.1.1)(typescript@5.8.3)(webextension-polyfill@0.12.0)(zod@3.25.67) + '@xyflow/react': 12.7.1(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) class-variance-authority: 0.7.1 clsx: 2.1.1 - lucide-react: 0.479.0(react@19.1.0) - next-themes: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - sonner: 2.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + lucide-react: 0.479.0(react@19.1.1) + next-themes: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + sonner: 2.0.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) tailwind-merge: 3.3.1 tailwindcss-animate: 1.0.7 uuid: 11.1.0 zod: 3.25.67 - zustand: 5.0.5(@types/react@18.3.20)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + zustand: 5.0.8(@types/react@18.3.24)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -6819,7 +7274,7 @@ snapshots: '@tari-project/typescript-bindings': 1.13.1 '@tari-project/wallet_jrpc_client': 1.7.2 - '@tari-project/tarijs-all@0.11.0(@types/react@18.3.20)(react@19.1.0)(typescript@5.8.3)(webextension-polyfill@0.12.0)(zod@3.25.67)': + '@tari-project/tarijs-all@0.11.0(@types/react@18.3.24)(react@19.1.1)(typescript@5.8.3)(webextension-polyfill@0.12.0)(zod@3.25.67)': dependencies: '@metamask/providers': 18.3.1(webextension-polyfill@0.12.0) '@tari-project/indexer-provider': 0.11.0 @@ -6831,7 +7286,7 @@ snapshots: '@tari-project/tarijs-builders': 0.11.0 '@tari-project/tarijs-types': 0.11.0 '@tari-project/typescript-bindings': 1.13.1 - '@tari-project/wallet-connect-signer': 0.11.0(@types/react@18.3.20)(react@19.1.0)(typescript@5.8.3)(zod@3.25.67) + '@tari-project/wallet-connect-signer': 0.11.0(@types/react@18.3.24)(react@19.1.1)(typescript@5.8.3)(zod@3.25.67) '@tari-project/wallet-daemon-signer': 0.11.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -6875,7 +7330,7 @@ snapshots: '@tari-project/typescript-bindings@1.13.1': {} - '@tari-project/wallet-connect-signer@0.11.0(@types/react@18.3.20)(react@19.1.0)(typescript@5.8.3)(zod@3.25.67)': + '@tari-project/wallet-connect-signer@0.11.0(@types/react@18.3.24)(react@19.1.1)(typescript@5.8.3)(zod@3.25.67)': dependencies: '@tari-project/tari-permissions': 0.11.0 '@tari-project/tari-signer': 0.11.0 @@ -6883,7 +7338,7 @@ snapshots: '@tari-project/tarijs-types': 0.11.0 '@tari-project/typescript-bindings': 1.13.1 '@tari-project/wallet_jrpc_client': 1.7.2 - '@walletconnect/modal': 2.7.0(@types/react@18.3.20)(react@19.1.0) + '@walletconnect/modal': 2.7.0(@types/react@18.3.24)(react@19.1.1) '@walletconnect/universal-provider': 2.21.4(typescript@5.8.3)(zod@3.25.67) transitivePeerDependencies: - '@azure/app-configuration' @@ -7013,6 +7468,10 @@ snapshots: dependencies: '@types/react': 18.3.20 + '@types/react-dom@18.3.7(@types/react@18.3.24)': + dependencies: + '@types/react': 18.3.24 + '@types/react-dom@19.1.2(@types/react@19.1.0)': dependencies: '@types/react': 19.1.0 @@ -7021,6 +7480,10 @@ snapshots: dependencies: '@types/react': 18.3.20 + '@types/react-transition-group@4.4.12(@types/react@18.3.24)': + dependencies: + '@types/react': 18.3.24 + '@types/react-transition-group@4.4.12(@types/react@19.1.0)': dependencies: '@types/react': 19.1.0 @@ -7030,6 +7493,11 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/react@18.3.24': + dependencies: + '@types/prop-types': 15.7.14 + csstype: 3.1.3 + '@types/react@19.1.0': dependencies: csstype: 3.1.3 @@ -7123,6 +7591,14 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@vitejs/plugin-react-swc@3.11.0(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.27 + '@swc/core': 1.13.5 + vite: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1) + transitivePeerDependencies: + - '@swc/helpers' + '@vitejs/plugin-react-swc@3.8.1(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@swc/core': 1.11.18 @@ -7191,6 +7667,92 @@ snapshots: - utf-8-validate - zod + '@walletconnect/core@2.21.7(typescript@5.8.3)(zod@3.25.67)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.7 + '@walletconnect/utils': 2.21.7(typescript@5.8.3)(zod@3.25.67) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.39.3 + events: 3.3.0 + uint8arrays: 3.1.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/core@2.21.8(typescript@5.8.3)(zod@3.25.67)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.8 + '@walletconnect/utils': 2.21.8(typescript@5.8.3)(zod@3.25.67) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.39.3 + events: 3.3.0 + uint8arrays: 3.1.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/environment@1.0.1': dependencies: tslib: 1.14.1 @@ -7271,16 +7833,16 @@ snapshots: '@walletconnect/safe-json': 1.0.2 pino: 7.11.0 - '@walletconnect/modal-core@2.7.0(@types/react@18.3.20)(react@19.1.0)': + '@walletconnect/modal-core@2.7.0(@types/react@18.3.24)(react@19.1.1)': dependencies: - valtio: 1.11.2(@types/react@18.3.20)(react@19.1.0) + valtio: 1.11.2(@types/react@18.3.24)(react@19.1.1) transitivePeerDependencies: - '@types/react' - react - '@walletconnect/modal-ui@2.7.0(@types/react@18.3.20)(react@19.1.0)': + '@walletconnect/modal-ui@2.7.0(@types/react@18.3.24)(react@19.1.1)': dependencies: - '@walletconnect/modal-core': 2.7.0(@types/react@18.3.20)(react@19.1.0) + '@walletconnect/modal-core': 2.7.0(@types/react@18.3.24)(react@19.1.1) lit: 2.8.0 motion: 10.16.2 qrcode: 1.5.3 @@ -7288,10 +7850,10 @@ snapshots: - '@types/react' - react - '@walletconnect/modal@2.7.0(@types/react@18.3.20)(react@19.1.0)': + '@walletconnect/modal@2.7.0(@types/react@18.3.24)(react@19.1.1)': dependencies: - '@walletconnect/modal-core': 2.7.0(@types/react@18.3.20)(react@19.1.0) - '@walletconnect/modal-ui': 2.7.0(@types/react@18.3.20)(react@19.1.0) + '@walletconnect/modal-core': 2.7.0(@types/react@18.3.24)(react@19.1.1) + '@walletconnect/modal-ui': 2.7.0(@types/react@18.3.24)(react@19.1.1) transitivePeerDependencies: - '@types/react' - react @@ -7347,6 +7909,41 @@ snapshots: - utf-8-validate - zod + '@walletconnect/sign-client@2.21.7(typescript@5.8.3)(zod@3.25.67)': + dependencies: + '@walletconnect/core': 2.21.7(typescript@5.8.3)(zod@3.25.67) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 2.1.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.7 + '@walletconnect/utils': 2.21.7(typescript@5.8.3)(zod@3.25.67) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/time@1.0.2': dependencies: tslib: 1.14.1 @@ -7379,6 +7976,62 @@ snapshots: - ioredis - uploadthing + '@walletconnect/types@2.21.7': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/types@2.21.8': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + '@walletconnect/universal-provider@2.21.4(typescript@5.8.3)(zod@3.25.67)': dependencies: '@walletconnect/events': 1.0.1 @@ -7464,6 +8117,98 @@ snapshots: - utf-8-validate - zod + '@walletconnect/utils@2.21.7(typescript@5.8.3)(zod@3.25.67)': + dependencies: + '@msgpack/msgpack': 3.1.2 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.2 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.7 + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + blakejs: 1.2.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.1 + viem: 2.31.0(typescript@5.8.3)(zod@3.25.67) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/utils@2.21.8(typescript@5.8.3)(zod@3.25.67)': + dependencies: + '@msgpack/msgpack': 3.1.2 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.2 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.8 + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + blakejs: 1.2.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.1 + viem: 2.31.0(typescript@5.8.3)(zod@3.25.67) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/window-getters@1.0.1': dependencies: tslib: 1.14.1 @@ -7473,13 +8218,13 @@ snapshots: '@walletconnect/window-getters': 1.0.1 tslib: 1.14.1 - '@xyflow/react@12.7.1(@types/react@18.3.20)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@xyflow/react@12.7.1(@types/react@18.3.24)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@xyflow/system': 0.0.63 classcat: 5.0.5 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - zustand: 4.5.7(@types/react@18.3.20)(react@19.1.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + zustand: 4.5.7(@types/react@18.3.24)(react@19.1.1) transitivePeerDependencies: - '@types/react' - immer @@ -8131,7 +8876,7 @@ snapshots: file-saver@2.0.5: {} - file-selector@0.2.4: + file-selector@2.1.2: dependencies: tslib: 2.8.1 @@ -8162,15 +8907,15 @@ snapshots: flatted@3.3.3: {} - framer-motion@12.23.12(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + framer-motion@12.23.12(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: motion-dom: 12.23.12 motion-utils: 12.23.6 tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 1.3.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) fresh@0.5.2: {} @@ -8679,9 +9424,9 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@0.479.0(react@19.1.0): + lucide-react@0.479.0(react@19.1.1): dependencies: - react: 19.1.0 + react: 19.1.1 markdown-extensions@2.0.0: {} @@ -9195,10 +9940,10 @@ snapshots: negotiator@0.6.3: {} - next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) node-fetch-native@1.6.6: {} @@ -9369,13 +10114,15 @@ snapshots: prettier@3.5.3: {} + prettier@3.6.2: {} + pretty-bytes@6.1.1: {} - prism-react-renderer@2.4.1(react@19.1.0): + prism-react-renderer@2.4.1(react@19.1.1): dependencies: '@types/prismjs': 1.26.5 clsx: 2.1.1 - react: 19.1.0 + react: 19.1.1 process-warning@1.0.0: {} @@ -9436,26 +10183,31 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 - react-hotkeys-hook@4.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + react-dom@19.1.1(react@19.1.1): dependencies: - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.1.1 + scheduler: 0.26.0 + + react-hotkeys-hook@4.6.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) react-icons@4.12.0(react@18.3.1): dependencies: react: 18.3.1 - react-icons@4.12.0(react@19.1.0): + react-icons@4.12.0(react@19.1.1): dependencies: - react: 19.1.0 + react: 19.1.1 react-icons@5.5.0(react@19.1.0): dependencies: react: 19.1.0 - react-inspector@6.0.2(react@19.1.0): + react-inspector@6.0.2(react@19.1.1): dependencies: - react: 19.1.0 + react: 19.1.1 react-is@16.13.1: {} @@ -9463,24 +10215,24 @@ snapshots: react-refresh@0.14.2: {} - react-remove-scroll-bar@2.3.8(@types/react@18.3.20)(react@19.1.0): + react-remove-scroll-bar@2.3.8(@types/react@18.3.24)(react@19.1.1): dependencies: - react: 19.1.0 - react-style-singleton: 2.2.3(@types/react@18.3.20)(react@19.1.0) + react: 19.1.1 + react-style-singleton: 2.2.3(@types/react@18.3.24)(react@19.1.1) tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - react-remove-scroll@2.7.1(@types/react@18.3.20)(react@19.1.0): + react-remove-scroll@2.7.1(@types/react@18.3.24)(react@19.1.1): dependencies: - react: 19.1.0 - react-remove-scroll-bar: 2.3.8(@types/react@18.3.20)(react@19.1.0) - react-style-singleton: 2.2.3(@types/react@18.3.20)(react@19.1.0) + react: 19.1.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.24)(react@19.1.1) + react-style-singleton: 2.2.3(@types/react@18.3.24)(react@19.1.1) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@18.3.20)(react@19.1.0) - use-sidecar: 1.1.3(@types/react@18.3.20)(react@19.1.0) + use-callback-ref: 1.3.3(@types/react@18.3.24)(react@19.1.1) + use-sidecar: 1.1.3(@types/react@18.3.24)(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -9496,6 +10248,13 @@ snapshots: react-dom: 19.1.0(react@19.1.0) react-router: 6.30.0(react@19.1.0) + react-router-dom@6.30.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + '@remix-run/router': 1.23.0 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-router: 6.30.1(react@19.1.1) + react-router-dom@7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -9512,6 +10271,11 @@ snapshots: '@remix-run/router': 1.23.0 react: 19.1.0 + react-router@6.30.1(react@19.1.1): + dependencies: + '@remix-run/router': 1.23.0 + react: 19.1.1 + react-router@7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: cookie: 1.0.2 @@ -9520,13 +10284,13 @@ snapshots: optionalDependencies: react-dom: 19.1.0(react@19.1.0) - react-style-singleton@2.2.3(@types/react@18.3.20)(react@19.1.0): + react-style-singleton@2.2.3(@types/react@18.3.24)(react@19.1.1): dependencies: get-nonce: 1.0.1 - react: 19.1.0 + react: 19.1.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -9546,12 +10310,23 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + react-transition-group@4.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + '@babel/runtime': 7.27.0 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 react@19.1.0: {} + react@19.1.1: {} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -9767,10 +10542,10 @@ snapshots: dependencies: atomic-sleep: 1.0.0 - sonner@2.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + sonner@2.0.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) source-map-js@1.2.1: {} @@ -10001,44 +10776,46 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 - use-callback-ref@1.3.3(@types/react@18.3.20)(react@19.1.0): + use-callback-ref@1.3.3(@types/react@18.3.24)(react@19.1.1): dependencies: - react: 19.1.0 + react: 19.1.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - use-file-picker@2.1.2(react@19.1.0): + use-file-picker@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react@19.1.1): dependencies: - file-selector: 0.2.4 - react: 19.1.0 + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + file-selector: 2.1.2 + react: 19.1.1 use-react-router-breadcrumbs@4.0.1(react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-router-dom: 6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - use-react-router-breadcrumbs@4.0.1(react-router-dom@6.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0): + use-react-router-breadcrumbs@4.0.1(react-router-dom@6.30.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): dependencies: - react: 19.1.0 - react-router-dom: 6.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.1 + react-router-dom: 6.30.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) use-react-router-breadcrumbs@4.0.1(react-router-dom@7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 react-router-dom: 7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - use-sidecar@1.1.3(@types/react@18.3.20)(react@19.1.0): + use-sidecar@1.1.3(@types/react@18.3.24)(react@19.1.1): dependencies: detect-node-es: 1.1.0 - react: 19.1.0 + react: 19.1.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.20 + '@types/react': 18.3.24 - use-sync-external-store@1.2.0(react@19.1.0): + use-sync-external-store@1.2.0(react@19.1.1): dependencies: - react: 19.1.0 + react: 19.1.1 use-sync-external-store@1.5.0(react@18.3.1): dependencies: @@ -10048,19 +10825,23 @@ snapshots: dependencies: react: 19.1.0 + use-sync-external-store@1.5.0(react@19.1.1): + dependencies: + react: 19.1.1 + util-deprecate@1.0.2: {} uuid@11.1.0: {} uuid@9.0.1: {} - valtio@1.11.2(@types/react@18.3.20)(react@19.1.0): + valtio@1.11.2(@types/react@18.3.24)(react@19.1.1): dependencies: proxy-compare: 2.5.1 - use-sync-external-store: 1.2.0(react@19.1.0) + use-sync-external-store: 1.2.0(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - react: 19.1.0 + '@types/react': 18.3.24 + react: 19.1.1 vary@1.1.2: {} @@ -10234,10 +11015,10 @@ snapshots: react: 19.1.0 zustand: 4.5.7(@types/react@18.3.20)(react@19.1.0) - zustand-persist@0.4.0(react@19.1.0)(zustand@5.0.5(@types/react@18.3.20)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))): + zustand-persist@0.4.0(react@19.1.1)(zustand@5.0.8(@types/react@18.3.24)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1))): dependencies: - react: 19.1.0 - zustand: 5.0.5(@types/react@18.3.20)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + react: 19.1.1 + zustand: 5.0.8(@types/react@18.3.24)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) zustand@4.5.6(@types/react@18.3.20)(react@18.3.1): dependencies: @@ -10260,10 +11041,17 @@ snapshots: '@types/react': 18.3.20 react: 19.1.0 - zustand@5.0.5(@types/react@18.3.20)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)): + zustand@4.5.7(@types/react@18.3.24)(react@19.1.1): + dependencies: + use-sync-external-store: 1.5.0(react@19.1.1) optionalDependencies: - '@types/react': 18.3.20 - react: 19.1.0 - use-sync-external-store: 1.5.0(react@19.1.0) + '@types/react': 18.3.24 + react: 19.1.1 + + zustand@5.0.8(@types/react@18.3.24)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): + optionalDependencies: + '@types/react': 18.3.24 + react: 19.1.1 + use-sync-external-store: 1.5.0(react@19.1.1) zwitch@2.0.4: {} diff --git a/scripts/build_webuis.sh b/scripts/build_webuis.sh index 3d8673f525..d6e5bdddac 100755 --- a/scripts/build_webuis.sh +++ b/scripts/build_webuis.sh @@ -77,6 +77,7 @@ function build() { pushd $base_path/$1 > /dev/null pnpm install > /dev/null if [ -z ${check_typescript+x} ]; then + pnpm run clean-dist pnpm run build else npx tsc From 1aa6f63a8997ed8932c2b4357527d74eff58fa7d Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Mon, 15 Sep 2025 13:24:38 +0400 Subject: [PATCH 2/2] review comments --- bindings/src/types/UtxoUnspent.ts | 3 +-- .../types/tari-indexer-client/GetUnspentUtxosRequest.ts | 8 ++++++-- bindings/src/types/wallet-daemon-client/KeyBranch.ts | 3 +-- crates/wallet/sdk/src/apis/key_manager.rs | 3 --- .../migrations/2023-02-08-122514_initial/up.sql | 3 ++- crates/wallet/storage_sqlite/src/writer.rs | 7 +------ 6 files changed, 11 insertions(+), 16 deletions(-) diff --git a/bindings/src/types/UtxoUnspent.ts b/bindings/src/types/UtxoUnspent.ts index 161a2638e1..5a1c6419c9 100644 --- a/bindings/src/types/UtxoUnspent.ts +++ b/bindings/src/types/UtxoUnspent.ts @@ -1,6 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { RistrettoPublicKeyBytes } from "./RistrettoPublicKeyBytes"; -import type { UtxoId } from "./UtxoId"; import type { UtxoTag } from "./UtxoTag"; -export type UtxoUnspent = { id: UtxoId; version: number; tag: UtxoTag; public_nonce: RistrettoPublicKeyBytes }; +export type UtxoUnspent = { tag: UtxoTag; public_nonce: RistrettoPublicKeyBytes }; diff --git a/bindings/src/types/tari-indexer-client/GetUnspentUtxosRequest.ts b/bindings/src/types/tari-indexer-client/GetUnspentUtxosRequest.ts index ffc9520d64..a850889f59 100644 --- a/bindings/src/types/tari-indexer-client/GetUnspentUtxosRequest.ts +++ b/bindings/src/types/tari-indexer-client/GetUnspentUtxosRequest.ts @@ -1,5 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PedersenCommitmentBytes } from "../PedersenCommitmentBytes"; import type { ResourceAddress } from "../ResourceAddress"; +import type { RistrettoPublicKeyBytes } from "../RistrettoPublicKeyBytes"; +import type { UtxoTag } from "../UtxoTag"; -export type GetUnspentUtxosRequest = { commitments: Array; resource_address: ResourceAddress }; +export type GetUnspentUtxosRequest = { + tag_and_nonce_pairs: Array<[UtxoTag, RistrettoPublicKeyBytes]>; + resource_address: ResourceAddress; +}; diff --git a/bindings/src/types/wallet-daemon-client/KeyBranch.ts b/bindings/src/types/wallet-daemon-client/KeyBranch.ts index 145d9ddc5e..2f2bbff8f7 100644 --- a/bindings/src/types/wallet-daemon-client/KeyBranch.ts +++ b/bindings/src/types/wallet-daemon-client/KeyBranch.ts @@ -6,5 +6,4 @@ export type KeyBranch = | "elgamal_encryption_view_key" | "stealth_masks" | "confidential_masks" - | "nonce" - | "view_key"; + | "nonce"; diff --git a/crates/wallet/sdk/src/apis/key_manager.rs b/crates/wallet/sdk/src/apis/key_manager.rs index 42b7d9c7c6..0d4b218d0f 100644 --- a/crates/wallet/sdk/src/apis/key_manager.rs +++ b/crates/wallet/sdk/src/apis/key_manager.rs @@ -37,8 +37,6 @@ pub enum KeyBranch { ConfidentialMasks, /// Used to generate nonces that need to be recreated later, e.g. to derive the DH secret for claim burn Nonce, - /// The view key branch, used to derive an encryption key for wallet recovery. - ViewKey, } impl KeyBranch { @@ -50,7 +48,6 @@ impl KeyBranch { Self::StealthMasks => "stealth_masks", Self::ConfidentialMasks => "confidential_masks", Self::Nonce => "nonce", - Self::ViewKey => "view_key", } } } 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 7306474de5..b0b8d0a78d 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 @@ -264,4 +264,5 @@ CREATE TABLE utxo_process_queue created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE UNIQUE INDEX utxo_process_queue_resource_tag_public_nonce_uniq ON utxo_process_queue (resource_address, utxo_tag, public_nonce); \ No newline at end of file +CREATE UNIQUE INDEX utxo_process_queue_account_resource_tag_nonce_uniq + ON utxo_process_queue (account_key_index, resource_address, utxo_tag, public_nonce); \ No newline at end of file diff --git a/crates/wallet/storage_sqlite/src/writer.rs b/crates/wallet/storage_sqlite/src/writer.rs index 5a9fb026ca..70e017283c 100644 --- a/crates/wallet/storage_sqlite/src/writer.rs +++ b/crates/wallet/storage_sqlite/src/writer.rs @@ -1432,12 +1432,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { utxo_process_queue::public_nonce.eq(serialize_hex(unspent.public_nonce)), utxo_process_queue::resource_address.eq(resource_address.to_string()), )) - .on_conflict(( - utxo_process_queue::resource_address, - utxo_process_queue::utxo_tag, - utxo_process_queue::public_nonce, - )) - .do_nothing() + .on_conflict_do_nothing() .execute(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; }