add yellowstone grpc
This commit is contained in:
Executable
+333
@@ -0,0 +1,333 @@
|
||||
//! 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
|
||||
}
|
||||
|
||||
pub fn get_token_price(&self) -> f64 {
|
||||
let v_sol = self.virtual_sol_reserves as f64 / 100_000_000.0;
|
||||
let v_tokens = self.virtual_token_reserves as f64 / 100_000.0;
|
||||
let token_price = v_sol / v_tokens;
|
||||
token_price
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
Executable
+201
@@ -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 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);
|
||||
}
|
||||
}
|
||||
Executable
+14
@@ -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::*;
|
||||
Executable
+53
@@ -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");
|
||||
}
|
||||
Executable
+181
@@ -0,0 +1,181 @@
|
||||
//! 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.
|
||||
//! - `InvalidInput`: Invalid input parameters were provided.
|
||||
//! - `InsufficientFunds`: Insufficient funds for a transaction.
|
||||
//! - `SimulationError`: Transaction simulation failed.
|
||||
//! - `RateLimitExceeded`: Rate limit exceeded.
|
||||
|
||||
use serde_json::Error;
|
||||
use solana_client::{
|
||||
client_error::ClientError as SolanaClientError,
|
||||
pubsub_client::PubsubClientError
|
||||
};
|
||||
use solana_sdk::pubkey::ParsePubkeyError;
|
||||
|
||||
#[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>),
|
||||
/// Invalid input parameters
|
||||
InvalidInput(&'static str),
|
||||
/// Insufficient funds for transaction
|
||||
InsufficientFunds,
|
||||
/// Transaction simulation failed
|
||||
SimulationError(String),
|
||||
/// Rate limit exceeded
|
||||
RateLimitExceeded,
|
||||
|
||||
OrderLimitExceeded,
|
||||
|
||||
ExternalService(String),
|
||||
|
||||
Redis(String, String),
|
||||
|
||||
Solana(String, String),
|
||||
|
||||
Parse(String, String),
|
||||
|
||||
Pubkey(String, String),
|
||||
|
||||
Jito(String, String),
|
||||
|
||||
Join(String),
|
||||
|
||||
Subscribe(String, String),
|
||||
|
||||
Send(String, String),
|
||||
|
||||
Other(String),
|
||||
|
||||
InvalidData(String),
|
||||
|
||||
PumpFunBuy(String),
|
||||
|
||||
PumpFunSell(String),
|
||||
|
||||
Timeout(String, String),
|
||||
|
||||
Duplicate(String),
|
||||
|
||||
InvalidEventType,
|
||||
|
||||
ChannelClosed,
|
||||
}
|
||||
|
||||
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::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::ExternalService(msg) => write!(f, "External service error: {}", msg),
|
||||
Self::RateLimitExceeded => write!(f, "Rate limit exceeded"),
|
||||
Self::OrderLimitExceeded => write!(f, "Order limit exceeded"),
|
||||
Self::Solana(msg, details) => write!(f, "Solana error: {}, details: {}", msg, details),
|
||||
Self::Parse(msg, details) => write!(f, "Parse error: {}, details: {}", msg, details),
|
||||
Self::Jito(msg, details) => write!(f, "Jito error: {}, details: {}", msg, details),
|
||||
Self::Redis(msg, details) => write!(f, "Redis error: {}, details: {}", msg, details),
|
||||
Self::Join(msg) => write!(f, "Task join error: {}", msg),
|
||||
Self::Pubkey(msg, details) => write!(f, "Pubkey error: {}, details: {}", msg, details),
|
||||
Self::Subscribe(msg, details) => write!(f, "Subscribe error: {}, details: {}", msg, details),
|
||||
Self::Send(msg, details) => write!(f, "Send error: {}, details: {}", msg, details),
|
||||
Self::Other(msg) => write!(f, "Other error: {}", msg),
|
||||
Self::PumpFunBuy(msg) => write!(f, "PumpFun buy error: {}", msg),
|
||||
Self::PumpFunSell(msg) => write!(f, "PumpFun sell error: {}", msg),
|
||||
Self::InvalidData(msg) => write!(f, "Invalid data: {}", msg),
|
||||
Self::Timeout(msg, details) => write!(f, "Operation timed out: {}, details: {}", msg, details),
|
||||
Self::Duplicate(msg) => write!(f, "Duplicate event: {}", msg),
|
||||
Self::InvalidEventType => write!(f, "Invalid event type"),
|
||||
Self::ChannelClosed => write!(f, "Channel closed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
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::ExternalService(_) => None,
|
||||
Self::Redis(_, _) => None,
|
||||
Self::Solana(_, _) => None,
|
||||
Self::Parse(_, _) => None,
|
||||
Self::Jito(_, _) => None,
|
||||
Self::Join(_) => None,
|
||||
Self::Pubkey(_, _) => None,
|
||||
Self::Subscribe(_, _) => None,
|
||||
Self::Send(_, _) => None,
|
||||
Self::Other(_) => None,
|
||||
Self::PumpFunBuy(_) => None,
|
||||
Self::PumpFunSell(_) => None,
|
||||
Self::Timeout(_, _) => None,
|
||||
Self::Duplicate(_) => None,
|
||||
Self::InvalidEventType => None,
|
||||
Self::ChannelClosed => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SolanaClientError> for ClientError {
|
||||
fn from(error: SolanaClientError) -> Self {
|
||||
ClientError::Solana(
|
||||
"Solana client error".to_string(),
|
||||
error.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PubsubClientError> for ClientError {
|
||||
fn from(error: PubsubClientError) -> Self {
|
||||
ClientError::Solana(
|
||||
"PubSub client error".to_string(),
|
||||
error.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParsePubkeyError> for ClientError {
|
||||
fn from(error: ParsePubkeyError) -> Self {
|
||||
ClientError::Pubkey(
|
||||
"Pubkey error".to_string(),
|
||||
error.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for ClientError {
|
||||
fn from(err: Error) -> Self {
|
||||
ClientError::Parse(
|
||||
"JSON serialization error".to_string(),
|
||||
err.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub type ClientResult<T> = Result<T, ClientError>;
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
use anyhow::anyhow;
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine;
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use regex::Regex;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
use super::myerror::AppError;
|
||||
|
||||
pub const PROGRAM_DATA: &str = "Program data: ";
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
|
||||
pub struct TradeEvent {
|
||||
pub mint: Pubkey,
|
||||
pub sol_amount: u64,
|
||||
pub token_amount: u64,
|
||||
pub is_buy: bool,
|
||||
pub user: Pubkey,
|
||||
pub timestamp: u64,
|
||||
pub virtual_sol_reserves: u64,
|
||||
pub virtual_token_reserves: u64,
|
||||
pub real_sol_reserves: u64,
|
||||
pub real_token_reserves: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
|
||||
pub struct CompleteEvent {
|
||||
pub user: Pubkey,
|
||||
pub mint: Pubkey,
|
||||
pub bonding_curve: Pubkey,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
|
||||
pub struct SwapBaseInLog {
|
||||
pub log_type: u8,
|
||||
// input
|
||||
pub amount_in: u64,
|
||||
pub minimum_out: u64,
|
||||
pub direction: u64,
|
||||
// user info
|
||||
pub user_source: u64,
|
||||
// pool info
|
||||
pub pool_coin: u64,
|
||||
pub pool_pc: u64,
|
||||
// calc result
|
||||
pub out_amount: u64,
|
||||
}
|
||||
|
||||
pub trait EventTrait: Sized + std::fmt::Debug {
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, AppError>;
|
||||
}
|
||||
|
||||
impl EventTrait for TradeEvent {
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, AppError> {
|
||||
TradeEvent::try_from_slice(bytes).map_err(|e| AppError::from(anyhow!(e.to_string())))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventTrait for CompleteEvent {
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, AppError> {
|
||||
CompleteEvent::try_from_slice(bytes).map_err(|e| AppError::from(anyhow!(e.to_string())))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventTrait for SwapBaseInLog {
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, AppError> {
|
||||
SwapBaseInLog::try_from_slice(bytes).map_err(|e| AppError::from(anyhow!(e.to_string())))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PumpEvent {}
|
||||
|
||||
impl PumpEvent {
|
||||
pub fn parse_logs<T: EventTrait + Clone>(logs: &Vec<String>) -> Option<T> {
|
||||
let mut event: Option<T> = None;
|
||||
if !logs.is_empty() {
|
||||
let logs_iter = logs.iter().peekable();
|
||||
|
||||
for l in logs_iter.rev() {
|
||||
if let Some(log) = l.strip_prefix(PROGRAM_DATA) {
|
||||
let borsh_bytes = general_purpose::STANDARD.decode(log).unwrap();
|
||||
let slice: &[u8] = &borsh_bytes[8..];
|
||||
|
||||
if let Ok(e) = T::from_bytes(slice) {
|
||||
event = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
event
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RaydiumEvent {}
|
||||
|
||||
impl RaydiumEvent {
|
||||
pub fn parse_logs<T: EventTrait + Clone>(logs: &Vec<String>) -> Option<T> {
|
||||
let mut event: Option<T> = None;
|
||||
|
||||
if !logs.is_empty() {
|
||||
let logs_iter = logs.iter().peekable();
|
||||
|
||||
for l in logs_iter.rev() {
|
||||
let re = Regex::new(r"ray_log: (?P<base64>[A-Za-z0-9+/=]+)").unwrap();
|
||||
|
||||
if let Some(caps) = re.captures(l) {
|
||||
if let Some(base64) = caps.name("base64") {
|
||||
let bytes = general_purpose::STANDARD.decode(base64.as_str()).unwrap();
|
||||
|
||||
if let Ok(e) = T::from_bytes(&bytes) {
|
||||
event = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
}
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
|
||||
pub mod event;
|
||||
pub mod myerror;
|
||||
// pub mod subscribe_logs;
|
||||
// pub mod subscribe_tx;
|
||||
pub mod yellowstone_grpc;
|
||||
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
use anyhow::Error;
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct AppError(Error);
|
||||
|
||||
impl<E> From<E> for AppError
|
||||
where
|
||||
E: Into<Error>,
|
||||
{
|
||||
fn from(err: E) -> Self {
|
||||
Self(err.into())
|
||||
}
|
||||
}
|
||||
Executable
+136
@@ -0,0 +1,136 @@
|
||||
#[cfg(test)]
|
||||
mod subscribe_tx_tests {
|
||||
use crate::common::{
|
||||
event::{PumpEvent, RaydiumEvent, SwapBaseInLog, TradeEvent},
|
||||
myerror::AppError,
|
||||
yellowstone_grpc::{TransactionPretty, YellowstoneGrpc},
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use chrono::Local;
|
||||
use dotenvy::dotenv;
|
||||
use futures::{channel::mpsc, sink::SinkExt, stream::StreamExt};
|
||||
use log::{error, info};
|
||||
use solana_sdk::pubkey;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_transaction_status::{
|
||||
option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta,
|
||||
};
|
||||
use std::env;
|
||||
use tokio::test;
|
||||
use yellowstone_grpc_proto::geyser::{
|
||||
subscribe_update::UpdateOneof, SubscribeRequest, SubscribeRequestPing,
|
||||
};
|
||||
const AMM_V4: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
|
||||
const PUMP_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
||||
pub enum SwapType {
|
||||
Pump,
|
||||
Raydium,
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn test_subscribe_tx() -> Result<(), AppError> {
|
||||
dotenv().ok();
|
||||
pretty_env_logger::init_custom_env("RUST_LOG");
|
||||
let yellowstone_url = env::var("YELLOWSTONE_URL")?;
|
||||
|
||||
info!("::: {:?}", yellowstone_url);
|
||||
let yellowstone_grpc = YellowstoneGrpc::new(yellowstone_url);
|
||||
|
||||
let addrs = vec![
|
||||
"Aa4QWNkS3RLUv7DA9BM1a2Hzm4HDQo5PyRefqDJnpump".to_string(),
|
||||
"BnDssYyGDF9aj5j2N5BwsJFk9YMneQ8P7LQkoYkrpump".to_string(),
|
||||
];
|
||||
let transactions = yellowstone_grpc.subscribe_transaction(addrs, vec![], vec![]);
|
||||
|
||||
let (mut subscribe_tx, mut stream) = yellowstone_grpc.connect(transactions).await??;
|
||||
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(1000);
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(message) = stream.next().await {
|
||||
match message {
|
||||
Ok(msg) => {
|
||||
match msg.update_oneof {
|
||||
Some(UpdateOneof::Transaction(sut)) => {
|
||||
let transaction_pretty: TransactionPretty = sut.into();
|
||||
let _ = tx.try_send(transaction_pretty);
|
||||
}
|
||||
Some(UpdateOneof::Ping(_)) => {
|
||||
// This is necessary to keep load balancers that expect client pings alive. If your load balancer doesn't
|
||||
// require periodic client pings then this is unnecessary
|
||||
let _ = subscribe_tx
|
||||
.send(SubscribeRequest {
|
||||
ping: Some(SubscribeRequestPing { id: 1 }),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
info!("service is ping: {}", Local::now());
|
||||
}
|
||||
Some(UpdateOneof::Pong(_)) => {
|
||||
info!("service is pong: {}", Local::now());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(error) => {
|
||||
error!("error: {error:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while let Some(transaction_pretty) = rx.next().await {
|
||||
let trade_raw = transaction_pretty.tx.clone();
|
||||
let meta = &trade_raw.meta.clone().unwrap();
|
||||
if meta.err.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let logs = if let OptionSerializer::Some(logs) = &meta.log_messages {
|
||||
logs
|
||||
} else {
|
||||
&vec![]
|
||||
};
|
||||
|
||||
if let Ok(swap_type) = get_swap_type(&trade_raw) {
|
||||
match swap_type {
|
||||
SwapType::Raydium => {
|
||||
let event = RaydiumEvent::parse_logs::<SwapBaseInLog>(logs);
|
||||
info!("RaydiumEvent {:#?}", event);
|
||||
}
|
||||
SwapType::Pump => {
|
||||
let event = PumpEvent::parse_logs::<TradeEvent>(logs);
|
||||
info!("PumpEvent {:#?}", event);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_swap_type(
|
||||
trade_raw: &EncodedTransactionWithStatusMeta,
|
||||
) -> Result<SwapType, AppError> {
|
||||
let transaction = &trade_raw.transaction.decode();
|
||||
if let Some(transaction) = transaction {
|
||||
let account_keys = transaction.message.static_account_keys();
|
||||
|
||||
let program_index = account_keys
|
||||
.iter()
|
||||
.position(|item| item == &AMM_V4 || item == &PUMP_PROGRAM_ID)
|
||||
.ok_or(anyhow!("swap type program_id not found"))?;
|
||||
|
||||
let program_id = account_keys[program_index];
|
||||
let _type = match program_id {
|
||||
AMM_V4 => Ok(SwapType::Raydium),
|
||||
PUMP_PROGRAM_ID => Ok(SwapType::Pump),
|
||||
_ => Err(AppError::from(anyhow!("program_id ix not found"))),
|
||||
};
|
||||
return _type;
|
||||
}
|
||||
Err(AppError::from(anyhow!("program_id ix not found")))
|
||||
}
|
||||
}
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#[cfg(test)]
|
||||
mod subscribe_tx_tests {
|
||||
use crate::common::{
|
||||
myerror::AppError,
|
||||
yellowstone_grpc::{TransactionPretty, YellowstoneGrpc},
|
||||
};
|
||||
use chrono::Local;
|
||||
use dotenvy::dotenv;
|
||||
use futures::{channel::mpsc, sink::SinkExt, stream::StreamExt};
|
||||
use log::{error, info};
|
||||
use std::env;
|
||||
use tokio::test;
|
||||
use yellowstone_grpc_proto::geyser::{
|
||||
subscribe_update::UpdateOneof, SubscribeRequest, SubscribeRequestPing,
|
||||
};
|
||||
|
||||
#[test]
|
||||
async fn test_subscribe_tx() -> Result<(), AppError> {
|
||||
dotenv().ok();
|
||||
pretty_env_logger::init_custom_env("RUST_LOG");
|
||||
let yellowstone_url = env::var("YELLOWSTONE_URL")?;
|
||||
|
||||
info!("::: {:?}", yellowstone_url);
|
||||
let yellowstone_grpc = YellowstoneGrpc::new(yellowstone_url);
|
||||
|
||||
let addrs = vec!["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P".to_string()];
|
||||
let transactions = yellowstone_grpc.subscribe_transaction(addrs, vec![], vec![]);
|
||||
|
||||
let (mut subscribe_tx, mut stream) = yellowstone_grpc.connect(transactions).await??;
|
||||
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(1000);
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(message) = stream.next().await {
|
||||
match message {
|
||||
Ok(msg) => {
|
||||
match msg.update_oneof {
|
||||
Some(UpdateOneof::Transaction(sut)) => {
|
||||
let transaction_pretty: TransactionPretty = sut.into();
|
||||
let _ = tx.try_send(transaction_pretty);
|
||||
}
|
||||
Some(UpdateOneof::Ping(_)) => {
|
||||
// This is necessary to keep load balancers that expect client pings alive. If your load balancer doesn't
|
||||
// require periodic client pings then this is unnecessary
|
||||
let _ = subscribe_tx
|
||||
.send(SubscribeRequest {
|
||||
ping: Some(SubscribeRequestPing { id: 1 }),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
info!("service is ping: {}", Local::now());
|
||||
}
|
||||
Some(UpdateOneof::Pong(_)) => {
|
||||
info!("service is pong: {}", Local::now());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(error) => {
|
||||
error!("error: {error:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while let Some(event) = rx.next().await {
|
||||
info!("TransactionPretty {:#?}", event);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Executable
+284
@@ -0,0 +1,284 @@
|
||||
use std::{collections::HashMap, fmt, time::Duration};
|
||||
|
||||
use futures::{channel::mpsc, sink::Sink, Stream, StreamExt, SinkExt};
|
||||
use rustls::crypto::{ring::default_provider, CryptoProvider};
|
||||
use tonic::{transport::channel::ClientTlsConfig, Status};
|
||||
use yellowstone_grpc_client::{GeyserGrpcClient, GeyserGrpcClientResult};
|
||||
use yellowstone_grpc_proto::geyser::{
|
||||
CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions, SubscribeUpdate,
|
||||
SubscribeUpdateTransaction, subscribe_update::UpdateOneof, SubscribeRequestPing,
|
||||
};
|
||||
use log::{error, info};
|
||||
use chrono::Local;
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey, signature::Signature};
|
||||
use solana_transaction_status::{
|
||||
option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiTransactionEncoding,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
|
||||
use crate::grpc::event::{PumpEvent, RaydiumEvent, SwapBaseInLog, TradeEvent};
|
||||
use crate::grpc::myerror::AppError;
|
||||
|
||||
// 类型别名定义
|
||||
type TransactionsFilterMap = HashMap<String, SubscribeRequestFilterTransactions>;
|
||||
type GrpcStreamResult = GeyserGrpcClientResult<(
|
||||
Box<dyn Sink<SubscribeRequest, Error = mpsc::SendError> + Unpin + Send>,
|
||||
Box<dyn Stream<Item = Result<SubscribeUpdate, Status>> + Unpin + Send>,
|
||||
)>;
|
||||
|
||||
// 常量定义
|
||||
const AMM_V4: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
|
||||
const PUMP_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
||||
const CONNECT_TIMEOUT: u64 = 10;
|
||||
const REQUEST_TIMEOUT: u64 = 60;
|
||||
const CHANNEL_SIZE: usize = 1000;
|
||||
|
||||
// 枚举定义
|
||||
#[derive(Debug)]
|
||||
pub enum SwapType {
|
||||
Pump,
|
||||
Raydium,
|
||||
}
|
||||
|
||||
// 结构体定义
|
||||
#[allow(dead_code)]
|
||||
pub struct TransactionPretty {
|
||||
pub slot: u64,
|
||||
pub signature: Signature,
|
||||
pub is_vote: bool,
|
||||
pub tx: EncodedTransactionWithStatusMeta,
|
||||
}
|
||||
|
||||
impl fmt::Debug for TransactionPretty {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
struct TxWrap<'a>(&'a EncodedTransactionWithStatusMeta);
|
||||
impl<'a> fmt::Debug for TxWrap<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let serialized = serde_json::to_string(self.0).expect("failed to serialize");
|
||||
fmt::Display::fmt(&serialized, f)
|
||||
}
|
||||
}
|
||||
|
||||
f.debug_struct("TransactionPretty")
|
||||
.field("slot", &self.slot)
|
||||
.field("signature", &self.signature)
|
||||
.field("is_vote", &self.is_vote)
|
||||
.field("tx", &TxWrap(&self.tx))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubscribeUpdateTransaction> for TransactionPretty {
|
||||
fn from(SubscribeUpdateTransaction { transaction, slot }: SubscribeUpdateTransaction) -> Self {
|
||||
let tx = transaction.expect("should be defined");
|
||||
Self {
|
||||
slot,
|
||||
signature: Signature::try_from(tx.signature.as_slice()).expect("valid signature"),
|
||||
is_vote: tx.is_vote,
|
||||
tx: yellowstone_grpc_proto::convert_from::create_tx_with_meta(tx)
|
||||
.expect("valid tx with meta")
|
||||
.encode(UiTransactionEncoding::Base64, Some(u8::MAX), true)
|
||||
.expect("failed to encode"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct YellowstoneGrpc {
|
||||
endpoint: String,
|
||||
}
|
||||
|
||||
impl YellowstoneGrpc {
|
||||
pub fn new(endpoint: String) -> Self {
|
||||
Self { endpoint }
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
&self,
|
||||
transactions: TransactionsFilterMap,
|
||||
) -> Result<
|
||||
GeyserGrpcClientResult<(
|
||||
impl Sink<SubscribeRequest, Error = mpsc::SendError>,
|
||||
impl Stream<Item = Result<SubscribeUpdate, Status>>,
|
||||
)>,
|
||||
AppError,
|
||||
> {
|
||||
if CryptoProvider::get_default().is_none() {
|
||||
default_provider()
|
||||
.install_default()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e))?;
|
||||
}
|
||||
|
||||
let mut client = GeyserGrpcClient::build_from_shared(self.endpoint.clone())?
|
||||
.tls_config(ClientTlsConfig::new().with_native_roots())?
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.timeout(Duration::from_secs(60))
|
||||
.connect()
|
||||
.await?;
|
||||
|
||||
let subscribe_request = SubscribeRequest {
|
||||
transactions,
|
||||
commitment: Some(CommitmentLevel::Processed.into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(client.subscribe_with_request(Some(subscribe_request)).await)
|
||||
}
|
||||
|
||||
pub async fn subscribe_accounts(&self, accounts: Vec<String>) -> Result<(), AppError> {
|
||||
let transactions = self.get_subscribe_request_filter(accounts, vec![], vec![]);
|
||||
let (mut subscribe_tx, mut stream) = self.connect(transactions).await??;
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(CHANNEL_SIZE);
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(message) = stream.next().await {
|
||||
match message {
|
||||
Ok(msg) => {
|
||||
if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await {
|
||||
error!("Error handling message: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Stream error: {error:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while let Some(transaction_pretty) = rx.next().await {
|
||||
if let Err(e) = Self::process_transaction(transaction_pretty).await {
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe_pumpfun(&self) -> Result<(), AppError> {
|
||||
let addrs = vec![PUMP_PROGRAM_ID.to_string()];
|
||||
let transactions = self.get_subscribe_request_filter(addrs, vec![], vec![]);
|
||||
let (mut subscribe_tx, mut stream) = self.connect(transactions).await??;
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(CHANNEL_SIZE);
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(message) = stream.next().await {
|
||||
match message {
|
||||
Ok(msg) => {
|
||||
if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await {
|
||||
error!("Error handling message: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Stream error: {error:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while let Some(transaction_pretty) = rx.next().await {
|
||||
if let Err(e) = Self::process_transaction(transaction_pretty).await {
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_subscribe_request_filter(
|
||||
&self,
|
||||
account_include: Vec<String>,
|
||||
account_exclude: Vec<String>,
|
||||
account_required: Vec<String>,
|
||||
) -> TransactionsFilterMap {
|
||||
let mut transactions = HashMap::new();
|
||||
transactions.insert(
|
||||
"client".to_string(),
|
||||
SubscribeRequestFilterTransactions {
|
||||
vote: Some(false),
|
||||
failed: Some(false),
|
||||
signature: None,
|
||||
account_include,
|
||||
account_exclude,
|
||||
account_required,
|
||||
},
|
||||
);
|
||||
transactions
|
||||
}
|
||||
|
||||
|
||||
async fn handle_stream_message(
|
||||
msg: SubscribeUpdate,
|
||||
tx: &mut mpsc::Sender<TransactionPretty>,
|
||||
subscribe_tx: &mut (impl Sink<SubscribeRequest, Error = mpsc::SendError> + Unpin),
|
||||
) -> Result<(), AppError> {
|
||||
match msg.update_oneof {
|
||||
Some(UpdateOneof::Transaction(sut)) => {
|
||||
let transaction_pretty = TransactionPretty::from(sut);
|
||||
tx.try_send(transaction_pretty).map_err(|e| AppError::from(anyhow!("Send error: {:?}", e)))?;
|
||||
}
|
||||
Some(UpdateOneof::Ping(_)) => {
|
||||
subscribe_tx
|
||||
.send(SubscribeRequest {
|
||||
ping: Some(SubscribeRequestPing { id: 1 }),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.map_err(|e| AppError::from(anyhow!("Ping error: {:?}", e)))?;
|
||||
info!("service is ping: {}", Local::now());
|
||||
}
|
||||
Some(UpdateOneof::Pong(_)) => {
|
||||
info!("service is pong: {}", Local::now());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_transaction(transaction_pretty: TransactionPretty) -> Result<(), AppError> {
|
||||
let trade_raw = transaction_pretty.tx;
|
||||
let meta = trade_raw.meta.as_ref()
|
||||
.ok_or_else(|| AppError::from(anyhow!("Missing transaction metadata")))?;
|
||||
|
||||
if meta.err.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let logs = if let OptionSerializer::Some(logs) = &meta.log_messages {
|
||||
logs
|
||||
} else {
|
||||
&vec![]
|
||||
};
|
||||
|
||||
if let Ok(swap_type) = Self::get_swap_type(&trade_raw) {
|
||||
match swap_type {
|
||||
SwapType::Raydium => {
|
||||
let event = RaydiumEvent::parse_logs::<SwapBaseInLog>(logs);
|
||||
info!("RaydiumEvent {:#?}", event);
|
||||
}
|
||||
SwapType::Pump => {
|
||||
let event = PumpEvent::parse_logs::<TradeEvent>(logs);
|
||||
info!("PumpEvent {:#?}", event);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_swap_type(trade_raw: &EncodedTransactionWithStatusMeta) -> Result<SwapType, AppError> {
|
||||
let transaction = trade_raw.transaction.decode()
|
||||
.ok_or_else(|| AppError::from(anyhow!("Failed to decode transaction")))?;
|
||||
|
||||
let account_keys = transaction.message.static_account_keys();
|
||||
let program_index = account_keys
|
||||
.iter()
|
||||
.position(|item| item == &AMM_V4 || item == &PUMP_PROGRAM_ID)
|
||||
.ok_or_else(|| AppError::from(anyhow!("swap type program_id not found")))?;
|
||||
|
||||
match account_keys[program_index] {
|
||||
AMM_V4 => Ok(SwapType::Raydium),
|
||||
PUMP_PROGRAM_ID => Ok(SwapType::Pump),
|
||||
_ => Err(AppError::from(anyhow!("Invalid program_id")))
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
use anyhow::anyhow;
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine;
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use regex::Regex;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
use super::myerror::AppError;
|
||||
|
||||
pub const PROGRAM_DATA: &str = "Program data: ";
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
|
||||
pub struct TradeEvent {
|
||||
pub mint: Pubkey,
|
||||
pub sol_amount: u64,
|
||||
pub token_amount: u64,
|
||||
pub is_buy: bool,
|
||||
pub user: Pubkey,
|
||||
pub timestamp: u64,
|
||||
pub virtual_sol_reserves: u64,
|
||||
pub virtual_token_reserves: u64,
|
||||
pub real_sol_reserves: u64,
|
||||
pub real_token_reserves: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
|
||||
pub struct CompleteEvent {
|
||||
pub user: Pubkey,
|
||||
pub mint: Pubkey,
|
||||
pub bonding_curve: Pubkey,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
|
||||
pub struct SwapBaseInLog {
|
||||
pub log_type: u8,
|
||||
// input
|
||||
pub amount_in: u64,
|
||||
pub minimum_out: u64,
|
||||
pub direction: u64,
|
||||
// user info
|
||||
pub user_source: u64,
|
||||
// pool info
|
||||
pub pool_coin: u64,
|
||||
pub pool_pc: u64,
|
||||
// calc result
|
||||
pub out_amount: u64,
|
||||
}
|
||||
|
||||
pub trait EventTrait: Sized + std::fmt::Debug {
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, AppError>;
|
||||
}
|
||||
|
||||
impl EventTrait for TradeEvent {
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, AppError> {
|
||||
TradeEvent::try_from_slice(bytes).map_err(|e| AppError::from(anyhow!(e.to_string())))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventTrait for CompleteEvent {
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, AppError> {
|
||||
CompleteEvent::try_from_slice(bytes).map_err(|e| AppError::from(anyhow!(e.to_string())))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventTrait for SwapBaseInLog {
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, AppError> {
|
||||
SwapBaseInLog::try_from_slice(bytes).map_err(|e| AppError::from(anyhow!(e.to_string())))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PumpEvent {}
|
||||
|
||||
impl PumpEvent {
|
||||
pub fn parse_logs<T: EventTrait + Clone>(logs: &Vec<String>) -> Option<T> {
|
||||
let mut event: Option<T> = None;
|
||||
if !logs.is_empty() {
|
||||
let logs_iter = logs.iter().peekable();
|
||||
|
||||
for l in logs_iter.rev() {
|
||||
if let Some(log) = l.strip_prefix(PROGRAM_DATA) {
|
||||
let borsh_bytes = general_purpose::STANDARD.decode(log).unwrap();
|
||||
let slice: &[u8] = &borsh_bytes[8..];
|
||||
|
||||
if let Ok(e) = T::from_bytes(slice) {
|
||||
event = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
event
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RaydiumEvent {}
|
||||
|
||||
impl RaydiumEvent {
|
||||
pub fn parse_logs<T: EventTrait + Clone>(logs: &Vec<String>) -> Option<T> {
|
||||
let mut event: Option<T> = None;
|
||||
|
||||
if !logs.is_empty() {
|
||||
let logs_iter = logs.iter().peekable();
|
||||
|
||||
for l in logs_iter.rev() {
|
||||
let re = Regex::new(r"ray_log: (?P<base64>[A-Za-z0-9+/=]+)").unwrap();
|
||||
|
||||
if let Some(caps) = re.captures(l) {
|
||||
if let Some(base64) = caps.name("base64") {
|
||||
let bytes = general_purpose::STANDARD.decode(base64.as_str()).unwrap();
|
||||
|
||||
if let Ok(e) = T::from_bytes(&bytes) {
|
||||
event = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
}
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DexInstruction {
|
||||
CreateToken(CreateTokenInfo),
|
||||
UserTrade(TradeInfo),
|
||||
BotTrade(TradeInfo),
|
||||
Other,
|
||||
}
|
||||
|
||||
// 添加新的数据结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct CreateTokenInfo {
|
||||
pub signature: String,
|
||||
pub name: String,
|
||||
pub symbol: String,
|
||||
pub uri: String,
|
||||
pub mint: String,
|
||||
pub bonding_curve: String,
|
||||
pub user: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct TradeInfo {
|
||||
pub signature: String,
|
||||
pub mint: String,
|
||||
pub bonding_curve: String,
|
||||
pub sol_amount: u64,
|
||||
pub token_amount: u64,
|
||||
pub is_buy: bool,
|
||||
pub user: String,
|
||||
pub timestamp: i64,
|
||||
pub virtual_sol_reserves: u64,
|
||||
pub virtual_token_reserves: u64,
|
||||
pub real_sol_reserves: u64,
|
||||
pub real_token_reserves: u64,
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::instruction::logs_data::{CreateTokenInfo, TradeInfo};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum DexEvent {
|
||||
NewToken(CreateTokenInfo),
|
||||
NewUserTrade(TradeInfo),
|
||||
NewBotTrade(TradeInfo),
|
||||
Error(String),
|
||||
}
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
use crate::instruction::logs_data::DexInstruction;
|
||||
use crate::instruction::logs_parser::{parse_create_token_data, parse_trade_data};
|
||||
use crate::error::ClientResult;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
pub struct LogFilter;
|
||||
|
||||
impl LogFilter {
|
||||
const PROGRAM_ID: &'static str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
|
||||
|
||||
/// Parse transaction logs and return instruction type and data
|
||||
pub fn parse_instruction(logs: &[String], bot_wallet: Option<Pubkey>) -> ClientResult<Vec<DexInstruction>> {
|
||||
let mut current_instruction = None;
|
||||
let mut program_data = String::new();
|
||||
let mut invoke_depth = 0;
|
||||
let mut last_data_len = 0;
|
||||
let mut instructions = Vec::new();
|
||||
for log in logs {
|
||||
// Check program invocation
|
||||
if log.contains(&format!("Program {} invoke", Self::PROGRAM_ID)) {
|
||||
invoke_depth += 1;
|
||||
if invoke_depth == 1 { // Only reset state at top level call
|
||||
current_instruction = None;
|
||||
program_data.clear();
|
||||
last_data_len = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if not in our program
|
||||
if invoke_depth == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Identify instruction type (only at top level)
|
||||
if invoke_depth == 1 && log.contains("Program log: Instruction:") {
|
||||
if log.contains("Create") {
|
||||
current_instruction = Some("create");
|
||||
} else if log.contains("Buy") || log.contains("Sell") {
|
||||
current_instruction = Some("trade");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect Program data
|
||||
if log.starts_with("Program data: ") {
|
||||
let data = log.trim_start_matches("Program data: ");
|
||||
if data.len() > last_data_len {
|
||||
program_data = data.to_string();
|
||||
last_data_len = data.len();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if program ends
|
||||
if log.contains(&format!("Program {} success", Self::PROGRAM_ID)) {
|
||||
invoke_depth -= 1;
|
||||
if invoke_depth == 0 { // Only process data when top level program ends
|
||||
if let Some(instruction_type) = current_instruction {
|
||||
if !program_data.is_empty() {
|
||||
match instruction_type {
|
||||
"create" => {
|
||||
if let Ok(token_info) = parse_create_token_data(&program_data) {
|
||||
instructions.push(DexInstruction::CreateToken(token_info));
|
||||
}
|
||||
},
|
||||
"trade" => {
|
||||
if let Ok(trade_info) = parse_trade_data(&program_data) {
|
||||
if let Some(bot_wallet_pubkey) = bot_wallet {
|
||||
if trade_info.user == bot_wallet_pubkey.to_string() {
|
||||
instructions.push(DexInstruction::BotTrade(trade_info));
|
||||
} else {
|
||||
instructions.push(DexInstruction::UserTrade(trade_info));
|
||||
}
|
||||
} else {
|
||||
instructions.push(DexInstruction::UserTrade(trade_info));
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
Executable
+173
@@ -0,0 +1,173 @@
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
|
||||
use crate::error::{ClientError, ClientResult};
|
||||
use crate::instruction::{
|
||||
logs_data::{DexInstruction, CreateTokenInfo, TradeInfo},
|
||||
logs_filters::LogFilter
|
||||
};
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub async fn process_logs<F>(
|
||||
signature: &str,
|
||||
logs: Vec<String>,
|
||||
callback: F,
|
||||
payer: Option<Pubkey>,
|
||||
) -> ClientResult<()>
|
||||
where
|
||||
F: Fn(&str, DexInstruction) + Send + Sync,
|
||||
{
|
||||
let instructions = LogFilter::parse_instruction(&logs, payer)?;
|
||||
for instruction in instructions {
|
||||
callback(signature, instruction);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Add parsing function
|
||||
pub fn parse_create_token_data(data: &str) -> ClientResult<CreateTokenInfo> {
|
||||
// First do base64 decoding
|
||||
let decoded = BASE64.decode(data)
|
||||
.map_err(|e| ClientError::Other(format!("Failed to decode base64: {}", e)))?;
|
||||
|
||||
// Skip prefix bytes (if any)
|
||||
let mut cursor = if decoded.len() > 8 { 8 } else { 0 };
|
||||
|
||||
// Read name length and name
|
||||
if cursor + 4 > decoded.len() {
|
||||
return Err(ClientError::Other("Data too short for name length".to_string()));
|
||||
}
|
||||
let name_len = read_u32(&decoded[cursor..]) as usize;
|
||||
cursor += 4;
|
||||
|
||||
if cursor + name_len > decoded.len() {
|
||||
return Err(ClientError::Other(format!("Data too short for name: need {} bytes", name_len)));
|
||||
}
|
||||
let name = String::from_utf8(decoded[cursor..cursor + name_len].to_vec())
|
||||
.map_err(|e| ClientError::Other(format!("Invalid UTF-8 in name: {}", e)))?;
|
||||
cursor += name_len;
|
||||
|
||||
// Read symbol length and symbol
|
||||
if cursor + 4 > decoded.len() {
|
||||
return Err(ClientError::Other("Data too short for symbol length".to_string()));
|
||||
}
|
||||
let symbol_len = read_u32(&decoded[cursor..]) as usize;
|
||||
cursor += 4;
|
||||
|
||||
if cursor + symbol_len > decoded.len() {
|
||||
return Err(ClientError::Other(format!("Data too short for symbol: need {} bytes", symbol_len)));
|
||||
}
|
||||
let symbol = String::from_utf8(decoded[cursor..cursor + symbol_len].to_vec())
|
||||
.map_err(|e| ClientError::Other(format!("Invalid UTF-8 in symbol: {}", e)))?;
|
||||
cursor += symbol_len;
|
||||
|
||||
// Read URI length and URI
|
||||
if cursor + 4 > decoded.len() {
|
||||
return Err(ClientError::Other("Data too short for URI length".to_string()));
|
||||
}
|
||||
let uri_len = read_u32(&decoded[cursor..]) as usize;
|
||||
cursor += 4;
|
||||
|
||||
if cursor + uri_len > decoded.len() {
|
||||
return Err(ClientError::Other(format!("Data too short for URI: need {} bytes", uri_len)));
|
||||
}
|
||||
let uri = String::from_utf8(decoded[cursor..cursor + uri_len].to_vec())
|
||||
.map_err(|e| ClientError::Other(format!("Invalid UTF-8 in uri: {}", e)))?;
|
||||
cursor += uri_len;
|
||||
|
||||
// Make sure there is enough data to read public keys
|
||||
if cursor + 32 * 3 > decoded.len() {
|
||||
return Err(ClientError::Other("Data too short for public keys".to_string()));
|
||||
}
|
||||
|
||||
// Parse Mint Public Key
|
||||
let mint = bs58::encode(&decoded[cursor..cursor+32]).into_string();
|
||||
cursor += 32;
|
||||
|
||||
// Parse Bonding Curve Public Key
|
||||
let bonding_curve = bs58::encode(&decoded[cursor..cursor+32]).into_string();
|
||||
cursor += 32;
|
||||
|
||||
// Parse User Public Key
|
||||
let user = bs58::encode(&decoded[cursor..cursor+32]).into_string();
|
||||
|
||||
Ok(CreateTokenInfo {
|
||||
signature: String::new(),
|
||||
name,
|
||||
symbol,
|
||||
uri,
|
||||
mint,
|
||||
bonding_curve,
|
||||
user,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_u32(data: &[u8]) -> u32 {
|
||||
let mut bytes = [0u8; 4];
|
||||
bytes.copy_from_slice(&data[..4]);
|
||||
u32::from_le_bytes(bytes)
|
||||
}
|
||||
|
||||
pub fn parse_trade_data(data: &str) -> ClientResult<TradeInfo> {
|
||||
let engine = base64::engine::general_purpose::STANDARD;
|
||||
let decoded = engine.decode(data).map_err(|e|
|
||||
ClientError::Parse(
|
||||
"Failed to decode base64".to_string(),
|
||||
e.to_string()
|
||||
)
|
||||
)?;
|
||||
|
||||
let mut cursor = 8; // Skip prefix
|
||||
|
||||
// 1. Mint (32 bytes)
|
||||
let mint = bs58::encode(&decoded[cursor..cursor + 32]).into_string();
|
||||
cursor += 32;
|
||||
|
||||
// 2. Sol Amount (8 bytes)
|
||||
let sol_amount = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
cursor += 8;
|
||||
|
||||
// 3. Token Amount (8 bytes)
|
||||
let token_amount = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
cursor += 8;
|
||||
|
||||
// 4. Is Buy (1 byte)
|
||||
let is_buy = decoded[cursor] != 0;
|
||||
cursor += 1;
|
||||
|
||||
// 5. User (32 bytes)
|
||||
let user = bs58::encode(&decoded[cursor..cursor + 32]).into_string();
|
||||
cursor += 32;
|
||||
|
||||
// 6. Timestamp (8 bytes)
|
||||
let timestamp = i64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
cursor += 8;
|
||||
|
||||
// 7. Virtual Sol Reserves (8 bytes)
|
||||
let virtual_sol_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
cursor += 8;
|
||||
|
||||
// 8. Virtual Token Reserves (8 bytes)
|
||||
let virtual_token_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
cursor += 8;
|
||||
|
||||
let real_sol_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
cursor += 8;
|
||||
|
||||
let real_token_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
|
||||
Ok(TradeInfo {
|
||||
signature: String::new(),
|
||||
mint,
|
||||
bonding_curve: String::new(),
|
||||
sol_amount,
|
||||
token_amount,
|
||||
is_buy,
|
||||
user,
|
||||
timestamp,
|
||||
virtual_sol_reserves,
|
||||
virtual_token_reserves,
|
||||
real_sol_reserves,
|
||||
real_token_reserves,
|
||||
})
|
||||
}
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
use solana_client::{
|
||||
nonblocking::pubsub_client::PubsubClient,
|
||||
rpc_config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter}
|
||||
};
|
||||
|
||||
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
use futures::StreamExt;
|
||||
use crate::{constants, instruction::{
|
||||
logs_data::DexInstruction, logs_events::DexEvent, logs_filters::LogFilter
|
||||
}};
|
||||
|
||||
/// Subscription handle containing task and unsubscribe logic
|
||||
pub struct SubscriptionHandle {
|
||||
pub task: JoinHandle<()>,
|
||||
pub unsub_fn: Box<dyn Fn() + Send>,
|
||||
}
|
||||
|
||||
impl SubscriptionHandle {
|
||||
pub async fn shutdown(self) {
|
||||
(self.unsub_fn)();
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_pubsub_client(ws_url: &str) -> PubsubClient {
|
||||
PubsubClient::new(ws_url).await.unwrap()
|
||||
}
|
||||
|
||||
/// 启动订阅
|
||||
pub async fn tokens_subscription<F>(
|
||||
ws_url: &str,
|
||||
commitment: CommitmentConfig,
|
||||
callback: F,
|
||||
bot_wallet: Option<Pubkey>,
|
||||
) -> Result<SubscriptionHandle, Box<dyn std::error::Error>>
|
||||
where
|
||||
F: Fn(DexEvent) + Send + Sync + 'static,
|
||||
{
|
||||
let program_address = constants::accounts::PUMPFUN.to_string();
|
||||
let logs_filter = RpcTransactionLogsFilter::Mentions(vec![program_address]);
|
||||
|
||||
let logs_config = RpcTransactionLogsConfig {
|
||||
commitment: Some(commitment),
|
||||
};
|
||||
|
||||
// Create PubsubClient
|
||||
let sub_client = Arc::new(PubsubClient::new(ws_url).await.unwrap());
|
||||
|
||||
let sub_client_clone = Arc::clone(&sub_client);
|
||||
|
||||
// Create channel for unsubscribe
|
||||
let (unsub_tx, _) = mpsc::channel(1);
|
||||
|
||||
// Start subscription task
|
||||
let task = tokio::spawn(async move {
|
||||
let (mut stream, _) = sub_client_clone.logs_subscribe(logs_filter, logs_config).await.unwrap();
|
||||
|
||||
loop {
|
||||
let msg = stream.next().await;
|
||||
match msg {
|
||||
Some(msg) => {
|
||||
if let Some(_err) = msg.value.err {
|
||||
continue;
|
||||
}
|
||||
|
||||
let instructions = LogFilter::parse_instruction(&msg.value.logs, bot_wallet).unwrap();
|
||||
for instruction in instructions {
|
||||
match instruction {
|
||||
DexInstruction::CreateToken(token_info) => {
|
||||
callback(DexEvent::NewToken(token_info));
|
||||
}
|
||||
DexInstruction::UserTrade(trade_info) => {
|
||||
callback(DexEvent::NewUserTrade(trade_info));
|
||||
}
|
||||
DexInstruction::BotTrade(trade_info) => {
|
||||
callback(DexEvent::NewBotTrade(trade_info));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("Token subscription stream ended");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return subscription handle and unsubscribe logic
|
||||
Ok(SubscriptionHandle {
|
||||
task,
|
||||
unsub_fn: Box::new(move || {
|
||||
let _ = unsub_tx.try_send(());
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn stop_subscription(handle: SubscriptionHandle) {
|
||||
handle.shutdown().await;
|
||||
}
|
||||
Executable
+207
@@ -0,0 +1,207 @@
|
||||
//! 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.
|
||||
|
||||
pub mod logs_data;
|
||||
pub mod logs_parser;
|
||||
pub mod logs_filters;
|
||||
pub mod logs_events;
|
||||
pub mod logs_subscribe;
|
||||
|
||||
pub use logs_data::*;
|
||||
pub use logs_parser::*;
|
||||
pub use logs_filters::*;
|
||||
pub use logs_events::*;
|
||||
pub use logs_subscribe::*;
|
||||
|
||||
use crate::{constants, PumpFun};
|
||||
use spl_associated_token_account::get_associated_token_address;
|
||||
|
||||
use solana_sdk::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
signature::Keypair,
|
||||
signer::Signer,
|
||||
};
|
||||
|
||||
pub struct Create {
|
||||
pub _name: String,
|
||||
pub _symbol: String,
|
||||
pub _uri: String,
|
||||
}
|
||||
|
||||
impl Create {
|
||||
pub fn data(&self) -> Vec<u8> {
|
||||
let mut data = Vec::with_capacity(8 + 8 + 8);
|
||||
data.extend_from_slice(&[24, 30, 200, 40, 5, 28, 7, 119]); // discriminator
|
||||
data.extend_from_slice(&self._name.as_bytes());
|
||||
data.extend_from_slice(&self._symbol.as_bytes());
|
||||
data.extend_from_slice(&self._uri.as_bytes());
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Buy {
|
||||
pub _amount: u64,
|
||||
pub _max_sol_cost: u64,
|
||||
}
|
||||
|
||||
impl Buy {
|
||||
pub fn data(&self) -> Vec<u8> {
|
||||
let mut data = Vec::with_capacity(8 + 8 + 8);
|
||||
data.extend_from_slice(&[102, 6, 61, 18, 1, 218, 235, 234]); // discriminator
|
||||
data.extend_from_slice(&self._amount.to_le_bytes());
|
||||
data.extend_from_slice(&self._max_sol_cost.to_le_bytes());
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Sell {
|
||||
pub _amount: u64,
|
||||
pub _min_sol_output: u64,
|
||||
}
|
||||
|
||||
impl Sell {
|
||||
pub fn data(&self) -> Vec<u8> {
|
||||
let mut data = Vec::with_capacity(8 + 8 + 8);
|
||||
data.extend_from_slice(&[51, 230, 133, 164, 1, 127, 131, 173]); // discriminator
|
||||
data.extend_from_slice(&self._amount.to_le_bytes());
|
||||
data.extend_from_slice(&self._min_sol_output.to_le_bytes());
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 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: 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: 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: 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),
|
||||
],
|
||||
)
|
||||
}
|
||||
Executable
+235
@@ -0,0 +1,235 @@
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use rand::seq::SliceRandom;
|
||||
use reqwest::Client;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use solana_sdk::{
|
||||
pubkey::Pubkey,
|
||||
transaction::Transaction,
|
||||
};
|
||||
|
||||
use crate::error::{ClientError, ClientResult};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct JitoClient {
|
||||
base_url: String,
|
||||
uuid: Option<String>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PrettyJsonValue(pub Value);
|
||||
|
||||
impl fmt::Display for PrettyJsonValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", serde_json::to_string_pretty(&self.0).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Value> for PrettyJsonValue {
|
||||
fn from(value: Value) -> Self {
|
||||
PrettyJsonValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl JitoClient {
|
||||
pub fn new(base_url: &str, uuid: Option<String>) -> Self {
|
||||
Self {
|
||||
base_url: base_url.to_string(),
|
||||
uuid,
|
||||
client: Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_request(&self, endpoint: &str, method: &str, params: Option<Value>) -> ClientResult<Value> {
|
||||
let url = format!("{}{}", self.base_url, endpoint);
|
||||
|
||||
let data = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": method,
|
||||
"params": params.unwrap_or(json!([]))
|
||||
});
|
||||
|
||||
// println!("Sending request to: {}", url);
|
||||
// println!("Request body: {}", serde_json::to_string_pretty(&data).unwrap());
|
||||
|
||||
let response = self.client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&data)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ClientError::Other(format!("Request failed: {}", e)))?;
|
||||
|
||||
let status = response.status();
|
||||
// println!("Response status: {}", status);
|
||||
|
||||
let body = response.json::<Value>().await
|
||||
.map_err(|e| ClientError::Other(format!("Failed to parse response: {}", e)))?;
|
||||
// println!("Response body: {}", serde_json::to_string_pretty(&body).unwrap());
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
pub async fn get_tip_accounts(&self) -> ClientResult<Value> {
|
||||
let endpoint = if let Some(uuid) = &self.uuid {
|
||||
format!("/bundles?uuid={}", uuid)
|
||||
} else {
|
||||
"/bundles".to_string()
|
||||
};
|
||||
|
||||
self.send_request(&endpoint, "getTipAccounts", None).await
|
||||
}
|
||||
|
||||
// Get a random tip account
|
||||
pub async fn get_tip_account(&self) -> ClientResult<Pubkey> {
|
||||
let tip_accounts_response = self.get_tip_accounts().await?;
|
||||
|
||||
let tip_accounts = tip_accounts_response["result"]
|
||||
.as_array()
|
||||
.ok_or_else(|| ClientError::Other("Failed to parse tip accounts as array".to_string()))?;
|
||||
|
||||
if tip_accounts.is_empty() {
|
||||
return Err(ClientError::Other("No tip accounts available".to_string()));
|
||||
}
|
||||
|
||||
let random_account = tip_accounts
|
||||
.choose(&mut rand::thread_rng())
|
||||
.ok_or_else(|| ClientError::Other("Failed to choose random tip account".to_string()))?;
|
||||
|
||||
let address = random_account
|
||||
.as_str()
|
||||
.ok_or_else(|| ClientError::Other("Failed to parse tip account as string".to_string()))?;
|
||||
|
||||
Pubkey::from_str(address)
|
||||
.map_err(|e| ClientError::Other(format!("Failed to parse pubkey: {}", e)))
|
||||
}
|
||||
|
||||
pub async fn get_bundle_statuses(&self, bundle_uuids: Vec<String>) -> ClientResult<Value> {
|
||||
let endpoint = if let Some(uuid) = &self.uuid {
|
||||
format!("/bundles?uuid={}", uuid)
|
||||
} else {
|
||||
"/bundles".to_string()
|
||||
};
|
||||
|
||||
// Construct the params as a list within a list
|
||||
let params = json!([bundle_uuids]);
|
||||
|
||||
self.send_request(&endpoint, "getBundleStatuses", Some(params))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_transaction(
|
||||
&self,
|
||||
transaction: &Transaction,
|
||||
) -> ClientResult<String> {
|
||||
let wire_transaction = bincode::serialize(transaction).map_err(|e| {
|
||||
ClientError::Parse(
|
||||
"Transaction serialization failed".to_string(),
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let serialized_tx = bs58::encode(&wire_transaction).into_string();
|
||||
|
||||
// Prepare bundle for submission (array of transactions)
|
||||
let bundle = json!([serialized_tx]);
|
||||
|
||||
// UUID for the bundle
|
||||
let uuid = None;
|
||||
|
||||
// Send bundle using Jito SDK
|
||||
// println!("Sending bundle with 1 transaction...");
|
||||
let response = self.send_bundle(Some(bundle), uuid).await?;
|
||||
|
||||
response["result"]
|
||||
.as_str()
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| ClientError::Parse(
|
||||
"Invalid response format".to_string(),
|
||||
"Missing result field".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn send_bundle(&self, params: Option<Value>, uuid: Option<&str>) -> ClientResult<Value> {
|
||||
let mut endpoint = "/bundles".to_string();
|
||||
|
||||
if let Some(uuid) = uuid {
|
||||
endpoint = format!("{}?uuid={}", endpoint, uuid);
|
||||
}
|
||||
|
||||
// Ensure params is an array of transactions
|
||||
let transactions = match params {
|
||||
Some(Value::Array(transactions)) => {
|
||||
if transactions.is_empty() {
|
||||
return Err(ClientError::Other("Bundle must contain at least one transaction".to_string()));
|
||||
}
|
||||
if transactions.len() > 5 {
|
||||
return Err(ClientError::Other("Bundle can contain at most 5 transactions".to_string()));
|
||||
}
|
||||
transactions
|
||||
},
|
||||
_ => return Err(ClientError::Other("Invalid bundle format: expected an array of transactions".to_string())),
|
||||
};
|
||||
|
||||
// Wrap the transactions array in another array
|
||||
let params = json!([transactions]);
|
||||
|
||||
// Send the wrapped transactions array
|
||||
self.send_request(&endpoint, "sendBundle", Some(params))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_txn(&self, params: Option<Value>, bundle_only: bool) -> ClientResult<Value> {
|
||||
let mut query_params = Vec::new();
|
||||
|
||||
if bundle_only {
|
||||
query_params.push("bundleOnly=true".to_string());
|
||||
}
|
||||
|
||||
let endpoint = if query_params.is_empty() {
|
||||
"/transactions".to_string()
|
||||
} else {
|
||||
format!("/transactions?{}", query_params.join("&"))
|
||||
};
|
||||
|
||||
// Construct params as an array instead of an object
|
||||
let params = match params {
|
||||
Some(Value::Object(map)) => {
|
||||
let tx = map.get("tx").and_then(Value::as_str).unwrap_or_default();
|
||||
let skip_preflight = map.get("skipPreflight").and_then(Value::as_bool).unwrap_or(false);
|
||||
json!([
|
||||
tx,
|
||||
{
|
||||
"encoding": "base64",
|
||||
"skipPreflight": skip_preflight
|
||||
}
|
||||
])
|
||||
},
|
||||
_ => json!([]),
|
||||
};
|
||||
|
||||
self.send_request(&endpoint, "sendTransaction", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn get_in_flight_bundle_statuses(&self, bundle_uuids: Vec<String>) -> ClientResult<Value> {
|
||||
let endpoint = if let Some(uuid) = &self.uuid {
|
||||
format!("/bundles?uuid={}", uuid)
|
||||
} else {
|
||||
"/bundles".to_string()
|
||||
};
|
||||
|
||||
// Construct the params as a list within a list
|
||||
let params = json!([bundle_uuids]);
|
||||
|
||||
self.send_request(&endpoint, "getInflightBundleStatuses", Some(params))
|
||||
.await
|
||||
}
|
||||
|
||||
// Helper method to convert Value to PrettyJsonValue
|
||||
pub fn prettify(value: Value) -> PrettyJsonValue {
|
||||
PrettyJsonValue(value)
|
||||
}
|
||||
}
|
||||
Executable
+642
@@ -0,0 +1,642 @@
|
||||
pub mod accounts;
|
||||
pub mod constants;
|
||||
pub mod error;
|
||||
pub mod instruction;
|
||||
pub mod utils;
|
||||
pub mod jito;
|
||||
pub mod grpc;
|
||||
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_sdk::{
|
||||
commitment_config::CommitmentConfig,
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, Signature},
|
||||
signer::Signer,
|
||||
instruction::Instruction,
|
||||
system_instruction,
|
||||
compute_budget::ComputeBudgetInstruction,
|
||||
transaction::Transaction,
|
||||
};
|
||||
use spl_associated_token_account::{
|
||||
get_associated_token_address,
|
||||
create_associated_token_account,
|
||||
};
|
||||
|
||||
use instruction::logs_subscribe;
|
||||
use instruction::logs_subscribe::SubscriptionHandle;
|
||||
use instruction::logs_events::DexEvent;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::jito::JitoClient;
|
||||
use crate::error::ClientError;
|
||||
|
||||
use borsh::BorshDeserialize;
|
||||
|
||||
// Constants
|
||||
const DEFAULT_SLIPPAGE: u64 = 1000; // 10%
|
||||
const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 10_000_000;
|
||||
const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500_000;
|
||||
const JITO_TIP_AMOUNT: u64 = 1_000; // 0.000001 SOL
|
||||
|
||||
/// Priority fee configuration
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PriorityFee {
|
||||
pub limit: Option<u32>,
|
||||
pub price: Option<u64>,
|
||||
}
|
||||
|
||||
impl Default for PriorityFee {
|
||||
fn default() -> Self {
|
||||
Self { limit: Some(DEFAULT_COMPUTE_UNIT_LIMIT), price: Some(DEFAULT_COMPUTE_UNIT_PRICE) }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PumpFun {
|
||||
pub rpc: RpcClient,
|
||||
pub payer: Arc<Keypair>,
|
||||
pub jito_client: Option<JitoClient>,
|
||||
}
|
||||
|
||||
impl Clone for PumpFun {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
rpc: RpcClient::new_with_commitment(
|
||||
self.rpc.url().to_string(),
|
||||
self.rpc.commitment()
|
||||
),
|
||||
payer: self.payer.clone(),
|
||||
jito_client: self.jito_client.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PumpFun {
|
||||
/// Create a new PumpFun client instance
|
||||
pub fn new(
|
||||
rpc_url: String,
|
||||
commitment: Option<CommitmentConfig>,
|
||||
payer: Arc<Keypair>,
|
||||
jito_url: Option<String>,
|
||||
) -> Self {
|
||||
let rpc = RpcClient::new_with_commitment(
|
||||
rpc_url,
|
||||
commitment.unwrap_or(CommitmentConfig::confirmed())
|
||||
);
|
||||
|
||||
let jito_client = jito_url.map(|url| JitoClient::new(&url, None));
|
||||
|
||||
Self {
|
||||
rpc,
|
||||
payer,
|
||||
jito_client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new token
|
||||
pub async fn create(
|
||||
&self,
|
||||
mint: &Keypair,
|
||||
metadata: utils::CreateTokenMetadata,
|
||||
priority_fee: Option<PriorityFee>,
|
||||
) -> Result<Signature, ClientError> {
|
||||
let ipfs = utils::create_token_metadata(metadata)
|
||||
.await
|
||||
.map_err(ClientError::UploadMetadataError)?;
|
||||
|
||||
let mut instructions = self.create_priority_fee_instructions(priority_fee);
|
||||
|
||||
instructions.push(instruction::create(
|
||||
&self.payer.clone(),
|
||||
mint,
|
||||
instruction::Create {
|
||||
_name: ipfs.metadata.name,
|
||||
_symbol: ipfs.metadata.symbol,
|
||||
_uri: ipfs.metadata.image,
|
||||
},
|
||||
));
|
||||
|
||||
let recent_blockhash = self.rpc.get_latest_blockhash()?;
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&instructions,
|
||||
Some(&self.payer.pubkey()),
|
||||
&[&self.payer.clone(), mint],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
let signature = self.rpc.send_and_confirm_transaction(&transaction)?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
/// Create and buy tokens in one transaction
|
||||
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, ClientError> {
|
||||
let ipfs = utils::create_token_metadata(metadata)
|
||||
.await
|
||||
.map_err(ClientError::UploadMetadataError)?;
|
||||
|
||||
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(DEFAULT_SLIPPAGE));
|
||||
|
||||
let mut instructions = self.create_priority_fee_instructions(priority_fee);
|
||||
|
||||
instructions.push(instruction::create(
|
||||
&self.payer.clone(),
|
||||
mint,
|
||||
instruction::Create {
|
||||
_name: ipfs.metadata.name,
|
||||
_symbol: ipfs.metadata.symbol,
|
||||
_uri: ipfs.metadata.image,
|
||||
},
|
||||
));
|
||||
|
||||
let ata = get_associated_token_address(&self.payer.pubkey(), &mint.pubkey());
|
||||
if self.rpc.get_account(&ata).is_err() {
|
||||
instructions.push(create_associated_token_account(
|
||||
&self.payer.pubkey(),
|
||||
&self.payer.pubkey(),
|
||||
&mint.pubkey(),
|
||||
));
|
||||
}
|
||||
|
||||
instructions.push(instruction::buy(
|
||||
&self.payer.clone(),
|
||||
&mint.pubkey(),
|
||||
&global_account.fee_recipient,
|
||||
instruction::Buy {
|
||||
_amount: buy_amount,
|
||||
_max_sol_cost: buy_amount_with_slippage,
|
||||
},
|
||||
));
|
||||
|
||||
let recent_blockhash = self.rpc.get_latest_blockhash()?;
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&instructions,
|
||||
Some(&self.payer.pubkey()),
|
||||
&[&self.payer.clone(), mint],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
let signature = self.rpc.send_and_confirm_transaction(&transaction)?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
/// Buy tokens
|
||||
pub async fn buy(
|
||||
&self,
|
||||
mint: &Pubkey,
|
||||
amount_sol: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: Option<PriorityFee>,
|
||||
) -> Result<Signature, ClientError> {
|
||||
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(ClientError::BondingCurveError)?;
|
||||
let buy_amount_with_slippage =
|
||||
utils::calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE));
|
||||
|
||||
let mut instructions = self.create_priority_fee_instructions(priority_fee);
|
||||
|
||||
let ata = get_associated_token_address(&self.payer.pubkey(), mint);
|
||||
if self.rpc.get_account(&ata).is_err() {
|
||||
instructions.push(create_associated_token_account(
|
||||
&self.payer.pubkey(),
|
||||
&self.payer.pubkey(),
|
||||
mint,
|
||||
));
|
||||
}
|
||||
|
||||
instructions.push(instruction::buy(
|
||||
&self.payer.clone(),
|
||||
mint,
|
||||
&global_account.fee_recipient,
|
||||
instruction::Buy {
|
||||
_amount: buy_amount,
|
||||
_max_sol_cost: buy_amount_with_slippage,
|
||||
},
|
||||
));
|
||||
|
||||
let recent_blockhash = self.rpc.get_latest_blockhash()?;
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&instructions,
|
||||
Some(&self.payer.pubkey()),
|
||||
&[&self.payer.clone()],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
let signature = self.rpc.send_transaction(&transaction)?;
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
/// Buy tokens using Jito
|
||||
pub async fn buy_with_jito(
|
||||
&self,
|
||||
mint: &Pubkey,
|
||||
amount_sol: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
) -> Result<String, ClientError> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
let jito_client = self.jito_client.as_ref()
|
||||
.ok_or_else(|| ClientError::Other("Jito client not found".to_string()))?;
|
||||
|
||||
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(ClientError::BondingCurveError)?;
|
||||
let buy_amount_with_slippage =
|
||||
utils::calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE));
|
||||
|
||||
let mut instructions = vec![];
|
||||
|
||||
let tip_account = jito_client.get_tip_account().await?;
|
||||
let ata = get_associated_token_address(&self.payer.pubkey(), mint);
|
||||
if self.rpc.get_account(&ata).is_err() {
|
||||
instructions.push(create_associated_token_account(
|
||||
&self.payer.pubkey(),
|
||||
&self.payer.pubkey(),
|
||||
mint,
|
||||
));
|
||||
}
|
||||
|
||||
instructions.push(instruction::buy(
|
||||
self.payer.as_ref(),
|
||||
mint,
|
||||
&global_account.fee_recipient,
|
||||
instruction::Buy {
|
||||
_amount: buy_amount,
|
||||
_max_sol_cost: buy_amount_with_slippage,
|
||||
},
|
||||
));
|
||||
|
||||
instructions.push(
|
||||
system_instruction::transfer(
|
||||
&self.payer.pubkey(),
|
||||
&tip_account,
|
||||
JITO_TIP_AMOUNT,
|
||||
),
|
||||
);
|
||||
|
||||
let recent_blockhash = self.rpc.get_latest_blockhash()?;
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&instructions,
|
||||
Some(&self.payer.pubkey()),
|
||||
&[&self.payer.clone()],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
let signature = jito_client.send_transaction(&transaction).await?;
|
||||
println!("Total Jito buy operation time: {:?}ms", start_time.elapsed().as_millis());
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
/// Sell tokens
|
||||
pub async fn sell(
|
||||
&self,
|
||||
mint: &Pubkey,
|
||||
amount_token: Option<u64>,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: Option<PriorityFee>,
|
||||
) -> Result<Signature, ClientError> {
|
||||
let ata = get_associated_token_address(&self.payer.pubkey(), mint);
|
||||
let balance = self.rpc.get_token_account_balance(&ata)?;
|
||||
let balance_u64 = balance.amount.parse::<u64>()
|
||||
.map_err(|_| ClientError::Other("Failed to parse token balance".to_string()))?;
|
||||
let amount = amount_token.unwrap_or(balance_u64);
|
||||
|
||||
if amount == 0 {
|
||||
return Err(ClientError::Other("Balance is 0".to_string()));
|
||||
}
|
||||
|
||||
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(ClientError::BondingCurveError)?;
|
||||
let min_sol_output_with_slippage = utils::calculate_with_slippage_sell(
|
||||
min_sol_output,
|
||||
slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
|
||||
let mut instructions = self.create_priority_fee_instructions(priority_fee);
|
||||
|
||||
instructions.push(instruction::sell(
|
||||
&self.payer.clone(),
|
||||
mint,
|
||||
&global_account.fee_recipient,
|
||||
instruction::Sell {
|
||||
_amount: amount,
|
||||
_min_sol_output: min_sol_output_with_slippage,
|
||||
},
|
||||
));
|
||||
|
||||
let recent_blockhash = self.rpc.get_latest_blockhash()?;
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&instructions,
|
||||
Some(&self.payer.pubkey()),
|
||||
&[&self.payer.clone()],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
let signature = self.rpc.send_and_confirm_transaction(&transaction)?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
/// Sell tokens by percentage
|
||||
pub async fn sell_by_percent(
|
||||
&self,
|
||||
mint: &Pubkey,
|
||||
percent: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: Option<PriorityFee>,
|
||||
) -> Result<Signature, ClientError> {
|
||||
if percent > 100 {
|
||||
return Err(ClientError::Other("Percentage must be between 0 and 100".to_string()));
|
||||
}
|
||||
|
||||
let ata = get_associated_token_address(&self.payer.pubkey(), mint);
|
||||
let balance = self.rpc.get_token_account_balance(&ata)?;
|
||||
let balance_u64 = balance.amount.parse::<u64>()
|
||||
.map_err(|_| ClientError::Other("Failed to parse token balance".to_string()))?;
|
||||
|
||||
if balance_u64 == 0 {
|
||||
return Err(ClientError::Other("Balance is 0".to_string()));
|
||||
}
|
||||
|
||||
let amount = balance_u64 * percent / 100;
|
||||
self.sell(mint, Some(amount), slippage_basis_points, priority_fee).await
|
||||
}
|
||||
|
||||
pub async fn sell_by_percent_with_jito(
|
||||
&self,
|
||||
mint: &Pubkey,
|
||||
percent: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
) -> Result<String, ClientError> {
|
||||
if percent > 100 {
|
||||
return Err(ClientError::Other("Percentage must be between 0 and 100".to_string()));
|
||||
}
|
||||
|
||||
let ata = get_associated_token_address(&self.payer.pubkey(), mint);
|
||||
let balance = self.rpc.get_token_account_balance(&ata)?;
|
||||
let balance_u64 = balance.amount.parse::<u64>()
|
||||
.map_err(|_| ClientError::Other("Failed to parse token balance".to_string()))?;
|
||||
|
||||
if balance_u64 == 0 {
|
||||
return Err(ClientError::Other("Balance is 0".to_string()));
|
||||
}
|
||||
|
||||
let amount = balance_u64 * percent / 100;
|
||||
self.sell_with_jito(mint, Some(amount), slippage_basis_points).await
|
||||
}
|
||||
|
||||
/// Sell tokens using Jito
|
||||
pub async fn sell_with_jito(
|
||||
&self,
|
||||
mint: &Pubkey,
|
||||
amount_token: Option<u64>,
|
||||
slippage_basis_points: Option<u64>,
|
||||
) -> Result<String, ClientError> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
let jito_client = self.jito_client.as_ref()
|
||||
.ok_or_else(|| ClientError::Other("Jito client not found".to_string()))?;
|
||||
|
||||
let ata = get_associated_token_address(&self.payer.pubkey(), mint);
|
||||
let balance = self.rpc.get_token_account_balance(&ata)?;
|
||||
let balance_u64 = balance.amount.parse::<u64>()
|
||||
.map_err(|_| ClientError::Other("Failed to parse token balance".to_string()))?;
|
||||
let amount = amount_token.unwrap_or(balance_u64);
|
||||
|
||||
if amount == 0 {
|
||||
return Err(ClientError::Other("Amount cannot be zero".to_string()));
|
||||
}
|
||||
|
||||
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(ClientError::BondingCurveError)?;
|
||||
let min_sol_output_with_slippage = utils::calculate_with_slippage_sell(
|
||||
min_sol_output,
|
||||
slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
|
||||
let mut instructions = vec![];
|
||||
let tip_account = jito_client.get_tip_account().await?;
|
||||
instructions.push(instruction::sell(
|
||||
&self.payer.clone(),
|
||||
mint,
|
||||
&global_account.fee_recipient,
|
||||
instruction::Sell {
|
||||
_amount: amount,
|
||||
_min_sol_output: min_sol_output_with_slippage,
|
||||
},
|
||||
));
|
||||
|
||||
instructions.push(
|
||||
system_instruction::transfer(
|
||||
&self.payer.pubkey(),
|
||||
&tip_account,
|
||||
JITO_TIP_AMOUNT,
|
||||
),
|
||||
);
|
||||
|
||||
let recent_blockhash = self.rpc.get_latest_blockhash()?;
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&instructions,
|
||||
Some(&self.payer.pubkey()),
|
||||
&[&self.payer.clone()],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
let signature = jito_client.send_transaction(&transaction).await?;
|
||||
println!("Total Jito sell operation time: {:?}ms", start_time.elapsed().as_millis());
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
fn create_priority_fee_instructions(&self, priority_fee: Option<PriorityFee>) -> Vec<Instruction> {
|
||||
let mut instructions = Vec::new();
|
||||
let fee = priority_fee.unwrap_or(PriorityFee::default());
|
||||
if let Some(limit) = fee.limit {
|
||||
instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(limit));
|
||||
}
|
||||
if let Some(price) = fee.price {
|
||||
instructions.push(ComputeBudgetInstruction::set_compute_unit_price(price));
|
||||
}
|
||||
|
||||
instructions
|
||||
}
|
||||
|
||||
// Public interface methods
|
||||
pub fn get_payer_pubkey(&self) -> Pubkey {
|
||||
self.payer.pubkey()
|
||||
}
|
||||
|
||||
pub fn get_token_balance(&self, account: &Pubkey, mint: &Pubkey) -> Result<u64, ClientError> {
|
||||
let ata = get_associated_token_address(account, mint);
|
||||
if self.rpc.get_account(&ata).is_err() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let balance = self.rpc.get_token_account_balance(&ata)?;
|
||||
balance.amount.parse::<u64>()
|
||||
.map_err(|_| ClientError::Other("Failed to parse token balance".to_string()))
|
||||
}
|
||||
|
||||
pub fn get_sol_balance(&self, account: &Pubkey) -> Result<u64, ClientError> {
|
||||
self.rpc.get_balance(account).map_err(ClientError::SolanaClientError)
|
||||
}
|
||||
|
||||
pub fn get_payer_token_balance(&self, mint: &Pubkey) -> Result<u64, ClientError> {
|
||||
self.get_token_balance(&self.payer.pubkey(), mint)
|
||||
}
|
||||
|
||||
pub fn get_payer_sol_balance(&self) -> Result<u64, ClientError> {
|
||||
self.get_sol_balance(&self.payer.pubkey())
|
||||
}
|
||||
|
||||
// PDA related methods
|
||||
pub fn get_global_pda() -> Pubkey {
|
||||
Pubkey::find_program_address(&[constants::seeds::GLOBAL_SEED], &constants::accounts::PUMPFUN).0
|
||||
}
|
||||
|
||||
pub fn get_mint_authority_pda() -> Pubkey {
|
||||
Pubkey::find_program_address(&[constants::seeds::MINT_AUTHORITY_SEED], &constants::accounts::PUMPFUN).0
|
||||
}
|
||||
|
||||
pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
Pubkey::try_find_program_address(
|
||||
&[constants::seeds::BONDING_CURVE_SEED, mint.as_ref()],
|
||||
&constants::accounts::PUMPFUN
|
||||
).map(|(pubkey, _)| pubkey)
|
||||
}
|
||||
|
||||
pub fn get_metadata_pda(mint: &Pubkey) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&[
|
||||
constants::seeds::METADATA_SEED,
|
||||
constants::accounts::MPL_TOKEN_METADATA.as_ref(),
|
||||
mint.as_ref(),
|
||||
],
|
||||
&constants::accounts::MPL_TOKEN_METADATA
|
||||
).0
|
||||
}
|
||||
|
||||
// Account related methods
|
||||
pub fn get_global_account(&self) -> Result<accounts::GlobalAccount, ClientError> {
|
||||
let global = Self::get_global_pda();
|
||||
let account = self.rpc.get_account(&global)?;
|
||||
accounts::GlobalAccount::try_from_slice(&account.data)
|
||||
.map_err(ClientError::BorshError)
|
||||
}
|
||||
|
||||
pub fn get_bonding_curve_account(
|
||||
&self,
|
||||
mint: &Pubkey,
|
||||
) -> Result<accounts::BondingCurveAccount, ClientError> {
|
||||
let bonding_curve_pda = Self::get_bonding_curve_pda(mint)
|
||||
.ok_or(ClientError::BondingCurveNotFound)?;
|
||||
let account = self.rpc.get_account(&bonding_curve_pda)?;
|
||||
accounts::BondingCurveAccount::try_from_slice(&account.data)
|
||||
.map_err(ClientError::BorshError)
|
||||
}
|
||||
|
||||
// Subscription related methods
|
||||
pub async fn tokens_subscription<F>(
|
||||
&self,
|
||||
ws_url: &str,
|
||||
commitment: CommitmentConfig,
|
||||
callback: F,
|
||||
bot_wallet: Option<Pubkey>,
|
||||
) -> Result<SubscriptionHandle, Box<dyn std::error::Error>>
|
||||
where
|
||||
F: Fn(DexEvent) + Send + Sync + 'static,
|
||||
{
|
||||
logs_subscribe::tokens_subscription(ws_url, commitment, callback, bot_wallet).await
|
||||
}
|
||||
|
||||
pub async fn stop_subscription(&self, subscription_handle: SubscriptionHandle) {
|
||||
subscription_handle.shutdown().await;
|
||||
}
|
||||
|
||||
pub fn get_buy_amount_with_slippage(&self, amount_sol: u64, slippage_basis_points: Option<u64>) -> u64 {
|
||||
utils::calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE))
|
||||
}
|
||||
|
||||
pub fn get_token_price(&self, virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 {
|
||||
let v_sol = virtual_sol_reserves as f64 / 100_000_000.0;
|
||||
let v_tokens = virtual_token_reserves as f64 / 100_000.0;
|
||||
let token_price = v_sol / v_tokens;
|
||||
token_price
|
||||
}
|
||||
|
||||
pub async fn get_token_price_in_usdc(&self, token_amount: f64) -> Result<f64, ClientError> {
|
||||
if token_amount == 0.0 {
|
||||
return Ok(0.0);
|
||||
}
|
||||
|
||||
let url = "https://api.jup.ag/price/v2?ids=So11111111111111111111111111111111111111112";
|
||||
let response: serde_json::Value = reqwest::get(url)
|
||||
.await
|
||||
.map_err(|e: reqwest::Error| ClientError::Other(Box::new(e).to_string()))?
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e: reqwest::Error| ClientError::Other(Box::new(e).to_string()))?;
|
||||
|
||||
let sol_price_str = response["data"]["So11111111111111111111111111111111111111112"]["price"]
|
||||
.as_str()
|
||||
.ok_or(ClientError::Other("Failed to find SOL price as a string".into()))?;
|
||||
|
||||
let sol_price_in_usdc: f64 = sol_price_str
|
||||
.parse()
|
||||
.map_err(|e: std::num::ParseFloatError| ClientError::Other(Box::new(e).to_string()))?;
|
||||
|
||||
let token_price_in_usdc = sol_price_in_usdc * token_amount;
|
||||
Ok(token_price_in_usdc)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_client() {
|
||||
let payer = Arc::new(Keypair::new());
|
||||
let client = PumpFun::new(Cluster::Devnet, None, Arc::clone(&payer), 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());
|
||||
}
|
||||
}
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
use mai3_pumpfun_sdk::instruction::{
|
||||
logs_events::DexEvent,
|
||||
logs_subscribe::{tokens_subscription, stop_subscription}
|
||||
};
|
||||
use solana_sdk::commitment_config::CommitmentConfig;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Starting token subscription\n");
|
||||
|
||||
let ws_url = "wss://api.mainnet-beta.solana.com";
|
||||
|
||||
// Set commitment
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
|
||||
// Define callback function
|
||||
let callback = |event: DexEvent| {
|
||||
match event {
|
||||
DexEvent::NewToken(token_info) => {
|
||||
println!("Received new token event: {:?}", token_info);
|
||||
},
|
||||
DexEvent::NewUserTrade(trade_info) => {
|
||||
println!("Received new trade event: {:?}", trade_info);
|
||||
},
|
||||
DexEvent::NewBotTrade(trade_info) => {
|
||||
println!("Received new bot trade event: {:?}", trade_info);
|
||||
},
|
||||
DexEvent::Error(err) => {
|
||||
println!("Received error: {}", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start subscription
|
||||
let subscription = tokens_subscription(
|
||||
ws_url,
|
||||
commitment,
|
||||
callback,
|
||||
None
|
||||
).await.unwrap();
|
||||
|
||||
// Wait for a while to receive events
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
|
||||
|
||||
// Stop subscription
|
||||
stop_subscription(subscription).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Executable
+270
@@ -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 mai3_pumpfun_sdk::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 mai3_pumpfun_sdk::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 mai3_pumpfun_sdk::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