Skip to main content

sendfun_sdk/
lib.rs

1#![doc = include_str!("../README.md")]
2//!
3//! ## Reading accounts
4//!
5//! Read an account that can be missing:
6//!
7//! ```
8//! # use solana_address::Address;
9//! # struct Account { owner: Address, data: Vec<u8> }
10//! # struct Response<T> { value: T }
11//! # struct RpcClient;
12//! # impl RpcClient {
13//! #     fn commitment(&self) {}
14//! #     fn get_account_with_commitment(
15//! #         &self,
16//! #         _: &Address,
17//! #         _: (),
18//! #     ) -> Result<Response<Option<Account>>, std::io::Error> {
19//! #         let owner = sendfun_sdk::constants::SYSTEM_PROGRAM_ID;
20//! #         Ok(Response { value: Some(Account { owner, data: Vec::new() }) })
21//! #     }
22//! # }
23//! # let rpc = RpcClient;
24//! # let pda = Address::new_from_array([9; 32]);
25//! use sendfun_sdk::nexus::accounts::PartnerConfig;
26//!
27//! let partner = match rpc
28//!     .get_account_with_commitment(&pda, rpc.commitment())?
29//!     .value
30//! {
31//!     Some(account) => {
32//!         PartnerConfig::from_account_maybe(&account.owner, &account.data)?
33//!     }
34//!     None => None,
35//! };
36//! # assert!(partner.is_none());
37//! # Ok::<(), Box<dyn std::error::Error>>(())
38//! ```
39//!
40//! Read several accounts in one call. The RPC accepts at most 100 addresses
41//! per call:
42//!
43//! ```
44//! # use solana_address::Address;
45//! # struct Account { owner: Address, data: Vec<u8> }
46//! # struct RpcClient;
47//! # impl RpcClient {
48//! #     fn get_multiple_accounts(
49//! #         &self,
50//! #         addresses: &[Address],
51//! #     ) -> Result<Vec<Option<Account>>, std::io::Error> {
52//! #         Ok(addresses.iter().map(|_| None).collect())
53//! #     }
54//! # }
55//! # let rpc = RpcClient;
56//! # let keys = [Address::new_from_array([1; 32]), Address::new_from_array([2; 32])];
57//! use sendfun_sdk::launchpad::accounts::BondingCurve;
58//!
59//! let curves = rpc
60//!     .get_multiple_accounts(&keys)?
61//!     .iter()
62//!     .map(|account| match account {
63//!         Some(account) => {
64//!             BondingCurve::from_account_maybe(&account.owner, &account.data)
65//!         }
66//!         None => Ok(None),
67//!     })
68//!     .collect::<Result<Vec<_>, _>>()?;
69//! # assert_eq!(curves.len(), 2);
70//! # assert!(curves.iter().all(Option::is_none));
71//! # Ok::<(), Box<dyn std::error::Error>>(())
72//! ```
73
74pub 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, &quote);
97			let (addr2, _) =
98				crate::launchpad::pda::find_bonding_curve_pda(&base, &quote);
99			assert_eq!(addr1, addr2);
100			let (addr3, _) =
101				crate::launchpad::pda::find_bonding_curve_pda(&quote, &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, &quote);
110			let (lp, _) = crate::dex::pda::find_lp_mint_pda(&base, &quote);
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					&quote,
142				);
143			let (addr2, _) =
144				crate::nexus::pda::find_creator_fee_config_pda_from_id(
145					"twitter",
146					"SomeCreator",
147					&quote,
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			// Platform-seeded PDAs have no constants. Compare the finders.
281			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			// Two platforms must not share a fee preset.
302			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			// Two platforms must not share partner metadata.
317			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		/// Must match the published addresses. Only these checks find a wrong
328		/// seed order.
329		#[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				&quote,
378			);
379		let (addr2, bump2) = crate::nexus::pda::find_creator_fee_config_pda(
380			&creator_hash,
381			&quote,
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		/// Same-named accounts of two programs have the same discriminator.
526		/// Only the owner check tells them apart.
527		#[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}