From f5a42b50d5ddb0f4bd190a9c81cb2b5b29d6fea9 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Mon, 26 Feb 2024 23:18:58 -0800 Subject: [PATCH] asset: add proto and domain support for equivalent values I added the `EquivalentPrice` to the asset proto package rather than solely in the TxP, because I realized that the use cases we care about most immediately (having equivalent prices for balances) don't actually involve `TransactionView`s at all. While doing that I realized that in the context of viewing a _transaction_, it doesn't really make sense to just have an "equivalent value" on its own, so I added an `as_of_height` to allow indicating when the prices were relevant. We probably shouldn't try to expose this in the transaction view just yet, since it's a little unclear how we'd want to indicate historical prices. --- Cargo.lock | 1 + crates/bin/pcli/src/transaction_view_ext.rs | 1 + .../src/gen/proto_descriptor.bin.no_lfs | Bin 98383 -> 81319 bytes crates/core/asset/Cargo.toml | 1 + crates/core/asset/src/equivalent_value.rs | 49 +++ crates/core/asset/src/estimated_price.rs | 57 +++ crates/core/asset/src/lib.rs | 4 + crates/core/asset/src/value.rs | 94 +++- .../core/component/shielded-pool/src/note.rs | 2 +- .../src/view/transaction_perspective.rs | 61 ++- .../proto/src/gen/penumbra.core.asset.v1.rs | 83 ++-- .../src/gen/penumbra.core.asset.v1.serde.rs | 401 +++++++++++++----- .../src/gen/penumbra.core.transaction.v1.rs | 28 ++ .../gen/penumbra.core.transaction.v1.serde.rs | 149 +++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 366229 -> 350711 bytes .../penumbra/core/asset/v1/asset.proto | 44 +- .../core/transaction/v1/transaction.proto | 11 + 17 files changed, 801 insertions(+), 185 deletions(-) create mode 100644 crates/core/asset/src/equivalent_value.rs create mode 100644 crates/core/asset/src/estimated_price.rs diff --git a/Cargo.lock b/Cargo.lock index 4f89d61475..5e897ce876 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4760,6 +4760,7 @@ dependencies = [ "ibig", "num-bigint", "once_cell", + "pbjson-types", "penumbra-num", "penumbra-proto", "poseidon377", diff --git a/crates/bin/pcli/src/transaction_view_ext.rs b/crates/bin/pcli/src/transaction_view_ext.rs index f4873f6210..91a7732a5e 100644 --- a/crates/bin/pcli/src/transaction_view_ext.rs +++ b/crates/bin/pcli/src/transaction_view_ext.rs @@ -104,6 +104,7 @@ fn format_value_view(value_view: &ValueView) -> String { ValueView::KnownAssetId { amount, metadata: denom, + .. } => { let unit = denom.default_unit(); format!("{}{}", unit.format_value(*amount), unit) diff --git a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs index 67d781308dc15349cc1633e0986b9be4d49fda1f..aea1e9b9c172697fe70b023165adfdd88442d114 100644 GIT binary patch delta 14089 zcmZ`=3v?7!n(lk+R&}R337txKg~T)=h=eyPg1i(#Mg$Rs@rW-XG!P~Ph9HQ`*ouMz z!$c@T7*OJ%NWc}*21j-rn87`Z?5K#S>o8|`c9C^};T17HK!*MP`>5)jJ?rs!^4+@s z_do7`|N8H(_Rtf~)=NUTA1}97i>G48grS*DC)V^VgUBm?&~E$4)rqo0xyP^hS)N;?FZT|L)Yh1=Uki{zqkfXpH|S_unhq zPC7E8^Cgx^cdumpF!NunoXTeVi5|20h{qfAOIf=$y{g;(=>zUYJ-b_EeU!P|+qf*( zv$|y-@AYal-96I5jkzr5Sv?{n-5i(4;_6Q|*fY{o4l>W8Kh>e8aqU27zx_$6~iq3ggjmzj1KSfx`n~ z2I1j>Fryh>M=i`CJiMgTy-o^q&l=%h8a!a&h(MS_ctjw~Aw0q>(ZU?UBg&HQ1St$K z(ytzJ_rQ^Xum$0f@r0gkL3m_Axj{6)Yd7~XDU9fi_P-voc;M(jSU`AmAdFC1qYIK+ zSU`Amm#*#rDU69@{N|wp(_;c*8w)TdUaY6v5FXR1n?VpBQ`tkVBIB5NJMX0*Ik4mQ zfW)C^dKEDdB)6BT^#%gT?LB&GlBj2m<-OCIB?^|Y0ZSAtW8EcP`yloW zZDvl1J%uZ806;P&AYt%(ijF;=(adC5wbp=OnQ~2c&4LY7rB{%14wkBbg%YhQU?F`~ zdId2PBvm>MB$Xn!n_ObtjZkW7_nowi{#?Wn1pt~OJ_GPQf9HU)CibY8;}iB#-? zz%oteH4sRq>AVI4$+R9lL5ZkX5l>9#Ke3boi{dxk?cjED9g)UEiu?>wTArJ{t#)e7 zBX!jaQdr-7H%gLnC1!}+5MwQ7hVTTYy^NdaDh7smRUDVy9)AS=cUMr*)c%)y{+M#EzAkCHe%9X{}~u(a~zn$}10_gKk!r$`Gf) zHwy``vIoj*f_1mSRTC^3RiYZbYdEJ$R8y>WE)Ym+f_2BlTHZ%5CRL)^fP^YhZIC=v ziBh#mldV*WY6DYHiso{Y5>$!i1}s#G<_4xj!7{g4)g{iOAemdy)!k0ppAt(v!XLH# zC#&1}HKUfN=_Q9zh&GP!_Xoy|z~6!Z*z;gCphFOwSVJt>ldSAo11*9B z2Dy`o)Aqzt=Ksg0ANz;ywEHY&c^%X=8ygx=@(K(@d0oodc5ttSS;BLcv&b3dTNC>A zUCv@9$bEr-BA&B?@omhKLcriy!J={ZZtN`87Kv5?4A<8%Q55h{E@L1x z#e&2dGQ|)hvJD`<&F^w|-*iJZIlXWivdIz3bxgDk$g!VkAQ28gVjU|i3e3SeHD*g- z^U^>l;XtA>kVvDKjCx`{@;l(fPNjjoZ~!9fS)w3N67$3_vwcR|(m?2Q3=+R2pN}ye z%^Xi`f=KaUmYi5Mqt~=lP4(>R`6++G#6daLwMq59_M0X?+1Cy9B8f>TT%#P&Zej(6 zZZFO1dZNj1p46wjDceOC)0$XeTepjz7Wc$v|NBW@)0?x|;+VFX*vwMqdg7(**yTb$ z212)TA@LIP!f5At;^pkD%7X+3!qDYG;$@c5=|a)Y_rw+^+6PYNLj(f_;Q&Ooup)Yw zsMKxaiC3~E+CTyWp+p-1n}$G5=4Q4P=fa8>m)&41zHDK z@+L#eMzu@G$_A(^8zA=Bn+%0hKLy#F3|*Us*xqk46mm&Lnlg-+=!eir&@kbh0Ei=m z5+z46tf2jLgF%&H#nd^%Dzb4`XcQU(f`$tH0AN5xdUvq`V-)%~Kp2IiF;M}&&HUWS zFARG-q(JY6(RdrJoyyYNEKy{jlxg3={WX^qP`{65{xW$W8@ey#z?lfcL2T&!xQ}@z zka(2L z!w!ZV4mb{mR!~Jd$nb7c9B4-X%e%Nwt6-p-eZc(c2bT`}Af$+b;)5`rRN+2g1!f)5 zi~!~u)t!kp9L^5S>|m@p(b9|7Wx zUyfI+?K$UJ9@vGK+u0{ECmGpx5iS3Ed8FLb*9__0u!KNB1^w#@h_D zpOsPAz?T?KzKV)AFrcBL4Sb2=?5m%G=@P@?S3@veVmST2PI>R(+~YjO`7&-5Z{o?j zs%mGXl5;Z4XLM%XQykrI2`VZkQ5c@)e!;D6$4Uf^PjmF7)bq#z1mbCqrZgCU)~7jo z-e3TFpXO+K4I?Hj^-HU-x@BoL5f!qf*+dkGr5x?8A)-ZF%29!2|L#x)TIoMt{f|Rd z@*o6MfL8Lj+GLmlj+MN$hYo>7BD{AWw??ioe5v=R&#E0-9}-aYspq%tENC4g#)071Xj&;!E0wOki|K+vxx z4{5QZls64r7k(fx0U9QXR|D78I*rG2c7fX9fS_vNy6{J-@HcW?ZZ=Cei-liDt~xQVR|Ev(M5|8 zF^a+SCI&!I2dLBm!t&=$3;?0-c@qObsC(YTAV#*lz;TQr!-2pAXqag4yujm0`818k za(3r#W;Uo^;5f=CqY#58j$@;uq8I=gDvCi9$H7rQ1yd8p$x%ZvHE|pr`yvK8p7Roq zynz^$|F0O_IlFA~OB@FqQkO$9c-h1N2)v)eS*IMceOHj+3Rbg0|nQyrWtlh)UTm749hM6-0OV z{pXCHydxx_?YD#L1|mmpza2bb2x$B5;2==+$VR}f0}RAlcn9x$y`~4#PXEZ9(NlJY z^wb&b4C$#e*csAOXRtG*w?V%%q_;u8ljvX1M-sT6^9hd}vRMB+{O69|n)%(_<4(aR zJXWYK7hPKZquk%Uuyxo`qaTCNf7FB;5c-dDPgQ6@=s(I)p{T^VqOs zAqOq|v5Dl;S|4aklS4|syfiodL9mmgv{FDc0c%qkPpgcIk z`}Q{w{Ac*JBjwq{L;AWvHH+3068*n_R13d)q$PzAM#tvq4EK$+6Y zyQ{)4>lR?op;{wKH{cCJ+_B^W9rwTI{BPX<^6@d=_dHtYj>O9k?;} zd-*1k!-(odo}1^6*7KnJ2kviA|ES9kTpz?}9-Sh7;5fsYdBB&s-}Z?gr7v@RJf?Y6 zo-cD8sA;#lv^ZBdAFPeVRU1LI#qbbY>eV19f9=P;P08m#JKxkNIm34MML4;r|C>t`*Ecr1{ zH-r?_`Zid48)FVA8mu6{sP%2Ig8YiY$ObFOuP8BEFj55r57$}TtQ_vK2%2C}f4olX zNy8Xwkk?rWRhLDZ>z1~iIr(FGVdM3)4O zp+nR@H>N|6a-gU&^e6|48bgoJTd%jcvBHHSK%r zr3Zw9O;)m>j#wQ0o2(vv@zs#%adolT-}lUu_iYa8b3wm3q^H}A&6Y0UkO2K=3m>(p zfCGYlvvsv9;DDgtZ1ua|U8Bs$MaNcuYd{1Gdk7`7`UNPu8hNRR-*uF!@A2zHSTdsP^4 zJF>@G^YeYf_Jj-tVAvBf(7nkXOSh>mF95?H3n#IERScdd_FDespZ{UR-jKor#omy@ z1I1p;GnK;w#a^pNFL%G9D8!-J;uXaipRG#zmA_~j3TTqH5LEAmRE3~=Hw;K2sNSW3 zES8H>0ZBRR1s4Dy^y5{)#yD-U-!Da2rTbe`nneoEbm#k?tO;gzL&Pi`xaWH z$NUNJ-RwW{+%4(%Eq&+f$~#{`b-e=3 zoDCr;VJ==FanCtyMSQ^|4egu5R;-=-xec%4vcEfSWo}*lM}Gft;}eEq=W(+g03mhU zN~i`E5I!BZ(4f+G0K|4UZt1raAhyGC3va2v(h|g}9XM(E|FHJ-{U<{O)D4^r6;L;D z(n|EvjMNRBwEC+nb1*=`N$a{%<|!1Mw1$s#&ngA*_MGLv+i)&@E>u8oqjRAGvf`Yn z7jfAtoU?G5qCB8h;ha_0*FY#ZXI1u55kufxEM6!BAE&^#Sn*snQwBbS=^R=ziRmQ} zr*hk3<*REYpudB1E}p8n65*~~S|oet7Au72>5GnxDdO|2z-QdmPV%0asrfTg^OE>a zX6*V|+<&?8`BBO(>T)qnrF)!Hck`^!F9SfB^Q=gyqXVGi#P1C%kZo}|y~4k=;p(0% zMBpq5WJA~_VVM?K>5tpkx!1~2fSQ|Z2muM{3rd{2`FelCrg6jZJww!oX5wN4Q12&9 z14+Hm`w0+c)(idlgkB13{D0qc|M)dR@1nTeMSyA-0Yd8SC zM6AetiQ0->YMV9)-n)1$_n&Qgty}G!`N>78`N`_pb8AwwQ?>I`(~`J;N1&5)=Heq? z-JD0}O-;G}?_ca@C9CKAtuJ;?Ltcd&Z17`)&?j9$DBB?Py(A#~*dTCLqNM^v25u0Y zRI3Sy4BQ~D?up(KhEb=tQSi;eQnTfUos9zb$7N{b@R%%rL5McRe#5Uy)A&n$7m!cj z{tF^6Uv{Z905h6|ZuTTr12Nep$^l3;(IV2FIt>8-O6U(O5<>z5>NANPkTcK+9+h#- zB^}LzcPjn^&-}|vW7rrI5;_OtK>)FQ%_bosCB+iz9taQ-YNiNnpi-4fX6zQ3BU?7| z^zM*@4#2yG?)oqZ9J>X6prFD^M}pk~mpB?C%DY9W>fkXMQQj@C>8GQNTl%*I-xoxg z?0HK>^W=ShuDYdv8_8PipK0#e1%Ihe7xD>=dt0Dm?k-~{Z|Z*|{Oh+>cKJ>AJnF1} zlRXc3uke>_tL(NndmcHvm*#yY6M)?KLhv5N2l6vVw(Yet&%L&tJN^6i@7FWw4}PP+ z|I?j?>4`I|>yopp7A0#^RnwC5=Om}jtE!ucYmVya@^>B!XCh~-YLUVVs%omIB_Cci zKb5STKQC1^J6T=lCZ|`|R@KxjO3tmCSBFf0WbPbHpE@;FSBGB?Bqxp^IsTf5`}FVE zZ&2R>{rX+o{l;W+RBhG6HL0Yet4hzWh5-Jpsd{V?$Wk@aNKgfyDiItAAQt|MaN()2 zd?ECGd#1$I+C9d`bdta&~ZZG%zQ%W0M%LH4|wzBeP=@jbSOU?Dxee1 zS)n`9TzR57EA$%%5DLx;{e}UAg0li|nBmd~{FZl0e{|8j7;@!n zGaEueLMqXDo^x45p0ZWHLXGleD$(@L$fG)Ug&Y}yKjFv~;|L%exndjvgxnS52p}A} zVjQ7fe2GmT-S5b=!MMcM?+ze{0V-DjVb~H|zdQ0hXQ>@ouMMLHe5s9llPNGPpA1`W z`_=oZhAp>s3Stn7mfQM;3kZ?rc0%1(0>Y-{Ha-dcHzipTmH}ie7>o-Hs*5@r<3Beeg5(5N(5u zu?$b3E@Qk0gkZ*a4+zULwmv2U!uyP^kI8`WK4asUOx`ER`?u7><8NqrE7U^%zh(T# zQ#n=p4+t%98UN|bxX1Q;zklDDJ;wEfbRAH+4hYxx7}pCt=N&upN9{Tt5#O<6ZQakv z^#XGJfSoz={=cz@4j6+l3M~hW+kg-{VB7|TK?jW6fN=YOaT^eBA0W5SQ&tsFv-Q5s zKeGLtLuW_6Z%5n9MhN4i`vY5)$WBROsKJ2xeS#c-A0OEGdQpKkfCez(L)#zxQP(aX z+WH4Iav1YIwA;0JZ_x9A583`x9}P?&vh{B`XdZ38L$>?@4f&1+<*>~^w$*JRFjfTv z>Wv48RXJ?y?;(Ite%LNiEhQj=aM;$jmw*Vu$F`%cGyoBVkL`krT)FrKr1(=T{^7AV z(VqV_R6=|HQ?vA#2PL1HJr4*apPHqo!~W+o^{+3W)c@S9KM=$K)%pWM-RHFaD=oYM zJkL31N4~K$pMSiZ6&MOfI-F^W| zS$wHeMnUv~t-lp}^kX-IhKahm3!zce)m^aD`VtGXL3JUFHYR^`Mx;Acd43+nmgnQ$LHP0H9@?qqP9ShHZ|{4nT-* zbBff_1`tZNIrwr$2Zlo0Dcc=>*-kMo{P zTsZ)r_B;9uxf}q+ey31XJ2?QA`yEs}%5^z_nzG+1>ER~am>xie?RQEm-H*9e3zCD5 z=vn-Gp1iqcVb!9#8apvplrfG;_sGMGU70Rtf??TAa+oU|t{rJbU#p^PNK@7E0}IDT+M|6l@b4Ge^& zv^jprCZ+B1gHzNi6anWCPTziXb;d=J=R6gq|I?@V&n!uw#T)Koe=ed6%%`IGUuhmi zCKXYhEQ>adVyrF&0F}$4Iwb*-lFOp;wz6OWLULKOo%-wv2xZHn`0Pn3Swt!MOq4f9 zRZ0S5_A^np$gQW}Jpz-e71730&Yn$62xTjx+CMn}|5ikGO3DFHtcdEAlmk$?BC1nT z4j>y=M0HB40WAKCD6(O^)(VbQQBhGmJbBwRT=G=o=vvoUVX;BuXg;MR9;+bB0T{6= zsS*J3i_N`5LQt;GrlbU0o$VJT*y^bMF^Uuc z*0K24 Kk4M?zNB;+Kc!Zk( delta 31020 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 diff --git a/crates/core/asset/Cargo.toml b/crates/core/asset/Cargo.toml index 68a189b5f9..e1ec70b1ce 100644 --- a/crates/core/asset/Cargo.toml +++ b/crates/core/asset/Cargo.toml @@ -45,6 +45,7 @@ serde_with = {workspace = true} sha2 = {workspace = true} thiserror = {workspace = true} tracing = {workspace = true} +pbjson-types = {workspace = true} [dev-dependencies] proptest = {workspace = true} diff --git a/crates/core/asset/src/equivalent_value.rs b/crates/core/asset/src/equivalent_value.rs new file mode 100644 index 0000000000..688f989c13 --- /dev/null +++ b/crates/core/asset/src/equivalent_value.rs @@ -0,0 +1,49 @@ +use crate::asset::Metadata; +use penumbra_num::Amount; +use penumbra_proto::{penumbra::core::asset::v1 as pb, DomainType}; +use serde::{Deserialize, Serialize}; + +/// An equivalent value in terms of a different numeraire. +/// +/// This is used within +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] +#[serde(try_from = "pb::EquivalentValue", into = "pb::EquivalentValue")] +pub struct EquivalentValue { + /// The equivalent amount of the parent [`Value`] in terms of the numeraire. + pub equivalent_amount: Amount, + /// Metadata describing the numeraire. + pub numeraire: Metadata, + /// If nonzero, gives some idea of when the equivalent value was estimated (in terms of block height). + pub as_of_height: u64, +} + +impl DomainType for EquivalentValue { + type Proto = pb::EquivalentValue; +} + +impl From for pb::EquivalentValue { + fn from(v: EquivalentValue) -> Self { + pb::EquivalentValue { + equivalent_amount: Some(v.equivalent_amount.into()), + numeraire: Some(v.numeraire.into()), + as_of_height: v.as_of_height, + } + } +} + +impl TryFrom for EquivalentValue { + type Error = anyhow::Error; + fn try_from(value: pb::EquivalentValue) -> Result { + Ok(EquivalentValue { + equivalent_amount: value + .equivalent_amount + .ok_or_else(|| anyhow::anyhow!("missing equivalent_amount field"))? + .try_into()?, + numeraire: value + .numeraire + .ok_or_else(|| anyhow::anyhow!("missing numeraire field"))? + .try_into()?, + as_of_height: value.as_of_height, + }) + } +} diff --git a/crates/core/asset/src/estimated_price.rs b/crates/core/asset/src/estimated_price.rs new file mode 100644 index 0000000000..6f8df75a2f --- /dev/null +++ b/crates/core/asset/src/estimated_price.rs @@ -0,0 +1,57 @@ +use crate::asset; +use penumbra_proto::{penumbra::core::asset::v1 as pb, DomainType}; +use serde::{Deserialize, Serialize}; + +/// The estimated price of one asset in terms of another. +/// +/// This is used to generate an [`EquivalentValue`](crate::EquivalentValue) +/// that may be helpful in interpreting a [`Value`](crate::Value). +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +#[serde(try_from = "pb::EstimatedPrice", into = "pb::EstimatedPrice")] + +pub struct EstimatedPrice { + /// The asset that is being priced. + pub priced_asset: asset::Id, + /// The numeraire that the price is being expressed in. + pub numeraire: asset::Id, + /// Multiply units of the priced asset by this number to get the value in the numeraire. + /// + /// This is a floating-point number since the price is approximate. + pub numeraire_per_unit: f64, + /// If nonzero, gives some idea of when the price was estimated (in terms of block height). + pub as_of_height: u64, +} + +impl DomainType for EstimatedPrice { + type Proto = pb::EstimatedPrice; +} + +impl From for pb::EstimatedPrice { + fn from(msg: EstimatedPrice) -> Self { + Self { + priced_asset: Some(msg.priced_asset.into()), + numeraire: Some(msg.numeraire.into()), + numeraire_per_unit: msg.numeraire_per_unit, + as_of_height: msg.as_of_height, + } + } +} + +impl TryFrom for EstimatedPrice { + type Error = anyhow::Error; + + fn try_from(msg: pb::EstimatedPrice) -> Result { + Ok(Self { + priced_asset: msg + .priced_asset + .ok_or_else(|| anyhow::anyhow!("missing priced asset"))? + .try_into()?, + numeraire: msg + .numeraire + .ok_or_else(|| anyhow::anyhow!("missing numeraire"))? + .try_into()?, + numeraire_per_unit: msg.numeraire_per_unit, + as_of_height: msg.as_of_height, + }) + } +} diff --git a/crates/core/asset/src/lib.rs b/crates/core/asset/src/lib.rs index 3166ef175e..3b9e68c3ba 100644 --- a/crates/core/asset/src/lib.rs +++ b/crates/core/asset/src/lib.rs @@ -4,9 +4,13 @@ use once_cell::sync::Lazy; pub mod asset; pub mod balance; +mod equivalent_value; +mod estimated_price; mod value; pub use balance::Balance; +pub use equivalent_value::EquivalentValue; +pub use estimated_price::EstimatedPrice; pub use value::{Value, ValueVar, ValueView}; pub static STAKING_TOKEN_DENOM: Lazy = Lazy::new(|| { diff --git a/crates/core/asset/src/value.rs b/crates/core/asset/src/value.rs index c67a89e118..63de88a024 100644 --- a/crates/core/asset/src/value.rs +++ b/crates/core/asset/src/value.rs @@ -16,7 +16,11 @@ use penumbra_proto::{penumbra::core::asset::v1 as pb, DomainType}; use regex::Regex; use serde::{Deserialize, Serialize}; -use crate::asset::{AssetIdVar, Cache, Id, Metadata, REGISTRY}; +use crate::EquivalentValue; +use crate::{ + asset::{AssetIdVar, Cache, Id, Metadata, REGISTRY}, + EstimatedPrice, +}; #[derive(Deserialize, Serialize, Copy, Clone, Debug, PartialEq, Eq)] #[serde(try_from = "pb::Value", into = "pb::Value")] @@ -27,11 +31,19 @@ pub struct Value { } /// Represents a value of a known or unknown denomination. -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] #[serde(try_from = "pb::ValueView", into = "pb::ValueView")] pub enum ValueView { - KnownAssetId { amount: Amount, metadata: Metadata }, - UnknownAssetId { amount: Amount, asset_id: Id }, + KnownAssetId { + amount: Amount, + metadata: Metadata, + equivalent_values: Vec, + extended_metadata: Option, + }, + UnknownAssetId { + amount: Amount, + asset_id: Id, + }, } impl ValueView { @@ -44,6 +56,56 @@ impl ValueView { pub fn asset_id(&self) -> Id { self.value().asset_id } + + /// Use the provided [`EstimatedPrice`]s and asset metadata [`Cache`] to add + /// equivalent values to this [`ValueView`]. + pub fn with_prices(mut self, prices: &[EstimatedPrice], known_metadata: &Cache) -> Self { + if let ValueView::KnownAssetId { + ref mut equivalent_values, + metadata, + amount, + .. + } = &mut self + { + // Set the equivalent values. + *equivalent_values = prices + .iter() + .filter_map(|price| { + if metadata.id() == price.priced_asset + && known_metadata.contains_key(&price.numeraire) + { + let equivalent_amount_f = + (amount.value() as f64) * price.numeraire_per_unit; + Some(EquivalentValue { + equivalent_amount: Amount::from(equivalent_amount_f as u128), + numeraire: known_metadata + .get(&price.numeraire) + .expect("we checked containment above") + .clone(), + as_of_height: price.as_of_height, + }) + } else { + None + } + }) + .collect(); + } + + self + } + + /// Use the provided extended metadata to add extended metadata to this [`ValueView`]. + pub fn with_extended_metadata(mut self, extended: Option) -> Self { + if let ValueView::KnownAssetId { + ref mut extended_metadata, + .. + } = &mut self + { + *extended_metadata = extended; + } + + self + } } impl Value { @@ -53,6 +115,8 @@ impl Value { Ok(ValueView::KnownAssetId { amount: self.amount, metadata: denom, + equivalent_values: Vec::new(), + extended_metadata: None, }) } else { Err(anyhow::anyhow!( @@ -69,6 +133,8 @@ impl Value { Some(denom) => ValueView::KnownAssetId { amount: self.amount, metadata: denom.clone(), + equivalent_values: Vec::new(), + extended_metadata: None, }, None => ValueView::UnknownAssetId { amount: self.amount, @@ -84,6 +150,7 @@ impl From for Value { ValueView::KnownAssetId { amount, metadata: denom, + .. } => Value { amount, asset_id: Id::from(denom), @@ -131,15 +198,18 @@ impl TryFrom for Value { impl From for pb::ValueView { fn from(v: ValueView) -> Self { match v { - ValueView::KnownAssetId { amount, metadata } => pb::ValueView { + ValueView::KnownAssetId { + amount, + metadata, + equivalent_values, + extended_metadata, + } => pb::ValueView { value_view: Some(pb::value_view::ValueView::KnownAssetId( pb::value_view::KnownAssetId { amount: Some(amount.into()), metadata: Some(metadata.into()), - // These fields are currently not used by the Rust stack. - // Support for them may be added to the Rust view server in the future. - equivalent_values: Vec::new(), - extended_metadata: None, + equivalent_values: equivalent_values.into_iter().map(Into::into).collect(), + extended_metadata, }, )), }, @@ -171,6 +241,12 @@ impl TryFrom for ValueView { .metadata .ok_or_else(|| anyhow::anyhow!("missing denom field"))? .try_into()?, + equivalent_values: v + .equivalent_values + .into_iter() + .map(TryInto::try_into) + .collect::>()?, + extended_metadata: v.extended_metadata, }), pb::value_view::ValueView::UnknownAssetId(v) => Ok(ValueView::UnknownAssetId { amount: v diff --git a/crates/core/component/shielded-pool/src/note.rs b/crates/core/component/shielded-pool/src/note.rs index 0b9a62c06d..930cb089de 100644 --- a/crates/core/component/shielded-pool/src/note.rs +++ b/crates/core/component/shielded-pool/src/note.rs @@ -48,7 +48,7 @@ pub struct Note { transmission_key_s: Fq, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(into = "pb::NoteView", try_from = "pb::NoteView")] pub struct NoteView { pub value: ValueView, diff --git a/crates/core/transaction/src/view/transaction_perspective.rs b/crates/core/transaction/src/view/transaction_perspective.rs index cac566e6af..09ce3d4ba2 100644 --- a/crates/core/transaction/src/view/transaction_perspective.rs +++ b/crates/core/transaction/src/view/transaction_perspective.rs @@ -1,5 +1,6 @@ use anyhow::anyhow; -use penumbra_asset::asset; +use pbjson_types::Any; +use penumbra_asset::{asset, EstimatedPrice, Value, ValueView}; use penumbra_keys::{Address, AddressView, PayloadKey}; use penumbra_proto::core::transaction::v1::{ self as pb, NullifierWithNote, PayloadKeyWithCommitment, @@ -13,7 +14,6 @@ use std::collections::BTreeMap; /// This represents the data to understand an individual transaction without /// disclosing viewing keys. #[derive(Debug, Clone, Default)] - pub struct TransactionPerspective { /// List of per-action payload keys. These can be used to decrypt /// the notes, swaps, and memo keys in the transaction. @@ -40,28 +40,24 @@ pub struct TransactionPerspective { pub denoms: asset::Cache, /// The transaction ID associated with this TransactionPerspective pub transaction_id: TransactionId, + /// Any relevant estimated prices. + pub prices: Vec, + /// Any relevant extended metadata. + pub extended_metadata: BTreeMap, } impl TransactionPerspective { - pub fn view_note(&self, note: Note) -> NoteView { - let note_address = note.address(); - - let address = match self - .address_views - .iter() - .find(|av| av.address() == note_address) - { - Some(av) => av.clone(), - None => AddressView::Opaque { - address: note_address, - }, - }; - - let value = note.value().view_with_cache(&self.denoms); + pub fn view_value(&self, value: Value) -> ValueView { + value + .view_with_cache(&self.denoms) + .with_prices(&self.prices, &self.denoms) + .with_extended_metadata(self.extended_metadata.get(&value.asset_id).cloned()) + } + pub fn view_note(&self, note: Note) -> NoteView { NoteView { - address, - value, + address: self.view_address(note.address()), + value: self.view_value(note.value()), rseed: note.rseed(), } } @@ -114,6 +110,15 @@ impl From for pb::TransactionPerspective { address_views, denoms, transaction_id: Some(msg.transaction_id.into()), + prices: msg.prices.into_iter().map(Into::into).collect(), + extended_metadata: msg + .extended_metadata + .into_iter() + .map(|(k, v)| pb::transaction_perspective::ExtendedMetadataById { + asset_id: Some(k.into()), + extended_metadata: Some(v), + }) + .collect(), } } } @@ -184,6 +189,24 @@ impl TryFrom for TransactionPerspective { address_views, denoms: denoms.try_into()?, transaction_id, + prices: msg + .prices + .into_iter() + .map(TryInto::try_into) + .collect::>()?, + extended_metadata: msg + .extended_metadata + .into_iter() + .map(|em| { + Ok(( + em.asset_id + .ok_or_else(|| anyhow!("missing asset ID in extended metadata"))? + .try_into()?, + em.extended_metadata + .ok_or_else(|| anyhow!("missing extended metadata"))?, + )) + }) + .collect::>()?, }) } } diff --git a/crates/proto/src/gen/penumbra.core.asset.v1.rs b/crates/proto/src/gen/penumbra.core.asset.v1.rs index c59d8dc667..01187b7973 100644 --- a/crates/proto/src/gen/penumbra.core.asset.v1.rs +++ b/crates/proto/src/gen/penumbra.core.asset.v1.rs @@ -152,13 +152,8 @@ pub mod value_view { #[prost(message, optional, tag = "2")] pub metadata: ::core::option::Option, /// Optionally, a list of equivalent values in other numeraires. - /// - /// For instance, this can provide a USD-equivalent value relative to a - /// stablecoin, or an amount of the staking token, etc. A view server can - /// optionally include this information to assist a frontend in displaying - /// information about the value in a user-friendly way. #[prost(message, repeated, tag = "3")] - pub equivalent_values: ::prost::alloc::vec::Vec, + pub equivalent_values: ::prost::alloc::vec::Vec, /// Optionally, extended, dynamically-typed metadata about the object this /// token represents. /// @@ -171,30 +166,6 @@ pub mod value_view { #[prost(message, optional, tag = "4")] pub extended_metadata: ::core::option::Option<::pbjson_types::Any>, } - /// Nested message and enum types in `KnownAssetId`. - pub mod known_asset_id { - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Message)] - pub struct EquivalentValue { - /// The equivalent amount of the parent Value in terms of the numeraire. - #[prost(message, optional, tag = "1")] - pub equivalent_amount: ::core::option::Option< - super::super::super::super::num::v1::Amount, - >, - /// Metadata describing the numeraire. - #[prost(message, optional, tag = "2")] - pub numeraire: ::core::option::Option, - } - impl ::prost::Name for EquivalentValue { - const NAME: &'static str = "EquivalentValue"; - const PACKAGE: &'static str = "penumbra.core.asset.v1"; - fn full_name() -> ::prost::alloc::string::String { - ::prost::alloc::format!( - "penumbra.core.asset.v1.ValueView.KnownAssetId.{}", Self::NAME - ) - } - } - } impl ::prost::Name for KnownAssetId { const NAME: &'static str = "KnownAssetId"; const PACKAGE: &'static str = "penumbra.core.asset.v1"; @@ -275,3 +246,55 @@ impl ::prost::Name for AssetImage { ::prost::alloc::format!("penumbra.core.asset.v1.{}", Self::NAME) } } +/// The estimated price of one asset in terms of a numeraire. +/// +/// This is used for generating "equivalent values" in ValueViews. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EstimatedPrice { + #[prost(message, optional, tag = "1")] + pub priced_asset: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub numeraire: ::core::option::Option, + /// Multiply units of the priced asset by this value to get the value in the numeraire. + /// + /// This is a floating-point number since the price is approximate. + #[prost(double, tag = "3")] + pub numeraire_per_unit: f64, + /// If set, gives some idea of when the price was estimated. + #[prost(uint64, tag = "4")] + pub as_of_height: u64, +} +impl ::prost::Name for EstimatedPrice { + const NAME: &'static str = "EstimatedPrice"; + const PACKAGE: &'static str = "penumbra.core.asset.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.core.asset.v1.{}", Self::NAME) + } +} +/// An "equivalent" value to a given value, in terms of a numeraire. +/// +/// For instance, this can provide a USD-equivalent value relative to a +/// stablecoin, or an amount of the staking token, etc. A view server can +/// optionally include this information to assist a frontend in displaying +/// information about the value in a user-friendly way. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EquivalentValue { + /// The equivalent amount of the parent Value in terms of the numeraire. + #[prost(message, optional, tag = "1")] + pub equivalent_amount: ::core::option::Option, + /// Metadata describing the numeraire. + #[prost(message, optional, tag = "2")] + pub numeraire: ::core::option::Option, + /// If set, gives some idea of when the price/equivalence was estimated. + #[prost(uint64, tag = "3")] + pub as_of_height: u64, +} +impl ::prost::Name for EquivalentValue { + const NAME: &'static str = "EquivalentValue"; + const PACKAGE: &'static str = "penumbra.core.asset.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.core.asset.v1.{}", Self::NAME) + } +} diff --git a/crates/proto/src/gen/penumbra.core.asset.v1.serde.rs b/crates/proto/src/gen/penumbra.core.asset.v1.serde.rs index 8d17937e87..9992e2e4f7 100644 --- a/crates/proto/src/gen/penumbra.core.asset.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.asset.v1.serde.rs @@ -716,6 +716,294 @@ impl<'de> serde::Deserialize<'de> for DenomUnit { deserializer.deserialize_struct("penumbra.core.asset.v1.DenomUnit", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for EquivalentValue { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.equivalent_amount.is_some() { + len += 1; + } + if self.numeraire.is_some() { + len += 1; + } + if self.as_of_height != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.asset.v1.EquivalentValue", len)?; + if let Some(v) = self.equivalent_amount.as_ref() { + struct_ser.serialize_field("equivalentAmount", v)?; + } + if let Some(v) = self.numeraire.as_ref() { + struct_ser.serialize_field("numeraire", v)?; + } + if self.as_of_height != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("asOfHeight", ToString::to_string(&self.as_of_height).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EquivalentValue { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "equivalent_amount", + "equivalentAmount", + "numeraire", + "as_of_height", + "asOfHeight", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + EquivalentAmount, + Numeraire, + AsOfHeight, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "equivalentAmount" | "equivalent_amount" => Ok(GeneratedField::EquivalentAmount), + "numeraire" => Ok(GeneratedField::Numeraire), + "asOfHeight" | "as_of_height" => Ok(GeneratedField::AsOfHeight), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EquivalentValue; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.asset.v1.EquivalentValue") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut equivalent_amount__ = None; + let mut numeraire__ = None; + let mut as_of_height__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::EquivalentAmount => { + if equivalent_amount__.is_some() { + return Err(serde::de::Error::duplicate_field("equivalentAmount")); + } + equivalent_amount__ = map_.next_value()?; + } + GeneratedField::Numeraire => { + if numeraire__.is_some() { + return Err(serde::de::Error::duplicate_field("numeraire")); + } + numeraire__ = map_.next_value()?; + } + GeneratedField::AsOfHeight => { + if as_of_height__.is_some() { + return Err(serde::de::Error::duplicate_field("asOfHeight")); + } + as_of_height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EquivalentValue { + equivalent_amount: equivalent_amount__, + numeraire: numeraire__, + as_of_height: as_of_height__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.core.asset.v1.EquivalentValue", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for EstimatedPrice { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.priced_asset.is_some() { + len += 1; + } + if self.numeraire.is_some() { + len += 1; + } + if self.numeraire_per_unit != 0. { + len += 1; + } + if self.as_of_height != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.asset.v1.EstimatedPrice", len)?; + if let Some(v) = self.priced_asset.as_ref() { + struct_ser.serialize_field("pricedAsset", v)?; + } + if let Some(v) = self.numeraire.as_ref() { + struct_ser.serialize_field("numeraire", v)?; + } + if self.numeraire_per_unit != 0. { + struct_ser.serialize_field("numerairePerUnit", &self.numeraire_per_unit)?; + } + if self.as_of_height != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("asOfHeight", ToString::to_string(&self.as_of_height).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EstimatedPrice { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "priced_asset", + "pricedAsset", + "numeraire", + "numeraire_per_unit", + "numerairePerUnit", + "as_of_height", + "asOfHeight", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + PricedAsset, + Numeraire, + NumerairePerUnit, + AsOfHeight, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "pricedAsset" | "priced_asset" => Ok(GeneratedField::PricedAsset), + "numeraire" => Ok(GeneratedField::Numeraire), + "numerairePerUnit" | "numeraire_per_unit" => Ok(GeneratedField::NumerairePerUnit), + "asOfHeight" | "as_of_height" => Ok(GeneratedField::AsOfHeight), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EstimatedPrice; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.asset.v1.EstimatedPrice") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut priced_asset__ = None; + let mut numeraire__ = None; + let mut numeraire_per_unit__ = None; + let mut as_of_height__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::PricedAsset => { + if priced_asset__.is_some() { + return Err(serde::de::Error::duplicate_field("pricedAsset")); + } + priced_asset__ = map_.next_value()?; + } + GeneratedField::Numeraire => { + if numeraire__.is_some() { + return Err(serde::de::Error::duplicate_field("numeraire")); + } + numeraire__ = map_.next_value()?; + } + GeneratedField::NumerairePerUnit => { + if numeraire_per_unit__.is_some() { + return Err(serde::de::Error::duplicate_field("numerairePerUnit")); + } + numeraire_per_unit__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::AsOfHeight => { + if as_of_height__.is_some() { + return Err(serde::de::Error::duplicate_field("asOfHeight")); + } + as_of_height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EstimatedPrice { + priced_asset: priced_asset__, + numeraire: numeraire__, + numeraire_per_unit: numeraire_per_unit__.unwrap_or_default(), + as_of_height: as_of_height__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.core.asset.v1.EstimatedPrice", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for Metadata { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -1308,119 +1596,6 @@ impl<'de> serde::Deserialize<'de> for value_view::KnownAssetId { deserializer.deserialize_struct("penumbra.core.asset.v1.ValueView.KnownAssetId", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for value_view::known_asset_id::EquivalentValue { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut len = 0; - if self.equivalent_amount.is_some() { - len += 1; - } - if self.numeraire.is_some() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("penumbra.core.asset.v1.ValueView.KnownAssetId.EquivalentValue", len)?; - if let Some(v) = self.equivalent_amount.as_ref() { - struct_ser.serialize_field("equivalentAmount", v)?; - } - if let Some(v) = self.numeraire.as_ref() { - struct_ser.serialize_field("numeraire", v)?; - } - struct_ser.end() - } -} -impl<'de> serde::Deserialize<'de> for value_view::known_asset_id::EquivalentValue { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "equivalent_amount", - "equivalentAmount", - "numeraire", - ]; - - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - EquivalentAmount, - Numeraire, - __SkipField__, - } - impl<'de> serde::Deserialize<'de> for GeneratedField { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - struct GeneratedVisitor; - - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = GeneratedField; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "expected one of: {:?}", &FIELDS) - } - - #[allow(unused_variables)] - fn visit_str(self, value: &str) -> std::result::Result - where - E: serde::de::Error, - { - match value { - "equivalentAmount" | "equivalent_amount" => Ok(GeneratedField::EquivalentAmount), - "numeraire" => Ok(GeneratedField::Numeraire), - _ => Ok(GeneratedField::__SkipField__), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) - } - } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = value_view::known_asset_id::EquivalentValue; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct penumbra.core.asset.v1.ValueView.KnownAssetId.EquivalentValue") - } - - fn visit_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, - { - let mut equivalent_amount__ = None; - let mut numeraire__ = None; - while let Some(k) = map_.next_key()? { - match k { - GeneratedField::EquivalentAmount => { - if equivalent_amount__.is_some() { - return Err(serde::de::Error::duplicate_field("equivalentAmount")); - } - equivalent_amount__ = map_.next_value()?; - } - GeneratedField::Numeraire => { - if numeraire__.is_some() { - return Err(serde::de::Error::duplicate_field("numeraire")); - } - numeraire__ = map_.next_value()?; - } - GeneratedField::__SkipField__ => { - let _ = map_.next_value::()?; - } - } - } - Ok(value_view::known_asset_id::EquivalentValue { - equivalent_amount: equivalent_amount__, - numeraire: numeraire__, - }) - } - } - deserializer.deserialize_struct("penumbra.core.asset.v1.ValueView.KnownAssetId.EquivalentValue", FIELDS, GeneratedVisitor) - } -} impl serde::Serialize for value_view::UnknownAssetId { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto/src/gen/penumbra.core.transaction.v1.rs b/crates/proto/src/gen/penumbra.core.transaction.v1.rs index ee7a7bcac2..be25c71d2d 100644 --- a/crates/proto/src/gen/penumbra.core.transaction.v1.rs +++ b/crates/proto/src/gen/penumbra.core.transaction.v1.rs @@ -199,6 +199,34 @@ pub struct TransactionPerspective { /// The transaction ID associated with this TransactionPerspective #[prost(message, optional, tag = "6")] pub transaction_id: ::core::option::Option, + /// Any relevant estimated prices + #[prost(message, repeated, tag = "20")] + pub prices: ::prost::alloc::vec::Vec, + /// Any relevant extended metadata, indexed by asset id. + #[prost(message, repeated, tag = "30")] + pub extended_metadata: ::prost::alloc::vec::Vec< + transaction_perspective::ExtendedMetadataById, + >, +} +/// Nested message and enum types in `TransactionPerspective`. +pub mod transaction_perspective { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct ExtendedMetadataById { + #[prost(message, optional, tag = "1")] + pub asset_id: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub extended_metadata: ::core::option::Option<::pbjson_types::Any>, + } + impl ::prost::Name for ExtendedMetadataById { + const NAME: &'static str = "ExtendedMetadataById"; + const PACKAGE: &'static str = "penumbra.core.transaction.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.transaction.v1.TransactionPerspective.{}", Self::NAME + ) + } + } } impl ::prost::Name for TransactionPerspective { const NAME: &'static str = "TransactionPerspective"; diff --git a/crates/proto/src/gen/penumbra.core.transaction.v1.serde.rs b/crates/proto/src/gen/penumbra.core.transaction.v1.serde.rs index b4a39471d1..0840d7e541 100644 --- a/crates/proto/src/gen/penumbra.core.transaction.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.transaction.v1.serde.rs @@ -3200,6 +3200,12 @@ impl serde::Serialize for TransactionPerspective { if self.transaction_id.is_some() { len += 1; } + if !self.prices.is_empty() { + len += 1; + } + if !self.extended_metadata.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.core.transaction.v1.TransactionPerspective", len)?; if !self.payload_keys.is_empty() { struct_ser.serialize_field("payloadKeys", &self.payload_keys)?; @@ -3219,6 +3225,12 @@ impl serde::Serialize for TransactionPerspective { if let Some(v) = self.transaction_id.as_ref() { struct_ser.serialize_field("transactionId", v)?; } + if !self.prices.is_empty() { + struct_ser.serialize_field("prices", &self.prices)?; + } + if !self.extended_metadata.is_empty() { + struct_ser.serialize_field("extendedMetadata", &self.extended_metadata)?; + } struct_ser.end() } } @@ -3240,6 +3252,9 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { "denoms", "transaction_id", "transactionId", + "prices", + "extended_metadata", + "extendedMetadata", ]; #[allow(clippy::enum_variant_names)] @@ -3250,6 +3265,8 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { AddressViews, Denoms, TransactionId, + Prices, + ExtendedMetadata, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -3278,6 +3295,8 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { "addressViews" | "address_views" => Ok(GeneratedField::AddressViews), "denoms" => Ok(GeneratedField::Denoms), "transactionId" | "transaction_id" => Ok(GeneratedField::TransactionId), + "prices" => Ok(GeneratedField::Prices), + "extendedMetadata" | "extended_metadata" => Ok(GeneratedField::ExtendedMetadata), _ => Ok(GeneratedField::__SkipField__), } } @@ -3303,6 +3322,8 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { let mut address_views__ = None; let mut denoms__ = None; let mut transaction_id__ = None; + let mut prices__ = None; + let mut extended_metadata__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::PayloadKeys => { @@ -3341,6 +3362,18 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { } transaction_id__ = map_.next_value()?; } + GeneratedField::Prices => { + if prices__.is_some() { + return Err(serde::de::Error::duplicate_field("prices")); + } + prices__ = Some(map_.next_value()?); + } + GeneratedField::ExtendedMetadata => { + if extended_metadata__.is_some() { + return Err(serde::de::Error::duplicate_field("extendedMetadata")); + } + extended_metadata__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -3353,12 +3386,128 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { address_views: address_views__.unwrap_or_default(), denoms: denoms__.unwrap_or_default(), transaction_id: transaction_id__, + prices: prices__.unwrap_or_default(), + extended_metadata: extended_metadata__.unwrap_or_default(), }) } } deserializer.deserialize_struct("penumbra.core.transaction.v1.TransactionPerspective", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for transaction_perspective::ExtendedMetadataById { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.asset_id.is_some() { + len += 1; + } + if self.extended_metadata.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.transaction.v1.TransactionPerspective.ExtendedMetadataById", len)?; + if let Some(v) = self.asset_id.as_ref() { + struct_ser.serialize_field("assetId", v)?; + } + if let Some(v) = self.extended_metadata.as_ref() { + struct_ser.serialize_field("extendedMetadata", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for transaction_perspective::ExtendedMetadataById { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "asset_id", + "assetId", + "extended_metadata", + "extendedMetadata", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + AssetId, + ExtendedMetadata, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "assetId" | "asset_id" => Ok(GeneratedField::AssetId), + "extendedMetadata" | "extended_metadata" => Ok(GeneratedField::ExtendedMetadata), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = transaction_perspective::ExtendedMetadataById; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.transaction.v1.TransactionPerspective.ExtendedMetadataById") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut asset_id__ = None; + let mut extended_metadata__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::AssetId => { + if asset_id__.is_some() { + return Err(serde::de::Error::duplicate_field("assetId")); + } + asset_id__ = map_.next_value()?; + } + GeneratedField::ExtendedMetadata => { + if extended_metadata__.is_some() { + return Err(serde::de::Error::duplicate_field("extendedMetadata")); + } + extended_metadata__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(transaction_perspective::ExtendedMetadataById { + asset_id: asset_id__, + extended_metadata: extended_metadata__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.transaction.v1.TransactionPerspective.ExtendedMetadataById", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for TransactionPlan { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index e135502b32e909f794108b12aa6a5f54e16749a8..f0c25300d2474054e85eedfc8297a2d4c7a6efe6 100644 GIT binary patch delta 25451 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%0Ar4HhKHt93TQ$0#PpJ!* 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