1use crate::math::amm::{self, AmmInput, BuyArgs, SellArgs};
2use crate::nexus::types::DexFees;
3use crate::utils::{
4 Landing, MarketQuote, MarketQuoteError, QuoteRequest, TradeDirection,
5 TradeMode,
6};
7
8#[derive(Clone, Copy, Debug)]
11pub struct PoolMarket<'a> {
12 pub base_vault_amount: u64,
15 pub quote_reserves: u64,
16 pub created_at: i64,
17 pub fees: &'a DexFees,
19 pub landing: Landing,
20}
21
22pub fn quote(
25 market: &PoolMarket<'_>,
26 request: QuoteRequest,
27) -> Result<MarketQuote, MarketQuoteError> {
28 let fee_bps = market
29 .fees
30 .effective_fee_bps(market.created_at, market.landing.unix_timestamp)
31 .ok_or(MarketQuoteError::FeeOutOfRange)?;
32 let amm = AmmInput {
33 quote_reserves: market.quote_reserves,
34 base_reserves: market.base_vault_amount,
35 amount: request.amount,
36 fee_bps,
37 };
38 let Landing {
39 base_fee,
40 quote_fee,
41 ..
42 } = market.landing;
43
44 match request.direction {
45 TradeDirection::Buy => {
46 let args = BuyArgs {
47 amm,
48 quote_fee,
49 base_fee,
50 base_reserve_cap: None,
51 };
52 let bought = match request.mode {
53 TradeMode::ExactIn => amm::buy_exact_in_with_fees(args),
54 TradeMode::ExactOut => amm::buy_exact_out_with_fees(args),
55 }?;
56 Ok(MarketQuote::bought(&bought, request, fee_bps))
57 }
58 TradeDirection::Sell => {
59 let args = SellArgs {
60 amm,
61 quote_fee,
62 base_fee,
63 };
64 let sold = match request.mode {
65 TradeMode::ExactIn => amm::sell_exact_in_with_fees(args),
66 TradeMode::ExactOut => amm::sell_exact_out_with_fees(args),
67 }?;
68 Ok(MarketQuote::sold(&sold, fee_bps))
69 }
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::math::amm::{BuyQuote, SellQuote};
77
78 const NOW: i64 = 1_000;
79 const BASE_VAULT: u64 = 400_000_000_000;
80 const QUOTE_RESERVES: u64 = 25_000_000_000;
81
82 const FEES: DexFees = DexFees {
83 creation_fee_cents: 0,
84 protocol_fee_bps: 80,
85 lp_fee_bps: 30,
86 creator_fee_bps: 40,
87 fee_decay_seconds: 0,
88 fee_decay_start_bps: 0,
89 };
90
91 const LANDING: Landing = Landing {
92 base_fee: None,
93 quote_fee: None,
94 unix_timestamp: NOW,
95 };
96
97 const MARKET: PoolMarket<'static> = PoolMarket {
98 base_vault_amount: BASE_VAULT,
99 quote_reserves: QUOTE_RESERVES,
100 created_at: 0,
101 fees: &FEES,
102 landing: LANDING,
103 };
104
105 const fn priced(amount: u64) -> AmmInput {
106 AmmInput {
107 quote_reserves: QUOTE_RESERVES,
108 base_reserves: BASE_VAULT,
109 amount,
110 fee_bps: 150,
111 }
112 }
113
114 fn bought(args: BuyArgs, mode: TradeMode) -> BuyQuote {
115 match mode {
116 TradeMode::ExactIn => amm::buy_exact_in_with_fees(args),
117 TradeMode::ExactOut => amm::buy_exact_out_with_fees(args),
118 }
119 .unwrap()
120 }
121
122 fn sold(args: SellArgs, mode: TradeMode) -> SellQuote {
123 match mode {
124 TradeMode::ExactIn => amm::sell_exact_in_with_fees(args),
125 TradeMode::ExactOut => amm::sell_exact_out_with_fees(args),
126 }
127 .unwrap()
128 }
129
130 #[test]
131 fn a_buy_prices_off_the_base_vault() {
132 let quote = quote(
133 &MARKET,
134 QuoteRequest {
135 direction: TradeDirection::Buy,
136 mode: TradeMode::ExactIn,
137 amount: 1_000_000_000,
138 },
139 )
140 .unwrap();
141 assert_eq!(quote.out_amount, 15_162_593_804);
142 assert_eq!(quote.fee, 15_000_000);
143 assert_eq!(quote.fee_bps, 150);
144 assert!(!quote.supply_capped);
145 }
146
147 #[test]
148 fn every_trade_matches_the_math_over_the_programs_inputs() {
149 let amount = 1_000_000_000;
150 for mode in [TradeMode::ExactIn, TradeMode::ExactOut] {
151 let buy = bought(
152 BuyArgs {
153 amm: priced(amount),
154 quote_fee: None,
155 base_fee: None,
156 base_reserve_cap: None,
157 },
158 mode,
159 );
160 assert_eq!(
161 quote(
162 &MARKET,
163 QuoteRequest {
164 direction: TradeDirection::Buy,
165 mode,
166 amount,
167 }
168 ),
169 Ok(MarketQuote::bought(
170 &buy,
171 QuoteRequest {
172 direction: TradeDirection::Buy,
173 mode,
174 amount,
175 },
176 150
177 ))
178 );
179
180 let sell = sold(
181 SellArgs {
182 amm: priced(amount),
183 quote_fee: None,
184 base_fee: None,
185 },
186 mode,
187 );
188 let quote = quote(
189 &MARKET,
190 QuoteRequest {
191 direction: TradeDirection::Sell,
192 mode,
193 amount,
194 },
195 )
196 .unwrap();
197 assert_eq!(quote, MarketQuote::sold(&sell, 150));
198 assert!(!quote.supply_capped);
199 }
200 }
201
202 #[test]
203 fn the_fee_decays_from_the_pools_created_at() {
204 let decaying = DexFees {
205 fee_decay_seconds: 12,
206 fee_decay_start_bps: 5_000,
207 ..FEES
208 };
209 let fresh = quote(
210 &PoolMarket {
211 created_at: NOW,
212 fees: &decaying,
213 ..MARKET
214 },
215 QuoteRequest {
216 direction: TradeDirection::Buy,
217 mode: TradeMode::ExactIn,
218 amount: 1_000_000_000,
219 },
220 )
221 .unwrap();
222 assert_eq!(fresh.fee_bps, 5_000);
223 assert_eq!(fresh.fee, 500_000_000);
224
225 assert_eq!(
226 quote(
227 &PoolMarket {
228 created_at: -1,
229 ..MARKET
230 },
231 QuoteRequest {
232 direction: TradeDirection::Sell,
233 mode: TradeMode::ExactIn,
234 amount: 1_000,
235 }
236 ),
237 Err(MarketQuoteError::FeeOutOfRange)
238 );
239 }
240}