diff --git a/crates/proto/src/gen/penumbra.view.v1.rs b/crates/proto/src/gen/penumbra.view.v1.rs index d6bd86a835..fc50713ffc 100644 --- a/crates/proto/src/gen/penumbra.view.v1.rs +++ b/crates/proto/src/gen/penumbra.view.v1.rs @@ -552,10 +552,25 @@ impl ::prost::Name for BalancesRequest { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct BalancesResponse { + /// Deprecated: use `account_address` instead. + #[deprecated] #[prost(message, optional, tag = "1")] pub account: ::core::option::Option, + /// Deprecated: use `balance_view` instead. + #[deprecated] #[prost(message, optional, tag = "2")] pub balance: ::core::option::Option, + /// The default address for the account. + /// + /// Note that the returned balance is for all funds sent to the account, + /// not just funds sent to its default address. + #[prost(message, optional, tag = "3")] + pub account_address: ::core::option::Option< + super::super::core::keys::v1::AddressView, + >, + /// The account's balance, with metadata. + #[prost(message, optional, tag = "4")] + pub balance_view: ::core::option::Option, } impl ::prost::Name for BalancesResponse { const NAME: &'static str = "BalancesResponse"; diff --git a/crates/proto/src/gen/penumbra.view.v1.serde.rs b/crates/proto/src/gen/penumbra.view.v1.serde.rs index 4a06866227..a4d5dcace3 100644 --- a/crates/proto/src/gen/penumbra.view.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.view.v1.serde.rs @@ -1377,6 +1377,12 @@ impl serde::Serialize for BalancesResponse { if self.balance.is_some() { len += 1; } + if self.account_address.is_some() { + len += 1; + } + if self.balance_view.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.view.v1.BalancesResponse", len)?; if let Some(v) = self.account.as_ref() { struct_ser.serialize_field("account", v)?; @@ -1384,6 +1390,12 @@ impl serde::Serialize for BalancesResponse { if let Some(v) = self.balance.as_ref() { struct_ser.serialize_field("balance", v)?; } + if let Some(v) = self.account_address.as_ref() { + struct_ser.serialize_field("accountAddress", v)?; + } + if let Some(v) = self.balance_view.as_ref() { + struct_ser.serialize_field("balanceView", v)?; + } struct_ser.end() } } @@ -1396,12 +1408,18 @@ impl<'de> serde::Deserialize<'de> for BalancesResponse { const FIELDS: &[&str] = &[ "account", "balance", + "account_address", + "accountAddress", + "balance_view", + "balanceView", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { Account, Balance, + AccountAddress, + BalanceView, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -1426,6 +1444,8 @@ impl<'de> serde::Deserialize<'de> for BalancesResponse { match value { "account" => Ok(GeneratedField::Account), "balance" => Ok(GeneratedField::Balance), + "accountAddress" | "account_address" => Ok(GeneratedField::AccountAddress), + "balanceView" | "balance_view" => Ok(GeneratedField::BalanceView), _ => Ok(GeneratedField::__SkipField__), } } @@ -1447,6 +1467,8 @@ impl<'de> serde::Deserialize<'de> for BalancesResponse { { let mut account__ = None; let mut balance__ = None; + let mut account_address__ = None; + let mut balance_view__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Account => { @@ -1461,6 +1483,18 @@ impl<'de> serde::Deserialize<'de> for BalancesResponse { } balance__ = map_.next_value()?; } + GeneratedField::AccountAddress => { + if account_address__.is_some() { + return Err(serde::de::Error::duplicate_field("accountAddress")); + } + account_address__ = map_.next_value()?; + } + GeneratedField::BalanceView => { + if balance_view__.is_some() { + return Err(serde::de::Error::duplicate_field("balanceView")); + } + balance_view__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -1469,6 +1503,8 @@ impl<'de> serde::Deserialize<'de> for BalancesResponse { Ok(BalancesResponse { account: account__, balance: balance__, + account_address: account_address__, + balance_view: balance_view__, }) } } diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 64f793dd02..d1d3e6bf10 100644 Binary files a/crates/proto/src/gen/proto_descriptor.bin.no_lfs and b/crates/proto/src/gen/proto_descriptor.bin.no_lfs differ diff --git a/crates/view/src/client.rs b/crates/view/src/client.rs index f87d1b4835..0a69e2c277 100644 --- a/crates/view/src/client.rs +++ b/crates/view/src/client.rs @@ -7,22 +7,23 @@ use tracing::instrument; use penumbra_app::params::AppParameters; use penumbra_asset::asset::{self, Id, Metadata}; +use penumbra_asset::ValueView; use penumbra_dex::{ lp::position::{self}, TradingPair, }; use penumbra_fee::GasPrices; -use penumbra_keys::{keys::AddressIndex, Address}; +use penumbra_keys::{Address, keys::AddressIndex}; use penumbra_num::Amount; use penumbra_proto::view::v1::{ - self as pb, view_service_client::ViewServiceClient, BroadcastTransactionResponse, - WitnessRequest, + BalancesResponse, BroadcastTransactionResponse, self as pb, + view_service_client::ViewServiceClient, WitnessRequest, }; use penumbra_sct::Nullifier; use penumbra_shielded_pool::{fmd, note}; use penumbra_stake::IdentityKey; use penumbra_transaction::{ - txhash::TransactionId, AuthorizationData, Transaction, TransactionPlan, WitnessData, + AuthorizationData, Transaction, TransactionPlan, txhash::TransactionId, WitnessData, }; use crate::{SpendableNoteRecord, StatusStreamResponse, SwapRecord, TransactionInfo}; @@ -503,26 +504,19 @@ where }), ); - let balances: Vec<_> = req.await?.into_inner().try_collect().await?; + let balances: Vec = req.await?.into_inner().try_collect().await?; balances .into_iter() .map(|rsp| { - let balance = rsp - .balance - .ok_or_else(|| anyhow::anyhow!("empty balance type"))?; - - let asset = balance - .asset_id - .ok_or_else(|| anyhow::anyhow!("empty asset type"))? - .try_into()?; - - let amount = balance - .amount - .ok_or_else(|| anyhow::anyhow!("empty amount type"))? - .try_into()?; - - Ok((asset, amount)) + let pb_value_view = rsp + .balance_view + .ok_or_else(|| anyhow::anyhow!("empty balance view"))?; + + let value_view: ValueView = pb_value_view.try_into()?; + let id = value_view.asset_id(); + let amount = value_view.value().amount; + Ok((id, amount)) }) .collect() } @@ -869,26 +863,27 @@ where Some(status) => match status { pb::witness_and_build_response::Status::BuildProgress(_) => { // TODO: should update progress here - }, + } pb::witness_and_build_response::Status::Complete(c) => { return c.transaction .ok_or_else(|| { anyhow::anyhow!("WitnessAndBuildResponse complete status message missing transaction") })? .try_into(); - }, + } }, None => { // No status is unexpected behavior return Err(anyhow::anyhow!( "empty WitnessAndBuildResponse message" - ));}, + )); + } } } Err(anyhow::anyhow!("should have received complete status or error")) } - .boxed() + .boxed() } fn unclaimed_swaps( diff --git a/crates/view/src/service.rs b/crates/view/src/service.rs index 285f255c59..d9ac996fa1 100644 --- a/crates/view/src/service.rs +++ b/crates/view/src/service.rs @@ -18,6 +18,7 @@ use tonic::{async_trait, transport::Channel, Request, Response, Status}; use tracing::instrument; use url::Url; +use penumbra_asset::asset::Metadata; use penumbra_asset::{asset, Value}; use penumbra_dex::{ lp::{ @@ -28,6 +29,8 @@ use penumbra_dex::{ TradingPair, }; use penumbra_fee::Fee; +use penumbra_keys::keys::WalletId; +use penumbra_keys::AddressView; use penumbra_keys::{ keys::{AddressIndex, FullViewingKey}, Address, @@ -918,6 +921,7 @@ impl ViewService for ViewServer { })) } + #[allow(deprecated)] #[instrument(skip(self, request))] async fn balances( &self, @@ -949,15 +953,56 @@ impl ViewService for ViewServer { tracing::debug!(?account_filter, ?asset_id_filter, ?result); + let self2 = self.clone(); let stream = try_stream! { + // retrieve balance and address views for element in result { - yield pb::BalancesResponse { - account: account_filter.clone().map(Into::into), - balance: Some(Value { - asset_id: element.0.into(), - amount: element.1.into(), - }.into()), + let metadata: Metadata = self2 + .asset_metadata_by_id(Request::new(pb::AssetMetadataByIdRequest { + asset_id: Some(element.id.into()), + })) + .await? + .into_inner() + .denom_metadata + .context("denom metadata not found")? + .try_into()?; + + let value = Value { + asset_id: element.id, + amount: element.amount.into(), + }; + let value_view = value.view_with_denom(metadata)?; + + let address: Address = self2 + .address_by_index(Request::new(pb::AddressByIndexRequest { + address_index: account_filter.clone().map(Into::into), + })) + .await? + .into_inner() + .address + .context("address not found")? + .try_into()?; + + let wallet_id: WalletId = self2 + .wallet_id(Request::new(pb::WalletIdRequest {})) + .await? + .into_inner() + .wallet_id + .context("wallet id not found")? + .try_into()?; + + let address_view = AddressView::Decoded { + address, + index: element.address_index, + wallet_id: wallet_id.into(), + }; + + yield pb::BalancesResponse { + account_address: Some(address_view.into()), + balance_view: Some(value_view.into()), + balance: None, + account: None, } } }; diff --git a/crates/view/src/storage.rs b/crates/view/src/storage.rs index 69b484ba24..fd75eb13d2 100644 --- a/crates/view/src/storage.rs +++ b/crates/view/src/storage.rs @@ -1,8 +1,22 @@ +use std::str::FromStr; +use std::{collections::BTreeMap, num::NonZeroU64, sync::Arc, time::Duration}; + use anyhow::{anyhow, Context}; use camino::Utf8Path; use decaf377::{FieldExt, Fq}; use once_cell::sync::Lazy; use parking_lot::Mutex; +use r2d2_sqlite::{ + rusqlite::{OpenFlags, OptionalExtension}, + SqliteConnectionManager, +}; +use sha2::{Digest, Sha256}; +use tokio::{ + sync::broadcast::{self, error::RecvError}, + task::spawn_blocking, +}; +use url::Url; + use penumbra_app::params::AppParameters; use penumbra_asset::{asset, asset::Id, asset::Metadata, Value}; use penumbra_dex::{ @@ -23,24 +37,19 @@ use penumbra_shielded_pool::{fmd, note, Note, Rseed}; use penumbra_stake::{DelegationToken, IdentityKey}; use penumbra_tct as tct; use penumbra_transaction::Transaction; -use r2d2_sqlite::{ - rusqlite::{OpenFlags, OptionalExtension}, - SqliteConnectionManager, -}; -use sha2::{Digest, Sha256}; -use std::str::FromStr; -use std::{collections::BTreeMap, num::NonZeroU64, sync::Arc, time::Duration}; +use sct::TreeStore; use tct::StateCommitment; -use tokio::{ - sync::broadcast::{self, error::RecvError}, - task::spawn_blocking, -}; -use url::Url; use crate::{sync::FilteredBlock, SpendableNoteRecord, SwapRecord}; mod sct; -use sct::TreeStore; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct BalanceEntry { + pub id: Id, + pub amount: u128, + pub address_index: AddressIndex, +} /// The hash of the schema for the database. static SCHEMA_HASH: Lazy = @@ -237,18 +246,18 @@ impl Storage { &self, address_index: Option, asset_id: Option, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let pool = self.pool.clone(); spawn_blocking(move || { let query = "SELECT notes.asset_id, notes.amount, spendable_notes.address_index - FROM notes - JOIN spendable_notes ON notes.note_commitment = spendable_notes.note_commitment - WHERE spendable_notes.height_spent IS NULL"; + FROM notes + JOIN spendable_notes ON notes.note_commitment = spendable_notes.note_commitment + WHERE spendable_notes.height_spent IS NULL"; tracing::debug!(?query); - let mut balances = BTreeMap::new(); + let mut entries = Vec::new(); for result in pool.get()?.prepare_cached(query)?.query_map([], |row| { let asset_id = row.get::<&str, Vec>("asset_id")?; @@ -277,19 +286,19 @@ impl Storage { continue; } } - // Skip this entry if not captured by asset id filter if let Some(asset_id) = asset_id { if asset_id != id { continue; } } - balances - .entry(id) - .and_modify(|x| *x += amount) - .or_insert(amount); + entries.push(BalanceEntry { + id, + amount, + address_index: index, + }); } - Ok(balances) + Ok(entries) }) .await? } diff --git a/proto/penumbra/penumbra/view/v1/view.proto b/proto/penumbra/penumbra/view/v1/view.proto index 6c6b82f2e5..f53dfcf5c1 100644 --- a/proto/penumbra/penumbra/view/v1/view.proto +++ b/proto/penumbra/penumbra/view/v1/view.proto @@ -307,8 +307,18 @@ message BalancesRequest { } message BalancesResponse { - core.keys.v1.AddressIndex account = 1; - core.asset.v1.Value balance = 2; + // Deprecated: use `account_address` instead. + core.keys.v1.AddressIndex account = 1 [deprecated = true]; + // Deprecated: use `balance_view` instead. + core.asset.v1.Value balance = 2 [deprecated = true]; + + // The default address for the account. + // + // Note that the returned balance is for all funds sent to the account, + // not just funds sent to its default address. + core.keys.v1.AddressView account_address = 3; + // The account's balance, with metadata. + core.asset.v1.ValueView balance_view = 4; } // Requests sync status of the view service.