1use crate::math::amm::{self, AmmInput, BuyArgs, SellArgs};
2use crate::nexus::EffectiveFeeArgs;
3use crate::nexus::types::DexFees;
4use crate::utils::{
5 Landing, MarketQuote, MarketQuoteError, QuoteRequest, TradeDirection,
6 TradeMode,
7};
8
9#[derive(Clone, Copy, Debug)]
12pub struct PoolMarket<'a> {
13 pub base_vault_amount: u64,
16 pub quote_reserves: u64,
17 pub created_at: i64,
18 pub fees: &'a DexFees,
21 pub landing: Landing,
22 pub creator_fee_bps: u16,
24}
25
26pub fn quote(
29 market: &PoolMarket<'_>,
30 request: QuoteRequest,
31) -> Result<MarketQuote, MarketQuoteError> {
32 let fee_bps = market
33 .fees
34 .effective_fee_bps(EffectiveFeeArgs {
35 creator_fee_bps: market.creator_fee_bps,
36 created_at: market.created_at,
37 now: market.landing.unix_timestamp,
38 })
39 .ok_or(MarketQuoteError::FeeOutOfRange)?;
40 let amm = AmmInput {
41 quote_reserves: market.quote_reserves,
42 base_reserves: market.base_vault_amount,
43 amount: request.amount,
44 fee_bps,
45 };
46 let Landing {
47 base_fee,
48 quote_fee,
49 ..
50 } = market.landing;
51
52 match request.direction {
53 TradeDirection::Buy => {
54 let args = BuyArgs {
55 amm,
56 quote_fee,
57 base_fee,
58 base_reserve_cap: None,
59 };
60 let bought = match request.mode {
61 TradeMode::ExactIn => amm::buy_exact_in_with_fees(args),
62 TradeMode::ExactOut => amm::buy_exact_out_with_fees(args),
63 }?;
64 Ok(MarketQuote::bought(&bought, request, fee_bps))
65 }
66 TradeDirection::Sell => {
67 let args = SellArgs {
68 amm,
69 quote_fee,
70 base_fee,
71 };
72 let sold = match request.mode {
73 TradeMode::ExactIn => amm::sell_exact_in_with_fees(args),
74 TradeMode::ExactOut => amm::sell_exact_out_with_fees(args),
75 }?;
76 Ok(MarketQuote::sold(&sold, fee_bps))
77 }
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::math::amm::{BuyQuote, SellQuote};
85
86 const NOW: i64 = 1_000;
87 const BASE_VAULT: u64 = 400_000_000_000;
88 const QUOTE_RESERVES: u64 = 25_000_000_000;
89
90 const FEES: DexFees = DexFees {
92 creation_fee_cents: 0,
93 protocol_fee_bps: 80,
94 lp_fee_bps: 30,
95 max_creator_fee_bps: 100,
96 fee_decay_seconds: 0,
97 fee_decay_start_bps: 0,
98 };
99
100 const LANDING: Landing = Landing {
101 base_fee: None,
102 quote_fee: None,
103 unix_timestamp: NOW,
104 };
105
106 const MARKET: PoolMarket<'static> = PoolMarket {
107 base_vault_amount: BASE_VAULT,
108 quote_reserves: QUOTE_RESERVES,
109 created_at: 0,
110 fees: &FEES,
111 landing: LANDING,
112 creator_fee_bps: 40,
113 };
114
115 const fn priced(amount: u64) -> AmmInput {
116 AmmInput {
117 quote_reserves: QUOTE_RESERVES,
118 base_reserves: BASE_VAULT,
119 amount,
120 fee_bps: 150,
121 }
122 }
123
124 fn bought(args: BuyArgs, mode: TradeMode) -> BuyQuote {
125 match mode {
126 TradeMode::ExactIn => amm::buy_exact_in_with_fees(args),
127 TradeMode::ExactOut => amm::buy_exact_out_with_fees(args),
128 }
129 .unwrap()
130 }
131
132 fn sold(args: SellArgs, mode: TradeMode) -> SellQuote {
133 match mode {
134 TradeMode::ExactIn => amm::sell_exact_in_with_fees(args),
135 TradeMode::ExactOut => amm::sell_exact_out_with_fees(args),
136 }
137 .unwrap()
138 }
139
140 #[test]
141 fn a_buy_prices_off_the_base_vault() {
142 let quote = quote(
143 &MARKET,
144 QuoteRequest {
145 direction: TradeDirection::Buy,
146 mode: TradeMode::ExactIn,
147 amount: 1_000_000_000,
148 },
149 )
150 .unwrap();
151 assert_eq!(quote.out_amount, 15_162_593_804);
152 assert_eq!(quote.fee, 15_000_000);
153 assert_eq!(quote.fee_bps, 150);
154 assert!(!quote.supply_capped);
155 }
156
157 #[test]
158 fn every_trade_matches_the_math_over_the_programs_inputs() {
159 let amount = 1_000_000_000;
160 for mode in [TradeMode::ExactIn, TradeMode::ExactOut] {
161 let buy = bought(
162 BuyArgs {
163 amm: priced(amount),
164 quote_fee: None,
165 base_fee: None,
166 base_reserve_cap: None,
167 },
168 mode,
169 );
170 assert_eq!(
171 quote(
172 &MARKET,
173 QuoteRequest {
174 direction: TradeDirection::Buy,
175 mode,
176 amount,
177 }
178 ),
179 Ok(MarketQuote::bought(
180 &buy,
181 QuoteRequest {
182 direction: TradeDirection::Buy,
183 mode,
184 amount,
185 },
186 150
187 ))
188 );
189
190 let sell = sold(
191 SellArgs {
192 amm: priced(amount),
193 quote_fee: None,
194 base_fee: None,
195 },
196 mode,
197 );
198 let quote = quote(
199 &MARKET,
200 QuoteRequest {
201 direction: TradeDirection::Sell,
202 mode,
203 amount,
204 },
205 )
206 .unwrap();
207 assert_eq!(quote, MarketQuote::sold(&sell, 150));
208 assert!(!quote.supply_capped);
209 }
210 }
211
212 #[test]
215 fn the_pools_creator_rate_prices_the_trade_not_the_partner_max() {
216 let decaying = DexFees {
217 fee_decay_seconds: 12,
218 fee_decay_start_bps: 5_000,
219 ..FEES
220 };
221 let buy = QuoteRequest {
222 direction: TradeDirection::Buy,
223 mode: TradeMode::ExactIn,
224 amount: 1_000_000_000,
225 };
226 for (creator_fee_bps, standard, halfway) in [
229 (
230 0,
231 (110, 11_000_000, 15_221_824_618),
232 (1_333, 133_300_000, 13_402_560_048),
233 ),
234 (
235 40,
236 (150, 15_000_000, 15_162_593_804),
237 (1_363, 136_300_000, 13_357_717_573),
238 ),
239 (
240 160,
241 (270, 27_000_000, 14_984_791_899),
242 (1_453, 145_300_000, 13_223_127_709),
243 ),
244 ] {
245 let market = PoolMarket {
246 creator_fee_bps,
247 fees: &decaying,
248 ..MARKET
249 };
250 let past = quote(&market, buy).unwrap();
251 assert_eq!((past.fee_bps, past.fee, past.out_amount), standard);
252
253 let inside = quote(
254 &PoolMarket {
255 created_at: NOW - 6,
256 ..market
257 },
258 buy,
259 )
260 .unwrap();
261 assert_eq!(
262 (inside.fee_bps, inside.fee, inside.out_amount),
263 halfway
264 );
265 }
266 }
267
268 #[test]
269 fn the_fee_decays_from_the_pools_created_at() {
270 let decaying = DexFees {
271 fee_decay_seconds: 12,
272 fee_decay_start_bps: 5_000,
273 ..FEES
274 };
275 let fresh = quote(
276 &PoolMarket {
277 created_at: NOW,
278 fees: &decaying,
279 ..MARKET
280 },
281 QuoteRequest {
282 direction: TradeDirection::Buy,
283 mode: TradeMode::ExactIn,
284 amount: 1_000_000_000,
285 },
286 )
287 .unwrap();
288 assert_eq!(fresh.fee_bps, 5_000);
289 assert_eq!(fresh.fee, 500_000_000);
290
291 assert_eq!(
292 quote(
293 &PoolMarket {
294 created_at: -1,
295 ..MARKET
296 },
297 QuoteRequest {
298 direction: TradeDirection::Sell,
299 mode: TradeMode::ExactIn,
300 amount: 1_000,
301 }
302 ),
303 Err(MarketQuoteError::FeeOutOfRange)
304 );
305 }
306}