Start work on new Amount type (#554)
This commit is contained in:
parent
51f6ce86ff
commit
8281b9040c
|
|
@ -408,6 +408,17 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "displaydoc"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6269d127174b18c665e683e23c2c55d3735fadbec4181c7c70b0450b764bfa5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2 1.0.18",
|
||||||
|
"quote 1.0.7",
|
||||||
|
"syn 1.0.33",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtoa"
|
name = "dtoa"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
|
|
@ -2309,6 +2320,8 @@ dependencies = [
|
||||||
"bs58",
|
"bs58",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"color-eyre",
|
||||||
|
"displaydoc",
|
||||||
"ed25519-zebra",
|
"ed25519-zebra",
|
||||||
"equihash",
|
"equihash",
|
||||||
"futures",
|
"futures",
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,10 @@ serde-big-array = "0.3.0"
|
||||||
ed25519-zebra = "0.4"
|
ed25519-zebra = "0.4"
|
||||||
redjubjub = "0.1"
|
redjubjub = "0.1"
|
||||||
equihash = { git = "https://github.com/ZcashFoundation/librustzcash.git", branch = "equihash-crate" }
|
equihash = { git = "https://github.com/ZcashFoundation/librustzcash.git", branch = "equihash-crate" }
|
||||||
|
displaydoc = "0.1.6"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = "0.10"
|
proptest = "0.10"
|
||||||
proptest-derive = "0.2.0"
|
proptest-derive = "0.2.0"
|
||||||
zebra-test = { path = "../zebra-test/" }
|
zebra-test = { path = "../zebra-test/" }
|
||||||
|
color-eyre = "0.5"
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,13 @@ pub enum SerializationError {
|
||||||
// XXX refine errors
|
// XXX refine errors
|
||||||
#[error("parse error: {0}")]
|
#[error("parse error: {0}")]
|
||||||
Parse(&'static str),
|
Parse(&'static str),
|
||||||
|
/// An error caused when validating a zatoshi `Amount`
|
||||||
|
#[error("input couldn't be parsed as a zatoshi `Amount`")]
|
||||||
|
Amount {
|
||||||
|
/// The source error indicating how the num failed to validate
|
||||||
|
#[from]
|
||||||
|
source: crate::types::amount::Error,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consensus-critical serialization for Zcash.
|
/// Consensus-critical serialization for Zcash.
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ pub use shielded_data::{Output, ShieldedData, Spend};
|
||||||
pub use transparent::{CoinbaseData, OutPoint, TransparentInput, TransparentOutput};
|
pub use transparent::{CoinbaseData, OutPoint, TransparentInput, TransparentOutput};
|
||||||
|
|
||||||
use crate::proofs::{Bctv14Proof, Groth16Proof};
|
use crate::proofs::{Bctv14Proof, Groth16Proof};
|
||||||
use crate::types::{BlockHeight, LockTime};
|
use crate::types::{amount::Amount, BlockHeight, LockTime};
|
||||||
|
|
||||||
/// A Zcash transaction.
|
/// A Zcash transaction.
|
||||||
///
|
///
|
||||||
|
|
@ -82,8 +82,7 @@ pub enum Transaction {
|
||||||
/// The latest block height that this transaction can be added to the chain.
|
/// The latest block height that this transaction can be added to the chain.
|
||||||
expiry_height: BlockHeight,
|
expiry_height: BlockHeight,
|
||||||
/// The net value of Sapling spend transfers minus output transfers.
|
/// The net value of Sapling spend transfers minus output transfers.
|
||||||
// XXX refine this to an Amount type.
|
value_balance: Amount,
|
||||||
value_balance: i64,
|
|
||||||
/// The shielded data for this transaction, if any.
|
/// The shielded data for this transaction, if any.
|
||||||
shielded_data: Option<ShieldedData>,
|
shielded_data: Option<ShieldedData>,
|
||||||
/// The JoinSplit data for this transaction, if any.
|
/// The JoinSplit data for this transaction, if any.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::types::amount::{Amount, NonNegative};
|
||||||
use crate::{ed25519_zebra, notes::sprout, proofs::ZkSnarkProof};
|
use crate::{ed25519_zebra, notes::sprout, proofs::ZkSnarkProof};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -8,14 +9,11 @@ use serde::{Deserialize, Serialize};
|
||||||
pub struct JoinSplit<P: ZkSnarkProof> {
|
pub struct JoinSplit<P: ZkSnarkProof> {
|
||||||
/// A value that the JoinSplit transfer removes from the transparent value
|
/// A value that the JoinSplit transfer removes from the transparent value
|
||||||
/// pool.
|
/// pool.
|
||||||
///
|
pub vpub_old: Amount<NonNegative>,
|
||||||
/// XXX refine to an Amount
|
|
||||||
pub vpub_old: u64,
|
|
||||||
/// A value that the JoinSplit transfer inserts into the transparent value
|
/// A value that the JoinSplit transfer inserts into the transparent value
|
||||||
/// pool.
|
/// pool.
|
||||||
///
|
///
|
||||||
/// XXX refine to an Amount
|
pub vpub_new: Amount<NonNegative>,
|
||||||
pub vpub_new: u64,
|
|
||||||
/// A root of the Sprout note commitment tree at some block height in the
|
/// A root of the Sprout note commitment tree at some block height in the
|
||||||
/// past, or the root produced by a previous JoinSplit transfer in this
|
/// past, or the root produced by a previous JoinSplit transfer in this
|
||||||
/// transaction.
|
/// transaction.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
use std::{
|
use std::{
|
||||||
|
convert::TryInto,
|
||||||
io::{self, Read},
|
io::{self, Read},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
@ -215,7 +216,7 @@ impl ZcashDeserialize for TransparentInput {
|
||||||
|
|
||||||
impl ZcashSerialize for TransparentOutput {
|
impl ZcashSerialize for TransparentOutput {
|
||||||
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
|
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
|
||||||
writer.write_u64::<LittleEndian>(self.value)?;
|
writer.write_u64::<LittleEndian>(self.value.into())?;
|
||||||
self.pk_script.zcash_serialize(&mut writer)?;
|
self.pk_script.zcash_serialize(&mut writer)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +225,7 @@ impl ZcashSerialize for TransparentOutput {
|
||||||
impl ZcashDeserialize for TransparentOutput {
|
impl ZcashDeserialize for TransparentOutput {
|
||||||
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
|
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
|
||||||
Ok(TransparentOutput {
|
Ok(TransparentOutput {
|
||||||
value: reader.read_u64::<LittleEndian>()?,
|
value: reader.read_u64::<LittleEndian>()?.try_into()?,
|
||||||
pk_script: Script::zcash_deserialize(&mut reader)?,
|
pk_script: Script::zcash_deserialize(&mut reader)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -232,8 +233,8 @@ impl ZcashDeserialize for TransparentOutput {
|
||||||
|
|
||||||
impl<P: ZkSnarkProof> ZcashSerialize for JoinSplit<P> {
|
impl<P: ZkSnarkProof> ZcashSerialize for JoinSplit<P> {
|
||||||
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
|
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
|
||||||
writer.write_u64::<LittleEndian>(self.vpub_old)?;
|
writer.write_u64::<LittleEndian>(self.vpub_old.into())?;
|
||||||
writer.write_u64::<LittleEndian>(self.vpub_new)?;
|
writer.write_u64::<LittleEndian>(self.vpub_new.into())?;
|
||||||
writer.write_all(&self.anchor[..])?;
|
writer.write_all(&self.anchor[..])?;
|
||||||
writer.write_all(&self.nullifiers[0][..])?;
|
writer.write_all(&self.nullifiers[0][..])?;
|
||||||
writer.write_all(&self.nullifiers[1][..])?;
|
writer.write_all(&self.nullifiers[1][..])?;
|
||||||
|
|
@ -253,8 +254,8 @@ impl<P: ZkSnarkProof> ZcashSerialize for JoinSplit<P> {
|
||||||
impl<P: ZkSnarkProof> ZcashDeserialize for JoinSplit<P> {
|
impl<P: ZkSnarkProof> ZcashDeserialize for JoinSplit<P> {
|
||||||
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
|
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
|
||||||
Ok(JoinSplit::<P> {
|
Ok(JoinSplit::<P> {
|
||||||
vpub_old: reader.read_u64::<LittleEndian>()?,
|
vpub_old: reader.read_u64::<LittleEndian>()?.try_into()?,
|
||||||
vpub_new: reader.read_u64::<LittleEndian>()?,
|
vpub_new: reader.read_u64::<LittleEndian>()?.try_into()?,
|
||||||
anchor: reader.read_32_bytes()?,
|
anchor: reader.read_32_bytes()?,
|
||||||
nullifiers: [reader.read_32_bytes()?, reader.read_32_bytes()?],
|
nullifiers: [reader.read_32_bytes()?, reader.read_32_bytes()?],
|
||||||
commitments: [reader.read_32_bytes()?, reader.read_32_bytes()?],
|
commitments: [reader.read_32_bytes()?, reader.read_32_bytes()?],
|
||||||
|
|
@ -433,7 +434,7 @@ impl ZcashSerialize for Transaction {
|
||||||
outputs.zcash_serialize(&mut writer)?;
|
outputs.zcash_serialize(&mut writer)?;
|
||||||
lock_time.zcash_serialize(&mut writer)?;
|
lock_time.zcash_serialize(&mut writer)?;
|
||||||
writer.write_u32::<LittleEndian>(expiry_height.0)?;
|
writer.write_u32::<LittleEndian>(expiry_height.0)?;
|
||||||
writer.write_i64::<LittleEndian>(*value_balance)?;
|
writer.write_i64::<LittleEndian>((*value_balance).into())?;
|
||||||
|
|
||||||
// The previous match arms serialize in one go, because the
|
// The previous match arms serialize in one go, because the
|
||||||
// internal structure happens to nicely line up with the
|
// internal structure happens to nicely line up with the
|
||||||
|
|
@ -540,7 +541,7 @@ impl ZcashDeserialize for Transaction {
|
||||||
let outputs = Vec::zcash_deserialize(&mut reader)?;
|
let outputs = Vec::zcash_deserialize(&mut reader)?;
|
||||||
let lock_time = LockTime::zcash_deserialize(&mut reader)?;
|
let lock_time = LockTime::zcash_deserialize(&mut reader)?;
|
||||||
let expiry_height = BlockHeight(reader.read_u32::<LittleEndian>()?);
|
let expiry_height = BlockHeight(reader.read_u32::<LittleEndian>()?);
|
||||||
let value_balance = reader.read_i64::<LittleEndian>()?;
|
let value_balance = reader.read_i64::<LittleEndian>()?.try_into()?;
|
||||||
let mut shielded_spends = Vec::zcash_deserialize(&mut reader)?;
|
let mut shielded_spends = Vec::zcash_deserialize(&mut reader)?;
|
||||||
let mut shielded_outputs = Vec::zcash_deserialize(&mut reader)?;
|
let mut shielded_outputs = Vec::zcash_deserialize(&mut reader)?;
|
||||||
let joinsplit_data = OptV4JSD::zcash_deserialize(&mut reader)?;
|
let joinsplit_data = OptV4JSD::zcash_deserialize(&mut reader)?;
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ impl Transaction {
|
||||||
vec(any::<TransparentOutput>(), 0..10),
|
vec(any::<TransparentOutput>(), 0..10),
|
||||||
any::<LockTime>(),
|
any::<LockTime>(),
|
||||||
any::<BlockHeight>(),
|
any::<BlockHeight>(),
|
||||||
any::<i64>(),
|
any::<Amount>(),
|
||||||
option::of(any::<ShieldedData>()),
|
option::of(any::<ShieldedData>()),
|
||||||
option::of(any::<JoinSplitData<Groth16Proof>>()),
|
option::of(any::<JoinSplitData<Groth16Proof>>()),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ use crate::{
|
||||||
CoinbaseData, JoinSplit, JoinSplitData, OutPoint, Output, ShieldedData, Spend, Transaction,
|
CoinbaseData, JoinSplit, JoinSplitData, OutPoint, Output, ShieldedData, Spend, Transaction,
|
||||||
TransparentInput,
|
TransparentInput,
|
||||||
},
|
},
|
||||||
types::{BlockHeight, Script},
|
types::{
|
||||||
|
amount::{Amount, NonNegative},
|
||||||
|
BlockHeight, Script,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use futures::future::Either;
|
use futures::future::Either;
|
||||||
use proptest::{array, collection::vec, prelude::*};
|
use proptest::{array, collection::vec, prelude::*};
|
||||||
|
|
@ -16,8 +19,8 @@ impl<P: ZkSnarkProof + Arbitrary + 'static> Arbitrary for JoinSplit<P> {
|
||||||
|
|
||||||
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
|
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
|
||||||
(
|
(
|
||||||
any::<u64>(),
|
any::<Amount<NonNegative>>(),
|
||||||
any::<u64>(),
|
any::<Amount<NonNegative>>(),
|
||||||
array::uniform32(any::<u8>()),
|
array::uniform32(any::<u8>()),
|
||||||
array::uniform2(array::uniform32(any::<u8>())),
|
array::uniform2(array::uniform32(any::<u8>())),
|
||||||
array::uniform2(array::uniform32(any::<u8>())),
|
array::uniform2(array::uniform32(any::<u8>())),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use proptest_derive::Arbitrary;
|
use proptest_derive::Arbitrary;
|
||||||
|
|
||||||
use crate::types::{BlockHeight, Script};
|
use crate::types::{
|
||||||
|
amount::{Amount, NonNegative},
|
||||||
|
BlockHeight, Script,
|
||||||
|
};
|
||||||
|
|
||||||
use super::TransactionHash;
|
use super::TransactionHash;
|
||||||
|
|
||||||
|
|
@ -80,8 +83,7 @@ pub enum TransparentInput {
|
||||||
pub struct TransparentOutput {
|
pub struct TransparentOutput {
|
||||||
/// Transaction value.
|
/// Transaction value.
|
||||||
// At https://en.bitcoin.it/wiki/Protocol_documentation#tx, this is an i64.
|
// At https://en.bitcoin.it/wiki/Protocol_documentation#tx, this is an i64.
|
||||||
// XXX refine to Amount ?
|
pub value: Amount<NonNegative>,
|
||||||
pub value: u64,
|
|
||||||
|
|
||||||
/// Usually contains the public key as a Bitcoin script setting up
|
/// Usually contains the public key as a Bitcoin script setting up
|
||||||
/// conditions to claim this output.
|
/// conditions to claim this output.
|
||||||
|
|
|
||||||
|
|
@ -15,28 +15,7 @@ use crate::serialization::{
|
||||||
ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashSerialize,
|
ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashSerialize,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A 4-byte checksum using truncated double-SHA256 (two rounds of SHA256).
|
pub mod amount;
|
||||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
|
||||||
pub struct Sha256dChecksum(pub [u8; 4]);
|
|
||||||
|
|
||||||
impl<'a> From<&'a [u8]> for Sha256dChecksum {
|
|
||||||
fn from(bytes: &'a [u8]) -> Self {
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
let hash1 = Sha256::digest(bytes);
|
|
||||||
let hash2 = Sha256::digest(&hash1);
|
|
||||||
let mut checksum = [0u8; 4];
|
|
||||||
checksum[0..4].copy_from_slice(&hash2[0..4]);
|
|
||||||
Self(checksum)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Sha256dChecksum {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
f.debug_tuple("Sha256dChecksum")
|
|
||||||
.field(&hex::encode(&self.0))
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A u32 which represents a block height value.
|
/// A u32 which represents a block height value.
|
||||||
///
|
///
|
||||||
|
|
@ -145,6 +124,29 @@ impl ZcashDeserialize for Script {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A 4-byte checksum using truncated double-SHA256 (two rounds of SHA256).
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub struct Sha256dChecksum(pub [u8; 4]);
|
||||||
|
|
||||||
|
impl<'a> From<&'a [u8]> for Sha256dChecksum {
|
||||||
|
fn from(bytes: &'a [u8]) -> Self {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
let hash1 = Sha256::digest(bytes);
|
||||||
|
let hash2 = Sha256::digest(&hash1);
|
||||||
|
let mut checksum = [0u8; 4];
|
||||||
|
checksum[0..4].copy_from_slice(&hash2[0..4]);
|
||||||
|
Self(checksum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Sha256dChecksum {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.debug_tuple("Sha256dChecksum")
|
||||||
|
.field(&hex::encode(&self.0))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,387 @@
|
||||||
|
//! Module of types for working with validated zatoshi Amounts
|
||||||
|
use std::{
|
||||||
|
convert::{TryFrom, TryInto},
|
||||||
|
marker::PhantomData,
|
||||||
|
ops::RangeInclusive,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
/// A runtime validated type for representing amounts of zatoshis
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub struct Amount<C = NegativeAllowed>(i64, PhantomData<C>);
|
||||||
|
|
||||||
|
impl<C> Amount<C> {
|
||||||
|
/// Convert this amount to a different Amount type if it satisfies the new constraint
|
||||||
|
pub fn constrain<C2>(self) -> Result<Amount<C2>>
|
||||||
|
where
|
||||||
|
C2: AmountConstraint,
|
||||||
|
{
|
||||||
|
self.0.try_into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> std::ops::Add<Amount<C>> for Amount<C>
|
||||||
|
where
|
||||||
|
C: AmountConstraint,
|
||||||
|
{
|
||||||
|
type Output = Result<Amount<C>>;
|
||||||
|
|
||||||
|
fn add(self, rhs: Amount<C>) -> Self::Output {
|
||||||
|
let value = self.0 + rhs.0;
|
||||||
|
value.try_into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> std::ops::Add<Amount<C>> for Result<Amount<C>>
|
||||||
|
where
|
||||||
|
C: AmountConstraint,
|
||||||
|
{
|
||||||
|
type Output = Result<Amount<C>>;
|
||||||
|
|
||||||
|
fn add(self, rhs: Amount<C>) -> Self::Output {
|
||||||
|
self? + rhs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> std::ops::Add<Result<Amount<C>>> for Amount<C>
|
||||||
|
where
|
||||||
|
C: AmountConstraint,
|
||||||
|
{
|
||||||
|
type Output = Result<Amount<C>>;
|
||||||
|
|
||||||
|
fn add(self, rhs: Result<Amount<C>>) -> Self::Output {
|
||||||
|
self + rhs?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> std::ops::AddAssign<Amount<C>> for Result<Amount<C>>
|
||||||
|
where
|
||||||
|
Amount<C>: Copy,
|
||||||
|
C: AmountConstraint,
|
||||||
|
{
|
||||||
|
fn add_assign(&mut self, rhs: Amount<C>) {
|
||||||
|
if let Ok(lhs) = *self {
|
||||||
|
*self = lhs + rhs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> std::ops::Sub<Amount<C>> for Amount<C>
|
||||||
|
where
|
||||||
|
C: AmountConstraint,
|
||||||
|
{
|
||||||
|
type Output = Result<Amount<C>>;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Amount<C>) -> Self::Output {
|
||||||
|
let value = self.0 - rhs.0;
|
||||||
|
value.try_into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> std::ops::Sub<Amount<C>> for Result<Amount<C>>
|
||||||
|
where
|
||||||
|
C: AmountConstraint,
|
||||||
|
{
|
||||||
|
type Output = Result<Amount<C>>;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Amount<C>) -> Self::Output {
|
||||||
|
self? - rhs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> std::ops::Sub<Result<Amount<C>>> for Amount<C>
|
||||||
|
where
|
||||||
|
C: AmountConstraint,
|
||||||
|
{
|
||||||
|
type Output = Result<Amount<C>>;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Result<Amount<C>>) -> Self::Output {
|
||||||
|
self - rhs?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> std::ops::SubAssign<Amount<C>> for Result<Amount<C>>
|
||||||
|
where
|
||||||
|
Amount<C>: Copy,
|
||||||
|
C: AmountConstraint,
|
||||||
|
{
|
||||||
|
fn sub_assign(&mut self, rhs: Amount<C>) {
|
||||||
|
if let Ok(lhs) = *self {
|
||||||
|
*self = lhs - rhs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> From<Amount<C>> for i64 {
|
||||||
|
fn from(amount: Amount<C>) -> Self {
|
||||||
|
amount.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Amount<NonNegative>> for u64 {
|
||||||
|
fn from(amount: Amount<NonNegative>) -> Self {
|
||||||
|
amount.0 as _
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> TryFrom<i64> for Amount<C>
|
||||||
|
where
|
||||||
|
C: AmountConstraint,
|
||||||
|
{
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: i64) -> Result<Self, Self::Error> {
|
||||||
|
C::validate(value).map(|v| Self(v, PhantomData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> TryFrom<i32> for Amount<C>
|
||||||
|
where
|
||||||
|
C: AmountConstraint,
|
||||||
|
{
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||||
|
C::validate(value as _).map(|v| Self(v, PhantomData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> TryFrom<u64> for Amount<C>
|
||||||
|
where
|
||||||
|
C: AmountConstraint,
|
||||||
|
{
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: u64) -> Result<Self, Self::Error> {
|
||||||
|
let value = value
|
||||||
|
.try_into()
|
||||||
|
.map_err(|source| Error::Convert { value, source })?;
|
||||||
|
|
||||||
|
C::validate(value).map(|v| Self(v, PhantomData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug, displaydoc::Display, Clone, PartialEq)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
/// Errors that can be returned when validating `Amount`s
|
||||||
|
pub enum Error {
|
||||||
|
/// input {value} is outside of valid range for zatoshi Amount, valid_range={range:?}
|
||||||
|
Contains {
|
||||||
|
range: RangeInclusive<i64>,
|
||||||
|
value: i64,
|
||||||
|
},
|
||||||
|
/// u64 {value} could not be converted to an i64 Amount
|
||||||
|
Convert {
|
||||||
|
value: u64,
|
||||||
|
source: std::num::TryFromIntError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
/// Marker type for `Amount` that restricts the values to `-MAX_MONEY..=MAX_MONEY`
|
||||||
|
pub enum NegativeAllowed {}
|
||||||
|
|
||||||
|
impl AmountConstraint for NegativeAllowed {
|
||||||
|
fn valid_range() -> RangeInclusive<i64> {
|
||||||
|
-MAX_MONEY..=MAX_MONEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
/// Marker type for `Amount` that restricts the value to positive numbers `0..=MAX_MONEY`
|
||||||
|
pub enum NonNegative {}
|
||||||
|
|
||||||
|
impl AmountConstraint for NonNegative {
|
||||||
|
fn valid_range() -> RangeInclusive<i64> {
|
||||||
|
0..=MAX_MONEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The max amount of money that can be obtained in zatoshis
|
||||||
|
pub const MAX_MONEY: i64 = 21_000_000 * 100_000_000;
|
||||||
|
|
||||||
|
/// A trait for defining constraints on `Amount`
|
||||||
|
pub trait AmountConstraint {
|
||||||
|
/// Returns the range of values that are valid under this constraint
|
||||||
|
fn valid_range() -> RangeInclusive<i64>;
|
||||||
|
|
||||||
|
/// Check if an input value is within the valid range
|
||||||
|
fn validate(value: i64) -> Result<i64, Error> {
|
||||||
|
let range = Self::valid_range();
|
||||||
|
|
||||||
|
if !range.contains(&value) {
|
||||||
|
Err(Error::Contains { range, value })
|
||||||
|
} else {
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
impl<C> Arbitrary for Amount<C>
|
||||||
|
where
|
||||||
|
C: AmountConstraint + fmt::Debug,
|
||||||
|
{
|
||||||
|
type Parameters = ();
|
||||||
|
|
||||||
|
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
|
||||||
|
C::valid_range().prop_map(|v| Self(v, PhantomData)).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Strategy = BoxedStrategy<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_bare() -> Result<()> {
|
||||||
|
zebra_test::init();
|
||||||
|
let one: Amount = 1.try_into()?;
|
||||||
|
let neg_one: Amount = (-1).try_into()?;
|
||||||
|
|
||||||
|
let zero: Amount = 0.try_into()?;
|
||||||
|
let new_zero = one + neg_one;
|
||||||
|
|
||||||
|
assert_eq!(zero, new_zero?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_opt_lhs() -> Result<()> {
|
||||||
|
zebra_test::init();
|
||||||
|
let one: Amount = 1.try_into()?;
|
||||||
|
let one = Ok(one);
|
||||||
|
let neg_one: Amount = (-1).try_into()?;
|
||||||
|
|
||||||
|
let zero: Amount = 0.try_into()?;
|
||||||
|
let new_zero = one + neg_one;
|
||||||
|
|
||||||
|
assert_eq!(zero, new_zero?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_opt_rhs() -> Result<()> {
|
||||||
|
zebra_test::init();
|
||||||
|
let one: Amount = 1.try_into()?;
|
||||||
|
let neg_one: Amount = (-1).try_into()?;
|
||||||
|
let neg_one = Ok(neg_one);
|
||||||
|
|
||||||
|
let zero: Amount = 0.try_into()?;
|
||||||
|
let new_zero = one + neg_one;
|
||||||
|
|
||||||
|
assert_eq!(zero, new_zero?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_opt_both() -> Result<()> {
|
||||||
|
zebra_test::init();
|
||||||
|
let one: Amount = 1.try_into()?;
|
||||||
|
let one = Ok(one);
|
||||||
|
let neg_one: Amount = (-1).try_into()?;
|
||||||
|
let neg_one = Ok(neg_one);
|
||||||
|
|
||||||
|
let zero: Amount = 0.try_into()?;
|
||||||
|
let new_zero = one.and_then(|one| one + neg_one);
|
||||||
|
|
||||||
|
assert_eq!(zero, new_zero?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_assign() -> Result<()> {
|
||||||
|
zebra_test::init();
|
||||||
|
let one: Amount = 1.try_into()?;
|
||||||
|
let neg_one: Amount = (-1).try_into()?;
|
||||||
|
let mut neg_one = Ok(neg_one);
|
||||||
|
|
||||||
|
let zero: Amount = 0.try_into()?;
|
||||||
|
neg_one += one;
|
||||||
|
let new_zero = neg_one;
|
||||||
|
|
||||||
|
assert_eq!(Ok(zero), new_zero);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sub_bare() -> Result<()> {
|
||||||
|
zebra_test::init();
|
||||||
|
let one: Amount = 1.try_into()?;
|
||||||
|
let zero: Amount = 0.try_into()?;
|
||||||
|
|
||||||
|
let neg_one: Amount = (-1).try_into()?;
|
||||||
|
let new_neg_one = zero - one;
|
||||||
|
|
||||||
|
assert_eq!(Ok(neg_one), new_neg_one);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sub_opt_lhs() -> Result<()> {
|
||||||
|
zebra_test::init();
|
||||||
|
let one: Amount = 1.try_into()?;
|
||||||
|
let one = Ok(one);
|
||||||
|
let zero: Amount = 0.try_into()?;
|
||||||
|
|
||||||
|
let neg_one: Amount = (-1).try_into()?;
|
||||||
|
let new_neg_one = zero - one;
|
||||||
|
|
||||||
|
assert_eq!(Ok(neg_one), new_neg_one);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sub_opt_rhs() -> Result<()> {
|
||||||
|
zebra_test::init();
|
||||||
|
let one: Amount = 1.try_into()?;
|
||||||
|
let zero: Amount = 0.try_into()?;
|
||||||
|
let zero = Ok(zero);
|
||||||
|
|
||||||
|
let neg_one: Amount = (-1).try_into()?;
|
||||||
|
let new_neg_one = zero - one;
|
||||||
|
|
||||||
|
assert_eq!(Ok(neg_one), new_neg_one);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sub_assign() -> Result<()> {
|
||||||
|
zebra_test::init();
|
||||||
|
let one: Amount = 1.try_into()?;
|
||||||
|
let zero: Amount = 0.try_into()?;
|
||||||
|
let mut zero = Ok(zero);
|
||||||
|
|
||||||
|
let neg_one: Amount = (-1).try_into()?;
|
||||||
|
zero -= one;
|
||||||
|
let new_neg_one = zero;
|
||||||
|
|
||||||
|
assert_eq!(Ok(neg_one), new_neg_one);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_with_diff_constraints() -> Result<()> {
|
||||||
|
let one = Amount::<NonNegative>::try_from(1)?;
|
||||||
|
let zero = Amount::<NegativeAllowed>::try_from(0)?;
|
||||||
|
|
||||||
|
(zero - one.constrain()).expect("should allow negative");
|
||||||
|
(zero.constrain() - one).expect_err("shouldn't allow negative");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue