sendfun-sdk 4.0.0

Rust SDK for send.fun programs
Documentation
use solana_address::Address;
use solana_instruction::Instruction;
use spl_tlv_account_resolution::solana_program_error::ProgramError;

use super::BuildError;
use crate::constants::{TOKEN_2022_PROGRAM_ID, TOKEN_DECIMALS};
use crate::math::amm::MintFee;
use crate::transfer_fee::FeeSchedule;
use crate::transfer_hook::{
	AccountView, Fetched, FetchedAccount, HookError, HookLegs, HookState,
	MintState, Step, find_validation_address, resolve_market,
};

/// A mint as the builders and [`route::trade`] take it.
///
/// Read the mint and the `Clock` in one call, then call [`load`](Self::load).
/// Fetch what [`missing`](Self::missing) names, [`insert`](Self::insert) it,
/// and repeat until `missing` is empty. Refresh it on a timer, like any
/// account: load it and fill it again. Do not insert new data for the mint
/// itself, because `hook_program` keeps the hook of the first read.
///
/// A builder quotes each transfer fee at the epoch of the read. Read the mint
/// again when the epoch changes, or call [`load_at`](Self::load_at) with the
/// epoch the trade lands in.
///
/// [`route::trade`]: crate::transfer_hook::route::trade
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct MintInfo {
	pub address: Address,
	pub token_program: Address,
	pub decimals: u8,
	/// `None` when the mint has no transfer fee.
	pub transfer_fee: Option<FeeSchedule>,
	/// `None` when the mint has no transfer hook.
	pub hook_program: Option<Address>,
	/// `None` when the mint has no transfer hook.
	pub hook: Option<HookState>,
}

impl MintInfo {
	/// `clock` is the `Clock` sysvar, read with the mint.
	pub fn load(
		address: Address,
		view: AccountView<'_>,
		clock: AccountView<'_>,
	) -> Result<Self, ProgramError> {
		Self::load_at(address, view, clock_epoch(clock)?)
	}

	/// The mint read at `epoch`.
	///
	/// Fails with `ProgramError::IncorrectProgramId` if no token program owns
	/// the mint, and with another error if its data is not an initialized
	/// mint.
	pub fn load_at(
		address: Address,
		view: AccountView<'_>,
		epoch: u64,
	) -> Result<Self, ProgramError> {
		let state = MintState::load(view)?;
		let hook = state.transfer_hook.map(|_| {
			let mut hook = HookState::default();
			hook.insert(
				address,
				Some(FetchedAccount {
					owner: *view.owner,
					data: view.data.to_vec(),
				}),
			);
			hook
		});
		Ok(Self {
			address,
			token_program: state.token_program,
			decimals: state.decimals,
			transfer_fee: state
				.transfer_fee
				.map(|config| FeeSchedule::of(&config, epoch)),
			hook_program: state.transfer_hook,
			hook,
		})
	}

	/// A mint with no fee and no hook.
	#[must_use]
	pub const fn plain(
		address: Address,
		token_program: Address,
		decimals: u8,
	) -> Self {
		Self {
			address,
			token_program,
			decimals,
			transfer_fee: None,
			hook_program: None,
			hook: None,
		}
	}

	/// A mint that the launchpad or the DEX created: Token-2022,
	/// `TOKEN_DECIMALS`, no fee, no hook.
	#[must_use]
	pub const fn launch(address: Address) -> Self {
		Self::plain(address, TOKEN_2022_PROGRAM_ID, TOKEN_DECIMALS)
	}

	/// The hook's validation account, which holds its list. `None` when the
	/// mint has no transfer hook.
	#[must_use]
	pub fn validation_address(&self) -> Option<Address> {
		self.hook_program
			.map(|program| find_validation_address(&self.address, &program))
	}

	/// A hook program with no hook. Only a caller that clears `hook` makes
	/// one: [`load_at`](Self::load_at) always fills it.
	pub(crate) const fn hook_not_read(&self) -> bool {
		self.hook_program.is_some() && self.hook.is_none()
	}

	/// The accounts to fetch next. Empty when the info is complete.
	pub fn missing(&self) -> Result<Vec<Address>, HookError> {
		self.hook.as_ref().map_or_else(
			|| Ok(Vec::new()),
			|hook| hook.missing(&[self.address]),
		)
	}

	/// What the snapshot holds of `key`.
	pub(crate) fn fetched(&self, key: &Address) -> Fetched<'_> {
		self.hook
			.as_ref()
			.map_or(Fetched::NotFetched, |hook| hook.fetched(key))
	}

	/// Records a fetched account: `None` for no account.
	pub fn insert(
		&mut self,
		address: Address,
		account: Option<FetchedAccount>,
	) {
		if let Some(hook) = &mut self.hook {
			hook.insert(address, account);
		}
	}
}

const SYSVAR_PROGRAM_ID: Address =
	solana_address::address!("Sysvar1111111111111111111111111111111111111");

/// The `Clock` has five fields. Each field is 8 bytes.
const CLOCK_LEN: usize = 40;
/// The epoch is the third field of the `Clock`.
const CLOCK_EPOCH: core::ops::Range<usize> = 16..24;

// An `AccountView` has no address. The length tells the `Clock` apart from
// the other sysvars.
fn clock_epoch(clock: AccountView<'_>) -> Result<u64, ProgramError> {
	if *clock.owner != SYSVAR_PROGRAM_ID {
		return Err(ProgramError::IncorrectProgramId);
	}
	if clock.data.len() != CLOCK_LEN {
		return Err(ProgramError::InvalidAccountData);
	}
	clock
		.data
		.get(CLOCK_EPOCH)
		.and_then(|bytes| bytes.try_into().ok())
		.map(u64::from_le_bytes)
		.ok_or(ProgramError::InvalidAccountData)
}

#[derive(Clone, Copy)]
pub struct TradeMints<'a> {
	pub base: &'a MintInfo,
	pub quote: &'a MintInfo,
}

pub struct TradeFees {
	pub base: Option<MintFee>,
	pub quote: Option<MintFee>,
}

pub fn trade_fees(mints: TradeMints<'_>) -> Result<TradeFees, BuildError> {
	check_base_mint(mints.base)?;
	Ok(TradeFees {
		base: mints.base.transfer_fee.map(|fee| fee.fee()),
		quote: mints.quote.transfer_fee.map(|fee| fee.fee()),
	})
}

pub fn check_base_mint(base: &MintInfo) -> Result<(), BuildError> {
	if base.token_program != TOKEN_2022_PROGRAM_ID {
		return Err(BuildError::BaseNotToken2022(base.address));
	}
	Ok(())
}

/// Appends the hook accounts of `legs` after all accounts of `instruction`.
/// It reads only the two snapshots: [`route::trade`] with no cache.
///
/// [`route::trade`]: crate::transfer_hook::route::trade
pub fn append_hooks(
	instruction: Instruction,
	mints: TradeMints<'_>,
	legs: &HookLegs,
) -> Result<Instruction, BuildError> {
	if let Some(info) = [mints.base, mints.quote]
		.into_iter()
		.find(|info| info.hook_not_read())
	{
		return Err(BuildError::HookNotRead(info.address));
	}
	match resolve_market(mints, legs, |_| Fetched::NotFetched)? {
		Step::Done(resolved) => {
			let mut instruction = instruction;
			instruction.accounts.extend(resolved.accounts);
			Ok(instruction)
		}
		Step::Fetch(addresses) => Err(HookError::NotLoaded(addresses).into()),
	}
}

#[cfg(test)]
mod tests;