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 solana_address::Address;
8
9#[derive(Clone, Debug)]
10pub struct BuildCreateTokenParams<'a> {
11	pub user: &'a Address,
12	pub payer: Option<&'a Address>,
13	/// The wallet that claims the creator fees.
14	pub coin_creator: &'a Address,
15	pub base_mint: &'a Address,
16	pub quote_mint: &'a Address,
17	pub partner: &'a Address,
18	pub platform_config: &'a Address,
19	pub quote_token_program: &'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` must be the address of a new keypair. Sign the transaction with
30/// that keypair.
31#[must_use]
32pub fn build_create_token_instruction(
33	p: &BuildCreateTokenParams<'_>,
34) -> solana_instruction::Instruction {
35	let (bonding_curve, _) =
36		super::pda::find_bonding_curve_pda(p.base_mint, p.quote_mint);
37	let (base_vault, _) = crate::utils::find_associated_token_pda(
38		&bonding_curve,
39		p.base_mint,
40		&TOKEN_2022_PROGRAM_ID,
41	);
42	let (quote_vault, _) = crate::utils::find_associated_token_pda(
43		&bonding_curve,
44		p.quote_mint,
45		p.quote_token_program,
46	);
47	let (partner_config, _) = crate::nexus::pda::find_partner_config_pda(
48		p.platform_config,
49		p.partner,
50	);
51	let (staking_reward_state, _) = crate::nexus::pda::find_reward_state_pda(
52		&STAKING_CONFIG_ADDRESS,
53		p.quote_mint,
54	);
55	let (staking_vault, _) = crate::utils::find_associated_token_pda(
56		&STAKING_CONFIG_ADDRESS,
57		p.quote_mint,
58		p.quote_token_program,
59	);
60	let (reward_accrual, _) =
61		super::generated::pdas::find_reward_accrual_pda(p.quote_mint);
62	let (creator_fee_config, _) =
63		super::pda::find_creator_fee_config_pda(p.base_mint, p.quote_mint);
64
65	let create = super::instructions::CreateToken {
66		user: *p.user,
67		payer: *p.payer.unwrap_or(p.user),
68		coin_creator: *p.coin_creator,
69		global_config: super::pda::GLOBAL_CONFIG_ADDRESS,
70		base_mint: *p.base_mint,
71		quote_mint: *p.quote_mint,
72		bonding_curve,
73		base_vault,
74		quote_vault,
75		creator_fee_config,
76		nexus_program: NEXUS_PROGRAM_ID,
77		nexus_global_config: crate::nexus::pda::GLOBAL_CONFIG_ADDRESS,
78		partner: crate::utils::partner_account(p.partner),
79		partner_config,
80		nexus_staking_config: STAKING_CONFIG_ADDRESS,
81		staking_reward_state,
82		reward_accrual,
83		staking_vault,
84		staking_wsol_vault: STAKING_WSOL_VAULT_ADDRESS,
85		wsol_reward_accrual:
86			super::generated::pdas::WSOL_REWARD_ACCRUAL_ADDRESS,
87		pyth_price_feed: PYTH_SOL_USD_PRICE_ACCOUNT,
88		base_token_program: TOKEN_2022_PROGRAM_ID,
89		quote_token_program: *p.quote_token_program,
90		wsol_token_program: TOKEN_PROGRAM_ID,
91		associated_token_program: ATA_PROGRAM_ID,
92		system_program: SYSTEM_PROGRAM_ID,
93		event_authority: super::pda::EVENT_AUTHORITY_ADDRESS,
94		program: LAUNCHPAD_PROGRAM_ID,
95	};
96	create.instruction(super::instructions::CreateTokenInstructionArgs {
97		platform_config: *p.platform_config,
98		name: p.name.to_owned(),
99		symbol: p.symbol.to_owned(),
100		uri: p.uri.to_owned(),
101		creator_fee_mode: p.creator_fee_mode,
102		creator_fee_bps: p.creator_fee_bps,
103		dex_creator_fee_bps: p.dex_creator_fee_bps,
104	})
105}
106
107#[cfg(test)]
108mod tests {
109	use solana_address::Address;
110
111	use super::{BuildCreateTokenParams, build_create_token_instruction};
112	use crate::constants::{
113		DEFAULT_PARTNER, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, WSOL_MINT,
114	};
115
116	const COIN_CREATOR_INDEX: usize = 2;
117	const CREATOR_FEE_CONFIG_INDEX: usize = 9;
118	const PARTNER_INDEX: usize = 12;
119
120	/// Differs from `user`.
121	const COIN_CREATOR: Address = Address::new_from_array([5u8; 32]);
122
123	/// A Token-2022 quote mint that the creator does not hold.
124	const TSLAX_MINT: Address =
125		solana_address::address!("XsDoVfqeBukxuZHWhdvWHBhgEHjGNst4MLodqsJHzoB");
126
127	fn sample_instruction(
128		partner: &Address,
129		quote_mint: &Address,
130		quote_token_program: &Address,
131	) -> solana_instruction::Instruction {
132		let user = Address::new_from_array([1u8; 32]);
133		let base_mint = Address::new_from_array([2u8; 32]);
134		let platform_config = Address::new_from_array([4u8; 32]);
135		build_create_token_instruction(&BuildCreateTokenParams {
136			user: &user,
137			payer: None,
138			coin_creator: &COIN_CREATOR,
139			base_mint: &base_mint,
140			quote_mint,
141			partner,
142			platform_config: &platform_config,
143			quote_token_program,
144			name: "Sample Token",
145			symbol: "SMPL",
146			uri: "https://example.com/meta.json",
147			creator_fee_mode: 0,
148			creator_fee_bps: 0,
149			dex_creator_fee_bps: 0,
150		})
151	}
152
153	fn wsol_instruction(partner: &Address) -> solana_instruction::Instruction {
154		sample_instruction(partner, &WSOL_MINT, &TOKEN_PROGRAM_ID)
155	}
156
157	#[test]
158	fn non_default_partner_is_marked_as_signer() {
159		let partner = Address::new_from_array([3u8; 32]);
160		let ix = wsol_instruction(&partner);
161
162		assert_eq!(ix.accounts[PARTNER_INDEX].pubkey, partner);
163		assert!(ix.accounts[PARTNER_INDEX].is_signer);
164
165		let matches =
166			ix.accounts.iter().filter(|m| m.pubkey == partner).count();
167		assert_eq!(matches, 1);
168	}
169
170	#[test]
171	fn default_partner_is_not_marked_as_signer() {
172		let ix = wsol_instruction(&DEFAULT_PARTNER);
173
174		assert_eq!(ix.accounts[PARTNER_INDEX].pubkey, DEFAULT_PARTNER);
175		assert!(!ix.accounts[PARTNER_INDEX].is_signer);
176	}
177
178	/// The creation fee is in SOL. The creator needs no quote tokens.
179	#[test]
180	fn no_account_is_derived_from_the_creator_and_the_quote_mint() {
181		let user = Address::new_from_array([1u8; 32]);
182		let ix = sample_instruction(
183			&DEFAULT_PARTNER,
184			&TSLAX_MINT,
185			&TOKEN_2022_PROGRAM_ID,
186		);
187
188		let (user_quote_ata, _) = crate::utils::find_associated_token_pda(
189			&user,
190			&TSLAX_MINT,
191			&TOKEN_2022_PROGRAM_ID,
192		);
193		assert!(!ix.accounts.iter().any(|m| m.pubkey == user_quote_ata));
194
195		let (staking_wsol_vault, _) = crate::utils::find_associated_token_pda(
196			&crate::nexus::pda::STAKING_CONFIG_ADDRESS,
197			&WSOL_MINT,
198			&TOKEN_PROGRAM_ID,
199		);
200		assert!(ix.accounts.iter().any(|m| m.pubkey == staking_wsol_vault));
201	}
202
203	/// `user` pays the creation fee. `payer` pays the rent.
204	#[test]
205	fn the_fee_paying_user_slot_is_writable() {
206		let ix = wsol_instruction(&DEFAULT_PARTNER);
207
208		assert!(ix.accounts[0].is_writable);
209		assert!(ix.accounts[0].is_signer);
210	}
211
212	#[test]
213	fn coin_creator_is_the_named_wallet_and_does_not_sign() {
214		let ix = wsol_instruction(&DEFAULT_PARTNER);
215
216		assert_eq!(ix.accounts[COIN_CREATOR_INDEX].pubkey, COIN_CREATOR);
217		assert!(!ix.accounts[COIN_CREATOR_INDEX].is_signer);
218		assert!(!ix.accounts[COIN_CREATOR_INDEX].is_writable);
219	}
220
221	#[test]
222	fn the_creator_fee_config_slot_is_the_curves_pda() {
223		let ix = sample_instruction(
224			&DEFAULT_PARTNER,
225			&TSLAX_MINT,
226			&TOKEN_2022_PROGRAM_ID,
227		);
228		let base_mint = Address::new_from_array([2u8; 32]);
229		let (derived, _) = Address::find_program_address(
230			&[
231				b"creator_fee_config",
232				base_mint.as_ref(),
233				TSLAX_MINT.as_ref(),
234			],
235			&crate::constants::LAUNCHPAD_PROGRAM_ID,
236		);
237
238		let slot = &ix.accounts[CREATOR_FEE_CONFIG_INDEX];
239		assert_eq!(slot.pubkey, derived);
240		assert_eq!(
241			slot.pubkey,
242			solana_address::address!(
243				"7wnaFX6M9fKrZmkuQitz2eKYyYDAbfAEZWEc4VagHYz1"
244			),
245		);
246		assert!(!slot.is_signer);
247		assert!(slot.is_writable);
248	}
249
250	#[test]
251	fn name_follows_platform_config_at_offset_40() {
252		let ix = wsol_instruction(&DEFAULT_PARTNER);
253
254		assert_eq!(ix.data[8..40], [4u8; 32]);
255		assert_eq!(ix.data[40..44], 12u32.to_le_bytes());
256		assert_eq!(&ix.data[44..56], b"Sample Token");
257		assert_eq!(ix.data[56..60], 4u32.to_le_bytes());
258		assert_eq!(&ix.data[60..64], b"SMPL");
259		assert_eq!(ix.data[64..68], 29u32.to_le_bytes());
260		assert_eq!(&ix.data[68..97], b"https://example.com/meta.json");
261		// `creator_fee_mode`, `creator_fee_bps`, `dex_creator_fee_bps`.
262		assert_eq!(ix.data[97..102], [0u8; 5]);
263		assert_eq!(ix.data.len(), 102);
264	}
265}