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 and from
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
22impl From<crate::dex::types::TradeDirection> for TradeDirection {
23	fn from(direction: crate::dex::types::TradeDirection) -> Self {
24		match direction {
25			crate::dex::types::TradeDirection::Buy => Self::Buy,
26			crate::dex::types::TradeDirection::Sell => Self::Sell,
27		}
28	}
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
32pub enum TradeMode {
33	ExactIn,
34	ExactOut,
35}
36
37/// The accounts of a trade instruction. `user_base_account` and
38/// `user_quote_account` can be any token accounts that `user` owns.
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
40pub struct TradeAccounts {
41	pub user: Address,
42	pub payer: Address,
43	/// Bonding curve or pool.
44	pub market: Address,
45	pub base_mint: Address,
46	pub quote_mint: Address,
47	pub base_vault: Address,
48	pub quote_vault: Address,
49	pub user_base_account: Address,
50	pub user_quote_account: Address,
51	/// Signs unless it is `DEFAULT_PARTNER`.
52	pub partner: Address,
53	/// The `PartnerConfig` PDA of `partner` on the market's `platform_config`.
54	pub partner_config: Address,
55	pub quote_token_program: Address,
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
59pub struct QuoteRequest {
60	pub direction: TradeDirection,
61	pub mode: TradeMode,
62	/// `ExactIn`: the amount that the user sends. `ExactOut`: the amount that
63	/// the user receives.
64	pub amount: u64,
65}
66
67/// The transfer fees and the Unix time, in seconds, when the trade lands. Get
68/// each fee from
69/// [`FeeSchedule::fee_at`](crate::transfer_fee::FeeSchedule::fee_at) with the
70/// landing epoch.
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub struct Landing {
73	pub base_fee: Option<MintFee>,
74	pub quote_fee: Option<MintFee>,
75	pub unix_timestamp: i64,
76}
77
78/// The trade that the program would make. The caller decides whether to send
79/// it.
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81#[non_exhaustive]
82pub struct MarketQuote {
83	/// The amount that the user sends, with the input mint's transfer fee.
84	pub in_amount: u64,
85	/// The amount that the user receives, after the output mint's transfer fee.
86	pub out_amount: u64,
87	/// Platform fee, in quote units.
88	pub fee: u64,
89	/// Platform fee rate in bps, with the fee decay premium.
90	pub fee_bps: u16,
91	/// `true` when the supply left on the curve makes the fill smaller than the
92	/// request. The program fills a capped `ExactIn` buy. It refuses a capped
93	/// `ExactOut` buy.
94	pub supply_capped: bool,
95}
96
97impl MarketQuote {
98	/// Only the supply cap makes a buy smaller than its request.
99	pub(crate) const fn bought(
100		quote: &BuyQuote,
101		request: QuoteRequest,
102		fee_bps: u16,
103	) -> Self {
104		let requested = match request.mode {
105			TradeMode::ExactIn => quote.quote_from_user,
106			TradeMode::ExactOut => quote.base_to_user,
107		};
108		Self {
109			in_amount: quote.quote_from_user,
110			out_amount: quote.base_to_user,
111			fee: quote.fee,
112			fee_bps,
113			supply_capped: requested < request.amount,
114		}
115	}
116
117	pub(crate) const fn sold(quote: &SellQuote, fee_bps: u16) -> Self {
118		Self {
119			in_amount: quote.base_from_user,
120			out_amount: quote.quote_to_user,
121			fee: quote.fee,
122			fee_bps,
123			supply_capped: false,
124		}
125	}
126}
127
128/// A trade that the program refuses.
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130#[non_exhaustive]
131pub enum MarketQuoteError {
132	Quote(QuoteError),
133	FeeOutOfRange,
134	/// `real_base_reserves` is 0. The program error is `ThresholdReached`.
135	SupplyExhausted,
136	/// The sell takes more quote from the curve than `real_quote_reserves`.
137	ExceedsQuoteHeld,
138}
139
140impl From<QuoteError> for MarketQuoteError {
141	fn from(error: QuoteError) -> Self {
142		Self::Quote(error)
143	}
144}
145
146impl fmt::Display for MarketQuoteError {
147	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148		match self {
149			Self::Quote(error) => error.fmt(f),
150			Self::FeeOutOfRange => write!(f, "fee out of range"),
151			Self::SupplyExhausted => write!(f, "the curve has no supply left"),
152			Self::ExceedsQuoteHeld => {
153				write!(f, "sell exceeds the quote the curve holds")
154			}
155		}
156	}
157}
158
159impl std::error::Error for MarketQuoteError {}
160
161pub(crate) struct TradeStateFields<'a> {
162	pub base_mint: &'a Address,
163	pub quote_mint: &'a Address,
164	/// The `platform_config` of the curve or pool. Seeds `partner_config`.
165	pub platform_config: &'a Address,
166}
167
168pub(crate) struct DerivedTradeAccounts {
169	pub user_base_account: Address,
170	pub user_quote_account: Address,
171	pub partner_config: Address,
172}
173
174pub(crate) struct TradeUserContext<'a> {
175	pub user: &'a Address,
176	pub partner: &'a Address,
177	pub quote_token_program: &'a Address,
178}
179
180pub(crate) struct DeriveTradeArgs<'a> {
181	pub state: &'a TradeStateFields<'a>,
182	pub ctx: TradeUserContext<'a>,
183}
184
185pub(crate) fn derive_trade_accounts(
186	args: DeriveTradeArgs<'_>,
187) -> DerivedTradeAccounts {
188	let state = args.state;
189	let user = args.ctx.user;
190	let partner = args.ctx.partner;
191	let quote_token_program = args.ctx.quote_token_program;
192
193	let (user_base_account, _) = find_associated_token_pda(
194		user,
195		state.base_mint,
196		&TOKEN_2022_PROGRAM_ID,
197	);
198	let (user_quote_account, _) =
199		find_associated_token_pda(user, state.quote_mint, quote_token_program);
200	let (partner_config, _) = crate::nexus::pda::find_partner_config_pda(
201		state.platform_config,
202		partner,
203	);
204
205	DerivedTradeAccounts {
206		user_base_account,
207		user_quote_account,
208		partner_config,
209	}
210}
211
212/// The accounts of one trade that its legs read.
213#[derive(Clone, Copy, Debug)]
214pub(crate) struct TradeLegInput {
215	pub accounts: TradeAccounts,
216	pub direction: TradeDirection,
217}
218
219/// A buy moves quote from the user's account to the quote vault, signed by
220/// `user`, then base from the base vault to the user's account, signed by
221/// the market. A sell moves base in, then quote out. The handler checks that
222/// `user` owns both user accounts. `writes_market`: the program writes the
223/// market before its transfers.
224pub(crate) fn legs_of_trade(
225	input: &TradeLegInput,
226	writes_market: bool,
227) -> crate::transfer_hook::HookLegs {
228	use crate::transfer_hook::{HookLeg, HookLegs};
229
230	let TradeLegInput {
231		accounts: a,
232		direction,
233	} = *input;
234	let into_market = |source, mint, vault| HookLeg {
235		source,
236		mint,
237		destination: vault,
238		authority: a.user,
239		destination_owner: a.market,
240	};
241	let out_of_market = |vault, mint, destination| HookLeg {
242		source: vault,
243		mint,
244		destination,
245		authority: a.market,
246		destination_owner: a.user,
247	};
248	let legs = match direction {
249		TradeDirection::Buy => vec![
250			into_market(a.user_quote_account, a.quote_mint, a.quote_vault),
251			out_of_market(a.base_vault, a.base_mint, a.user_base_account),
252		],
253		TradeDirection::Sell => vec![
254			into_market(a.user_base_account, a.base_mint, a.base_vault),
255			out_of_market(a.quote_vault, a.quote_mint, a.user_quote_account),
256		],
257	};
258	HookLegs {
259		legs,
260		written: if writes_market {
261			vec![a.market]
262		} else {
263			Vec::new()
264		},
265	}
266}
267
268/// The error of a builder.
269#[derive(Clone, Debug, PartialEq, Eq)]
270#[non_exhaustive]
271pub enum BuildError {
272	Quote(QuoteError),
273	Hook(crate::transfer_hook::HookError),
274	/// The base mint is not a Token-2022 mint.
275	BaseNotToken2022(Address),
276	/// The mint has a hook program and no hook. Read it with
277	/// [`MintInfo::load`](super::MintInfo::load).
278	HookNotRead(Address),
279}
280
281impl From<QuoteError> for BuildError {
282	fn from(error: QuoteError) -> Self {
283		Self::Quote(error)
284	}
285}
286
287impl From<crate::math::amm::AmmError> for BuildError {
288	fn from(error: crate::math::amm::AmmError) -> Self {
289		Self::Quote(error.into())
290	}
291}
292
293impl From<crate::transfer_hook::HookError> for BuildError {
294	fn from(error: crate::transfer_hook::HookError) -> Self {
295		Self::Hook(error)
296	}
297}
298
299impl fmt::Display for BuildError {
300	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301		match self {
302			Self::Quote(error) => error.fmt(f),
303			Self::Hook(error) => error.fmt(f),
304			Self::BaseNotToken2022(mint) => {
305				write!(f, "the base mint {mint} is not a Token-2022 mint")
306			}
307			Self::HookNotRead(mint) => {
308				write!(f, "the mint {mint} has a hook program and no hook")
309			}
310		}
311	}
312}
313
314impl std::error::Error for BuildError {}
315
316/// A trade builder's hook inputs.
317pub(crate) struct TradeHookStep<'a> {
318	pub input: TradeLegInput,
319	pub writes_market: bool,
320	pub mints: super::TradeMints<'a>,
321}
322
323pub(crate) fn append_trade_hooks(
324	instruction: solana_instruction::Instruction,
325	step: &TradeHookStep<'_>,
326) -> Result<solana_instruction::Instruction, BuildError> {
327	super::append_hooks(
328		instruction,
329		step.mints,
330		&legs_of_trade(&step.input, step.writes_market),
331	)
332}