1use super::types::TradeDirection;
2use crate::constants::{
3 ATA_PROGRAM_ID, LAUNCHPAD_PROGRAM_ID, SYSTEM_PROGRAM_ID,
4 TOKEN_2022_PROGRAM_ID,
5};
6use crate::math::amm::{self, QuoteError};
7use crate::utils::{
8 DeriveTradeArgs, TradeAccounts, TradeMode, TradeStateFields,
9 TradeUserContext, derive_trade_accounts, partner_account,
10};
11use solana_address::Address;
12
13#[derive(Clone, Debug)]
14pub struct BuyExactInParams<'a> {
15 pub user: &'a Address,
16 pub payer: Option<&'a Address>,
17 pub base_mint: &'a Address,
18 pub quote_mint: &'a Address,
19 pub virtual_base_reserves: u64,
20 pub virtual_quote_reserves: u64,
21 pub fee_bps: u16,
22 pub slippage_bps: u16,
23 pub partner: &'a Address,
24 pub platform_config: &'a Address,
25 pub quote_token_program: &'a Address,
26 pub quote_fee: Option<amm::MintFee>,
29 pub base_fee: Option<amm::MintFee>,
31 pub real_base_reserves: u64,
33 pub quote_amount: u64,
34}
35
36pub fn buy_exact_in(
37 p: &BuyExactInParams<'_>,
38) -> Result<(solana_instruction::Instruction, amm::BuyQuote), QuoteError> {
39 let q = amm::buy_exact_in_with_fees(amm::BuyArgs {
40 amm: amm::AmmInput {
41 quote_reserves: p.virtual_quote_reserves,
42 base_reserves: p.virtual_base_reserves,
43 amount: p.quote_amount,
44 fee_bps: p.fee_bps,
45 },
46 quote_fee: p.quote_fee,
47 base_fee: p.base_fee,
48 base_reserve_cap: Some(p.real_base_reserves),
50 })?;
51 let min_base_out =
53 amm::calculate_slippage_down(q.base_to_user, p.slippage_bps)?;
54 let ix = build_buy_exact_in_instruction(&BuildBuyExactInParams {
55 user: p.user,
56 payer: p.payer,
57 base_mint: p.base_mint,
58 quote_mint: p.quote_mint,
59 partner: p.partner,
60 platform_config: p.platform_config,
61 quote_token_program: p.quote_token_program,
62 amount_in: p.quote_amount,
63 min_amount_out: min_base_out,
64 });
65 Ok((ix, q))
66}
67
68#[derive(Clone, Debug)]
69pub struct BuyExactOutParams<'a> {
70 pub user: &'a Address,
71 pub payer: Option<&'a Address>,
72 pub base_mint: &'a Address,
73 pub quote_mint: &'a Address,
74 pub virtual_base_reserves: u64,
75 pub virtual_quote_reserves: u64,
76 pub fee_bps: u16,
77 pub slippage_bps: u16,
78 pub partner: &'a Address,
79 pub platform_config: &'a Address,
80 pub quote_token_program: &'a Address,
81 pub quote_fee: Option<amm::MintFee>,
84 pub base_fee: Option<amm::MintFee>,
86 pub real_base_reserves: u64,
88 pub base_amount: u64,
89}
90
91pub fn buy_exact_out(
92 p: &BuyExactOutParams<'_>,
93) -> Result<(solana_instruction::Instruction, amm::BuyQuote), QuoteError> {
94 let q = amm::buy_exact_out_with_fees(amm::BuyArgs {
95 amm: amm::AmmInput {
96 quote_reserves: p.virtual_quote_reserves,
97 base_reserves: p.virtual_base_reserves,
98 amount: p.base_amount,
99 fee_bps: p.fee_bps,
100 },
101 quote_fee: p.quote_fee,
102 base_fee: p.base_fee,
103 base_reserve_cap: Some(p.real_base_reserves),
104 })?;
105 let max_quote_in =
108 amm::calculate_slippage_up(q.quote_from_user, p.slippage_bps)?;
109 let ix = build_buy_exact_out_instruction(&BuildBuyExactOutParams {
110 user: p.user,
111 payer: p.payer,
112 base_mint: p.base_mint,
113 quote_mint: p.quote_mint,
114 partner: p.partner,
115 platform_config: p.platform_config,
116 quote_token_program: p.quote_token_program,
117 amount_out: q.base_to_user,
120 max_amount_in: max_quote_in,
121 });
122 Ok((ix, q))
123}
124
125#[derive(Clone, Debug)]
126pub struct SellExactInParams<'a> {
127 pub user: &'a Address,
128 pub payer: Option<&'a Address>,
129 pub base_mint: &'a Address,
130 pub quote_mint: &'a Address,
131 pub virtual_base_reserves: u64,
132 pub virtual_quote_reserves: u64,
133 pub fee_bps: u16,
134 pub slippage_bps: u16,
135 pub partner: &'a Address,
136 pub platform_config: &'a Address,
137 pub quote_token_program: &'a Address,
138 pub quote_fee: Option<amm::MintFee>,
141 pub base_fee: Option<amm::MintFee>,
143 pub base_amount: u64,
144}
145
146pub fn sell_exact_in(
147 p: &SellExactInParams<'_>,
148) -> Result<(solana_instruction::Instruction, amm::SellQuote), QuoteError> {
149 let q = amm::sell_exact_in_with_fees(amm::SellArgs {
150 amm: amm::AmmInput {
151 quote_reserves: p.virtual_quote_reserves,
152 base_reserves: p.virtual_base_reserves,
153 amount: p.base_amount,
154 fee_bps: p.fee_bps,
155 },
156 quote_fee: p.quote_fee,
157 base_fee: p.base_fee,
158 })?;
159 let min_quote_out =
162 amm::calculate_slippage_down(q.quote_to_user, p.slippage_bps)?;
163 let ix = build_sell_exact_in_instruction(&BuildSellExactInParams {
164 user: p.user,
165 payer: p.payer,
166 base_mint: p.base_mint,
167 quote_mint: p.quote_mint,
168 partner: p.partner,
169 platform_config: p.platform_config,
170 quote_token_program: p.quote_token_program,
171 amount_in: p.base_amount,
172 min_amount_out: min_quote_out,
173 });
174 Ok((ix, q))
175}
176
177#[derive(Clone, Debug)]
178pub struct SellExactOutParams<'a> {
179 pub user: &'a Address,
180 pub payer: Option<&'a Address>,
181 pub base_mint: &'a Address,
182 pub quote_mint: &'a Address,
183 pub virtual_base_reserves: u64,
184 pub virtual_quote_reserves: u64,
185 pub fee_bps: u16,
186 pub slippage_bps: u16,
187 pub partner: &'a Address,
188 pub platform_config: &'a Address,
189 pub quote_token_program: &'a Address,
190 pub quote_fee: Option<amm::MintFee>,
193 pub base_fee: Option<amm::MintFee>,
195 pub quote_amount: u64,
196}
197
198pub fn sell_exact_out(
199 p: &SellExactOutParams<'_>,
200) -> Result<(solana_instruction::Instruction, amm::SellQuote), QuoteError> {
201 let q = amm::sell_exact_out_with_fees(amm::SellArgs {
202 amm: amm::AmmInput {
203 quote_reserves: p.virtual_quote_reserves,
204 base_reserves: p.virtual_base_reserves,
205 amount: p.quote_amount,
206 fee_bps: p.fee_bps,
207 },
208 quote_fee: p.quote_fee,
209 base_fee: p.base_fee,
210 })?;
211 let max_base_in =
214 amm::calculate_slippage_up(q.base_from_user, p.slippage_bps)?;
215 let ix = build_sell_exact_out_instruction(&BuildSellExactOutParams {
216 user: p.user,
217 payer: p.payer,
218 base_mint: p.base_mint,
219 quote_mint: p.quote_mint,
220 partner: p.partner,
221 platform_config: p.platform_config,
222 quote_token_program: p.quote_token_program,
223 amount_out: p.quote_amount,
224 max_amount_in: max_base_in,
225 });
226 Ok((ix, q))
227}
228
229#[derive(Clone, Debug)]
230pub struct BuildBuyExactInParams<'a> {
231 pub user: &'a Address,
232 pub payer: Option<&'a Address>,
233 pub base_mint: &'a Address,
234 pub quote_mint: &'a Address,
235 pub partner: &'a Address,
236 pub platform_config: &'a Address,
237 pub quote_token_program: &'a Address,
238 pub amount_in: u64,
239 pub min_amount_out: u64,
240}
241
242#[must_use]
243pub fn build_buy_exact_in_instruction(
244 p: &BuildBuyExactInParams<'_>,
245) -> solana_instruction::Instruction {
246 launchpad_trade_accounts!(
247 super::instructions::BuyExactIn,
248 &derived_trade_accounts!(p)
249 )
250 .instruction(super::instructions::BuyExactInInstructionArgs {
251 amount_in: p.amount_in,
252 min_amount_out: p.min_amount_out,
253 })
254}
255
256#[derive(Clone, Debug)]
257pub struct BuildBuyExactOutParams<'a> {
258 pub user: &'a Address,
259 pub payer: Option<&'a Address>,
260 pub base_mint: &'a Address,
261 pub quote_mint: &'a Address,
262 pub partner: &'a Address,
263 pub platform_config: &'a Address,
264 pub quote_token_program: &'a Address,
265 pub amount_out: u64,
266 pub max_amount_in: u64,
267}
268
269#[must_use]
270pub fn build_buy_exact_out_instruction(
271 p: &BuildBuyExactOutParams<'_>,
272) -> solana_instruction::Instruction {
273 launchpad_trade_accounts!(
274 super::instructions::BuyExactOut,
275 &derived_trade_accounts!(p)
276 )
277 .instruction(super::instructions::BuyExactOutInstructionArgs {
278 amount_out: p.amount_out,
279 max_amount_in: p.max_amount_in,
280 })
281}
282
283#[derive(Clone, Debug)]
284pub struct BuildSellExactInParams<'a> {
285 pub user: &'a Address,
286 pub payer: Option<&'a Address>,
287 pub base_mint: &'a Address,
288 pub quote_mint: &'a Address,
289 pub partner: &'a Address,
290 pub platform_config: &'a Address,
291 pub quote_token_program: &'a Address,
292 pub amount_in: u64,
293 pub min_amount_out: u64,
294}
295
296#[must_use]
297pub fn build_sell_exact_in_instruction(
298 p: &BuildSellExactInParams<'_>,
299) -> solana_instruction::Instruction {
300 launchpad_trade_accounts!(
301 super::instructions::SellExactIn,
302 &derived_trade_accounts!(p)
303 )
304 .instruction(super::instructions::SellExactInInstructionArgs {
305 amount_in: p.amount_in,
306 min_amount_out: p.min_amount_out,
307 })
308}
309
310#[derive(Clone, Debug)]
311pub struct BuildSellExactOutParams<'a> {
312 pub user: &'a Address,
313 pub payer: Option<&'a Address>,
314 pub base_mint: &'a Address,
315 pub quote_mint: &'a Address,
316 pub partner: &'a Address,
317 pub platform_config: &'a Address,
318 pub quote_token_program: &'a Address,
319 pub amount_out: u64,
320 pub max_amount_in: u64,
321}
322
323#[must_use]
324pub fn build_sell_exact_out_instruction(
325 p: &BuildSellExactOutParams<'_>,
326) -> solana_instruction::Instruction {
327 launchpad_trade_accounts!(
328 super::instructions::SellExactOut,
329 &derived_trade_accounts!(p)
330 )
331 .instruction(super::instructions::SellExactOutInstructionArgs {
332 amount_out: p.amount_out,
333 max_amount_in: p.max_amount_in,
334 })
335}
336
337#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
338pub struct TradeArgs {
339 pub direction: TradeDirection,
340 pub mode: TradeMode,
341 pub amount: u64,
343 pub limit: u64,
345}
346
347#[must_use]
348pub fn trade_instruction(
349 accounts: &TradeAccounts,
350 args: TradeArgs,
351) -> solana_instruction::Instruction {
352 let TradeArgs {
353 direction,
354 mode,
355 amount,
356 limit,
357 } = args;
358 match (direction, mode) {
359 (TradeDirection::Buy, TradeMode::ExactIn) => {
360 launchpad_trade_accounts!(super::instructions::BuyExactIn, accounts)
361 .instruction(super::instructions::BuyExactInInstructionArgs {
362 amount_in: amount,
363 min_amount_out: limit,
364 })
365 }
366 (TradeDirection::Buy, TradeMode::ExactOut) => {
367 launchpad_trade_accounts!(
368 super::instructions::BuyExactOut,
369 accounts
370 )
371 .instruction(super::instructions::BuyExactOutInstructionArgs {
372 amount_out: amount,
373 max_amount_in: limit,
374 })
375 }
376 (TradeDirection::Sell, TradeMode::ExactIn) => {
377 launchpad_trade_accounts!(
378 super::instructions::SellExactIn,
379 accounts
380 )
381 .instruction(super::instructions::SellExactInInstructionArgs {
382 amount_in: amount,
383 min_amount_out: limit,
384 })
385 }
386 (TradeDirection::Sell, TradeMode::ExactOut) => {
387 launchpad_trade_accounts!(
388 super::instructions::SellExactOut,
389 accounts
390 )
391 .instruction(super::instructions::SellExactOutInstructionArgs {
392 amount_out: amount,
393 max_amount_in: limit,
394 })
395 }
396 }
397}
398
399#[must_use]
400pub fn trade_account_metas(
401 accounts: &TradeAccounts,
402) -> Vec<solana_instruction::AccountMeta> {
403 launchpad_trade_accounts!(super::instructions::BuyExactIn, accounts)
404 .instruction(super::instructions::BuyExactInInstructionArgs {
405 amount_in: 0,
406 min_amount_out: 0,
407 })
408 .accounts
409}
410
411#[must_use]
412pub fn trade_data(args: TradeArgs) -> Vec<u8> {
413 trade_instruction(&TradeAccounts::default(), args).data
415}
416
417macro_rules! launchpad_trade_accounts {
418 ($ty:path, $accounts:expr) => {{
419 let a: &TradeAccounts = $accounts;
420 $ty {
421 user: a.user,
422 payer: a.payer,
423 bonding_curve: a.market,
424 base_mint: a.base_mint,
425 quote_mint: a.quote_mint,
426 base_vault: a.base_vault,
427 quote_vault: a.quote_vault,
428 user_base_account: a.user_base_account,
429 user_quote_account: a.user_quote_account,
430 partner: partner_account(&a.partner),
431 partner_config: a.partner_config,
432 base_token_program: TOKEN_2022_PROGRAM_ID,
433 quote_token_program: a.quote_token_program,
434 associated_token_program: ATA_PROGRAM_ID,
435 system_program: SYSTEM_PROGRAM_ID,
436 event_authority: super::pda::EVENT_AUTHORITY_ADDRESS,
437 program: LAUNCHPAD_PROGRAM_ID,
438 }
439 }};
440}
441use launchpad_trade_accounts;
442
443macro_rules! derived_trade_accounts {
444 ($p:expr) => {
445 derive(
446 &TradeStateFields {
447 base_mint: $p.base_mint,
448 quote_mint: $p.quote_mint,
449 platform_config: $p.platform_config,
450 },
451 TradeUserContext {
452 user: $p.user,
453 partner: $p.partner,
454 quote_token_program: $p.quote_token_program,
455 },
456 $p.payer,
457 )
458 };
459}
460use derived_trade_accounts;
461
462fn derive(
463 state: &TradeStateFields<'_>,
464 ctx: TradeUserContext<'_>,
465 payer: Option<&Address>,
466) -> TradeAccounts {
467 let (bonding_curve, _) =
468 super::pda::find_bonding_curve_pda(state.base_mint, state.quote_mint);
469 let (base_vault, _) = crate::utils::find_associated_token_pda(
471 &bonding_curve,
472 state.base_mint,
473 &TOKEN_2022_PROGRAM_ID,
474 );
475 let (quote_vault, _) = crate::utils::find_associated_token_pda(
476 &bonding_curve,
477 state.quote_mint,
478 ctx.quote_token_program,
479 );
480 let user = *ctx.user;
481 let payer = *payer.unwrap_or(ctx.user);
482 let partner = *ctx.partner;
483 let quote_token_program = *ctx.quote_token_program;
484 let trade = derive_trade_accounts(DeriveTradeArgs { state, ctx });
485 TradeAccounts {
486 user,
487 payer,
488 market: bonding_curve,
489 base_mint: *state.base_mint,
490 quote_mint: *state.quote_mint,
491 base_vault,
492 quote_vault,
493 user_base_account: trade.user_base_account,
494 user_quote_account: trade.user_quote_account,
495 partner,
496 partner_config: trade.partner_config,
497 quote_token_program,
498 }
499}
500
501#[cfg(test)]
502mod tests {
503 use solana_address::Address;
504
505 use super::{
506 BuildBuyExactInParams, BuildBuyExactOutParams, BuildSellExactInParams,
507 BuildSellExactOutParams, BuyExactInParams, BuyExactOutParams,
508 TradeArgs, build_buy_exact_in_instruction,
509 build_buy_exact_out_instruction, build_sell_exact_in_instruction,
510 build_sell_exact_out_instruction, buy_exact_in, buy_exact_out,
511 trade_account_metas, trade_data, trade_instruction,
512 };
513 use crate::constants::{
514 ATA_PROGRAM_ID, DEFAULT_PARTNER, LAUNCHPAD_PROGRAM_ID,
515 SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, WSOL_MINT,
516 };
517 use crate::launchpad::events::TradeEvent;
518 use crate::launchpad::types::TradeDirection;
519 use crate::math::amm::MintFee;
520 use crate::utils::{TradeAccounts, TradeMode};
521 use solana_instruction::AccountMeta;
522
523 const BONDING_CURVE_INDEX: usize = 2;
524 const BASE_VAULT_INDEX: usize = 5;
525 const QUOTE_VAULT_INDEX: usize = 6;
526 const PARTNER_INDEX: usize = 9;
527 const PARTNER_CONFIG_INDEX: usize = 10;
528
529 const USER: Address = Address::new_from_array([1u8; 32]);
530 const BASE_MINT: Address = Address::new_from_array([2u8; 32]);
531 const PARTNER: Address = Address::new_from_array([3u8; 32]);
532 const PLATFORM_CONFIG: Address = Address::new_from_array([4u8; 32]);
533
534 fn assert_derived_market_accounts(
535 ix: &solana_instruction::Instruction,
536 quote_mint: &Address,
537 quote_token_program: &Address,
538 ) {
539 let (bonding_curve, _) = crate::launchpad::pda::find_bonding_curve_pda(
540 &BASE_MINT, quote_mint,
541 );
542 let (base_vault, _) = crate::utils::find_associated_token_pda(
543 &bonding_curve,
544 &BASE_MINT,
545 &TOKEN_2022_PROGRAM_ID,
546 );
547 let (quote_vault, _) = crate::utils::find_associated_token_pda(
548 &bonding_curve,
549 quote_mint,
550 quote_token_program,
551 );
552 assert_eq!(ix.accounts.len(), 17);
553 assert_eq!(ix.accounts[BONDING_CURVE_INDEX].pubkey, bonding_curve);
554 assert_eq!(ix.accounts[BASE_VAULT_INDEX].pubkey, base_vault);
555 assert_eq!(ix.accounts[QUOTE_VAULT_INDEX].pubkey, quote_vault);
556 assert_eq!(ix.accounts[PARTNER_INDEX].pubkey, PARTNER);
557 assert!(ix.accounts[PARTNER_INDEX].is_signer);
558 }
559
560 #[test]
561 fn build_buy_exact_in_derives_the_market_accounts() {
562 let ix = build_buy_exact_in_instruction(&BuildBuyExactInParams {
563 user: &USER,
564 payer: None,
565 base_mint: &BASE_MINT,
566 quote_mint: &WSOL_MINT,
567 partner: &PARTNER,
568 platform_config: &PLATFORM_CONFIG,
569 quote_token_program: &TOKEN_PROGRAM_ID,
570 amount_in: 1_000,
571 min_amount_out: 900,
572 });
573 assert_derived_market_accounts(&ix, &WSOL_MINT, &TOKEN_PROGRAM_ID);
574 }
575
576 #[test]
577 fn build_buy_exact_out_derives_the_market_accounts() {
578 let ix = build_buy_exact_out_instruction(&BuildBuyExactOutParams {
579 user: &USER,
580 payer: None,
581 base_mint: &BASE_MINT,
582 quote_mint: &WSOL_MINT,
583 partner: &PARTNER,
584 platform_config: &PLATFORM_CONFIG,
585 quote_token_program: &TOKEN_PROGRAM_ID,
586 amount_out: 1_000,
587 max_amount_in: 1_100,
588 });
589 assert_derived_market_accounts(&ix, &WSOL_MINT, &TOKEN_PROGRAM_ID);
590 }
591
592 #[test]
593 fn build_sell_exact_in_derives_the_market_accounts() {
594 let ix = build_sell_exact_in_instruction(&BuildSellExactInParams {
595 user: &USER,
596 payer: None,
597 base_mint: &BASE_MINT,
598 quote_mint: &WSOL_MINT,
599 partner: &PARTNER,
600 platform_config: &PLATFORM_CONFIG,
601 quote_token_program: &TOKEN_PROGRAM_ID,
602 amount_in: 1_000,
603 min_amount_out: 900,
604 });
605 assert_derived_market_accounts(&ix, &WSOL_MINT, &TOKEN_PROGRAM_ID);
606 }
607
608 #[test]
609 fn build_sell_exact_out_derives_the_market_accounts() {
610 let ix = build_sell_exact_out_instruction(&BuildSellExactOutParams {
611 user: &USER,
612 payer: None,
613 base_mint: &BASE_MINT,
614 quote_mint: &WSOL_MINT,
615 partner: &PARTNER,
616 platform_config: &PLATFORM_CONFIG,
617 quote_token_program: &TOKEN_PROGRAM_ID,
618 amount_out: 1_000,
619 max_amount_in: 1_100,
620 });
621 assert_derived_market_accounts(&ix, &WSOL_MINT, &TOKEN_PROGRAM_ID);
622 }
623
624 #[test]
627 fn the_quote_vault_follows_the_quote_token_program() {
628 let quote_mint = Address::new_from_array([5u8; 32]);
629 let ix = build_buy_exact_in_instruction(&BuildBuyExactInParams {
630 user: &USER,
631 payer: None,
632 base_mint: &BASE_MINT,
633 quote_mint: "e_mint,
634 partner: &PARTNER,
635 platform_config: &PLATFORM_CONFIG,
636 quote_token_program: &TOKEN_2022_PROGRAM_ID,
637 amount_in: 1_000,
638 min_amount_out: 900,
639 });
640 assert_derived_market_accounts(
641 &ix,
642 "e_mint,
643 &TOKEN_2022_PROGRAM_ID,
644 );
645 }
646
647 #[test]
649 fn buy_exact_in_builds_from_a_trade_event() {
650 let (bonding_curve, _) = crate::launchpad::pda::find_bonding_curve_pda(
651 &BASE_MINT, &WSOL_MINT,
652 );
653 let event = TradeEvent {
654 bonding_curve,
655 base_mint: BASE_MINT,
656 quote_mint: WSOL_MINT,
657 user: Address::new_from_array([6u8; 32]),
658 coin_creator: Address::new_from_array([7u8; 32]),
659 padding0: [0; 4],
660 platform_config: PLATFORM_CONFIG,
661 base_amount: 0,
662 quote_amount_gross: 0,
663 quote_amount_net: 0,
664 trade_direction: TradeDirection::Buy,
665 partner: Address::new_from_array([9u8; 32]),
666 is_partner: false,
667 protocol_fee: 0,
668 creator_fee: 0,
669 base_transfer_fee: 0,
670 quote_transfer_fee: 0,
671 virtual_base_reserves: 1_000_000_000_000_000,
672 virtual_quote_reserves: 30_000_000_000,
673 real_base_reserves: 500_000_000_000_000,
674 real_quote_reserves: 0,
675 timestamp: 0,
676 creator_fee_mode: 0,
677 creator_fee_bps: 0,
678 };
679
680 let (ix, quote) = buy_exact_in(&BuyExactInParams {
681 user: &USER,
682 payer: None,
683 base_mint: &event.base_mint,
684 quote_mint: &event.quote_mint,
685 virtual_base_reserves: event.virtual_base_reserves,
686 virtual_quote_reserves: event.virtual_quote_reserves,
687 fee_bps: 100,
688 slippage_bps: 100,
689 partner: &PARTNER,
690 platform_config: &event.platform_config,
691 quote_token_program: &TOKEN_PROGRAM_ID,
692 quote_fee: None,
693 base_fee: None,
694 real_base_reserves: event.real_base_reserves,
695 quote_amount: 1_000_000_000,
696 })
697 .unwrap();
698
699 assert_eq!(quote.quote_from_user, 1_000_000_000);
700 assert_eq!(quote.fee, 10_000_000);
701 assert_eq!(quote.base_to_user, 31_945_788_964_181);
702
703 assert_derived_market_accounts(&ix, &WSOL_MINT, &TOKEN_PROGRAM_ID);
704 assert_eq!(
705 ix.accounts[BONDING_CURVE_INDEX].pubkey,
706 event.bonding_curve
707 );
708 let (partner_config, _) = crate::nexus::pda::find_partner_config_pda(
709 &event.platform_config,
710 &PARTNER,
711 );
712 assert_eq!(ix.accounts[PARTNER_CONFIG_INDEX].pubkey, partner_config);
713
714 assert_eq!(
715 ix.data[..8],
716 crate::launchpad::instructions::BUY_EXACT_IN_DISCRIMINATOR
717 );
718 let args = <crate::launchpad::instructions::BuyExactInInstructionArgs as borsh::BorshDeserialize>::try_from_slice(&ix.data[8..]).unwrap();
719 assert_eq!(args.amount_in, 1_000_000_000);
720 assert_eq!(args.min_amount_out, 31_626_331_074_539);
722 }
723
724 fn signed_buy_exact_out(
725 real_base_reserves: u64,
726 base_fee: Option<MintFee>,
727 base_amount: u64,
728 ) -> (u64, crate::math::amm::BuyQuote) {
729 let (ix, quote) = buy_exact_out(&BuyExactOutParams {
730 user: &USER,
731 payer: None,
732 base_mint: &BASE_MINT,
733 quote_mint: &WSOL_MINT,
734 virtual_base_reserves: 1_000_000_000_000_000,
735 virtual_quote_reserves: 30_000_000_000,
736 fee_bps: 100,
737 slippage_bps: 100,
738 partner: &PARTNER,
739 platform_config: &PLATFORM_CONFIG,
740 quote_token_program: &TOKEN_PROGRAM_ID,
741 quote_fee: None,
742 base_fee,
743 real_base_reserves,
744 base_amount,
745 })
746 .unwrap();
747 assert_eq!(
748 ix.data[..8],
749 crate::launchpad::instructions::BUY_EXACT_OUT_DISCRIMINATOR
750 );
751 let args = <crate::launchpad::instructions::BuyExactOutInstructionArgs as borsh::BorshDeserialize>::try_from_slice(&ix.data[8..]).unwrap();
752 (args.amount_out, quote)
753 }
754
755 #[test]
756 fn buy_exact_out_signs_an_uncapped_request_unchanged() {
757 let (amount_out, quote) =
758 signed_buy_exact_out(500_000_000_000_000, None, 10_000_000_000);
759 assert_eq!(quote.base_to_user, 10_000_000_000);
760 assert_eq!(amount_out, 10_000_000_000);
761 }
762
763 #[test]
764 fn buy_exact_out_clamps_a_request_over_the_supply_left() {
765 let (amount_out, quote) =
766 signed_buy_exact_out(1_000_000_000, None, 5_000_000_000);
767 assert_eq!(quote.base_to_user, 1_000_000_000);
768 assert_eq!(amount_out, 1_000_000_000);
769 }
770
771 #[test]
772 fn buy_exact_out_clamps_to_the_net_under_a_base_transfer_fee() {
773 let (amount_out, quote) = signed_buy_exact_out(
774 1_000_000_000,
775 Some(MintFee {
776 bps: 20,
777 maximum_fee: u64::MAX,
778 }),
779 5_000_000_000,
780 );
781 assert_eq!(quote.base_amount, 1_000_000_000);
782 assert_eq!(quote.base_to_user, 998_000_000);
783 assert_eq!(amount_out, 998_000_000);
784 }
785
786 const AMOUNT: u64 = 1_234_567;
787 const LIMIT: u64 = 7_654_321;
788
789 const ACCOUNTS: TradeAccounts = TradeAccounts {
791 user: Address::new_from_array([11u8; 32]),
792 payer: Address::new_from_array([12u8; 32]),
793 market: Address::new_from_array([13u8; 32]),
794 base_mint: Address::new_from_array([14u8; 32]),
795 quote_mint: Address::new_from_array([15u8; 32]),
796 base_vault: Address::new_from_array([16u8; 32]),
797 quote_vault: Address::new_from_array([17u8; 32]),
798 user_base_account: Address::new_from_array([18u8; 32]),
799 user_quote_account: Address::new_from_array([19u8; 32]),
800 partner: Address::new_from_array([20u8; 32]),
801 partner_config: Address::new_from_array([21u8; 32]),
802 quote_token_program: Address::new_from_array([22u8; 32]),
803 };
804
805 const TRADES: [(TradeDirection, TradeMode); 4] = [
806 (TradeDirection::Buy, TradeMode::ExactIn),
807 (TradeDirection::Buy, TradeMode::ExactOut),
808 (TradeDirection::Sell, TradeMode::ExactIn),
809 (TradeDirection::Sell, TradeMode::ExactOut),
810 ];
811
812 const fn trade_args(
813 direction: TradeDirection,
814 mode: TradeMode,
815 ) -> TradeArgs {
816 TradeArgs {
817 direction,
818 mode,
819 amount: AMOUNT,
820 limit: LIMIT,
821 }
822 }
823
824 macro_rules! by_hand {
825 ($ty:ident) => {
826 crate::launchpad::instructions::$ty {
827 user: ACCOUNTS.user,
828 payer: ACCOUNTS.payer,
829 bonding_curve: ACCOUNTS.market,
830 base_mint: ACCOUNTS.base_mint,
831 quote_mint: ACCOUNTS.quote_mint,
832 base_vault: ACCOUNTS.base_vault,
833 quote_vault: ACCOUNTS.quote_vault,
834 user_base_account: ACCOUNTS.user_base_account,
835 user_quote_account: ACCOUNTS.user_quote_account,
836 partner: (ACCOUNTS.partner, true),
837 partner_config: ACCOUNTS.partner_config,
838 base_token_program: TOKEN_2022_PROGRAM_ID,
839 quote_token_program: ACCOUNTS.quote_token_program,
840 associated_token_program: ATA_PROGRAM_ID,
841 system_program: SYSTEM_PROGRAM_ID,
842 event_authority: crate::launchpad::pda::EVENT_AUTHORITY_ADDRESS,
843 program: LAUNCHPAD_PROGRAM_ID,
844 }
845 };
846 }
847
848 #[test]
849 fn trade_instruction_maps_every_account_onto_each_builder() {
850 use crate::launchpad::instructions::{
851 BuyExactInInstructionArgs, BuyExactOutInstructionArgs,
852 SellExactInInstructionArgs, SellExactOutInstructionArgs,
853 };
854 let expected = [
855 by_hand!(BuyExactIn).instruction(BuyExactInInstructionArgs {
856 amount_in: AMOUNT,
857 min_amount_out: LIMIT,
858 }),
859 by_hand!(BuyExactOut).instruction(BuyExactOutInstructionArgs {
860 amount_out: AMOUNT,
861 max_amount_in: LIMIT,
862 }),
863 by_hand!(SellExactIn).instruction(SellExactInInstructionArgs {
864 amount_in: AMOUNT,
865 min_amount_out: LIMIT,
866 }),
867 by_hand!(SellExactOut).instruction(SellExactOutInstructionArgs {
868 amount_out: AMOUNT,
869 max_amount_in: LIMIT,
870 }),
871 ];
872 for ((direction, mode), expected) in TRADES.into_iter().zip(expected) {
873 assert_eq!(
874 trade_instruction(&ACCOUNTS, trade_args(direction, mode)),
875 expected,
876 "{direction:?} {mode:?}"
877 );
878 }
879 }
880
881 #[test]
882 fn all_four_trades_share_the_account_metas() {
883 for (direction, mode) in TRADES {
884 assert_eq!(
885 trade_account_metas(&ACCOUNTS),
886 trade_instruction(&ACCOUNTS, trade_args(direction, mode))
887 .accounts,
888 "{direction:?} {mode:?}"
889 );
890 }
891 }
892
893 #[test]
894 fn trade_data_selects_each_trades_discriminator() {
895 use crate::launchpad::instructions::{
896 BUY_EXACT_IN_DISCRIMINATOR, BUY_EXACT_OUT_DISCRIMINATOR,
897 SELL_EXACT_IN_DISCRIMINATOR, SELL_EXACT_OUT_DISCRIMINATOR,
898 };
899 let discriminators = [
900 BUY_EXACT_IN_DISCRIMINATOR,
901 BUY_EXACT_OUT_DISCRIMINATOR,
902 SELL_EXACT_IN_DISCRIMINATOR,
903 SELL_EXACT_OUT_DISCRIMINATOR,
904 ];
905 for ((direction, mode), discriminator) in
906 TRADES.into_iter().zip(discriminators)
907 {
908 assert_eq!(
909 trade_data(trade_args(direction, mode)),
910 [discriminator, AMOUNT.to_le_bytes(), LIMIT.to_le_bytes()]
911 .concat(),
912 "{direction:?} {mode:?}"
913 );
914 }
915 }
916
917 #[test]
918 fn trade_data_ignores_the_accounts() {
919 for (direction, mode) in TRADES {
920 let args = trade_args(direction, mode);
921 assert_eq!(
922 trade_data(args),
923 trade_instruction(&ACCOUNTS, args).data
924 );
925 }
926 }
927
928 #[test]
930 fn only_a_partner_other_than_the_default_signs() {
931 let unpartnered = TradeAccounts {
932 partner: DEFAULT_PARTNER,
933 ..ACCOUNTS
934 };
935 assert_eq!(
936 trade_account_metas(&unpartnered)[PARTNER_INDEX],
937 AccountMeta::new_readonly(DEFAULT_PARTNER, false)
938 );
939 assert_eq!(
940 trade_account_metas(&ACCOUNTS)[PARTNER_INDEX],
941 AccountMeta::new_readonly(ACCOUNTS.partner, true)
942 );
943 }
944}