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/// The Token-2022 interface that `transfer_fee::mint_extensions` and
86/// `transfer_fee::extension` take their types from.
87pub 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, &quote);
101			let (addr2, _) =
102				crate::launchpad::pda::find_bonding_curve_pda(&base, &quote);
103			assert_eq!(addr1, addr2);
104			let (addr3, _) =
105				crate::launchpad::pda::find_bonding_curve_pda(&quote, &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, &quote);
114			let (lp, _) = crate::dex::pda::find_lp_mint_pda(&base, &quote);
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			// Platform-seeded PDAs have no constants. Compare the finders.
267			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			// Two platforms must not share a fee preset.
288			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			// Two platforms must not share partner metadata.
303			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		/// Must match the published addresses. Only these checks find a wrong
314		/// seed order.
315		#[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		/// Same-named accounts of two programs have the same discriminator.
495		/// Only the owner check tells them apart.
496		#[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}