futarchy_amm_jup_sdk/
lib.rs

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        // we don't do Dao deserialization in case it changes, just deserialize the amm
84        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        // this isn't exact because of compounding, but should be close enough
179        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        // buy 1 USDC worth
242        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        // sell 10 META worth
254        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}