From 737d0c11885435b697e42406a64e817fc12684aa Mon Sep 17 00:00:00 2001 From: Shuhui Luo Date: Sun, 24 Dec 2023 15:32:36 -0800 Subject: [PATCH 01/13] factor out `FractionTrait` and test --- src/entities/fractions/fraction.rs | 283 ++++++++++++++++++++++------- src/utils/sqrt.rs | 4 +- 2 files changed, 220 insertions(+), 67 deletions(-) diff --git a/src/entities/fractions/fraction.rs b/src/entities/fractions/fraction.rs index 97850b5..c84525d 100644 --- a/src/entities/fractions/fraction.rs +++ b/src/entities/fractions/fraction.rs @@ -1,16 +1,13 @@ +use crate::constants::Rounding; use num_bigint::BigInt; +use num_integer::Integer; use rust_decimal::prelude::*; +use std::ops::Div; #[derive(Clone, PartialEq)] pub struct Fraction { - pub numerator: BigInt, - pub denominator: BigInt, -} - -pub enum Rounding { - RoundDown, - RoundHalfUp, - RoundUp, + numerator: BigInt, + denominator: BigInt, } fn to_rounding_strategy(rounding: Rounding) -> RoundingStrategy { @@ -21,103 +18,259 @@ fn to_rounding_strategy(rounding: Rounding) -> RoundingStrategy { } } -impl Fraction { - pub fn new(numerator: BigInt, denominator: BigInt) -> Self { - assert!(denominator != BigInt::from(0), "DENOMINATOR CAN'T BE ZERO"); - Self { - numerator, - denominator, - } - } +pub trait FractionTrait: Sized { + fn new(numerator: impl Into, denominator: impl Into, meta: M) -> Self; + + /// Additional metadata that can be attached to the fraction + fn meta(&self) -> M; - pub fn quotient(&self) -> BigInt { - &self.numerator / &self.denominator + fn numerator(&self) -> &BigInt; + + fn denominator(&self) -> &BigInt; + + /// performs floor division + fn quotient(&self) -> BigInt { + self.numerator().div_floor(self.denominator()) } - pub fn remainder(&self) -> Fraction { - Fraction::new( - &self.numerator % &self.denominator, - self.denominator.clone(), + /// remainder after floor division + fn remainder(&self) -> Self { + Self::new( + self.numerator() % self.denominator(), + self.denominator().clone(), + self.meta(), ) } - pub fn invert(&self) -> Self { - Fraction::new(self.numerator.clone(), self.denominator.clone()) + fn invert(&self) -> Self { + Self::new( + self.denominator().clone(), + self.numerator().clone(), + self.meta(), + ) } - pub fn add(&self, other: &Fraction) -> Fraction { - if self.denominator == other.denominator { - Fraction::new(&self.numerator + &other.numerator, self.denominator.clone()) + fn add(&self, other: &Self) -> Self { + if self.denominator() == other.denominator() { + Self::new( + self.numerator() + other.numerator(), + self.denominator().clone(), + self.meta(), + ) } else { - Fraction::new( - &self.numerator * &other.denominator + &other.numerator * &self.denominator, - &self.denominator * &other.denominator, + Self::new( + self.numerator() * other.denominator() + other.numerator() * self.denominator(), + self.denominator() * other.denominator(), + self.meta(), ) } } - pub fn subtract(&self, other: &Fraction) -> Fraction { - if self.denominator == other.denominator { - Fraction::new(&self.numerator - &other.numerator, self.denominator.clone()) + fn subtract(&self, other: &Self) -> Self { + if self.denominator() == other.denominator() { + Self::new( + self.numerator() - other.numerator(), + self.denominator().clone(), + self.meta(), + ) } else { - Fraction::new( - &self.numerator * &other.denominator - &other.numerator * &self.denominator, - &self.denominator * &other.denominator, + Self::new( + self.numerator() * other.denominator() - other.numerator() * self.denominator(), + self.denominator() * other.denominator(), + self.meta(), ) } } - pub fn less_than(&self, other: &Fraction) -> bool { - &self.numerator * &other.denominator < &other.numerator * &self.denominator + fn multiply(&self, other: &Self) -> Self { + Self::new( + self.numerator() * other.numerator(), + self.denominator() * other.denominator(), + self.meta(), + ) + } + + fn divide(&self, other: &Self) -> Self { + Self::new( + self.numerator() * other.denominator(), + self.denominator() * other.numerator(), + self.meta(), + ) } - pub fn equal_to(&self, other: &Fraction) -> bool { - &self.numerator * &other.denominator == &other.numerator * &self.denominator + fn less_than(&self, other: &Self) -> bool { + self.numerator() * other.denominator() < other.numerator() * self.denominator() } - pub fn greater_than(&self, other: &Fraction) -> bool { - &self.numerator * &other.denominator > &other.numerator * &self.denominator + fn equal_to(&self, other: &Self) -> bool { + self.numerator() * other.denominator() == other.numerator() * self.denominator() } - pub fn multiply(&self, other: &Fraction) -> Fraction { - Fraction::new( - &self.numerator * &other.numerator, - &self.denominator * &other.denominator, - ) + fn greater_than(&self, other: &Self) -> bool { + self.numerator() * other.denominator() > other.numerator() * self.denominator() } - pub fn divide(&self, other: &Fraction) -> Fraction { - Fraction::new( - &self.numerator * &other.denominator, - &self.denominator * &other.numerator, - ) + fn to_decimal(&self) -> Decimal { + Decimal::from_str(&self.numerator().to_str_radix(10)) + .unwrap() + .div(Decimal::from_str(&self.denominator().to_str_radix(10)).unwrap()) } - pub fn to_significant(&self, significant_digits: u32, rounding: Rounding) -> String { + fn to_significant(&self, significant_digits: u32, rounding: Rounding) -> String { assert!( significant_digits > 0, "Significant digits must be positive." ); let rounding_strategy = to_rounding_strategy(rounding); + let quotient = self + .to_decimal() + .round_sf_with_strategy(significant_digits, rounding_strategy); - let quotient = &self.numerator / &self.denominator; - let quotient = Decimal::from_str("ient.to_str_radix(10)).unwrap(); - let quotient = quotient.round_dp_with_strategy(significant_digits, rounding_strategy); - - quotient.to_string() + quotient.unwrap().normalize().to_string() } - pub fn to_fixed(&self, decimal_places: u32, rounding: Rounding) -> String { + fn to_fixed(&self, decimal_places: u32, rounding: Rounding) -> String { let rounding_strategy = to_rounding_strategy(rounding); + let quotient = self + .to_decimal() + .round_dp_with_strategy(decimal_places, rounding_strategy); + + format!("{:.1$}", quotient, decimal_places as usize) + } - let quotient = &self.numerator / &self.denominator; - let quotient = Decimal::from_str("ient.to_str_radix(10)).unwrap(); - let quotient = quotient.round_dp_with_strategy(decimal_places, rounding_strategy); + /// Helper method for converting any super class back to a fraction + fn as_fraction(&self) -> Fraction { + Fraction::new(self.numerator().clone(), self.denominator().clone(), ()) + } +} + +impl FractionTrait<()> for Fraction { + fn new(numerator: impl Into, denominator: impl Into, _: ()) -> Self { + let denominator = denominator.into(); + assert_ne!(denominator, 0.into(), "DENOMINATOR CAN'T BE ZERO"); + Self { + numerator: numerator.into(), + denominator, + } + } - quotient.to_string() + fn meta(&self) -> () { + () } - pub fn as_fraction(&self) -> Fraction { - Fraction::new(self.numerator.clone(), self.denominator.clone()) + + fn numerator(&self) -> &BigInt { + &self.numerator + } + + fn denominator(&self) -> &BigInt { + &self.denominator + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_quotient() { + assert_eq!(Fraction::new(8, 3, ()).quotient(), BigInt::from(2)); + assert_eq!(Fraction::new(12, 4, ()).quotient(), BigInt::from(3)); + assert_eq!(Fraction::new(16, 5, ()).quotient(), BigInt::from(3)); + } + + #[test] + fn test_remainder() { + assert!(Fraction::new(8, 3, ()) + .remainder() + .equal_to(&Fraction::new(2, 3, ()))); + assert!(Fraction::new(12, 4, ()) + .remainder() + .equal_to(&Fraction::new(0, 4, ()))); + assert!(Fraction::new(16, 5, ()) + .remainder() + .equal_to(&Fraction::new(1, 5, ()))); + } + + #[test] + fn test_invert() { + let fraction = Fraction::new(5, 10, ()).invert(); + assert_eq!(fraction.numerator, BigInt::from(10)); + assert_eq!(fraction.denominator, BigInt::from(5)); + } + + #[test] + fn test_add() { + assert!(Fraction::new(1, 10, ()) + .add(&Fraction::new(4, 12, ())) + .equal_to(&Fraction::new(52, 120, ()))); + assert!(Fraction::new(1, 5, ()) + .add(&Fraction::new(2, 5, ())) + .equal_to(&Fraction::new(3, 5, ()))); + } + + #[test] + fn test_subtract() { + assert!(Fraction::new(1, 10, ()) + .subtract(&Fraction::new(4, 12, ())) + .equal_to(&Fraction::new(-28, 120, ()))); + assert!(Fraction::new(3, 5, ()) + .subtract(&Fraction::new(2, 5, ())) + .equal_to(&Fraction::new(1, 5, ()))); + } + + #[test] + fn test_less_than() { + assert!(Fraction::new(1, 10, ()).less_than(&Fraction::new(4, 12, ()))); + assert!(!Fraction::new(1, 3, ()).less_than(&Fraction::new(4, 12, ()))); + assert!(!Fraction::new(5, 12, ()).less_than(&Fraction::new(4, 12, ()))); + } + + #[test] + fn test_equal_to() { + assert!(!Fraction::new(1, 10, ()).equal_to(&Fraction::new(4, 12, ()))); + assert!(Fraction::new(1, 3, ()).equal_to(&Fraction::new(4, 12, ()))); + assert!(!Fraction::new(5, 12, ()).equal_to(&Fraction::new(4, 12, ()))); + } + + #[test] + fn test_greater_than() { + assert!(!Fraction::new(1, 10, ()).greater_than(&Fraction::new(4, 12, ()))); + assert!(!Fraction::new(1, 3, ()).greater_than(&Fraction::new(4, 12, ()))); + assert!(Fraction::new(5, 12, ()).greater_than(&Fraction::new(4, 12, ()))); + } + + #[test] + fn test_multiply() { + assert!(Fraction::new(1, 10, ()) + .multiply(&Fraction::new(4, 12, ())) + .equal_to(&Fraction::new(4, 120, ()))); + assert!(Fraction::new(1, 3, ()) + .multiply(&Fraction::new(4, 12, ())) + .equal_to(&Fraction::new(4, 36, ()))); + assert!(Fraction::new(5, 12, ()) + .multiply(&Fraction::new(4, 12, ())) + .equal_to(&Fraction::new(20, 144, ()))); + } + + #[test] + fn test_divide() { + assert!(Fraction::new(1, 10, ()) + .divide(&Fraction::new(4, 12, ())) + .equal_to(&Fraction::new(12, 40, ()))); + assert!(Fraction::new(1, 3, ()) + .divide(&Fraction::new(4, 12, ())) + .equal_to(&Fraction::new(12, 12, ()))); + assert!(Fraction::new(5, 12, ()) + .divide(&Fraction::new(4, 12, ())) + .equal_to(&Fraction::new(60, 48, ()))); + } + + #[test] + fn test_as_faction() { + let f = Fraction::new(1, 2, ()); + // returns an equivalent but not the same reference fraction + assert!(f.as_fraction().equal_to(&f)); + assert_ne!(&f as *const _, &f.as_fraction() as *const _); } } diff --git a/src/utils/sqrt.rs b/src/utils/sqrt.rs index 0fcfc9f..d651d28 100644 --- a/src/utils/sqrt.rs +++ b/src/utils/sqrt.rs @@ -5,11 +5,11 @@ use num_traits::{ToPrimitive, Zero}; /// /// # Arguments /// -/// * `value`: The value for which to compute the square root, rounded down +/// * `value`: the value for which to compute the square root, rounded down /// /// returns: BigInt /// -fn sqrt(value: &BigInt) -> BigInt { +pub fn sqrt(value: &BigInt) -> BigInt { assert!(*value >= Zero::zero(), "NEGATIVE"); // If the value is less than or equal to MAX_SAFE_INTEGER, From 413fef6120cc54693e44d1159b2b8ab5a34dc95c Mon Sep 17 00:00:00 2001 From: Shuhui Luo Date: Sun, 24 Dec 2023 15:35:36 -0800 Subject: [PATCH 02/13] impl `FractionTrait` for `Percent` and test --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/entities/fractions/percent.rs | 119 +++++++++++++++++++++++------- 3 files changed, 94 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d77816..4baa3ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -640,7 +640,7 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "uniswap-sdk-core-rust" -version = "0.1.1" +version = "0.2.0" dependencies = [ "eth_checksum", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 5282d6e..02a3c05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uniswap-sdk-core-rust" -version = "0.1.1" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/entities/fractions/percent.rs b/src/entities/fractions/percent.rs index 612e3e1..2d08104 100644 --- a/src/entities/fractions/percent.rs +++ b/src/entities/fractions/percent.rs @@ -1,49 +1,114 @@ -use super::fraction::{Fraction, Rounding}; +use super::fraction::{Fraction, FractionTrait}; +use crate::constants::Rounding; +use lazy_static::lazy_static; use num_bigint::BigInt; +lazy_static! { + static ref ONE_HUNDRED: Fraction = Fraction::new(100, 1, ()); +} + +#[derive(Clone, PartialEq)] pub struct Percent { - fraction: Fraction, + numerator: BigInt, + denominator: BigInt, } impl Percent { + /// This boolean prevents a fraction from being interpreted as a Percent pub const IS_PERCENT: bool = true; +} - pub fn new(fraction: Fraction) -> Self { - Self { fraction } +impl FractionTrait<()> for Percent { + fn new(numerator: impl Into, denominator: impl Into, _: ()) -> Self { + Self { + numerator: numerator.into(), + denominator: denominator.into(), + } } - pub fn add(&self, other: &Fraction) -> Percent { - let fraction = self.fraction.add(other); - Percent::new(fraction) + fn meta(&self) -> () { + () } - pub fn sub(&self, other: &Fraction) -> Percent { - let fraction = self.fraction.subtract(other); - Percent::new(fraction) + fn numerator(&self) -> &BigInt { + &self.numerator } - pub fn multiply(&self, other: &Fraction) -> Percent { - let fraction = self.fraction.multiply(other); - Percent::new(fraction) - } - pub fn divide(&self, other: &Fraction) -> Percent { - let fraction = self.fraction.divide(other); - Percent::new(fraction) + fn denominator(&self) -> &BigInt { + &self.denominator } - pub fn to_significant(&self, other: &Fraction) -> String { - let hundred = Fraction::new(BigInt::from(100), BigInt::from(1)); - let mult = Fraction::multiply(&hundred, other); - Fraction::to_significant(&mult, 5, Rounding::RoundUp) + fn to_significant(&self, significant_digits: u32, rounding: Rounding) -> String { + self.as_fraction() + .multiply(&ONE_HUNDRED) + .to_significant(significant_digits, rounding) } - pub fn to_fixed(&self, other: &Fraction) -> String { - let hundred = Fraction::new(BigInt::from(100), BigInt::from(1)); - let mult = Fraction::multiply(&hundred, other); - Fraction::to_fixed(&mult, 2, Rounding::RoundUp) + fn to_fixed(&self, decimal_places: u32, rounding: Rounding) -> String { + self.as_fraction() + .multiply(&ONE_HUNDRED) + .to_fixed(decimal_places, rounding) } } -pub fn to_percent(fraction: Fraction) -> Percent { - Percent::new(fraction) +#[cfg(test)] +mod tests { + use super::*; + use crate::constants::Rounding; + + #[test] + fn test_add() { + assert!(Percent::new(1, 100, ()) + .add(&Percent::new(2, 100, ())) + .equal_to(&Percent::new(3, 100, ()))); + assert!(Percent::new(1, 25, ()) + .add(&Percent::new(2, 100, ())) + .equal_to(&Percent::new(150, 2500, ()))); + } + + #[test] + fn test_subtract() { + assert!(Percent::new(1, 100, ()) + .subtract(&Percent::new(2, 100, ())) + .equal_to(&Percent::new(-1, 100, ()))); + assert!(Percent::new(1, 25, ()) + .subtract(&Percent::new(2, 100, ())) + .equal_to(&Percent::new(50, 2500, ()))); + } + + #[test] + fn test_multiply() { + assert!(Percent::new(1, 100, ()) + .multiply(&Percent::new(2, 100, ())) + .equal_to(&Percent::new(2, 10000, ()))); + assert!(Percent::new(1, 25, ()) + .multiply(&Percent::new(2, 100, ())) + .equal_to(&Percent::new(2, 2500, ()))); + } + + #[test] + fn test_divide() { + assert!(Percent::new(1, 100, ()) + .divide(&Percent::new(2, 100, ())) + .equal_to(&Percent::new(100, 200, ()))); + assert!(Percent::new(1, 25, ()) + .divide(&Percent::new(2, 100, ())) + .equal_to(&Percent::new(100, 50, ()))); + } + + #[test] + fn test_to_significant() { + assert_eq!( + Percent::new(154, 10000, ()).to_significant(3, Rounding::RoundHalfUp), + "1.54".to_string() + ); + } + + #[test] + fn test_to_fixed() { + assert_eq!( + Percent::new(154, 10000, ()).to_fixed(2, Rounding::RoundHalfUp), + "1.54".to_string() + ); + } } From 35ea7e0af7875697ecf39a2db7ee15c029bfac1e Mon Sep 17 00:00:00 2001 From: Shuhui Luo Date: Sun, 24 Dec 2023 15:38:57 -0800 Subject: [PATCH 03/13] make `BaseCurrency` & `NativeCurrency` trait --- src/entities/base_currency.rs | 47 ++++++++++++++++----------------- src/entities/native_currency.rs | 8 +++--- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/entities/base_currency.rs b/src/entities/base_currency.rs index 5f5135d..c719b73 100644 --- a/src/entities/base_currency.rs +++ b/src/entities/base_currency.rs @@ -1,29 +1,28 @@ +use super::{currency::Currency, token::Token}; + /// A currency is any fungible financial instrument, including Ether, all ERC20 tokens, and other chain-native currencies -#[derive(Clone, PartialEq)] -pub struct BaseCurrency { - pub chain_id: u32, - pub decimals: u32, - pub name: Option, - pub symbol: Option, - pub is_native: bool, -} +pub trait BaseCurrency { + /// The chain ID on which this currency resides + fn chain_id(&self) -> u32; + + /// The decimals used in representing currency amounts + fn decimals(&self) -> u32; + + /// The symbol of the currency, i.e. a short textual non-unique identifier + fn symbol(&self) -> Option; -impl BaseCurrency { - pub fn new(chain_id: u32, decimals: u32, name: Option, symbol: Option) -> Self { - assert!(chain_id > 0, "CHAIN_ID"); - assert!(decimals < 255, "DECIMALS"); + /// The name of the currency, i.e. a descriptive textual non-unique identifier + fn name(&self) -> Option; - Self { - chain_id, - decimals, - name, - symbol, - is_native: Self::is_native(), - } - } + /// Returns whether this currency is functionally equivalent to the other currency + /// + /// # Arguments + /// + /// * `other`: the other currency + /// + fn equals(&self, other: &Currency) -> bool; - /// Returns whether the currency is native to the chain and must be wrapped (e.g. Ether) - pub fn is_native() -> bool { - true - } + /// Return the wrapped version of this currency that can be used with the Uniswap contracts. + /// Currencies must implement this to be used in Uniswap + fn wrapped(&self) -> Token; } diff --git a/src/entities/native_currency.rs b/src/entities/native_currency.rs index e4aa7ef..d32ed76 100644 --- a/src/entities/native_currency.rs +++ b/src/entities/native_currency.rs @@ -1,5 +1,3 @@ -#[derive(Clone, PartialEq)] -pub struct NativeCurrency { - pub is_native: bool, - pub is_token: bool, -} +use super::base_currency::BaseCurrency; + +pub trait NativeCurrency: BaseCurrency {} From c42ae6b81ecad059ae0cb8c3652403d1789b0b0c Mon Sep 17 00:00:00 2001 From: Shuhui Luo Date: Sun, 24 Dec 2023 15:40:48 -0800 Subject: [PATCH 04/13] impl `BaseCurrency` for `Token` --- src/entities/token.rs | 107 +++++++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 38 deletions(-) diff --git a/src/entities/token.rs b/src/entities/token.rs index 878e7e3..7aca063 100644 --- a/src/entities/token.rs +++ b/src/entities/token.rs @@ -1,15 +1,59 @@ -use super::base_currency::BaseCurrency; +use super::{base_currency::BaseCurrency, currency::Currency}; use num_bigint::BigUint; /// Represents an ERC20 token with a unique address and some metadata. #[derive(Clone, PartialEq)] pub struct Token { - pub base_currency: BaseCurrency, + pub chain_id: u32, pub address: String, + pub decimals: u32, + pub symbol: Option, + pub name: Option, pub buy_fee_bps: Option, pub sell_fee_bps: Option, } +impl BaseCurrency for Token { + fn chain_id(&self) -> u32 { + self.chain_id + } + + fn decimals(&self) -> u32 { + self.decimals + } + + fn symbol(&self) -> Option { + self.symbol.clone() + } + + fn name(&self) -> Option { + self.name.clone() + } + + /// Returns true if the two tokens are equivalent, i.e. have the same chainId and address. + /// + /// # Arguments + /// + /// * `other`: other token to compare + /// + /// returns: bool + /// + fn equals(&self, other: &Currency) -> bool { + match other { + Currency::Token(other) => { + self.chain_id == other.chain_id + && self.address.to_lowercase() == other.address.to_lowercase() + } + _ => false, + } + } + + /// Return this token, which does not need to be wrapped + fn wrapped(&self) -> Token { + self.clone() + } +} + impl Token { pub fn new( chain_id: u32, @@ -23,45 +67,25 @@ impl Token { assert!(chain_id > 0, "CHAIN_ID"); assert!(decimals < 255, "DECIMALS"); Self { - base_currency: BaseCurrency::new(chain_id, decimals, symbol, name), + chain_id, address, + decimals, + symbol, + name, buy_fee_bps, sell_fee_bps, } } - pub fn is_native(&self) -> bool { - false - } - - pub fn is_token(&self) -> bool { - true - } - - /// Returns true if the two tokens are equivalent, i.e. have the same chainId and address. - /// - /// # Arguments - /// - /// * `other` - The other token to compare. - /// - pub fn equals(&self, other: &Token) -> bool { - other.is_token() - && self.base_currency.chain_id == other.base_currency.chain_id - && self.address.to_lowercase() == other.address.to_lowercase() - } - /// Returns true if the address of this token sorts before the address of the other token. /// Panics if the tokens have the same address or if the tokens are on different chains. /// /// # Arguments /// - /// * `other` - The other token to compare. + /// * `other`: other token to compare /// pub fn sorts_before(&self, other: &Token) -> bool { - assert_eq!( - self.base_currency.chain_id, other.base_currency.chain_id, - "CHAIN_IDS" - ); + assert_eq!(self.chain_id, other.chain_id, "CHAIN_IDS"); assert_ne!( self.address.to_lowercase(), other.address.to_lowercase(), @@ -69,16 +93,13 @@ impl Token { ); self.address.to_lowercase() < other.address.to_lowercase() } - - pub fn wrapped(&self) -> Token { - self.clone() - } } #[cfg(test)] mod tests { //should test for neg chain_id or neg decimals or neg buy_fee or neg sell_fee, but the compiler will panic by itself, so no need - use super::Token; + use super::*; + const ADDRESS_ONE: &str = "0x0000000000000000000000000000000000000001"; const ADDRESS_TWO: &str = "0x0000000000000000000000000000000000000002"; const DAI_MAINNET: &str = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; @@ -146,7 +167,7 @@ mod tests { ); assert!( - token.equals(&token_1), + token.equals(&Currency::Token(token_1)), "SHOULD_FAILS_EVEN_THOUGH_CHAIN_ID_IS_DIFFERENT" ); } @@ -173,7 +194,10 @@ mod tests { None, ); - assert!(token.equals(&token_1), "true even if names differ"); + assert!( + token.equals(&Currency::Token(token_1)), + "true even if names differ" + ); } #[test] @@ -198,7 +222,10 @@ mod tests { None, ); - assert!(token.equals(&token_1), "true even if symbols differ"); + assert!( + token.equals(&Currency::Token(token_1)), + "true even if symbols differ" + ); } #[test] @@ -225,7 +252,7 @@ mod tests { ); assert!( - token.equals(&token_1), + token.equals(&Currency::Token(token_1)), "SHOULD_FAILS_EVEN_THOUGH_ADDRESS_IS_DIFFERENT" ); } @@ -252,6 +279,10 @@ mod tests { None, ); - assert_eq!(token.equals(&token_1), token_1.equals(&token), "SHOULD_FAILS_EVEN_THOUGH_ADDRESS_IS_DIFFERENT, SHOULD ONLY REVERT FOR DIFFERENT CHAIN_ID"); + assert_eq!( + token.equals(&Currency::Token(token_1.clone())), + token_1.equals(&Currency::Token(token)), + "SHOULD_FAILS_EVEN_THOUGH_ADDRESS_IS_DIFFERENT, SHOULD ONLY REVERT FOR DIFFERENT CHAIN_ID" + ); } } From 55d0c72f6835e2ea19214b8b6e5ae4e81c57dabb Mon Sep 17 00:00:00 2001 From: Shuhui Luo Date: Sun, 24 Dec 2023 15:45:09 -0800 Subject: [PATCH 05/13] impl `NativeCurrency` for `Ether` --- src/entities/currency.rs | 6 ++-- src/entities/ether.rs | 68 +++++++++++++++++++++++++--------------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/entities/currency.rs b/src/entities/currency.rs index c0de993..5304d28 100644 --- a/src/entities/currency.rs +++ b/src/entities/currency.rs @@ -1,7 +1,7 @@ use super::{native_currency::NativeCurrency, token::Token}; -#[derive(Clone, PartialEq)] -pub enum Currency { - NativeCurrency(NativeCurrency), +#[derive(Clone)] +pub enum Currency<'a> { + NativeCurrency(&'a dyn NativeCurrency), Token(Token), } diff --git a/src/entities/ether.rs b/src/entities/ether.rs index 6745787..4d383ce 100644 --- a/src/entities/ether.rs +++ b/src/entities/ether.rs @@ -1,4 +1,6 @@ -use super::{base_currency::BaseCurrency, token::Token}; +use super::{ + base_currency::BaseCurrency, currency::Currency, native_currency::NativeCurrency, token::Token, +}; use lazy_static::lazy_static; use std::{collections::HashMap, sync::Mutex}; @@ -9,35 +11,22 @@ lazy_static! { /// Ether is the main usage of a 'native' currency, i.e. for Ethereum mainnet and all testnets #[derive(Clone, PartialEq)] pub struct Ether { - base_currency: BaseCurrency, - wrapped: Token, + pub chain_id: u32, + pub decimals: u32, + pub symbol: Option, + pub name: Option, } impl Ether { pub fn new(chain_id: u32) -> Self { Self { - base_currency: BaseCurrency::new( - chain_id, - 18, - Some("Ether".to_string()), - Some("ETH".to_string()), - ), - wrapped: Token::new( - chain_id, - "0x".to_string(), - 18, - Some("WETH".to_string()), - Some("Wrapped Ether".to_string()), - None, - None, - ), + chain_id, + decimals: 18, + symbol: Some("ETH".to_string()), + name: Some("Ether".to_string()), } } - pub fn wrapped(&self) -> &Token { - &self.wrapped - } - pub fn on_chain(chain_id: u32) -> Self { let mut cache = ETHER_CACHE.lock().unwrap(); match cache.get(&chain_id) { @@ -49,9 +38,36 @@ impl Ether { } } } +} + +impl NativeCurrency for Ether {} + +impl BaseCurrency for Ether { + fn chain_id(&self) -> u32 { + self.chain_id + } + + fn decimals(&self) -> u32 { + self.decimals + } + + fn symbol(&self) -> Option { + self.symbol.clone() + } + + fn name(&self) -> Option { + self.name.clone() + } + + fn equals(&self, other: &Currency) -> bool { + match other { + Currency::NativeCurrency(other) => self.chain_id() == other.chain_id(), + _ => false, + } + } - pub fn equals(&self, other: &BaseCurrency) -> bool { - other.is_native && other.chain_id == self.base_currency.chain_id + fn wrapped(&self) -> Token { + todo!() } } @@ -71,11 +87,11 @@ mod tests { #[test] fn test_equals_returns_false_for_different_chains() { - assert!(!Ether::on_chain(1).equals(&Ether::on_chain(2).base_currency)); + assert!(!Ether::on_chain(1).equals(&Currency::NativeCurrency(&Ether::on_chain(2)))); } #[test] fn test_equals_returns_true_for_same_chains() { - assert!(Ether::on_chain(1).equals(&Ether::on_chain(1).base_currency)); + assert!(Ether::on_chain(1).equals(&Currency::NativeCurrency(&Ether::on_chain(1)))); } } From ac713132a5f3503e1c23218d671bdfeb1ca615a9 Mon Sep 17 00:00:00 2001 From: Shuhui Luo Date: Sun, 24 Dec 2023 15:53:40 -0800 Subject: [PATCH 06/13] impl and test `CurrencyAmount` --- src/entities/currency.rs | 46 ++- src/entities/fractions/currency_amount.rs | 365 +++++++++++++++++----- 2 files changed, 337 insertions(+), 74 deletions(-) diff --git a/src/entities/currency.rs b/src/entities/currency.rs index 5304d28..53d227b 100644 --- a/src/entities/currency.rs +++ b/src/entities/currency.rs @@ -1,7 +1,51 @@ -use super::{native_currency::NativeCurrency, token::Token}; +use super::{base_currency::BaseCurrency, native_currency::NativeCurrency, token::Token}; #[derive(Clone)] pub enum Currency<'a> { NativeCurrency(&'a dyn NativeCurrency), Token(Token), } + +impl BaseCurrency for Currency<'_> { + fn chain_id(&self) -> u32 { + match self { + Currency::NativeCurrency(native_currency) => native_currency.chain_id(), + Currency::Token(token) => token.chain_id(), + } + } + + fn decimals(&self) -> u32 { + match self { + Currency::NativeCurrency(native_currency) => native_currency.decimals(), + Currency::Token(token) => token.decimals(), + } + } + + fn symbol(&self) -> Option { + match self { + Currency::NativeCurrency(native_currency) => native_currency.symbol(), + Currency::Token(token) => token.symbol(), + } + } + + fn name(&self) -> Option { + match self { + Currency::NativeCurrency(native_currency) => native_currency.name(), + Currency::Token(token) => token.name(), + } + } + + fn equals(&self, other: &Currency) -> bool { + match self { + Currency::NativeCurrency(native_currency) => native_currency.equals(other), + Currency::Token(token) => token.equals(other), + } + } + + fn wrapped(&self) -> Token { + match self { + Currency::NativeCurrency(native_currency) => native_currency.wrapped(), + Currency::Token(token) => token.clone(), + } + } +} diff --git a/src/entities/fractions/currency_amount.rs b/src/entities/fractions/currency_amount.rs index 1f3de4c..fa3551d 100644 --- a/src/entities/fractions/currency_amount.rs +++ b/src/entities/fractions/currency_amount.rs @@ -1,104 +1,323 @@ -use crate::entities::{ - currency::Currency, - fractions::fraction::{Fraction, Rounding}, +use crate::{ + constants::{Rounding, MAX_UINT256}, + entities::{ + base_currency::BaseCurrency, + currency::Currency, + fractions::fraction::{Fraction, FractionTrait}, + }, }; -use num_bigint::{BigInt, BigUint}; -use num_traits::ToPrimitive; - -#[derive(Clone, PartialEq)] -pub struct CurrencyAmount { - pub currency: Currency, - pub decimal_scale: Option, - pub fraction: Option, +use num_bigint::{BigInt, BigUint, ToBigInt}; +use num_integer::Integer; +use rust_decimal::Decimal; +use std::{ops::Div, str::FromStr}; + +#[derive(Clone)] +pub struct CurrencyAmount<'a> { + numerator: BigInt, + denominator: BigInt, + pub meta: CurrencyMeta<'a>, +} + +#[derive(Clone)] +pub struct CurrencyMeta<'a> { + pub currency: Currency<'a>, + pub decimal_scale: BigUint, } -impl CurrencyAmount { - pub fn new( - currency: Currency, - decimal_scale: Option, - fraction: Option, +impl<'a> CurrencyAmount<'a> { + fn new( + currency: Currency<'a>, + numerator: impl Into, + denominator: impl Into, ) -> Self { + let numerator = numerator.into(); + let denominator = denominator.into(); + assert!(numerator.div_floor(&denominator).le(&MAX_UINT256), "AMOUNT"); + let exponent = match ¤cy { + Currency::Token(currency) => currency.decimals(), + Currency::NativeCurrency(currency) => currency.decimals(), + }; Self { - currency, - decimal_scale, - fraction, + numerator, + denominator, + meta: CurrencyMeta { + currency, + decimal_scale: BigUint::from(10u64).pow(exponent), + }, } } - pub fn from_raw_amount(currency: Currency, raw_amount: BigInt) -> CurrencyAmount { - let decimal_scale = BigUint::from(10_u32.pow(18_u32)); - let fraction = Fraction { - numerator: raw_amount.clone(), - denominator: BigInt::from(1), - }; + /// Returns a new currency amount instance from the unitless amount of token, i.e. the raw amount + /// + /// # Arguments + /// + /// * `currency`: the currency in the amount + /// * `raw_amount`: the raw token or ether amount + /// + /// returns: CurrencyAmount + /// + pub fn from_raw_amount( + currency: Currency<'a>, + raw_amount: impl Into, + ) -> CurrencyAmount { + Self::new(currency, raw_amount, 1) + } + + /// Construct a currency amount with a denominator that is not equal to 1 + /// + /// # Arguments + /// + /// * `currency`: the currency + /// * `numerator`: the numerator of the fractional token amount + /// * `denominator`: the denominator of the fractional token amount + /// + /// returns: CurrencyAmount + /// + pub fn from_fractional_amount( + currency: Currency<'a>, + numerator: impl Into, + denominator: impl Into, + ) -> CurrencyAmount { + Self::new(currency, numerator, denominator) + } + + pub fn multiply(&self, other: impl FractionTrait) -> Self { + let multiplied = self.as_fraction().multiply(&other.as_fraction()); + Self::from_fractional_amount( + self.meta.currency.clone(), + multiplied.numerator().clone(), + multiplied.denominator().clone(), + ) + } + + pub fn divide(&self, other: impl FractionTrait) -> Self { + let divided = self.as_fraction().divide(&other.as_fraction()); + Self::from_fractional_amount( + self.meta.currency.clone(), + divided.numerator().clone(), + divided.denominator().clone(), + ) + } + + pub fn to_exact(&self) -> String { + Decimal::from_str(&self.quotient().to_str_radix(10)) + .unwrap() + .div(Decimal::from_str(&self.meta.decimal_scale.to_str_radix(10)).unwrap()) + .to_string() + } + + pub fn wrapped(&self) -> CurrencyAmount { + match &self.meta.currency { + Currency::NativeCurrency(native_currency) => Self::from_fractional_amount( + Currency::Token(native_currency.wrapped()), + self.numerator.clone(), + self.denominator.clone(), + ), + Currency::Token(_) => self.clone(), + } + } +} +impl<'a> FractionTrait> for CurrencyAmount<'a> { + fn new( + numerator: impl Into, + denominator: impl Into, + meta: CurrencyMeta<'a>, + ) -> Self { Self { - currency, - decimal_scale: Some(decimal_scale), - fraction: Some(fraction), + numerator: numerator.into(), + denominator: denominator.into(), + meta, } } - pub fn from_fractional_amount( - currency: Currency, - numerator: BigInt, - denominator: BigInt, - ) -> CurrencyAmount { - CurrencyAmount::new(currency, None, Some(Fraction::new(numerator, denominator))) + fn meta(&self) -> CurrencyMeta<'a> { + self.meta.clone() } - pub fn add(&self, other: CurrencyAmount) -> CurrencyAmount { - assert!(self.currency == other.currency, "CURRENCY SHOULD EQUAL"); - let add = Fraction::add(&self.fraction.clone().unwrap(), &other.fraction.unwrap()); - CurrencyAmount::from_fractional_amount(other.currency, add.numerator, add.denominator) + fn numerator(&self) -> &BigInt { + &self.numerator } - pub fn subtract(&self, other: CurrencyAmount) -> CurrencyAmount { - assert!(self.currency == other.currency, "CURRENCY SHOULD EQUAL"); - let sub = Fraction::subtract(&self.fraction.clone().unwrap(), &other.fraction.unwrap()); - CurrencyAmount::from_fractional_amount(other.currency, sub.numerator, sub.denominator) + fn denominator(&self) -> &BigInt { + &self.denominator } - pub fn multiply(&self, other: CurrencyAmount) -> CurrencyAmount { - let multiplied = - Fraction::multiply(&self.fraction.clone().unwrap(), &other.fraction.unwrap()); - CurrencyAmount::from_fractional_amount( - other.currency, - multiplied.numerator, - multiplied.denominator, + fn add(&self, other: &Self) -> Self { + assert!(self.meta.currency.equals(&other.meta.currency), "CURRENCY"); + let added = self.as_fraction().add(&other.as_fraction()); + Self::from_fractional_amount( + self.meta.currency.clone(), + added.numerator().clone(), + added.denominator().clone(), ) } - pub fn divide(&self, other: CurrencyAmount) -> CurrencyAmount { - let divided = Fraction::divide(&self.fraction.clone().unwrap(), &other.fraction.unwrap()); - CurrencyAmount::from_fractional_amount( - other.currency, - divided.numerator, - divided.denominator, + fn subtract(&self, other: &Self) -> Self { + assert!(self.meta.currency.equals(&other.meta.currency), "CURRENCY"); + let subtracted = self.as_fraction().subtract(&other.as_fraction()); + Self::from_fractional_amount( + self.meta.currency.clone(), + subtracted.numerator().clone(), + subtracted.denominator().clone(), ) } - pub fn to_significant(&self) -> String { - Fraction::to_significant(&self.fraction.clone().unwrap(), 6, Rounding::RoundUp) + fn to_significant(&self, significant_digits: u32, rounding: Rounding) -> String { + self.as_fraction() + .divide(&Fraction::new( + self.meta.decimal_scale.to_bigint().unwrap(), + 1, + (), + )) + .to_significant(significant_digits, rounding) } - pub fn to_fixed(&self, decimals: BigUint) -> String { - assert!(decimals <= self.decimal_scale.clone().unwrap(), "DECIMALS"); - Fraction::to_fixed( - &self.fraction.clone().unwrap(), - decimals.to_u32().unwrap(), - Rounding::RoundUp, - ) + fn to_fixed(&self, decimal_places: u32, rounding: Rounding) -> String { + assert!(decimal_places <= self.meta.currency.decimals(), "DECIMALS"); + self.as_fraction() + .divide(&Fraction::new( + self.meta.decimal_scale.to_bigint().unwrap(), + 1, + (), + )) + .to_fixed(decimal_places, rounding) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::entities::{ether::Ether, fractions::percent::Percent, token::Token}; + + const ADDRESS_ONE: &str = "0x0000000000000000000000000000000000000001"; + + fn token18() -> Currency<'static> { + Currency::Token(Token::new( + 1, + ADDRESS_ONE.to_string(), + 18, + None, + None, + None, + None, + )) } - //Implementation not done yet - pub fn to_exact() {} + fn token0() -> Currency<'static> { + Currency::Token(Token::new( + 1, + ADDRESS_ONE.to_string(), + 0, + None, + None, + None, + None, + )) + } + + #[test] + fn test_constructor() { + let amount = CurrencyAmount::from_raw_amount(token18(), 100); + assert_eq!(amount.quotient(), 100.into()); + } + + #[test] + fn test_quotient() { + let amount = + CurrencyAmount::from_raw_amount(token18(), 100).multiply(Percent::new(15, 100, ())); + assert_eq!(amount.quotient(), BigInt::from(15)); + } + + #[test] + fn test_ether() { + let ether = Ether::on_chain(1); + let amount = CurrencyAmount::from_raw_amount(Currency::NativeCurrency(ðer), 100); + assert_eq!(amount.quotient(), 100.into()); + assert!(amount + .meta + .currency + .equals(&Currency::NativeCurrency(ðer))); + } + + #[test] + fn test_token_amount_max_uint256() { + let amount = CurrencyAmount::from_raw_amount(token18(), MAX_UINT256.clone()); + assert_eq!(amount.quotient(), MAX_UINT256.clone()); + } + + #[test] + #[should_panic(expected = "AMOUNT")] + fn test_token_amount_exceeds_max_uint256() { + let _ = CurrencyAmount::from_raw_amount(token18(), MAX_UINT256.clone() + 1); + } + + #[test] + #[should_panic(expected = "AMOUNT")] + fn test_token_amount_quotient_exceeds_max_uint256() { + let numerator: BigInt = (MAX_UINT256.clone() + 1) * 2; + let _ = CurrencyAmount::from_fractional_amount(token18(), numerator, 2); + } + + #[test] + fn test_token_amount_numerator_gt_uint256() { + let numerator: BigInt = MAX_UINT256.clone() + 2; + let amount = CurrencyAmount::from_fractional_amount(token18(), numerator.clone(), 2); + assert_eq!(amount.numerator(), &numerator); + } + + #[test] + #[should_panic(expected = "DECIMALS")] + fn to_fixed_decimals_exceeds_currency_decimals() { + let amount = CurrencyAmount::from_raw_amount(token0(), 1000); + let _ = amount.to_fixed(3, Rounding::RoundDown); + } + + #[test] + fn to_fixed_0_decimals() { + let amount = CurrencyAmount::from_raw_amount(token0(), 123456); + assert_eq!(amount.to_fixed(0, Rounding::RoundDown), "123456"); + } + + #[test] + fn to_fixed_18_decimals() { + let amount = CurrencyAmount::from_raw_amount(token18(), 1e15 as i64); + assert_eq!(amount.to_fixed(9, Rounding::RoundDown), "0.001000000"); + } + + #[test] + fn to_significant_does_not_throw() { + let amount = CurrencyAmount::from_raw_amount(token0(), 1000); + assert_eq!(amount.to_significant(3, Rounding::RoundDown), "1000"); + } + + #[test] + fn to_significant_0_decimals() { + let amount = CurrencyAmount::from_raw_amount(token0(), 123456); + assert_eq!(amount.to_significant(4, Rounding::RoundDown), "123400"); + } + + #[test] + fn to_significant_18_decimals() { + let amount = CurrencyAmount::from_raw_amount(token18(), 1e15 as i64); + assert_eq!(amount.to_significant(9, Rounding::RoundDown), "0.001"); + } + + #[test] + fn to_exact_does_not_throw() { + let amount = CurrencyAmount::from_raw_amount(token0(), 1000); + assert_eq!(amount.to_exact(), "1000"); + } + + #[test] + fn to_exact_0_decimals() { + let amount = CurrencyAmount::from_raw_amount(token0(), 123456); + assert_eq!(amount.to_exact(), "123456"); + } - // pub fn wrapped(&self) -> CurrencyAmount { - // if Token::is_token() {//don't really understand this part, but is_token will always return true anyway - // self.clone() - // } else { - // let wrapped = Token::wrapped(Currency::Token(Token)); - // } - // } + #[test] + fn to_exact_18_decimals() { + let amount = CurrencyAmount::from_raw_amount(token18(), 123e13 as i64); + assert_eq!(amount.to_exact(), "0.00123"); + } } From c1f8604e26cc5e5e738c6485f4fa73035bc0109e Mon Sep 17 00:00:00 2001 From: Shuhui Luo Date: Sun, 24 Dec 2023 15:57:33 -0800 Subject: [PATCH 07/13] add ci --- .github/workflows/rust.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..5e81d31 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,37 @@ +name: Rust + +on: + push: + branches: + - master + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: Rust Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Cache Cargo registry + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-registry- + + - name: Build + run: cargo build --verbose + + - name: Run tests + run: cargo test --verbose From 985fba004362b86f15ff8b4c42007f7bc0bf8b2b Mon Sep 17 00:00:00 2001 From: Shuhui Luo Date: Sun, 24 Dec 2023 18:04:03 -0800 Subject: [PATCH 08/13] factor out `CurrencyTrait` and refactor --- src/entities/base_currency.rs | 6 +- src/entities/currency.rs | 35 ++++++++-- src/entities/ether.rs | 17 ++--- src/entities/fractions/currency_amount.rs | 82 +++++++++++------------ src/entities/mod.rs | 1 - src/entities/native_currency.rs | 3 - src/entities/token.rs | 13 ++-- 7 files changed, 86 insertions(+), 71 deletions(-) delete mode 100644 src/entities/native_currency.rs diff --git a/src/entities/base_currency.rs b/src/entities/base_currency.rs index c719b73..20d17e9 100644 --- a/src/entities/base_currency.rs +++ b/src/entities/base_currency.rs @@ -1,7 +1,7 @@ -use super::{currency::Currency, token::Token}; +use super::{currency::CurrencyTrait, token::Token}; /// A currency is any fungible financial instrument, including Ether, all ERC20 tokens, and other chain-native currencies -pub trait BaseCurrency { +pub trait BaseCurrency: Clone { /// The chain ID on which this currency resides fn chain_id(&self) -> u32; @@ -20,7 +20,7 @@ pub trait BaseCurrency { /// /// * `other`: the other currency /// - fn equals(&self, other: &Currency) -> bool; + fn equals(&self, other: &impl CurrencyTrait) -> bool; /// Return the wrapped version of this currency that can be used with the Uniswap contracts. /// Currencies must implement this to be used in Uniswap diff --git a/src/entities/currency.rs b/src/entities/currency.rs index 53d227b..f81b4d3 100644 --- a/src/entities/currency.rs +++ b/src/entities/currency.rs @@ -1,12 +1,35 @@ -use super::{base_currency::BaseCurrency, native_currency::NativeCurrency, token::Token}; +use super::{base_currency::BaseCurrency, ether::Ether, token::Token}; -#[derive(Clone)] -pub enum Currency<'a> { - NativeCurrency(&'a dyn NativeCurrency), +#[derive(Clone, PartialEq)] +pub enum Currency { + NativeCurrency(Ether), Token(Token), } -impl BaseCurrency for Currency<'_> { +pub trait CurrencyTrait: BaseCurrency { + /// Returns whether the currency is native to the chain and must be wrapped (e.g. Ether) + fn is_native(&self) -> bool; + + fn address(&self) -> String; +} + +impl CurrencyTrait for Currency { + fn is_native(&self) -> bool { + match self { + Currency::NativeCurrency(_) => true, + Currency::Token(_) => false, + } + } + + fn address(&self) -> String { + match self { + Currency::NativeCurrency(native_currency) => native_currency.wrapped().address, + Currency::Token(token) => token.address.to_string(), + } + } +} + +impl BaseCurrency for Currency { fn chain_id(&self) -> u32 { match self { Currency::NativeCurrency(native_currency) => native_currency.chain_id(), @@ -35,7 +58,7 @@ impl BaseCurrency for Currency<'_> { } } - fn equals(&self, other: &Currency) -> bool { + fn equals(&self, other: &impl CurrencyTrait) -> bool { match self { Currency::NativeCurrency(native_currency) => native_currency.equals(other), Currency::Token(token) => token.equals(other), diff --git a/src/entities/ether.rs b/src/entities/ether.rs index 4d383ce..56c9b3b 100644 --- a/src/entities/ether.rs +++ b/src/entities/ether.rs @@ -1,6 +1,4 @@ -use super::{ - base_currency::BaseCurrency, currency::Currency, native_currency::NativeCurrency, token::Token, -}; +use super::{base_currency::BaseCurrency, currency::CurrencyTrait, token::Token}; use lazy_static::lazy_static; use std::{collections::HashMap, sync::Mutex}; @@ -40,8 +38,6 @@ impl Ether { } } -impl NativeCurrency for Ether {} - impl BaseCurrency for Ether { fn chain_id(&self) -> u32 { self.chain_id @@ -59,9 +55,9 @@ impl BaseCurrency for Ether { self.name.clone() } - fn equals(&self, other: &Currency) -> bool { - match other { - Currency::NativeCurrency(other) => self.chain_id() == other.chain_id(), + fn equals(&self, other: &impl CurrencyTrait) -> bool { + match other.is_native() { + true => self.chain_id() == other.chain_id(), _ => false, } } @@ -74,6 +70,7 @@ impl BaseCurrency for Ether { #[cfg(test)] mod tests { use super::*; + use crate::entities::currency::Currency; #[test] fn test_static_constructor_uses_cache() { @@ -87,11 +84,11 @@ mod tests { #[test] fn test_equals_returns_false_for_different_chains() { - assert!(!Ether::on_chain(1).equals(&Currency::NativeCurrency(&Ether::on_chain(2)))); + assert!(!Ether::on_chain(1).equals(&Currency::NativeCurrency(Ether::on_chain(2)))); } #[test] fn test_equals_returns_true_for_same_chains() { - assert!(Ether::on_chain(1).equals(&Currency::NativeCurrency(&Ether::on_chain(1)))); + assert!(Ether::on_chain(1).equals(&Currency::NativeCurrency(Ether::on_chain(1)))); } } diff --git a/src/entities/fractions/currency_amount.rs b/src/entities/fractions/currency_amount.rs index fa3551d..b181076 100644 --- a/src/entities/fractions/currency_amount.rs +++ b/src/entities/fractions/currency_amount.rs @@ -11,22 +11,22 @@ use num_integer::Integer; use rust_decimal::Decimal; use std::{ops::Div, str::FromStr}; -#[derive(Clone)] -pub struct CurrencyAmount<'a> { +#[derive(Clone, PartialEq)] +pub struct CurrencyAmount { numerator: BigInt, denominator: BigInt, - pub meta: CurrencyMeta<'a>, + pub meta: CurrencyMeta, } -#[derive(Clone)] -pub struct CurrencyMeta<'a> { - pub currency: Currency<'a>, +#[derive(Clone, PartialEq)] +pub struct CurrencyMeta { + pub currency: Currency, pub decimal_scale: BigUint, } -impl<'a> CurrencyAmount<'a> { +impl CurrencyAmount { fn new( - currency: Currency<'a>, + currency: Currency, numerator: impl Into, denominator: impl Into, ) -> Self { @@ -56,10 +56,7 @@ impl<'a> CurrencyAmount<'a> { /// /// returns: CurrencyAmount /// - pub fn from_raw_amount( - currency: Currency<'a>, - raw_amount: impl Into, - ) -> CurrencyAmount { + pub fn from_raw_amount(currency: Currency, raw_amount: impl Into) -> CurrencyAmount { Self::new(currency, raw_amount, 1) } @@ -74,7 +71,7 @@ impl<'a> CurrencyAmount<'a> { /// returns: CurrencyAmount /// pub fn from_fractional_amount( - currency: Currency<'a>, + currency: Currency, numerator: impl Into, denominator: impl Into, ) -> CurrencyAmount { @@ -118,11 +115,11 @@ impl<'a> CurrencyAmount<'a> { } } -impl<'a> FractionTrait> for CurrencyAmount<'a> { +impl FractionTrait for CurrencyAmount { fn new( numerator: impl Into, denominator: impl Into, - meta: CurrencyMeta<'a>, + meta: CurrencyMeta, ) -> Self { Self { numerator: numerator.into(), @@ -131,7 +128,7 @@ impl<'a> FractionTrait> for CurrencyAmount<'a> { } } - fn meta(&self) -> CurrencyMeta<'a> { + fn meta(&self) -> CurrencyMeta { self.meta.clone() } @@ -189,11 +186,12 @@ impl<'a> FractionTrait> for CurrencyAmount<'a> { mod tests { use super::*; use crate::entities::{ether::Ether, fractions::percent::Percent, token::Token}; + use lazy_static::lazy_static; const ADDRESS_ONE: &str = "0x0000000000000000000000000000000000000001"; - fn token18() -> Currency<'static> { - Currency::Token(Token::new( + lazy_static! { + static ref TOKEN18: Currency = Currency::Token(Token::new( 1, ADDRESS_ONE.to_string(), 18, @@ -201,11 +199,8 @@ mod tests { None, None, None, - )) - } - - fn token0() -> Currency<'static> { - Currency::Token(Token::new( + )); + static ref TOKEN0: Currency = Currency::Token(Token::new( 1, ADDRESS_ONE.to_string(), 0, @@ -213,111 +208,114 @@ mod tests { None, None, None, - )) + )); } #[test] fn test_constructor() { - let amount = CurrencyAmount::from_raw_amount(token18(), 100); + let amount = CurrencyAmount::from_raw_amount(TOKEN18.clone(), 100); assert_eq!(amount.quotient(), 100.into()); } #[test] fn test_quotient() { - let amount = - CurrencyAmount::from_raw_amount(token18(), 100).multiply(Percent::new(15, 100, ())); + let amount = CurrencyAmount::from_raw_amount(TOKEN18.clone(), 100).multiply(Percent::new( + 15, + 100, + (), + )); assert_eq!(amount.quotient(), BigInt::from(15)); } #[test] fn test_ether() { let ether = Ether::on_chain(1); - let amount = CurrencyAmount::from_raw_amount(Currency::NativeCurrency(ðer), 100); + let amount = CurrencyAmount::from_raw_amount(Currency::NativeCurrency(ether.clone()), 100); assert_eq!(amount.quotient(), 100.into()); assert!(amount .meta .currency - .equals(&Currency::NativeCurrency(ðer))); + .equals(&Currency::NativeCurrency(ether.clone()))); } #[test] fn test_token_amount_max_uint256() { - let amount = CurrencyAmount::from_raw_amount(token18(), MAX_UINT256.clone()); + let amount = CurrencyAmount::from_raw_amount(TOKEN18.clone(), MAX_UINT256.clone()); assert_eq!(amount.quotient(), MAX_UINT256.clone()); } #[test] #[should_panic(expected = "AMOUNT")] fn test_token_amount_exceeds_max_uint256() { - let _ = CurrencyAmount::from_raw_amount(token18(), MAX_UINT256.clone() + 1); + let _ = CurrencyAmount::from_raw_amount(TOKEN18.clone(), MAX_UINT256.clone() + 1); } #[test] #[should_panic(expected = "AMOUNT")] fn test_token_amount_quotient_exceeds_max_uint256() { let numerator: BigInt = (MAX_UINT256.clone() + 1) * 2; - let _ = CurrencyAmount::from_fractional_amount(token18(), numerator, 2); + let _ = CurrencyAmount::from_fractional_amount(TOKEN18.clone(), numerator, 2); } #[test] fn test_token_amount_numerator_gt_uint256() { let numerator: BigInt = MAX_UINT256.clone() + 2; - let amount = CurrencyAmount::from_fractional_amount(token18(), numerator.clone(), 2); + let amount = CurrencyAmount::from_fractional_amount(TOKEN18.clone(), numerator.clone(), 2); assert_eq!(amount.numerator(), &numerator); } #[test] #[should_panic(expected = "DECIMALS")] fn to_fixed_decimals_exceeds_currency_decimals() { - let amount = CurrencyAmount::from_raw_amount(token0(), 1000); + let amount = CurrencyAmount::from_raw_amount(TOKEN0.clone(), 1000); let _ = amount.to_fixed(3, Rounding::RoundDown); } #[test] fn to_fixed_0_decimals() { - let amount = CurrencyAmount::from_raw_amount(token0(), 123456); + let amount = CurrencyAmount::from_raw_amount(TOKEN0.clone(), 123456); assert_eq!(amount.to_fixed(0, Rounding::RoundDown), "123456"); } #[test] fn to_fixed_18_decimals() { - let amount = CurrencyAmount::from_raw_amount(token18(), 1e15 as i64); + let amount = CurrencyAmount::from_raw_amount(TOKEN18.clone(), 1e15 as i64); assert_eq!(amount.to_fixed(9, Rounding::RoundDown), "0.001000000"); } #[test] fn to_significant_does_not_throw() { - let amount = CurrencyAmount::from_raw_amount(token0(), 1000); + let amount = CurrencyAmount::from_raw_amount(TOKEN0.clone(), 1000); assert_eq!(amount.to_significant(3, Rounding::RoundDown), "1000"); } #[test] fn to_significant_0_decimals() { - let amount = CurrencyAmount::from_raw_amount(token0(), 123456); + let amount = CurrencyAmount::from_raw_amount(TOKEN0.clone(), 123456); assert_eq!(amount.to_significant(4, Rounding::RoundDown), "123400"); } #[test] fn to_significant_18_decimals() { - let amount = CurrencyAmount::from_raw_amount(token18(), 1e15 as i64); + let amount = CurrencyAmount::from_raw_amount(TOKEN18.clone(), 1e15 as i64); assert_eq!(amount.to_significant(9, Rounding::RoundDown), "0.001"); } #[test] fn to_exact_does_not_throw() { - let amount = CurrencyAmount::from_raw_amount(token0(), 1000); + let amount = CurrencyAmount::from_raw_amount(TOKEN0.clone(), 1000); assert_eq!(amount.to_exact(), "1000"); } #[test] fn to_exact_0_decimals() { - let amount = CurrencyAmount::from_raw_amount(token0(), 123456); + let amount = CurrencyAmount::from_raw_amount(TOKEN0.clone(), 123456); assert_eq!(amount.to_exact(), "123456"); } #[test] fn to_exact_18_decimals() { - let amount = CurrencyAmount::from_raw_amount(token18(), 123e13 as i64); + let amount = CurrencyAmount::from_raw_amount(TOKEN18.clone(), 123e13 as i64); assert_eq!(amount.to_exact(), "0.00123"); } } diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 6e0c6df..fbd5c8a 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -2,6 +2,5 @@ pub mod base_currency; pub mod currency; pub mod ether; pub mod fractions; -pub mod native_currency; pub mod token; pub mod weth9; diff --git a/src/entities/native_currency.rs b/src/entities/native_currency.rs deleted file mode 100644 index d32ed76..0000000 --- a/src/entities/native_currency.rs +++ /dev/null @@ -1,3 +0,0 @@ -use super::base_currency::BaseCurrency; - -pub trait NativeCurrency: BaseCurrency {} diff --git a/src/entities/token.rs b/src/entities/token.rs index 7aca063..c5d6bf5 100644 --- a/src/entities/token.rs +++ b/src/entities/token.rs @@ -1,4 +1,4 @@ -use super::{base_currency::BaseCurrency, currency::Currency}; +use super::{base_currency::BaseCurrency, currency::CurrencyTrait}; use num_bigint::BigUint; /// Represents an ERC20 token with a unique address and some metadata. @@ -38,11 +38,11 @@ impl BaseCurrency for Token { /// /// returns: bool /// - fn equals(&self, other: &Currency) -> bool { - match other { - Currency::Token(other) => { - self.chain_id == other.chain_id - && self.address.to_lowercase() == other.address.to_lowercase() + fn equals(&self, other: &impl CurrencyTrait) -> bool { + match other.is_native() { + false => { + self.chain_id == other.chain_id() + && self.address.to_lowercase() == other.address().to_lowercase() } _ => false, } @@ -97,6 +97,7 @@ impl Token { #[cfg(test)] mod tests { + use crate::entities::currency::Currency; //should test for neg chain_id or neg decimals or neg buy_fee or neg sell_fee, but the compiler will panic by itself, so no need use super::*; From 81c28e6e5a9f7f334b4c248f805a96e23d44be3d Mon Sep 17 00:00:00 2001 From: Shuhui Luo Date: Sun, 24 Dec 2023 18:49:20 -0800 Subject: [PATCH 09/13] make `CurrencyAmount` generic --- src/entities/fractions/currency_amount.rs | 53 ++++++++++------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/src/entities/fractions/currency_amount.rs b/src/entities/fractions/currency_amount.rs index b181076..b279e85 100644 --- a/src/entities/fractions/currency_amount.rs +++ b/src/entities/fractions/currency_amount.rs @@ -2,7 +2,7 @@ use crate::{ constants::{Rounding, MAX_UINT256}, entities::{ base_currency::BaseCurrency, - currency::Currency, + currency::{Currency, CurrencyTrait}, fractions::fraction::{Fraction, FractionTrait}, }, }; @@ -12,31 +12,24 @@ use rust_decimal::Decimal; use std::{ops::Div, str::FromStr}; #[derive(Clone, PartialEq)] -pub struct CurrencyAmount { +pub struct CurrencyAmount { numerator: BigInt, denominator: BigInt, - pub meta: CurrencyMeta, + pub meta: CurrencyMeta, } #[derive(Clone, PartialEq)] -pub struct CurrencyMeta { - pub currency: Currency, +pub struct CurrencyMeta { + pub currency: T, pub decimal_scale: BigUint, } -impl CurrencyAmount { - fn new( - currency: Currency, - numerator: impl Into, - denominator: impl Into, - ) -> Self { +impl CurrencyAmount { + fn new(currency: T, numerator: impl Into, denominator: impl Into) -> Self { let numerator = numerator.into(); let denominator = denominator.into(); assert!(numerator.div_floor(&denominator).le(&MAX_UINT256), "AMOUNT"); - let exponent = match ¤cy { - Currency::Token(currency) => currency.decimals(), - Currency::NativeCurrency(currency) => currency.decimals(), - }; + let exponent = currency.decimals(); Self { numerator, denominator, @@ -56,7 +49,7 @@ impl CurrencyAmount { /// /// returns: CurrencyAmount /// - pub fn from_raw_amount(currency: Currency, raw_amount: impl Into) -> CurrencyAmount { + pub fn from_raw_amount(currency: T, raw_amount: impl Into) -> CurrencyAmount { Self::new(currency, raw_amount, 1) } @@ -71,14 +64,14 @@ impl CurrencyAmount { /// returns: CurrencyAmount /// pub fn from_fractional_amount( - currency: Currency, + currency: T, numerator: impl Into, denominator: impl Into, - ) -> CurrencyAmount { + ) -> CurrencyAmount { Self::new(currency, numerator, denominator) } - pub fn multiply(&self, other: impl FractionTrait) -> Self { + pub fn multiply(&self, other: &impl FractionTrait) -> Self { let multiplied = self.as_fraction().multiply(&other.as_fraction()); Self::from_fractional_amount( self.meta.currency.clone(), @@ -87,7 +80,7 @@ impl CurrencyAmount { ) } - pub fn divide(&self, other: impl FractionTrait) -> Self { + pub fn divide(&self, other: &impl FractionTrait) -> Self { let divided = self.as_fraction().divide(&other.as_fraction()); Self::from_fractional_amount( self.meta.currency.clone(), @@ -102,24 +95,26 @@ impl CurrencyAmount { .div(Decimal::from_str(&self.meta.decimal_scale.to_str_radix(10)).unwrap()) .to_string() } +} - pub fn wrapped(&self) -> CurrencyAmount { - match &self.meta.currency { - Currency::NativeCurrency(native_currency) => Self::from_fractional_amount( - Currency::Token(native_currency.wrapped()), +impl CurrencyAmount { + pub fn wrapped(&self) -> CurrencyAmount { + match &self.meta.currency.is_native() { + true => Self::from_fractional_amount( + Currency::Token(self.meta.currency.wrapped()), self.numerator.clone(), self.denominator.clone(), ), - Currency::Token(_) => self.clone(), + false => self.clone(), } } } -impl FractionTrait for CurrencyAmount { +impl FractionTrait> for CurrencyAmount { fn new( numerator: impl Into, denominator: impl Into, - meta: CurrencyMeta, + meta: CurrencyMeta, ) -> Self { Self { numerator: numerator.into(), @@ -128,7 +123,7 @@ impl FractionTrait for CurrencyAmount { } } - fn meta(&self) -> CurrencyMeta { + fn meta(&self) -> CurrencyMeta { self.meta.clone() } @@ -219,7 +214,7 @@ mod tests { #[test] fn test_quotient() { - let amount = CurrencyAmount::from_raw_amount(TOKEN18.clone(), 100).multiply(Percent::new( + let amount = CurrencyAmount::from_raw_amount(TOKEN18.clone(), 100).multiply(&Percent::new( 15, 100, (), From 974df6d11e185aaca5ee5fcf7a80c61371886e8a Mon Sep 17 00:00:00 2001 From: Shuhui Luo Date: Sun, 24 Dec 2023 18:50:02 -0800 Subject: [PATCH 10/13] impl `Price` --- src/entities/fractions/price.rs | 227 ++++++++++++++++++++++++-------- 1 file changed, 170 insertions(+), 57 deletions(-) diff --git a/src/entities/fractions/price.rs b/src/entities/fractions/price.rs index 74e8836..337dd0d 100644 --- a/src/entities/fractions/price.rs +++ b/src/entities/fractions/price.rs @@ -1,65 +1,178 @@ -// use super::{ -// currency_amount::CurrencyAmount, -// fraction::{Fraction, Rounding}, -// }; -// use num_bigint::BigInt; +use crate::{ + constants::Rounding, + entities::{ + currency::CurrencyTrait, + fractions::{ + currency_amount::CurrencyAmount, + fraction::{Fraction, FractionTrait}, + }, + }, +}; +use num_bigint::BigInt; -// pub trait Currency { -// fn decimals(&self) -> u32; -// } +#[derive(Clone)] +pub struct Price +where + TBase: CurrencyTrait, + TQuote: CurrencyTrait, +{ + numerator: BigInt, + denominator: BigInt, + pub meta: PriceMeta, +} -// #[derive(Clone, PartialEq)] +#[derive(Clone)] +pub struct PriceMeta +where + TBase: CurrencyTrait, + TQuote: CurrencyTrait, +{ + pub base_currency: TBase, + pub quote_currency: TQuote, + pub scalar: Fraction, +} -// pub struct Price { -// pub base_currency: TBase, -// pub quote_currency: TQuote, -// pub numerator: BigInt, -// pub denominator: BigInt, -// pub scalar: Fraction, -// } +impl FractionTrait> for Price +where + TBase: CurrencyTrait, + TQuote: CurrencyTrait, +{ + fn new( + numerator: impl Into, + denominator: impl Into, + meta: PriceMeta, + ) -> Self { + Self { + numerator: numerator.into(), + denominator: denominator.into(), + meta, + } + } -// impl Price { -// pub fn new( -// base_currency: TBase, -// quote_currency: TQuote, -// numerator: BigInt, -// denominator: BigInt, -// ) -> Self { -// let scalar = Fraction::new( -// BigInt::from(10).pow(base_currency.decimals() as u32), -// BigInt::from(10).pow(quote_currency.decimals() as u32), -// ); + fn meta(&self) -> PriceMeta { + self.meta.clone() + } -// Self { -// base_currency, -// quote_currency, -// numerator, -// denominator, -// scalar, -// } -// } + fn numerator(&self) -> &BigInt { + &self.numerator + } -// /** -// * Flip the price, switching the base and quote currency -// */ -// pub fn invert(self) -> Price { -// Price::new( -// self.base_currency, -// self.quote_currency, -// self.numerator, -// self.denominator, -// ) -// } + fn denominator(&self) -> &BigInt { + &self.denominator + } -// /** -// * Multiply the price by another price, returning a new price. The other price must have the same base currency as this price's quote currency -// * @param other the other price -// */ -// pub fn multiply(&self, other: Price) -> Price { -// assert!(self.quote_currency == other.base_currency, "TOKEN"); -// let fract = Fraction::multiply(&self, other); -// Price::new(self.base_currency.clone(), other.quote_currency.clone(), fract.numerator, fract.denominator) -// } + fn to_significant(&self, significant_digits: u32, rounding: Rounding) -> String { + self.adjusted_for_decimals() + .to_significant(significant_digits, rounding) + } -// // Methods toSignificant and toFixed would be implemented here -// } + fn to_fixed(&self, decimal_places: u32, rounding: Rounding) -> String { + self.adjusted_for_decimals() + .to_fixed(decimal_places, rounding) + } +} + +impl Price +where + TBase: CurrencyTrait, + TQuote: CurrencyTrait, +{ + pub fn new( + base_currency: TBase, + quote_currency: TQuote, + denominator: impl Into, + numerator: impl Into, + ) -> Self { + let scalar = Fraction::new( + BigInt::from(10).pow(base_currency.decimals()), + BigInt::from(10).pow(quote_currency.decimals()), + (), + ); + Self { + numerator: numerator.into(), + denominator: denominator.into(), + meta: PriceMeta { + base_currency, + quote_currency, + scalar, + }, + } + } + + pub fn from_currency_amounts( + base_amount: CurrencyAmount, + quote_amount: CurrencyAmount, + ) -> Self { + let res = quote_amount.divide(&base_amount); + Self::new( + base_amount.meta.currency, + quote_amount.meta.currency, + res.denominator().clone(), + res.numerator().clone(), + ) + } + + /// Flip the price, switching the base and quote currency + pub fn invert(self) -> Price { + Price::new( + self.meta.quote_currency, + self.meta.base_currency, + self.numerator, + self.denominator, + ) + } + + /// Multiply the price by another price, returning a new price. The other price must have the same base currency as this price's quote currency + /// + /// # Arguments + /// + /// * `other`: the other price + /// + /// returns: Price + /// + pub fn multiply( + &self, + other: Price, + ) -> Price { + assert!( + self.meta.quote_currency.equals(&other.meta.base_currency), + "TOKEN" + ); + let fraction = self.as_fraction().multiply(&other.as_fraction()); + Price::new( + self.meta.base_currency.clone(), + other.meta.quote_currency.clone(), + fraction.denominator().clone(), + fraction.numerator().clone(), + ) + } + + /// Return the amount of quote currency corresponding to a given amount of the base currency + /// + /// # Arguments + /// + /// * `currency_amount`: the amount of base currency to quote against the price + /// + /// returns: CurrencyAmount + /// + pub fn quote(&self, currency_amount: CurrencyAmount) -> CurrencyAmount { + assert!( + currency_amount + .meta + .currency + .equals(&self.meta.base_currency), + "TOKEN" + ); + let fraction = self.as_fraction().multiply(¤cy_amount.as_fraction()); + CurrencyAmount::from_fractional_amount( + self.meta.quote_currency.clone(), + fraction.numerator().clone(), + fraction.denominator().clone(), + ) + } + + /// Get the value scaled by decimals for formatting + pub fn adjusted_for_decimals(&self) -> Fraction { + self.as_fraction().multiply(&self.meta.scalar) + } +} From 27ab61b94ffa80c3c03c27d7bd1c4d4799abbb14 Mon Sep 17 00:00:00 2001 From: Shuhui Luo Date: Sun, 24 Dec 2023 19:11:52 -0800 Subject: [PATCH 11/13] test `Price` --- src/entities/fractions/price.rs | 103 ++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/entities/fractions/price.rs b/src/entities/fractions/price.rs index 337dd0d..3abde75 100644 --- a/src/entities/fractions/price.rs +++ b/src/entities/fractions/price.rs @@ -176,3 +176,106 @@ where self.as_fraction().multiply(&self.meta.scalar) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::entities::{base_currency::BaseCurrency, currency::Currency, token::Token}; + use lazy_static::lazy_static; + + const ADDRESS_ZERO: &str = "0x0000000000000000000000000000000000000000"; + const ADDRESS_ONE: &str = "0x0000000000000000000000000000000000000001"; + + lazy_static! { + static ref TOKEN0: Currency = Currency::Token(Token::new( + 1, + ADDRESS_ZERO.to_string(), + 18, + None, + None, + None, + None, + )); + static ref TOKEN0_6: Currency = Currency::Token(Token::new( + 1, + ADDRESS_ZERO.to_string(), + 6, + None, + None, + None, + None, + )); + static ref TOKEN1: Currency = Currency::Token(Token::new( + 1, + ADDRESS_ONE.to_string(), + 18, + None, + None, + None, + None, + )); + } + + #[test] + fn test_constructor_array_format_works() { + let price = Price::new(TOKEN0.clone(), TOKEN1.clone(), 1, 54321); + assert_eq!(price.to_significant(5, Rounding::RoundDown), "54321"); + assert!(price.meta.base_currency.equals(&TOKEN0.clone())); + assert!(price.meta.quote_currency.equals(&TOKEN1.clone())); + } + + #[test] + fn test_constructor_object_format_works() { + let price = Price::from_currency_amounts( + CurrencyAmount::from_raw_amount(TOKEN0.clone(), 1), + CurrencyAmount::from_raw_amount(TOKEN1.clone(), 54321), + ); + assert_eq!(price.to_significant(5, Rounding::RoundDown), "54321"); + assert!(price.meta.base_currency.equals(&TOKEN0.clone())); + assert!(price.meta.quote_currency.equals(&TOKEN1.clone())); + } + + #[test] + fn test_quote_returns_correct_value() { + let price = Price::new(TOKEN0.clone(), TOKEN1.clone(), 1, 5); + assert!(price + .quote(CurrencyAmount::from_raw_amount(TOKEN0.clone(), 10)) + .equal_to(&CurrencyAmount::from_raw_amount(TOKEN1.clone(), 50))); + } + + #[test] + fn test_to_significant_no_decimals() { + let p = Price::new(TOKEN0.clone(), TOKEN1.clone(), 123, 456); + assert_eq!(p.to_significant(4, Rounding::RoundDown), "3.707"); + } + + #[test] + fn test_to_significant_no_decimals_flip_ratio() { + let p = Price::new(TOKEN0.clone(), TOKEN1.clone(), 456, 123); + assert_eq!(p.to_significant(4, Rounding::RoundDown), "0.2697"); + } + + #[test] + fn test_to_significant_with_decimal_difference() { + let p = Price::new(TOKEN0_6.clone(), TOKEN1.clone(), 123, 456); + assert_eq!( + p.to_significant(4, Rounding::RoundDown), + "0.000000000003707" + ); + } + + #[test] + fn test_to_significant_with_decimal_difference_flipped() { + let p = Price::new(TOKEN0_6.clone(), TOKEN1.clone(), 456, 123); + assert_eq!( + p.to_significant(4, Rounding::RoundDown), + "0.0000000000002697" + ); + } + + #[test] + fn test_to_significant_with_decimal_difference_flipped_base_quote_flipped() { + let p = Price::new(TOKEN1.clone(), TOKEN0_6.clone(), 456, 123); + assert_eq!(p.to_significant(4, Rounding::RoundDown), "269700000000"); + } +} From 51af44f87ea7f9abb51832fede8b1b0018e059b8 Mon Sep 17 00:00:00 2001 From: Shuhui Luo Date: Sun, 24 Dec 2023 19:38:05 -0800 Subject: [PATCH 12/13] impl `Ether::wrapped` --- src/entities/ether.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/entities/ether.rs b/src/entities/ether.rs index 56c9b3b..610bb14 100644 --- a/src/entities/ether.rs +++ b/src/entities/ether.rs @@ -1,4 +1,5 @@ use super::{base_currency::BaseCurrency, currency::CurrencyTrait, token::Token}; +use crate::entities::weth9::WETH9; use lazy_static::lazy_static; use std::{collections::HashMap, sync::Mutex}; @@ -63,7 +64,10 @@ impl BaseCurrency for Ether { } fn wrapped(&self) -> Token { - todo!() + match WETH9::default().get(self.chain_id()) { + Some(weth9) => weth9.clone(), + None => panic!("WRAPPED"), + } } } From a1dfa527b16f41590cab24176a6eb90edf4d6418 Mon Sep 17 00:00:00 2001 From: Shuhui Luo Date: Sun, 24 Dec 2023 20:24:19 -0800 Subject: [PATCH 13/13] impl `CurrencyTrait` for `Token` and `Ether` --- src/entities/currency.rs | 4 ++-- src/entities/ether.rs | 10 ++++++++++ src/entities/fractions/currency_amount.rs | 13 ++++++++----- src/entities/token.rs | 10 ++++++++++ 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/entities/currency.rs b/src/entities/currency.rs index f81b4d3..c2c0919 100644 --- a/src/entities/currency.rs +++ b/src/entities/currency.rs @@ -23,8 +23,8 @@ impl CurrencyTrait for Currency { fn address(&self) -> String { match self { - Currency::NativeCurrency(native_currency) => native_currency.wrapped().address, - Currency::Token(token) => token.address.to_string(), + Currency::NativeCurrency(native_currency) => native_currency.address(), + Currency::Token(token) => token.address(), } } } diff --git a/src/entities/ether.rs b/src/entities/ether.rs index 610bb14..5f8fa57 100644 --- a/src/entities/ether.rs +++ b/src/entities/ether.rs @@ -16,6 +16,16 @@ pub struct Ether { pub name: Option, } +impl CurrencyTrait for Ether { + fn is_native(&self) -> bool { + true + } + + fn address(&self) -> String { + self.wrapped().address() + } +} + impl Ether { pub fn new(chain_id: u32) -> Self { Self { diff --git a/src/entities/fractions/currency_amount.rs b/src/entities/fractions/currency_amount.rs index b279e85..43e44c3 100644 --- a/src/entities/fractions/currency_amount.rs +++ b/src/entities/fractions/currency_amount.rs @@ -2,8 +2,9 @@ use crate::{ constants::{Rounding, MAX_UINT256}, entities::{ base_currency::BaseCurrency, - currency::{Currency, CurrencyTrait}, + currency::CurrencyTrait, fractions::fraction::{Fraction, FractionTrait}, + token::Token, }, }; use num_bigint::{BigInt, BigUint, ToBigInt}; @@ -97,11 +98,11 @@ impl CurrencyAmount { } } -impl CurrencyAmount { - pub fn wrapped(&self) -> CurrencyAmount { +impl CurrencyAmount { + pub fn wrapped(&self) -> CurrencyAmount { match &self.meta.currency.is_native() { true => Self::from_fractional_amount( - Currency::Token(self.meta.currency.wrapped()), + self.meta.currency.wrapped(), self.numerator.clone(), self.denominator.clone(), ), @@ -180,7 +181,9 @@ impl FractionTrait> for CurrencyAmount { #[cfg(test)] mod tests { use super::*; - use crate::entities::{ether::Ether, fractions::percent::Percent, token::Token}; + use crate::entities::{ + currency::Currency, ether::Ether, fractions::percent::Percent, token::Token, + }; use lazy_static::lazy_static; const ADDRESS_ONE: &str = "0x0000000000000000000000000000000000000001"; diff --git a/src/entities/token.rs b/src/entities/token.rs index c5d6bf5..4580023 100644 --- a/src/entities/token.rs +++ b/src/entities/token.rs @@ -13,6 +13,16 @@ pub struct Token { pub sell_fee_bps: Option, } +impl CurrencyTrait for Token { + fn is_native(&self) -> bool { + false + } + + fn address(&self) -> String { + self.address.to_string() + } +} + impl BaseCurrency for Token { fn chain_id(&self) -> u32 { self.chain_id