1use 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
29pub struct CreatePoolInstructions {
31 pub instructions: Vec<Instruction>,
33
34 pub initialization_cost: u64,
36
37 pub pool_address: Pubkey,
39
40 pub additional_signers: Vec<Keypair>,
42}
43
44pub 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 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; 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; 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; 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; 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}