From b2d12bee9921a72acf6b54b676981a4535a97f20 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Fri, 24 Oct 2025 17:21:01 +0400 Subject: [PATCH 1/2] fix(wallet)!: non-xtr resource fixes, support for badges --- .../config_presets/c_validator_node.toml | 3 + .../tari_validator_node/src/bootstrap.rs | 1 + .../tari_validator_node/src/config.rs | 20 + .../tari_validator_node/src/consensus/mod.rs | 4 +- .../tari_walletd/src/handlers/accounts.rs | 1 + .../src/handlers/stealth_utxos.rs | 42 +- .../Tokens/components/SendMoney.tsx | 13 +- .../AssetVault/Tokens/steps/FormStep.tsx | 1 - .../Components/DecryptUtxoBalance.tsx | 137 +++++ .../web_ui/src/routes/Settings/Settings.tsx | 6 + .../src/services/api/hooks/useAccounts.ts | 16 +- .../tari_walletd/web_ui/src/utils/json_rpc.ts | 6 + .../IndexerGetIdentityResponse.ts | 2 +- .../types/wallet-daemon-client/BadgeUsage.ts | 10 + .../StealthTransferRequest.ts | 2 + bindings/src/wallet-daemon-client.ts | 1 + clients/wallet_daemon_client/src/types.rs | 4 +- crates/consensus/src/hotstuff/config.rs | 1 + crates/consensus/src/hotstuff/on_propose.rs | 2 + crates/consensus_tests/src/support/harness.rs | 1 + crates/engine_types/src/crypto/elgamal.rs | 1 + .../templates/account/src/lib.rs | 4 + .../sdk/src/apis/stealth_transfer/api.rs | 523 ++++++++++-------- .../sdk/src/apis/stealth_transfer/error.rs | 3 + .../sdk/src/apis/stealth_transfer/params.rs | 36 +- integration_tests/src/wallet_daemon_client.rs | 3 +- 26 files changed, 586 insertions(+), 257 deletions(-) create mode 100644 applications/tari_walletd/web_ui/src/routes/Settings/Components/DecryptUtxoBalance.tsx create mode 100644 bindings/src/types/wallet-daemon-client/BadgeUsage.ts diff --git a/applications/tari_app_utilities/config_presets/c_validator_node.toml b/applications/tari_app_utilities/config_presets/c_validator_node.toml index 0ad487fb1e..be795bed5e 100644 --- a/applications/tari_app_utilities/config_presets/c_validator_node.toml +++ b/applications/tari_app_utilities/config_presets/c_validator_node.toml @@ -30,6 +30,9 @@ # Set to true to enable auto registration for each epoch (default = true) #auto_register = true +[validator_node.consensus] +# Disable evictions for now +enable_eviction_proposals = false #[validator_node.database] ## The type of database ("rocksdb" or "sqlite") to use (default = "sqlite") diff --git a/applications/tari_validator_node/src/bootstrap.rs b/applications/tari_validator_node/src/bootstrap.rs index 1af5d1fab4..7072076f58 100644 --- a/applications/tari_validator_node/src/bootstrap.rs +++ b/applications/tari_validator_node/src/bootstrap.rs @@ -329,6 +329,7 @@ pub async fn spawn_services( let signing_service = consensus::TariSignatureService::new(keypair.clone()); let (consensus_join_handle, consensus_handle) = consensus::spawn( config.network, + &config.validator_node.consensus, sidechain_id, state_store.clone(), local_address, diff --git a/applications/tari_validator_node/src/config.rs b/applications/tari_validator_node/src/config.rs index c09385f2c5..66fa2077c1 100644 --- a/applications/tari_validator_node/src/config.rs +++ b/applications/tari_validator_node/src/config.rs @@ -106,6 +106,8 @@ pub struct ValidatorNodeConfig { pub burnt_utxo_sidechain_id: Option, /// The path to store layer-one transactions. pub layer_one_transaction_path: PathBuf, + /// Consensus configuration + pub consensus: ConsensusConfig, } impl ValidatorNodeConfig { @@ -159,6 +161,7 @@ impl Default for ValidatorNodeConfig { template_sidechain_id: None, burnt_utxo_sidechain_id: None, layer_one_transaction_path: PathBuf::from("data/layer_one_transactions"), + consensus: ConsensusConfig::default(), } } } @@ -168,3 +171,20 @@ impl SubConfigPath for ValidatorNodeConfig { "validator_node" } } + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct ConsensusConfig { + /// Enable proposing evictions for inactive validators. If disabled, this validator will still vote on eviction + /// proposals from other validators, including voting in the affirmative if applicable, but will never propose + /// evictions itself. + pub enable_eviction_proposal: bool, +} + +impl Default for ConsensusConfig { + fn default() -> Self { + Self { + enable_eviction_proposal: true, + } + } +} diff --git a/applications/tari_validator_node/src/consensus/mod.rs b/applications/tari_validator_node/src/consensus/mod.rs index 731b689d04..bd0337e8ff 100644 --- a/applications/tari_validator_node/src/consensus/mod.rs +++ b/applications/tari_validator_node/src/consensus/mod.rs @@ -44,12 +44,13 @@ use tari_consensus::{consensus_constants::ConsensusConstants, hotstuff::Hotstuff use tari_template_lib::prelude::RistrettoPublicKeyBytes; use tari_template_manager::interface::TemplateManagerHandle; -use crate::{consensus::spec::ValidatorNodeStateStore, p2p::NopLogger}; +use crate::{config::ConsensusConfig, consensus::spec::ValidatorNodeStateStore, p2p::NopLogger}; pub type ConsensusTransactionValidator = BoxedValidator; pub async fn spawn( network: Network, + consensus_config: &ConsensusConfig, sidechain_id: Option, store: ValidatorNodeStateStore, local_addr: PeerAddress, @@ -80,6 +81,7 @@ pub async fn spawn( // TODO: make these configurable (defaults should probably be longer than 1 hour) state_tree_cleanup_interval: Duration::from_secs(60 * 60), epoch_gc_interval: Duration::from_secs(60 * 60), + enable_eviction_proposal: consensus_config.enable_eviction_proposal, }; let hotstuff_worker = HotstuffWorker::::new( diff --git a/applications/tari_walletd/src/handlers/accounts.rs b/applications/tari_walletd/src/handlers/accounts.rs index 1429d05421..92bb2972a4 100644 --- a/applications/tari_walletd/src/handlers/accounts.rs +++ b/applications/tari_walletd/src/handlers/accounts.rs @@ -972,6 +972,7 @@ pub async fn handle_stealth_transfer( input_selection: req.input_selection, resource_address: req.resource_address, max_fee: req.max_fee, + badge_usage: req.badge_usage, outputs: req .transfers .into_iter() diff --git a/applications/tari_walletd/src/handlers/stealth_utxos.rs b/applications/tari_walletd/src/handlers/stealth_utxos.rs index 4493262eef..10280938f0 100644 --- a/applications/tari_walletd/src/handlers/stealth_utxos.rs +++ b/applications/tari_walletd/src/handlers/stealth_utxos.rs @@ -19,7 +19,7 @@ use tari_wallet_daemon_client::{ UtxoInfo, }, }; -use tokio::{task::block_in_place, time::Instant}; +use tokio::{task::spawn_blocking, time::Instant}; use crate::handlers::{helpers::invalid_params, HandlerContext}; @@ -99,29 +99,37 @@ pub async fn handle_decrypt_value( .collect::>(); let timer = Instant::now(); - let balances = match context.config().value_lookup_table_file.as_ref() { + let elgamal_proofs = proofs.values().copied().cloned().collect::>(); + let sdk = sdk.clone(); + let balances = match context.config().value_lookup_table_file.clone() { Some(file) => { - let mut file = fs::File::open(file) - .map_err(|e| anyhow!("Unable to load value lookup file '{}': {e}", file.display()))?; - let mut lookup = IoReaderValueLookup::load(&mut file)?; + spawn_blocking(move || { + let mut file = fs::File::open(&file) + .map_err(|e| anyhow!("Unable to load value lookup file '{}': {e}", file.display()))?; + let mut lookup = IoReaderValueLookup::load(&mut file)?; - block_in_place(|| { - sdk.viewable_balance_api().try_brute_force_commitment_balances( + let balance = sdk.viewable_balance_api().try_brute_force_commitment_balances( &view_key.key, - proofs.values().copied(), // Copying the reference, not the ElgamalVerifiableBalanceBytes + elgamal_proofs.iter(), value_range, &mut lookup, + )?; + + anyhow::Ok(balance) + }) + .await?? + }, + None => { + spawn_blocking(move || { + sdk.viewable_balance_api().try_brute_force_commitment_balances( + &view_key.key, + elgamal_proofs.iter(), + value_range, + &mut AlwaysMissLookupTable, ) - })? + }) + .await?? }, - None => block_in_place(|| { - sdk.viewable_balance_api().try_brute_force_commitment_balances( - &view_key.key, - proofs.values().copied(), - value_range, - &mut AlwaysMissLookupTable, - ) - })?, }; info!(target: LOG_TARGET, "Brute force balance lookup took {:.2?}", timer.elapsed()); 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 7837b10ad3..a06e0bd113 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 @@ -27,8 +27,10 @@ import { useAccountsGetBalances, useAccountsTransfer } from "@api/hooks/useAccou import useAccountStore from "@store/accountStore"; import { SelectChangeEvent } from "@mui/material/Select/Select"; import { + BadgeUsage, BalanceEntry, ConfidentialTransferInputSelection, + rejectReasonToString, ResourceAddress, ResourceType, substateIdToString, @@ -195,7 +197,7 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { resourceType: props.resource_type, output_to_revealed: !transferFormState.outputToConfidential, input_selection: transferFormState.inputSelection as ConfidentialTransferInputSelection, - badge: transferFormState.badge, + badge_usage: transferFormState.badge ? { Resource: transferFormState.badge } : ("None" as BadgeUsage), output_memo: transferFormState.memo ? { Message: transferFormState.memo } : undefined, }; @@ -206,11 +208,11 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { if (!transactionResult) { throw new Error("Fee estimation failed"); } - if ("Rejected" in transactionResult) { - throw new Error(`Transaction rejected: ${transactionResult.Rejected}`); + if ("Reject" in transactionResult) { + throw new Error(`Transaction rejected: ${rejectReasonToString(transactionResult.Reject)}`); } if ("AcceptFeeRejectRest" in transactionResult) { - throw new Error(`Transaction rejected: ${transactionResult.AcceptFeeRejectRest[1]}`); + throw new Error(`Transaction rejected: ${rejectReasonToString(transactionResult.AcceptFeeRejectRest[1])}`); } let fee = resp.final_fee; @@ -270,7 +272,8 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { resourceType: props.resource_type, output_to_revealed: !transferFormState.outputToConfidential, input_selection: transferFormState.inputSelection as ConfidentialTransferInputSelection, - badge: transferFormState.badge, + // TODO: support for other types of BadgeUsage + badge_usage: transferFormState.badge ? { Resource: transferFormState.badge } : ("None" as BadgeUsage), output_memo: transferFormState.memo ? { Message: transferFormState.memo } : undefined, }; diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx index e977164d33..336e3f0c55 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx @@ -248,7 +248,6 @@ export default function FormStep({ } placeholder={isEstimatingFee ? "Estimating..." : "Auto-calculated"} onChange={onFormValueChange} - disabled={true} style={{ flexGrow: 1 }} InputProps={{ endAdornment: diff --git a/applications/tari_walletd/web_ui/src/routes/Settings/Components/DecryptUtxoBalance.tsx b/applications/tari_walletd/web_ui/src/routes/Settings/Components/DecryptUtxoBalance.tsx new file mode 100644 index 0000000000..946e979b0f --- /dev/null +++ b/applications/tari_walletd/web_ui/src/routes/Settings/Components/DecryptUtxoBalance.tsx @@ -0,0 +1,137 @@ +// 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. + +import Typography from "@mui/material/Typography"; +import TextField from "@mui/material/TextField"; +import { useState } from "react"; +import Button from "@mui/material/Button"; +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +import { Divider } from "@mui/material"; +import { stealthDecryptUtxoBalance } from "../../../utils/json_rpc"; +import { StealthUtxosDecryptValueRequest } from "@tari-project/typescript-bindings"; + +function DecryptUtxoBalanceForm() { + const [formState, setFormState] = useState({ + resourceAddress: null, + utxoId: null, + minimumExpectedValue: null, + maximumExpectedValue: null, + keyId: 0, + }); + const [balance, setBalance] = useState(null); + + const onViewBalanceClicked = async () => { + const resp = await stealthDecryptUtxoBalance({ + resource_address: formState.resourceAddress!, + ids: [formState.utxoId!], + minimum_expected_value: formState.minimumExpectedValue ? BigInt(formState.minimumExpectedValue) : null, + maximum_expected_value: formState.maximumExpectedValue ? BigInt(formState.maximumExpectedValue) : null, + view_key_id: BigInt(formState.keyId), + } as StealthUtxosDecryptValueRequest); + + setBalance(resp); + }; + + const balances = + balance && + Object.keys(balance?.balances).map((key) => { + return ( + + + {key}: {balance.balances[key] || "Failed not decrypt value"} + + + ); + }); + + const onChange = (e: React.ChangeEvent) => { + setFormState({ + ...formState, + [e.target.name]: e.target.value, + }); + }; + + return ( + <> + + + + + + + + + + {balances && ( + <> + Balances + {balances} + + )} + + ); +} + +function DecryptUtxoBalance() { + const theme = useTheme(); + return ( + +

