diff --git a/Cargo.lock b/Cargo.lock index 8295459..4984df3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,7 +268,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", "syn_derive", ] @@ -1198,7 +1198,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -1247,9 +1247,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.42" +version = "2.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8" +checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" dependencies = [ "proc-macro2", "quote", @@ -1265,7 +1265,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -1289,22 +1289,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.51" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.51" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -1397,7 +1397,7 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "uniswap-sdk-core-rust" -version = "0.3.0" +version = "0.4.0" dependencies = [ "alloy-primitives", "eth_checksum", @@ -1641,5 +1641,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] diff --git a/Cargo.toml b/Cargo.toml index 900f128..4f6fca3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uniswap-sdk-core-rust" -version = "0.3.0" +version = "0.4.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/entities/base_currency.rs b/src/entities/base_currency.rs index 98a833f..40aa0d1 100644 --- a/src/entities/base_currency.rs +++ b/src/entities/base_currency.rs @@ -1,4 +1,11 @@ -use super::{currency::CurrencyTrait, token::Token}; +#[derive(Clone, PartialEq)] +pub struct CurrencyLike { + pub chain_id: u32, + pub decimals: u8, + pub symbol: Option, + pub name: Option, + pub meta: M, +} /// A currency is any fungible financial instrument, including Ether, all ERC20 tokens, and other chain-native currencies pub trait BaseCurrency: Clone { @@ -13,16 +20,22 @@ pub trait BaseCurrency: Clone { /// The name of the currency, i.e. a descriptive textual non-unique identifier fn name(&self) -> Option; +} + +impl BaseCurrency for CurrencyLike { + fn chain_id(&self) -> u32 { + self.chain_id + } + + fn decimals(&self) -> u8 { + self.decimals + } + + fn symbol(&self) -> Option { + self.symbol.clone() + } - /// Returns whether this currency is functionally equivalent to the other currency - /// - /// # Arguments - /// - /// * `other`: the other currency - /// - 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 - fn wrapped(&self) -> Token; + fn name(&self) -> Option { + self.name.clone() + } } diff --git a/src/entities/currency.rs b/src/entities/currency.rs index c574de5..1d78578 100644 --- a/src/entities/currency.rs +++ b/src/entities/currency.rs @@ -12,6 +12,18 @@ pub trait CurrencyTrait: BaseCurrency { fn is_native(&self) -> bool; fn address(&self) -> Address; + + /// Returns whether this currency is functionally equivalent to the other currency + /// + /// # Arguments + /// + /// * `other`: the other currency + /// + 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 + fn wrapped(&self) -> Token; } impl CurrencyTrait for Currency { @@ -28,6 +40,20 @@ impl CurrencyTrait for Currency { Currency::Token(token) => token.address(), } } + + fn equals(&self, other: &impl CurrencyTrait) -> 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(), + } + } } impl BaseCurrency for Currency { @@ -58,18 +84,53 @@ impl BaseCurrency for Currency { Currency::Token(token) => token.name(), } } +} - fn equals(&self, other: &impl CurrencyTrait) -> bool { - match self { - Currency::NativeCurrency(native_currency) => native_currency.equals(other), - Currency::Token(token) => token.equals(other), - } +#[cfg(test)] +mod tests { + use super::*; + use lazy_static::lazy_static; + + const ADDRESS_ZERO: &str = "0x0000000000000000000000000000000000000000"; + const ADDRESS_ONE: &str = "0x0000000000000000000000000000000000000001"; + + lazy_static! { + static ref TOKEN0: Token = + Token::new(1, ADDRESS_ZERO.to_string(), 18, None, None, None, None,); + static ref TOKEN1: Token = + Token::new(1, ADDRESS_ONE.to_string(), 18, None, None, None, None,); } - fn wrapped(&self) -> Token { - match self { - Currency::NativeCurrency(native_currency) => native_currency.wrapped(), - Currency::Token(token) => token.clone(), - } + #[test] + fn equals_ether_on_same_chains_is_ether() { + assert!(Ether::on_chain(1).equals(&Ether::on_chain(1))); + } + + #[test] + fn equals_ether_is_not_token0() { + assert!(!Ether::on_chain(1).equals(&TOKEN0.clone())); + } + + #[test] + fn equals_token1_is_not_token0() { + assert!(!TOKEN1.equals(&TOKEN0.clone())); + } + + #[test] + fn equals_token0_is_token0() { + assert!(TOKEN0.equals(&TOKEN0.clone())); + } + + #[test] + fn equals_token0_is_equal_to_another_token0() { + assert!(TOKEN0.equals(&Token::new( + 1, + ADDRESS_ZERO.to_owned(), + 18, + Some("symbol".to_owned()), + Some("name".to_owned()), + None, + None + ))); } } diff --git a/src/entities/ether.rs b/src/entities/ether.rs index 7be5211..9440fac 100644 --- a/src/entities/ether.rs +++ b/src/entities/ether.rs @@ -1,4 +1,9 @@ -use super::{base_currency::BaseCurrency, currency::CurrencyTrait, token::Token, weth9::WETH9}; +use super::{ + base_currency::{BaseCurrency, CurrencyLike}, + currency::CurrencyTrait, + token::Token, + weth9::WETH9, +}; use alloy_primitives::Address; use lazy_static::lazy_static; use std::{collections::HashMap, sync::Mutex}; @@ -8,13 +13,7 @@ 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 { - pub chain_id: u32, - pub decimals: u8, - pub symbol: Option, - pub name: Option, -} +pub type Ether = CurrencyLike<()>; impl CurrencyTrait for Ether { fn is_native(&self) -> bool { @@ -24,6 +23,20 @@ impl CurrencyTrait for Ether { fn address(&self) -> Address { self.wrapped().address() } + + fn equals(&self, other: &impl CurrencyTrait) -> bool { + match other.is_native() { + true => self.chain_id() == other.chain_id(), + _ => false, + } + } + + fn wrapped(&self) -> Token { + match WETH9::default().get(self.chain_id()) { + Some(weth9) => weth9.clone(), + None => panic!("WRAPPED"), + } + } } impl Ether { @@ -33,6 +46,7 @@ impl Ether { decimals: 18, symbol: Some("ETH".to_string()), name: Some("Ether".to_string()), + meta: (), } } @@ -49,38 +63,6 @@ impl Ether { } } -impl BaseCurrency for Ether { - fn chain_id(&self) -> u32 { - self.chain_id - } - - fn decimals(&self) -> u8 { - self.decimals - } - - fn symbol(&self) -> Option { - self.symbol.clone() - } - - fn name(&self) -> Option { - self.name.clone() - } - - fn equals(&self, other: &impl CurrencyTrait) -> bool { - match other.is_native() { - true => self.chain_id() == other.chain_id(), - _ => false, - } - } - - fn wrapped(&self) -> Token { - match WETH9::default().get(self.chain_id()) { - Some(weth9) => weth9.clone(), - None => panic!("WRAPPED"), - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/entities/fractions/currency_amount.rs b/src/entities/fractions/currency_amount.rs index ac5fcd4..581c48d 100644 --- a/src/entities/fractions/currency_amount.rs +++ b/src/entities/fractions/currency_amount.rs @@ -1,23 +1,17 @@ use crate::{ constants::{Rounding, MAX_UINT256}, entities::{ - base_currency::BaseCurrency, currency::CurrencyTrait, - fractions::fraction::{Fraction, FractionTrait}, + fractions::fraction::{Fraction, FractionLike, FractionTrait}, token::Token, }, }; -use num_bigint::{BigInt, BigUint, ToBigInt}; +use num_bigint::{BigInt, BigUint}; use num_integer::Integer; use rust_decimal::Decimal; use std::{ops::Div, str::FromStr}; -#[derive(Clone, PartialEq)] -pub struct CurrencyAmount { - numerator: BigInt, - denominator: BigInt, - pub meta: CurrencyMeta, -} +pub type CurrencyAmount = FractionLike>; #[derive(Clone, PartialEq)] pub struct CurrencyMeta { @@ -31,14 +25,14 @@ impl CurrencyAmount { let denominator = denominator.into(); assert!(numerator.div_floor(&denominator).le(&MAX_UINT256), "AMOUNT"); let exponent = currency.decimals(); - Self { + FractionTrait::new( numerator, denominator, - meta: CurrencyMeta { + CurrencyMeta { currency, decimal_scale: BigUint::from(10u64).pow(exponent as u32), }, - } + ) } /// Returns a new currency amount instance from the unitless amount of token, i.e. the raw amount @@ -96,47 +90,8 @@ impl CurrencyAmount { .div(Decimal::from_str(&self.meta.decimal_scale.to_str_radix(10)).unwrap()) .to_string() } -} - -impl CurrencyAmount { - pub fn wrapped(&self) -> CurrencyAmount { - match &self.meta.currency.is_native() { - true => Self::from_fractional_amount( - self.meta.currency.wrapped(), - self.numerator.clone(), - self.denominator.clone(), - ), - false => self.clone(), - } - } -} -impl FractionTrait> for CurrencyAmount { - fn new( - numerator: impl Into, - denominator: impl Into, - meta: CurrencyMeta, - ) -> Self { - Self { - numerator: numerator.into(), - denominator: denominator.into(), - meta, - } - } - - fn meta(&self) -> CurrencyMeta { - self.meta.clone() - } - - fn numerator(&self) -> &BigInt { - &self.numerator - } - - fn denominator(&self) -> &BigInt { - &self.denominator - } - - fn add(&self, other: &Self) -> Self { + pub 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( @@ -146,7 +101,7 @@ impl FractionTrait> for CurrencyAmount { ) } - fn subtract(&self, other: &Self) -> Self { + pub 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( @@ -156,28 +111,33 @@ impl FractionTrait> for CurrencyAmount { ) } - fn to_significant(&self, significant_digits: u8, rounding: Rounding) -> String { + pub fn to_significant(&self, significant_digits: u8, rounding: Rounding) -> String { self.as_fraction() - .divide(&Fraction::new( - self.meta.decimal_scale.to_bigint().unwrap(), - 1, - (), - )) + .divide(&Fraction::new(self.meta.decimal_scale.clone(), 1)) .to_significant(significant_digits, rounding) } - fn to_fixed(&self, decimal_places: u8, rounding: Rounding) -> String { + pub fn to_fixed(&self, decimal_places: u8, 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, - (), - )) + .divide(&Fraction::new(self.meta.decimal_scale.clone(), 1)) .to_fixed(decimal_places, rounding) } } +impl CurrencyAmount { + pub fn wrapped(&self) -> CurrencyAmount { + match &self.meta.currency.is_native() { + true => Self::from_fractional_amount( + self.meta.currency.wrapped(), + self.numerator().clone(), + self.denominator().clone(), + ), + false => self.clone(), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -217,11 +177,8 @@ mod tests { #[test] fn test_quotient() { - let amount = CurrencyAmount::from_raw_amount(TOKEN18.clone(), 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)); } diff --git a/src/entities/fractions/fraction.rs b/src/entities/fractions/fraction.rs index eb65ef0..cb7507c 100644 --- a/src/entities/fractions/fraction.rs +++ b/src/entities/fractions/fraction.rs @@ -5,9 +5,18 @@ use rust_decimal::prelude::*; use std::ops::Div; #[derive(Clone, PartialEq)] -pub struct Fraction { +pub struct FractionLike { numerator: BigInt, denominator: BigInt, + pub meta: M, +} + +pub type Fraction = FractionLike<()>; + +impl Fraction { + pub fn new(numerator: impl Into, denominator: impl Into) -> Self { + FractionTrait::new(numerator, denominator, ()) + } } fn to_rounding_strategy(rounding: Rounding) -> RoundingStrategy { @@ -141,22 +150,23 @@ pub trait FractionTrait: Sized { /// Helper method for converting any super class back to a fraction fn as_fraction(&self) -> Fraction { - Fraction::new(self.numerator().clone(), self.denominator().clone(), ()) + Fraction::new(self.numerator().clone(), self.denominator().clone()) } } -impl FractionTrait<()> for Fraction { - fn new(numerator: impl Into, denominator: impl Into, _: ()) -> Self { +impl FractionTrait for FractionLike { + fn new(numerator: impl Into, denominator: impl Into, meta: M) -> Self { let denominator = denominator.into(); - assert_ne!(denominator, 0.into(), "DENOMINATOR CAN'T BE ZERO"); + assert!(!denominator.is_zero(), "DENOMINATOR CAN'T BE ZERO"); Self { numerator: numerator.into(), denominator, + meta, } } - fn meta(&self) -> () { - () + fn meta(&self) -> M { + self.meta.clone() } fn numerator(&self) -> &BigInt { @@ -174,101 +184,101 @@ mod tests { #[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)); + 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, ()) + assert!(Fraction::new(8, 3) .remainder() - .equal_to(&Fraction::new(2, 3, ()))); - assert!(Fraction::new(12, 4, ()) + .equal_to(&Fraction::new(2, 3))); + assert!(Fraction::new(12, 4) .remainder() - .equal_to(&Fraction::new(0, 4, ()))); - assert!(Fraction::new(16, 5, ()) + .equal_to(&Fraction::new(0, 4))); + assert!(Fraction::new(16, 5) .remainder() - .equal_to(&Fraction::new(1, 5, ()))); + .equal_to(&Fraction::new(1, 5))); } #[test] fn test_invert() { - let fraction = Fraction::new(5, 10, ()).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, ()))); + 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, ()))); + 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, ()))); + 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, ()))); + 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, ()))); + 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, ()))); + 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, ()))); + 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, ()); + 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/entities/fractions/percent.rs b/src/entities/fractions/percent.rs index 71f1208..3816a21 100644 --- a/src/entities/fractions/percent.rs +++ b/src/entities/fractions/percent.rs @@ -1,50 +1,30 @@ -use super::fraction::{Fraction, FractionTrait}; +use super::fraction::{Fraction, FractionLike, 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, ()); + static ref ONE_HUNDRED: Fraction = Fraction::new(100, 1); } -#[derive(Clone, PartialEq)] -pub struct Percent { - numerator: BigInt, - denominator: BigInt, -} - -impl Percent { - /// This boolean prevents a fraction from being interpreted as a Percent - pub const IS_PERCENT: bool = true; -} - -impl FractionTrait<()> for Percent { - fn new(numerator: impl Into, denominator: impl Into, _: ()) -> Self { - Self { - numerator: numerator.into(), - denominator: denominator.into(), - } - } +/// Unit struct to distinguish between a fraction and a percent +#[derive(Clone)] +pub struct IsPercent; - fn meta(&self) -> () { - () - } +pub type Percent = FractionLike; - fn numerator(&self) -> &BigInt { - &self.numerator - } - - fn denominator(&self) -> &BigInt { - &self.denominator +impl Percent { + pub fn new(numerator: impl Into, denominator: impl Into) -> Self { + FractionTrait::new(numerator, denominator, IsPercent) } - fn to_significant(&self, significant_digits: u8, rounding: Rounding) -> String { + pub fn to_significant(&self, significant_digits: u8, rounding: Rounding) -> String { self.as_fraction() .multiply(&ONE_HUNDRED) .to_significant(significant_digits, rounding) } - fn to_fixed(&self, decimal_places: u8, rounding: Rounding) -> String { + pub fn to_fixed(&self, decimal_places: u8, rounding: Rounding) -> String { self.as_fraction() .multiply(&ONE_HUNDRED) .to_fixed(decimal_places, rounding) @@ -58,48 +38,48 @@ mod tests { #[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, ()))); + 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, ()))); + 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, ()))); + 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, ()))); + 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), + Percent::new(154, 10000).to_significant(3, Rounding::RoundHalfUp), "1.54".to_string() ); } @@ -107,7 +87,7 @@ mod tests { #[test] fn test_to_fixed() { assert_eq!( - Percent::new(154, 10000, ()).to_fixed(2, Rounding::RoundHalfUp), + Percent::new(154, 10000).to_fixed(2, Rounding::RoundHalfUp), "1.54".to_string() ); } diff --git a/src/entities/fractions/price.rs b/src/entities/fractions/price.rs index b0c2b58..a8bb6d1 100644 --- a/src/entities/fractions/price.rs +++ b/src/entities/fractions/price.rs @@ -4,22 +4,13 @@ use crate::{ currency::CurrencyTrait, fractions::{ currency_amount::CurrencyAmount, - fraction::{Fraction, FractionTrait}, + fraction::{Fraction, FractionLike, FractionTrait}, }, }, }; use num_bigint::BigInt; -#[derive(Clone)] -pub struct Price -where - TBase: CurrencyTrait, - TQuote: CurrencyTrait, -{ - numerator: BigInt, - denominator: BigInt, - pub meta: PriceMeta, -} +pub type Price = FractionLike>; #[derive(Clone)] pub struct PriceMeta @@ -32,46 +23,6 @@ where 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, - } - } - - fn meta(&self) -> PriceMeta { - self.meta.clone() - } - - fn numerator(&self) -> &BigInt { - &self.numerator - } - - fn denominator(&self) -> &BigInt { - &self.denominator - } - - fn to_significant(&self, significant_digits: u8, rounding: Rounding) -> String { - self.adjusted_for_decimals() - .to_significant(significant_digits, rounding) - } - - fn to_fixed(&self, decimal_places: u8, rounding: Rounding) -> String { - self.adjusted_for_decimals() - .to_fixed(decimal_places, rounding) - } -} - impl Price where TBase: CurrencyTrait, @@ -86,17 +37,16 @@ where let scalar = Fraction::new( BigInt::from(10).pow(base_currency.decimals() as u32), BigInt::from(10).pow(quote_currency.decimals() as u32), - (), ); - Self { - numerator: numerator.into(), - denominator: denominator.into(), - meta: PriceMeta { + FractionTrait::new( + numerator, + denominator, + PriceMeta { base_currency, quote_currency, scalar, }, - } + ) } pub fn from_currency_amounts( @@ -113,12 +63,12 @@ where } /// Flip the price, switching the base and quote currency - pub fn invert(self) -> Price { + pub fn invert(&self) -> Price { Price::new( - self.meta.quote_currency, - self.meta.base_currency, - self.numerator, - self.denominator, + self.meta.quote_currency.clone(), + self.meta.base_currency.clone(), + self.numerator().clone(), + self.denominator().clone(), ) } @@ -132,7 +82,7 @@ where /// pub fn multiply( &self, - other: Price, + other: &Price, ) -> Price { assert!( self.meta.quote_currency.equals(&other.meta.base_currency), @@ -175,12 +125,22 @@ where pub fn adjusted_for_decimals(&self) -> Fraction { self.as_fraction().multiply(&self.meta.scalar) } + + pub fn to_significant(&self, significant_digits: u8, rounding: Rounding) -> String { + self.adjusted_for_decimals() + .to_significant(significant_digits, rounding) + } + + pub fn to_fixed(&self, decimal_places: u8, rounding: Rounding) -> String { + self.adjusted_for_decimals() + .to_fixed(decimal_places, rounding) + } } #[cfg(test)] mod test { use super::*; - use crate::entities::{base_currency::BaseCurrency, currency::Currency, token::Token}; + use crate::entities::{currency::Currency, token::Token}; use lazy_static::lazy_static; const ADDRESS_ZERO: &str = "0x0000000000000000000000000000000000000000"; diff --git a/src/entities/token.rs b/src/entities/token.rs index bb7a150..a932fb4 100644 --- a/src/entities/token.rs +++ b/src/entities/token.rs @@ -1,15 +1,13 @@ -use super::{base_currency::BaseCurrency, currency::CurrencyTrait}; +use super::{base_currency::CurrencyLike, currency::CurrencyTrait}; use alloy_primitives::Address; use num_bigint::BigUint; /// Represents an ERC20 token with a unique address and some metadata. +pub type Token = CurrencyLike; + #[derive(Clone, PartialEq)] -pub struct Token { - pub chain_id: u32, +pub struct TokenMeta { pub address: Address, - pub decimals: u8, - pub symbol: Option, - pub name: Option, pub buy_fee_bps: Option, pub sell_fee_bps: Option, } @@ -20,25 +18,7 @@ impl CurrencyTrait for Token { } fn address(&self) -> Address { - self.address - } -} - -impl BaseCurrency for Token { - fn chain_id(&self) -> u32 { - self.chain_id - } - - fn decimals(&self) -> u8 { - self.decimals - } - - fn symbol(&self) -> Option { - self.symbol.clone() - } - - fn name(&self) -> Option { - self.name.clone() + self.meta.address } /// Returns true if the two tokens are equivalent, i.e. have the same chainId and address. @@ -51,7 +31,7 @@ impl BaseCurrency for Token { /// fn equals(&self, other: &impl CurrencyTrait) -> bool { match other.is_native() { - false => self.chain_id == other.chain_id() && self.address == other.address(), + false => self.chain_id == other.chain_id() && self.address() == other.address(), _ => false, } } @@ -76,12 +56,14 @@ impl Token { assert!(decimals < 255, "DECIMALS"); Self { chain_id, - address: address.parse().unwrap(), decimals, symbol, name, - buy_fee_bps, - sell_fee_bps, + meta: TokenMeta { + address: address.parse().unwrap(), + buy_fee_bps, + sell_fee_bps, + }, } } @@ -94,8 +76,8 @@ impl Token { /// pub fn sorts_before(&self, other: &Token) -> bool { assert_eq!(self.chain_id, other.chain_id, "CHAIN_IDS"); - assert_ne!(self.address, other.address, "ADDRESSES"); - self.address.lt(&other.address) + assert_ne!(self.address(), other.address(), "ADDRESSES"); + self.address().lt(&other.address()) } } @@ -129,8 +111,10 @@ mod tests { None, ); - assert!(token.address.eq(&ADDRESS_ONE.parse::
().unwrap())); - assert!(token_1.address.eq(&ADDRESS_TWO.parse::
().unwrap())); + assert!(token.address().eq(&ADDRESS_ONE.parse::
().unwrap())); + assert!(token_1 + .address() + .eq(&ADDRESS_TWO.parse::
().unwrap())); } #[test] diff --git a/src/utils/validate_and_parse_address.rs b/src/utils/validate_and_parse_address.rs index f4b4a36..237f4db 100644 --- a/src/utils/validate_and_parse_address.rs +++ b/src/utils/validate_and_parse_address.rs @@ -1,69 +1,75 @@ -use regex::Regex; - -/// Checks if the input string is a valid Ethereum address. -/// -/// # Arguments -/// -/// * `ethereum_address` - A string slice that holds the Ethereum address to be validated. -/// -/// # Returns -/// -/// * If the input string satisfies the condition of starting with `0x` and being 42 characters long with only hexadecimal characters after `0x`, returns `Ok(ethereum_address.to_string())`. -/// * Otherwise, returns an error message in the form of `Err(format!("{} is not a valid Ethereum address.", ethereum_address))`. -pub fn check_valid_ethereum_address(ethereum_address: &str) -> Result { - let valid_address_regex = Regex::new(r"^0x[0-9a-fA-F]{40}$").unwrap(); - if valid_address_regex.is_match(ethereum_address) { - Ok(ethereum_address.to_string()) - } else { - Err(format!("{} is not a valid Ethereum address.", ethereum_address)) - } -} - -/// Validates the input string as an Ethereum address and returns the checksummed address. -/// -/// # Arguments -/// -/// * `ethereum_address` - A string slice that holds the Ethereum address to be validated and checksummed. -/// -/// # Returns -/// -/// * If the input string satisfies the condition of starting with `0x` and being 42 characters long with only hexadecimal characters after `0x`, returns the checksummed address. -/// * Otherwise, returns an error message in the form of `Err(format!("{} is not a valid Ethereum address.", ethereum_address))`. -pub fn validate_and_parse_address(ethereum_address: &str) -> Result { - let checksummed_address = eth_checksum::checksum(ethereum_address); - check_valid_ethereum_address(&checksummed_address)?; - Ok(checksummed_address) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_valid_ethereum_address() { - let valid_address = "0x1234567890123456789012345678901234567890"; - assert!(check_valid_ethereum_address(valid_address).is_ok()); - } - - #[test] - fn test_invalid_ethereum_address() { - let invalid_address = "0xinvalidaddress"; - assert!(check_valid_ethereum_address(invalid_address).is_err()); - } - - #[test] - fn test_validate_and_parse_address() { - let valid_address = "0x1234567890123456789012345678901234567890"; - - assert_eq!( - validate_and_parse_address(valid_address), - Ok(valid_address.to_string()) - ); - - let invalid_address = "0xInvAlIdAddrEsS"; - assert_eq!( - validate_and_parse_address(invalid_address), - Err(format!("{} is not a valid Ethereum address.", invalid_address)) - ); - } -} +use regex::Regex; + +/// Checks if the input string is a valid Ethereum address. +/// +/// # Arguments +/// +/// * `ethereum_address` - A string slice that holds the Ethereum address to be validated. +/// +/// # Returns +/// +/// * If the input string satisfies the condition of starting with `0x` and being 42 characters long with only hexadecimal characters after `0x`, returns `Ok(ethereum_address.to_string())`. +/// * Otherwise, returns an error message in the form of `Err(format!("{} is not a valid Ethereum address.", ethereum_address))`. +pub fn check_valid_ethereum_address(ethereum_address: &str) -> Result { + let valid_address_regex = Regex::new(r"^0x[0-9a-fA-F]{40}$").unwrap(); + if valid_address_regex.is_match(ethereum_address) { + Ok(ethereum_address.to_string()) + } else { + Err(format!( + "{} is not a valid Ethereum address.", + ethereum_address + )) + } +} + +/// Validates the input string as an Ethereum address and returns the checksummed address. +/// +/// # Arguments +/// +/// * `ethereum_address` - A string slice that holds the Ethereum address to be validated and checksummed. +/// +/// # Returns +/// +/// * If the input string satisfies the condition of starting with `0x` and being 42 characters long with only hexadecimal characters after `0x`, returns the checksummed address. +/// * Otherwise, returns an error message in the form of `Err(format!("{} is not a valid Ethereum address.", ethereum_address))`. +pub fn validate_and_parse_address(ethereum_address: &str) -> Result { + let checksummed_address = eth_checksum::checksum(ethereum_address); + check_valid_ethereum_address(&checksummed_address)?; + Ok(checksummed_address) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_ethereum_address() { + let valid_address = "0x1234567890123456789012345678901234567890"; + assert!(check_valid_ethereum_address(valid_address).is_ok()); + } + + #[test] + fn test_invalid_ethereum_address() { + let invalid_address = "0xinvalidaddress"; + assert!(check_valid_ethereum_address(invalid_address).is_err()); + } + + #[test] + fn test_validate_and_parse_address() { + let valid_address = "0x1234567890123456789012345678901234567890"; + + assert_eq!( + validate_and_parse_address(valid_address), + Ok(valid_address.to_string()) + ); + + let invalid_address = "0xInvAlIdAddrEsS"; + assert_eq!( + validate_and_parse_address(invalid_address), + Err(format!( + "{} is not a valid Ethereum address.", + invalid_address + )) + ); + } +}