From c382b0f4e94ed5bb0edbcbcaf2b8aea08d358d11 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Fri, 23 Feb 2024 16:07:54 +0100 Subject: [PATCH] proto: change BalancesResponse to have Views --- crates/proto/src/gen/penumbra.view.v1.rs | 15 +++++ .../proto/src/gen/penumbra.view.v1.serde.rs | 36 +++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 346387 -> 347010 bytes crates/view/src/client.rs | 43 ++++++------- crates/view/src/service.rs | 57 ++++++++++++++++-- crates/view/src/storage.rs | 57 ++++++++++-------- proto/penumbra/penumbra/view/v1/view.proto | 14 ++++- 7 files changed, 166 insertions(+), 56 deletions(-) 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 64f793dd0227fae6c3b31dd13631b3335ad6b0ec..d1d3e6bf10013414f54c6ec8c9c7d1119d1bc7f9 100644 GIT binary patch delta 6080 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 delta 5508 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 CFubS$ 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.