1use crate::constants::{
2 ATA_PROGRAM_ID, LAUNCHPAD_PROGRAM_ID, NEXUS_PROGRAM_ID,
3 PYTH_SOL_USD_PRICE_ACCOUNT, STAKING_WSOL_VAULT_ADDRESS, SYSTEM_PROGRAM_ID,
4 TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID,
5};
6use crate::nexus::pda::STAKING_CONFIG_ADDRESS;
7use crate::utils::MintInfo;
8use solana_address::Address;
9
10#[derive(Clone, Debug)]
11pub struct BuildCreateTokenParams<'a> {
12 pub user: &'a Address,
13 pub payer: Option<&'a Address>,
14 pub coin_creator: &'a Address,
16 pub base_mint_info: &'a MintInfo,
17 pub quote_mint_info: &'a MintInfo,
18 pub partner: &'a Address,
19 pub platform_config: &'a Address,
20 pub name: &'a str,
21 pub symbol: &'a str,
22 pub uri: &'a str,
23 pub creator_fee_mode: u8,
24 pub creator_fee_bps: u16,
25 pub dex_creator_fee_bps: u16,
27}
28
29#[must_use]
32pub fn build_create_token_instruction(
33 p: &BuildCreateTokenParams<'_>,
34) -> solana_instruction::Instruction {
35 let base_mint = &p.base_mint_info.address;
36 let quote_mint = &p.quote_mint_info.address;
37 let quote_token_program = &p.quote_mint_info.token_program;
38 let (bonding_curve, _) =
39 super::pda::find_bonding_curve_pda(base_mint, quote_mint);
40 let (base_vault, _) = crate::utils::find_associated_token_pda(
41 &bonding_curve,
42 base_mint,
43 &TOKEN_2022_PROGRAM_ID,
44 );
45 let (quote_vault, _) = crate::utils::find_associated_token_pda(
46 &bonding_curve,
47 quote_mint,
48 quote_token_program,
49 );
50 let (partner_config, _) = crate::nexus::pda::find_partner_config_pda(
51 p.platform_config,
52 p.partner,
53 );
54 let (staking_reward_state, _) = crate::nexus::pda::find_reward_state_pda(
55 &STAKING_CONFIG_ADDRESS,
56 quote_mint,
57 );
58 let (staking_vault, _) = crate::utils::find_associated_token_pda(
59 &STAKING_CONFIG_ADDRESS,
60 quote_mint,
61 quote_token_program,
62 );
63 let (reward_accrual, _) =
64 super::generated::pdas::find_reward_accrual_pda(quote_mint);
65 let (creator_fee_config, _) =
66 super::pda::find_creator_fee_config_pda(base_mint, quote_mint);
67
68 let create = super::instructions::CreateToken {
69 user: *p.user,
70 payer: *p.payer.unwrap_or(p.user),
71 coin_creator: *p.coin_creator,
72 global_config: super::pda::GLOBAL_CONFIG_ADDRESS,
73 base_mint: *base_mint,
74 quote_mint: *quote_mint,
75 bonding_curve,
76 base_vault,
77 quote_vault,
78 creator_fee_config,
79 nexus_program: NEXUS_PROGRAM_ID,
80 nexus_global_config: crate::nexus::pda::GLOBAL_CONFIG_ADDRESS,
81 partner: crate::utils::partner_account(p.partner),
82 partner_config,
83 nexus_staking_config: STAKING_CONFIG_ADDRESS,
84 staking_reward_state,
85 reward_accrual,
86 staking_vault,
87 staking_wsol_vault: STAKING_WSOL_VAULT_ADDRESS,
88 wsol_reward_accrual:
89 super::generated::pdas::WSOL_REWARD_ACCRUAL_ADDRESS,
90 pyth_price_feed: PYTH_SOL_USD_PRICE_ACCOUNT,
91 base_token_program: TOKEN_2022_PROGRAM_ID,
92 quote_token_program: *quote_token_program,
93 wsol_token_program: TOKEN_PROGRAM_ID,
94 associated_token_program: ATA_PROGRAM_ID,
95 system_program: SYSTEM_PROGRAM_ID,
96 event_authority: super::pda::EVENT_AUTHORITY_ADDRESS,
97 program: LAUNCHPAD_PROGRAM_ID,
98 };
99 create.instruction(super::instructions::CreateTokenInstructionArgs {
100 platform_config: *p.platform_config,
101 name: p.name.to_owned(),
102 symbol: p.symbol.to_owned(),
103 uri: p.uri.to_owned(),
104 creator_fee_mode: p.creator_fee_mode,
105 creator_fee_bps: p.creator_fee_bps,
106 dex_creator_fee_bps: p.dex_creator_fee_bps,
107 })
108}
109
110#[cfg(test)]
111mod tests {
112 use solana_address::{Address, address};
113 use solana_instruction::AccountMeta;
114
115 use super::{BuildCreateTokenParams, build_create_token_instruction};
116 use crate::constants::{
117 DEFAULT_PARTNER, TOKEN_2022_PROGRAM_ID, TOKEN_DECIMALS,
118 TOKEN_PROGRAM_ID, WSOL_MINT,
119 };
120 use crate::utils::MintInfo;
121
122 const COIN_CREATOR_INDEX: usize = 2;
123 const CREATOR_FEE_CONFIG_INDEX: usize = 9;
124 const PARTNER_INDEX: usize = 12;
125
126 const COIN_CREATOR: Address = Address::new_from_array([5u8; 32]);
128
129 const TSLAX_MINT: Address =
131 solana_address::address!("XsDoVfqeBukxuZHWhdvWHBhgEHjGNst4MLodqsJHzoB");
132
133 fn sample_instruction(
134 partner: &Address,
135 quote_mint: &Address,
136 quote_token_program: &Address,
137 ) -> solana_instruction::Instruction {
138 let user = Address::new_from_array([1u8; 32]);
139 let base_mint = Address::new_from_array([2u8; 32]);
140 let platform_config = Address::new_from_array([4u8; 32]);
141 build_create_token_instruction(&BuildCreateTokenParams {
142 user: &user,
143 payer: None,
144 coin_creator: &COIN_CREATOR,
145 base_mint_info: &MintInfo::plain(
146 base_mint,
147 TOKEN_2022_PROGRAM_ID,
148 TOKEN_DECIMALS,
149 ),
150 quote_mint_info: &MintInfo::plain(
151 *quote_mint,
152 *quote_token_program,
153 6,
154 ),
155 partner,
156 platform_config: &platform_config,
157 name: "Sample Token",
158 symbol: "SMPL",
159 uri: "https://example.com/meta.json",
160 creator_fee_mode: 0,
161 creator_fee_bps: 0,
162 dex_creator_fee_bps: 0,
163 })
164 }
165
166 fn wsol_instruction(partner: &Address) -> solana_instruction::Instruction {
167 sample_instruction(partner, &WSOL_MINT, &TOKEN_PROGRAM_ID)
168 }
169
170 #[test]
171 fn non_default_partner_is_marked_as_signer() {
172 let partner = Address::new_from_array([3u8; 32]);
173 let ix = wsol_instruction(&partner);
174
175 assert_eq!(ix.accounts[PARTNER_INDEX].pubkey, partner);
176 assert!(ix.accounts[PARTNER_INDEX].is_signer);
177
178 let matches =
179 ix.accounts.iter().filter(|m| m.pubkey == partner).count();
180 assert_eq!(matches, 1);
181 }
182
183 #[test]
184 fn default_partner_is_not_marked_as_signer() {
185 let ix = wsol_instruction(&DEFAULT_PARTNER);
186
187 assert_eq!(ix.accounts[PARTNER_INDEX].pubkey, DEFAULT_PARTNER);
188 assert!(!ix.accounts[PARTNER_INDEX].is_signer);
189 }
190
191 #[test]
193 fn no_account_is_derived_from_the_creator_and_the_quote_mint() {
194 let user = Address::new_from_array([1u8; 32]);
195 let ix = sample_instruction(
196 &DEFAULT_PARTNER,
197 &TSLAX_MINT,
198 &TOKEN_2022_PROGRAM_ID,
199 );
200
201 let (user_quote_ata, _) = crate::utils::find_associated_token_pda(
202 &user,
203 &TSLAX_MINT,
204 &TOKEN_2022_PROGRAM_ID,
205 );
206 assert!(!ix.accounts.iter().any(|m| m.pubkey == user_quote_ata));
207
208 let (staking_wsol_vault, _) = crate::utils::find_associated_token_pda(
209 &crate::nexus::pda::STAKING_CONFIG_ADDRESS,
210 &WSOL_MINT,
211 &TOKEN_PROGRAM_ID,
212 );
213 assert!(ix.accounts.iter().any(|m| m.pubkey == staking_wsol_vault));
214 }
215
216 #[test]
218 fn the_fee_paying_user_slot_is_writable() {
219 let ix = wsol_instruction(&DEFAULT_PARTNER);
220
221 assert!(ix.accounts[0].is_writable);
222 assert!(ix.accounts[0].is_signer);
223 }
224
225 #[test]
226 fn coin_creator_is_the_named_wallet_and_does_not_sign() {
227 let ix = wsol_instruction(&DEFAULT_PARTNER);
228
229 assert_eq!(ix.accounts[COIN_CREATOR_INDEX].pubkey, COIN_CREATOR);
230 assert!(!ix.accounts[COIN_CREATOR_INDEX].is_signer);
231 assert!(!ix.accounts[COIN_CREATOR_INDEX].is_writable);
232 }
233
234 #[test]
235 fn the_creator_fee_config_slot_is_the_curves_pda() {
236 let ix = sample_instruction(
237 &DEFAULT_PARTNER,
238 &TSLAX_MINT,
239 &TOKEN_2022_PROGRAM_ID,
240 );
241 let base_mint = Address::new_from_array([2u8; 32]);
242 let (derived, _) = Address::find_program_address(
243 &[
244 b"creator_fee_config",
245 base_mint.as_ref(),
246 TSLAX_MINT.as_ref(),
247 ],
248 &crate::constants::LAUNCHPAD_PROGRAM_ID,
249 );
250
251 let slot = &ix.accounts[CREATOR_FEE_CONFIG_INDEX];
252 assert_eq!(slot.pubkey, derived);
253 assert_eq!(
254 slot.pubkey,
255 solana_address::address!(
256 "7wnaFX6M9fKrZmkuQitz2eKYyYDAbfAEZWEc4VagHYz1"
257 ),
258 );
259 assert!(!slot.is_signer);
260 assert!(slot.is_writable);
261 }
262
263 #[test]
264 fn name_follows_platform_config_at_offset_40() {
265 let ix = wsol_instruction(&DEFAULT_PARTNER);
266
267 assert_eq!(ix.data[8..40], [4u8; 32]);
268 assert_eq!(ix.data[40..44], 12u32.to_le_bytes());
269 assert_eq!(&ix.data[44..56], b"Sample Token");
270 assert_eq!(ix.data[56..60], 4u32.to_le_bytes());
271 assert_eq!(&ix.data[60..64], b"SMPL");
272 assert_eq!(ix.data[64..68], 29u32.to_le_bytes());
273 assert_eq!(&ix.data[68..97], b"https://example.com/meta.json");
274 assert_eq!(ix.data[97..102], [0u8; 5]);
276 assert_eq!(ix.data.len(), 102);
277 }
278
279 #[test]
281 fn builds_the_instruction_it_built_from_base_mint() {
282 let ix = wsol_instruction(&DEFAULT_PARTNER);
283
284 assert_eq!(ix.program_id, crate::constants::LAUNCHPAD_PROGRAM_ID);
285 assert_eq!(
286 ix.accounts,
287 [
288 AccountMeta::new(
289 address!("4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi"),
290 true
291 ),
292 AccountMeta::new(
293 address!("4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi"),
294 true
295 ),
296 AccountMeta::new_readonly(
297 address!("LbUiWL3xVV8hTFYBVdbTNrpDo41NKS6o3LHHuDzjfcY"),
298 false
299 ),
300 AccountMeta::new_readonly(
301 address!("H3csa1cgR7zPJdxCzNbS5chYymHMKS3XivDA5nRx4Bdq"),
302 false
303 ),
304 AccountMeta::new(
305 address!("8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR"),
306 true
307 ),
308 AccountMeta::new_readonly(
309 address!("So11111111111111111111111111111111111111112"),
310 false
311 ),
312 AccountMeta::new(
313 address!("ANXdciAmkaGrLUFTfVTiS2k1my7KvGBngDoiLU6yK2Dk"),
314 false
315 ),
316 AccountMeta::new(
317 address!("EyR21vk446MMHcLP6z7mQmNNq6FArNrmLvts24hF5DTs"),
318 false
319 ),
320 AccountMeta::new(
321 address!("AHsSu3fu8D21tHYxM6x6r8kK2YkrdoDCPkmrS1iMPUMK"),
322 false
323 ),
324 AccountMeta::new(
325 address!("BMrF9UWCQwk4bYq9nYsfLWFCVx6UhgLXrxJCABENvd19"),
326 false
327 ),
328 AccountMeta::new_readonly(
329 address!("7rrCurqdWFesbzwtYPo95fM8waTtVXgXwCom45x1sfNX"),
330 false
331 ),
332 AccountMeta::new_readonly(
333 address!("3bycfBeRULfXYqcg3DmiYFBjv6mMVidMuqKdFZJTGdNr"),
334 false
335 ),
336 AccountMeta::new_readonly(
337 address!("11111111111111111111111111111111"),
338 false
339 ),
340 AccountMeta::new_readonly(
341 address!("HsRhbMCEq9ZdswciDsdUEsoGyvKZ7HknXRFWaDLDLpuy"),
342 false
343 ),
344 AccountMeta::new_readonly(
345 address!("9WUowYRb6KAWjWVB5KMmTngBidZXEXgQcQRTXbhxeGbF"),
346 false
347 ),
348 AccountMeta::new(
349 address!("B9Z3t5c3AFtbuUfdeK41zGGL3dQ2AkgtGCjTcakZYwFr"),
350 false
351 ),
352 AccountMeta::new(
353 address!("91AVurtPdUC2yG3joLoujzC216KAfH5HEKnZEELFMmVH"),
354 false
355 ),
356 AccountMeta::new(
357 address!("AyisZJxXz9ywXhMwR3sb3oePxBB6UBvixXycAQCdSgrz"),
358 false
359 ),
360 AccountMeta::new(
361 address!("AyisZJxXz9ywXhMwR3sb3oePxBB6UBvixXycAQCdSgrz"),
362 false
363 ),
364 AccountMeta::new(
365 address!("91AVurtPdUC2yG3joLoujzC216KAfH5HEKnZEELFMmVH"),
366 false
367 ),
368 AccountMeta::new_readonly(
369 address!("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE"),
370 false
371 ),
372 AccountMeta::new_readonly(
373 address!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"),
374 false
375 ),
376 AccountMeta::new_readonly(
377 address!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"),
378 false
379 ),
380 AccountMeta::new_readonly(
381 address!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"),
382 false
383 ),
384 AccountMeta::new_readonly(
385 address!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"),
386 false
387 ),
388 AccountMeta::new_readonly(
389 address!("11111111111111111111111111111111"),
390 false
391 ),
392 AccountMeta::new_readonly(
393 address!("FJVebqCthxH6JM8vvkJb7yRYA7Emsib1C1vu4ZePSP3T"),
394 false
395 ),
396 AccountMeta::new_readonly(
397 address!("5R1uFyEE4oxqkm7hJDqHq6gXaLVF9dxeF3LF4yz1sfLP"),
398 false
399 ),
400 ]
401 );
402 let data = [
403 &[84, 52, 204, 228, 24, 140, 234, 75][..],
404 &[4; 32],
405 &12u32.to_le_bytes(),
406 b"Sample Token",
407 &4u32.to_le_bytes(),
408 b"SMPL",
409 &29u32.to_le_bytes(),
410 b"https://example.com/meta.json",
411 &[0; 5],
412 ]
413 .concat();
414 assert_eq!(ix.data, data);
415 }
416}