1use crate::{Hash, cbor, pallas, pretty};
6use anyhow::anyhow;
7use num::{CheckedSub, Num, Zero};
8use std::{
9 collections::{BTreeMap, btree_map},
10 fmt,
11 fmt::Display,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Value<Quantity>(u64, BTreeMap<Hash<28>, BTreeMap<Vec<u8>, Quantity>>);
20
21impl<Quantity: fmt::Debug + Copy> fmt::Display for Value<Quantity> {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 let mut debug_struct = f.debug_struct("Value");
24
25 debug_struct.field("lovelace", &self.0);
26
27 if !self.assets().is_empty() {
28 debug_struct.field(
29 "assets",
30 &pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
31 let mut outer = f.debug_map();
32 for (script_hash, assets) in &self.1 {
33 outer.entry(
34 &pretty::ViaDisplayNoAlloc(script_hash),
35 &pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
36 let mut inner = f.debug_map();
37 for (name, qty) in assets {
38 if let Ok(utf8) = str::from_utf8(name.as_slice()) {
39 inner.entry(&pretty::ViaDisplayNoAlloc(utf8), qty);
40 } else {
41 inner.entry(
42 &pretty::ViaDisplayNoAlloc(&hex::encode(
43 name.as_slice(),
44 )),
45 qty,
46 );
47 }
48 }
49 inner.finish()
50 }),
51 );
52 }
53 outer.finish()
54 }),
55 );
56 }
57
58 debug_struct.finish()
59 }
60}
61
62impl<Quantity> Default for Value<Quantity> {
65 fn default() -> Self {
66 Self::new(0)
67 }
68}
69
70impl<Quantity> Value<Quantity> {
71 pub fn new(lovelace: u64) -> Self {
83 Self(lovelace, BTreeMap::default())
84 }
85
86 pub fn with_lovelace(&mut self, lovelace: u64) -> &mut Self {
96 self.0 = lovelace;
97 self
98 }
99}
100
101impl<Quantity: Zero> Value<Quantity> {
102 pub fn with_assets<AssetName>(
127 mut self,
128 assets: impl IntoIterator<Item = (Hash<28>, impl IntoIterator<Item = (AssetName, Quantity)>)>,
129 ) -> Self
130 where
131 AssetName: AsRef<[u8]>,
132 {
133 with_assets(&mut self, assets);
134 self
135 }
136}
137
138impl<Quantity: Num + CheckedSub + Copy + Display> Value<Quantity> {
139 pub fn add(&mut self, rhs: &Self) -> &mut Self {
142 self.0 += rhs.0;
143
144 for (script_hash, assets) in &rhs.1 {
145 self.1
146 .entry(*script_hash)
147 .and_modify(|lhs| {
148 for (asset_name, quantity) in assets {
149 lhs.entry(asset_name.clone())
150 .and_modify(|q| *q = q.add(*quantity))
151 .or_insert(*quantity);
152 }
153 })
154 .or_insert(assets.clone());
155 }
156
157 prune_null_values(&mut self.1);
158
159 self
160 }
161
162 pub fn checked_sub(&mut self, rhs: &Self) -> anyhow::Result<&mut Self> {
226 self.0 = self.0.checked_sub(rhs.0).ok_or_else(|| {
227 anyhow!("insufficient lhs lovelace")
228 .context(format!("lhs = {}, rhs = {}", self.0, rhs.0))
229 })?;
230
231 for (script_hash, assets) in &rhs.1 {
232 match self.1.entry(*script_hash) {
233 btree_map::Entry::Vacant(_) => {
234 return Err(anyhow!("script_hash={}", script_hash)
235 .context("insufficient lhs asset: unknown asset script_hash"));
236 }
237 btree_map::Entry::Occupied(mut lhs) => {
238 for (asset_name, quantity) in assets {
239 match lhs.get_mut().entry(asset_name.clone()) {
240 btree_map::Entry::Vacant(_) => {
241 return Err(anyhow!(
242 "script hash={}, asset name={}",
243 script_hash,
244 display_asset_name(asset_name),
245 )
246 .context("insufficient lhs asset: unknown asset"));
247 }
248 btree_map::Entry::Occupied(mut q) => {
249 *q.get_mut() = q.get().checked_sub(quantity).ok_or_else(|| {
250 anyhow!(
251 "script hash={}, asset name={}",
252 script_hash,
253 display_asset_name(asset_name),
254 )
255 .context(format!(
256 "lhs quantity={}, rhs quantity={}",
257 q.get(),
258 quantity,
259 ))
260 .context("insufficient lhs asset: insufficient quantity")
261 })?;
262 }
263 }
264 }
265 }
266 }
267 }
268
269 prune_null_values(&mut self.1);
270
271 Ok(self)
272 }
273}
274
275impl<Quantity> Value<Quantity> {
278 pub fn lovelace(&self) -> u64 {
279 self.0
280 }
281
282 pub fn assets(&self) -> &BTreeMap<Hash<28>, BTreeMap<Vec<u8>, Quantity>> {
283 &self.1
284 }
285
286 pub fn is_empty(&self) -> bool {
287 self.lovelace() == 0 && self.assets().is_empty()
288 }
289}
290
291impl From<&pallas::alonzo::Value> for Value<u64> {
294 fn from(value: &pallas::alonzo::Value) -> Self {
295 match value {
296 pallas_primitives::alonzo::Value::Coin(lovelace) => {
297 Self(*lovelace, BTreeMap::default())
298 }
299 pallas_primitives::alonzo::Value::Multiasset(lovelace, assets) => Self(
300 *lovelace,
301 assets
302 .iter()
303 .map(|(script_hash, inner)| {
304 (
305 Hash::from(script_hash),
306 inner
307 .iter()
308 .map(|(asset_name, quantity)| (asset_name.to_vec(), *quantity))
309 .collect(),
310 )
311 })
312 .collect(),
313 ),
314 }
315 }
316}
317
318impl From<&pallas::Value> for Value<u64> {
319 fn from(value: &pallas::Value) -> Self {
320 match value {
321 pallas_primitives::conway::Value::Coin(lovelace) => {
322 Self(*lovelace, BTreeMap::default())
323 }
324 pallas_primitives::conway::Value::Multiasset(lovelace, assets) => {
325 Self(*lovelace, from_multiasset(assets, |q| u64::from(q)))
326 }
327 }
328 }
329}
330
331impl From<&pallas::Multiasset<pallas::NonZeroInt>> for Value<i64> {
332 fn from(assets: &pallas::Multiasset<pallas::NonZeroInt>) -> Self {
333 Self(0, from_multiasset(assets, |q| i64::from(q)))
334 }
335}
336
337fn from_multiasset<Quantity: Copy, PositiveCoin: Copy>(
338 assets: &pallas::Multiasset<PositiveCoin>,
339 from_quantity: impl Fn(&PositiveCoin) -> Quantity,
340) -> BTreeMap<Hash<28>, BTreeMap<Vec<u8>, Quantity>> {
341 assets
342 .iter()
343 .map(|(script_hash, inner)| {
344 (
345 Hash::from(script_hash),
346 inner
347 .iter()
348 .map(|(asset_name, quantity)| (asset_name.to_vec(), from_quantity(quantity)))
349 .collect(),
350 )
351 })
352 .collect()
353}
354
355impl From<&Value<u64>> for pallas::Value {
358 fn from(Value(lovelace, assets): &Value<u64>) -> Self {
359 into_multiasset(assets, |quantity: &u64| {
360 pallas::PositiveCoin::try_from(*quantity).ok()
361 })
362 .map(|assets| pallas::Value::Multiasset(*lovelace, assets))
363 .unwrap_or_else(|| pallas::Value::Coin(*lovelace))
364 }
365}
366
367impl From<&Value<i64>> for Option<pallas::Multiasset<pallas::NonZeroInt>> {
368 fn from(value @ Value(lovelace, assets): &Value<i64>) -> Self {
369 debug_assert!(
370 *lovelace == 0,
371 "somehow found a mint value with a non-zero Ada quantity: {value:#?}"
372 );
373 into_multiasset(assets, |quantity: &i64| {
374 pallas::NonZeroInt::try_from(*quantity).ok()
375 })
376 }
377}
378
379fn into_multiasset<Quantity: Copy, PositiveCoin: Copy>(
382 assets: &BTreeMap<Hash<28>, BTreeMap<Vec<u8>, Quantity>>,
383 from_quantity: impl Fn(&Quantity) -> Option<PositiveCoin>,
384) -> Option<pallas::Multiasset<PositiveCoin>> {
385 pallas::NonEmptyKeyValuePairs::from_vec(
386 assets
387 .iter()
388 .filter_map(|(script_hash, inner)| {
389 pallas::NonEmptyKeyValuePairs::from_vec(
390 inner
391 .iter()
392 .filter_map(|(asset_name, quantity)| {
393 from_quantity(quantity)
394 .map(|quantity| (pallas::Bytes::from(asset_name.clone()), quantity))
395 })
396 .collect::<Vec<_>>(),
397 )
398 .map(|inner| (pallas::Hash::from(script_hash), inner))
399 })
400 .collect::<Vec<_>>(),
401 )
402}
403
404impl<C> cbor::Encode<C> for Value<u64> {
407 fn encode<W: cbor::encode::write::Write>(
408 &self,
409 e: &mut cbor::Encoder<W>,
410 ctx: &mut C,
411 ) -> Result<(), cbor::encode::Error<W::Error>> {
412 pallas::Value::from(self).encode(e, ctx)
413 }
414}
415
416impl<'d, C> cbor::Decode<'d, C> for Value<u64> {
417 fn decode(d: &mut cbor::Decoder<'d>, ctx: &mut C) -> Result<Self, cbor::decode::Error> {
418 let value: pallas::Value = d.decode_with(ctx)?;
419 Ok(Self::from(&value))
420 }
421}
422
423fn prune_null_values<Quantity: Zero>(value: &mut BTreeMap<Hash<28>, BTreeMap<Vec<u8>, Quantity>>) {
426 let mut script_hashes_to_remove = Vec::new();
427
428 for (script_hash, assets) in value.iter_mut() {
429 let mut assets_to_remove = Vec::new();
430
431 for (asset_name, quantity) in assets.iter() {
432 if quantity.is_zero() {
433 assets_to_remove.push(asset_name.clone());
434 }
435 }
436
437 for asset_name in assets_to_remove {
438 assets.remove(&asset_name);
439 }
440
441 if assets.is_empty() {
442 script_hashes_to_remove.push(*script_hash)
443 }
444 }
445
446 for script_hash in script_hashes_to_remove {
447 value.remove(&script_hash);
448 }
449}
450
451fn display_asset_name(asset_name: &[u8]) -> String {
452 if let Ok(utf8) = str::from_utf8(asset_name) {
453 utf8.to_string()
454 } else {
455 hex::encode(asset_name)
456 }
457}
458
459fn with_assets<AssetName, Quantity: Zero>(
460 value: &mut Value<Quantity>,
461 assets: impl IntoIterator<Item = (Hash<28>, impl IntoIterator<Item = (AssetName, Quantity)>)>,
462) where
463 AssetName: AsRef<[u8]>,
464{
465 for (script_hash, inner) in assets.into_iter() {
466 let mut inner = inner
467 .into_iter()
468 .filter_map(|(asset_name, quantity)| {
469 if quantity.is_zero() {
470 None
471 } else {
472 Some((Vec::from(asset_name.as_ref()), quantity))
473 }
474 })
475 .collect::<BTreeMap<_, _>>();
476
477 value
478 .1
479 .entry(script_hash)
480 .and_modify(|entry| entry.append(&mut inner))
481 .or_insert(inner);
482 }
483}
484
485#[cfg(test)]
488mod tests {
489 use super::Value;
490 use crate::value;
491
492 #[test]
493 fn display_only_lovelace() {
494 let value: Value<u64> = Value::new(42);
495 assert_eq!(value.to_string(), "Value { lovelace: 42 }")
496 }
497
498 #[test]
499 fn display_value_with_assets() {
500 let value: Value<u64> = value!(
501 6687232,
502 (
503 "279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f",
504 "534e454b",
505 1376
506 ),
507 (
508 "a0028f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c235",
509 "484f534b59",
510 134468443
511 ),
512 (
513 "f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c2a002835",
514 "b4d8cdcb5b039b",
515 1
516 ),
517 );
518 assert_eq!(
519 value.to_string(),
520 "Value { \
521 lovelace: 6687232, \
522 assets: {\
523 279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f: {SNEK: 1376}, \
524 a0028f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c235: {HOSKY: 134468443}, \
525 f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c2a002835: {b4d8cdcb5b039b: 1}\
526 } \
527 }",
528 )
529 }
530}