Skip to main content

fusionamm_sdk/
create_pool.rs

1//
2// Copyright (c) Cryptic Dot
3//
4// Modification based on Orca Whirlpools (https://github.com/orca-so/whirlpools),
5// originally licensed under the Apache License, Version 2.0, prior to February 26, 2025.
6//
7// Modifications licensed under FusionAMM SDK Source-Available License v1.0
8// See the LICENSE file in the project root for license information.
9//
10
11use std::error::Error;
12
13use fusionamm_client::FusionPool;
14use fusionamm_client::{get_fusion_pool_address, get_fusion_pools_config_address, get_token_badge_address};
15use fusionamm_client::{InitializePool, InitializePoolInstructionArgs};
16use fusionamm_core::price_to_sqrt_price;
17use solana_client::nonblocking::rpc_client::RpcClient;
18use solana_keypair::Keypair;
19use solana_program::rent::Rent;
20use solana_program::sysvar::SysvarId;
21use solana_program::{instruction::Instruction, pubkey::Pubkey};
22use solana_sdk_ids::system_program;
23use solana_signer::Signer;
24use spl_token_2022::extension::StateWithExtensions;
25use spl_token_2022::state::Mint;
26
27use crate::{get_account_data_size, get_rent, order_mints, FUNDER};
28
29/// Represents the instructions and metadata for creating a pool.
30pub struct CreatePoolInstructions {
31    /// The list of instructions needed to create the pool.
32    pub instructions: Vec<Instruction>,
33
34    /// The estimated rent exemption cost for initializing the pool, in lamports.
35    pub initialization_cost: u64,
36
37    /// The address of the newly created pool.
38    pub pool_address: Pubkey,
39
40    /// The list of signers for the instructions.
41    pub additional_signers: Vec<Keypair>,
42}
43
44/// Creates the necessary instructions to initialize a Concentrated Liquidity Pool (CLMM).
45///
46/// # Arguments
47///
48/// * `rpc` - A reference to a Solana RPC client for communicating with the blockchain.
49/// * `token_a` - The public key of the first token mint address to include in the pool.
50/// * `token_b` - The public key of the second token mint address to include in the pool.
51/// * `tick_spacing` - The spacing between price ticks for the pool.
52/// * `fee_rate` - Pool fee rate.
53/// * `initial_price` - An optional initial price of token A in terms of token B. Defaults to 1.0 if not provided.
54/// * `funder` - An optional public key of the account funding the initialization process. Defaults to the global funder if not provided.
55///
56/// # Returns
57///
58/// A `Result` containing `CreatePoolInstructions` on success:
59/// * `instructions` - A vector of Solana instructions needed to initialize the pool.
60/// * `initialization_cost` - The estimated rent exemption cost for initializing the pool, in lamports.
61/// * `pool_address` - The public key of the newly created pool.
62/// * `additional_signers` - A vector of `Keypair` objects representing additional signers required for the instructions.
63///
64/// # Errors
65///
66/// This function will return an error if:
67/// - The funder account is invalid.
68/// - Token mints are not found or have invalid data.
69/// - The token mint order does not match the canonical byte order.
70/// - Any RPC request to the blockchain fails.
71///
72/// # Example
73///
74/// ```
75/// use fusionamm_sdk::create_fusion_pool_instructions;
76/// use solana_client::nonblocking::rpc_client::RpcClient;
77/// use solana_keypair::Keypair;
78/// use solana_pubkey::pubkey;
79/// use solana_signer::Signer;
80///
81/// #[tokio::main]
82/// async fn main() {
83///     let rpc = RpcClient::new("https://api.devnet.solana.com".to_string());
84///     let token_a = pubkey!("So11111111111111111111111111111111111111112");
85///     let token_b = pubkey!("BRjpCHtyQLNCo8gqRUr8jtdAj5AjPYQaoqbvcZiHok1k"); // devUSDC
86///     let tick_spacing = 64;
87///     let fee_rate = 300;
88///     let initial_price = Some(0.01);
89///     let wallet = Keypair::new(); // CAUTION: This wallet is not persistent.
90///     let funder = Some(wallet.pubkey());
91///
92///     let create_pool_instructions = create_fusion_pool_instructions(
93///         &rpc,
94///         token_a,
95///         token_b,
96///         tick_spacing,
97///         fee_rate,
98///         initial_price,
99///         funder,
100///     )
101///     .await
102///     .unwrap();
103///
104///     println!("Pool Address: {:?}", create_pool_instructions.pool_address);
105///     println!(
106///         "Initialization Cost: {} lamports",
107///         create_pool_instructions.initialization_cost
108///     );
109/// }
110/// ```
111pub async fn create_fusion_pool_instructions(
112    rpc: &RpcClient,
113    token_a: Pubkey,
114    token_b: Pubkey,
115    tick_spacing: u16,
116    fee_rate: u16,
117    initial_price: Option<f64>,
118    funder: Option<Pubkey>,
119) -> Result<CreatePoolInstructions, Box<dyn Error>> {
120    let initial_price = initial_price.unwrap_or(1.0);
121    let funder = funder.unwrap_or(*FUNDER.try_lock()?);
122    if funder == Pubkey::default() {
123        return Err("Funder must be provided".into());
124    }
125    if order_mints(token_a, token_b)[0] != token_a {
126        return Err("Token order needs to be flipped to match the canonical ordering (i.e. sorted on the byte repr. of the mint pubkeys)".into());
127    }
128
129    let rent = get_rent(rpc).await?;
130
131    let account_infos = rpc.get_multiple_accounts(&[token_a, token_b]).await?;
132    let mint_a_info = account_infos[0].as_ref().ok_or(format!("Mint {} not found", token_a))?;
133    let mint_a = StateWithExtensions::<Mint>::unpack(&mint_a_info.data)?;
134    let decimals_a = mint_a.base.decimals;
135    let token_program_a = mint_a_info.owner;
136    let mint_b_info = account_infos[1].as_ref().ok_or(format!("Mint {} not found", token_b))?;
137    let mint_b = StateWithExtensions::<Mint>::unpack(&mint_b_info.data)?;
138    let decimals_b = mint_b.base.decimals;
139    let token_program_b = mint_b_info.owner;
140
141    let initial_sqrt_price: u128 = price_to_sqrt_price(initial_price, decimals_a, decimals_b);
142
143    let pool_address = get_fusion_pool_address(&token_a, &token_b, tick_spacing)?.0;
144    let token_badge_a = get_token_badge_address(&token_a)?.0;
145    let token_badge_b = get_token_badge_address(&token_b)?.0;
146
147    let token_vault_a = Keypair::new();
148    let token_vault_b = Keypair::new();
149
150    let mut instructions = vec![];
151    let mut initialization_cost: u64 = 0;
152
153    instructions.push(
154        InitializePool {
155            fusion_pools_config: get_fusion_pools_config_address()?.0,
156            token_mint_a: token_a,
157            token_mint_b: token_b,
158            token_badge_a,
159            token_badge_b,
160            funder,
161            fusion_pool: pool_address,
162            token_vault_a: token_vault_a.pubkey(),
163            token_vault_b: token_vault_b.pubkey(),
164            token_program_a,
165            token_program_b,
166            system_program: system_program::id(),
167            rent: Rent::id(),
168        }
169        .instruction(InitializePoolInstructionArgs {
170            tick_spacing,
171            fee_rate,
172            initial_sqrt_price,
173        }),
174    );
175
176    initialization_cost += rent.minimum_balance(FusionPool::LEN);
177    let token_a_space = get_account_data_size(token_program_a, mint_a_info)?;
178    initialization_cost += rent.minimum_balance(token_a_space);
179    let token_b_space = get_account_data_size(token_program_b, mint_b_info)?;
180    initialization_cost += rent.minimum_balance(token_b_space);
181    /*
182        let full_range = get_full_range_tick_indexes(tick_spacing);
183        let lower_tick_index = get_tick_array_start_tick_index(full_range.tick_lower_index, tick_spacing);
184        let upper_tick_index = get_tick_array_start_tick_index(full_range.tick_upper_index, tick_spacing);
185        let initial_tick_index = sqrt_price_to_tick_index(initial_sqrt_price);
186        let current_tick_index = get_tick_array_start_tick_index(initial_tick_index, tick_spacing);
187
188        let tick_array_indexes = HashSet::from([lower_tick_index, upper_tick_index, current_tick_index]);
189        for start_tick_index in tick_array_indexes {
190            let tick_array_address = get_tick_array_address(&pool_address, start_tick_index)?;
191            instructions.push(
192                InitializeTickArray {
193                    fusion_pool: pool_address,
194                    tick_array: tick_array_address.0,
195                    funder,
196                    system_program: system_program::id(),
197                }
198                .instruction(InitializeTickArrayInstructionArgs { start_tick_index }),
199            );
200            initialization_cost += rent.minimum_balance(TickArray::MIN_LEN);
201        }
202    */
203    Ok(CreatePoolInstructions {
204        instructions,
205        initialization_cost,
206        pool_address,
207        additional_signers: vec![token_vault_a, token_vault_b],
208    })
209}
210
211#[cfg(test)]
212mod tests {
213    use crate::tests::{setup_mint, setup_mint_te, setup_mint_te_fee, RpcContext};
214
215    use super::*;
216    use serial_test::serial;
217
218    async fn fetch_pool(rpc: &RpcClient, pool_address: Pubkey) -> Result<FusionPool, Box<dyn Error>> {
219        let account = rpc.get_account(&pool_address).await?;
220        FusionPool::from_bytes(&account.data).map_err(|e| e.into())
221    }
222
223    #[tokio::test]
224    #[serial]
225    async fn test_error_if_no_funder() {
226        let ctx = RpcContext::new().await;
227        let mint_a = setup_mint(&ctx).await.unwrap();
228        let mint_b = setup_mint(&ctx).await.unwrap();
229
230        let result = create_fusion_pool_instructions(&ctx.rpc, mint_a, mint_b, 64, 300, Some(1.0), None).await;
231
232        assert!(result.is_err());
233    }
234
235    #[tokio::test]
236    #[serial]
237    async fn test_error_if_tokens_not_ordered() {
238        let ctx = RpcContext::new().await;
239        let mint_a = setup_mint(&ctx).await.unwrap();
240        let mint_b = setup_mint(&ctx).await.unwrap();
241
242        let result = create_fusion_pool_instructions(&ctx.rpc, mint_b, mint_a, 64, 300, Some(1.0), Some(ctx.signer.pubkey())).await;
243
244        assert!(result.is_err());
245    }
246
247    #[tokio::test]
248    #[serial]
249    async fn test_create_concentrated_liquidity_pool() {
250        let ctx = RpcContext::new().await;
251        let mint_a = setup_mint(&ctx).await.unwrap();
252        let mint_b = setup_mint(&ctx).await.unwrap();
253        let price = 10.0;
254        let fee_rate = 300;
255        let sqrt_price = price_to_sqrt_price(price, 9, 9);
256
257        let result = create_fusion_pool_instructions(&ctx.rpc, mint_a, mint_b, 64, fee_rate, Some(price), Some(ctx.signer.pubkey()))
258            .await
259            .unwrap();
260
261        let balance_before = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
262        let pool_before = fetch_pool(&ctx.rpc, result.pool_address).await;
263        assert!(pool_before.is_err());
264
265        let instructions = result.instructions;
266        ctx.send_transaction_with_signers(instructions, result.additional_signers.iter().collect())
267            .await
268            .unwrap();
269
270        let pool_after = fetch_pool(&ctx.rpc, result.pool_address).await.unwrap();
271        let balance_after = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
272        let balance_change = balance_before - balance_after;
273        let tx_fee = 15000; // 3 signing accounts * 5000 lamports
274        let min_rent_exempt = balance_change - tx_fee;
275
276        assert_eq!(result.initialization_cost, min_rent_exempt);
277        assert_eq!(sqrt_price, pool_after.sqrt_price);
278        assert_eq!(mint_a, pool_after.token_mint_a);
279        assert_eq!(mint_b, pool_after.token_mint_b);
280        assert_eq!(64, pool_after.tick_spacing);
281        assert_eq!(300, pool_after.fee_rate);
282    }
283
284    #[tokio::test]
285    #[serial]
286    async fn test_create_concentrated_liquidity_pool_with_one_te_token() {
287        let ctx = RpcContext::new().await;
288        let mint = setup_mint(&ctx).await.unwrap();
289        let mint_te = setup_mint_te(&ctx, &[]).await.unwrap();
290        let price = 10.0;
291        let fee_rate = 300;
292        let sqrt_price = price_to_sqrt_price(price, 9, 6);
293
294        let result = create_fusion_pool_instructions(&ctx.rpc, mint, mint_te, 64, fee_rate, Some(price), Some(ctx.signer.pubkey()))
295            .await
296            .unwrap();
297
298        let balance_before = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
299        let pool_before = fetch_pool(&ctx.rpc, result.pool_address).await;
300        assert!(pool_before.is_err());
301
302        let instructions = result.instructions;
303        ctx.send_transaction_with_signers(instructions, result.additional_signers.iter().collect())
304            .await
305            .unwrap();
306
307        let pool_after = fetch_pool(&ctx.rpc, result.pool_address).await.unwrap();
308        let balance_after = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
309        let balance_change = balance_before - balance_after;
310        let tx_fee = 15000; // 3 signing accounts * 5000 lamports
311        let min_rent_exempt = balance_change - tx_fee;
312
313        assert_eq!(result.initialization_cost, min_rent_exempt);
314        assert_eq!(sqrt_price, pool_after.sqrt_price);
315        assert_eq!(mint, pool_after.token_mint_a);
316        assert_eq!(mint_te, pool_after.token_mint_b);
317        assert_eq!(64, pool_after.tick_spacing);
318        assert_eq!(300, pool_after.fee_rate);
319    }
320
321    #[tokio::test]
322    #[serial]
323    async fn test_create_concentrated_liquidity_pool_with_two_te_tokens() {
324        let ctx = RpcContext::new().await;
325        let mint_te_a = setup_mint_te(&ctx, &[]).await.unwrap();
326        let mint_te_b = setup_mint_te(&ctx, &[]).await.unwrap();
327        let price = 10.0;
328        let fee_rate = 300;
329        let sqrt_price = price_to_sqrt_price(price, 6, 6);
330
331        let result = create_fusion_pool_instructions(&ctx.rpc, mint_te_a, mint_te_b, 64, fee_rate, Some(price), Some(ctx.signer.pubkey()))
332            .await
333            .unwrap();
334
335        let balance_before = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
336        let pool_before = fetch_pool(&ctx.rpc, result.pool_address).await;
337        assert!(pool_before.is_err());
338
339        let instructions = result.instructions;
340        ctx.send_transaction_with_signers(instructions, result.additional_signers.iter().collect())
341            .await
342            .unwrap();
343
344        let pool_after = fetch_pool(&ctx.rpc, result.pool_address).await.unwrap();
345        let balance_after = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
346        let balance_change = balance_before - balance_after;
347        let tx_fee = 15000; // 3 signing accounts * 5000 lamports
348        let min_rent_exempt = balance_change - tx_fee;
349
350        assert_eq!(result.initialization_cost, min_rent_exempt);
351        assert_eq!(sqrt_price, pool_after.sqrt_price);
352        assert_eq!(mint_te_a, pool_after.token_mint_a);
353        assert_eq!(mint_te_b, pool_after.token_mint_b);
354        assert_eq!(64, pool_after.tick_spacing);
355        assert_eq!(300, pool_after.fee_rate);
356    }
357
358    #[tokio::test]
359    #[serial]
360    async fn test_create_concentrated_liquidity_pool_with_transfer_fee() {
361        let ctx = RpcContext::new().await;
362        let mint = setup_mint(&ctx).await.unwrap();
363        let mint_te_fee = setup_mint_te_fee(&ctx).await.unwrap();
364        let price = 10.0;
365        let fee_rate = 300;
366        let sqrt_price = price_to_sqrt_price(price, 9, 6);
367
368        let result = create_fusion_pool_instructions(&ctx.rpc, mint, mint_te_fee, 64, fee_rate, Some(price), Some(ctx.signer.pubkey()))
369            .await
370            .unwrap();
371
372        let balance_before = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
373        let pool_before = fetch_pool(&ctx.rpc, result.pool_address).await;
374        assert!(pool_before.is_err());
375
376        let instructions = result.instructions;
377        ctx.send_transaction_with_signers(instructions, result.additional_signers.iter().collect())
378            .await
379            .unwrap();
380
381        let pool_after = fetch_pool(&ctx.rpc, result.pool_address).await.unwrap();
382        let balance_after = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
383        let balance_change = balance_before - balance_after;
384        let tx_fee = 15000; // 3 signing accounts * 5000 lamports
385        let min_rent_exempt = balance_change - tx_fee;
386
387        assert_eq!(result.initialization_cost, min_rent_exempt);
388        assert_eq!(sqrt_price, pool_after.sqrt_price);
389        assert_eq!(mint, pool_after.token_mint_a);
390        assert_eq!(mint_te_fee, pool_after.token_mint_b);
391        assert_eq!(64, pool_after.tick_spacing);
392        assert_eq!(300, pool_after.fee_rate);
393    }
394}