Skip to main content

sendfun_sdk/launchpad/
create.rs

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	/// The wallet that claims the creator fees.
15	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	/// The creator fee of the pool after migration, in bps.
26	pub dex_creator_fee_bps: u16,
27}
28
29/// `base_mint_info` is [`MintInfo::launch`] of a new keypair. Sign the
30/// transaction with that keypair.
31#[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	/// Differs from `user`.
127	const COIN_CREATOR: Address = Address::new_from_array([5u8; 32]);
128
129	/// A Token-2022 quote mint that the creator does not hold.
130	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	/// The creation fee is in SOL. The creator needs no quote tokens.
192	#[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	/// `user` pays the creation fee. `payer` pays the rent.
217	#[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		// `creator_fee_mode`, `creator_fee_bps`, `dex_creator_fee_bps`.
275		assert_eq!(ix.data[97..102], [0u8; 5]);
276		assert_eq!(ix.data.len(), 102);
277	}
278
279	/// The instruction that the builder built when it took `base_mint`.
280	#[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}