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