1#![doc = include_str!("../README.md")]
2pub mod dex;
75pub mod launchpad;
76pub mod nexus;
77
78pub mod constants;
79pub mod math;
80pub mod transfer_fee;
81pub mod transfer_hook;
82
83pub mod utils;
84
85pub use spl_token_2022_interface;
88
89#[cfg(test)]
90mod tests {
91
92 mod pda_tests {
93 use solana_address::address;
94
95 #[test]
96 fn launchpad_bonding_curve_pda() {
97 let base = address!("So11111111111111111111111111111111111111112");
98 let quote = address!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
99 let (addr1, _) =
100 crate::launchpad::pda::find_bonding_curve_pda(&base, "e);
101 let (addr2, _) =
102 crate::launchpad::pda::find_bonding_curve_pda(&base, "e);
103 assert_eq!(addr1, addr2);
104 let (addr3, _) =
105 crate::launchpad::pda::find_bonding_curve_pda("e, &base);
106 assert_ne!(addr1, addr3);
107 }
108
109 #[test]
110 fn dex_pool_and_lp_mint_pdas_differ() {
111 let base = address!("So11111111111111111111111111111111111111112");
112 let quote = address!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
113 let (pool, _) = crate::dex::pda::find_pool_pda(&base, "e);
114 let (lp, _) = crate::dex::pda::find_lp_mint_pda(&base, "e);
115 assert_ne!(pool, lp);
116 }
117
118 #[test]
119 fn nexus_staking_pdas() {
120 let user = address!("11111111111111111111111111111112");
121 let staking_mint =
122 address!("So11111111111111111111111111111111111111112");
123 let reward_mint =
124 address!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
125
126 let (pos, _) = crate::nexus::pda::find_user_stake_position_pda(
127 &user,
128 &staking_mint,
129 );
130 let (debt, _) = crate::nexus::pda::find_user_reward_debt_pda(
131 &user,
132 &staking_mint,
133 &reward_mint,
134 );
135 assert_ne!(pos, debt);
136 }
137
138 #[test]
139 fn event_authority_pdas_differ_per_program() {
140 let (lp, _) = crate::launchpad::pda::find_event_authority_pda();
141 let (dex, _) = crate::dex::pda::find_event_authority_pda();
142 let (nexus, _) = crate::nexus::pda::find_event_authority_pda();
143 assert_ne!(lp, dex);
144 assert_ne!(dex, nexus);
145 assert_ne!(lp, nexus);
146 }
147 }
148
149 mod generated_constants {
150 use solana_address::Address;
151
152 struct PdaCheck<'a> {
153 label: &'a str,
154 address: Address,
155 bump: u8,
156 seeds: &'a [&'a [u8]],
157 program_id: Address,
158 }
159
160 #[track_caller]
161 fn assert_pda(check: PdaCheck<'_>) {
162 let (derived, derived_bump) =
163 Address::find_program_address(check.seeds, &check.program_id);
164 assert_eq!(check.address, derived, "{}: address", check.label);
165 assert_eq!(check.bump, derived_bump, "{}: bump", check.label);
166 }
167
168 #[test]
169 fn launchpad_pda_constants_match_derivation() {
170 use crate::launchpad::generated::SEND_LAUNCHPAD_ID as ID;
171 use crate::launchpad::pda::{
172 EVENT_AUTHORITY_ADDRESS, EVENT_AUTHORITY_BUMP,
173 EVENT_AUTHORITY_SEED, GLOBAL_CONFIG_ADDRESS,
174 GLOBAL_CONFIG_BUMP, GLOBAL_CONFIG_SEED,
175 MIGRATION_AUTHORITY_ADDRESS, MIGRATION_AUTHORITY_BUMP,
176 MIGRATION_AUTHORITY_SEED,
177 };
178
179 assert_pda(PdaCheck {
180 label: "launchpad global_config",
181 address: GLOBAL_CONFIG_ADDRESS,
182 bump: GLOBAL_CONFIG_BUMP,
183 seeds: &[GLOBAL_CONFIG_SEED],
184 program_id: ID,
185 });
186 assert_pda(PdaCheck {
187 label: "launchpad event_authority",
188 address: EVENT_AUTHORITY_ADDRESS,
189 bump: EVENT_AUTHORITY_BUMP,
190 seeds: &[EVENT_AUTHORITY_SEED],
191 program_id: ID,
192 });
193 assert_pda(PdaCheck {
194 label: "launchpad migration_authority",
195 address: MIGRATION_AUTHORITY_ADDRESS,
196 bump: MIGRATION_AUTHORITY_BUMP,
197 seeds: &[MIGRATION_AUTHORITY_SEED],
198 program_id: ID,
199 });
200 }
201
202 #[test]
203 fn dex_pda_constants_match_derivation() {
204 use crate::dex::generated::SEND_DEX_ID as ID;
205 use crate::dex::pda::{
206 EVENT_AUTHORITY_ADDRESS, EVENT_AUTHORITY_BUMP,
207 EVENT_AUTHORITY_SEED, GLOBAL_CONFIG_ADDRESS,
208 GLOBAL_CONFIG_BUMP, GLOBAL_CONFIG_SEED,
209 };
210
211 assert_pda(PdaCheck {
212 label: "dex global_config",
213 address: GLOBAL_CONFIG_ADDRESS,
214 bump: GLOBAL_CONFIG_BUMP,
215 seeds: &[GLOBAL_CONFIG_SEED],
216 program_id: ID,
217 });
218 assert_pda(PdaCheck {
219 label: "dex event_authority",
220 address: EVENT_AUTHORITY_ADDRESS,
221 bump: EVENT_AUTHORITY_BUMP,
222 seeds: &[EVENT_AUTHORITY_SEED],
223 program_id: ID,
224 });
225 }
226
227 #[test]
228 fn nexus_pda_constants_match_derivation() {
229 use crate::nexus::generated::SEND_NEXUS_ID as ID;
230 use crate::nexus::pda::{
231 ALT_REGISTRY_ADDRESS, ALT_REGISTRY_BUMP, ALT_REGISTRY_SEED,
232 EVENT_AUTHORITY_ADDRESS, EVENT_AUTHORITY_BUMP,
233 EVENT_AUTHORITY_SEED, GLOBAL_CONFIG_ADDRESS,
234 GLOBAL_CONFIG_BUMP, GLOBAL_CONFIG_SEED, STAKING_CONFIG_ADDRESS,
235 STAKING_CONFIG_BUMP, STAKING_CONFIG_SEED,
236 };
237
238 assert_pda(PdaCheck {
239 label: "nexus global_config",
240 address: GLOBAL_CONFIG_ADDRESS,
241 bump: GLOBAL_CONFIG_BUMP,
242 seeds: &[GLOBAL_CONFIG_SEED],
243 program_id: ID,
244 });
245 assert_pda(PdaCheck {
246 label: "nexus staking_config",
247 address: STAKING_CONFIG_ADDRESS,
248 bump: STAKING_CONFIG_BUMP,
249 seeds: &[STAKING_CONFIG_SEED],
250 program_id: ID,
251 });
252 assert_pda(PdaCheck {
253 label: "nexus event_authority",
254 address: EVENT_AUTHORITY_ADDRESS,
255 bump: EVENT_AUTHORITY_BUMP,
256 seeds: &[EVENT_AUTHORITY_SEED],
257 program_id: ID,
258 });
259 assert_pda(PdaCheck {
260 label: "nexus alt_registry",
261 address: ALT_REGISTRY_ADDRESS,
262 bump: ALT_REGISTRY_BUMP,
263 seeds: &[ALT_REGISTRY_SEED],
264 program_id: ID,
265 });
266 let platform =
268 crate::constants::find_platform_address(b"sendfun").0;
269 assert_eq!(
270 crate::nexus::pda::find_default_partner_pda(&platform),
271 crate::nexus::pda::find_partner_config_pda(
272 &platform,
273 &crate::constants::DEFAULT_PARTNER,
274 ),
275 );
276 assert_ne!(
277 crate::nexus::pda::find_default_partner_pda(&platform).0,
278 crate::nexus::pda::find_default_partner_pda(
279 &crate::constants::find_platform_address(b"acme").0
280 )
281 .0,
282 );
283 assert_eq!(
284 crate::nexus::pda::find_default_fee_preset_pda(&platform),
285 crate::nexus::pda::find_fee_preset_pda(&platform, 0),
286 );
287 assert_ne!(
289 crate::nexus::pda::find_default_fee_preset_pda(&platform).0,
290 crate::nexus::pda::find_default_fee_preset_pda(
291 &crate::constants::find_platform_address(b"acme").0
292 )
293 .0,
294 );
295 assert_eq!(
296 crate::nexus::pda::find_default_partner_metadata_pda(&platform),
297 crate::nexus::pda::find_partner_metadata_pda(
298 &platform,
299 &crate::constants::DEFAULT_PARTNER,
300 ),
301 );
302 assert_ne!(
304 crate::nexus::pda::find_default_partner_metadata_pda(&platform)
305 .0,
306 crate::nexus::pda::find_default_partner_metadata_pda(
307 &crate::constants::find_platform_address(b"acme").0
308 )
309 .0,
310 );
311 }
312
313 #[test]
316 fn sendfun_default_partner_addresses_are_pinned() {
317 use crate::constants::{DEFAULT_PARTNER, find_platform_address};
318 use solana_address::address;
319
320 let platform = find_platform_address(b"sendfun").0;
321 assert_eq!(
322 platform,
323 address!("2PJedAsa7pCnScks2XM3U4o2nPaTvvBmi2553VcCnGWB"),
324 "sendfun platform"
325 );
326
327 assert_eq!(
328 crate::nexus::pda::find_partner_config_pda(
329 &platform,
330 &DEFAULT_PARTNER
331 )
332 .0,
333 address!("B77P3Hvj9xLi4tcFtyjNcf6NjhkQrfJvLkz3nVZnBzec"),
334 "default partner config"
335 );
336 assert_eq!(
337 crate::nexus::pda::find_fee_preset_pda(&platform, 0).0,
338 address!("DTWXzPHfg7vjnhwhE7rxhdPZPGGFjuqD1excog3xmKTx"),
339 "default fee preset 0"
340 );
341 assert_eq!(
342 crate::nexus::pda::find_partner_metadata_pda(
343 &platform,
344 &DEFAULT_PARTNER
345 )
346 .0,
347 address!("8WqZvqH4B1LtcfxZHeAdYWCYvwfMaUkQvJqZsw4fmii5"),
348 "default partner metadata"
349 );
350 }
351 }
352
353 #[test]
354 fn bonding_curve_status_values() {
355 assert_eq!(
356 crate::launchpad::types::BondingCurveStatus::Funding as u8,
357 0
358 );
359 assert_eq!(
360 crate::launchpad::types::BondingCurveStatus::Completed as u8,
361 1
362 );
363 assert_eq!(
364 crate::launchpad::types::BondingCurveStatus::Migrated as u8,
365 2
366 );
367 assert_eq!(
368 crate::launchpad::types::BondingCurveStatus::Paused as u8,
369 3
370 );
371 }
372
373 #[test]
374 fn pool_status_values() {
375 assert_eq!(crate::dex::types::PoolStatus::Active as u8, 0);
376 assert_eq!(crate::dex::types::PoolStatus::Paused as u8, 1);
377 }
378
379 #[test]
380 fn launchpad_fees_total() {
381 let fees = crate::nexus::types::LaunchpadFees {
382 creation_fee_cents: 0,
383 protocol_fee_bps: 100,
384 max_creator_fee_bps: 50,
385 fee_decay_seconds: 0,
386 fee_decay_start_bps: 0,
387 };
388 assert_eq!(fees.total_fee_bps(30), Some(130));
389 }
390
391 #[test]
392 fn dex_fees_total() {
393 let fees = crate::nexus::types::DexFees {
394 creation_fee_cents: 0,
395 protocol_fee_bps: 80,
396 lp_fee_bps: 30,
397 max_creator_fee_bps: 40,
398 fee_decay_seconds: 0,
399 fee_decay_start_bps: 0,
400 };
401 assert_eq!(fees.total_fee_bps(25), Some(135));
402 }
403
404 mod from_account {
405 use borsh::BorshSerialize;
406 use solana_address::Address;
407
408 use crate::constants::{
409 DEX_PROGRAM_ID, LAUNCHPAD_PROGRAM_ID, NEXUS_PROGRAM_ID,
410 SYSTEM_PROGRAM_ID,
411 };
412 use crate::launchpad::accounts::{
413 BONDING_CURVE_DISCRIMINATOR, BondingCurve,
414 GLOBAL_CONFIG_DISCRIMINATOR, GlobalConfig,
415 };
416 use crate::launchpad::types::BondingCurveStatus;
417 use crate::nexus::accounts::StakingConfig;
418
419 const STRANGER: Address = Address::new_from_array([99; 32]);
420
421 fn curve() -> BondingCurve {
422 BondingCurve {
423 discriminator: BONDING_CURVE_DISCRIMINATOR,
424 version: 1,
425 bump: 254,
426 status: BondingCurveStatus::Funding,
427 base_mint: Address::new_from_array([1; 32]),
428 base_decimals: 6,
429 base_supply: 1_000_000_000_000_000,
430 initial_virtual_base: 1_073_000_000_000_000,
431 initial_virtual_quote: 30_000_000_000,
432 initial_real_base: 793_100_000_000_000,
433 quote_mint: Address::new_from_array([2; 32]),
434 quote_decimals: 9,
435 coin_creator: Address::new_from_array([3; 32]),
436 padding0: [0x0404_0404_0404_0404; 4],
437 partner: Address::new_from_array([5; 32]),
438 base_vault: Address::new_from_array([6; 32]),
439 quote_vault: Address::new_from_array([7; 32]),
440 virtual_base_reserves: 1_073_000_000_000_000,
441 virtual_quote_reserves: 30_000_000_000,
442 real_base_reserves: 793_100_000_000_000,
443 real_quote_reserves: 1,
444 created_at: 1_700_000_000,
445 protocol_owed: 0,
446 creator_owed: 0,
447 platform_config: Address::new_from_array([8; 32]),
448 creator_fee_mode: 0,
449 creator_fee_bps: 0,
450 dex_creator_fee_bps: 0,
451 reserved: [0; 256],
452 }
453 }
454
455 fn curve_data() -> Vec<u8> {
456 let mut data = Vec::new();
457 curve().serialize(&mut data).unwrap();
458 data
459 }
460
461 #[test]
462 fn decodes_an_account_its_program_owns() {
463 assert_eq!(
464 BondingCurve::from_account(
465 &LAUNCHPAD_PROGRAM_ID,
466 &curve_data()
467 )
468 .unwrap(),
469 curve()
470 );
471 }
472
473 #[test]
474 fn rejects_another_owner() {
475 let error = BondingCurve::from_account(&STRANGER, &curve_data())
476 .unwrap_err();
477 assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
478 assert_eq!(
479 error.to_string(),
480 format!(
481 "invalid account owner: {STRANGER}, expected {LAUNCHPAD_PROGRAM_ID}"
482 )
483 );
484 }
485
486 #[test]
487 fn rejects_another_accounts_discriminator() {
488 let error =
489 StakingConfig::from_account(&NEXUS_PROGRAM_ID, &curve_data())
490 .unwrap_err();
491 assert_eq!(error.to_string(), "invalid account discriminator");
492 }
493
494 #[test]
497 fn rejects_a_same_named_account_of_another_program() {
498 let config = GlobalConfig {
499 discriminator: GLOBAL_CONFIG_DISCRIMINATOR,
500 version: 1,
501 bump: 255,
502 authority: Address::new_from_array([1; 32]),
503 operator: Address::new_from_array([2; 32]),
504 create_token_disabled: false,
505 normalized_sol_price_cents: 15_000,
506 initial_base_supply: 1_000_000_000_000_000,
507 initial_virtual_base_reserves: 1_073_000_000_000_000,
508 initial_real_base_reserves: 793_100_000_000_000,
509 initial_virtual_quote_reserves: 30_000_000_000,
510 reserved: [0; 64],
511 };
512 let mut data = Vec::new();
513 config.serialize(&mut data).unwrap();
514
515 assert_eq!(
516 crate::dex::accounts::GLOBAL_CONFIG_DISCRIMINATOR,
517 GLOBAL_CONFIG_DISCRIMINATOR
518 );
519 assert!(
520 crate::dex::accounts::GlobalConfig::from_bytes(&data).is_ok()
521 );
522 let error = crate::dex::accounts::GlobalConfig::from_account(
523 &LAUNCHPAD_PROGRAM_ID,
524 &data,
525 )
526 .unwrap_err();
527 assert_eq!(
528 error.to_string(),
529 format!(
530 "invalid account owner: {LAUNCHPAD_PROGRAM_ID}, expected {DEX_PROGRAM_ID}"
531 )
532 );
533 }
534
535 #[test]
536 fn maybe_is_none_for_a_lamports_only_address() {
537 assert_eq!(
538 BondingCurve::from_account_maybe(&SYSTEM_PROGRAM_ID, &[])
539 .unwrap(),
540 None
541 );
542 }
543
544 #[test]
545 fn maybe_rejects_system_owned_data() {
546 let error = BondingCurve::from_account_maybe(
547 &SYSTEM_PROGRAM_ID,
548 &curve_data(),
549 )
550 .unwrap_err();
551 assert_eq!(
552 error.to_string(),
553 format!(
554 "invalid account owner: {SYSTEM_PROGRAM_ID}, expected {LAUNCHPAD_PROGRAM_ID}"
555 )
556 );
557 }
558
559 #[test]
560 fn maybe_rejects_empty_data_its_program_owns() {
561 let error =
562 BondingCurve::from_account_maybe(&LAUNCHPAD_PROGRAM_ID, &[])
563 .unwrap_err();
564 assert_eq!(error.to_string(), "invalid account discriminator");
565 }
566
567 #[test]
568 fn maybe_decodes_an_account_its_program_owns() {
569 assert_eq!(
570 BondingCurve::from_account_maybe(
571 &LAUNCHPAD_PROGRAM_ID,
572 &curve_data()
573 )
574 .unwrap(),
575 Some(curve())
576 );
577 }
578 }
579}