1use anchor_lang::prelude::{AnchorDeserialize, Pubkey, Space};
2
3use anyhow::{Context, Result, anyhow, bail};
4use jupiter_amm_interface::{
5 AccountMap, Amm, AmmContext, AmmProgramIdToLabel, KeyedAccount, Quote, Swap,
6 SwapAndAccountMetas, SwapMode, SwapParams,
7};
8
9pub mod futarchy_amm;
10
11pub use futarchy_amm::{FutarchyAmm, MAX_BPS, TAKER_FEE_BPS};
12use rust_decimal::Decimal;
13
14use crate::futarchy_amm::{FutarchyAmmSwap, SwapType};
15
16pub const FUTARCHY_PROGRAM_ID: Pubkey =
17 Pubkey::from_str_const("FUTARELBfJfQ8RDGhg1wdhddq1odMAJUePHFuBYfUxKq");
18pub const SPL_TOKEN_PROGRAM_ID: Pubkey =
19 Pubkey::from_str_const("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
20pub const FUTARCHY_EVENT_AUTHORITY_KEY: Pubkey =
21 Pubkey::from_str_const("DGEympSS4qLvdr9r3uGHTfACdN8snShk4iGdJtZPxuBC");
22
23impl AmmProgramIdToLabel for FutarchyAmmClient {
24 const PROGRAM_ID_TO_LABELS: &[(Pubkey, jupiter_amm_interface::AmmLabel)] =
25 &[(FUTARCHY_PROGRAM_ID, "MetaDAO AMM")];
26}
27
28#[derive(Debug)]
29pub enum FutarchyAmmError {
30 MathOverflow,
31 InvalidReserves,
32 AmmInvariantViolated,
33 InvalidQuoteParams,
34 ExactOutNotSupported,
35 InvalidAmmData,
36}
37
38impl std::fmt::Display for FutarchyAmmError {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 write!(f, "{:?}", self)
41 }
42}
43
44#[derive(Debug, Clone)]
45pub struct FutarchyAmmClient {
46 pub dao_address: Pubkey,
47 pub state: FutarchyAmm,
48}
49
50impl Amm for FutarchyAmmClient {
51 fn label(&self) -> String {
52 "MetaDAO AMM".to_string()
53 }
54
55 fn program_id(&self) -> Pubkey {
56 FUTARCHY_PROGRAM_ID
57 }
58
59 fn key(&self) -> Pubkey {
60 self.dao_address
61 }
62
63 fn get_reserve_mints(&self) -> Vec<Pubkey> {
64 vec![self.state.base_mint, self.state.quote_mint]
65 }
66
67 fn get_accounts_to_update(&self) -> Vec<Pubkey> {
68 vec![self.dao_address]
69 }
70
71 fn update(&mut self, account_map: &AccountMap) -> Result<()> {
72 let dao_account = account_map.get(&self.dao_address).with_context(|| {
73 format!(
74 "DAO account not found for dao address: {}",
75 self.dao_address
76 )
77 })?;
78
79 if dao_account.data.len() < 8 + FutarchyAmm::INIT_SPACE {
80 bail!(FutarchyAmmError::InvalidAmmData);
81 }
82
83 let amm_data =
85 FutarchyAmm::deserialize(&mut &dao_account.data[8..8 + FutarchyAmm::INIT_SPACE])?;
86
87 self.state = amm_data;
88
89 Ok(())
90 }
91
92 fn get_accounts_len(&self) -> usize {
93 9
94 }
95
96 fn from_keyed_account(keyed_account: &KeyedAccount, _amm_context: &AmmContext) -> Result<Self>
97 where
98 Self: Sized,
99 {
100 if keyed_account.account.data.len() < 8 + FutarchyAmm::INIT_SPACE {
101 bail!(FutarchyAmmError::InvalidAmmData);
102 }
103
104 let amm_data = FutarchyAmm::deserialize(
105 &mut &keyed_account.account.data[8..8 + FutarchyAmm::INIT_SPACE],
106 )?;
107
108 Ok(Self {
109 dao_address: keyed_account.key,
110 state: amm_data,
111 })
112 }
113
114 fn get_swap_and_account_metas(&self, swap_params: &SwapParams) -> Result<SwapAndAccountMetas> {
115 let SwapParams {
116 source_mint,
117 destination_token_account,
118 source_token_account,
119 token_transfer_authority,
120 ..
121 } = swap_params;
122
123 let (user_base_account, user_quote_account) = if *source_mint == self.state.base_mint {
124 (*source_token_account, *destination_token_account)
125 } else {
126 (*destination_token_account, *source_token_account)
127 };
128
129 Ok(SwapAndAccountMetas {
130 swap: Swap::TokenSwap,
131 account_metas: FutarchyAmmSwap {
132 dao: self.dao_address,
133 trader: *token_transfer_authority,
134 user_base_account,
135 user_quote_account,
136 amm_base_vault: self.state.amm_base_vault,
137 amm_quote_vault: self.state.amm_quote_vault,
138 token_program: SPL_TOKEN_PROGRAM_ID,
139 futarchy_program: FUTARCHY_PROGRAM_ID,
140 futarchy_event_authority: FUTARCHY_EVENT_AUTHORITY_KEY,
141 }
142 .into(),
143 })
144 }
145
146 fn clone_amm(&self) -> Box<dyn Amm + Send + Sync> {
147 Box::new(self.clone())
148 }
149
150 fn quote(
151 &self,
152 quote_params: &jupiter_amm_interface::QuoteParams,
153 ) -> Result<jupiter_amm_interface::Quote> {
154 let swap_type = if quote_params.input_mint == self.state.quote_mint
155 && quote_params.output_mint == self.state.base_mint
156 {
157 SwapType::Buy
158 } else if quote_params.input_mint == self.state.base_mint
159 && quote_params.output_mint == self.state.quote_mint
160 {
161 SwapType::Sell
162 } else {
163 bail!(FutarchyAmmError::InvalidQuoteParams);
164 };
165
166 if quote_params.swap_mode == SwapMode::ExactOut {
167 bail!(FutarchyAmmError::ExactOutNotSupported);
168 }
169
170 let out_amount = self
171 .state
172 .state
173 .clone()
174 .swap(quote_params.amount, swap_type)?;
175
176 let fee_pct = Decimal::new(TAKER_FEE_BPS as i64, 2);
177
178 let fee_amount = (quote_params.amount as u128)
180 .checked_mul(TAKER_FEE_BPS as u128)
181 .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?
182 .checked_div(MAX_BPS as u128)
183 .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?
184 as u64;
185
186 Ok(Quote {
187 in_amount: quote_params.amount,
188 out_amount,
189 fee_amount,
190 fee_mint: quote_params.input_mint,
191 fee_pct,
192 })
193 }
194}
195
196#[cfg(test)]
197mod tests {
198
199 use super::*;
200
201 use jupiter_amm_interface::{ClockRef, KeyedAccount, SwapMode};
202 use solana_client::rpc_client::RpcClient;
203 use solana_commitment_config::CommitmentConfig;
204 use solana_sdk::pubkey;
205
206 #[test]
207 fn test_futarchy_amm() {
208 use solana_sdk::account::Account;
209 use std::collections::HashMap;
210
211 let rpc_url = "https://api.devnet.solana.com".to_string();
212 let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());
213
214 let dao_pubkey = pubkey!("9o2vDc7mnqLVu3humkRY1p87q2pFtXhF7QfnTo5qgCXE");
215
216 let dao_account = client.get_account(&dao_pubkey).unwrap();
217
218 let keyed_dao_account = KeyedAccount {
219 key: dao_pubkey,
220 account: dao_account.clone(),
221 params: None,
222 };
223
224 let amm_context = AmmContext {
225 clock_ref: ClockRef::default(),
226 };
227
228 let mut futarchy_amm =
229 FutarchyAmmClient::from_keyed_account(&keyed_dao_account, &amm_context).unwrap();
230
231 let accounts_to_update = futarchy_amm.get_accounts_to_update();
232 let accounts_map: HashMap<Pubkey, Account, ahash::RandomState> = client
233 .get_multiple_accounts(&accounts_to_update)
234 .unwrap()
235 .into_iter()
236 .zip(accounts_to_update)
237 .filter_map(|(account, pubkey)| account.map(|a| (pubkey, a)))
238 .collect();
239 futarchy_amm.update(&accounts_map).unwrap();
240
241 let res = futarchy_amm
243 .quote(&jupiter_amm_interface::QuoteParams {
244 amount: 1e6 as u64,
245 input_mint: futarchy_amm.state.quote_mint,
246 output_mint: futarchy_amm.state.base_mint,
247 swap_mode: SwapMode::ExactIn,
248 })
249 .unwrap();
250
251 println!("res: {:?}", res);
252
253 let res = futarchy_amm
255 .quote(&jupiter_amm_interface::QuoteParams {
256 amount: 1e6 as u64,
257 input_mint: futarchy_amm.state.base_mint,
258 output_mint: futarchy_amm.state.quote_mint,
259 swap_mode: SwapMode::ExactIn,
260 })
261 .unwrap();
262
263 println!("res: {:?}", res);
264 }
265}