1use crate::math::get_limit_order_output_amount;
9use crate::{
10 tick_index_to_sqrt_price, try_apply_transfer_fee, try_mul_div, try_reverse_apply_swap_fee, CoreError, FusionPoolFacade, LimitOrderDecreaseQuote,
11 LimitOrderFacade, TickFacade, TransferFee, AMOUNT_EXCEEDS_LIMIT_ORDER_INPUT_AMOUNT, AMOUNT_EXCEEDS_MAX_U64, FEE_RATE_MUL_VALUE,
12 LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC, PROTOCOL_FEE_RATE_MUL_VALUE,
13};
14
15#[cfg(feature = "wasm")]
16use fusionamm_macros::wasm_expose;
17
18#[cfg_attr(feature = "wasm", wasm_expose)]
25pub fn limit_order_quote_by_input_token(
26 amount_in: u64,
27 a_to_b_order: bool,
28 tick_index: i32,
29 fusion_pool: FusionPoolFacade,
30) -> Result<u64, CoreError> {
31 let sqrt_price: u128 = tick_index_to_sqrt_price(tick_index).into();
32 let mut amount_out = get_limit_order_output_amount(amount_in, a_to_b_order, sqrt_price, false)?;
33 amount_out += limit_order_reward_by_output_token(amount_out, fusion_pool.fee_rate, fusion_pool.protocol_fee_rate)?;
34 Ok(amount_out)
35}
36
37#[cfg_attr(feature = "wasm", wasm_expose)]
44pub fn limit_order_quote_by_output_token(
45 amount_out: u64,
46 a_to_b_order: bool,
47 tick_index: i32,
48 fusion_pool: FusionPoolFacade,
49) -> Result<u64, CoreError> {
50 let sqrt_price: u128 = tick_index_to_sqrt_price(tick_index).into();
51
52 let f = fusion_pool.fee_rate as f64 / FEE_RATE_MUL_VALUE as f64;
53 let p = fusion_pool.protocol_fee_rate as f64 / PROTOCOL_FEE_RATE_MUL_VALUE as f64;
54
55 let denominator = 1.0 + (f / (1.0 - f) * (1.0 - p));
59 let amount_out_with_fees = amount_out as f64 / denominator;
60
61 if amount_out_with_fees < 0.0 || amount_out_with_fees > u64::MAX as f64 {
62 return Err(AMOUNT_EXCEEDS_MAX_U64);
63 }
64
65 let amount_in = get_limit_order_output_amount(amount_out_with_fees as u64, !a_to_b_order, sqrt_price, true)?;
66
67 Ok(amount_in)
68}
69
70#[cfg_attr(feature = "wasm", wasm_expose)]
77pub fn limit_order_reward_by_output_token(amount_out: u64, fee_rate: u16, protocol_fee_rate: u16) -> Result<u64, CoreError> {
78 let swap_fee = try_reverse_apply_swap_fee(amount_out.into(), fee_rate)? - amount_out;
80 let reward = swap_fee - try_mul_div(swap_fee, protocol_fee_rate as u128, PROTOCOL_FEE_RATE_MUL_VALUE as u128, false)?;
82 Ok(reward)
83}
84
85#[cfg_attr(feature = "wasm", wasm_expose)]
86pub fn decrease_limit_order_quote(
87 fusion_pool: FusionPoolFacade,
88 limit_order: LimitOrderFacade,
89 tick: TickFacade,
90 amount: u64,
91 transfer_fee_a: Option<TransferFee>,
92 transfer_fee_b: Option<TransferFee>,
93) -> Result<LimitOrderDecreaseQuote, CoreError> {
94 if amount > limit_order.amount {
95 return Err(AMOUNT_EXCEEDS_LIMIT_ORDER_INPUT_AMOUNT);
96 }
97
98 let (amount_in, amount_out) = if limit_order.age == tick.age {
100 (amount, 0)
101 }
102 else if limit_order.age + 1 == tick.age {
104 if tick.part_filled_orders_input == 0 {
105 return Err(LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC);
106 }
107 let sqrt_price: u128 = tick_index_to_sqrt_price(limit_order.tick_index).into();
108 let remaining_input = try_mul_div(amount, tick.part_filled_orders_remaining_input as u128, tick.part_filled_orders_input as u128, false)?;
109 let amount_out = get_limit_order_output_amount(amount - remaining_input, limit_order.a_to_b, sqrt_price, false)?;
110 (remaining_input, amount_out)
111 }
112 else if limit_order.age + 2 <= tick.age {
114 let sqrt_price: u128 = tick_index_to_sqrt_price(limit_order.tick_index).into();
115 let amount_out = get_limit_order_output_amount(amount, limit_order.a_to_b, sqrt_price, false)?;
116 (0, amount_out)
117 } else {
118 return Err(LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC);
119 };
120
121 let mut amount_out_a;
122 let mut amount_out_b;
123 let mut reward_a = 0;
124 let mut reward_b = 0;
125
126 if limit_order.a_to_b {
127 let filled_amount = amount - amount_in;
128 if filled_amount > 0 {
130 if fusion_pool.orders_filled_amount_a == 0 {
131 return Err(LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC);
132 }
133 reward_b = try_mul_div(fusion_pool.olp_fee_owed_b, filled_amount as u128, fusion_pool.orders_filled_amount_a as u128, false)?;
134 }
135 amount_out_a = amount_in;
137 amount_out_b = amount_out + reward_b;
138 } else {
139 let filled_amount = amount - amount_in;
140 if filled_amount > 0 {
142 if fusion_pool.orders_filled_amount_b == 0 {
143 return Err(LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC);
144 }
145 reward_a = try_mul_div(fusion_pool.olp_fee_owed_a, filled_amount as u128, fusion_pool.orders_filled_amount_b as u128, false)?;
146 }
147 amount_out_a = amount_out + reward_a;
149 amount_out_b = amount_in;
150 }
151
152 amount_out_a = try_apply_transfer_fee(amount_out_a, transfer_fee_a.unwrap_or_default())?;
153 amount_out_b = try_apply_transfer_fee(amount_out_b, transfer_fee_b.unwrap_or_default())?;
154
155 Ok(LimitOrderDecreaseQuote {
156 amount_out_a,
157 amount_out_b,
158 reward_a,
159 reward_b,
160 })
161}
162
163#[cfg(all(test, not(feature = "wasm")))]
164mod tests {
165 use crate::{
166 decrease_limit_order_quote, limit_order_quote_by_input_token, limit_order_quote_by_output_token, price_to_tick_index,
167 sqrt_price_to_tick_index, FusionPoolFacade, LimitOrderFacade, TickFacade,
168 };
169 const TEN_PCT: u16 = 1000;
170 const ONE_PCT_FEE_RATE: u16 = 10000;
171
172 fn test_fusion_pool(sqrt_price: u128, fee_rate: u16, protocol_fee_rate: u16) -> FusionPoolFacade {
173 let tick_current_index = sqrt_price_to_tick_index(sqrt_price);
174 FusionPoolFacade {
175 tick_current_index,
176 fee_rate,
177 protocol_fee_rate,
178 sqrt_price,
179 tick_spacing: 2,
180 ..FusionPoolFacade::default()
181 }
182 }
183
184 #[test]
185 fn partially_decrease_not_filled_a_to_b_order() {
187 let quote = decrease_limit_order_quote(
188 FusionPoolFacade {
189 ..FusionPoolFacade::default()
190 },
191 LimitOrderFacade {
192 tick_index: 128,
193 amount: 50_000,
194 a_to_b: true,
195 age: 5,
196 },
197 TickFacade {
198 age: 5,
199 open_orders_input: 100_000,
200 part_filled_orders_input: 0,
201 part_filled_orders_remaining_input: 0,
202 fulfilled_a_to_b_orders_input: 0,
203 fulfilled_b_to_a_orders_input: 0,
204 ..TickFacade::default()
205 },
206 25_000,
207 None,
208 None,
209 )
210 .unwrap();
211
212 assert_eq!(quote.amount_out_a, 25_000);
213 assert_eq!(quote.amount_out_b, 0);
214 }
215
216 #[test]
217 fn partially_decrease_semi_filled_a_to_b_order() {
219 let quote = decrease_limit_order_quote(
220 FusionPoolFacade {
221 protocol_fee_rate: TEN_PCT,
222 orders_filled_amount_a: 80_000,
223 olp_fee_owed_b: 500,
224 ..FusionPoolFacade::default()
225 },
226 LimitOrderFacade {
227 tick_index: 128,
228 amount: 50_000,
229 a_to_b: true,
230 age: 5,
231 },
232 TickFacade {
233 age: 6,
234 open_orders_input: 0,
235 part_filled_orders_input: 200_000,
236 part_filled_orders_remaining_input: 120_000,
237 fulfilled_a_to_b_orders_input: 0,
238 fulfilled_b_to_a_orders_input: 0,
239 ..TickFacade::default()
240 },
241 25_000,
242 None,
243 None,
244 )
245 .unwrap();
246
247 assert_eq!(quote.amount_out_a, 15000);
248 assert_eq!(quote.amount_out_b, 10190);
249 assert_eq!(quote.reward_a, 0);
250 assert_eq!(quote.reward_b, 62);
251 }
252
253 #[test]
254 fn partially_decrease_semi_filled_b_to_a_order() {
256 let quote = decrease_limit_order_quote(
257 FusionPoolFacade {
258 protocol_fee_rate: TEN_PCT,
259 orders_filled_amount_b: 80_000,
260 olp_fee_owed_a: 500,
261 ..FusionPoolFacade::default()
262 },
263 LimitOrderFacade {
264 tick_index: 128,
265 amount: 50_000,
266 a_to_b: false,
267 age: 5,
268 },
269 TickFacade {
270 age: 6,
271 open_orders_input: 0,
272 part_filled_orders_input: 200_000,
273 part_filled_orders_remaining_input: 120_000,
274 fulfilled_a_to_b_orders_input: 0,
275 fulfilled_b_to_a_orders_input: 0,
276 ..TickFacade::default()
277 },
278 25_000,
279 None,
280 None,
281 )
282 .unwrap();
283
284 assert_eq!(quote.amount_out_a, 9934);
285 assert_eq!(quote.amount_out_b, 15000);
286 assert_eq!(quote.reward_a, 62);
287 assert_eq!(quote.reward_b, 0);
288 }
289
290 #[test]
291 fn partially_decrease_fulfilled_a_to_b() {
293 let quote = decrease_limit_order_quote(
294 FusionPoolFacade {
295 protocol_fee_rate: TEN_PCT,
296 orders_filled_amount_a: 100_000,
297 olp_fee_owed_b: 500,
298 ..FusionPoolFacade::default()
299 },
300 LimitOrderFacade {
301 tick_index: 128,
302 amount: 100_000,
303 a_to_b: true,
304 age: 5,
305 },
306 TickFacade {
307 age: 7,
308 open_orders_input: 0,
309 part_filled_orders_input: 0,
310 part_filled_orders_remaining_input: 0,
311 fulfilled_a_to_b_orders_input: 100_000,
312 fulfilled_b_to_a_orders_input: 80_000,
313 ..TickFacade::default()
314 },
315 10_000,
316 None,
317 None,
318 )
319 .unwrap();
320
321 assert_eq!(quote.amount_out_a, 0);
322 assert_eq!(quote.amount_out_b, 10178);
323 assert_eq!(quote.reward_a, 0);
324 assert_eq!(quote.reward_b, 50);
325 }
326
327 #[test]
328 fn partially_decrease_fulfilled_b_to_a() {
330 let quote = decrease_limit_order_quote(
331 FusionPoolFacade {
332 protocol_fee_rate: TEN_PCT,
333 orders_filled_amount_b: 80_000,
334 olp_fee_owed_a: 500,
335 ..FusionPoolFacade::default()
336 },
337 LimitOrderFacade {
338 tick_index: 128,
339 amount: 100_000,
340 a_to_b: false,
341 age: 5,
342 },
343 TickFacade {
344 age: 7,
345 open_orders_input: 0,
346 part_filled_orders_input: 0,
347 part_filled_orders_remaining_input: 0,
348 fulfilled_a_to_b_orders_input: 100_000,
349 fulfilled_b_to_a_orders_input: 80_000,
350 ..TickFacade::default()
351 },
352 10_000,
353 None,
354 None,
355 )
356 .unwrap();
357
358 assert_eq!(quote.amount_out_a, 9934);
359 assert_eq!(quote.amount_out_b, 0);
360 assert_eq!(quote.reward_a, 62);
361 assert_eq!(quote.reward_b, 0);
362 }
363
364 #[test]
365 fn test_limit_order_quote_by_input_token() {
366 assert_eq!(
368 limit_order_quote_by_input_token(10_000, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, 0, 0)).unwrap(),
369 19998
370 );
371
372 assert_eq!(
374 limit_order_quote_by_input_token(10_000, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, ONE_PCT_FEE_RATE, 0)).unwrap(),
375 20200
376 );
377
378 assert_eq!(
380 limit_order_quote_by_input_token(10_000, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, ONE_PCT_FEE_RATE, TEN_PCT))
381 .unwrap(),
382 20180
383 );
384 }
385
386 #[test]
387 fn test_limit_order_quote_by_output_token() {
388 assert_eq!(
390 limit_order_quote_by_output_token(19998, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, 0, TEN_PCT)).unwrap(),
391 10_000
392 );
393
394 assert_eq!(
396 limit_order_quote_by_output_token(20200, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, ONE_PCT_FEE_RATE, 0)).unwrap(),
397 10_000
398 );
399
400 assert_eq!(
402 limit_order_quote_by_output_token(20180, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, ONE_PCT_FEE_RATE, TEN_PCT))
403 .unwrap(),
404 10_000
405 );
406 }
407}