sendfun_sdk/utils/
mint_info.rs1use 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#[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 pub transfer_fee: Option<FeeSchedule>,
35 pub hook_program: Option<Address>,
37 pub hook: Option<HookState>,
39}
40
41impl MintInfo {
42 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 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 #[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 #[must_use]
105 pub const fn launch(address: Address) -> Self {
106 Self::plain(address, TOKEN_2022_PROGRAM_ID, TOKEN_DECIMALS)
107 }
108
109 #[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 pub(crate) const fn hook_not_read(&self) -> bool {
120 self.hook_program.is_some() && self.hook.is_none()
121 }
122
123 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 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 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
153const CLOCK_LEN: usize = 40;
155const CLOCK_EPOCH: core::ops::Range<usize> = 16..24;
157
158fn 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
201pub 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;