Skip to main content

sendfun_sdk/launchpad/
trade.rs

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	/// The quote mint's transfer fee at the landing epoch. `None` only for a
27	/// mint without a transfer fee. A wrong value gives a wrong slippage limit.
28	pub quote_fee: Option<amm::MintFee>,
29	/// The base mint's transfer fee. Same rules as `quote_fee`.
30	pub base_fee: Option<amm::MintFee>,
31	/// Caps the fill at the supply left on the curve.
32	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		// The program caps every buy at the supply left on the curve.
49		base_reserve_cap: Some(p.real_base_reserves),
50	})?;
51	// The program applies `min_amount_out` to the base that the buyer receives.
52	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	/// The quote mint's transfer fee at the landing epoch. `None` only for a
82	/// mint without a transfer fee. A wrong value gives a wrong slippage limit.
83	pub quote_fee: Option<amm::MintFee>,
84	/// The base mint's transfer fee. Same rules as `quote_fee`.
85	pub base_fee: Option<amm::MintFee>,
86	/// Caps the fill at the supply left on the curve.
87	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	// The program applies `max_amount_in` to the quote that the buyer sends,
106	// with the transfer fee.
107	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		// The capped amount. The program refuses an `amount_out` above the
118		// supply left.
119		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	/// The quote mint's transfer fee at the landing epoch. `None` only for a
139	/// mint without a transfer fee. A wrong value gives a wrong slippage limit.
140	pub quote_fee: Option<amm::MintFee>,
141	/// The base mint's transfer fee. Same rules as `quote_fee`.
142	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	// The program applies `min_amount_out` to the quote that the seller
160	// receives.
161	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	/// The quote mint's transfer fee at the landing epoch. `None` only for a
191	/// mint without a transfer fee. A wrong value gives a wrong slippage limit.
192	pub quote_fee: Option<amm::MintFee>,
193	/// The base mint's transfer fee. Same rules as `quote_fee`.
194	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	// The program applies `max_amount_in` to the base that the seller sends,
212	// with the transfer fee.
213	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	/// `amount_in` for `ExactIn`. `amount_out` for `ExactOut`.
342	pub amount: u64,
343	/// `min_amount_out` for `ExactIn`. `max_amount_in` for `ExactOut`.
344	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	// Default accounts: `trade_instruction` must not check its accounts.
414	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	// Every base mint is a Token-2022 mint.
470	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	/// The quote token program changes the quote vault address, not the base
625	/// vault address.
626	#[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: &quote_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			&quote_mint,
643			&TOKEN_2022_PROGRAM_ID,
644		);
645	}
646
647	/// Builds a trade from a `TradeEvent` alone.
648	#[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		// 31_945_788_964_181 * 9_900 / 10_000, floored.
721		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	/// Each key is different, so a wrong field mapping fails the test.
790	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	/// `DEFAULT_PARTNER` is the system program. It cannot sign.
929	#[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}