mirror of
https://github.com/0xfnzero/solana-streamer.git
synced 2026-08-21 04:48:07 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
//! Bonding curve account for the Pump.fun Solana Program
|
||||
//!
|
||||
//! This module contains the definition for the bonding curve account.
|
||||
//!
|
||||
//! # Bonding Curve Account
|
||||
//!
|
||||
//! The bonding curve account is used to manage token pricing and liquidity.
|
||||
//!
|
||||
//! # Fields
|
||||
//!
|
||||
//! - `discriminator`: Unique identifier for the bonding curve
|
||||
//! - `virtual_token_reserves`: Virtual token reserves used for price calculations
|
||||
//! - `virtual_sol_reserves`: Virtual SOL reserves used for price calculations
|
||||
//! - `real_token_reserves`: Actual token reserves available for trading
|
||||
//! - `real_sol_reserves`: Actual SOL reserves available for trading
|
||||
//! - `token_total_supply`: Total supply of tokens
|
||||
//! - `complete`: Whether the bonding curve is complete/finalized
|
||||
//!
|
||||
//! # Methods
|
||||
//!
|
||||
//! - `new`: Creates a new bonding curve instance
|
||||
//! - `get_buy_price`: Calculates the amount of tokens received for a given SOL amount
|
||||
//! - `get_sell_price`: Calculates the amount of SOL received for selling tokens
|
||||
//! - `get_market_cap_sol`: Calculates the current market cap in SOL
|
||||
//! - `get_final_market_cap_sol`: Calculates the final market cap in SOL after all tokens are sold
|
||||
//! - `get_buy_out_price`: Calculates the price to buy out all remaining tokens
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
|
||||
/// Represents a bonding curve for token pricing and liquidity management
|
||||
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
|
||||
pub struct BondingCurveAccount {
|
||||
/// Unique identifier for the bonding curve
|
||||
pub discriminator: u64,
|
||||
/// Virtual token reserves used for price calculations
|
||||
pub virtual_token_reserves: u64,
|
||||
/// Virtual SOL reserves used for price calculations
|
||||
pub virtual_sol_reserves: u64,
|
||||
/// Actual token reserves available for trading
|
||||
pub real_token_reserves: u64,
|
||||
/// Actual SOL reserves available for trading
|
||||
pub real_sol_reserves: u64,
|
||||
/// Total supply of tokens
|
||||
pub token_total_supply: u64,
|
||||
/// Whether the bonding curve is complete/finalized
|
||||
pub complete: bool,
|
||||
}
|
||||
|
||||
impl BondingCurveAccount {
|
||||
/// Creates a new bonding curve instance
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `discriminator` - Unique identifier for the curve
|
||||
/// * `virtual_token_reserves` - Virtual token reserves for price calculations
|
||||
/// * `virtual_sol_reserves` - Virtual SOL reserves for price calculations
|
||||
/// * `real_token_reserves` - Actual token reserves available
|
||||
/// * `real_sol_reserves` - Actual SOL reserves available
|
||||
/// * `token_total_supply` - Total supply of tokens
|
||||
/// * `complete` - Whether the curve is complete
|
||||
pub fn new(
|
||||
discriminator: u64,
|
||||
virtual_token_reserves: u64,
|
||||
virtual_sol_reserves: u64,
|
||||
real_token_reserves: u64,
|
||||
real_sol_reserves: u64,
|
||||
token_total_supply: u64,
|
||||
complete: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
discriminator,
|
||||
virtual_token_reserves,
|
||||
virtual_sol_reserves,
|
||||
real_token_reserves,
|
||||
real_sol_reserves,
|
||||
token_total_supply,
|
||||
complete,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the amount of tokens received for a given SOL amount
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `amount` - Amount of SOL to spend
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(u64)` - Amount of tokens that would be received
|
||||
/// * `Err(&str)` - Error message if curve is complete
|
||||
pub fn get_buy_price(&self, amount: u64) -> Result<u64, &'static str> {
|
||||
if self.complete {
|
||||
return Err("Curve is complete");
|
||||
}
|
||||
|
||||
if amount == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// Calculate the product of virtual reserves using u128 to avoid overflow
|
||||
let n: u128 = (self.virtual_sol_reserves as u128) * (self.virtual_token_reserves as u128);
|
||||
|
||||
// Calculate the new virtual sol reserves after the purchase
|
||||
let i: u128 = (self.virtual_sol_reserves as u128) + (amount as u128);
|
||||
|
||||
// Calculate the new virtual token reserves after the purchase
|
||||
let r: u128 = n / i + 1;
|
||||
|
||||
// Calculate the amount of tokens to be purchased
|
||||
let s: u128 = (self.virtual_token_reserves as u128) - r;
|
||||
|
||||
// Convert back to u64 and return the minimum of calculated tokens and real reserves
|
||||
let s_u64 = s as u64;
|
||||
Ok(if s_u64 < self.real_token_reserves {
|
||||
s_u64
|
||||
} else {
|
||||
self.real_token_reserves
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculates the amount of SOL received for selling tokens
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `amount` - Amount of tokens to sell
|
||||
/// * `fee_basis_points` - Fee in basis points (1/100th of a percent)
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(u64)` - Amount of SOL that would be received after fees
|
||||
/// * `Err(&str)` - Error message if curve is complete
|
||||
pub fn get_sell_price(&self, amount: u64, fee_basis_points: u64) -> Result<u64, &'static str> {
|
||||
if self.complete {
|
||||
return Err("Curve is complete");
|
||||
}
|
||||
|
||||
if amount == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// Calculate the proportional amount of virtual sol reserves to be received using u128
|
||||
let n: u128 = ((amount as u128) * (self.virtual_sol_reserves as u128))
|
||||
/ ((self.virtual_token_reserves as u128) + (amount as u128));
|
||||
|
||||
// Calculate the fee amount in the same units
|
||||
let a: u128 = (n * (fee_basis_points as u128)) / 10000;
|
||||
|
||||
// Return the net amount after deducting the fee, converting back to u64
|
||||
Ok((n - a) as u64)
|
||||
}
|
||||
|
||||
/// Calculates the current market cap in SOL
|
||||
pub fn get_market_cap_sol(&self) -> u64 {
|
||||
if self.virtual_token_reserves == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
((self.token_total_supply as u128) * (self.virtual_sol_reserves as u128)
|
||||
/ (self.virtual_token_reserves as u128)) as u64
|
||||
}
|
||||
|
||||
/// Calculates the final market cap in SOL after all tokens are sold
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `fee_basis_points` - Fee in basis points (1/100th of a percent)
|
||||
pub fn get_final_market_cap_sol(&self, fee_basis_points: u64) -> u64 {
|
||||
let total_sell_value: u128 =
|
||||
self.get_buy_out_price(self.real_token_reserves, fee_basis_points) as u128;
|
||||
let total_virtual_value: u128 = (self.virtual_sol_reserves as u128) + total_sell_value;
|
||||
let total_virtual_tokens: u128 =
|
||||
(self.virtual_token_reserves as u128) - (self.real_token_reserves as u128);
|
||||
|
||||
if total_virtual_tokens == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
((self.token_total_supply as u128) * total_virtual_value / total_virtual_tokens) as u64
|
||||
}
|
||||
|
||||
/// Calculates the price to buy out all remaining tokens
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `amount` - Amount of tokens to buy
|
||||
/// * `fee_basis_points` - Fee in basis points (1/100th of a percent)
|
||||
pub fn get_buy_out_price(&self, amount: u64, fee_basis_points: u64) -> u64 {
|
||||
// Get the effective amount of sol tokens
|
||||
let sol_tokens: u128 = if amount < self.real_sol_reserves {
|
||||
self.real_sol_reserves as u128
|
||||
} else {
|
||||
amount as u128
|
||||
};
|
||||
|
||||
// Calculate total sell value
|
||||
let total_sell_value: u128 = (sol_tokens * (self.virtual_sol_reserves as u128))
|
||||
/ ((self.virtual_token_reserves as u128) - sol_tokens)
|
||||
+ 1;
|
||||
|
||||
// Calculate fee
|
||||
let fee: u128 = (total_sell_value * (fee_basis_points as u128)) / 10000;
|
||||
|
||||
// Return total including fee, converting back to u64
|
||||
(total_sell_value + fee) as u64
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn get_bonding_curve() -> BondingCurveAccount {
|
||||
BondingCurveAccount::new(
|
||||
1, // discriminator
|
||||
1000, // virtual_token_reserves
|
||||
1000, // virtual_sol_reserves
|
||||
500, // real_token_reserves
|
||||
500, // real_sol_reserves
|
||||
1000, // token_total_supply
|
||||
false, // complete
|
||||
)
|
||||
}
|
||||
|
||||
fn get_large_bonding_curve() -> BondingCurveAccount {
|
||||
BondingCurveAccount::new(
|
||||
1, // discriminator
|
||||
u64::MAX / 2, // virtual_token_reserves
|
||||
u64::MAX / 2, // virtual_sol_reserves
|
||||
u64::MAX / 4, // real_token_reserves
|
||||
u64::MAX / 4, // real_sol_reserves
|
||||
u64::MAX / 2, // token_total_supply
|
||||
false, // complete
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bonding_curve_account() {
|
||||
let bonding_curve: BondingCurveAccount = get_bonding_curve();
|
||||
|
||||
// Test buy price calculation
|
||||
assert_eq!(bonding_curve.get_buy_price(0).unwrap(), 0);
|
||||
|
||||
let buy_price = bonding_curve.get_buy_price(100).unwrap();
|
||||
assert!(buy_price > 0);
|
||||
assert!(buy_price <= bonding_curve.real_token_reserves);
|
||||
|
||||
// Test sell price calculation
|
||||
assert_eq!(bonding_curve.get_sell_price(0, 250).unwrap(), 0);
|
||||
|
||||
let sell_price = bonding_curve.get_sell_price(100, 250).unwrap();
|
||||
assert!(sell_price > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bonding_curve_complete() {
|
||||
let mut bonding_curve: BondingCurveAccount = get_bonding_curve();
|
||||
|
||||
// Test operations work when not complete
|
||||
assert!(bonding_curve.get_buy_price(100).is_ok());
|
||||
assert!(bonding_curve.get_sell_price(100, 250).is_ok());
|
||||
|
||||
// Set curve to complete
|
||||
bonding_curve.complete = true;
|
||||
|
||||
// Test operations fail when complete
|
||||
assert!(bonding_curve.get_buy_price(100).is_err());
|
||||
assert!(bonding_curve.get_sell_price(100, 250).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_market_cap_calculations() {
|
||||
let bonding_curve: BondingCurveAccount = get_bonding_curve();
|
||||
|
||||
// Test market cap calculations
|
||||
let market_cap = bonding_curve.get_market_cap_sol();
|
||||
assert!(market_cap > 0);
|
||||
|
||||
let final_market_cap = bonding_curve.get_final_market_cap_sol(250);
|
||||
assert!(final_market_cap > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buy_out_price() {
|
||||
let bonding_curve: BondingCurveAccount = get_bonding_curve();
|
||||
|
||||
let buy_out_price = bonding_curve.get_buy_out_price(100, 250);
|
||||
assert!(buy_out_price > 0);
|
||||
|
||||
// Test with amount less than real_sol_reserves
|
||||
let small_buy_out = bonding_curve.get_buy_out_price(400, 250);
|
||||
assert!(small_buy_out > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overflow_buy_price() {
|
||||
let bonding_curve = get_large_bonding_curve();
|
||||
|
||||
// Test buying with large SOL amount
|
||||
let buy_price = bonding_curve.get_buy_price(u64::MAX).unwrap();
|
||||
assert!(buy_price > 0);
|
||||
assert!(buy_price <= bonding_curve.real_token_reserves);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overflow_sell_price() {
|
||||
let bonding_curve = get_large_bonding_curve();
|
||||
|
||||
// Test selling large token amount
|
||||
let sell_price = bonding_curve.get_sell_price(u64::MAX / 4, 250).unwrap();
|
||||
assert!(sell_price > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overflow_market_cap() {
|
||||
let bonding_curve = get_large_bonding_curve();
|
||||
|
||||
// Test market cap with large values
|
||||
let market_cap = bonding_curve.get_market_cap_sol();
|
||||
assert!(market_cap > 0);
|
||||
|
||||
let final_market_cap = bonding_curve.get_final_market_cap_sol(250);
|
||||
assert!(final_market_cap > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overflow_buy_out_price() {
|
||||
let bonding_curve = get_large_bonding_curve();
|
||||
|
||||
// Test buy out with large token amount
|
||||
let buy_out_price = bonding_curve.get_buy_out_price(u64::MAX / 4, 250);
|
||||
assert!(buy_out_price > 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
//! Global account for the Pump.fun Solana Program
|
||||
//!
|
||||
//! This module contains the definition for the global configuration account.
|
||||
//!
|
||||
//! # Global Account
|
||||
//!
|
||||
//! The global account is used to store the global configuration for the Pump.fun program.
|
||||
//!
|
||||
//! # Fields
|
||||
//!
|
||||
//! - `discriminator`: Unique identifier for the global account
|
||||
//! - `initialized`: Whether the global account has been initialized
|
||||
//! - `authority`: Authority pubkey that can modify settings
|
||||
//! - `fee_recipient`: Account that receives fees
|
||||
//! - `initial_virtual_token_reserves`: Initial virtual token reserves for price calculations
|
||||
//! - `initial_virtual_sol_reserves`: Initial virtual SOL reserves for price calculations
|
||||
//! - `initial_real_token_reserves`: Initial actual token reserves available for trading
|
||||
//! - `token_total_supply`: Total supply of tokens
|
||||
//! - `fee_basis_points`: Fee in basis points (1/100th of a percent)
|
||||
//!
|
||||
//! # Methods
|
||||
//!
|
||||
//! - `new`: Creates a new global account instance
|
||||
//! - `get_initial_buy_price`: Calculates the initial amount of tokens received for a given SOL amount
|
||||
|
||||
use anchor_client::solana_sdk::pubkey::Pubkey;
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
|
||||
/// Represents the global configuration account for token pricing and fees
|
||||
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
|
||||
pub struct GlobalAccount {
|
||||
/// Unique identifier for the global account
|
||||
pub discriminator: u64,
|
||||
/// Whether the global account has been initialized
|
||||
pub initialized: bool,
|
||||
/// Authority that can modify global settings
|
||||
pub authority: Pubkey,
|
||||
/// Account that receives fees
|
||||
pub fee_recipient: Pubkey,
|
||||
/// Initial virtual token reserves for price calculations
|
||||
pub initial_virtual_token_reserves: u64,
|
||||
/// Initial virtual SOL reserves for price calculations
|
||||
pub initial_virtual_sol_reserves: u64,
|
||||
/// Initial actual token reserves available for trading
|
||||
pub initial_real_token_reserves: u64,
|
||||
/// Total supply of tokens
|
||||
pub token_total_supply: u64,
|
||||
/// Fee in basis points (1/100th of a percent)
|
||||
pub fee_basis_points: u64,
|
||||
}
|
||||
|
||||
impl GlobalAccount {
|
||||
/// Creates a new global account instance
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `discriminator` - Unique identifier for the account
|
||||
/// * `initialized` - Whether the account is initialized
|
||||
/// * `authority` - Authority pubkey that can modify settings
|
||||
/// * `fee_recipient` - Account that receives fees
|
||||
/// * `initial_virtual_token_reserves` - Initial virtual token reserves
|
||||
/// * `initial_virtual_sol_reserves` - Initial virtual SOL reserves
|
||||
/// * `initial_real_token_reserves` - Initial actual token reserves
|
||||
/// * `token_total_supply` - Total supply of tokens
|
||||
/// * `fee_basis_points` - Fee in basis points
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
discriminator: u64,
|
||||
initialized: bool,
|
||||
authority: Pubkey,
|
||||
fee_recipient: Pubkey,
|
||||
initial_virtual_token_reserves: u64,
|
||||
initial_virtual_sol_reserves: u64,
|
||||
initial_real_token_reserves: u64,
|
||||
token_total_supply: u64,
|
||||
fee_basis_points: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
discriminator,
|
||||
initialized,
|
||||
authority,
|
||||
fee_recipient,
|
||||
initial_virtual_token_reserves,
|
||||
initial_virtual_sol_reserves,
|
||||
initial_real_token_reserves,
|
||||
token_total_supply,
|
||||
fee_basis_points,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the initial amount of tokens received for a given SOL amount
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `amount` - Amount of SOL to spend
|
||||
///
|
||||
/// # Returns
|
||||
/// Amount of tokens that would be received
|
||||
pub fn get_initial_buy_price(&self, amount: u64) -> u64 {
|
||||
if amount == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let n: u128 = (self.initial_virtual_sol_reserves as u128)
|
||||
* (self.initial_virtual_token_reserves as u128);
|
||||
let i: u128 = (self.initial_virtual_sol_reserves as u128) + (amount as u128);
|
||||
let r: u128 = n / i + 1;
|
||||
let s: u128 = (self.initial_virtual_token_reserves as u128) - r;
|
||||
|
||||
if s < (self.initial_real_token_reserves as u128) {
|
||||
s as u64
|
||||
} else {
|
||||
self.initial_real_token_reserves
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn get_global() -> GlobalAccount {
|
||||
GlobalAccount::new(
|
||||
1,
|
||||
true,
|
||||
Pubkey::new_unique(),
|
||||
Pubkey::new_unique(),
|
||||
1000,
|
||||
1000,
|
||||
500,
|
||||
1000,
|
||||
250,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_large_global() -> GlobalAccount {
|
||||
GlobalAccount::new(
|
||||
1,
|
||||
true,
|
||||
Pubkey::new_unique(),
|
||||
Pubkey::new_unique(),
|
||||
u64::MAX,
|
||||
u64::MAX,
|
||||
u64::MAX / 2,
|
||||
u64::MAX,
|
||||
250,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_account() {
|
||||
let global: GlobalAccount = get_global();
|
||||
|
||||
// Test initial buy price calculation
|
||||
assert_eq!(global.get_initial_buy_price(0), 0);
|
||||
|
||||
let price: u64 = global.get_initial_buy_price(100);
|
||||
assert!(price > 0);
|
||||
assert!(price <= global.initial_real_token_reserves);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_account_max_reserves() {
|
||||
let mut global: GlobalAccount = get_global();
|
||||
global.initial_real_token_reserves = 100;
|
||||
|
||||
// Test that returned amount is capped by real_token_reserves
|
||||
let price: u64 = global.get_initial_buy_price(1000);
|
||||
assert_eq!(price, global.initial_real_token_reserves);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_account_overflow() {
|
||||
let global: GlobalAccount = get_large_global();
|
||||
|
||||
// Test with maximum possible SOL amount
|
||||
let price: u64 = global.get_initial_buy_price(u64::MAX);
|
||||
assert!(price > 0);
|
||||
assert!(price <= global.initial_real_token_reserves);
|
||||
|
||||
// Test with large but not maximum SOL amount
|
||||
let price: u64 = global.get_initial_buy_price(u64::MAX / 2);
|
||||
assert!(price > 0);
|
||||
assert!(price <= global.initial_real_token_reserves);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_account_overflow_edge_cases() {
|
||||
let mut global: GlobalAccount = get_large_global();
|
||||
global.initial_virtual_sol_reserves = u64::MAX - 1000;
|
||||
global.initial_virtual_token_reserves = u64::MAX - 1000;
|
||||
global.initial_real_token_reserves = u64::MAX / 4;
|
||||
|
||||
// Test with amounts near u64::MAX
|
||||
let price: u64 = global.get_initial_buy_price(u64::MAX - 1);
|
||||
assert!(price > 0);
|
||||
assert!(price <= global.initial_real_token_reserves);
|
||||
|
||||
let price: u64 = global.get_initial_buy_price(u64::MAX - 1000);
|
||||
assert!(price > 0);
|
||||
assert!(price <= global.initial_real_token_reserves);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//! Accounts for the Pump.fun Solana Program
|
||||
//!
|
||||
//! This module contains the definitions for the accounts used by the Pump.fun program.
|
||||
//!
|
||||
//! # Accounts
|
||||
//!
|
||||
//! - `BondingCurve`: Represents a bonding curve account.
|
||||
//! - `Global`: Represents the global configuration account.
|
||||
|
||||
mod bonding_curve;
|
||||
mod global;
|
||||
|
||||
pub use bonding_curve::*;
|
||||
pub use global::*;
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Constants used by the crate.
|
||||
//!
|
||||
//! This module contains various constants used throughout the crate, including:
|
||||
//!
|
||||
//! - Seeds for deriving Program Derived Addresses (PDAs)
|
||||
//! - Program account addresses and public keys
|
||||
//!
|
||||
//! The constants are organized into submodules for better organization:
|
||||
//!
|
||||
//! - `seeds`: Contains seed values used for PDA derivation
|
||||
//! - `accounts`: Contains important program account addresses
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
pub mod seeds {
|
||||
/// Seed for the global state PDA
|
||||
pub const GLOBAL_SEED: &[u8] = b"global";
|
||||
|
||||
/// Seed for the mint authority PDA
|
||||
pub const MINT_AUTHORITY_SEED: &[u8] = b"mint-authority";
|
||||
|
||||
/// Seed for bonding curve PDAs
|
||||
pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve";
|
||||
|
||||
/// Seed for metadata PDAs
|
||||
pub const METADATA_SEED: &[u8] = b"metadata";
|
||||
}
|
||||
|
||||
/// Constants related to program accounts and authorities
|
||||
pub mod accounts {
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
/// Public key for the Pump.fun program
|
||||
pub const PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
||||
|
||||
/// Public key for the MPL Token Metadata program
|
||||
pub const MPL_TOKEN_METADATA: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");
|
||||
|
||||
/// Authority for program events
|
||||
pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1");
|
||||
|
||||
/// System Program ID
|
||||
pub const SYSTEM_PROGRAM: Pubkey = pubkey!("11111111111111111111111111111111");
|
||||
|
||||
/// Token Program ID
|
||||
pub const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
|
||||
|
||||
/// Associated Token Program ID
|
||||
pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey =
|
||||
pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
||||
|
||||
/// Rent Sysvar ID
|
||||
pub const RENT: Pubkey = pubkey!("SysvarRent111111111111111111111111111111111");
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//! Error types for the Pump.fun SDK.
|
||||
//!
|
||||
//! This module defines the `ClientError` enum, which encompasses various error types that can occur when interacting with the Pump.fun program.
|
||||
//! It includes specific error cases for bonding curve operations, metadata uploads, Solana client errors, and more.
|
||||
//!
|
||||
//! The `ClientError` enum provides a comprehensive set of error types to help developers handle and debug issues that may arise during interactions with the Pump.fun program.
|
||||
//!
|
||||
//! # Error Types
|
||||
//!
|
||||
//! - `BondingCurveNotFound`: The bonding curve account was not found.
|
||||
//! - `BondingCurveError`: An error occurred while interacting with the bonding curve.
|
||||
//! - `BorshError`: An error occurred while serializing or deserializing data using Borsh.
|
||||
//! - `SolanaClientError`: An error occurred while interacting with the Solana RPC client.
|
||||
//! - `UploadMetadataError`: An error occurred while uploading metadata to IPFS.
|
||||
//! - `AnchorClientError`: An error occurred while interacting with the Anchor client.
|
||||
//! - `InvalidInput`: Invalid input parameters were provided.
|
||||
//! - `InsufficientFunds`: Insufficient funds for a transaction.
|
||||
//! - `SimulationError`: Transaction simulation failed.
|
||||
//! - `RateLimitExceeded`: Rate limit exceeded.
|
||||
|
||||
use anchor_client::solana_client;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClientError {
|
||||
/// Bonding curve account was not found
|
||||
BondingCurveNotFound,
|
||||
/// Error related to bonding curve operations
|
||||
BondingCurveError(&'static str),
|
||||
/// Error deserializing data using Borsh
|
||||
BorshError(std::io::Error),
|
||||
/// Error from Solana RPC client
|
||||
SolanaClientError(solana_client::client_error::ClientError),
|
||||
/// Error uploading metadata
|
||||
UploadMetadataError(Box<dyn std::error::Error>),
|
||||
/// Error from Anchor client
|
||||
AnchorClientError(anchor_client::ClientError),
|
||||
/// Invalid input parameters
|
||||
InvalidInput(&'static str),
|
||||
/// Insufficient funds for transaction
|
||||
InsufficientFunds,
|
||||
/// Transaction simulation failed
|
||||
SimulationError(String),
|
||||
/// Rate limit exceeded
|
||||
RateLimitExceeded,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ClientError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::BondingCurveNotFound => write!(f, "Bonding curve not found"),
|
||||
Self::BondingCurveError(msg) => write!(f, "Bonding curve error: {}", msg),
|
||||
Self::BorshError(err) => write!(f, "Borsh serialization error: {}", err),
|
||||
Self::SolanaClientError(err) => write!(f, "Solana client error: {}", err),
|
||||
Self::UploadMetadataError(err) => write!(f, "Metadata upload error: {}", err),
|
||||
Self::AnchorClientError(err) => write!(f, "Anchor client error: {}", err),
|
||||
Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
|
||||
Self::InsufficientFunds => write!(f, "Insufficient funds for transaction"),
|
||||
Self::SimulationError(msg) => write!(f, "Transaction simulation failed: {}", msg),
|
||||
Self::RateLimitExceeded => write!(f, "Rate limit exceeded"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ClientError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::BorshError(err) => Some(err),
|
||||
Self::SolanaClientError(err) => Some(err),
|
||||
Self::UploadMetadataError(err) => Some(err.as_ref()),
|
||||
Self::AnchorClientError(err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//! Instructions for interacting with the Pump.fun program.
|
||||
//!
|
||||
//! This module contains instruction builders for creating Solana instructions to interact with the
|
||||
//! Pump.fun program. Each function takes the required accounts and instruction data and returns a
|
||||
//! properly formatted Solana instruction.
|
||||
//!
|
||||
//! # Instructions
|
||||
//!
|
||||
//! - `create`: Instruction to create a new token with an associated bonding curve.
|
||||
//! - `buy`: Instruction to buy tokens from a bonding curve by providing SOL.
|
||||
//! - `sell`: Instruction to sell tokens back to the bonding curve in exchange for SOL.
|
||||
|
||||
use crate::{constants, PumpFun};
|
||||
use anchor_client::anchor_lang::InstructionData;
|
||||
use anchor_spl::associated_token::get_associated_token_address;
|
||||
use pumpfun_cpi as cpi;
|
||||
use solana_sdk::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
signature::Keypair,
|
||||
signer::Signer,
|
||||
};
|
||||
|
||||
/// Creates an instruction to create a new token with bonding curve
|
||||
///
|
||||
/// Creates a new SPL token with an associated bonding curve that determines its price.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `payer` - Keypair that will pay for account creation and transaction fees
|
||||
/// * `mint` - Keypair for the new token mint account that will be created
|
||||
/// * `args` - Create instruction data containing token name, symbol and metadata URI
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a Solana instruction that when executed will create the token and its accounts
|
||||
pub fn create(payer: &Keypair, mint: &Keypair, args: cpi::instruction::Create) -> Instruction {
|
||||
let bonding_curve: Pubkey = PumpFun::get_bonding_curve_pda(&mint.pubkey()).unwrap();
|
||||
Instruction::new_with_bytes(
|
||||
constants::accounts::PUMPFUN,
|
||||
&args.data(),
|
||||
vec![
|
||||
AccountMeta::new(mint.pubkey(), true),
|
||||
AccountMeta::new(PumpFun::get_mint_authority_pda(), false),
|
||||
AccountMeta::new(bonding_curve, false),
|
||||
AccountMeta::new(
|
||||
get_associated_token_address(&bonding_curve, &mint.pubkey()),
|
||||
false,
|
||||
),
|
||||
AccountMeta::new_readonly(PumpFun::get_global_pda(), false),
|
||||
AccountMeta::new_readonly(constants::accounts::MPL_TOKEN_METADATA, false),
|
||||
AccountMeta::new(PumpFun::get_metadata_pda(&mint.pubkey()), false),
|
||||
AccountMeta::new(payer.pubkey(), true),
|
||||
AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::accounts::ASSOCIATED_TOKEN_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::accounts::RENT, false),
|
||||
AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false),
|
||||
AccountMeta::new_readonly(constants::accounts::PUMPFUN, false),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates an instruction to buy tokens from a bonding curve
|
||||
///
|
||||
/// Buys tokens by providing SOL. The amount of tokens received is calculated based on
|
||||
/// the bonding curve formula. A portion of the SOL is taken as a fee and sent to the
|
||||
/// fee recipient account.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `payer` - Keypair that will provide the SOL to buy tokens
|
||||
/// * `mint` - Public key of the token mint to buy
|
||||
/// * `fee_recipient` - Public key of the account that will receive the transaction fee
|
||||
/// * `args` - Buy instruction data containing the SOL amount and maximum acceptable token price
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a Solana instruction that when executed will buy tokens from the bonding curve
|
||||
pub fn buy(
|
||||
payer: &Keypair,
|
||||
mint: &Pubkey,
|
||||
fee_recipient: &Pubkey,
|
||||
args: cpi::instruction::Buy,
|
||||
) -> Instruction {
|
||||
let bonding_curve: Pubkey = PumpFun::get_bonding_curve_pda(mint).unwrap();
|
||||
Instruction::new_with_bytes(
|
||||
constants::accounts::PUMPFUN,
|
||||
&args.data(),
|
||||
vec![
|
||||
AccountMeta::new_readonly(PumpFun::get_global_pda(), false),
|
||||
AccountMeta::new(*fee_recipient, false),
|
||||
AccountMeta::new_readonly(*mint, false),
|
||||
AccountMeta::new(bonding_curve, false),
|
||||
AccountMeta::new(get_associated_token_address(&bonding_curve, mint), false),
|
||||
AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false),
|
||||
AccountMeta::new(payer.pubkey(), true),
|
||||
AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::accounts::RENT, false),
|
||||
AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false),
|
||||
AccountMeta::new_readonly(constants::accounts::PUMPFUN, false),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates an instruction to sell tokens back to a bonding curve
|
||||
///
|
||||
/// Sells tokens back to the bonding curve in exchange for SOL. The amount of SOL received
|
||||
/// is calculated based on the bonding curve formula. A portion of the SOL is taken as
|
||||
/// a fee and sent to the fee recipient account.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `payer` - Keypair that owns the tokens to sell
|
||||
/// * `mint` - Public key of the token mint to sell
|
||||
/// * `fee_recipient` - Public key of the account that will receive the transaction fee
|
||||
/// * `args` - Sell instruction data containing token amount and minimum acceptable SOL output
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a Solana instruction that when executed will sell tokens to the bonding curve
|
||||
pub fn sell(
|
||||
payer: &Keypair,
|
||||
mint: &Pubkey,
|
||||
fee_recipient: &Pubkey,
|
||||
args: cpi::instruction::Sell,
|
||||
) -> Instruction {
|
||||
let bonding_curve: Pubkey = PumpFun::get_bonding_curve_pda(mint).unwrap();
|
||||
Instruction::new_with_bytes(
|
||||
constants::accounts::PUMPFUN,
|
||||
&args.data(),
|
||||
vec![
|
||||
AccountMeta::new_readonly(PumpFun::get_global_pda(), false),
|
||||
AccountMeta::new(*fee_recipient, false),
|
||||
AccountMeta::new_readonly(*mint, false),
|
||||
AccountMeta::new(bonding_curve, false),
|
||||
AccountMeta::new(get_associated_token_address(&bonding_curve, mint), false),
|
||||
AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false),
|
||||
AccountMeta::new(payer.pubkey(), true),
|
||||
AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::accounts::ASSOCIATED_TOKEN_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false),
|
||||
AccountMeta::new_readonly(constants::accounts::PUMPFUN, false),
|
||||
],
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
// #![doc = include_str!("../RUSTDOC.md")]
|
||||
|
||||
pub mod accounts;
|
||||
pub mod constants;
|
||||
pub mod error;
|
||||
pub mod instruction;
|
||||
pub mod utils;
|
||||
|
||||
use anchor_client::{
|
||||
solana_client::rpc_client::RpcClient,
|
||||
solana_sdk::{
|
||||
commitment_config::CommitmentConfig,
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, Signature},
|
||||
signer::Signer,
|
||||
},
|
||||
Client, Cluster, Program,
|
||||
};
|
||||
use anchor_spl::associated_token::{
|
||||
get_associated_token_address,
|
||||
spl_associated_token_account::instruction::create_associated_token_account,
|
||||
};
|
||||
use borsh::BorshDeserialize;
|
||||
pub use pumpfun_cpi as cpi;
|
||||
use solana_sdk::compute_budget::ComputeBudgetInstruction;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Configuration for priority fee compute unit parameters
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PriorityFee {
|
||||
/// Maximum compute units that can be consumed by the transaction
|
||||
pub limit: Option<u32>,
|
||||
/// Price in micro-lamports per compute unit
|
||||
pub price: Option<u64>,
|
||||
}
|
||||
|
||||
/// Main client for interacting with the Pump.fun program
|
||||
pub struct PumpFun {
|
||||
/// RPC client for Solana network requests
|
||||
pub rpc: RpcClient,
|
||||
/// Keypair used to sign transactions
|
||||
pub payer: Arc<Keypair>,
|
||||
/// Anchor client instance
|
||||
pub client: Client<Arc<Keypair>>,
|
||||
/// Anchor program instance
|
||||
pub program: Program<Arc<Keypair>>,
|
||||
}
|
||||
|
||||
impl PumpFun {
|
||||
/// Creates a new PumpFun client instance
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cluster` - Solana cluster to connect to (e.g. devnet, mainnet-beta)
|
||||
/// * `payer` - Keypair used to sign and pay for transactions
|
||||
/// * `options` - Optional commitment config for transaction finality
|
||||
/// * `ws` - Whether to use websocket connection instead of HTTP
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a new PumpFun client instance configured with the provided parameters
|
||||
pub fn new(
|
||||
cluster: Cluster,
|
||||
payer: Arc<Keypair>,
|
||||
options: Option<CommitmentConfig>,
|
||||
ws: Option<bool>,
|
||||
) -> Self {
|
||||
// Create Solana RPC Client with either WS or HTTP endpoint
|
||||
let rpc: RpcClient = RpcClient::new(if ws.unwrap_or(false) {
|
||||
cluster.ws_url()
|
||||
} else {
|
||||
cluster.url()
|
||||
});
|
||||
|
||||
// Create Anchor Client with optional commitment config
|
||||
let client: Client<Arc<Keypair>> = if let Some(options) = options {
|
||||
Client::new_with_options(cluster.clone(), payer.clone(), options)
|
||||
} else {
|
||||
Client::new(cluster.clone(), payer.clone())
|
||||
};
|
||||
|
||||
// Create Anchor Program instance for Pump.fun
|
||||
let program: Program<Arc<Keypair>> = client.program(cpi::ID).unwrap();
|
||||
|
||||
// Return configured PumpFun client
|
||||
Self {
|
||||
rpc,
|
||||
payer,
|
||||
client,
|
||||
program,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new token with metadata by uploading metadata to IPFS and initializing on-chain accounts
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `mint` - Keypair for the new token mint account that will be created
|
||||
/// * `metadata` - Token metadata including name, symbol, description and image file
|
||||
/// * `priority_fee` - Optional priority fee configuration for compute units
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns the transaction signature if successful, or a ClientError if the operation fails
|
||||
pub async fn create(
|
||||
&self,
|
||||
mint: &Keypair,
|
||||
metadata: utils::CreateTokenMetadata,
|
||||
priority_fee: Option<PriorityFee>,
|
||||
) -> Result<Signature, error::ClientError> {
|
||||
// First upload metadata and image to IPFS
|
||||
let ipfs: utils::TokenMetadataResponse = utils::create_token_metadata(metadata)
|
||||
.await
|
||||
.map_err(error::ClientError::UploadMetadataError)?;
|
||||
|
||||
let mut request = self.program.request();
|
||||
|
||||
// Add priority fee if provided
|
||||
if let Some(fee) = priority_fee {
|
||||
if let Some(limit) = fee.limit {
|
||||
let limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(limit);
|
||||
request = request.instruction(limit_ix);
|
||||
}
|
||||
|
||||
if let Some(price) = fee.price {
|
||||
let price_ix = ComputeBudgetInstruction::set_compute_unit_price(price);
|
||||
request = request.instruction(price_ix);
|
||||
}
|
||||
}
|
||||
|
||||
// Add create token instruction
|
||||
request = request.instruction(instruction::create(
|
||||
&self.payer.clone().as_ref(),
|
||||
mint,
|
||||
cpi::instruction::Create {
|
||||
_name: ipfs.metadata.name,
|
||||
_symbol: ipfs.metadata.symbol,
|
||||
_uri: ipfs.metadata.image,
|
||||
},
|
||||
));
|
||||
|
||||
// Add signers
|
||||
request = request.signer(&self.payer).signer(mint);
|
||||
|
||||
// Send transaction
|
||||
let signature: Signature = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(error::ClientError::AnchorClientError)?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
/// Creates a new token and immediately buys an initial amount in a single atomic transaction
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `mint` - Keypair for the new token mint
|
||||
/// * `metadata` - Token metadata to upload to IPFS
|
||||
/// * `amount_sol` - Amount of SOL to spend on initial buy in lamports
|
||||
/// * `slippage_basis_points` - Optional maximum acceptable slippage in basis points (1 bp = 0.01%). Defaults to 500
|
||||
/// * `priority_fee` - Optional priority fee configuration for compute units
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns the transaction signature if successful, or a ClientError if the operation fails
|
||||
pub async fn create_and_buy(
|
||||
&self,
|
||||
mint: &Keypair,
|
||||
metadata: utils::CreateTokenMetadata,
|
||||
amount_sol: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: Option<PriorityFee>,
|
||||
) -> Result<Signature, error::ClientError> {
|
||||
// Upload metadata to IPFS first
|
||||
let ipfs: utils::TokenMetadataResponse = utils::create_token_metadata(metadata)
|
||||
.await
|
||||
.map_err(error::ClientError::UploadMetadataError)?;
|
||||
|
||||
// Get accounts and calculate buy amounts
|
||||
let global_account = self.get_global_account()?;
|
||||
let buy_amount = global_account.get_initial_buy_price(amount_sol);
|
||||
let buy_amount_with_slippage =
|
||||
utils::calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(500));
|
||||
|
||||
let mut request = self.program.request();
|
||||
|
||||
// Add priority fee if provided
|
||||
if let Some(fee) = priority_fee {
|
||||
if let Some(limit) = fee.limit {
|
||||
let limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(limit);
|
||||
request = request.instruction(limit_ix);
|
||||
}
|
||||
|
||||
if let Some(price) = fee.price {
|
||||
let price_ix = ComputeBudgetInstruction::set_compute_unit_price(price);
|
||||
request = request.instruction(price_ix);
|
||||
}
|
||||
}
|
||||
|
||||
// Add create token instruction
|
||||
request = request.instruction(instruction::create(
|
||||
&self.payer.clone().as_ref(),
|
||||
mint,
|
||||
cpi::instruction::Create {
|
||||
_name: ipfs.metadata.name,
|
||||
_symbol: ipfs.metadata.symbol,
|
||||
_uri: ipfs.metadata.image,
|
||||
},
|
||||
));
|
||||
|
||||
// Create Associated Token Account if needed
|
||||
let ata: Pubkey = get_associated_token_address(&self.payer.pubkey(), &mint.pubkey());
|
||||
if self.rpc.get_account(&ata).is_err() {
|
||||
request = request.instruction(create_associated_token_account(
|
||||
&self.payer.pubkey(),
|
||||
&self.payer.pubkey(),
|
||||
&mint.pubkey(),
|
||||
&constants::accounts::TOKEN_PROGRAM,
|
||||
));
|
||||
}
|
||||
|
||||
// Add buy instruction
|
||||
request = request.instruction(instruction::buy(
|
||||
&self.payer.clone().as_ref(),
|
||||
&mint.pubkey(),
|
||||
&global_account.fee_recipient,
|
||||
cpi::instruction::Buy {
|
||||
_amount: buy_amount,
|
||||
_max_sol_cost: buy_amount_with_slippage,
|
||||
},
|
||||
));
|
||||
|
||||
// Add signers and send transaction
|
||||
let signature: Signature = request
|
||||
.signer(&self.payer)
|
||||
.signer(mint)
|
||||
.send()
|
||||
.await
|
||||
.map_err(error::ClientError::AnchorClientError)?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
/// Buys tokens from a bonding curve by spending SOL
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `mint` - Public key of the token mint to buy
|
||||
/// * `amount_sol` - Amount of SOL to spend in lamports
|
||||
/// * `slippage_basis_points` - Optional maximum acceptable slippage in basis points (1 bp = 0.01%). Defaults to 500
|
||||
/// * `priority_fee` - Optional priority fee configuration for compute units
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns the transaction signature if successful, or a ClientError if the operation fails
|
||||
pub async fn buy(
|
||||
&self,
|
||||
mint: &Pubkey,
|
||||
amount_sol: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: Option<PriorityFee>,
|
||||
) -> Result<Signature, error::ClientError> {
|
||||
// Get accounts and calculate buy amounts
|
||||
let global_account = self.get_global_account()?;
|
||||
let bonding_curve_account = self.get_bonding_curve_account(mint)?;
|
||||
let buy_amount = bonding_curve_account
|
||||
.get_buy_price(amount_sol)
|
||||
.map_err(error::ClientError::BondingCurveError)?;
|
||||
let buy_amount_with_slippage =
|
||||
utils::calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(500));
|
||||
|
||||
let mut request = self.program.request();
|
||||
|
||||
// Add priority fee if provided
|
||||
if let Some(fee) = priority_fee {
|
||||
if let Some(limit) = fee.limit {
|
||||
let limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(limit);
|
||||
request = request.instruction(limit_ix);
|
||||
}
|
||||
|
||||
if let Some(price) = fee.price {
|
||||
let price_ix = ComputeBudgetInstruction::set_compute_unit_price(price);
|
||||
request = request.instruction(price_ix);
|
||||
}
|
||||
}
|
||||
|
||||
// Create Associated Token Account if needed
|
||||
let ata: Pubkey = get_associated_token_address(&self.payer.pubkey(), mint);
|
||||
if self.rpc.get_account(&ata).is_err() {
|
||||
request = request.instruction(create_associated_token_account(
|
||||
&self.payer.pubkey(),
|
||||
&self.payer.pubkey(),
|
||||
mint,
|
||||
&constants::accounts::TOKEN_PROGRAM,
|
||||
));
|
||||
}
|
||||
|
||||
// Add buy instruction
|
||||
request = request.instruction(instruction::buy(
|
||||
&self.payer.clone().as_ref(),
|
||||
mint,
|
||||
&global_account.fee_recipient,
|
||||
cpi::instruction::Buy {
|
||||
_amount: buy_amount,
|
||||
_max_sol_cost: buy_amount_with_slippage,
|
||||
},
|
||||
));
|
||||
|
||||
// Add signer
|
||||
request = request.signer(&self.payer);
|
||||
|
||||
// Send transaction
|
||||
let signature: Signature = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(error::ClientError::AnchorClientError)?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
/// Sells tokens back to the bonding curve in exchange for SOL
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `mint` - Public key of the token mint to sell
|
||||
/// * `amount_token` - Optional amount of tokens to sell in base units. If None, sells entire balance
|
||||
/// * `slippage_basis_points` - Optional maximum acceptable slippage in basis points (1 bp = 0.01%). Defaults to 500
|
||||
/// * `priority_fee` - Optional priority fee configuration for compute units
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns the transaction signature if successful, or a ClientError if the operation fails
|
||||
pub async fn sell(
|
||||
&self,
|
||||
mint: &Pubkey,
|
||||
amount_token: Option<u64>,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: Option<PriorityFee>,
|
||||
) -> Result<Signature, error::ClientError> {
|
||||
// Get accounts and calculate sell amounts
|
||||
let ata: Pubkey = get_associated_token_address(&self.payer.pubkey(), mint);
|
||||
let balance = self.rpc.get_token_account_balance(&ata).unwrap();
|
||||
let balance_u64: u64 = balance.amount.parse::<u64>().unwrap();
|
||||
let _amount = amount_token.unwrap_or(balance_u64);
|
||||
let global_account = self.get_global_account()?;
|
||||
let bonding_curve_account = self.get_bonding_curve_account(mint)?;
|
||||
let min_sol_output = bonding_curve_account
|
||||
.get_sell_price(_amount, global_account.fee_basis_points)
|
||||
.map_err(error::ClientError::BondingCurveError)?;
|
||||
let _min_sol_output = utils::calculate_with_slippage_sell(
|
||||
min_sol_output,
|
||||
slippage_basis_points.unwrap_or(500),
|
||||
);
|
||||
|
||||
let mut request = self.program.request();
|
||||
|
||||
// Add priority fee if provided
|
||||
if let Some(fee) = priority_fee {
|
||||
if let Some(limit) = fee.limit {
|
||||
let limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(limit);
|
||||
request = request.instruction(limit_ix);
|
||||
}
|
||||
|
||||
if let Some(price) = fee.price {
|
||||
let price_ix = ComputeBudgetInstruction::set_compute_unit_price(price);
|
||||
request = request.instruction(price_ix);
|
||||
}
|
||||
}
|
||||
|
||||
// Add sell instruction
|
||||
request = request.instruction(instruction::sell(
|
||||
&self.payer.clone().as_ref(),
|
||||
mint,
|
||||
&global_account.fee_recipient,
|
||||
cpi::instruction::Sell {
|
||||
_amount,
|
||||
_min_sol_output,
|
||||
},
|
||||
));
|
||||
|
||||
// Add signer
|
||||
request = request.signer(&self.payer);
|
||||
|
||||
// Send transaction
|
||||
let signature: Signature = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(error::ClientError::AnchorClientError)?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
/// Gets the Program Derived Address (PDA) for the global state account
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns the PDA public key derived from the GLOBAL_SEED
|
||||
pub fn get_global_pda() -> Pubkey {
|
||||
let seeds: &[&[u8]; 1] = &[constants::seeds::GLOBAL_SEED];
|
||||
let program_id: &Pubkey = &cpi::ID;
|
||||
Pubkey::find_program_address(seeds, program_id).0
|
||||
}
|
||||
|
||||
/// Gets the Program Derived Address (PDA) for the mint authority
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns the PDA public key derived from the MINT_AUTHORITY_SEED
|
||||
pub fn get_mint_authority_pda() -> Pubkey {
|
||||
let seeds: &[&[u8]; 1] = &[constants::seeds::MINT_AUTHORITY_SEED];
|
||||
let program_id: &Pubkey = &cpi::ID;
|
||||
Pubkey::find_program_address(seeds, program_id).0
|
||||
}
|
||||
|
||||
/// Gets the Program Derived Address (PDA) for a token's bonding curve account
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `mint` - Public key of the token mint
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns Some(PDA) if derivation succeeds, or None if it fails
|
||||
pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
let seeds: &[&[u8]; 2] = &[constants::seeds::BONDING_CURVE_SEED, mint.as_ref()];
|
||||
let program_id: &Pubkey = &cpi::ID;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
}
|
||||
|
||||
/// Gets the Program Derived Address (PDA) for a token's metadata account
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `mint` - Public key of the token mint
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns the PDA public key for the token's metadata account
|
||||
pub fn get_metadata_pda(mint: &Pubkey) -> Pubkey {
|
||||
let seeds: &[&[u8]; 3] = &[
|
||||
constants::seeds::METADATA_SEED,
|
||||
constants::accounts::MPL_TOKEN_METADATA.as_ref(),
|
||||
mint.as_ref(),
|
||||
];
|
||||
let program_id: &Pubkey = &constants::accounts::MPL_TOKEN_METADATA;
|
||||
Pubkey::find_program_address(seeds, program_id).0
|
||||
}
|
||||
|
||||
/// Gets the global state account data containing program-wide configuration
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns the deserialized GlobalAccount if successful, or a ClientError if the operation fails
|
||||
pub fn get_global_account(&self) -> Result<accounts::GlobalAccount, error::ClientError> {
|
||||
let global: Pubkey = Self::get_global_pda();
|
||||
|
||||
let account = self
|
||||
.rpc
|
||||
.get_account(&global)
|
||||
.map_err(error::ClientError::SolanaClientError)?;
|
||||
|
||||
accounts::GlobalAccount::try_from_slice(&account.data)
|
||||
.map_err(error::ClientError::BorshError)
|
||||
}
|
||||
|
||||
/// Gets a token's bonding curve account data containing pricing parameters
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `mint` - Public key of the token mint
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns the deserialized BondingCurveAccount if successful, or a ClientError if the operation fails
|
||||
pub fn get_bonding_curve_account(
|
||||
&self,
|
||||
mint: &Pubkey,
|
||||
) -> Result<accounts::BondingCurveAccount, error::ClientError> {
|
||||
let bonding_curve_pda =
|
||||
Self::get_bonding_curve_pda(mint).ok_or(error::ClientError::BondingCurveNotFound)?;
|
||||
|
||||
let account = self
|
||||
.rpc
|
||||
.get_account(&bonding_curve_pda)
|
||||
.map_err(error::ClientError::SolanaClientError)?;
|
||||
|
||||
accounts::BondingCurveAccount::try_from_slice(&account.data)
|
||||
.map_err(error::ClientError::BorshError)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anchor_client::solana_sdk::signer::keypair::Keypair;
|
||||
|
||||
#[test]
|
||||
fn test_new_client() {
|
||||
let payer = Arc::new(Keypair::new());
|
||||
let client = PumpFun::new(Cluster::Devnet, Arc::clone(&payer), None, None);
|
||||
assert_eq!(client.payer.pubkey(), payer.pubkey());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_pdas() {
|
||||
let mint = Keypair::new();
|
||||
let global_pda = PumpFun::get_global_pda();
|
||||
let mint_authority_pda = PumpFun::get_mint_authority_pda();
|
||||
let bonding_curve_pda = PumpFun::get_bonding_curve_pda(&mint.pubkey());
|
||||
let metadata_pda = PumpFun::get_metadata_pda(&mint.pubkey());
|
||||
|
||||
assert!(global_pda != Pubkey::default());
|
||||
assert!(mint_authority_pda != Pubkey::default());
|
||||
assert!(bonding_curve_pda.is_some());
|
||||
assert!(metadata_pda != Pubkey::default());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
//! Utilities for working with token metadata and IPFS uploads.
|
||||
//!
|
||||
//! This module provides functionality for creating and managing token metadata,
|
||||
//! including uploading image and metadata to IPFS via the Pump.fun API.
|
||||
|
||||
use isahc::AsyncReadResponseExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
/// Metadata structure for a token, matching the format expected by Pump.fun.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TokenMetadata {
|
||||
/// Name of the token
|
||||
pub name: String,
|
||||
/// Token symbol (e.g. "BTC")
|
||||
pub symbol: String,
|
||||
/// Description of the token
|
||||
pub description: String,
|
||||
/// IPFS URL of the token's image
|
||||
pub image: String,
|
||||
/// Whether to display the token's name
|
||||
pub show_name: bool,
|
||||
/// Creation timestamp/source
|
||||
pub created_on: String,
|
||||
/// Twitter handle
|
||||
pub twitter: Option<String>,
|
||||
/// Telegram handle
|
||||
pub telegram: Option<String>,
|
||||
/// Website URL
|
||||
pub website: Option<String>,
|
||||
}
|
||||
|
||||
/// Response received after successfully uploading token metadata.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TokenMetadataResponse {
|
||||
/// The uploaded token metadata
|
||||
pub metadata: TokenMetadata,
|
||||
/// IPFS URI where the metadata is stored
|
||||
pub metadata_uri: String,
|
||||
}
|
||||
|
||||
/// Parameters for creating new token metadata.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateTokenMetadata {
|
||||
/// Name of the token
|
||||
pub name: String,
|
||||
/// Token symbol (e.g. "BTC")
|
||||
pub symbol: String,
|
||||
/// Description of the token
|
||||
pub description: String,
|
||||
/// Path to the token's image file
|
||||
pub file: String,
|
||||
/// Optional Twitter handle
|
||||
pub twitter: Option<String>,
|
||||
/// Optional Telegram group
|
||||
pub telegram: Option<String>,
|
||||
/// Optional website URL
|
||||
pub website: Option<String>,
|
||||
}
|
||||
|
||||
/// Creates and uploads token metadata to IPFS via the Pump.fun API.
|
||||
///
|
||||
/// This function takes token metadata and an image file, constructs a multipart form request,
|
||||
/// and uploads it to the Pump.fun IPFS API endpoint. The metadata and image are stored on IPFS
|
||||
/// and the function returns the IPFS locations.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `metadata` - Token metadata and image file information
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a `Result` containing the `TokenMetadataResponse` with IPFS locations on success,
|
||||
/// or an error if the upload fails.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pumpfun::utils::{CreateTokenMetadata, create_token_metadata};
|
||||
///
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let metadata = CreateTokenMetadata {
|
||||
/// name: "My Token".to_string(),
|
||||
/// symbol: "MT".to_string(),
|
||||
/// description: "A test token".to_string(),
|
||||
/// file: "path/to/image.png".to_string(),
|
||||
/// twitter: None,
|
||||
/// telegram: None,
|
||||
/// website: Some("https://example.com".to_string()),
|
||||
/// };
|
||||
///
|
||||
/// let response = create_token_metadata(metadata).await?;
|
||||
/// println!("Metadata URI: {}", response.metadata_uri);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn create_token_metadata(
|
||||
metadata: CreateTokenMetadata,
|
||||
) -> Result<TokenMetadataResponse, Box<dyn std::error::Error>> {
|
||||
let boundary = "------------------------f4d9c2e8b7a5310f";
|
||||
let mut body = Vec::new();
|
||||
|
||||
// Helper function to append form data
|
||||
fn append_text_field(body: &mut Vec<u8>, boundary: &str, name: &str, value: &str) {
|
||||
body.extend_from_slice(b"--");
|
||||
body.extend_from_slice(boundary.as_bytes());
|
||||
body.extend_from_slice(b"\r\n");
|
||||
body.extend_from_slice(
|
||||
format!("Content-Disposition: form-data; name=\"{}\"\r\n\r\n", name).as_bytes(),
|
||||
);
|
||||
body.extend_from_slice(value.as_bytes());
|
||||
body.extend_from_slice(b"\r\n");
|
||||
}
|
||||
|
||||
// Append form fields
|
||||
append_text_field(&mut body, boundary, "name", &metadata.name);
|
||||
append_text_field(&mut body, boundary, "symbol", &metadata.symbol);
|
||||
append_text_field(&mut body, boundary, "description", &metadata.description);
|
||||
if let Some(twitter) = metadata.twitter {
|
||||
append_text_field(&mut body, boundary, "twitter", &twitter);
|
||||
}
|
||||
if let Some(telegram) = metadata.telegram {
|
||||
append_text_field(&mut body, boundary, "telegram", &telegram);
|
||||
}
|
||||
if let Some(website) = metadata.website {
|
||||
append_text_field(&mut body, boundary, "website", &website);
|
||||
}
|
||||
append_text_field(&mut body, boundary, "showName", "true");
|
||||
|
||||
// Append file part
|
||||
body.extend_from_slice(b"--");
|
||||
body.extend_from_slice(boundary.as_bytes());
|
||||
body.extend_from_slice(b"\r\n");
|
||||
body.extend_from_slice(b"Content-Disposition: form-data; name=\"file\"; filename=\"file\"\r\n");
|
||||
body.extend_from_slice(b"Content-Type: application/octet-stream\r\n\r\n");
|
||||
|
||||
// Read the file contents
|
||||
let mut file = File::open(&metadata.file)?;
|
||||
let mut file_contents = Vec::new();
|
||||
file.read_to_end(&mut file_contents)?;
|
||||
body.extend_from_slice(&file_contents);
|
||||
|
||||
// Close the boundary
|
||||
body.extend_from_slice(b"\r\n--");
|
||||
body.extend_from_slice(boundary.as_bytes());
|
||||
body.extend_from_slice(b"--\r\n");
|
||||
|
||||
let client = isahc::HttpClient::new()?;
|
||||
let request = isahc::Request::builder()
|
||||
.method("POST")
|
||||
.uri("https://pump.fun/api/ipfs")
|
||||
.header(
|
||||
"Content-Type",
|
||||
format!("multipart/form-data; boundary={}", boundary),
|
||||
)
|
||||
.header("Content-Length", body.len() as u64)
|
||||
.body(isahc::AsyncBody::from(body))?;
|
||||
|
||||
// Send request and print response
|
||||
let mut response = client.send_async(request).await?;
|
||||
let text = response.text().await?;
|
||||
let json: TokenMetadataResponse = serde_json::from_str(&text)?;
|
||||
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Calculates the maximum amount to pay when buying tokens, accounting for slippage tolerance
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `amount` - The base amount in lamports (1 SOL = 1,000,000,000 lamports)
|
||||
/// * `basis_points` - The slippage tolerance in basis points (1% = 100 basis points)
|
||||
///
|
||||
/// # Returns
|
||||
/// The maximum amount to pay, including slippage tolerance
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use pumpfun::utils;
|
||||
///
|
||||
/// let amount = 1_000_000_000; // 1 SOL in lamports
|
||||
/// let slippage = 100; // 1% slippage tolerance
|
||||
///
|
||||
/// let max_amount = utils::calculate_with_slippage_buy(amount, slippage);
|
||||
/// assert_eq!(max_amount, 1_010_000_000); // 1.01 SOL
|
||||
/// ```
|
||||
pub fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64 {
|
||||
amount + (amount * basis_points) / 10000
|
||||
}
|
||||
|
||||
/// Calculates the minimum amount to receive when selling tokens, accounting for slippage tolerance
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `amount` - The base amount in lamports (1 SOL = 1,000,000,000 lamports)
|
||||
/// * `basis_points` - The slippage tolerance in basis points (1% = 100 basis points)
|
||||
///
|
||||
/// # Returns
|
||||
/// The minimum amount to receive, accounting for slippage tolerance
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use pumpfun::utils;
|
||||
///
|
||||
/// let amount = 1_000_000_000; // 1 SOL in lamports
|
||||
/// let slippage = 100; // 1% slippage tolerance
|
||||
///
|
||||
/// let min_amount = utils::calculate_with_slippage_sell(amount, slippage);
|
||||
/// assert_eq!(min_amount, 990_000_000); // 0.99 SOL
|
||||
/// ```
|
||||
pub fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 {
|
||||
amount - (amount * basis_points) / 10000
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs::write;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_token_metadata() {
|
||||
// Create a temporary file
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let file_path = temp_dir.join("test_image.png");
|
||||
write(&file_path, b"fake image data").unwrap();
|
||||
|
||||
// Create test metadata
|
||||
let metadata = CreateTokenMetadata {
|
||||
name: "Test Token".to_string(),
|
||||
symbol: "TEST".to_string(),
|
||||
description: "Test Description".to_string(),
|
||||
file: file_path.to_str().unwrap().to_string(),
|
||||
twitter: None,
|
||||
telegram: None,
|
||||
website: Some("https://example.com".to_string()),
|
||||
};
|
||||
|
||||
// Call the function
|
||||
let result = create_token_metadata(metadata).await;
|
||||
|
||||
// Assert the result
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
|
||||
// Verify response fields
|
||||
assert_eq!(response.metadata.name, "Test Token");
|
||||
assert_eq!(response.metadata.symbol, "TEST");
|
||||
assert_eq!(response.metadata.description, "Test Description");
|
||||
assert!(response.metadata.image.starts_with("https://ipfs.io/ipfs/"));
|
||||
assert!(response.metadata.show_name);
|
||||
assert_eq!(response.metadata.created_on, "https://pump.fun");
|
||||
assert!(response.metadata_uri.starts_with("https://ipfs.io/ipfs/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_with_slippage_buy() {
|
||||
let amount = 1_000_000_000; // 1 SOL in lamports
|
||||
let slippage = 100; // 1% slippage tolerance
|
||||
let max_amount = calculate_with_slippage_buy(amount, slippage);
|
||||
assert_eq!(max_amount, 1_010_000_000); // 1.01 SOL
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_with_slippage_sell() {
|
||||
let amount = 1_000_000_000; // 1 SOL in lamports
|
||||
let slippage = 100; // 1% slippage tolerance
|
||||
let min_amount = calculate_with_slippage_sell(amount, slippage);
|
||||
assert_eq!(min_amount, 990_000_000); // 0.99 SOL
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user