Skip to main content

sendfun_sdk/utils/
mint_info.rs

1use solana_address::Address;
2use solana_instruction::Instruction;
3use spl_tlv_account_resolution::solana_program_error::ProgramError;
4
5use super::BuildError;
6use crate::constants::{TOKEN_2022_PROGRAM_ID, TOKEN_DECIMALS};
7use crate::math::amm::MintFee;
8use crate::transfer_fee::FeeSchedule;
9use crate::transfer_hook::{
10	AccountView, Fetched, FetchedAccount, HookError, HookLegs, HookState,
11	MintState, Step, find_validation_address, resolve_market,
12};
13
14/// A mint as the builders and [`route::trade`] take it.
15///
16/// Read the mint and the `Clock` in one call, then call [`load`](Self::load).
17/// Fetch what [`missing`](Self::missing) names, [`insert`](Self::insert) it,
18/// and repeat until `missing` is empty. Refresh it on a timer, like any
19/// account: load it and fill it again. Do not insert new data for the mint
20/// itself, because `hook_program` keeps the hook of the first read.
21///
22/// A builder quotes each transfer fee at the epoch of the read. Read the mint
23/// again when the epoch changes, or call [`load_at`](Self::load_at) with the
24/// epoch the trade lands in.
25///
26/// [`route::trade`]: crate::transfer_hook::route::trade
27#[derive(Clone, Debug, PartialEq, Eq)]
28#[non_exhaustive]
29pub struct MintInfo {
30	pub address: Address,
31	pub token_program: Address,
32	pub decimals: u8,
33	/// `None` when the mint has no transfer fee.
34	pub transfer_fee: Option<FeeSchedule>,
35	/// `None` when the mint has no transfer hook.
36	pub hook_program: Option<Address>,
37	/// `None` when the mint has no transfer hook.
38	pub hook: Option<HookState>,
39}
40
41impl MintInfo {
42	/// `clock` is the `Clock` sysvar, read with the mint.
43	pub fn load(
44		address: Address,
45		view: AccountView<'_>,
46		clock: AccountView<'_>,
47	) -> Result<Self, ProgramError> {
48		Self::load_at(address, view, clock_epoch(clock)?)
49	}
50
51	/// The mint read at `epoch`.
52	///
53	/// Fails with `ProgramError::IncorrectProgramId` if no token program owns
54	/// the mint, and with another error if its data is not an initialized
55	/// mint.
56	pub fn load_at(
57		address: Address,
58		view: AccountView<'_>,
59		epoch: u64,
60	) -> Result<Self, ProgramError> {
61		let state = MintState::load(view)?;
62		let hook = state.transfer_hook.map(|_| {
63			let mut hook = HookState::default();
64			hook.insert(
65				address,
66				Some(FetchedAccount {
67					owner: *view.owner,
68					data: view.data.to_vec(),
69				}),
70			);
71			hook
72		});
73		Ok(Self {
74			address,
75			token_program: state.token_program,
76			decimals: state.decimals,
77			transfer_fee: state
78				.transfer_fee
79				.map(|config| FeeSchedule::of(&config, epoch)),
80			hook_program: state.transfer_hook,
81			hook,
82		})
83	}
84
85	/// A mint with no fee and no hook.
86	#[must_use]
87	pub const fn plain(
88		address: Address,
89		token_program: Address,
90		decimals: u8,
91	) -> Self {
92		Self {
93			address,
94			token_program,
95			decimals,
96			transfer_fee: None,
97			hook_program: None,
98			hook: None,
99		}
100	}
101
102	/// A mint that the launchpad or the DEX created: Token-2022,
103	/// `TOKEN_DECIMALS`, no fee, no hook.
104	#[must_use]
105	pub const fn launch(address: Address) -> Self {
106		Self::plain(address, TOKEN_2022_PROGRAM_ID, TOKEN_DECIMALS)
107	}
108
109	/// The hook's validation account, which holds its list. `None` when the
110	/// mint has no transfer hook.
111	#[must_use]
112	pub fn validation_address(&self) -> Option<Address> {
113		self.hook_program
114			.map(|program| find_validation_address(&self.address, &program))
115	}
116
117	/// A hook program with no hook. Only a caller that clears `hook` makes
118	/// one: [`load_at`](Self::load_at) always fills it.
119	pub(crate) const fn hook_not_read(&self) -> bool {
120		self.hook_program.is_some() && self.hook.is_none()
121	}
122
123	/// The accounts to fetch next. Empty when the info is complete.
124	pub fn missing(&self) -> Result<Vec<Address>, HookError> {
125		self.hook.as_ref().map_or_else(
126			|| Ok(Vec::new()),
127			|hook| hook.missing(&[self.address]),
128		)
129	}
130
131	/// What the snapshot holds of `key`.
132	pub(crate) fn fetched(&self, key: &Address) -> Fetched<'_> {
133		self.hook
134			.as_ref()
135			.map_or(Fetched::NotFetched, |hook| hook.fetched(key))
136	}
137
138	/// Records a fetched account: `None` for no account.
139	pub fn insert(
140		&mut self,
141		address: Address,
142		account: Option<FetchedAccount>,
143	) {
144		if let Some(hook) = &mut self.hook {
145			hook.insert(address, account);
146		}
147	}
148}
149
150const SYSVAR_PROGRAM_ID: Address =
151	solana_address::address!("Sysvar1111111111111111111111111111111111111");
152
153/// The `Clock` has five fields. Each field is 8 bytes.
154const CLOCK_LEN: usize = 40;
155/// The epoch is the third field of the `Clock`.
156const CLOCK_EPOCH: core::ops::Range<usize> = 16..24;
157
158// An `AccountView` has no address. The length tells the `Clock` apart from
159// the other sysvars.
160fn clock_epoch(clock: AccountView<'_>) -> Result<u64, ProgramError> {
161	if *clock.owner != SYSVAR_PROGRAM_ID {
162		return Err(ProgramError::IncorrectProgramId);
163	}
164	if clock.data.len() != CLOCK_LEN {
165		return Err(ProgramError::InvalidAccountData);
166	}
167	clock
168		.data
169		.get(CLOCK_EPOCH)
170		.and_then(|bytes| bytes.try_into().ok())
171		.map(u64::from_le_bytes)
172		.ok_or(ProgramError::InvalidAccountData)
173}
174
175#[derive(Clone, Copy)]
176pub struct TradeMints<'a> {
177	pub base: &'a MintInfo,
178	pub quote: &'a MintInfo,
179}
180
181pub struct TradeFees {
182	pub base: Option<MintFee>,
183	pub quote: Option<MintFee>,
184}
185
186pub fn trade_fees(mints: TradeMints<'_>) -> Result<TradeFees, BuildError> {
187	check_base_mint(mints.base)?;
188	Ok(TradeFees {
189		base: mints.base.transfer_fee.map(|fee| fee.fee()),
190		quote: mints.quote.transfer_fee.map(|fee| fee.fee()),
191	})
192}
193
194pub fn check_base_mint(base: &MintInfo) -> Result<(), BuildError> {
195	if base.token_program != TOKEN_2022_PROGRAM_ID {
196		return Err(BuildError::BaseNotToken2022(base.address));
197	}
198	Ok(())
199}
200
201/// Appends the hook accounts of `legs` after all accounts of `instruction`.
202/// It reads only the two snapshots: [`route::trade`] with no cache.
203///
204/// [`route::trade`]: crate::transfer_hook::route::trade
205pub fn append_hooks(
206	instruction: Instruction,
207	mints: TradeMints<'_>,
208	legs: &HookLegs,
209) -> Result<Instruction, BuildError> {
210	if let Some(info) = [mints.base, mints.quote]
211		.into_iter()
212		.find(|info| info.hook_not_read())
213	{
214		return Err(BuildError::HookNotRead(info.address));
215	}
216	match resolve_market(mints, legs, |_| Fetched::NotFetched)? {
217		Step::Done(resolved) => {
218			let mut instruction = instruction;
219			instruction.accounts.extend(resolved.accounts);
220			Ok(instruction)
221		}
222		Step::Fetch(addresses) => Err(HookError::NotLoaded(addresses).into()),
223	}
224}
225
226#[cfg(test)]
227mod tests;