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