Skip to main content

sendfun_sdk/utils/
trade.rs

1use std::fmt;
2
3use solana_address::Address;
4
5use super::pda::find_associated_token_pda;
6use crate::constants::TOKEN_2022_PROGRAM_ID;
7use crate::math::amm::{BuyQuote, MintFee, QuoteError, SellQuote};
8
9/// `launchpad::types::TradeDirection`. `.into()` converts it to
10/// `dex::types::TradeDirection`.
11pub use crate::launchpad::types::TradeDirection;
12
13impl From<TradeDirection> for crate::dex::types::TradeDirection {
14	fn from(direction: TradeDirection) -> Self {
15		match direction {
16			TradeDirection::Buy => Self::Buy,
17			TradeDirection::Sell => Self::Sell,
18		}
19	}
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub enum TradeMode {
24	ExactIn,
25	ExactOut,
26}
27
28/// The accounts of a trade instruction. `user_base_account` and
29/// `user_quote_account` can be any token accounts that `user` owns.
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub struct TradeAccounts {
32	pub user: Address,
33	pub payer: Address,
34	/// Bonding curve or pool.
35	pub market: Address,
36	pub base_mint: Address,
37	pub quote_mint: Address,
38	pub base_vault: Address,
39	pub quote_vault: Address,
40	pub user_base_account: Address,
41	pub user_quote_account: Address,
42	/// Signs unless it is `DEFAULT_PARTNER`.
43	pub partner: Address,
44	/// The `PartnerConfig` PDA of `partner` on the market's `platform_config`.
45	pub partner_config: Address,
46	pub quote_token_program: Address,
47}
48
49impl TradeAccounts {
50	/// Sets `user`, `payer`, `user_base_account` and `user_quote_account` to
51	/// fixed keys that no wallet holds.
52	#[must_use]
53	pub const fn with_stand_in_trader(self) -> StandInTrade {
54		StandInTrade(Self {
55			user: Address::new_from_array([1; 32]),
56			payer: Address::new_from_array([2; 32]),
57			user_base_account: Address::new_from_array([3; 32]),
58			user_quote_account: Address::new_from_array([4; 32]),
59			..self
60		})
61	}
62}
63
64/// A trade with the keys of [`TradeAccounts::with_stand_in_trader`].
65/// [`MarketHooks::screen`](crate::transfer_hook::MarketHooks::screen) takes
66/// only this type.
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub struct StandInTrade(TradeAccounts);
69
70impl std::ops::Deref for StandInTrade {
71	type Target = TradeAccounts;
72
73	fn deref(&self) -> &TradeAccounts {
74		&self.0
75	}
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79pub struct QuoteRequest {
80	pub direction: TradeDirection,
81	pub mode: TradeMode,
82	/// `ExactIn`: the amount that the user sends. `ExactOut`: the amount that
83	/// the user receives.
84	pub amount: u64,
85}
86
87/// The transfer fees and the Unix time, in seconds, when the trade lands. Get
88/// each fee from
89/// [`MintState::fee_at`](crate::transfer_hook::MintState::fee_at) with the
90/// landing epoch.
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92pub struct Landing {
93	pub base_fee: Option<MintFee>,
94	pub quote_fee: Option<MintFee>,
95	pub unix_timestamp: i64,
96}
97
98/// The trade that the program would make. The caller decides whether to send
99/// it.
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101#[non_exhaustive]
102pub struct MarketQuote {
103	/// The amount that the user sends, with the input mint's transfer fee.
104	pub in_amount: u64,
105	/// The amount that the user receives, after the output mint's transfer fee.
106	pub out_amount: u64,
107	/// Platform fee, in quote units.
108	pub fee: u64,
109	/// Platform fee rate in bps, with the fee decay premium.
110	pub fee_bps: u16,
111	/// `true` when the supply left on the curve makes the fill smaller than the
112	/// request. The program fills a capped `ExactIn` buy. It refuses a capped
113	/// `ExactOut` buy.
114	pub supply_capped: bool,
115}
116
117impl MarketQuote {
118	/// Only the supply cap makes a buy smaller than its request.
119	pub(crate) const fn bought(
120		quote: &BuyQuote,
121		request: QuoteRequest,
122		fee_bps: u16,
123	) -> Self {
124		let requested = match request.mode {
125			TradeMode::ExactIn => quote.quote_from_user,
126			TradeMode::ExactOut => quote.base_to_user,
127		};
128		Self {
129			in_amount: quote.quote_from_user,
130			out_amount: quote.base_to_user,
131			fee: quote.fee,
132			fee_bps,
133			supply_capped: requested < request.amount,
134		}
135	}
136
137	pub(crate) const fn sold(quote: &SellQuote, fee_bps: u16) -> Self {
138		Self {
139			in_amount: quote.base_from_user,
140			out_amount: quote.quote_to_user,
141			fee: quote.fee,
142			fee_bps,
143			supply_capped: false,
144		}
145	}
146}
147
148/// A trade that the program refuses.
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150#[non_exhaustive]
151pub enum MarketQuoteError {
152	Quote(QuoteError),
153	FeeOutOfRange,
154	/// `real_base_reserves` is 0. The program error is `ThresholdReached`.
155	SupplyExhausted,
156	/// The sell takes more quote from the curve than `real_quote_reserves`.
157	ExceedsQuoteHeld,
158}
159
160impl From<QuoteError> for MarketQuoteError {
161	fn from(error: QuoteError) -> Self {
162		Self::Quote(error)
163	}
164}
165
166impl fmt::Display for MarketQuoteError {
167	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168		match self {
169			Self::Quote(error) => error.fmt(f),
170			Self::FeeOutOfRange => write!(f, "fee out of range"),
171			Self::SupplyExhausted => write!(f, "the curve has no supply left"),
172			Self::ExceedsQuoteHeld => {
173				write!(f, "sell exceeds the quote the curve holds")
174			}
175		}
176	}
177}
178
179impl std::error::Error for MarketQuoteError {
180	fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
181		match self {
182			Self::Quote(error) => Some(error),
183			_ => None,
184		}
185	}
186}
187
188pub(crate) struct TradeStateFields<'a> {
189	pub base_mint: &'a Address,
190	pub quote_mint: &'a Address,
191	/// The `platform_config` of the curve or pool. Seeds `partner_config`.
192	pub platform_config: &'a Address,
193}
194
195pub(crate) struct DerivedTradeAccounts {
196	pub user_base_account: Address,
197	pub user_quote_account: Address,
198	pub partner_config: Address,
199}
200
201pub(crate) struct TradeUserContext<'a> {
202	pub user: &'a Address,
203	pub partner: &'a Address,
204	pub quote_token_program: &'a Address,
205}
206
207pub(crate) struct DeriveTradeArgs<'a> {
208	pub state: &'a TradeStateFields<'a>,
209	pub ctx: TradeUserContext<'a>,
210}
211
212pub(crate) fn derive_trade_accounts(
213	args: DeriveTradeArgs<'_>,
214) -> DerivedTradeAccounts {
215	let state = args.state;
216	let user = args.ctx.user;
217	let partner = args.ctx.partner;
218	let quote_token_program = args.ctx.quote_token_program;
219
220	let (user_base_account, _) = find_associated_token_pda(
221		user,
222		state.base_mint,
223		&TOKEN_2022_PROGRAM_ID,
224	);
225	let (user_quote_account, _) =
226		find_associated_token_pda(user, state.quote_mint, quote_token_program);
227	let (partner_config, _) = crate::nexus::pda::find_partner_config_pda(
228		state.platform_config,
229		partner,
230	);
231
232	DerivedTradeAccounts {
233		user_base_account,
234		user_quote_account,
235		partner_config,
236	}
237}