Skip to main content

sendfun_sdk/dex/
trade.rs

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