penumbra_sdk_asset/asset/
id.rs1use crate::Value;
2use ark_ff::ToConstraintField;
3use ark_serialize::CanonicalDeserialize;
4use base64::Engine;
5use decaf377::Fq;
6use once_cell::sync::Lazy;
7use penumbra_sdk_num::Amount;
8use penumbra_sdk_proto::{penumbra::core::asset::v1 as pb, serializers::bech32str, DomainType};
9use serde::{Deserialize, Serialize};
10
11#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
30#[serde(try_from = "pb::AssetId", into = "pb::AssetId")]
31pub struct Id(pub Fq);
32
33impl From<Id> for pb::AssetId {
34 fn from(id: Id) -> Self {
35 pb::AssetId {
36 inner: id.0.to_bytes().to_vec(),
37 alt_bech32m: String::new(),
39 alt_base_denom: String::new(),
41 }
42 }
43}
44
45impl TryFrom<pb::AssetId> for Id {
46 type Error = anyhow::Error;
47 fn try_from(value: pb::AssetId) -> Result<Self, Self::Error> {
48 if !value.inner.is_empty() {
49 if !value.alt_base_denom.is_empty() || !value.alt_bech32m.is_empty() {
50 anyhow::bail!(
51 "AssetId proto has both inner and alt_bech32m or alt_base_denom fields set"
52 );
53 }
54 value.inner.as_slice().try_into()
55 } else if !value.alt_bech32m.is_empty() {
56 value.alt_bech32m.parse()
57 } else if !value.alt_base_denom.is_empty() {
58 Ok(Self::from_raw_denom(&value.alt_base_denom))
59 } else {
60 Err(anyhow::anyhow!(
61 "AssetId proto has neither inner nor alt_bech32m nor alt_base_denom fields set"
62 ))
63 }
64 }
65}
66
67impl DomainType for Id {
68 type Proto = pb::AssetId;
69}
70
71impl TryFrom<&[u8]> for Id {
72 type Error = anyhow::Error;
73
74 fn try_from(slice: &[u8]) -> Result<Id, Self::Error> {
75 Ok(Id(Fq::deserialize_compressed(slice)?))
76 }
77}
78
79impl TryFrom<[u8; 32]> for Id {
80 type Error = anyhow::Error;
81
82 fn try_from(bytes: [u8; 32]) -> Result<Id, Self::Error> {
83 Ok(Id(Fq::from_bytes_checked(&bytes).expect("convert to bytes")))
84 }
85}
86
87impl std::fmt::Debug for Id {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.write_str(&bech32str::encode(
90 &self.0.to_bytes(),
91 bech32str::asset_id::BECH32_PREFIX,
92 bech32str::Bech32m,
93 ))
94 }
95}
96
97impl std::fmt::Display for Id {
98 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
100 f.write_str(&bech32str::encode(
101 &self.0.to_bytes(),
102 bech32str::asset_id::BECH32_PREFIX,
103 bech32str::Bech32m,
104 ))
105 }
106}
107
108impl std::str::FromStr for Id {
109 type Err = anyhow::Error;
110
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
112 let inner = bech32str::decode(s, bech32str::asset_id::BECH32_PREFIX, bech32str::Bech32m)?;
113 pb::AssetId {
114 inner,
115 alt_bech32m: String::new(),
116 alt_base_denom: String::new(),
117 }
118 .try_into()
119 }
120}
121
122impl ToConstraintField<Fq> for Id {
123 fn to_field_elements(&self) -> Option<Vec<Fq>> {
124 let mut elements = Vec::new();
125 elements.extend_from_slice(&[self.0]);
126 Some(elements)
127 }
128}
129
130pub static VALUE_GENERATOR_DOMAIN_SEP: Lazy<Fq> = Lazy::new(|| {
132 Fq::from_le_bytes_mod_order(blake2b_simd::blake2b(b"penumbra.value.generator").as_bytes())
133});
134
135impl Id {
136 pub fn value_generator(&self) -> decaf377::Element {
138 decaf377::Element::encode_to_curve(&poseidon377::hash_1(
139 &VALUE_GENERATOR_DOMAIN_SEP,
140 self.0,
141 ))
142 }
143
144 pub fn to_bytes(&self) -> [u8; 32] {
146 self.0.to_bytes()
147 }
148
149 pub fn value(&self, amount: Amount) -> Value {
151 Value {
152 amount,
153 asset_id: *self,
154 }
155 }
156
157 pub(super) fn from_raw_denom(base_denom: &str) -> Self {
158 Id(Fq::from_le_bytes_mod_order(
159 blake2b_simd::Params::default()
161 .personal(b"Penumbra_AssetID")
162 .hash(base_denom.as_bytes())
163 .as_bytes(),
164 ))
165 }
166
167 pub fn to_base64(&self) -> String {
169 base64::engine::general_purpose::STANDARD.encode(self.to_bytes())
170 }
171}
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use hex;
176 use serde_json;
177 use std::str::FromStr;
178
179 #[test]
180 fn asset_id_encoding() {
181 let id = Id::from_raw_denom("upenumbra");
182
183 let bech32m_id = format!("{id}");
184
185 let id2 = Id::from_str(&bech32m_id).expect("can decode valid asset id");
186
187 use penumbra_sdk_proto::Message;
188
189 let proto = id.encode_to_vec();
190 let proto2 = pb::AssetId {
191 alt_bech32m: bech32m_id,
192 ..Default::default()
193 }
194 .encode_to_vec();
195 let proto3 = pb::AssetId {
196 alt_base_denom: "upenumbra".to_owned(),
197 ..Default::default()
198 }
199 .encode_to_vec();
200
201 let id3 = Id::decode(proto.as_ref()).expect("can decode valid asset id");
202 let id4 = Id::decode(proto2.as_ref()).expect("can decode valid asset id");
203 let id5 = Id::decode(proto3.as_ref()).expect("can decode valid asset id");
204
205 assert_eq!(id2, id);
206 assert_eq!(id3, id);
207 assert_eq!(id4, id);
208 assert_eq!(id5, id);
209 }
210
211 #[test]
212 fn hex_to_bech32() {
213 let hex_strings = [
214 "cc0d3c9eef0c7ff4e225eca85a3094603691d289aeaf428ab0d87319ad93a302", "a7a339f42e671b2db1de226d4483d3e63036661cad1554d75f5f76fe04ec1e00", "29ea9c2f3371f6a487e7e95c247041f4a356f983eb064e5d2b3bcf322ca96a10", "76b3e4b10681358c123b381f90638476b7789040e47802de879f0fb3eedc8d0b", "2923a0a87b3a2421f165cc853dbf73a9bdafb5da0d948564b6059cb0217c4407", "07ef660132a4c3235fab272d43d9b9752a8337b2d108597abffaff5f246d0f0f", "5314b33eecfd5ca2e99c0b6d1e0ccafe3d2dd581c952d814fb64fdf51f85c411", "516108d0d0bba3f76e1f982d0a7cde118833307b03c0cd4ccb94e882b53c1f0f", "414e723f74bd987c02ccbc997585ed52b196e2ffe75b3793aa68cc2996626910", "bf8b035dda339b6cda8f221e79773b0fd871f27a472920f84c4aa2b4f98a700d", ];
225
226 for hex in hex_strings {
227 let bytes = hex::decode(hex).expect("valid hex string");
228 let bytes_array: [u8; 32] = bytes.try_into().expect("hex is 32 bytes");
229
230 let id = Id::try_from(bytes_array).expect("valid asset ID bytes");
231 let bech32_str = id.to_string();
232
233 println!("Asset ID for {}:", hex);
234 println!(" Bech32: {}", bech32_str);
235
236 let proto: pb::AssetId = id.into();
238 println!(" Proto JSON: {}\n", serde_json::to_string(&proto).unwrap());
239
240 let id_decoded = Id::from_str(&bech32_str).expect("valid bech32 string");
242 assert_eq!(id, id_decoded);
243 }
244 }
245}