Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions applications/tari_walletd/src/handlers/confidential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,18 +295,18 @@ pub async fn handle_view_vault_balance(
let mut lookup = IoReaderValueLookup::load(&mut file)?;

block_in_place(|| {
sdk.confidential_crypto_api().try_brute_force_commitment_balances(
sdk.viewable_balance_api().try_brute_force_commitment_balances(
&view_key.key,
commitments.values(),
commitments.values().filter_map(|o| o.viewable_balance.as_ref()),
value_range,
&mut lookup,
)
})?
},
None => block_in_place(|| {
sdk.confidential_crypto_api().try_brute_force_commitment_balances(
sdk.viewable_balance_api().try_brute_force_commitment_balances(
&view_key.key,
commitments.values(),
commitments.values().filter_map(|o| o.viewable_balance.as_ref()),
value_range,
&mut AlwaysMissLookupTable,
)
Expand All @@ -316,6 +316,10 @@ pub async fn handle_view_vault_balance(
info!(target: LOG_TARGET, "Brute force balance lookup took {:.2?}", timer.elapsed());

Ok(ConfidentialViewVaultBalanceResponse {
balances: commitments.keys().copied().zip(balances).collect(),
balances: commitments
.iter()
.filter_map(|(id, o)| o.viewable_balance.as_ref().map(|_| *id))
.zip(balances)
.collect(),
})
Comment on lines +319 to 324

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Confirm API semantics: dropping non-viewable outputs from response.

This returns balances only for outputs that have a viewable_balance. If clients expect a full list with Nones for non-viewable items, this could be a breaking behavioral change.


🏁 Script executed:

#!/bin/bash
# Inspect response type to confirm expected semantics
rg -n -C3 'struct\s+ConfidentialViewVaultBalanceResponse|type\s+ConfidentialViewVaultBalanceResponse' --type=rust

Length of output: 659


🏁 Script executed:

#!/bin/bash
# Locate handler function and context around the balances construction
rg -n 'ConfidentialViewVaultBalanceResponse' -C8 applications/tari_walletd/src/handlers/confidential.rs
rg -n 'view_vault_balance' -C5 applications/tari_walletd/src/handlers/confidential.rs

Length of output: 2132


Include non-viewable outputs as null in response.
Handler currently drops outputs without a viewable_balance, but ConfidentialViewVaultBalanceResponse.balances: HashMap<PedersenCommitmentBytes, Option<u64>> expects keys with None for non-viewable items. In applications/tari_walletd/src/handlers/confidential.rs:319–324, replace the filter_map(...).zip(balances) logic with an iterator over all commitments, mapping each to Some(balance) or None accordingly.

🤖 Prompt for AI Agents
In applications/tari_walletd/src/handlers/confidential.rs around lines 319-324,
the current code filters out non-viewable outputs and zips only viewable
commitments with balances; instead iterate over all commitments and build the
balances map so each commitment key is present with Some(balance) for viewable
outputs and None for non-viewable ones. Implement this by creating a mutable
iterator over the balances vector and then mapping over commitments: for each
(id, output) if output.viewable_balance.is_some() take the next balance from the
balances iterator and assign Some(balance), otherwise assign None; collect that
into the HashMap<PedersenCommitmentBytes, Option<u64>>.

}
100 changes: 98 additions & 2 deletions applications/tari_walletd/src/handlers/stealth_utxos.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
// Copyright 2025 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause

use std::fs;

use anyhow::anyhow;
use axum_extra::headers::authorization::Bearer;
use indexmap::IndexMap;
use log::info;
use tari_ootle_wallet_crypto::{AlwaysMissLookupTable, IoReaderValueLookup};
use tari_ootle_wallet_sdk::apis::key_manager::KeyBranch;
use tari_template_lib::models::UtxoAddress;
use tari_wallet_daemon_client::{
permissions::JrpcPermission,
types::{StealthUtxosListRequest, StealthUtxosListResponse, UtxoInfo},
types::{
StealthUtxosDecryptValueRequest,
StealthUtxosDecryptValueResponse,
StealthUtxosListRequest,
StealthUtxosListResponse,
UtxoInfo,
},
};
use tokio::{task::block_in_place, time::Instant};

use crate::handlers::{helpers::invalid_params, HandlerContext};

use crate::handlers::HandlerContext;
const LOG_TARGET: &str = "tari::walletd::handlers::stealth_utxos";

pub async fn handle_list(
context: &HandlerContext,
Expand Down Expand Up @@ -36,3 +53,82 @@ pub async fn handle_list(
.collect(),
})
}

pub async fn handle_decrypt_value(
context: &HandlerContext,
token: Option<&Bearer>,
req: StealthUtxosDecryptValueRequest,
) -> Result<StealthUtxosDecryptValueResponse, anyhow::Error> {
let sdk = context.wallet_sdk();
context.check_auth(token, &[JrpcPermission::Admin])?;

if req.ids.len() > 10 {
return Err(invalid_params(
"ids",
Some("Cannot request more than 10 UTXOs at a time"),
));
}

let utxo_ids = req
.ids
.into_iter()
.map(|id| UtxoAddress::new(req.resource_address, id))
.map(Into::into)
.collect::<Vec<_>>();

let substates = sdk.substate_api().get_substates_from_network(utxo_ids).await?;

// Get view secret key
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);

// NOTE: we iterate in a random order (HashMap) but collect into a deterministic order (IndexMap) so that the
Comment thread
sdbondi marked this conversation as resolved.
// results are always in the same order for the same input
let proofs = substates
.iter()
.filter_map(|(id, s)| {
let id = id.as_utxo_address().map(|a| a.into_contents().id)?;
let output = s
.substate_value()
.as_utxo()
.and_then(|u| u.output())
.and_then(|o| o.output.viewable_balance.as_ref())?;
Some((id, output))
})
.collect::<IndexMap<_, _>>();

let timer = Instant::now();
let balances = match context.config().value_lookup_table_file.as_ref() {
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)?;

block_in_place(|| {
sdk.viewable_balance_api().try_brute_force_commitment_balances(
&view_key.key,
proofs.values().copied(), // Copying the reference, not the ElgamalVerifiableBalanceBytes
value_range,
&mut lookup,
)
})?
},
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());

Ok(StealthUtxosDecryptValueResponse {
balances: proofs.into_keys().zip(balances).collect(),
})
}
11 changes: 4 additions & 7 deletions applications/tari_walletd/src/jrpc_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,10 @@ async fn handler(
"claim_fees" => call_handler(context, value, token, validator::handle_claim_validator_fees).await,
_ => Ok(value.method_not_found(&value.method)),
},
Some(("stealth_utxos", method)) =>
{
#[allow(clippy::collapsible_match)]
match method {
"list" => call_handler(context, value, token, stealth_utxos::handle_list).await,
_ => Ok(value.method_not_found(&value.method)),
}
Some(("stealth_utxos", method)) => match method {
"list" => call_handler(context, value, token, stealth_utxos::handle_list).await,
"decrypt_value" => call_handler(context, value, token, stealth_utxos::handle_decrypt_value).await,
_ => Ok(value.method_not_found(&value.method)),
},
Some(("wallet", "get_info")) => call_handler(context, value, token, wallet::handle_get_info).await,
_ => Ok(value.method_not_found(&value.method)),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ResourceAddress } from "../ResourceAddress";
import type { UtxoId } from "../UtxoId";