+ Brute force a UTXO balance using a secret view key. This applies to resources that have the view key enabled. +

+ + + + +
+ ); +} + +export default DecryptUtxoBalance; diff --git a/applications/tari_walletd/web_ui/src/routes/Settings/Settings.tsx b/applications/tari_walletd/web_ui/src/routes/Settings/Settings.tsx index b37289b2dc..07c6865ebf 100644 --- a/applications/tari_walletd/web_ui/src/routes/Settings/Settings.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Settings/Settings.tsx @@ -29,6 +29,7 @@ import AccessTokens from "@routes/Wallet/Components/AccessTokens"; import SettingsTabs from "@routes/Settings/Components/SettingsTabs"; import GeneralSettings from "@routes/Settings/Components/GeneralSettings"; import ViewVaultBalance from "@routes/Settings/Components/ViewVaultBalance"; +import DecryptUtxoBalance from "@routes/Settings/Components/DecryptUtxoBalance"; export interface ISettingsMenu { label: string; @@ -63,6 +64,11 @@ function SettingsPage() { title: "View Vault Balance", content: , }, + { + label: "Decrypt UTXO", + title: "Decrypt UTXO Balance", + content: , + }, ]; return ( diff --git a/applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts b/applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts index 723aea897d..f493100674 100644 --- a/applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts +++ b/applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts @@ -41,6 +41,7 @@ import { ApiError } from "@api/helpers/types"; import queryClient from "@api/queryClient"; import { AccountOrKeyId, + BadgeUsage, ClaimBurnRequest, ComponentAddress, ComponentAddressOrName, @@ -124,7 +125,7 @@ export interface TransferParams { resourceType: ResourceType; output_to_revealed: boolean; input_selection: ConfidentialTransferInputSelection; - badge: string | null; + badge_usage: BadgeUsage; dry_run: boolean; output_memo?: Memo; } @@ -142,7 +143,11 @@ export const useAccountsTransfer = () => { resource_address: params.resource_address, destination_address: params.destination_address, max_fee, - proof_from_badge_resource: params.badge, + // TODO: we only support Resource badge usage for confidential transfers for now + proof_from_badge_resource: + typeof params.badge_usage === "object" && "Resource" in params.badge_usage + ? params.badge_usage.Resource + : null, input_selection: params.input_selection, output_to_revealed: params.output_to_revealed, output_memo: params.output_memo || null, @@ -154,6 +159,7 @@ export const useAccountsTransfer = () => { owner_account: account, input_selection: params.input_selection, resource_address: params.resource_address, + badge_usage: params.badge_usage, transfers: [ { destination_address: params.destination_address, @@ -174,7 +180,11 @@ export const useAccountsTransfer = () => { resource_address: params.resource_address, destination_public_key: parsedAddress.accountPublicKey, max_fee, - proof_from_badge_resource: params.badge, + // TODO: we only support Resource badge usage for public fungible transfers for now + proof_from_badge_resource: + typeof params.badge_usage === "object" && "Resource" in params.badge_usage + ? params.badge_usage.Resource + : null, input_selection: params.input_selection, output_to_revealed: params.output_to_revealed, dry_run: params.dry_run, 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 e76d987f43..9d79858cdc 100644 --- a/applications/tari_walletd/web_ui/src/utils/json_rpc.ts +++ b/applications/tari_walletd/web_ui/src/utils/json_rpc.ts @@ -107,6 +107,8 @@ import type { WebauthnStartRegisterResponse, WebRtcStartRequest, WebRtcStartResponse, + StealthUtxosDecryptValueRequest, + StealthUtxosDecryptValueResponse, } from "@tari-project/typescript-bindings"; import { WalletDaemonClient } from "@tari-project/wallet_jrpc_client"; import useAuthStore from "@store/authStore"; @@ -308,6 +310,10 @@ export const confidentialViewVaultBalance = ( request: ConfidentialViewVaultBalanceRequest, ): Promise => client().then((c) => c.viewVaultBalance(request)); +export const stealthDecryptUtxoBalance = ( + request: StealthUtxosDecryptValueRequest, +): Promise => client().then((c) => c.stealthUtxosDecryptValue(request)); + // nfts export const nftList = (request: ListNftsRequest): Promise => client().then((c) => c.nftsList(request)); diff --git a/bindings/src/types/tari-indexer-client/IndexerGetIdentityResponse.ts b/bindings/src/types/tari-indexer-client/IndexerGetIdentityResponse.ts index 99db681649..cd3d5752fb 100644 --- a/bindings/src/types/tari-indexer-client/IndexerGetIdentityResponse.ts +++ b/bindings/src/types/tari-indexer-client/IndexerGetIdentityResponse.ts @@ -4,5 +4,5 @@ import type { RistrettoPublicKeyBytes } from "../RistrettoPublicKeyBytes"; export type IndexerGetIdentityResponse = { peer_id: string; public_key: RistrettoPublicKeyBytes; - public_addresses: Array; + public_addresses: string[]; }; diff --git a/bindings/src/types/wallet-daemon-client/BadgeUsage.ts b/bindings/src/types/wallet-daemon-client/BadgeUsage.ts new file mode 100644 index 0000000000..585179011e --- /dev/null +++ b/bindings/src/types/wallet-daemon-client/BadgeUsage.ts @@ -0,0 +1,10 @@ +// 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 { NonFungibleAddress } from "../NonFungibleAddress"; +import type { ResourceAddress } from "../ResourceAddress"; + +export type BadgeUsage = + | "None" + | { Resource: ResourceAddress } + | { NonFungible: NonFungibleAddress } + | { AmountOfResource: { resource: ResourceAddress; amount: Amount } }; diff --git a/bindings/src/types/wallet-daemon-client/StealthTransferRequest.ts b/bindings/src/types/wallet-daemon-client/StealthTransferRequest.ts index 5860472d00..0b6d5347e3 100644 --- a/bindings/src/types/wallet-daemon-client/StealthTransferRequest.ts +++ b/bindings/src/types/wallet-daemon-client/StealthTransferRequest.ts @@ -1,6 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ConfidentialTransferInputSelection } from "../ConfidentialTransferInputSelection"; import type { ResourceAddress } from "../ResourceAddress"; +import type { BadgeUsage } from "./BadgeUsage"; import type { ComponentAddressOrName } from "./ComponentAddressOrName"; import type { StealthTransfer } from "./StealthTransfer"; @@ -8,6 +9,7 @@ export type StealthTransferRequest = { owner_account: ComponentAddressOrName; input_selection: ConfidentialTransferInputSelection; resource_address: ResourceAddress; + badge_usage: BadgeUsage; transfers: Array; max_fee: number; dry_run: boolean; diff --git a/bindings/src/wallet-daemon-client.ts b/bindings/src/wallet-daemon-client.ts index d9b3f1ddf4..f49266ec31 100644 --- a/bindings/src/wallet-daemon-client.ts +++ b/bindings/src/wallet-daemon-client.ts @@ -109,6 +109,7 @@ export * from "./types/wallet-daemon-client/SubstatesListRequest"; export * from "./types/wallet-daemon-client/BalanceEntry"; export * from "./types/wallet-daemon-client/TemplatesGetRequest"; export * from "./types/wallet-daemon-client/ProofsCancelRequest"; +export * from "./types/wallet-daemon-client/BadgeUsage"; export * from "./types/wallet-daemon-client/AccountSetDefaultResponse"; export * from "./types/wallet-daemon-client/GetNftRequest"; export * from "./types/wallet-daemon-client/SubstatesListResponse"; diff --git a/clients/wallet_daemon_client/src/types.rs b/clients/wallet_daemon_client/src/types.rs index 174866a52c..659fb5ef7b 100644 --- a/clients/wallet_daemon_client/src/types.rs +++ b/clients/wallet_daemon_client/src/types.rs @@ -39,7 +39,7 @@ use tari_ootle_common_types::{ SubstateRequirement, }; use tari_ootle_wallet_sdk::{ - apis::confidential_transfer::ConfidentialTransferInputSelection, + apis::{confidential_transfer::ConfidentialTransferInputSelection, stealth_transfer::BadgeUsage}, crypto::memo::Memo, models::{ Account, @@ -1061,6 +1061,8 @@ pub struct StealthTransferRequest { pub owner_account: ComponentAddressOrName, pub input_selection: ConfidentialTransferInputSelection, pub resource_address: ResourceAddress, + #[serde(default, skip_serializing_if = "BadgeUsage::is_none")] + pub badge_usage: BadgeUsage, pub transfers: Vec, #[cfg_attr(feature = "ts", ts(type = "number"))] pub max_fee: u64, diff --git a/crates/consensus/src/hotstuff/config.rs b/crates/consensus/src/hotstuff/config.rs index e30007879d..7314b46188 100644 --- a/crates/consensus/src/hotstuff/config.rs +++ b/crates/consensus/src/hotstuff/config.rs @@ -15,4 +15,5 @@ pub struct HotstuffConfig { pub consensus_constants: ConsensusConstants, pub state_tree_cleanup_interval: Duration, pub epoch_gc_interval: Duration, + pub enable_eviction_proposal: bool, } diff --git a/crates/consensus/src/hotstuff/on_propose.rs b/crates/consensus/src/hotstuff/on_propose.rs index 958cd33aac..07962f88e9 100644 --- a/crates/consensus/src/hotstuff/on_propose.rs +++ b/crates/consensus/src/hotstuff/on_propose.rs @@ -525,6 +525,8 @@ where TConsensusSpec: ConsensusSpec ); let evict_nodes = remaining_block_size + // Disable eviction proposals if not enabled in config + .filter(|_| self.config.enable_eviction_proposal) .map(|max| { let num_evicted = ValidatorConsensusStats::count_number_evicted_nodes(tx, start_of_chain_block.epoch())?; diff --git a/crates/consensus_tests/src/support/harness.rs b/crates/consensus_tests/src/support/harness.rs index bfb4426ca3..208fce03a9 100644 --- a/crates/consensus_tests/src/support/harness.rs +++ b/crates/consensus_tests/src/support/harness.rs @@ -653,6 +653,7 @@ impl TestBuilder { }, state_tree_cleanup_interval: Duration::from_secs(60), epoch_gc_interval: Duration::from_secs(60), + enable_eviction_proposal: true, }, } } diff --git a/crates/engine_types/src/crypto/elgamal.rs b/crates/engine_types/src/crypto/elgamal.rs index 47583a707a..78092aec4b 100644 --- a/crates/engine_types/src/crypto/elgamal.rs +++ b/crates/engine_types/src/crypto/elgamal.rs @@ -209,6 +209,7 @@ impl ElgamalVerifiableBalance { let mut results = vec![None; balances.len()]; for v in value_range { + log::error!(target: "tari::ootle::wallet", "Brute forcing value: {}", v); let value = lookup_table.lookup(v)?.unwrap_or_else(|| { // Fallback to slow lookup method if the lookup table does not contain a key for the value let pk = RistrettoPublicKey::from_secret_key(&RistrettoSecretKey::from(v)); diff --git a/crates/template_builtin/templates/account/src/lib.rs b/crates/template_builtin/templates/account/src/lib.rs index 3e14fd9713..e42777fea3 100644 --- a/crates/template_builtin/templates/account/src/lib.rs +++ b/crates/template_builtin/templates/account/src/lib.rs @@ -178,6 +178,10 @@ mod account_template { v.create_proof() } + pub fn create_proof_by_non_fungible(&mut self, nft: NonFungibleAddress) -> Proof { + self.create_proof_by_non_fungible_ids(*nft.resource_address(), vec![nft.id().clone()]) + } + pub fn create_proof_by_non_fungible_ids( &mut self, resource: ResourceAddress, diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/api.rs b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs index 4994ea177b..5ecbb1d221 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/api.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs @@ -27,12 +27,13 @@ use tari_template_lib::{ types::Amount, }; use tari_transaction::{args, Transaction, UnsignedTransaction}; -use tokio::sync::Semaphore; +use tokio::{sync::Semaphore, task::block_in_place}; use super::{ error::StealthTransferApiError, params::StealthTransferParams, types::{InputsToSpend, StealthOutputToCreate, StealthTransferOutput}, + BadgeUsage, TransferOutput, }; use crate::{ @@ -343,250 +344,264 @@ where // Critical section - TODO: use a DB transaction let _permit = self.semaphore.acquire().await.expect("semaphore is never closed"); - let lock_id = self.outputs_api.create_lock()?; + block_in_place(|| { + let lock_id = self.outputs_api.create_lock()?; - // Lock up funds for fees and transfer - let fee_inputs_to_spend = self.unlock_on_failure( - lock_id, - self.lock_fee_inputs(lock_id, &owner_account, params.max_fee, params.input_selection), - )?; - - let fee_stealth_change_amt = fee_inputs_to_spend - .total_stealth_input_amount() - .saturating_sub(params.max_fee.into()); - - // Generate fee change outputs if required - let fee_change_output = Some(StealthOutputToCreate { - owner_address: owner_address.clone(), - amount: fee_stealth_change_amt, - memo: None, - }) - .filter(|o| o.amount.is_positive()); - - // Figure out which signing key to use - if there are no revealed funds, which necessitate using an account - // withdraw auth signature, then we can use a nonce key. - let must_sign_with_account_key = fee_inputs_to_spend.revealed.is_positive(); - let (signing_key_branch, signing_key_id) = if must_sign_with_account_key { - (KeyBranch::Account, owner_key_id) - } else { - let next_index = - self.unlock_on_failure(lock_id, self.key_manager_api.next_derived_key_index(KeyBranch::Nonce))?; - (KeyBranch::Nonce, KeyId::derived(next_index)) - }; - let required_signer = self - .key_manager_api - .get_public_key(signing_key_branch, signing_key_id)?; - let required_signer_pk = required_signer.public_key.to_byte_type(); - let fee_signer = required_signer; - - // Generate fee transfer statement - let fee_transfer_statement = self.unlock_on_failure( - lock_id, - self.outputs_api.generate_transfer_statement(TransferStatementParams { - spend_key_branch: KeyBranch::Account, - spend_key_id: owner_key_id, - view_only_key_id: owner_account.view_only_key_id(), - resource_address: ¶ms.resource_address, - resource_view_key: resource_view_key.clone(), - inputs: &fee_inputs_to_spend.inputs, - input_revealed_amount: fee_inputs_to_spend.revealed, - outputs: fee_change_output, - output_revealed_amount: Amount::from(params.max_fee), - required_signer: required_signer_pk, - }), - )?; - - // Add the unconfirmed fee change output to the wallet store - if let Some(output) = fee_transfer_statement.outputs_statement.outputs.first() { - debug!( - target: LOG_TARGET, - "Adding FEE unconfirmed output with commitment {} for amount {} to account {}", - output.output.commitment, - fee_stealth_change_amt, - owner_account.component_address() - ); - self.unlock_on_failure( + // Lock up funds for fees and transfer + let fee_inputs_to_spend = self.unlock_on_failure( lock_id, - self.add_unconfirmed_output_from_statement( - lock_id, - &owner_account, - XTR, - output, - fee_stealth_change_amt, - None, - ), + self.lock_fee_inputs(lock_id, &owner_account, params.max_fee, params.input_selection), )?; - } - // NOTE: important to add this after we add the fee change, because this allows us to spend the fee change - // UTXO (XTR case) - let inputs_to_spend = self.unlock_on_failure( - lock_id, - self.lock_inputs_for_transfer( - lock_id, - owner_account.account().component_address(), - params.resource_address, - params.total_output_amount(), - params.input_selection, - ), - )?; - - // Signing key for main transfer intent - let must_sign_with_account_key = inputs_to_spend.revealed.is_positive(); - let (signing_key_branch, signing_key_id) = if must_sign_with_account_key { - (KeyBranch::Account, owner_key_id) - } else { - let next_index = - self.unlock_on_failure(lock_id, self.key_manager_api.next_derived_key_index(KeyBranch::Nonce))?; - (KeyBranch::Nonce, KeyId::derived(next_index)) - }; - let main_signer = if signing_key_branch == fee_signer.branch && signing_key_id == fee_signer.key_id { - None - } else { + let fee_stealth_change_amt = fee_inputs_to_spend + .total_stealth_input_amount() + .saturating_sub(params.max_fee.into()); + + // Generate fee change outputs if required + let fee_change_output = Some(StealthOutputToCreate { + owner_address: owner_address.clone(), + amount: fee_stealth_change_amt, + memo: None, + }) + .filter(|o| o.amount.is_positive()); + + // Figure out which signing key to use - if there are no revealed funds, which necessitate using an account + // withdraw auth signature, then we can use a nonce key. + let must_sign_with_account_key = fee_inputs_to_spend.revealed.is_positive(); + let (signing_key_branch, signing_key_id) = if must_sign_with_account_key { + (KeyBranch::Account, owner_key_id) + } else { + let next_index = + self.unlock_on_failure(lock_id, self.key_manager_api.next_derived_key_index(KeyBranch::Nonce))?; + (KeyBranch::Nonce, KeyId::derived(next_index)) + }; let required_signer = self .key_manager_api .get_public_key(signing_key_branch, signing_key_id)?; - Some(required_signer) - }; - let required_signer_pk = main_signer.as_ref().unwrap_or(&fee_signer).public_key().to_byte_type(); - - // If we're spending from the owner account, add the inputs - if inputs_to_spend.revealed.is_positive() || fee_inputs_to_spend.revealed.is_positive() { - substate_inputs.push(SubstateRequirement::unversioned(*owner_account.component_address())); - - // Add the vaults for XTR (fees) and the spending resource if different - if let Some(vault) = self - .accounts_api - .get_vault_by_resource(owner_account.component_address(), &XTR) - .optional()? - { - substate_inputs.push(SubstateRequirement::unversioned(vault.id)); - substate_inputs.push(SubstateRequirement::unversioned(vault.resource_address)); + let required_signer_pk = required_signer.public_key.to_byte_type(); + let fee_signer = required_signer; + + // Generate fee transfer statement + let fee_transfer_statement = self.unlock_on_failure( + lock_id, + self.outputs_api.generate_transfer_statement(TransferStatementParams { + spend_key_branch: KeyBranch::Account, + spend_key_id: owner_key_id, + view_only_key_id: owner_account.view_only_key_id(), + resource_address: ¶ms.resource_address, + resource_view_key: None, + inputs: &fee_inputs_to_spend.inputs, + input_revealed_amount: fee_inputs_to_spend.revealed, + outputs: fee_change_output, + output_revealed_amount: Amount::from(params.max_fee), + required_signer: required_signer_pk, + }), + )?; + + // Add the unconfirmed fee change output to the wallet store + if let Some(output) = fee_transfer_statement.outputs_statement.outputs.first() { + debug!( + target: LOG_TARGET, + "Adding FEE unconfirmed output with commitment {} for amount {} to account {}", + output.output.commitment, + fee_stealth_change_amt, + owner_account.component_address() + ); + self.unlock_on_failure( + lock_id, + self.add_unconfirmed_output_from_statement( + lock_id, + &owner_account, + XTR, + output, + fee_stealth_change_amt, + None, + ), + )?; } - if params.resource_address != XTR { + + // NOTE: important to add this after we add the fee change, because this allows us to spend the fee change + // UTXO (XTR case) + let inputs_to_spend = self.unlock_on_failure( + lock_id, + self.lock_inputs_for_transfer( + lock_id, + owner_account.account().component_address(), + params.resource_address, + params.total_output_amount(), + params.input_selection, + ), + )?; + + // Signing key for main transfer intent + let must_sign_with_account_key = !params.badge_usage.is_none() || inputs_to_spend.revealed.is_positive(); + let (signing_key_branch, signing_key_id) = if must_sign_with_account_key { + (KeyBranch::Account, owner_key_id) + } else { + let next_index = + self.unlock_on_failure(lock_id, self.key_manager_api.next_derived_key_index(KeyBranch::Nonce))?; + (KeyBranch::Nonce, KeyId::derived(next_index)) + }; + let main_signer = if signing_key_branch == fee_signer.branch && signing_key_id == fee_signer.key_id { + None + } else { + let required_signer = self + .key_manager_api + .get_public_key(signing_key_branch, signing_key_id)?; + Some(required_signer) + }; + let required_signer_pk = main_signer.as_ref().unwrap_or(&fee_signer).public_key().to_byte_type(); + + // If we're spending from the owner account, add the inputs + if inputs_to_spend.revealed.is_positive() || fee_inputs_to_spend.revealed.is_positive() { + substate_inputs.push(SubstateRequirement::unversioned(*owner_account.component_address())); + + // Add the vaults for XTR (fees) and the spending resource if different if let Some(vault) = self .accounts_api - .get_vault_by_resource(owner_account.component_address(), ¶ms.resource_address) + .get_vault_by_resource(owner_account.component_address(), &XTR) .optional()? { substate_inputs.push(SubstateRequirement::unversioned(vault.id)); substate_inputs.push(SubstateRequirement::unversioned(vault.resource_address)); } + if params.resource_address != XTR { + if let Some(vault) = self + .accounts_api + .get_vault_by_resource(owner_account.component_address(), ¶ms.resource_address) + .optional()? + { + substate_inputs.push(SubstateRequirement::unversioned(vault.id)); + substate_inputs.push(SubstateRequirement::unversioned(vault.resource_address)); + } + } } - } - // Any change outputs? - let change_amount = inputs_to_spend - .total_amount() - .checked_sub_positive(params.total_output_amount()) - .unwrap_or_else(|| { - // This is a bug because the wallet chooses inputs based on the required outputs. - error!( - target: LOG_TARGET, - "BUG: total_stealth_input_amount or params.total_amount() are negative after validation" - ); - panic!("BUG: total_stealth_input_amount or params.total_amount() are negative after validation"); + // Any change outputs? + let change_amount = inputs_to_spend + .total_amount() + .checked_sub_positive(params.total_output_amount()) + .unwrap_or_else(|| { + // This is a bug because the wallet chooses inputs based on the required outputs. + error!( + target: LOG_TARGET, + "BUG: total_stealth_input_amount or params.total_amount() are negative after validation" + ); + panic!("BUG: total_stealth_input_amount or params.total_amount() are negative after validation"); + }); + + let change_output = Some(StealthOutputToCreate { + owner_address, + amount: change_amount, + memo: None, }); - let change_output = Some(StealthOutputToCreate { - owner_address, - amount: change_amount, - memo: None, - }); + let outputs_to_create = params + .outputs + .iter() + .map(TryInto::try_into) + .collect::, StealthTransferApiError>>()?; - let outputs_to_create = params - .outputs - .iter() - .map(TryInto::try_into) - .collect::, StealthTransferApiError>>()?; + let transfer_statement = self.unlock_on_failure( + lock_id, + self.outputs_api.generate_transfer_statement(TransferStatementParams { + spend_key_branch: KeyBranch::Account, + spend_key_id: owner_key_id, + view_only_key_id: owner_account.view_only_key_id(), + resource_address: ¶ms.resource_address, + resource_view_key, + inputs: &inputs_to_spend.inputs, + input_revealed_amount: inputs_to_spend.revealed, + outputs: outputs_to_create + .into_iter() + .chain(change_output) + .filter(|o| o.amount.is_positive()), + output_revealed_amount: params.total_revealed_output_amount(), + required_signer: required_signer_pk, + }), + )?; - let transfer_statement = self.unlock_on_failure( - lock_id, - self.outputs_api.generate_transfer_statement(TransferStatementParams { - spend_key_branch: KeyBranch::Account, - spend_key_id: owner_key_id, - view_only_key_id: owner_account.view_only_key_id(), - resource_address: ¶ms.resource_address, - resource_view_key, - inputs: &inputs_to_spend.inputs, - input_revealed_amount: inputs_to_spend.revealed, - outputs: outputs_to_create - .into_iter() - .chain(change_output) - .filter(|o| o.amount.is_positive()), - output_revealed_amount: params.total_revealed_output_amount(), - required_signer: required_signer_pk, - }), - )?; - - // Add the unconfirmed change output to the wallet store - // NOTE: we can get the nth element because outputs are guaranteed to be in the order we pass them to - // generate_transfer_statement - if change_amount.is_positive() { - if let Some(output) = transfer_statement.outputs_statement.outputs.last() { - debug ! ( - target: LOG_TARGET, - "Adding TRANSFER unconfirmed output with commitment {} for amount {} to account {}", - output.output.commitment, - change_amount, - owner_account.component_address() - ); - self.unlock_on_failure( - lock_id, - self.add_unconfirmed_output_from_statement( + // Add the unconfirmed change output to the wallet store + // NOTE: we can get the nth element because outputs are guaranteed to be in the order we pass them to + // generate_transfer_statement + if change_amount.is_positive() { + if let Some(output) = transfer_statement.outputs_statement.outputs.last() { + debug!( + target: LOG_TARGET, + "Adding TRANSFER unconfirmed output with commitment {} for amount {} to account {}", + output.output.commitment, + change_amount, + owner_account.component_address() + ); + self.unlock_on_failure( lock_id, - &owner_account, - params.resource_address, - output, - change_amount, - None, - ), - )?; + self.add_unconfirmed_output_from_statement( + lock_id, + &owner_account, + params.resource_address, + output, + change_amount, + None, + ), + )?; + } } - } - // Add all input UTXO substates to transaction inputs - substate_inputs.extend( - fee_inputs_to_spend - .inputs - .iter() - // If spending XTR, we may lock the fee change UTXO for spending, however since this does not exist yet, we do not include it as a tx input - .filter(|i| i.is_on_chain) - .map(|i| &i.commitment) - .map(|commitment| UtxoAddress::new(XTR, (*commitment).into())) - .map(SubstateRequirement::unversioned), - ); - - substate_inputs.extend( - inputs_to_spend - .inputs - .iter() - .filter(|i| i.is_on_chain) - .map(|i| &i.commitment) - .map(|commitment| UtxoAddress::new(params.resource_address, (*commitment).into())) - .map(SubstateRequirement::unversioned), - ); + // Add all input UTXO substates to transaction inputs + substate_inputs.extend( + fee_inputs_to_spend + .inputs + .iter() + // If spending XTR, we may lock the fee change UTXO for spending, however since this does not exist yet, we do not include it as a tx input + .filter(|i| i.is_on_chain) + .map(|i| &i.commitment) + .map(|commitment| UtxoAddress::new(XTR, (*commitment).into())) + .map(SubstateRequirement::unversioned), + ); - let transaction = self.unlock_on_failure( - lock_id, - self.generate_transfer_transaction( - network, - &owner_account, - params, - substate_inputs, - fee_transfer_statement, - transfer_statement, - &accounts_to_create, - ), - )?; - - Ok(StealthTransferOutput { - transaction, - lock_id, - fee_inputs: fee_inputs_to_spend, - transfer_inputs: inputs_to_spend, - additional_signer: main_signer, - main_signer: fee_signer, + substate_inputs.extend( + inputs_to_spend + .inputs + .iter() + .filter(|i| i.is_on_chain) + .map(|i| &i.commitment) + .map(|commitment| UtxoAddress::new(params.resource_address, (*commitment).into())) + .map(SubstateRequirement::unversioned), + ); + + // Add badge vault if needed + if let Some(badge_resource_address) = params.badge_usage.resource_address() { + let badge_vault = self + .accounts_api + .get_vault_by_resource(owner_account.component_address(), badge_resource_address) + .optional()? + .ok_or_else(|| StealthTransferApiError::BadgeVaultNotFound { + resource_address: *badge_resource_address, + })?; + substate_inputs.push(SubstateRequirement::unversioned(badge_vault.id)); + } + + let transaction = self.unlock_on_failure( + lock_id, + self.generate_transfer_transaction( + network, + &owner_account, + params, + substate_inputs, + fee_transfer_statement, + transfer_statement, + &accounts_to_create, + ), + )?; + + Ok(StealthTransferOutput { + transaction, + lock_id, + fee_inputs: fee_inputs_to_spend, + transfer_inputs: inputs_to_spend, + additional_signer: main_signer, + main_signer: fee_signer, + }) }) } @@ -691,6 +706,7 @@ where } } + #[allow(clippy::too_many_lines)] fn generate_transfer_transaction( &self, network: Network, @@ -720,6 +736,40 @@ where builder.pay_fee_stealth(fee_transfer_statement) } }) + .then(|builder| { + // Badge if required + match ¶ms.badge_usage { + BadgeUsage::None => builder, + BadgeUsage::Resource(resx) => { + builder.call_method( + *owner_account.component_address(), + "create_proof_for_resource", + args![resx], + ) + .put_last_instruction_output_on_workspace("proof") + .add_input(*resx) + }, + BadgeUsage::NonFungible(nft) => { + builder.call_method( + *owner_account.component_address(), + "create_proof_by_non_fungible", + args![nft], + ) + .put_last_instruction_output_on_workspace("proof") + .add_input(*nft.resource_address()) + .add_input(nft.clone()) + } + BadgeUsage::AmountOfResource { amount, resource } => { + builder.call_method( + *owner_account.component_address(), + "create_proof_by_amount", + args![resource, amount], + ) + .put_last_instruction_output_on_workspace("proof") + .add_input(*resource) + } + } + }) .then(|builder| { if revealed_input_amount.is_positive() { builder @@ -747,27 +797,48 @@ where if !output.revealed_amount.is_positive() { return builder; } + let needs_to_split = params.outputs.len() > 1; let dest_account = derive_account_address_from_public_key(output.address.account_public_key()); let need_to_create_account = accounts_to_create.contains(&dest_account); - let sub_bucket_name = format!("output-sub-bucket-{i}"); - if need_to_create_account { + if needs_to_split { + let sub_bucket_name = format!("output-sub-bucket-{i}"); + if need_to_create_account { + builder + .take_from_bucket("output_bucket", output.revealed_amount, &sub_bucket_name) + .create_account_with_bucket( + *output.address.account_public_key(), + sub_bucket_name + ) + } else { + builder + .take_from_bucket("output_bucket", output.revealed_amount, &sub_bucket_name) + .call_method(dest_account, "deposit", args![Workspace( + sub_bucket_name + )]) + } + } else if need_to_create_account { builder - .take_from_bucket("output_bucket", output.revealed_amount, &sub_bucket_name) .create_account_with_bucket( *output.address.account_public_key(), - sub_bucket_name + "output_bucket" ) } else { builder - .take_from_bucket("output_bucket", output.revealed_amount, &sub_bucket_name) .call_method(dest_account, "deposit", args![Workspace( - sub_bucket_name + "output_bucket" )]) } }) }) }) + .then(|builder| { + if params.badge_usage.is_none() { + builder + } else { + builder.drop_all_proofs_in_workspace() + } + }) .with_inputs(inputs) // TODO: remove the need to add this input .add_input(XTR) diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/error.rs b/crates/wallet/sdk/src/apis/stealth_transfer/error.rs index 663233874f..1e23ef60d8 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/error.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/error.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_ootle_common_types::optional::IsNotFoundError; +use tari_template_lib::models::ResourceAddress; use crate::{ apis::{ @@ -29,6 +30,8 @@ pub enum StealthTransferApiError { KeyManagerApi(#[from] KeyManagerApiError), #[error("Insufficient funds")] InsufficientFunds, + #[error("Badge vault not found for resource {resource_address}")] + BadgeVaultNotFound { resource_address: ResourceAddress }, #[error("Accounts API error: {0}")] Accounts(#[from] AccountsApiError), #[error("Invalid parameter `{param}`: {reason}")] diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/params.rs b/crates/wallet/sdk/src/apis/stealth_transfer/params.rs index 13315f0049..f0c2e3e11f 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/params.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/params.rs @@ -1,11 +1,15 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +use tari_bor::{Deserialize, Serialize}; use tari_engine_types::{crypto::MAX_LAZY_BP_AGG_FACTORS, FromByteType}; use tari_ootle_address::OotleAddress; use tari_ootle_common_types::Network; use tari_ootle_wallet_crypto::memo::Memo; -use tari_template_lib::{models::ResourceAddress, prelude::Amount}; +use tari_template_lib::{ + models::{NonFungibleAddress, ResourceAddress}, + prelude::Amount, +}; use crate::apis::{ confidential_transfer::ConfidentialTransferInputSelection, @@ -17,6 +21,7 @@ pub struct StealthTransferParams { /// Strategy for input selection pub input_selection: ConfidentialTransferInputSelection, pub outputs: Vec, + pub badge_usage: BadgeUsage, /// Address of the resource to transfer pub resource_address: ResourceAddress, /// Fee to lock for the transaction @@ -134,3 +139,32 @@ impl<'a> TryFrom<&'a TransferOutput> for StealthOutputToCreate<'a> { }) } } + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] +pub enum BadgeUsage { + /// Do not use a badge + #[default] + None, + /// Use a resource as a badge + Resource(ResourceAddress), + /// Use a specific NFT as a badge + NonFungible(NonFungibleAddress), + /// Use a specified amount of resource as a badge + AmountOfResource { resource: ResourceAddress, amount: Amount }, +} + +impl BadgeUsage { + pub fn resource_address(&self) -> Option<&ResourceAddress> { + match self { + BadgeUsage::None => None, + BadgeUsage::Resource(addr) => Some(addr), + BadgeUsage::NonFungible(nft_addr) => Some(nft_addr.resource_address()), + BadgeUsage::AmountOfResource { resource, .. } => Some(resource), + } + } + + pub fn is_none(&self) -> bool { + matches!(self, BadgeUsage::None) + } +} diff --git a/integration_tests/src/wallet_daemon_client.rs b/integration_tests/src/wallet_daemon_client.rs index eefc482578..4c2d350a16 100644 --- a/integration_tests/src/wallet_daemon_client.rs +++ b/integration_tests/src/wallet_daemon_client.rs @@ -28,7 +28,7 @@ use tari_engine_types::substate::SubstateId; use tari_ootle_address::OotleAddress; use tari_ootle_common_types::{Epoch, SubstateRequirement}; use tari_ootle_wallet_sdk::{ - apis::confidential_transfer::ConfidentialTransferInputSelection, + apis::{confidential_transfer::ConfidentialTransferInputSelection, stealth_transfer::BadgeUsage}, models::{Account, AccountWithAddress, NonFungibleToken}, }; use tari_template_lib::{ @@ -123,6 +123,7 @@ pub async fn transfer_stealth( owner_account: source_account_name, input_selection: ConfidentialTransferInputSelection::PreferRevealed, resource_address, + badge_usage: BadgeUsage::None, transfers: vec![StealthTransfer { destination_address: dest_account.address, blinded_output_amount: amount, From 112b5311359c4798fa4ce2a6b42c7ea2e3b52e9f Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Mon, 27 Oct 2025 08:11:08 +0400 Subject: [PATCH 2/2] add new view key subcommand --- .../config_presets/c_validator_node.toml | 2 +- applications/tari_walletd/src/cli.rs | 7 ++++ applications/tari_walletd/src/main.rs | 37 +++++++++++++++++++ crates/engine_types/src/crypto/elgamal.rs | 1 - .../sdk/src/apis/stealth_transfer/api.rs | 15 ++++---- .../sdk/src/apis/stealth_transfer/error.rs | 2 + 6 files changed, 54 insertions(+), 10 deletions(-) diff --git a/applications/tari_app_utilities/config_presets/c_validator_node.toml b/applications/tari_app_utilities/config_presets/c_validator_node.toml index be795bed5e..df4b52ed1e 100644 --- a/applications/tari_app_utilities/config_presets/c_validator_node.toml +++ b/applications/tari_app_utilities/config_presets/c_validator_node.toml @@ -32,7 +32,7 @@ #auto_register = true [validator_node.consensus] # Disable evictions for now -enable_eviction_proposals = false +enable_eviction_proposal = false #[validator_node.database] ## The type of database ("rocksdb" or "sqlite") to use (default = "sqlite") diff --git a/applications/tari_walletd/src/cli.rs b/applications/tari_walletd/src/cli.rs index e42537cc22..d0c91fc967 100644 --- a/applications/tari_walletd/src/cli.rs +++ b/applications/tari_walletd/src/cli.rs @@ -138,6 +138,13 @@ pub enum Subcommand { #[clap(long, alias = "output", short = 'o')] output_path: Option, }, + #[clap(about = "Generate a key to use for resources with viewable balances")] + NewViewableBalanceKey { + #[clap(long, alias = "key")] + key_index: u64, + #[clap(long, alias = "output", short = 'o')] + output_path: Option, + }, #[clap( name = "seed-words", about = "Get current seed words of wallet (used for wallet retrieval)" diff --git a/applications/tari_walletd/src/main.rs b/applications/tari_walletd/src/main.rs index 9c6f5019cd..838f0b41d1 100644 --- a/applications/tari_walletd/src/main.rs +++ b/applications/tari_walletd/src/main.rs @@ -139,6 +139,43 @@ async fn main() -> Result<(), anyhow::Error> { return Ok(()); }, + Some(Subcommand::NewViewableBalanceKey { key_index, output_path }) => { + let wallet_store = init_wallet_store(&config)?; + let mut sdk = initialize_wallet_sdk(&config, wallet_store)?; + sdk.initialize_cipher_seed( + cli.wallet_restore + .seed_words + .as_ref() + .map(CipherSeedRestore::FromSeedWords) + .unwrap_or_default(), + )?; + let km = sdk.key_manager_api(); + let key = km.get_elgamal_encrypted_view_key(*key_index)?; + let public_key = key.to_public_key().to_byte_type(); + + let json = json!({ + "viewable_balance_public_key": public_key, + "viewable_balance_private_key": hex::encode(key.key.as_bytes()), + "key_index": key_index, + }); + match output_path { + Some(path) => { + let mut file = fs::File::options() + .create(true) + .write(true) + .truncate(true) + .open(path) + .context("failed to open file for writing")?; + serde_json::to_writer_pretty(&mut file, &json).context("failed to encode key json to file")?; + println!("Key written to {}", path.display()); + }, + None => { + println!("{}", json); + }, + } + + return Ok(()); + }, Some(Subcommand::SeedWords) => { let wallet_store = init_wallet_store(&config)?; let mut sdk = initialize_wallet_sdk(&config, wallet_store)?; diff --git a/crates/engine_types/src/crypto/elgamal.rs b/crates/engine_types/src/crypto/elgamal.rs index 78092aec4b..47583a707a 100644 --- a/crates/engine_types/src/crypto/elgamal.rs +++ b/crates/engine_types/src/crypto/elgamal.rs @@ -209,7 +209,6 @@ impl ElgamalVerifiableBalance { let mut results = vec![None; balances.len()]; for v in value_range { - log::error!(target: "tari::ootle::wallet", "Brute forcing value: {}", v); let value = lookup_table.lookup(v)?.unwrap_or_else(|| { // Fallback to slow lookup method if the lookup table does not contain a key for the value let pk = RistrettoPublicKey::from_secret_key(&RistrettoSecretKey::from(v)); diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/api.rs b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs index 5ecbb1d221..d4eb264eb6 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/api.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs @@ -481,14 +481,13 @@ where let change_amount = inputs_to_spend .total_amount() .checked_sub_positive(params.total_output_amount()) - .unwrap_or_else(|| { - // This is a bug because the wallet chooses inputs based on the required outputs. - error!( - target: LOG_TARGET, - "BUG: total_stealth_input_amount or params.total_amount() are negative after validation" - ); - panic!("BUG: total_stealth_input_amount or params.total_amount() are negative after validation"); - }); + .ok_or_else(|| StealthTransferApiError::InvariantViolation { + details: format!( + "Total input amount {} is less than total output amount {}", + inputs_to_spend.total_amount(), + params.total_output_amount() + ), + })?; let change_output = Some(StealthOutputToCreate { owner_address, diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/error.rs b/crates/wallet/sdk/src/apis/stealth_transfer/error.rs index 1e23ef60d8..611c25e11e 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/error.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/error.rs @@ -44,6 +44,8 @@ pub enum StealthTransferApiError { AmountOverflow { param: &'static str, details: String }, #[error("Insufficient revealed funds: {details}")] InsufficientRevealedFunds { details: String }, + #[error("Invariant violation: {details}")] + InvariantViolation { details: String }, } impl IsNotFoundError for StealthTransferApiError {