export type StealthUtxosDecryptValueRequest = {
resource_address: ResourceAddress;
ids: Array<UtxoId>;
view_key_id: bigint;
minimum_expected_value: bigint | null;
maximum_expected_value: bigint | null;
};
Comment thread
sdbondi marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +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 StealthUtxosDecryptValueResponse = { balances: { [key in UtxoId]?: bigint | null } };
2 changes: 2 additions & 0 deletions bindings/src/wallet-daemon-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ 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/AuthLoginDenyResponse";
export * from "./types/wallet-daemon-client/StealthUtxosDecryptValueResponse";
export * from "./types/wallet-daemon-client/MintFaucetNftRequest";
export * from "./types/wallet-daemon-client/AuthLoginDenyRequest";
export * from "./types/wallet-daemon-client/PublishTemplateRequest";
Expand Down Expand Up @@ -108,6 +109,7 @@ export * from "./types/wallet-daemon-client/ProofsCancelRequest";
export * from "./types/wallet-daemon-client/AccountSetDefaultResponse";
export * from "./types/wallet-daemon-client/GetNftRequest";
export * from "./types/wallet-daemon-client/SubstatesListResponse";
export * from "./types/wallet-daemon-client/StealthUtxosDecryptValueRequest";
export * from "./types/wallet-daemon-client/AccountOrKeyIndex";
export * from "./types/wallet-daemon-client/WebauthnStartAuthResponse";
export * from "./types/wallet-daemon-client/TransferNftRequest";
Expand Down
2 changes: 1 addition & 1 deletion clients/javascript/wallet_daemon_client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tari-project/wallet_jrpc_client",
"version": "1.9.1",
"version": "1.9.2",
"description": "Tari wallet JSON-RPC client library",
"homepage": "https://github.com/tari-project/tari-ootle#readme",
"bugs": {
Expand Down
18 changes: 15 additions & 3 deletions clients/javascript/wallet_daemon_client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import type {
AccountGetDefaultRequest,
AccountGetRequest,
AccountGetResponse, AccountsAssociateStealthResourceRequest, AccountsAssociateStealthResourceResponse,
AccountGetResponse,
AccountsAssociateStealthResourceRequest,
AccountsAssociateStealthResourceResponse,
AccountsCreateFreeTestCoinsRequest,
AccountsCreateFreeTestCoinsResponse,
AccountsCreateRequest,
Expand All @@ -16,7 +18,9 @@ import type {
AccountsGetBalancesRequest,
AccountsGetBalancesResponse,
AccountsListRequest,
AccountsListResponse, AccountsRenameRequest, AccountsRenameResponse,
AccountsListResponse,
AccountsRenameRequest,
AccountsRenameResponse,
AccountsTransferRequest,
AccountsTransferResponse,
AuthGetAllJwtRequest,
Expand Down Expand Up @@ -50,7 +54,11 @@ import type {
rejectReasonToString,
SettingsGetResponse,
SettingsSetRequest,
SettingsSetResponse, StealthTransferRequest, StealthTransferResponse, StealthUtxosListRequest,
SettingsSetResponse,
StealthTransferRequest,
StealthTransferResponse,
StealthUtxosDecryptValueRequest, StealthUtxosDecryptValueResponse,
StealthUtxosListRequest,
StealthUtxosListResponse,
stringToSubstateId,
substateIdToString,
Expand Down Expand Up @@ -342,6 +350,10 @@ export class WalletDaemonClient {
}


public stealthUtxosDecryptValue(params: StealthUtxosDecryptValueRequest): Promise<StealthUtxosDecryptValueResponse> {
return this.__invokeRpc("stealth_utxos.decrypt_value", params);
}

async __invokeRpc(method: string, params: object = null) {
const id = this.id++;
const response = await this.transport.sendRequest<any>(
Expand Down
9 changes: 9 additions & 0 deletions clients/wallet_daemon_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ use crate::{
SettingsGetResponse,
StealthTransferRequest,
StealthTransferResponse,
StealthUtxosDecryptValueRequest,
StealthUtxosDecryptValueResponse,
StealthUtxosListRequest,
StealthUtxosListResponse,
TransactionGetAllRequest,
Expand Down Expand Up @@ -479,6 +481,13 @@ impl WalletDaemonClient {
self.send_request("stealth_utxos.list", request.borrow()).await
}

pub async fn stealth_utxos_decrypt_value<T: Borrow<StealthUtxosDecryptValueRequest>>(
&mut self,
request: T,
) -> Result<StealthUtxosDecryptValueResponse, WalletDaemonClientError> {
self.send_request("stealth_utxos.decrypt_value", request.borrow()).await
}

pub async fn get_settings(&mut self) -> Result<SettingsGetResponse, WalletDaemonClientError> {
self.send_request("settings.get", &json!({})).await
}
Expand Down
26 changes: 25 additions & 1 deletion clients/wallet_daemon_client/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,15 @@ use tari_ootle_wallet_sdk::{
};
use tari_template_abi::{FunctionDef, TemplateDef};
use tari_template_lib::{
models::{ConfidentialOutputStatement, EncryptedData, NonFungibleId, ResourceAddress, UtxoAddress, VaultId},
models::{
ConfidentialOutputStatement,
EncryptedData,
NonFungibleId,
ResourceAddress,
UtxoAddress,
UtxoId,
VaultId,
},
prelude::{ComponentAddress, ConfidentialWithdrawProof, ResourceType, RistrettoPublicKeyBytes},
types::{crypto::PedersenCommitmentBytes, Amount, TemplateAddress},
};
Expand Down Expand Up @@ -1118,3 +1126,19 @@ pub struct UtxoInfo {
pub is_frozen: bool,
pub is_on_chain: bool,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))]
pub struct StealthUtxosDecryptValueRequest {
pub resource_address: ResourceAddress,
pub ids: Vec<UtxoId>,
pub view_key_id: u64,
pub minimum_expected_value: Option<u64>,
pub maximum_expected_value: Option<u64>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))]
pub struct StealthUtxosDecryptValueResponse {
pub balances: HashMap<UtxoId, Option<u64>>,
}
Comment thread
sdbondi marked this conversation as resolved.
8 changes: 6 additions & 2 deletions crates/template_lib/src/models/utxo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ impl UtxoAddress {
pub fn id(&self) -> &UtxoId {
&self.0.inner().id
}

pub fn into_contents(self) -> UtxoAddressContents {
self.0.into_inner()
}
}

impl FromStr for UtxoAddress {
Expand Down Expand Up @@ -120,8 +124,8 @@ impl Display for UtxoId {
#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
pub struct UtxoAddressContents {
resource_address: ResourceAddress,
id: UtxoId,
pub resource_address: ResourceAddress,
pub id: UtxoId,
}

#[cfg(feature = "borsh")]
Expand Down
Loading
Loading