feat: refactor event parsing architecture and add Raydium Launchpad support
Major changes: - Refactor event parsing system: migrate scattered logs_* modules to unified event_parser architecture - Add unified UnifiedEvent trait and EventParser trait for standardized event parsing interface - Introduce GenericEventParser and EventParserFactory for plugin-style protocol extension - Add match_event! macro to simplify event type matching and handling - Add Raydium Launchpad (Bonk.fun) protocol support: - Complete buy/sell trading functionality - Pool state management and querying - Event parsing and subscription - Update trading system to support new protocol architecture - Refactor constants organization, rename raydium to raydium_launchpad - Update documentation and example code Technical improvements: - Unified event interface design for better code maintainability - Factory pattern implementation for dynamic protocol loading - Generic event parser to reduce code duplication - Improved error handling and type safety Breaking Changes: - Remove old logs_* modules, use new event_parser system - Protocol constants path change: raydium -> raydium_launchpad - Event subscription API updated to unified interface
This commit is contained in:
+3
-1
@@ -18,4 +18,6 @@ Cargo.lock
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
#.idea/
|
||||
|
||||
.cargo/
|
||||
@@ -1,18 +1,18 @@
|
||||
# Sol Trade SDK
|
||||
|
||||
A comprehensive Rust SDK for seamless interaction with Solana DEX trading programs. This SDK provides a robust set of tools and interfaces to integrate PumpFun and PumpSwap functionality into your applications.
|
||||
A comprehensive Rust SDK for seamless interaction with Solana DEX trading programs. This SDK provides a robust set of tools and interfaces to integrate PumpFun, PumpSwap, and Raydium Launchpad (Bonk.fun) functionality into your applications.
|
||||
|
||||
## Features
|
||||
## Project Features
|
||||
|
||||
1. **PumpFun Trading**: Support for `buy`, `sell` operations
|
||||
1. **PumpFun Trading**: Support for `buy` and `sell` operations
|
||||
2. **PumpSwap Trading**: Support for PumpSwap pool trading operations
|
||||
3. **Raydium Trading**: Support for Raydium DEX trading operations
|
||||
4. **Logs Subscription**: Subscribe to PumpFun, PumpSwap, and Raydium program transaction logs
|
||||
5. **Yellowstone gRPC**: Subscribe to program logs using Yellowstone gRPC
|
||||
6. **ShredStream Support**: Subscribe to program logs using ShredStream
|
||||
7. **Multiple MEV Protection**: Support for Jito, Nextblock, 0slot, Nozomi services
|
||||
8. **Concurrent Transactions**: Submit transactions using multiple MEV services simultaneously; the fastest succeeds while others fail
|
||||
9. **Real-time Pricing**: Get real-time token prices and liquidity information
|
||||
3. **Raydium Trading**: Support for Raydium Launchpad (Bonk.fun) trading operations
|
||||
4. **Event Subscription**: Subscribe to PumpFun, PumpSwap, and Raydium Launchpad (Bonk.fun) program trading events
|
||||
5. **Yellowstone gRPC**: Subscribe to program events using Yellowstone gRPC
|
||||
6. **ShredStream Support**: Subscribe to program events using ShredStream
|
||||
7. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, and other services
|
||||
8. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail
|
||||
9. **Unified Trading Interface**: Use unified parameter structures for trading operations
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -32,53 +32,425 @@ sol-trade-sdk = { path = "./sol-trade-sdk", version = "0.1.0" }
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### 1. Logs Subscription - Monitor Token Trading
|
||||
### 1. Event Subscription - Monitor Token Trading
|
||||
|
||||
#### 1.1 Subscribe to Events Using Yellowstone gRPC
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{common::pumpfun::logs_events::PumpfunEvent, grpc::YellowstoneGrpc};
|
||||
use solana_sdk::signature::Keypair;
|
||||
|
||||
// Create gRPC client with Yellowstone
|
||||
let grpc_url = "https://solana-yellowstone-grpc.publicnode.com:443";
|
||||
let x_token = None; // Optional auth token
|
||||
let client = YellowstoneGrpc::new(grpc_url.to_string(), x_token)?;
|
||||
|
||||
// Define callback function
|
||||
let callback = |event: PumpfunEvent| match event {
|
||||
PumpfunEvent::NewDevTrade(trade_info) => {
|
||||
println!("Received new dev trade event: {:?}", trade_info);
|
||||
}
|
||||
PumpfunEvent::NewToken(token_info) => {
|
||||
println!("Received new token event: {:?}", token_info);
|
||||
}
|
||||
PumpfunEvent::NewUserTrade(trade_info) => {
|
||||
println!("Received new trade event: {:?}", trade_info);
|
||||
}
|
||||
PumpfunEvent::NewBotTrade(trade_info) => {
|
||||
println!("Received new bot trade event: {:?}", trade_info);
|
||||
}
|
||||
PumpfunEvent::Error(err) => {
|
||||
println!("Received error: {}", err);
|
||||
}
|
||||
use sol_trade_sdk::{
|
||||
event_parser::{
|
||||
protocols::{
|
||||
pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent},
|
||||
pumpswap::{
|
||||
PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent,
|
||||
PumpSwapSellEvent, PumpSwapWithdrawEvent,
|
||||
},
|
||||
raydium_launchpad::{RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent},
|
||||
},
|
||||
Protocol, UnifiedEvent,
|
||||
},
|
||||
grpc::YellowstoneGrpc,
|
||||
match_event,
|
||||
};
|
||||
|
||||
client.subscribe_pumpfun(callback, None).await?;
|
||||
async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Subscribe to events using GRPC client
|
||||
println!("Subscribing to GRPC events...");
|
||||
|
||||
let grpc = YellowstoneGrpc::new(
|
||||
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
||||
None,
|
||||
)?;
|
||||
|
||||
// Define callback function to handle events
|
||||
let callback = |event: Box<dyn UnifiedEvent>| {
|
||||
match_event!(event, {
|
||||
RaydiumLaunchpadPoolCreateEvent => |e: RaydiumLaunchpadPoolCreateEvent| {
|
||||
println!("RaydiumLaunchpadPoolCreateEvent: {:?}", e.base_mint_param.symbol);
|
||||
},
|
||||
RaydiumLaunchpadTradeEvent => |e: RaydiumLaunchpadTradeEvent| {
|
||||
println!("RaydiumLaunchpadTradeEvent: {:?}", e);
|
||||
},
|
||||
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
|
||||
println!("PumpFunTradeEvent: {:?}", e);
|
||||
},
|
||||
PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| {
|
||||
println!("PumpFunCreateTokenEvent: {:?}", e);
|
||||
},
|
||||
PumpSwapBuyEvent => |e: PumpSwapBuyEvent| {
|
||||
println!("Buy event: {:?}", e);
|
||||
},
|
||||
PumpSwapSellEvent => |e: PumpSwapSellEvent| {
|
||||
println!("Sell event: {:?}", e);
|
||||
},
|
||||
PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| {
|
||||
println!("CreatePool event: {:?}", e);
|
||||
},
|
||||
PumpSwapDepositEvent => |e: PumpSwapDepositEvent| {
|
||||
println!("Deposit event: {:?}", e);
|
||||
},
|
||||
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
|
||||
println!("Withdraw event: {:?}", e);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Subscribe to events from multiple protocols
|
||||
println!("Starting to listen for events, press Ctrl+C to stop...");
|
||||
let protocols = vec![
|
||||
Protocol::PumpFun,
|
||||
Protocol::PumpSwap,
|
||||
Protocol::RaydiumLaunchpad,
|
||||
];
|
||||
grpc.subscribe_events(protocols, None, None, None, callback)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 Subscribe to Events Using ShredStream
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::grpc::ShredStreamGrpc;
|
||||
|
||||
async fn test_shreds() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Subscribe to events using ShredStream client
|
||||
println!("Subscribing to ShredStream events...");
|
||||
|
||||
let shred_stream = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?;
|
||||
|
||||
// Define callback function to handle events (same as above)
|
||||
let callback = |event: Box<dyn UnifiedEvent>| {
|
||||
match_event!(event, {
|
||||
RaydiumLaunchpadPoolCreateEvent => |e: RaydiumLaunchpadPoolCreateEvent| {
|
||||
println!("RaydiumLaunchpadPoolCreateEvent: {:?}", e.base_mint_param.symbol);
|
||||
},
|
||||
RaydiumLaunchpadTradeEvent => |e: RaydiumLaunchpadTradeEvent| {
|
||||
println!("RaydiumLaunchpadTradeEvent: {:?}", e);
|
||||
},
|
||||
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
|
||||
println!("PumpFunTradeEvent: {:?}", e);
|
||||
},
|
||||
PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| {
|
||||
println!("PumpFunCreateTokenEvent: {:?}", e);
|
||||
},
|
||||
PumpSwapBuyEvent => |e: PumpSwapBuyEvent| {
|
||||
println!("Buy event: {:?}", e);
|
||||
},
|
||||
PumpSwapSellEvent => |e: PumpSwapSellEvent| {
|
||||
println!("Sell event: {:?}", e);
|
||||
},
|
||||
PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| {
|
||||
println!("CreatePool event: {:?}", e);
|
||||
},
|
||||
PumpSwapDepositEvent => |e: PumpSwapDepositEvent| {
|
||||
println!("Deposit event: {:?}", e);
|
||||
},
|
||||
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
|
||||
println!("Withdraw event: {:?}", e);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Subscribe to events
|
||||
println!("Starting to listen for events, press Ctrl+C to stop...");
|
||||
let protocols = vec![
|
||||
Protocol::PumpFun,
|
||||
Protocol::PumpSwap,
|
||||
Protocol::RaydiumLaunchpad,
|
||||
];
|
||||
shred_stream
|
||||
.shredstream_subscribe(protocols, None, callback)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Initialize SolanaTrade Instance
|
||||
|
||||
```rust
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use sol_trade_sdk::{
|
||||
common::{AnyResult, PriorityFee, TradeConfig},
|
||||
swqos::{SwqosConfig, SwqosRegion, SwqosType},
|
||||
common::{AnyResult, PriorityFee, TradeConfig},
|
||||
swqos::{SwqosConfig, SwqosRegion},
|
||||
SolanaTrade
|
||||
};
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Keypair};
|
||||
|
||||
// Configure priority fees
|
||||
// Create trader account
|
||||
let payer = Keypair::new();
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
|
||||
// Configure multiple MEV services to support concurrent trading
|
||||
let swqos_configs = vec![
|
||||
SwqosConfig::Jito(SwqosRegion::Frankfurt),
|
||||
SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
];
|
||||
|
||||
// Define trading configuration
|
||||
let trade_config = TradeConfig {
|
||||
rpc_url: rpc_url.clone(),
|
||||
commitment: CommitmentConfig::confirmed(),
|
||||
priority_fee: PriorityFee::default(),
|
||||
swqos_configs,
|
||||
lookup_table_key: None,
|
||||
};
|
||||
|
||||
// Create SolanaTrade instance
|
||||
let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
```
|
||||
|
||||
### 3. PumpFun Trading Operations
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{
|
||||
accounts::BondingCurveAccount,
|
||||
constants::{pumpfun::global_constants::TOKEN_TOTAL_SUPPLY, trade_type},
|
||||
pumpfun::common::get_bonding_curve_account_v2,
|
||||
trading::{
|
||||
core::params::{PumpFunParams, PumpFunSellParams},
|
||||
BuyParams, SellParams,
|
||||
},
|
||||
};
|
||||
|
||||
async fn test_pumpfun() -> AnyResult<()> {
|
||||
// Basic parameter setup
|
||||
let creator = Pubkey::from_str("xxxxxx")?; // Developer account
|
||||
let mint_pubkey = Pubkey::from_str("xxxxxx")?; // Token address
|
||||
let buy_sol_cost = 100_000; // 0.0001 SOL
|
||||
let slippage_basis_points = Some(100);
|
||||
let rpc = RpcClient::new(rpc_url);
|
||||
let recent_blockhash = rpc.get_latest_blockhash().unwrap();
|
||||
|
||||
println!("Buying tokens from PumpFun...");
|
||||
|
||||
// Get bonding curve information
|
||||
let (bonding_curve, bonding_curve_pda) =
|
||||
get_bonding_curve_account_v2(&solana_trade_client.rpc, &mint_pubkey).await?;
|
||||
|
||||
let bonding_curve = BondingCurveAccount {
|
||||
discriminator: bonding_curve.discriminator,
|
||||
account: bonding_curve_pda,
|
||||
virtual_token_reserves: bonding_curve.virtual_token_reserves,
|
||||
virtual_sol_reserves: bonding_curve.virtual_sol_reserves,
|
||||
real_token_reserves: bonding_curve.real_token_reserves,
|
||||
real_sol_reserves: bonding_curve.real_sol_reserves,
|
||||
token_total_supply: TOKEN_TOTAL_SUPPLY,
|
||||
complete: false,
|
||||
creator: creator,
|
||||
};
|
||||
|
||||
// Buy operation
|
||||
let buy_protocol_params = PumpFunParams {
|
||||
trade_type: trade_type::COPY_BUY.to_string(),
|
||||
bonding_curve: Some(Arc::new(bonding_curve)),
|
||||
};
|
||||
|
||||
let buy_params = BuyParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint_pubkey,
|
||||
creator: creator,
|
||||
amount_sol: buy_sol_cost,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
data_size_limit: 0,
|
||||
protocol_params: Box::new(buy_protocol_params.clone()),
|
||||
};
|
||||
|
||||
// Buy with MEV protection
|
||||
let buy_with_tip_params = buy_params
|
||||
.clone()
|
||||
.with_tip(solana_trade_client.swqos_clients.clone());
|
||||
|
||||
solana_trade_client
|
||||
.buy_use_buy_params(buy_with_tip_params, None)
|
||||
.await?;
|
||||
|
||||
// Sell operation
|
||||
println!("Selling tokens from PumpFun...");
|
||||
let sell_protocol_params = PumpFunSellParams {};
|
||||
let amount_token = 1000000; // Enter the actual token amount
|
||||
|
||||
let sell_params = SellParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint_pubkey,
|
||||
creator: creator,
|
||||
amount_token: Some(amount_token),
|
||||
slippage_basis_points: None,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
protocol_params: Box::new(sell_protocol_params.clone()),
|
||||
};
|
||||
|
||||
solana_trade_client
|
||||
.sell_by_amount_use_sell_params(sell_params)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 4. PumpSwap Trading Operations
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::trading::core::params::PumpSwapParams;
|
||||
|
||||
async fn test_pumpswap() -> AnyResult<()> {
|
||||
// Basic parameter setup
|
||||
let creator = Pubkey::from_str("11111111111111111111111111111111")?; // Developer account
|
||||
let mint_pubkey = Pubkey::from_str("2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv")?; // Token address
|
||||
let buy_sol_cost = 100_000; // 0.0001 SOL
|
||||
let slippage_basis_points = Some(100);
|
||||
let rpc = RpcClient::new(rpc_url);
|
||||
let recent_blockhash = rpc.get_latest_blockhash().unwrap();
|
||||
|
||||
println!("Buying tokens from PumpSwap...");
|
||||
|
||||
// PumpSwap parameter configuration
|
||||
let protocol_params = PumpSwapParams {
|
||||
pool: None,
|
||||
pool_base_token_account: None,
|
||||
pool_quote_token_account: None,
|
||||
user_base_token_account: None,
|
||||
user_quote_token_account: None,
|
||||
auto_handle_wsol: true,
|
||||
};
|
||||
|
||||
// Buy operation
|
||||
let buy_params = BuyParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint_pubkey,
|
||||
creator: creator,
|
||||
amount_sol: buy_sol_cost,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
data_size_limit: 0,
|
||||
protocol_params: Box::new(protocol_params.clone()),
|
||||
};
|
||||
|
||||
let buy_with_tip_params = buy_params
|
||||
.clone()
|
||||
.with_tip(solana_trade_client.swqos_clients.clone());
|
||||
|
||||
solana_trade_client
|
||||
.buy_use_buy_params(buy_with_tip_params, None)
|
||||
.await?;
|
||||
|
||||
// Sell operation
|
||||
println!("Selling tokens from PumpSwap...");
|
||||
let amount_token = 1000000; // Enter the actual token amount
|
||||
|
||||
let sell_params = SellParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint_pubkey,
|
||||
creator: creator,
|
||||
amount_token: Some(amount_token),
|
||||
slippage_basis_points: None,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
protocol_params: Box::new(protocol_params.clone()),
|
||||
};
|
||||
|
||||
solana_trade_client
|
||||
.sell_by_amount_use_sell_params(sell_params)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Raydium Launchpad Trading Operations
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::trading::core::params::RaydiumLaunchpadParams;
|
||||
|
||||
async fn test_raydium_launchpad() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Basic parameter setup
|
||||
let amount = 100_000; // 0.0001 SOL
|
||||
let mint = Pubkey::from_str("xxxxxxx")?;
|
||||
let recent_blockhash = solana_trade_client.rpc.get_latest_blockhash().await?;
|
||||
|
||||
// Raydium Launchpad parameter configuration
|
||||
let raydium_launchpad_params = RaydiumLaunchpadParams {
|
||||
virtual_base: None,
|
||||
virtual_quote: None,
|
||||
real_base_before: None,
|
||||
real_quote_before: None,
|
||||
auto_handle_wsol: true,
|
||||
};
|
||||
|
||||
println!("Buying tokens from Raydium Launchpad...");
|
||||
|
||||
// Buy operation
|
||||
let buy_params = BuyParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint,
|
||||
creator: Pubkey::default(),
|
||||
amount_sol: amount,
|
||||
slippage_basis_points: None,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
data_size_limit: 0,
|
||||
protocol_params: Box::new(raydium_launchpad_params.clone()),
|
||||
};
|
||||
|
||||
let buy_with_tip_params = buy_params
|
||||
.clone()
|
||||
.with_tip(solana_trade_client.swqos_clients.clone());
|
||||
|
||||
solana_trade_client
|
||||
.buy_use_buy_params(buy_with_tip_params, None)
|
||||
.await?;
|
||||
|
||||
// Sell operation
|
||||
println!("Selling tokens from Raydium Launchpad...");
|
||||
|
||||
let sell_params = SellParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint,
|
||||
creator: Pubkey::default(),
|
||||
amount_token: None,
|
||||
slippage_basis_points: None,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
protocol_params: Box::new(raydium_launchpad_params.clone()),
|
||||
};
|
||||
|
||||
solana_trade_client
|
||||
.sell_by_amount_use_sell_params(sell_params)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Custom Priority Fee Configuration
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::common::PriorityFee;
|
||||
|
||||
// Custom priority fee configuration
|
||||
let priority_fee = PriorityFee {
|
||||
unit_limit: 190000,
|
||||
unit_price: 1000000,
|
||||
@@ -89,327 +461,79 @@ let priority_fee = PriorityFee {
|
||||
sell_tip_fee: 0.0001,
|
||||
};
|
||||
|
||||
// Configure multiple swqos in single region, can send transactions concurrently
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let swqos_configs = vec![
|
||||
SwqosConfig::Jito(SwqosRegion::Frankfurt),
|
||||
SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
];
|
||||
|
||||
// Define sdk configuration
|
||||
// Use custom priority fee in TradeConfig
|
||||
let trade_config = TradeConfig {
|
||||
rpc_url: rpc_url.clone(),
|
||||
commitment: CommitmentConfig::confirmed(),
|
||||
priority_fee,
|
||||
priority_fee, // Use custom priority fee
|
||||
swqos_configs,
|
||||
lookup_table_key: None,
|
||||
};
|
||||
|
||||
// Create SolanaTrade instance
|
||||
let payer = Keypair::from_base58_string("your_private_key");
|
||||
let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
```
|
||||
|
||||
### 3. Buy Tokens
|
||||
|
||||
### 3.1 Buy Tokens --- Sniping
|
||||
```rust
|
||||
use solana_sdk::{pubkey::Pubkey, hash::Hash};
|
||||
use std::sync::Arc;
|
||||
use sol_trade_sdk::accounts::BondingCurveAccount;
|
||||
|
||||
let mint_pubkey = Pubkey::from_str("token_address")?;
|
||||
let creator = Pubkey::from_str("creator_address")?;
|
||||
let recent_blockhash = Hash::default(); // Get latest blockhash
|
||||
let buy_sol_cost = 50000; // 0.00005 SOL
|
||||
let slippage_basis_points = Some(100); // 1%
|
||||
|
||||
// Sniping buy (quick purchase when new token launches)
|
||||
let dev_buy_token = 100_000; // Test value
|
||||
let dev_cost_sol = 10_000; // Test value
|
||||
let bonding_curve = BondingCurveAccount::new(&mint_pubkey, dev_buy_token, dev_cost_sol, creator);
|
||||
|
||||
solana_trade_client.sniper_buy(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
buy_sol_cost,
|
||||
slippage_basis_points,
|
||||
recent_blockhash,
|
||||
Some(Arc::new(bonding_curve)),
|
||||
).await?;
|
||||
|
||||
// Buy with MEV protection using tips
|
||||
solana_trade_client.sniper_buy_with_tip(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
buy_sol_cost,
|
||||
slippage_basis_points,
|
||||
recent_blockhash,
|
||||
Some(Arc::new(bonding_curve)),
|
||||
None, // Custom tip
|
||||
).await?;
|
||||
```
|
||||
|
||||
### 3.2 Buy Tokens --- Copy Trading
|
||||
```rust
|
||||
use solana_sdk::{pubkey::Pubkey, hash::Hash};
|
||||
use std::sync::Arc;
|
||||
use sol_trade_sdk::accounts::BondingCurveAccount;
|
||||
use sol_trade_sdk::{constants::{pumpfun::global_constants::TOKEN_TOTAL_SUPPLY, trade_type::COPY_BUY}, pumpfun::common::get_bonding_curve_pda};
|
||||
|
||||
let mint_pubkey = Pubkey::from_str("token_address")?;
|
||||
let creator = Pubkey::from_str("creator_address")?;
|
||||
let recent_blockhash = Hash::default(); // Get latest blockhash
|
||||
let buy_sol_cost = 50000; // 0.00005 SOL
|
||||
let slippage_basis_points = Some(100); // 1%
|
||||
|
||||
// Copy trading buy
|
||||
let dev_buy_token = 100_000; // Test value
|
||||
let dev_cost_sol = 10_000; // Test value
|
||||
// trade_info comes from pumpfun parsed data, refer to section 1. Logs Subscription above
|
||||
let bonding_curve = Some(Arc::new(BondingCurveAccount {
|
||||
discriminator: 0,
|
||||
account: get_bonding_curve_pda(&trade_info.mint).unwrap(),
|
||||
virtual_token_reserves: trade_info.virtual_token_reserves,
|
||||
virtual_sol_reserves: trade_info.virtual_sol_reserves,
|
||||
real_token_reserves: trade_info.real_token_reserves,
|
||||
real_sol_reserves: trade_info.real_sol_reserves,
|
||||
token_total_supply: TOKEN_TOTAL_SUPPLY,
|
||||
complete: false,
|
||||
creator: Pubkey::from_str(&creator).unwrap(),
|
||||
}));
|
||||
|
||||
solana_trade_client.buy(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
buy_sol_cost,
|
||||
slippage_basis_points,
|
||||
recent_blockhash,
|
||||
Some(Arc::new(bonding_curve)),
|
||||
"pumpfun".to_string(),
|
||||
).await?;
|
||||
|
||||
// Buy with MEV protection using tips
|
||||
solana_trade_client.buy_with_tip(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
buy_sol_cost,
|
||||
slippage_basis_points,
|
||||
recent_blockhash,
|
||||
Some(Arc::new(bonding_curve)),
|
||||
"pumpfun".to_string(),
|
||||
None, // Custom tip
|
||||
).await?;
|
||||
```
|
||||
|
||||
### 4. Sell Tokens
|
||||
|
||||
```rust
|
||||
// Sell by amount
|
||||
solana_trade_client.sell_by_amount_with_tip(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
1000000, // token amount
|
||||
recent_blockhash,
|
||||
"pumpfun".to_string(), // trading platform
|
||||
).await?;
|
||||
|
||||
// Sell by percentage
|
||||
solana_trade_client.sell_by_percent_with_tip(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
50, // percentage (50%)
|
||||
2000000, // total token amount
|
||||
recent_blockhash,
|
||||
"pumpfun".to_string(), // trading platform
|
||||
).await?;
|
||||
```
|
||||
|
||||
### 5. Get Price and Balance Information
|
||||
|
||||
```rust
|
||||
// Get current token price
|
||||
let price = solana_trade_client.get_current_price(&mint_pubkey).await?;
|
||||
println!("Current price: {}", price);
|
||||
|
||||
// Get SOL balance
|
||||
let sol_balance = solana_trade_client.get_payer_sol_balance().await?;
|
||||
println!("SOL balance: {} lamports", sol_balance);
|
||||
|
||||
// Get token balance
|
||||
let token_balance = solana_trade_client.get_payer_token_balance(&mint_pubkey).await?;
|
||||
println!("Token balance: {}", token_balance);
|
||||
|
||||
// Get liquidity information
|
||||
let sol_reserves = solana_trade_client.get_real_sol_reserves(&mint_pubkey).await?;
|
||||
println!("SOL reserves: {} lamports", sol_reserves);
|
||||
```
|
||||
|
||||
### 6. PumpSwap Subscription - Monitor AMM Events
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{common::pumpswap::logs_events::PumpSwapEvent, grpc::YellowstoneGrpc};
|
||||
|
||||
// Create gRPC client with Yellowstone
|
||||
let grpc_url = "https://solana-yellowstone-grpc.publicnode.com:443";
|
||||
let x_token = None;
|
||||
let client = YellowstoneGrpc::new(grpc_url.to_string(), x_token)?;
|
||||
|
||||
// Define callback function for PumpSwap events
|
||||
let callback = |event: PumpSwapEvent| match event {
|
||||
PumpSwapEvent::Buy(buy_event) => {
|
||||
println!("buy_event: {:?}", buy_event);
|
||||
}
|
||||
PumpSwapEvent::Sell(sell_event) => {
|
||||
println!("sell_event: {:?}", sell_event);
|
||||
}
|
||||
PumpSwapEvent::CreatePool(create_event) => {
|
||||
println!("create_event: {:?}", create_event);
|
||||
}
|
||||
PumpSwapEvent::Deposit(deposit_event) => {
|
||||
println!("deposit_event: {:?}", deposit_event);
|
||||
}
|
||||
PumpSwapEvent::Withdraw(withdraw_event) => {
|
||||
println!("withdraw_event: {:?}", withdraw_event);
|
||||
}
|
||||
PumpSwapEvent::Disable(disable_event) => {
|
||||
println!("disable_event: {:?}", disable_event);
|
||||
}
|
||||
PumpSwapEvent::UpdateAdmin(update_admin_event) => {
|
||||
println!("update_admin_event: {:?}", update_admin_event);
|
||||
}
|
||||
PumpSwapEvent::UpdateFeeConfig(update_fee_event) => {
|
||||
println!("update_fee_event: {:?}", update_fee_event);
|
||||
}
|
||||
PumpSwapEvent::Error(err) => {
|
||||
println!("error: {}", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Subscribe to PumpSwap events
|
||||
println!("Monitoring PumpSwap events, press Ctrl+C to stop...");
|
||||
client.subscribe_pumpswap(callback).await?;
|
||||
```
|
||||
|
||||
### 7. PumpSwap Trading Operations
|
||||
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
use solana_sdk::{pubkey::Pubkey, hash::Hash, signature::Keypair};
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use sol_trade_sdk::{common::{Cluster, PriorityFee}, SolanaTrade};
|
||||
|
||||
// Configure multiple swqos in single region, can send transactions concurrently
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let swqos_configs = vec![
|
||||
SwqosConfig::Jito(SwqosRegion::Frankfurt),
|
||||
SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
];
|
||||
|
||||
// Define sdk configuration
|
||||
let trade_config = TradeConfig {
|
||||
rpc_url: rpc_url.clone(),
|
||||
commitment: CommitmentConfig::confirmed(),
|
||||
priority_fee,
|
||||
swqos_configs,
|
||||
lookup_table_key: None,
|
||||
};
|
||||
|
||||
// Create SolanaTrade instance
|
||||
let payer = Keypair::from_base58_string("your_private_key");
|
||||
let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
|
||||
let creator = Pubkey::from_str("11111111111111111111111111111111")?; // dev account
|
||||
let buy_sol_cost = 500_000; // 0.0005 SOL
|
||||
let slippage_basis_points = Some(100);
|
||||
let rpc = RpcClient::new(cluster.rpc_url);
|
||||
let recent_blockhash = rpc.get_latest_blockhash().unwrap();
|
||||
let trade_platform = "pumpswap".to_string();
|
||||
let mint_pubkey = Pubkey::from_str("YOUR_TOKEN_MINT")?; // token mint
|
||||
|
||||
println!("Buying tokens from PumpSwap...");
|
||||
solana_trade_client
|
||||
.buy(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
buy_sol_cost,
|
||||
slippage_basis_points,
|
||||
recent_blockhash,
|
||||
None,
|
||||
trade_platform.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Sell 30% * amount_token quantity
|
||||
solana_trade_client
|
||||
.sell_by_percent(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
30, // percentage (30%)
|
||||
100, // total token amount
|
||||
recent_blockhash,
|
||||
trade_platform.clone(),
|
||||
)
|
||||
.await?;
|
||||
```
|
||||
|
||||
### 8. PumpSwap Pool Information
|
||||
|
||||
```rust
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
let pool_address = Pubkey::from_str("pool_address")?;
|
||||
|
||||
// Get current price from PumpSwap pool
|
||||
let price = solana_trade_client.get_current_price_with_pumpswap(&pool_address).await?;
|
||||
println!("PumpSwap pool price: {}", price);
|
||||
|
||||
// Get SOL reserves in PumpSwap pool
|
||||
let sol_reserves = solana_trade_client.get_real_sol_reserves_with_pumpswap(&pool_address).await?;
|
||||
println!("PumpSwap SOL reserves: {} lamports", sol_reserves);
|
||||
|
||||
// Get token balance in PumpSwap pool
|
||||
let token_balance = solana_trade_client.get_payer_token_balance_with_pumpswap(&pool_address).await?;
|
||||
println!("PumpSwap token balance: {}", token_balance);
|
||||
```
|
||||
|
||||
## Supported Trading Platforms
|
||||
|
||||
- **PumpFun**: Primary meme coin trading platform
|
||||
- **PumpSwap**: PumpFun's swap protocol
|
||||
- **Raydium**: Integrated Raydium DEX functionality
|
||||
- **Raydium Launchpad**: Raydium's token launch platform (Bonk.fun)
|
||||
|
||||
## MEV Protection Services
|
||||
|
||||
- **Jito**: High-performance block space
|
||||
- **Nextblock**: Fast transaction execution
|
||||
- **0slot**: Zero-latency transactions
|
||||
- **Nozomi**: MEV protection service
|
||||
- **NextBlock**: Fast transaction execution
|
||||
- **ZeroSlot**: Zero-latency transactions
|
||||
- **Temporal**: Time-sensitive transactions
|
||||
- **Bloxroute**: Blockchain network acceleration
|
||||
|
||||
## New Architecture Features
|
||||
|
||||
### Unified Parameter Structure
|
||||
|
||||
- **BuyParams**: Unified buy parameter structure
|
||||
- **SellParams**: Unified sell parameter structure
|
||||
- **Protocol-specific Parameters**: Each protocol has its own parameter structure (PumpFunParams, PumpSwapParams, RaydiumLaunchpadParams)
|
||||
|
||||
### Event Parsing System
|
||||
|
||||
- **Unified Event Interface**: All protocol events implement the UnifiedEvent trait
|
||||
- **Protocol-specific Events**: Each protocol has its own event types
|
||||
- **Event Factory**: Automatically identifies and parses events from different protocols
|
||||
|
||||
### Trading Engine
|
||||
|
||||
- **Unified Trading Interface**: All trading operations use the same methods
|
||||
- **Protocol Abstraction**: Supports trading operations across multiple protocols
|
||||
- **Concurrent Execution**: Supports sending transactions to multiple MEV services simultaneously
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── accounts/ # Account-related definitions
|
||||
├── common/ # Common utilities and tools
|
||||
├── constants/ # Constant definitions
|
||||
├── error/ # Error handling
|
||||
├── grpc/ # gRPC clients
|
||||
├── instruction/ # Instruction building
|
||||
├── pumpfun/ # PumpFun trading functionality
|
||||
├── pumpswap/ # PumpSwap trading functionality
|
||||
├── swqos/ # MEV service clients
|
||||
├── trading/ # Unified trading engine
|
||||
├── lib.rs # Main library file
|
||||
└── main.rs # Example program
|
||||
├── accounts/ # Account-related definitions
|
||||
├── common/ # Common functionality and tools
|
||||
├── constants/ # Constant definitions
|
||||
├── error/ # Error handling
|
||||
├── event_parser/ # Event parsing system
|
||||
│ ├── common/ # Common event parsing tools
|
||||
│ ├── core/ # Core parsing traits and interfaces
|
||||
│ ├── protocols/ # Protocol-specific parsers
|
||||
│ │ ├── pumpfun/ # PumpFun event parsing
|
||||
│ │ ├── pumpswap/ # PumpSwap event parsing
|
||||
│ │ └── raydium_launchpad/ # Raydium Launchpad event parsing
|
||||
│ └── factory.rs # Parser factory
|
||||
├── grpc/ # gRPC clients
|
||||
├── instruction/ # Instruction building
|
||||
├── protos/ # Protocol buffer definitions
|
||||
├── pumpfun/ # PumpFun trading functionality
|
||||
├── pumpswap/ # PumpSwap trading functionality
|
||||
├── raydium_launchpad/ # Raydium Launchpad trading functionality
|
||||
├── swqos/ # MEV service clients
|
||||
├── trading/ # Unified trading engine
|
||||
│ ├── common/ # Common trading tools
|
||||
│ ├── core/ # Core trading engine
|
||||
│ └── protocols/ # Protocol-specific trading implementations
|
||||
├── lib.rs # Main library file
|
||||
└── main.rs # Example program
|
||||
```
|
||||
|
||||
## License
|
||||
@@ -418,7 +542,7 @@ MIT License
|
||||
|
||||
## Contact
|
||||
|
||||
- Repository: https://github.com/0xfnzero/sol-trade-sdk
|
||||
- Project Repository: https://github.com/0xfnzero/sol-trade-sdk
|
||||
- Telegram Group: https://t.me/fnzero_group
|
||||
|
||||
## Important Notes
|
||||
|
||||
+473
-349
@@ -1,18 +1,18 @@
|
||||
# Sol Trade SDK
|
||||
|
||||
一个全面的Rust SDK,用于与Solana DEX交易程序进行无缝交互。此SDK提供强大的工具和接口集,将PumpFun和PumpSwap功能集成到您的应用程序中。
|
||||
一个全面的 Rust SDK,用于与 Solana DEX 交易程序进行无缝交互。此 SDK 提供强大的工具和接口集,将 PumpFun、PumpSwap 和 Raydium Launchpad(Bonk.fun)功能集成到您的应用程序中。
|
||||
|
||||
## 项目特性
|
||||
|
||||
1. **PumpFun交易**: 支持`购买`、`卖出`功能
|
||||
2. **PumpSwap交易**: 支持PumpSwap池的交易操作
|
||||
3. **Raydium交易**: 支持Raydium DEX的交易操作
|
||||
4. **日志订阅**: 订阅PumpFun、PumpSwap和Raydium程序的交易日志
|
||||
5. **Yellowstone gRPC**: 使用Yellowstone gRPC订阅程序日志
|
||||
6. **ShredStream支持**: 使用ShredStream订阅程序日志
|
||||
7. **多种MEV保护**: 支持Jito、Nextblock、0slot、Nozomi等服务
|
||||
8. **并发交易**: 同时使用多个MEV服务发送交易,最快的成功,其他失败
|
||||
9. **实时价格**: 获取代币实时价格和流动性信息
|
||||
1. **PumpFun 交易**: 支持`购买`、`卖出`功能
|
||||
2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作
|
||||
3. **Raydium 交易**: 支持 Raydium Launchpad(Bonk.fun)的交易操作
|
||||
4. **事件订阅**: 订阅 PumpFun、PumpSwap 和 Raydium Launchpad(Bonk.fun)程序的交易事件
|
||||
5. **Yellowstone gRPC**: 使用 Yellowstone gRPC 订阅程序事件
|
||||
6. **ShredStream 支持**: 使用 ShredStream 订阅程序事件
|
||||
7. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute 等服务
|
||||
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
|
||||
9. **统一交易接口**: 使用统一的参数结构进行交易操作
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -32,53 +32,425 @@ sol-trade-sdk = { path = "./sol-trade-sdk", version = "0.1.0" }
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 1. 日志订阅 - 监听代币交易
|
||||
### 1. 事件订阅 - 监听代币交易
|
||||
|
||||
#### 1.1 使用 Yellowstone gRPC 订阅事件
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{common::pumpfun::logs_events::PumpfunEvent, grpc::YellowstoneGrpc};
|
||||
use solana_sdk::signature::Keypair;
|
||||
|
||||
// 创建Yellowstone gRPC客户端
|
||||
let grpc_url = "https://solana-yellowstone-grpc.publicnode.com:443";
|
||||
let x_token = None; // 可选的认证令牌
|
||||
let client = YellowstoneGrpc::new(grpc_url.to_string(), x_token)?;
|
||||
|
||||
// 定义回调函数
|
||||
let callback = |event: PumpfunEvent| match event {
|
||||
PumpfunEvent::NewDevTrade(trade_info) => {
|
||||
println!("收到开发者交易事件: {:?}", trade_info);
|
||||
}
|
||||
PumpfunEvent::NewToken(token_info) => {
|
||||
println!("收到新代币事件: {:?}", token_info);
|
||||
}
|
||||
PumpfunEvent::NewUserTrade(trade_info) => {
|
||||
println!("收到用户交易事件: {:?}", trade_info);
|
||||
}
|
||||
PumpfunEvent::NewBotTrade(trade_info) => {
|
||||
println!("收到机器人交易事件: {:?}", trade_info);
|
||||
}
|
||||
PumpfunEvent::Error(err) => {
|
||||
println!("收到错误: {}", err);
|
||||
}
|
||||
use sol_trade_sdk::{
|
||||
event_parser::{
|
||||
protocols::{
|
||||
pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent},
|
||||
pumpswap::{
|
||||
PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent,
|
||||
PumpSwapSellEvent, PumpSwapWithdrawEvent,
|
||||
},
|
||||
raydium_launchpad::{RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent},
|
||||
},
|
||||
Protocol, UnifiedEvent,
|
||||
},
|
||||
grpc::YellowstoneGrpc,
|
||||
match_event,
|
||||
};
|
||||
|
||||
client.subscribe_pumpfun(callback, None).await?;
|
||||
async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 使用 GRPC 客户端订阅事件
|
||||
println!("正在订阅 GRPC 事件...");
|
||||
|
||||
let grpc = YellowstoneGrpc::new(
|
||||
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
||||
None,
|
||||
)?;
|
||||
|
||||
// 定义回调函数处理事件
|
||||
let callback = |event: Box<dyn UnifiedEvent>| {
|
||||
match_event!(event, {
|
||||
RaydiumLaunchpadPoolCreateEvent => |e: RaydiumLaunchpadPoolCreateEvent| {
|
||||
println!("RaydiumLaunchpadPoolCreateEvent: {:?}", e.base_mint_param.symbol);
|
||||
},
|
||||
RaydiumLaunchpadTradeEvent => |e: RaydiumLaunchpadTradeEvent| {
|
||||
println!("RaydiumLaunchpadTradeEvent: {:?}", e);
|
||||
},
|
||||
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
|
||||
println!("PumpFunTradeEvent: {:?}", e);
|
||||
},
|
||||
PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| {
|
||||
println!("PumpFunCreateTokenEvent: {:?}", e);
|
||||
},
|
||||
PumpSwapBuyEvent => |e: PumpSwapBuyEvent| {
|
||||
println!("Buy event: {:?}", e);
|
||||
},
|
||||
PumpSwapSellEvent => |e: PumpSwapSellEvent| {
|
||||
println!("Sell event: {:?}", e);
|
||||
},
|
||||
PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| {
|
||||
println!("CreatePool event: {:?}", e);
|
||||
},
|
||||
PumpSwapDepositEvent => |e: PumpSwapDepositEvent| {
|
||||
println!("Deposit event: {:?}", e);
|
||||
},
|
||||
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
|
||||
println!("Withdraw event: {:?}", e);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 订阅多个协议的事件
|
||||
println!("开始监听事件,按 Ctrl+C 停止...");
|
||||
let protocols = vec![
|
||||
Protocol::PumpFun,
|
||||
Protocol::PumpSwap,
|
||||
Protocol::RaydiumLaunchpad,
|
||||
];
|
||||
grpc.subscribe_events(protocols, None, None, None, callback)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 初始化SolanaTrade实例
|
||||
#### 1.2 使用 ShredStream 订阅事件
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::grpc::ShredStreamGrpc;
|
||||
|
||||
async fn test_shreds() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 使用 ShredStream 客户端订阅事件
|
||||
println!("正在订阅 ShredStream 事件...");
|
||||
|
||||
let shred_stream = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?;
|
||||
|
||||
// 定义回调函数处理事件(与上面相同)
|
||||
let callback = |event: Box<dyn UnifiedEvent>| {
|
||||
match_event!(event, {
|
||||
RaydiumLaunchpadPoolCreateEvent => |e: RaydiumLaunchpadPoolCreateEvent| {
|
||||
println!("RaydiumLaunchpadPoolCreateEvent: {:?}", e.base_mint_param.symbol);
|
||||
},
|
||||
RaydiumLaunchpadTradeEvent => |e: RaydiumLaunchpadTradeEvent| {
|
||||
println!("RaydiumLaunchpadTradeEvent: {:?}", e);
|
||||
},
|
||||
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
|
||||
println!("PumpFunTradeEvent: {:?}", e);
|
||||
},
|
||||
PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| {
|
||||
println!("PumpFunCreateTokenEvent: {:?}", e);
|
||||
},
|
||||
PumpSwapBuyEvent => |e: PumpSwapBuyEvent| {
|
||||
println!("Buy event: {:?}", e);
|
||||
},
|
||||
PumpSwapSellEvent => |e: PumpSwapSellEvent| {
|
||||
println!("Sell event: {:?}", e);
|
||||
},
|
||||
PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| {
|
||||
println!("CreatePool event: {:?}", e);
|
||||
},
|
||||
PumpSwapDepositEvent => |e: PumpSwapDepositEvent| {
|
||||
println!("Deposit event: {:?}", e);
|
||||
},
|
||||
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
|
||||
println!("Withdraw event: {:?}", e);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 订阅事件
|
||||
println!("开始监听事件,按 Ctrl+C 停止...");
|
||||
let protocols = vec![
|
||||
Protocol::PumpFun,
|
||||
Protocol::PumpSwap,
|
||||
Protocol::RaydiumLaunchpad,
|
||||
];
|
||||
shred_stream
|
||||
.shredstream_subscribe(protocols, None, callback)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 初始化 SolanaTrade 实例
|
||||
|
||||
```rust
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use sol_trade_sdk::{
|
||||
common::{AnyResult, PriorityFee, TradeConfig},
|
||||
swqos::{SwqosConfig, SwqosRegion, SwqosType},
|
||||
common::{AnyResult, PriorityFee, TradeConfig},
|
||||
swqos::{SwqosConfig, SwqosRegion},
|
||||
SolanaTrade
|
||||
};
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Keypair};
|
||||
|
||||
// 配置优先费用
|
||||
// 创建交易者账户
|
||||
let payer = Keypair::new();
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
|
||||
// 配置多个MEV服务,支持并发交易
|
||||
let swqos_configs = vec![
|
||||
SwqosConfig::Jito(SwqosRegion::Frankfurt),
|
||||
SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
];
|
||||
|
||||
// 定义交易配置
|
||||
let trade_config = TradeConfig {
|
||||
rpc_url: rpc_url.clone(),
|
||||
commitment: CommitmentConfig::confirmed(),
|
||||
priority_fee: PriorityFee::default(),
|
||||
swqos_configs,
|
||||
lookup_table_key: None,
|
||||
};
|
||||
|
||||
// 创建SolanaTrade实例
|
||||
let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
```
|
||||
|
||||
### 3. PumpFun 交易操作
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{
|
||||
accounts::BondingCurveAccount,
|
||||
constants::{pumpfun::global_constants::TOKEN_TOTAL_SUPPLY, trade_type},
|
||||
pumpfun::common::get_bonding_curve_account_v2,
|
||||
trading::{
|
||||
core::params::{PumpFunParams, PumpFunSellParams},
|
||||
BuyParams, SellParams,
|
||||
},
|
||||
};
|
||||
|
||||
async fn test_pumpfun() -> AnyResult<()> {
|
||||
// 基本参数设置
|
||||
let creator = Pubkey::from_str("xxxxxx")?; // 开发者账户
|
||||
let mint_pubkey = Pubkey::from_str("xxxxxx")?; // 代币地址
|
||||
let buy_sol_cost = 100_000; // 0.0001 SOL
|
||||
let slippage_basis_points = Some(100);
|
||||
let rpc = RpcClient::new(rpc_url);
|
||||
let recent_blockhash = rpc.get_latest_blockhash().unwrap();
|
||||
|
||||
println!("Buying tokens from PumpFun...");
|
||||
|
||||
// 获取bonding curve信息
|
||||
let (bonding_curve, bonding_curve_pda) =
|
||||
get_bonding_curve_account_v2(&solana_trade_client.rpc, &mint_pubkey).await?;
|
||||
|
||||
let bonding_curve = BondingCurveAccount {
|
||||
discriminator: bonding_curve.discriminator,
|
||||
account: bonding_curve_pda,
|
||||
virtual_token_reserves: bonding_curve.virtual_token_reserves,
|
||||
virtual_sol_reserves: bonding_curve.virtual_sol_reserves,
|
||||
real_token_reserves: bonding_curve.real_token_reserves,
|
||||
real_sol_reserves: bonding_curve.real_sol_reserves,
|
||||
token_total_supply: TOKEN_TOTAL_SUPPLY,
|
||||
complete: false,
|
||||
creator: creator,
|
||||
};
|
||||
|
||||
// 购买操作
|
||||
let buy_protocol_params = PumpFunParams {
|
||||
trade_type: trade_type::COPY_BUY.to_string(),
|
||||
bonding_curve: Some(Arc::new(bonding_curve)),
|
||||
};
|
||||
|
||||
let buy_params = BuyParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint_pubkey,
|
||||
creator: creator,
|
||||
amount_sol: buy_sol_cost,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
data_size_limit: 0,
|
||||
protocol_params: Box::new(buy_protocol_params.clone()),
|
||||
};
|
||||
|
||||
// 使用MEV保护的购买
|
||||
let buy_with_tip_params = buy_params
|
||||
.clone()
|
||||
.with_tip(solana_trade_client.swqos_clients.clone());
|
||||
|
||||
solana_trade_client
|
||||
.buy_use_buy_params(buy_with_tip_params, None)
|
||||
.await?;
|
||||
|
||||
// 卖出操作
|
||||
println!("Selling tokens from PumpFun...");
|
||||
let sell_protocol_params = PumpFunSellParams {};
|
||||
let amount_token = 1000000; // 写上真实的代币数量
|
||||
|
||||
let sell_params = SellParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint_pubkey,
|
||||
creator: creator,
|
||||
amount_token: Some(amount_token),
|
||||
slippage_basis_points: None,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
protocol_params: Box::new(sell_protocol_params.clone()),
|
||||
};
|
||||
|
||||
solana_trade_client
|
||||
.sell_by_amount_use_sell_params(sell_params)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 4. PumpSwap 交易操作
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::trading::core::params::PumpSwapParams;
|
||||
|
||||
async fn test_pumpswap() -> AnyResult<()> {
|
||||
// 基本参数设置
|
||||
let creator = Pubkey::from_str("11111111111111111111111111111111")?; // 开发者账户
|
||||
let mint_pubkey = Pubkey::from_str("2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv")?; // 代币地址
|
||||
let buy_sol_cost = 100_000; // 0.0001 SOL
|
||||
let slippage_basis_points = Some(100);
|
||||
let rpc = RpcClient::new(rpc_url);
|
||||
let recent_blockhash = rpc.get_latest_blockhash().unwrap();
|
||||
|
||||
println!("Buying tokens from PumpSwap...");
|
||||
|
||||
// PumpSwap参数配置
|
||||
let protocol_params = PumpSwapParams {
|
||||
pool: None,
|
||||
pool_base_token_account: None,
|
||||
pool_quote_token_account: None,
|
||||
user_base_token_account: None,
|
||||
user_quote_token_account: None,
|
||||
auto_handle_wsol: true,
|
||||
};
|
||||
|
||||
// 购买操作
|
||||
let buy_params = BuyParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint_pubkey,
|
||||
creator: creator,
|
||||
amount_sol: buy_sol_cost,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
data_size_limit: 0,
|
||||
protocol_params: Box::new(protocol_params.clone()),
|
||||
};
|
||||
|
||||
let buy_with_tip_params = buy_params
|
||||
.clone()
|
||||
.with_tip(solana_trade_client.swqos_clients.clone());
|
||||
|
||||
solana_trade_client
|
||||
.buy_use_buy_params(buy_with_tip_params, None)
|
||||
.await?;
|
||||
|
||||
// 卖出操作
|
||||
println!("Selling tokens from PumpSwap...");
|
||||
let amount_token = 1000000; // 写上真实的代币数量
|
||||
|
||||
let sell_params = SellParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint_pubkey,
|
||||
creator: creator,
|
||||
amount_token: Some(amount_token),
|
||||
slippage_basis_points: None,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
protocol_params: Box::new(protocol_params.clone()),
|
||||
};
|
||||
|
||||
solana_trade_client
|
||||
.sell_by_amount_use_sell_params(sell_params)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Raydium Launchpad 交易操作
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::trading::core::params::RaydiumLaunchpadParams;
|
||||
|
||||
async fn test_raydium_launchpad() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 基本参数设置
|
||||
let amount = 100_000; // 0.0001 SOL
|
||||
let mint = Pubkey::from_str("xxxxxxx")?;
|
||||
let recent_blockhash = solana_trade_client.rpc.get_latest_blockhash().await?;
|
||||
|
||||
// Raydium Launchpad参数配置
|
||||
let raydium_launchpad_params = RaydiumLaunchpadParams {
|
||||
virtual_base: None,
|
||||
virtual_quote: None,
|
||||
real_base_before: None,
|
||||
real_quote_before: None,
|
||||
auto_handle_wsol: true,
|
||||
};
|
||||
|
||||
println!("Buying tokens from Raydium Launchpad...");
|
||||
|
||||
// 购买操作
|
||||
let buy_params = BuyParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint,
|
||||
creator: Pubkey::default(),
|
||||
amount_sol: amount,
|
||||
slippage_basis_points: None,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
data_size_limit: 0,
|
||||
protocol_params: Box::new(raydium_launchpad_params.clone()),
|
||||
};
|
||||
|
||||
let buy_with_tip_params = buy_params
|
||||
.clone()
|
||||
.with_tip(solana_trade_client.swqos_clients.clone());
|
||||
|
||||
solana_trade_client
|
||||
.buy_use_buy_params(buy_with_tip_params, None)
|
||||
.await?;
|
||||
|
||||
// 卖出操作
|
||||
println!("Selling tokens from Raydium Launchpad...");
|
||||
|
||||
let sell_params = SellParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint,
|
||||
creator: Pubkey::default(),
|
||||
amount_token: None,
|
||||
slippage_basis_points: None,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
protocol_params: Box::new(raydium_launchpad_params.clone()),
|
||||
};
|
||||
|
||||
solana_trade_client
|
||||
.sell_by_amount_use_sell_params(sell_params)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 自定义优先费用配置
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::common::PriorityFee;
|
||||
|
||||
// 自定义优先费用配置
|
||||
let priority_fee = PriorityFee {
|
||||
unit_limit: 190000,
|
||||
unit_price: 1000000,
|
||||
@@ -89,342 +461,94 @@ let priority_fee = PriorityFee {
|
||||
sell_tip_fee: 0.0001,
|
||||
};
|
||||
|
||||
// 单区域配置多个swqos,可同时发送交易
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let swqos_configs = vec![
|
||||
SwqosConfig::Jito(SwqosRegion::Frankfurt),
|
||||
SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
];
|
||||
|
||||
// 定义sdk配置参数
|
||||
// 在TradeConfig中使用自定义优先费用
|
||||
let trade_config = TradeConfig {
|
||||
rpc_url: rpc_url.clone(),
|
||||
commitment: CommitmentConfig::confirmed(),
|
||||
priority_fee,
|
||||
priority_fee, // 使用自定义优先费用
|
||||
swqos_configs,
|
||||
lookup_table_key: None,
|
||||
};
|
||||
|
||||
// 创建SolanaTrade实例
|
||||
let payer = Keypair::from_base58_string("your_private_key");
|
||||
let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
```
|
||||
|
||||
### 3. 购买代币
|
||||
|
||||
### 3.1 购买代币 --- 狙击
|
||||
```rust
|
||||
use solana_sdk::{pubkey::Pubkey, hash::Hash};
|
||||
use std::sync::Arc;
|
||||
use sol_trade_sdk::accounts::BondingCurveAccount;
|
||||
|
||||
let mint_pubkey = Pubkey::from_str("代币地址")?;
|
||||
let creator = Pubkey::from_str("创建者地址")?;
|
||||
let recent_blockhash = Hash::default(); // 获取最新区块哈希
|
||||
let buy_sol_cost = 50000; // 0.00005 SOL
|
||||
let slippage_basis_points = Some(100); // 1%
|
||||
|
||||
// 狙击购买(新代币上线时快速购买)
|
||||
let dev_buy_token = 100_000; // 测试值
|
||||
let dev_cost_sol = 10_000; // 测试值
|
||||
let bonding_curve = BondingCurveAccount::new(&mint_pubkey, dev_buy_token, dev_cost_sol, creator);
|
||||
|
||||
solana_trade_client.sniper_buy(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
buy_sol_cost,
|
||||
slippage_basis_points,
|
||||
recent_blockhash,
|
||||
Some(Arc::new(bonding_curve)),
|
||||
).await?;
|
||||
|
||||
// 使用小费进行MEV保护的购买
|
||||
solana_trade_client.sniper_buy_with_tip(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
buy_sol_cost,
|
||||
slippage_basis_points,
|
||||
recent_blockhash,
|
||||
Some(Arc::new(bonding_curve)),
|
||||
None, // 自定义小费
|
||||
).await?;
|
||||
```
|
||||
|
||||
### 3.2 购买代币 --- 跟单
|
||||
```rust
|
||||
use solana_sdk::{pubkey::Pubkey, hash::Hash};
|
||||
use std::sync::Arc;
|
||||
use sol_trade_sdk::accounts::BondingCurveAccount;
|
||||
use sol_trade_sdk::{constants::{pumpfun::global_constants::TOKEN_TOTAL_SUPPLY, trade_type::COPY_BUY}, pumpfun::common::get_bonding_curve_pda};
|
||||
|
||||
let mint_pubkey = Pubkey::from_str("代币地址")?;
|
||||
let creator = Pubkey::from_str("创建者地址")?;
|
||||
let recent_blockhash = Hash::default(); // 获取最新区块哈希
|
||||
let buy_sol_cost = 50000; // 0.00005 SOL
|
||||
let slippage_basis_points = Some(100); // 1%
|
||||
|
||||
// 跟单购买
|
||||
let dev_buy_token = 100_000; // 测试值
|
||||
let dev_cost_sol = 10_000; // 测试值
|
||||
// trade_info来自pumpfun解析出来的数据,可以参考上面 1. 日志订阅
|
||||
let bonding_curve = Some(Arc::new(BondingCurveAccount {
|
||||
discriminator: 0,
|
||||
account: get_bonding_curve_pda(&trade_info.mint).unwrap(),
|
||||
virtual_token_reserves: trade_info.virtual_token_reserves,
|
||||
virtual_sol_reserves: trade_info.virtual_sol_reserves,
|
||||
real_token_reserves: trade_info.real_token_reserves,
|
||||
real_sol_reserves: trade_info.real_sol_reserves,
|
||||
token_total_supply: TOKEN_TOTAL_SUPPLY,
|
||||
complete: false,
|
||||
creator: Pubkey::from_str(&creator).unwrap(),
|
||||
}));
|
||||
|
||||
solana_trade_client.buy(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
buy_sol_cost,
|
||||
slippage_basis_points,
|
||||
recent_blockhash,
|
||||
Some(Arc::new(bonding_curve)),
|
||||
"pumpfun".to_string(),
|
||||
).await?;
|
||||
|
||||
// 使用小费进行MEV保护的购买
|
||||
solana_trade_client.buy_with_tip(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
buy_sol_cost,
|
||||
slippage_basis_points,
|
||||
recent_blockhash,
|
||||
Some(Arc::new(bonding_curve)),
|
||||
"pumpfun".to_string(),
|
||||
None, // 自定义小费
|
||||
).await?;
|
||||
```
|
||||
|
||||
### 4. 卖出代币
|
||||
|
||||
```rust
|
||||
// 按数量卖出
|
||||
solana_trade_client.sell_by_amount_with_tip(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
1000000, // 代币数量
|
||||
recent_blockhash,
|
||||
"pumpfun".to_string(), // 交易平台
|
||||
).await?;
|
||||
|
||||
// 按百分比卖出
|
||||
solana_trade_client.sell_by_percent_with_tip(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
50, // 百分比 (50%)
|
||||
2000000, // 总代币数量
|
||||
recent_blockhash,
|
||||
"pumpfun".to_string(), // 交易平台
|
||||
).await?;
|
||||
```
|
||||
|
||||
### 5. 获取价格和余额信息
|
||||
|
||||
```rust
|
||||
// 获取代币当前价格
|
||||
let price = solana_trade_client.get_current_price(&mint_pubkey).await?;
|
||||
println!("当前价格: {}", price);
|
||||
|
||||
// 获取SOL余额
|
||||
let sol_balance = solana_trade_client.get_payer_sol_balance().await?;
|
||||
println!("SOL余额: {} lamports", sol_balance);
|
||||
|
||||
// 获取代币余额
|
||||
let token_balance = solana_trade_client.get_payer_token_balance(&mint_pubkey).await?;
|
||||
println!("代币余额: {}", token_balance);
|
||||
|
||||
// 获取流动性信息
|
||||
let sol_reserves = solana_trade_client.get_real_sol_reserves(&mint_pubkey).await?;
|
||||
println!("SOL储备: {} lamports", sol_reserves);
|
||||
```
|
||||
|
||||
### 6. PumpSwap订阅 - 监听AMM事件
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{common::pumpswap::logs_events::PumpSwapEvent, grpc::YellowstoneGrpc};
|
||||
|
||||
// 创建Yellowstone gRPC客户端
|
||||
let grpc_url = "https://solana-yellowstone-grpc.publicnode.com:443";
|
||||
let x_token = None;
|
||||
let client = YellowstoneGrpc::new(grpc_url.to_string(), x_token)?;
|
||||
|
||||
// 定义PumpSwap事件的回调函数
|
||||
let callback = |event: PumpSwapEvent| match event {
|
||||
PumpSwapEvent::Buy(buy_event) => {
|
||||
println!("buy_event: {:?}", buy_event);
|
||||
}
|
||||
PumpSwapEvent::Sell(sell_event) => {
|
||||
println!("sell_event: {:?}", sell_event);
|
||||
}
|
||||
PumpSwapEvent::CreatePool(create_event) => {
|
||||
println!("create_event: {:?}", create_event);
|
||||
}
|
||||
PumpSwapEvent::Deposit(deposit_event) => {
|
||||
println!("deposit_event: {:?}", deposit_event);
|
||||
}
|
||||
PumpSwapEvent::Withdraw(withdraw_event) => {
|
||||
println!("withdraw_event: {:?}", withdraw_event);
|
||||
}
|
||||
PumpSwapEvent::Disable(disable_event) => {
|
||||
println!("disable_event: {:?}", disable_event);
|
||||
}
|
||||
PumpSwapEvent::UpdateAdmin(update_admin_event) => {
|
||||
println!("update_admin_event: {:?}", update_admin_event);
|
||||
}
|
||||
PumpSwapEvent::UpdateFeeConfig(update_fee_event) => {
|
||||
println!("update_fee_event: {:?}", update_fee_event);
|
||||
}
|
||||
PumpSwapEvent::Error(err) => {
|
||||
println!("error: {}", err);
|
||||
}
|
||||
};
|
||||
|
||||
// 订阅PumpSwap事件
|
||||
println!("开始监听PumpSwap事件,按Ctrl+C停止...");
|
||||
client.subscribe_pumpswap(callback).await?;
|
||||
```
|
||||
|
||||
### 7. PumpSwap交易操作
|
||||
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
use solana_sdk::{pubkey::Pubkey, hash::Hash, signature::Keypair};
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use sol_trade_sdk::{common::{Cluster, PriorityFee}, SolanaTrade};
|
||||
|
||||
// 单区域配置多个swqos,可同时发送交易
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let swqos_configs = vec![
|
||||
SwqosConfig::Jito(SwqosRegion::Frankfurt),
|
||||
SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
];
|
||||
|
||||
// 定义sdk配置参数
|
||||
let trade_config = TradeConfig {
|
||||
rpc_url: rpc_url.clone(),
|
||||
commitment: CommitmentConfig::confirmed(),
|
||||
priority_fee,
|
||||
swqos_configs,
|
||||
lookup_table_key: None,
|
||||
};
|
||||
|
||||
// 创建SolanaTrade实例
|
||||
let payer = Keypair::from_base58_string("your_private_key");
|
||||
let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
|
||||
let creator = Pubkey::from_str("11111111111111111111111111111111")?; // 开发者账户
|
||||
let buy_sol_cost = 500_000; // 0.0005 SOL
|
||||
let slippage_basis_points = Some(100);
|
||||
let rpc = RpcClient::new(cluster.rpc_url);
|
||||
let recent_blockhash = rpc.get_latest_blockhash().unwrap();
|
||||
let trade_platform = "pumpswap".to_string();
|
||||
let mint_pubkey = Pubkey::from_str("您的代币铸造地址")?; // 代币铸造地址
|
||||
|
||||
println!("从PumpSwap购买代币...");
|
||||
solana_trade_client
|
||||
.buy(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
buy_sol_cost,
|
||||
slippage_basis_points,
|
||||
recent_blockhash,
|
||||
None,
|
||||
trade_platform.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 卖出30%的代币数量
|
||||
solana_trade_client
|
||||
.sell_by_percent(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
30, // 百分比 (30%)
|
||||
100, // 总代币数量
|
||||
recent_blockhash,
|
||||
trade_platform.clone(),
|
||||
)
|
||||
.await?;
|
||||
```
|
||||
|
||||
### 8. PumpSwap池信息
|
||||
|
||||
```rust
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
let pool_address = Pubkey::from_str("池地址")?;
|
||||
|
||||
// 从PumpSwap池获取当前价格
|
||||
let price = solana_trade_client.get_current_price_with_pumpswap(&pool_address).await?;
|
||||
println!("PumpSwap池价格: {}", price);
|
||||
|
||||
// 获取PumpSwap池中的SOL储备
|
||||
let sol_reserves = solana_trade_client.get_real_sol_reserves_with_pumpswap(&pool_address).await?;
|
||||
println!("PumpSwap SOL储备: {} lamports", sol_reserves);
|
||||
|
||||
// 获取PumpSwap池中的代币余额
|
||||
let token_balance = solana_trade_client.get_payer_token_balance_with_pumpswap(&pool_address).await?;
|
||||
println!("PumpSwap代币余额: {}", token_balance);
|
||||
```
|
||||
|
||||
## 支持的交易平台
|
||||
|
||||
- **PumpFun**: 主要的meme币交易平台
|
||||
- **PumpSwap**: PumpFun的交换协议
|
||||
- **Raydium**: 集成Raydium DEX功能
|
||||
- **PumpFun**: 主要的 meme 币交易平台
|
||||
- **PumpSwap**: PumpFun 的交换协议
|
||||
- **Raydium Launchpad**: Raydium 的代币发行平台(Bonk.fun)
|
||||
|
||||
## MEV保护服务
|
||||
## MEV 保护服务
|
||||
|
||||
- **Jito**: 高性能区块空间
|
||||
- **Nextblock**: 快速交易执行
|
||||
- **0slot**: 零延迟交易
|
||||
- **Nozomi**: MEV保护服务
|
||||
- **NextBlock**: 快速交易执行
|
||||
- **ZeroSlot**: 零延迟交易
|
||||
- **Temporal**: 时间敏感交易
|
||||
- **Bloxroute**: 区块链网络加速
|
||||
|
||||
## 新架构特性
|
||||
|
||||
### 统一参数结构
|
||||
|
||||
- **BuyParams**: 统一的购买参数结构
|
||||
- **SellParams**: 统一的卖出参数结构
|
||||
- **协议特定参数**: 每个协议都有自己的参数结构(PumpFunParams、PumpSwapParams、RaydiumLaunchpadParams)
|
||||
|
||||
### 事件解析系统
|
||||
|
||||
- **统一事件接口**: 所有协议事件都实现 UnifiedEvent 特征
|
||||
- **协议特定事件**: 每个协议都有自己的事件类型
|
||||
- **事件工厂**: 自动识别和解析不同协议的事件
|
||||
|
||||
### 交易引擎
|
||||
|
||||
- **统一交易接口**: 所有交易操作都使用相同的方法
|
||||
- **协议抽象**: 支持多个协议的交易操作
|
||||
- **并发执行**: 支持同时向多个 MEV 服务发送交易
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── accounts/ # 账户相关定义
|
||||
├── common/ # 通用功能和工具
|
||||
├── constants/ # 常量定义
|
||||
├── error/ # 错误处理
|
||||
├── grpc/ # gRPC客户端
|
||||
├── instruction/ # 指令构建
|
||||
├── pumpfun/ # PumpFun交易功能
|
||||
├── pumpswap/ # PumpSwap交易功能
|
||||
├── swqos/ # MEV服务客户端
|
||||
├── trading/ # 统一交易引擎
|
||||
├── lib.rs # 主库文件
|
||||
└── main.rs # 示例程序
|
||||
├── accounts/ # 账户相关定义
|
||||
├── common/ # 通用功能和工具
|
||||
├── constants/ # 常量定义
|
||||
├── error/ # 错误处理
|
||||
├── event_parser/ # 事件解析系统
|
||||
│ ├── common/ # 通用事件解析工具
|
||||
│ ├── core/ # 核心解析特征和接口
|
||||
│ ├── protocols/ # 协议特定解析器
|
||||
│ │ ├── pumpfun/ # PumpFun事件解析
|
||||
│ │ ├── pumpswap/ # PumpSwap事件解析
|
||||
│ │ └── raydium_launchpad/ # Raydium Launchpad事件解析
|
||||
│ └── factory.rs # 解析器工厂
|
||||
├── grpc/ # gRPC客户端
|
||||
├── instruction/ # 指令构建
|
||||
├── protos/ # 协议缓冲区定义
|
||||
├── pumpfun/ # PumpFun交易功能
|
||||
├── pumpswap/ # PumpSwap交易功能
|
||||
├── raydium_launchpad/ # Raydium Launchpad交易功能
|
||||
├── swqos/ # MEV服务客户端
|
||||
├── trading/ # 统一交易引擎
|
||||
│ ├── common/ # 通用交易工具
|
||||
│ ├── core/ # 核心交易引擎
|
||||
│ └── protocols/ # 协议特定交易实现
|
||||
├── lib.rs # 主库文件
|
||||
└── main.rs # 示例程序
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT许可证
|
||||
MIT 许可证
|
||||
|
||||
## 联系方式
|
||||
|
||||
- 项目仓库: https://github.com/0xfnzero/sol-trade-sdk
|
||||
- Telegram群组: https://t.me/fnzero_group
|
||||
- Telegram 群组: https://t.me/fnzero_group
|
||||
|
||||
## 重要注意事项
|
||||
|
||||
1. 在主网使用前请充分测试
|
||||
2. 正确设置私钥和API令牌
|
||||
2. 正确设置私钥和 API 令牌
|
||||
3. 注意滑点设置避免交易失败
|
||||
4. 监控余额和交易费用
|
||||
5. 遵循相关法律法规
|
||||
@@ -432,4 +556,4 @@ MIT许可证
|
||||
## 语言版本
|
||||
|
||||
- [English](README.md)
|
||||
- [中文](README_CN.md)
|
||||
- [中文](README_CN.md)
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
pub mod pumpfun;
|
||||
pub mod pumpswap;
|
||||
pub mod raydium;
|
||||
pub mod address_lookup;
|
||||
pub mod nonce_cache;
|
||||
pub mod tip_cache;
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction};
|
||||
|
||||
use crate::error::{ClientError, ClientResult};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DexInstruction {
|
||||
CreateToken(CreateTokenInfo),
|
||||
UserTrade(TradeInfo),
|
||||
BotTrade(TradeInfo),
|
||||
Tip(TipInfo),
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
|
||||
pub struct CreateTokenInfo {
|
||||
pub slot: u64,
|
||||
pub name: String,
|
||||
pub symbol: String,
|
||||
pub uri: String,
|
||||
pub mint: Pubkey,
|
||||
pub bonding_curve: Pubkey,
|
||||
pub user: Pubkey,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
|
||||
pub struct TradeInfo {
|
||||
pub slot: u64,
|
||||
pub mint: Pubkey,
|
||||
pub sol_amount: u64,
|
||||
pub token_amount: u64,
|
||||
pub is_buy: bool,
|
||||
pub user: Pubkey,
|
||||
pub timestamp: i64,
|
||||
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 TipInfo {
|
||||
pub slot: u64,
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
|
||||
pub struct CompleteInfo {
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct TransferInfo {
|
||||
pub slot: u64,
|
||||
pub signature: String,
|
||||
pub tx: Option<VersionedTransaction>,
|
||||
}
|
||||
|
||||
pub trait EventTrait: Sized + std::fmt::Debug {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self>;
|
||||
}
|
||||
|
||||
impl EventTrait for CreateTokenInfo {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self> {
|
||||
CreateTokenInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventTrait for TradeInfo {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self> {
|
||||
TradeInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventTrait for CompleteInfo {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self> {
|
||||
CompleteInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventTrait for SwapBaseInLog {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self> {
|
||||
SwapBaseInLog::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine;
|
||||
use regex::Regex;
|
||||
use crate::common::pumpfun::logs_data::{CreateTokenInfo, TradeInfo, EventTrait, TransferInfo, TipInfo};
|
||||
|
||||
pub const PROGRAM_DATA: &str = "Program data: ";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PumpfunEvent {
|
||||
NewToken(CreateTokenInfo),
|
||||
NewDevTrade(TradeInfo),
|
||||
NewUserTrade(TradeInfo),
|
||||
NewBotTrade(TradeInfo),
|
||||
// NewTip(TipInfo),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DexEvent {
|
||||
NewToken(CreateTokenInfo),
|
||||
NewUserTrade(TradeInfo),
|
||||
NewBotTrade(TradeInfo),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SystemEvent {
|
||||
NewTransfer(TransferInfo),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
// #[derive(Debug, Clone, Copy)]
|
||||
// pub struct PumpEvent {}
|
||||
|
||||
impl PumpfunEvent {
|
||||
pub fn parse_logs(logs: &Vec<String>) -> (Option<CreateTokenInfo>, Option<TradeInfo>) {
|
||||
let mut create_info: Option<CreateTokenInfo> = None;
|
||||
let mut trade_info: Option<TradeInfo> = 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 create_info.is_none() {
|
||||
if let Ok(e) = CreateTokenInfo::from_bytes(slice) {
|
||||
create_info = Some(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if trade_info.is_none() {
|
||||
if let Ok(e) = TradeInfo::from_bytes(slice) {
|
||||
trade_info = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(create_info, trade_info)
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
use crate::common::pumpfun::logs_data::DexInstruction;
|
||||
use crate::common::pumpfun::logs_parser::{parse_create_token_data, parse_trade_data, parse_instruction_create_token_data, parse_instruction_trade_data};
|
||||
use crate::error::ClientResult;
|
||||
pub struct LogFilter;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::str::FromStr;
|
||||
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
|
||||
impl LogFilter {
|
||||
const PROGRAM_ID: &'static str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
|
||||
|
||||
/// Parse transaction logs and return instruction type and data
|
||||
pub fn parse_compiled_instruction(
|
||||
versioned_tx: VersionedTransaction,
|
||||
bot_wallet: Option<Pubkey>) -> ClientResult<Vec<DexInstruction>> {
|
||||
let compiled_instructions = versioned_tx.message.instructions();
|
||||
let accounts = versioned_tx.message.static_account_keys();
|
||||
let program_id = Pubkey::from_str(Self::PROGRAM_ID).unwrap_or_default();
|
||||
let pump_index = accounts.iter().position(|key| key == &program_id);
|
||||
let mut instructions: Vec<DexInstruction> = Vec::new();
|
||||
if let Some(index) = pump_index {
|
||||
for instruction in compiled_instructions {
|
||||
if instruction.program_id_index as usize == index {
|
||||
let all_accounts_valid = instruction.accounts.iter()
|
||||
.all(|&acc_idx| (acc_idx as usize) < accounts.len());
|
||||
if !all_accounts_valid {
|
||||
continue;
|
||||
}
|
||||
match instruction.data.first() {
|
||||
// create
|
||||
Some(&24) => {
|
||||
if let Ok(token_info) = parse_instruction_create_token_data(instruction, accounts) {
|
||||
instructions.push(DexInstruction::CreateToken(token_info));
|
||||
};
|
||||
}
|
||||
// buy
|
||||
Some(&102) if instruction.data.len() == 24 && instruction.accounts.len() >= 12 => {
|
||||
if let Ok(trade_info) = parse_instruction_trade_data(instruction, accounts, true) {
|
||||
if let Some(bot_wallet_pubkey) = bot_wallet {
|
||||
if trade_info.user.to_string() == 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));
|
||||
}
|
||||
};
|
||||
}
|
||||
// sell
|
||||
Some(&51) if instruction.data.len() == 24 && instruction.accounts.len() >= 12 => {
|
||||
if let Ok(trade_info) = parse_instruction_trade_data(instruction, accounts, false) {
|
||||
if let Some(bot_wallet_pubkey) = bot_wallet {
|
||||
if trade_info.user.to_string() == 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)
|
||||
}
|
||||
|
||||
|
||||
/// 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.to_string() == 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)
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
|
||||
use crate::error::{ClientError, ClientResult};
|
||||
use crate::common::pumpfun::{
|
||||
logs_data::{DexInstruction, CreateTokenInfo, TradeInfo},
|
||||
logs_filters::LogFilter
|
||||
};
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::instruction::CompiledInstruction;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
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 {
|
||||
slot: 0,
|
||||
name,
|
||||
symbol,
|
||||
uri,
|
||||
mint: Pubkey::from_str(&mint).unwrap(),
|
||||
bonding_curve: Pubkey::from_str(&bonding_curve).unwrap(),
|
||||
user: Pubkey::from_str(&user).unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
slot: 0,
|
||||
mint: Pubkey::from_str(&mint).unwrap(),
|
||||
sol_amount,
|
||||
token_amount,
|
||||
is_buy,
|
||||
user: Pubkey::from_str(&user).unwrap(),
|
||||
timestamp,
|
||||
virtual_sol_reserves,
|
||||
virtual_token_reserves,
|
||||
real_sol_reserves,
|
||||
real_token_reserves,
|
||||
})
|
||||
}
|
||||
|
||||
fn current_timestamp_millis() -> i64 {
|
||||
let duration = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards");
|
||||
|
||||
duration.as_millis() as i64
|
||||
}
|
||||
|
||||
pub fn parse_instruction_create_token_data(instruction: &CompiledInstruction, accounts: &[Pubkey]) -> ClientResult<CreateTokenInfo> {
|
||||
let data = instruction.data.clone();
|
||||
let mut offset = 0;
|
||||
offset += 8;
|
||||
let len1 = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
|
||||
offset += 4;
|
||||
let name = String::from_utf8_lossy(&data[offset..offset + len1]);
|
||||
offset += len1;
|
||||
let len2 = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
|
||||
offset += 4;
|
||||
let symbol = String::from_utf8_lossy(&data[offset..offset + len2]);
|
||||
offset += len2;
|
||||
let _flag = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
|
||||
offset += 4;
|
||||
let hash_start = data.len() - 32;
|
||||
let ipfs_bytes = &data[offset..hash_start];
|
||||
let uri = String::from_utf8_lossy(ipfs_bytes);
|
||||
let mint = accounts[instruction.accounts[0] as usize];
|
||||
let user = accounts[instruction.accounts[7] as usize];
|
||||
let bonding_curve= accounts[instruction.accounts[2] as usize];
|
||||
Ok(CreateTokenInfo {
|
||||
slot: 0,
|
||||
name: name.to_string(),
|
||||
symbol: symbol.to_string(),
|
||||
uri: uri.to_string(),
|
||||
mint,
|
||||
bonding_curve,
|
||||
user,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_instruction_trade_data(instruction: &CompiledInstruction, accounts: &[Pubkey], is_buy: bool) -> ClientResult<TradeInfo> {
|
||||
let data = instruction.data.clone();
|
||||
let amount = u64::from_le_bytes(data[8..16].try_into().unwrap());
|
||||
let max_sol_cost_or_min_sol_output = u64::from_le_bytes(data[16..24].try_into().unwrap());
|
||||
let user = accounts[instruction.accounts[6] as usize];
|
||||
let mint = accounts[instruction.accounts[2] as usize];
|
||||
Ok(TradeInfo {
|
||||
slot: 0,
|
||||
mint,
|
||||
sol_amount: max_sol_cost_or_min_sol_output,
|
||||
token_amount: amount,
|
||||
is_buy,
|
||||
user,
|
||||
timestamp: current_timestamp_millis(),
|
||||
virtual_sol_reserves: 0,
|
||||
virtual_token_reserves: 0,
|
||||
real_sol_reserves: 0,
|
||||
real_token_reserves: 0,
|
||||
})
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
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, common::pumpfun::{
|
||||
logs_data::DexInstruction, logs_events::DexEvent, logs_filters::LogFilter
|
||||
}};
|
||||
|
||||
use super::logs_events::PumpfunEvent;
|
||||
|
||||
/// 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(PumpfunEvent) + Send + Sync + 'static,
|
||||
{
|
||||
let program_address = constants::pumpfun::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(PumpfunEvent::NewToken(token_info));
|
||||
}
|
||||
DexInstruction::UserTrade(trade_info) => {
|
||||
callback(PumpfunEvent::NewUserTrade(trade_info));
|
||||
}
|
||||
DexInstruction::BotTrade(trade_info) => {
|
||||
callback(PumpfunEvent::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;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
pub mod logs_data;
|
||||
pub mod logs_parser;
|
||||
pub mod logs_filters;
|
||||
pub mod logs_subscribe;
|
||||
pub mod logs_events;
|
||||
|
||||
pub use logs_data::*;
|
||||
pub use logs_parser::*;
|
||||
pub use logs_filters::*;
|
||||
pub use logs_subscribe::*;
|
||||
pub use logs_events::*;
|
||||
@@ -1,353 +0,0 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{ClientError, ClientResult};
|
||||
|
||||
/// PumpSwap指令类型
|
||||
#[derive(Debug)]
|
||||
pub enum PumpSwapInstruction {
|
||||
Buy(BuyEvent),
|
||||
Sell(SellEvent),
|
||||
CreatePool(CreatePoolEvent),
|
||||
Deposit(DepositEvent),
|
||||
Withdraw(WithdrawEvent),
|
||||
Disable(DisableEvent),
|
||||
UpdateAdmin(UpdateAdminEvent),
|
||||
UpdateFeeConfig(UpdateFeeConfigEvent),
|
||||
Other,
|
||||
}
|
||||
|
||||
/// 买入事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct BuyEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub base_amount_out: u64,
|
||||
pub max_quote_amount_in: u64,
|
||||
pub user_base_token_reserves: u64,
|
||||
pub user_quote_token_reserves: u64,
|
||||
pub pool_base_token_reserves: u64,
|
||||
pub pool_quote_token_reserves: u64,
|
||||
pub quote_amount_in: u64,
|
||||
pub lp_fee_basis_points: u64,
|
||||
pub lp_fee: u64,
|
||||
pub protocol_fee_basis_points: u64,
|
||||
pub protocol_fee: u64,
|
||||
pub quote_amount_in_with_lp_fee: u64,
|
||||
pub user_quote_amount_in: u64,
|
||||
pub pool: Pubkey,
|
||||
pub user: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
pub protocol_fee_recipient: Pubkey,
|
||||
pub protocol_fee_recipient_token_account: Pubkey,
|
||||
pub coin_creator: Pubkey,
|
||||
pub coin_creator_fee_basis_points: u64,
|
||||
pub coin_creator_fee: u64,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
|
||||
#[borsh(skip)]
|
||||
pub base_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub quote_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_base_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub coin_creator_vault_ata: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub coin_creator_vault_authority: Pubkey,
|
||||
}
|
||||
|
||||
/// 卖出事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct SellEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub base_amount_in: u64,
|
||||
pub min_quote_amount_out: u64,
|
||||
pub user_base_token_reserves: u64,
|
||||
pub user_quote_token_reserves: u64,
|
||||
pub pool_base_token_reserves: u64,
|
||||
pub pool_quote_token_reserves: u64,
|
||||
pub quote_amount_out: u64,
|
||||
pub lp_fee_basis_points: u64,
|
||||
pub lp_fee: u64,
|
||||
pub protocol_fee_basis_points: u64,
|
||||
pub protocol_fee: u64,
|
||||
pub quote_amount_out_without_lp_fee: u64,
|
||||
pub user_quote_amount_out: u64,
|
||||
pub pool: Pubkey,
|
||||
pub user: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
pub protocol_fee_recipient: Pubkey,
|
||||
pub protocol_fee_recipient_token_account: Pubkey,
|
||||
pub coin_creator: Pubkey,
|
||||
pub coin_creator_fee_basis_points: u64,
|
||||
pub coin_creator_fee: u64,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
|
||||
#[borsh(skip)]
|
||||
pub base_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub quote_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_base_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub coin_creator_vault_ata: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub coin_creator_vault_authority: Pubkey,
|
||||
}
|
||||
|
||||
/// 创建池子事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct CreatePoolEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub index: u16,
|
||||
pub creator: Pubkey,
|
||||
pub base_mint: Pubkey,
|
||||
pub quote_mint: Pubkey,
|
||||
pub base_mint_decimals: u8,
|
||||
pub quote_mint_decimals: u8,
|
||||
pub base_amount_in: u64,
|
||||
pub quote_amount_in: u64,
|
||||
pub pool_base_amount: u64,
|
||||
pub pool_quote_amount: u64,
|
||||
pub minimum_liquidity: u64,
|
||||
pub initial_liquidity: u64,
|
||||
pub lp_token_amount_out: u64,
|
||||
pub pool_bump: u8,
|
||||
pub pool: Pubkey,
|
||||
pub lp_mint: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 存款事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct DepositEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub lp_token_amount_out: u64,
|
||||
pub max_base_amount_in: u64,
|
||||
pub max_quote_amount_in: u64,
|
||||
pub user_base_token_reserves: u64,
|
||||
pub user_quote_token_reserves: u64,
|
||||
pub pool_base_token_reserves: u64,
|
||||
pub pool_quote_token_reserves: u64,
|
||||
pub base_amount_in: u64,
|
||||
pub quote_amount_in: u64,
|
||||
pub lp_mint_supply: u64,
|
||||
pub pool: Pubkey,
|
||||
pub user: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
pub user_pool_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 提款事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct WithdrawEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub lp_token_amount_in: u64,
|
||||
pub min_base_amount_out: u64,
|
||||
pub min_quote_amount_out: u64,
|
||||
pub user_base_token_reserves: u64,
|
||||
pub user_quote_token_reserves: u64,
|
||||
pub pool_base_token_reserves: u64,
|
||||
pub pool_quote_token_reserves: u64,
|
||||
pub base_amount_out: u64,
|
||||
pub quote_amount_out: u64,
|
||||
pub lp_mint_supply: u64,
|
||||
pub pool: Pubkey,
|
||||
pub user: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
pub user_pool_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 禁用事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct DisableEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub admin: Pubkey,
|
||||
pub disable_create_pool: bool,
|
||||
pub disable_deposit: bool,
|
||||
pub disable_withdraw: bool,
|
||||
pub disable_buy: bool,
|
||||
pub disable_sell: bool,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 更新管理员事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct UpdateAdminEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub old_admin: Pubkey,
|
||||
pub new_admin: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 更新费用配置事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct UpdateFeeConfigEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub admin: Pubkey,
|
||||
pub old_lp_fee_basis_points: u64,
|
||||
pub new_lp_fee_basis_points: u64,
|
||||
pub old_protocol_fee_basis_points: u64,
|
||||
pub new_protocol_fee_basis_points: u64,
|
||||
pub old_protocol_fee_recipients: [Pubkey; 8],
|
||||
pub new_protocol_fee_recipients: [Pubkey; 8],
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 全局配置
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct GlobalConfig {
|
||||
pub admin: Pubkey,
|
||||
pub lp_fee_basis_points: u64,
|
||||
pub protocol_fee_basis_points: u64,
|
||||
pub disable_flags: u8,
|
||||
pub protocol_fee_recipients: [Pubkey; 8],
|
||||
}
|
||||
|
||||
/// 池子信息
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct Pool {
|
||||
pub index: u16,
|
||||
pub base_mint: Pubkey,
|
||||
pub quote_mint: Pubkey,
|
||||
pub lp_mint: Pubkey,
|
||||
pub base_mint_decimals: u8,
|
||||
pub quote_mint_decimals: u8,
|
||||
pub lp_mint_decimals: u8,
|
||||
pub base_token_account: Pubkey,
|
||||
pub quote_token_account: Pubkey,
|
||||
pub bump: u8,
|
||||
pub is_disabled: bool,
|
||||
}
|
||||
|
||||
/// 事件特性
|
||||
pub trait EventTrait: Sized + std::fmt::Debug {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self>;
|
||||
fn discriminator() -> &'static [u8];
|
||||
}
|
||||
|
||||
/// 从字节中提取鉴别器
|
||||
pub fn extract_discriminator(length: usize, data: &[u8]) -> Option<(&[u8], &[u8])> {
|
||||
if data.len() < length {
|
||||
return None;
|
||||
}
|
||||
Some((&data[..length], &data[length..]))
|
||||
}
|
||||
|
||||
/// 事件鉴别器常量
|
||||
pub mod discriminators {
|
||||
// 事件鉴别器
|
||||
pub const BUY_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x67, 0xf4, 0x52, 0x1f, 0x2c, 0xf5, 0x77, 0x77];
|
||||
pub const SELL_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x3e, 0x2f, 0x37, 0x0a, 0xa5, 0x03, 0xdc, 0x2a];
|
||||
pub const CREATE_POOL_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xb1, 0x31, 0x0c, 0xd2, 0xa0, 0x76, 0xa7, 0x74];
|
||||
pub const DEPOSIT_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x78, 0xf8, 0x3d, 0x53, 0x1f, 0x8e, 0x6b, 0x90];
|
||||
pub const WITHDRAW_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x16, 0x09, 0x85, 0x1a, 0xa0, 0x2c, 0x47, 0xc0];
|
||||
pub const DISABLE_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x6b, 0xfd, 0xc1, 0x4c, 0xe4, 0xca, 0x1b, 0x68];
|
||||
pub const UPDATE_ADMIN_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xe1, 0x98, 0xab, 0x57, 0xf6, 0x3f, 0x42, 0xea];
|
||||
pub const UPDATE_FEE_CONFIG_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x5a, 0x17, 0x41, 0x23, 0x3e, 0xf4, 0xbc, 0xd0];
|
||||
|
||||
// 指令鉴别器
|
||||
pub const BUY_IX: &[u8] = &[102,
|
||||
6,
|
||||
61,
|
||||
18,
|
||||
1,
|
||||
218,
|
||||
235,
|
||||
234];
|
||||
pub const SELL_IX: &[u8] = &[51,
|
||||
230,
|
||||
133,
|
||||
164,
|
||||
1,
|
||||
127,
|
||||
131,
|
||||
173];
|
||||
pub const CREATE_POOL_IX: &[u8] = &[233,
|
||||
146,
|
||||
209,
|
||||
142,
|
||||
207,
|
||||
104,
|
||||
64,
|
||||
188];
|
||||
pub const DEPOSIT_IX: &[u8] = &[242,
|
||||
35,
|
||||
198,
|
||||
137,
|
||||
82,
|
||||
225,
|
||||
242,
|
||||
182];
|
||||
pub const WITHDRAW_IX: &[u8] = &[183,
|
||||
18,
|
||||
70,
|
||||
156,
|
||||
148,
|
||||
109,
|
||||
161,
|
||||
34];
|
||||
pub const DISABLE_IX: &[u8] = &[107,
|
||||
253,
|
||||
193,
|
||||
76,
|
||||
228,
|
||||
202,
|
||||
27,
|
||||
104];
|
||||
pub const UPDATE_ADMIN_IX: &[u8] = &[225,
|
||||
152,
|
||||
171,
|
||||
87,
|
||||
246,
|
||||
63,
|
||||
66,
|
||||
234];
|
||||
pub const UPDATE_FEE_CONFIG_IX: &[u8] = &[90,
|
||||
23,
|
||||
65,
|
||||
35,
|
||||
62,
|
||||
244,
|
||||
188,
|
||||
208];
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine;
|
||||
use crate::common::pumpswap::logs_data::{
|
||||
BuyEvent, SellEvent, CreatePoolEvent, DepositEvent, WithdrawEvent,
|
||||
DisableEvent, UpdateAdminEvent, UpdateFeeConfigEvent, discriminators
|
||||
};
|
||||
use borsh::BorshDeserialize;
|
||||
|
||||
pub const PROGRAM_DATA: &str = "Program data: ";
|
||||
pub const PROGRAM_LOG_PREFIX: &str = "Program log: PumpSwap: ";
|
||||
|
||||
/// PumpSwap事件枚举
|
||||
#[derive(Debug)]
|
||||
pub enum PumpSwapEvent {
|
||||
Buy(BuyEvent),
|
||||
Sell(SellEvent),
|
||||
CreatePool(CreatePoolEvent),
|
||||
Deposit(DepositEvent),
|
||||
Withdraw(WithdrawEvent),
|
||||
Disable(DisableEvent),
|
||||
UpdateAdmin(UpdateAdminEvent),
|
||||
UpdateFeeConfig(UpdateFeeConfigEvent),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl PumpSwapEvent {
|
||||
/// 解析日志并提取PumpSwap事件
|
||||
pub fn parse_logs(logs: &[String]) -> Vec<PumpSwapEvent> {
|
||||
let mut events = Vec::new();
|
||||
|
||||
if logs.is_empty() {
|
||||
return events;
|
||||
}
|
||||
|
||||
for log in logs {
|
||||
// 检查是否是事件日志
|
||||
if let Some(event_data) = log.strip_prefix(PROGRAM_DATA) {
|
||||
let borsh_bytes = match general_purpose::STANDARD.decode(event_data) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
// 检查鉴别器
|
||||
if borsh_bytes.len() < 16 {
|
||||
continue;
|
||||
}
|
||||
let prefix = [0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d];
|
||||
let discriminator = &[&prefix[..], &borsh_bytes[..8]].concat();
|
||||
let data = &borsh_bytes[8..];
|
||||
// 根据鉴别器解析不同类型的事件
|
||||
if discriminator == discriminators::BUY_EVENT {
|
||||
if let Ok(mut event) = BuyEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Buy(event));
|
||||
}
|
||||
} else if discriminator == discriminators::SELL_EVENT {
|
||||
if let Ok(mut event) = SellEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Sell(event));
|
||||
}
|
||||
} else if discriminator == discriminators::CREATE_POOL_EVENT {
|
||||
if let Ok(mut event) = CreatePoolEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::CreatePool(event));
|
||||
}
|
||||
} else if discriminator == discriminators::DEPOSIT_EVENT {
|
||||
if let Ok(mut event) = DepositEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Deposit(event));
|
||||
}
|
||||
} else if discriminator == discriminators::WITHDRAW_EVENT {
|
||||
if let Ok(mut event) = WithdrawEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Withdraw(event));
|
||||
}
|
||||
} else if discriminator == discriminators::DISABLE_EVENT {
|
||||
if let Ok(mut event) = DisableEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Disable(event));
|
||||
}
|
||||
} else if discriminator == discriminators::UPDATE_ADMIN_EVENT {
|
||||
if let Ok(mut event) = UpdateAdminEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::UpdateAdmin(event));
|
||||
}
|
||||
} else if discriminator == discriminators::UPDATE_FEE_CONFIG_EVENT {
|
||||
if let Ok(mut event) = UpdateFeeConfigEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::UpdateFeeConfig(event));
|
||||
}
|
||||
}
|
||||
} else if let Some(event_log) = log.strip_prefix(PROGRAM_LOG_PREFIX) {
|
||||
// 处理程序日志中的事件信息
|
||||
if event_log.contains("BuyEvent") {
|
||||
// 这里可以添加从日志文本中解析事件的逻辑
|
||||
// 例如使用正则表达式提取关键信息
|
||||
} else if event_log.contains("SellEvent") {
|
||||
// 同上
|
||||
}
|
||||
// 其他事件类型...
|
||||
}
|
||||
}
|
||||
|
||||
events
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
use crate::common::pumpswap::logs_data::PumpSwapInstruction;
|
||||
use crate::common::pumpswap::logs_parser::parse_pumpswap_instruction;
|
||||
use crate::common::pumpswap::logs_events::PumpSwapEvent;
|
||||
use crate::constants::pumpswap::accounts;
|
||||
use crate::error::ClientResult;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
|
||||
pub struct LogFilter;
|
||||
|
||||
impl LogFilter {
|
||||
/// 解析PumpSwap编译后的指令并返回指令类型和数据
|
||||
pub fn parse_pumpswap_compiled_instruction(
|
||||
versioned_tx: VersionedTransaction) -> ClientResult<Vec<PumpSwapInstruction>> {
|
||||
let compiled_instructions = versioned_tx.message.instructions();
|
||||
let accounts = versioned_tx.message.static_account_keys();
|
||||
let program_id = accounts::AMM_PROGRAM;
|
||||
let pump_index = accounts.iter().position(|key| key == &program_id);
|
||||
let mut instructions: Vec<PumpSwapInstruction> = Vec::new();
|
||||
|
||||
if let Some(index) = pump_index {
|
||||
for instruction in compiled_instructions {
|
||||
if instruction.program_id_index as usize == index {
|
||||
let all_accounts_valid = instruction.accounts.iter()
|
||||
.all(|&acc_idx| (acc_idx as usize) < accounts.len());
|
||||
if !all_accounts_valid {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parsed_instruction) = parse_pumpswap_instruction(instruction, accounts) {
|
||||
instructions.push(parsed_instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
/// 解析PumpSwap交易日志并返回事件
|
||||
pub fn parse_pumpswap_logs(logs: &[String]) -> Vec<PumpSwapEvent> {
|
||||
PumpSwapEvent::parse_logs(logs)
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
use crate::error::ClientResult;
|
||||
use crate::common::pumpswap::{
|
||||
logs_data::{
|
||||
PumpSwapInstruction,
|
||||
BuyEvent, SellEvent, CreatePoolEvent, DepositEvent, WithdrawEvent,
|
||||
DisableEvent, UpdateAdminEvent, UpdateFeeConfigEvent, discriminators
|
||||
},
|
||||
logs_events::PumpSwapEvent
|
||||
};
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::instruction::CompiledInstruction;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// 处理PumpSwap日志并调用回调函数
|
||||
pub async fn process_pumpswap_logs<F>(
|
||||
signature: &str,
|
||||
logs: Vec<String>,
|
||||
slot: Option<u64>,
|
||||
callback: F,
|
||||
) -> ClientResult<()>
|
||||
where
|
||||
F: Fn(&str, PumpSwapEvent) + Send + Sync,
|
||||
{
|
||||
let events = PumpSwapEvent::parse_logs(&logs);
|
||||
for mut event in events {
|
||||
// 设置签名和slot
|
||||
match &mut event {
|
||||
PumpSwapEvent::Buy(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::Sell(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::CreatePool(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::Deposit(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::Withdraw(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::Disable(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::UpdateAdmin(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::UpdateFeeConfig(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
callback(signature, event);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取当前时间戳
|
||||
fn current_timestamp() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs() as i64
|
||||
}
|
||||
|
||||
/// 从指令中解析PumpSwap指令
|
||||
pub fn parse_pumpswap_instruction(instruction: &CompiledInstruction, _accounts: &[Pubkey]) -> Option<PumpSwapInstruction> {
|
||||
if instruction.data.len() < 8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let discriminator = &instruction.data[..8];
|
||||
let data = &instruction.data[8..];
|
||||
|
||||
let accounts: Vec<Pubkey> = instruction.accounts.iter()
|
||||
.map(|&idx| _accounts[idx as usize])
|
||||
.collect();
|
||||
|
||||
match discriminator {
|
||||
d if d == discriminators::BUY_IX => {
|
||||
// buy指令参数: base_amount_out: u64, max_quote_amount_in: u64
|
||||
// 账户顺序:pool, user, global_config, base_mint, quote_mint, user_base_token_account,
|
||||
// user_quote_token_account, pool_base_token_account, pool_quote_token_account,
|
||||
// protocol_fee_recipient, protocol_fee_recipient_token_account, ...
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let base_amount_out = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let max_quote_amount_in = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
Some(PumpSwapInstruction::Buy(BuyEvent {
|
||||
base_amount_out,
|
||||
max_quote_amount_in,
|
||||
pool: accounts[0],
|
||||
user: accounts[1],
|
||||
user_base_token_account: accounts[5],
|
||||
user_quote_token_account: accounts[6],
|
||||
protocol_fee_recipient: accounts[9],
|
||||
protocol_fee_recipient_token_account: accounts[10],
|
||||
timestamp: current_timestamp(),
|
||||
|
||||
base_mint: accounts[3],
|
||||
quote_mint: accounts[4],
|
||||
pool_base_token_account: accounts[7],
|
||||
pool_quote_token_account: accounts[8],
|
||||
coin_creator_vault_ata: if accounts.len() > 17 { accounts[17] } else { Pubkey::default() },
|
||||
coin_creator_vault_authority: if accounts.len() > 18 { accounts[18] } else { Pubkey::default() },
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::SELL_IX => {
|
||||
// sell指令参数: base_amount_in: u64, min_quote_amount_out: u64
|
||||
// 账户顺序:pool, user, global_config, base_mint, quote_mint, user_base_token_account,
|
||||
// user_quote_token_account, pool_base_token_account, pool_quote_token_account,
|
||||
// protocol_fee_recipient, protocol_fee_recipient_token_account, ...
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let base_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let min_quote_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
Some(PumpSwapInstruction::Sell(SellEvent {
|
||||
base_amount_in,
|
||||
min_quote_amount_out,
|
||||
pool: accounts[0],
|
||||
user: accounts[1],
|
||||
user_base_token_account: accounts[5],
|
||||
user_quote_token_account: accounts[6],
|
||||
protocol_fee_recipient: accounts[9],
|
||||
protocol_fee_recipient_token_account: accounts[10],
|
||||
timestamp: current_timestamp(),
|
||||
|
||||
base_mint: accounts[3],
|
||||
quote_mint: accounts[4],
|
||||
pool_base_token_account: accounts[7],
|
||||
pool_quote_token_account: accounts[8],
|
||||
coin_creator_vault_ata: if accounts.len() > 17 { accounts[17] } else { Pubkey::default() },
|
||||
coin_creator_vault_authority: if accounts.len() > 18 { accounts[18] } else { Pubkey::default() },
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::CREATE_POOL_IX => {
|
||||
// create_pool指令参数: index: u16, base_amount_in: u64, quote_amount_in: u64
|
||||
// 账户顺序:pool, global_config, creator, base_mint, quote_mint, lp_mint,
|
||||
// user_base_token_account, user_quote_token_account, user_pool_token_account,
|
||||
// pool_base_token_account, pool_quote_token_account, ...
|
||||
if data.len() < 18 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let index = u16::from_le_bytes(data[0..2].try_into().ok()?);
|
||||
let base_amount_in = u64::from_le_bytes(data[2..10].try_into().ok()?);
|
||||
let quote_amount_in = u64::from_le_bytes(data[10..18].try_into().ok()?);
|
||||
|
||||
Some(PumpSwapInstruction::CreatePool(CreatePoolEvent {
|
||||
index,
|
||||
base_amount_in,
|
||||
quote_amount_in,
|
||||
pool: accounts[0],
|
||||
creator: accounts[2],
|
||||
base_mint: accounts[3],
|
||||
quote_mint: accounts[4],
|
||||
lp_mint: accounts[5],
|
||||
user_base_token_account: accounts[6],
|
||||
user_quote_token_account: accounts[7],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::DEPOSIT_IX => {
|
||||
// deposit指令参数: lp_token_amount_out: u64, max_base_amount_in: u64, max_quote_amount_in: u64
|
||||
// 账户顺序:pool, global_config, user, base_mint, quote_mint, lp_mint,
|
||||
// user_base_token_account, user_quote_token_account, user_pool_token_account,
|
||||
// pool_base_token_account, pool_quote_token_account, ...
|
||||
if data.len() < 24 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let lp_token_amount_out = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let max_base_amount_in = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
let max_quote_amount_in = u64::from_le_bytes(data[16..24].try_into().ok()?);
|
||||
|
||||
Some(PumpSwapInstruction::Deposit(DepositEvent {
|
||||
lp_token_amount_out,
|
||||
max_base_amount_in,
|
||||
max_quote_amount_in,
|
||||
pool: accounts[0],
|
||||
user: accounts[2],
|
||||
user_base_token_account: accounts[6],
|
||||
user_quote_token_account: accounts[7],
|
||||
user_pool_token_account: accounts[8],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::WITHDRAW_IX => {
|
||||
// withdraw指令参数: lp_token_amount_in: u64, min_base_amount_out: u64, min_quote_amount_out: u64
|
||||
// 账户顺序:pool, global_config, user, base_mint, quote_mint, lp_mint,
|
||||
// user_base_token_account, user_quote_token_account, user_pool_token_account,
|
||||
// pool_base_token_account, pool_quote_token_account, ...
|
||||
if data.len() < 24 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let lp_token_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let min_base_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
let min_quote_amount_out = u64::from_le_bytes(data[16..24].try_into().ok()?);
|
||||
|
||||
Some(PumpSwapInstruction::Withdraw(WithdrawEvent {
|
||||
lp_token_amount_in,
|
||||
min_base_amount_out,
|
||||
min_quote_amount_out,
|
||||
pool: accounts[0],
|
||||
user: accounts[2],
|
||||
user_base_token_account: accounts[6],
|
||||
user_quote_token_account: accounts[7],
|
||||
user_pool_token_account: accounts[8],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::DISABLE_IX => {
|
||||
// disable指令参数: disable_create_pool: bool, disable_deposit: bool, disable_withdraw: bool, disable_buy: bool, disable_sell: bool
|
||||
// 账户顺序:admin, global_config, event_authority, program
|
||||
if data.len() < 5 || accounts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let disable_create_pool = data[0] != 0;
|
||||
let disable_deposit = data[1] != 0;
|
||||
let disable_withdraw = data[2] != 0;
|
||||
let disable_buy = data[3] != 0;
|
||||
let disable_sell = data[4] != 0;
|
||||
|
||||
Some(PumpSwapInstruction::Disable(DisableEvent {
|
||||
disable_create_pool,
|
||||
disable_deposit,
|
||||
disable_withdraw,
|
||||
disable_buy,
|
||||
disable_sell,
|
||||
admin: accounts[0],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::UPDATE_ADMIN_IX => {
|
||||
// update_admin指令参数: 无
|
||||
// 账户顺序:admin, global_config, new_admin, event_authority, program
|
||||
if accounts.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
Some(PumpSwapInstruction::UpdateAdmin(UpdateAdminEvent {
|
||||
old_admin: accounts[0],
|
||||
new_admin: accounts[2],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::UPDATE_FEE_CONFIG_IX => {
|
||||
// update_fee_config指令参数: lp_fee_basis_points: u64, protocol_fee_basis_points: u64, protocol_fee_recipients: [pubkey; 8]
|
||||
// 账户顺序:admin, global_config, event_authority, program
|
||||
if data.len() < 272 || accounts.len() < 2 { // 8 + 8 + 32*8 = 272 bytes
|
||||
return None;
|
||||
}
|
||||
let lp_fee_basis_points = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let protocol_fee_basis_points = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
let mut protocol_fee_recipients = [Pubkey::default(); 8];
|
||||
for i in 0..8 {
|
||||
let start = 16 + i * 32;
|
||||
let end = start + 32;
|
||||
if let Ok(pubkey_bytes) = data[start..end].try_into() {
|
||||
protocol_fee_recipients[i] = Pubkey::new_from_array(pubkey_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
Some(PumpSwapInstruction::UpdateFeeConfig(UpdateFeeConfigEvent {
|
||||
admin: accounts[0],
|
||||
new_lp_fee_basis_points: lp_fee_basis_points,
|
||||
new_protocol_fee_basis_points: protocol_fee_basis_points,
|
||||
new_protocol_fee_recipients: protocol_fee_recipients,
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
_ => Some(PumpSwapInstruction::Other),
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
pub mod logs_data;
|
||||
pub mod logs_parser;
|
||||
pub mod logs_filters;
|
||||
pub mod logs_events;
|
||||
|
||||
pub use logs_data::*;
|
||||
pub use logs_parser::*;
|
||||
pub use logs_filters::*;
|
||||
pub use logs_events::*;
|
||||
@@ -1,161 +0,0 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
use crate::error::{ClientError, ClientResult};
|
||||
|
||||
/// Raydium指令类型
|
||||
#[derive(Debug)]
|
||||
pub enum RaydiumInstruction {
|
||||
V4Swap(V4SwapEvent),
|
||||
SwapBaseInput(SwapBaseInputEvent),
|
||||
SwapBaseOutput(SwapBaseOutputEvent),
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct SwapBaseOutputEvent {
|
||||
#[borsh(skip)]
|
||||
pub timestamp: i64,
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
pub max_amount_in: u64,
|
||||
pub amount_out: u64,
|
||||
|
||||
#[borsh(skip)]
|
||||
pub payer: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub authority: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_config: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_state: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_token_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_token_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_token_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_token_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub observation_state: Pubkey,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct SwapBaseInputEvent {
|
||||
#[borsh(skip)]
|
||||
pub timestamp: i64,
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
pub amount_in: u64,
|
||||
pub minimum_amount_out: u64,
|
||||
|
||||
#[borsh(skip)]
|
||||
pub payer: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub authority: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_config: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_state: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_token_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_token_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_token_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_token_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub observation_state: Pubkey,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct V4SwapEvent {
|
||||
#[borsh(skip)]
|
||||
pub timestamp: i64,
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
pub amount_in: u64,
|
||||
pub minimum_amount_out: u64,
|
||||
|
||||
#[borsh(skip)]
|
||||
pub amm: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_authority: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_open_orders: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_target_orders: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_coin_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_pc_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_market: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_bids: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_asks: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_event_queue: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_coin_vault_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_pc_vault_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_vault_signer: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub user_source_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub user_destination_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub user_source_owner: Pubkey,
|
||||
}
|
||||
|
||||
/// 事件特性
|
||||
pub trait EventTrait: Sized + std::fmt::Debug {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self>;
|
||||
fn discriminator() -> &'static [u8];
|
||||
}
|
||||
|
||||
/// 从字节中提取鉴别器
|
||||
pub fn extract_discriminator(length: usize, data: &[u8]) -> Option<(&[u8], &[u8])> {
|
||||
if data.len() < length {
|
||||
return None;
|
||||
}
|
||||
Some((&data[..length], &data[length..]))
|
||||
}
|
||||
|
||||
/// 事件鉴别器常量
|
||||
pub mod discriminators {
|
||||
pub const V4_SWAP_IX: &u8 = &9;
|
||||
|
||||
pub const SWAP_BASE_INPUT_IX: &[u8] = &[143, 190, 90, 218, 196, 30, 51, 222];
|
||||
pub const SWAP_BASE_OUTPUT_IX: &[u8] = &[55, 217, 98, 86, 163, 74, 180, 173];
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
use crate::common::raydium::logs_data::{discriminators, SwapBaseInputEvent, V4SwapEvent};
|
||||
use crate::common::raydium::SwapBaseOutputEvent;
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine;
|
||||
use borsh::BorshDeserialize;
|
||||
|
||||
pub const PROGRAM_DATA: &str = "Program data: ";
|
||||
pub const PROGRAM_LOG_PREFIX: &str = "Program log: ray_log: ";
|
||||
|
||||
/// Raydium事件枚举
|
||||
#[derive(Debug)]
|
||||
pub enum RaydiumEvent {
|
||||
V4Swap(V4SwapEvent),
|
||||
SwapBaseInput(SwapBaseInputEvent),
|
||||
SwapBaseOutput(SwapBaseOutputEvent),
|
||||
Error(String),
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
use crate::common::raydium::logs_data::RaydiumInstruction;
|
||||
use crate::common::raydium::logs_parser::parse_raydium_instruction;
|
||||
use crate::constants::raydium::accounts;
|
||||
use crate::error::ClientResult;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
|
||||
pub struct LogFilter;
|
||||
|
||||
impl LogFilter {
|
||||
/// 解析Raydium编译后的指令并返回指令类型和数据
|
||||
pub fn parse_raydium_compiled_instruction(
|
||||
versioned_tx: VersionedTransaction,
|
||||
) -> ClientResult<Vec<RaydiumInstruction>> {
|
||||
let compiled_instructions = versioned_tx.message.instructions();
|
||||
let accounts = versioned_tx.message.static_account_keys();
|
||||
let ammv4_program_id = accounts::AMMV4_PROGRAM;
|
||||
let cpmm_program_id = accounts::CPMM_PROGRAM;
|
||||
let raydium_index = accounts.iter().position(|key| key == &ammv4_program_id);
|
||||
let cpmm_index = accounts.iter().position(|key| key == &cpmm_program_id);
|
||||
let mut instructions: Vec<RaydiumInstruction> = Vec::new();
|
||||
if let Some(index) = raydium_index {
|
||||
for instruction in compiled_instructions {
|
||||
if instruction.program_id_index as usize == index {
|
||||
let all_accounts_valid = instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.all(|&acc_idx| (acc_idx as usize) < accounts.len());
|
||||
if !all_accounts_valid {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parsed_instruction) =
|
||||
parse_raydium_instruction(instruction, accounts, &ammv4_program_id)
|
||||
{
|
||||
instructions.push(parsed_instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(index) = cpmm_index {
|
||||
for instruction in compiled_instructions {
|
||||
if instruction.program_id_index as usize == index {
|
||||
let all_accounts_valid = instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.all(|&acc_idx| (acc_idx as usize) < accounts.len());
|
||||
if !all_accounts_valid {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parsed_instruction) =
|
||||
parse_raydium_instruction(instruction, accounts, &cpmm_program_id)
|
||||
{
|
||||
instructions.push(parsed_instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
use crate::common::raydium::{
|
||||
logs_data::{discriminators, RaydiumInstruction, V4SwapEvent},
|
||||
logs_events::RaydiumEvent,
|
||||
};
|
||||
use crate::error::ClientResult;
|
||||
|
||||
use solana_sdk::instruction::CompiledInstruction;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// 获取当前时间戳
|
||||
fn current_timestamp() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs() as i64
|
||||
}
|
||||
|
||||
/// 从指令中解析Raydium指令
|
||||
pub fn parse_raydium_instruction(
|
||||
instruction: &CompiledInstruction,
|
||||
_accounts: &[Pubkey],
|
||||
program_id: &Pubkey,
|
||||
) -> Option<RaydiumInstruction> {
|
||||
if instruction.data.len() < 8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if program_id == &crate::constants::raydium::accounts::CPMM_PROGRAM {
|
||||
let discriminator = &instruction.data[..8];
|
||||
let data = &instruction.data[8..];
|
||||
|
||||
let accounts: Vec<Pubkey> = instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|&idx| _accounts[idx as usize])
|
||||
.collect();
|
||||
|
||||
match discriminator {
|
||||
d if d == discriminators::SWAP_BASE_INPUT_IX => {
|
||||
if data.len() < 16 || accounts.len() < 12 {
|
||||
return None;
|
||||
}
|
||||
let amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let minimum_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
Some(RaydiumInstruction::SwapBaseInput(
|
||||
crate::common::raydium::SwapBaseInputEvent {
|
||||
timestamp: current_timestamp(),
|
||||
amount_in: amount_in,
|
||||
minimum_amount_out: minimum_amount_out,
|
||||
|
||||
payer: accounts[0],
|
||||
authority: accounts[1],
|
||||
amm_config: accounts[2],
|
||||
pool_state: accounts[3],
|
||||
input_token_account: accounts[4],
|
||||
output_token_account: accounts[5],
|
||||
input_vault: accounts[6],
|
||||
output_vault: accounts[7],
|
||||
input_token_program: accounts[8],
|
||||
output_token_program: accounts[9],
|
||||
input_token_mint: accounts[10],
|
||||
output_token_mint: accounts[11],
|
||||
observation_state: accounts[12],
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
}
|
||||
d if d == discriminators::SWAP_BASE_OUTPUT_IX => {
|
||||
println!("data.len(): {:?}", data.len());
|
||||
println!("accounts.len(): {:?}", accounts.len());
|
||||
if data.len() < 16 || accounts.len() < 12 {
|
||||
return None;
|
||||
}
|
||||
let max_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
Some(RaydiumInstruction::SwapBaseOutput(
|
||||
crate::common::raydium::SwapBaseOutputEvent {
|
||||
timestamp: current_timestamp(),
|
||||
max_amount_in: max_amount_in,
|
||||
amount_out: amount_out,
|
||||
|
||||
payer: accounts[0],
|
||||
authority: accounts[1],
|
||||
amm_config: accounts[2],
|
||||
pool_state: accounts[3],
|
||||
input_token_account: accounts[4],
|
||||
output_token_account: accounts[5],
|
||||
input_vault: accounts[6],
|
||||
output_vault: accounts[7],
|
||||
input_token_program: accounts[8],
|
||||
output_token_program: accounts[9],
|
||||
input_token_mint: accounts[10],
|
||||
output_token_mint: accounts[11],
|
||||
observation_state: accounts[12],
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
}
|
||||
_ => Some(RaydiumInstruction::Other),
|
||||
}
|
||||
} else if program_id == &crate::constants::raydium::accounts::AMMV4_PROGRAM {
|
||||
let discriminator = &instruction.data[0];
|
||||
let data = &instruction.data[1..];
|
||||
|
||||
let mut accounts: Vec<Pubkey> = instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|&idx| _accounts[idx as usize])
|
||||
.collect();
|
||||
|
||||
match discriminator {
|
||||
d if d == discriminators::V4_SWAP_IX => {
|
||||
if data.len() < 16 || accounts.len() < 17 {
|
||||
return None;
|
||||
}
|
||||
let amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let minimum_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
if accounts.len() == 17 {
|
||||
accounts.insert(4, Pubkey::default());
|
||||
}
|
||||
|
||||
Some(RaydiumInstruction::V4Swap(V4SwapEvent {
|
||||
timestamp: current_timestamp(),
|
||||
amount_in: amount_in,
|
||||
minimum_amount_out: minimum_amount_out,
|
||||
amm: accounts[1],
|
||||
amm_authority: accounts[2],
|
||||
amm_open_orders: accounts[3],
|
||||
amm_target_orders: accounts[4],
|
||||
pool_coin_token_account: accounts[5],
|
||||
pool_pc_token_account: accounts[6],
|
||||
serum_program: accounts[7],
|
||||
serum_market: accounts[8],
|
||||
serum_bids: accounts[9],
|
||||
serum_asks: accounts[10],
|
||||
serum_event_queue: accounts[11],
|
||||
serum_coin_vault_account: accounts[12],
|
||||
serum_pc_vault_account: accounts[13],
|
||||
serum_vault_signer: accounts[14],
|
||||
user_source_token_account: accounts[15],
|
||||
user_destination_token_account: accounts[16],
|
||||
user_source_owner: accounts[17],
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
_ => Some(RaydiumInstruction::Other),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
pub mod logs_data;
|
||||
pub mod logs_parser;
|
||||
pub mod logs_filters;
|
||||
pub mod logs_events;
|
||||
|
||||
pub use logs_data::*;
|
||||
pub use logs_parser::*;
|
||||
pub use logs_filters::*;
|
||||
pub use logs_events::*;
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod pumpfun;
|
||||
pub mod pumpswap;
|
||||
pub mod raydium;
|
||||
pub mod raydium_launchpad;
|
||||
pub mod swqos;
|
||||
|
||||
pub mod trade_type {
|
||||
@@ -13,5 +13,5 @@ pub mod trade_type {
|
||||
pub mod trade_platform {
|
||||
pub const PUMPFUN: &'static str = "pumpfun";
|
||||
pub const PUMPFUN_SWAP: &'static str = "pumpswap";
|
||||
pub const RAYDIUM: &'static str = "raydium";
|
||||
pub const RAYDIUM_LAUNCHPAD: &'static str = "raydium_launchpad";
|
||||
}
|
||||
@@ -57,58 +57,6 @@ pub mod accounts {
|
||||
/// Rent Sysvar ID
|
||||
pub const RENT: Pubkey = pubkey!("SysvarRent111111111111111111111111111111111");
|
||||
|
||||
pub const JITO_TIP_ACCOUNTS: &[&str] = &[
|
||||
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
|
||||
"HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe",
|
||||
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
|
||||
"ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
|
||||
"DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
|
||||
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
|
||||
"DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
|
||||
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
|
||||
];
|
||||
|
||||
|
||||
/// Tip accounts
|
||||
pub const NEXTBLOCK_TIP_ACCOUNTS: &[&str] = &[
|
||||
"NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE",
|
||||
"NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2",
|
||||
"NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X",
|
||||
"NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb",
|
||||
"neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At",
|
||||
"nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG",
|
||||
"NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid",
|
||||
"nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc"
|
||||
];
|
||||
|
||||
pub const ZEROSLOT_TIP_ACCOUNTS: &[&str] = &[
|
||||
"Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3",
|
||||
"FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe",
|
||||
"ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13",
|
||||
"6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK",
|
||||
"Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr",
|
||||
];
|
||||
|
||||
pub const NOZOMI_TIP_ACCOUNTS: &[&str] = &[
|
||||
"TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq",
|
||||
"noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4",
|
||||
"noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE",
|
||||
"noz6uoYCDijhu1V7cutCpwxNiSovEwLdRHPwmgCGDNo",
|
||||
"noz9EPNcT7WH6Sou3sr3GGjHQYVkN3DNirpbvDkv9YJ",
|
||||
"nozc5yT15LazbLTFVZzoNZCwjh3yUtW86LoUyqsBu4L",
|
||||
"nozFrhfnNGoyqwVuwPAW4aaGqempx4PU6g6D9CJMv7Z",
|
||||
"nozievPk7HyK1Rqy1MPJwVQ7qQg2QoJGyP71oeDwbsu",
|
||||
"noznbgwYnBLDHu8wcQVCEw6kDrXkPdKkydGJGNXGvL7",
|
||||
"nozNVWs5N8mgzuD3qigrCG2UoKxZttxzZ85pvAQVrbP",
|
||||
"nozpEGbwx4BcGp6pvEdAh1JoC2CQGZdU6HbNP1v2p6P",
|
||||
"nozrhjhkCr3zXT3BiT4WCodYCUFeQvcdUkM7MqhKqge",
|
||||
"nozrwQtWhEdrA6W8dkbt9gnUaMs52PdAv5byipnadq3",
|
||||
"nozUacTVWub3cL4mJmGCYjKZTnE9RbdY5AP46iQgbPJ",
|
||||
"nozWCyTPppJjRuw2fpzDhhWbW355fzosWSzrrMYB1Qk",
|
||||
"nozWNju6dY353eMkMqURqwQEoM3SFgEKC6psLCSfUne",
|
||||
"nozxNBgWohjR75vdspfxR5H9ceC7XXH99xpxhVGt3Bb"
|
||||
];
|
||||
|
||||
pub const AMM_PROGRAM: Pubkey = pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA");
|
||||
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
//! 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";
|
||||
}
|
||||
|
||||
/// Constants related to program accounts and authorities
|
||||
pub mod accounts {
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
pub const JITO_TIP_ACCOUNTS: &[&str] = &[
|
||||
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
|
||||
"HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe",
|
||||
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
|
||||
"ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
|
||||
"DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
|
||||
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
|
||||
"DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
|
||||
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
|
||||
];
|
||||
|
||||
/// Tip accounts
|
||||
pub const NEXTBLOCK_TIP_ACCOUNTS: &[&str] = &[
|
||||
"NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE",
|
||||
"NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2",
|
||||
"NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X",
|
||||
"NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb",
|
||||
"neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At",
|
||||
"nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG",
|
||||
"NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid",
|
||||
"nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc",
|
||||
];
|
||||
|
||||
pub const ZEROSLOT_TIP_ACCOUNTS: &[&str] = &[
|
||||
"Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3",
|
||||
"FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe",
|
||||
"ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13",
|
||||
"6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK",
|
||||
"Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr",
|
||||
];
|
||||
|
||||
pub const NOZOMI_TIP_ACCOUNTS: &[&str] = &[
|
||||
"TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq",
|
||||
"noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4",
|
||||
"noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE",
|
||||
"noz6uoYCDijhu1V7cutCpwxNiSovEwLdRHPwmgCGDNo",
|
||||
"noz9EPNcT7WH6Sou3sr3GGjHQYVkN3DNirpbvDkv9YJ",
|
||||
"nozc5yT15LazbLTFVZzoNZCwjh3yUtW86LoUyqsBu4L",
|
||||
"nozFrhfnNGoyqwVuwPAW4aaGqempx4PU6g6D9CJMv7Z",
|
||||
"nozievPk7HyK1Rqy1MPJwVQ7qQg2QoJGyP71oeDwbsu",
|
||||
"noznbgwYnBLDHu8wcQVCEw6kDrXkPdKkydGJGNXGvL7",
|
||||
"nozNVWs5N8mgzuD3qigrCG2UoKxZttxzZ85pvAQVrbP",
|
||||
"nozpEGbwx4BcGp6pvEdAh1JoC2CQGZdU6HbNP1v2p6P",
|
||||
"nozrhjhkCr3zXT3BiT4WCodYCUFeQvcdUkM7MqhKqge",
|
||||
"nozrwQtWhEdrA6W8dkbt9gnUaMs52PdAv5byipnadq3",
|
||||
"nozUacTVWub3cL4mJmGCYjKZTnE9RbdY5AP46iQgbPJ",
|
||||
"nozWCyTPppJjRuw2fpzDhhWbW355fzosWSzrrMYB1Qk",
|
||||
"nozWNju6dY353eMkMqURqwQEoM3SFgEKC6psLCSfUne",
|
||||
"nozxNBgWohjR75vdspfxR5H9ceC7XXH99xpxhVGt3Bb",
|
||||
];
|
||||
|
||||
pub const AMMV4_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
|
||||
pub const CPMM_PROGRAM: Pubkey = pubkey!("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C");
|
||||
}
|
||||
|
||||
pub mod trade {
|
||||
pub const TRADER_TIP_AMOUNT: u64 = 100000; // 0.0001 SOL in lamports
|
||||
pub const DEFAULT_SLIPPAGE: u64 = 1000; // 10%
|
||||
pub const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 78000;
|
||||
pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500000;
|
||||
pub const DEFAULT_BUY_TIP_FEE: u64 = 600000; // 0.0006 SOL in lamports
|
||||
pub const DEFAULT_SELL_TIP_FEE: u64 = 100000; // 0.0001 SOL in lamports
|
||||
}
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
//! 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 {
|
||||
pub const POOL_SEED: &[u8] = b"pool";
|
||||
pub const POOL_VAULT_SEED: &[u8] = b"pool_vault";
|
||||
}
|
||||
|
||||
/// Constants related to program accounts and authorities
|
||||
pub mod accounts {
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
pub const AUTHORITY: Pubkey = pubkey!("WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh");
|
||||
pub const GLOBAL_CONFIG: Pubkey = pubkey!("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX");
|
||||
pub const PLATFORM_CONFIG: Pubkey = pubkey!("FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1");
|
||||
pub const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
|
||||
pub const EVENT_AUTHORITY: Pubkey = pubkey!("2DPAtwB8L12vrMRExbLuyGnC7n2J5LNoZQSejeQGpwkr");
|
||||
pub const WSOL_TOKEN_ACCOUNT: Pubkey = pubkey!("So11111111111111111111111111111111111111112");
|
||||
pub const LAUNCHPAD_PROGRAM: Pubkey = pubkey!("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj");
|
||||
|
||||
pub const PLATFORM_FEE_RATE: u128 = 100; // 1%
|
||||
pub const PROTOCOL_FEE_RATE: u128 = 25; // 0.25%
|
||||
pub const SHARE_FEE_RATE: u128 = 0; // 0%
|
||||
}
|
||||
|
||||
pub const BUY_EXECT_IN_DISCRIMINATOR: [u8; 8] = [250, 234, 13, 123, 213, 156, 19, 236];
|
||||
pub const SELL_EXECT_IN_DISCRIMINATOR: [u8; 8] = [149, 39, 222, 155, 211, 124, 152, 26];
|
||||
|
||||
pub mod trade {
|
||||
pub const TRADER_TIP_AMOUNT: u64 = 100000; // 0.0001 SOL in lamports
|
||||
pub const DEFAULT_SLIPPAGE: u64 = 100; // 1%
|
||||
pub const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 78000;
|
||||
pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500000;
|
||||
pub const DEFAULT_BUY_TIP_FEE: u64 = 600000; // 0.0006 SOL in lamports
|
||||
pub const DEFAULT_SELL_TIP_FEE: u64 = 100000; // 0.0001 SOL in lamports
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
|
||||
/// 自动生成UnifiedEvent trait实现的宏
|
||||
#[macro_export]
|
||||
macro_rules! impl_unified_event {
|
||||
// 带有自定义ID表达式的版本
|
||||
($struct_name:ident, $($field:ident),*) => {
|
||||
impl $crate::event_parser::core::traits::UnifiedEvent for $struct_name {
|
||||
fn id(&self) -> &str {
|
||||
&self.metadata.id
|
||||
}
|
||||
|
||||
fn event_type(&self) -> $crate::event_parser::common::types::EventType {
|
||||
self.metadata.event_type.clone()
|
||||
}
|
||||
|
||||
fn signature(&self) -> &str {
|
||||
&self.metadata.signature
|
||||
}
|
||||
|
||||
fn slot(&self) -> u64 {
|
||||
self.metadata.slot
|
||||
}
|
||||
|
||||
fn program_received_time_ms(&self) -> i64 {
|
||||
self.metadata.program_received_time_ms
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn clone_boxed(&self) -> Box<dyn $crate::event_parser::core::traits::UnifiedEvent> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: Box<dyn $crate::event_parser::core::traits::UnifiedEvent>) {
|
||||
if let Some(e) = other.as_any().downcast_ref::<$struct_name>() {
|
||||
$(
|
||||
self.$field = e.$field.clone();
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub use types::*;
|
||||
pub use utils::*;
|
||||
@@ -0,0 +1,138 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
|
||||
)]
|
||||
pub enum ProtocolType {
|
||||
#[default]
|
||||
PumpSwap,
|
||||
PumpFun,
|
||||
RaydiumLaunchpad,
|
||||
Raydium,
|
||||
}
|
||||
|
||||
/// 事件类型枚举
|
||||
#[derive(
|
||||
Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
|
||||
)]
|
||||
pub enum EventType {
|
||||
// PumpSwap 事件
|
||||
#[default]
|
||||
PumpSwapBuy,
|
||||
PumpSwapSell,
|
||||
PumpSwapCreatePool,
|
||||
PumpSwapDeposit,
|
||||
PumpSwapWithdraw,
|
||||
|
||||
// PumpFun 事件
|
||||
PumpFunCreateToken,
|
||||
PumpFunBuy,
|
||||
PumpFunSell,
|
||||
|
||||
// RaydiumLaunchpad 事件
|
||||
RaydiumLaunchpadBuyExactIn,
|
||||
RaydiumLaunchpadBuyExactOut,
|
||||
RaydiumLaunchpadSellExactIn,
|
||||
RaydiumLaunchpadSellExactOut,
|
||||
RaydiumLaunchpadInitialize,
|
||||
|
||||
// 通用事件
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// 解析结果
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParseResult<T> {
|
||||
pub success: bool,
|
||||
pub data: Option<T>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> ParseResult<T> {
|
||||
pub fn success(data: T) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
data: Some(data),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn failure(error: String) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_success(&self) -> bool {
|
||||
self.success
|
||||
}
|
||||
|
||||
pub fn is_failure(&self) -> bool {
|
||||
!self.success
|
||||
}
|
||||
}
|
||||
|
||||
/// 协议信息
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ProtocolInfo {
|
||||
pub name: String,
|
||||
pub program_ids: Vec<Pubkey>,
|
||||
}
|
||||
|
||||
impl ProtocolInfo {
|
||||
pub fn new(name: String, program_ids: Vec<Pubkey>) -> Self {
|
||||
Self { name, program_ids }
|
||||
}
|
||||
|
||||
pub fn supports_program(&self, program_id: &Pubkey) -> bool {
|
||||
self.program_ids.contains(program_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// 事件元数据
|
||||
#[derive(
|
||||
Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
|
||||
)]
|
||||
pub struct EventMetadata {
|
||||
pub id: String,
|
||||
pub signature: String,
|
||||
pub slot: u64,
|
||||
pub program_received_time_ms: i64,
|
||||
pub protocol: ProtocolType,
|
||||
pub event_type: EventType,
|
||||
pub program_id: Pubkey,
|
||||
}
|
||||
|
||||
impl EventMetadata {
|
||||
pub fn new(
|
||||
id: String,
|
||||
signature: String,
|
||||
slot: u64,
|
||||
protocol: ProtocolType,
|
||||
event_type: EventType,
|
||||
program_id: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
signature,
|
||||
slot,
|
||||
program_received_time_ms: chrono::Utc::now().timestamp_millis(),
|
||||
protocol,
|
||||
event_type,
|
||||
program_id,
|
||||
}
|
||||
}
|
||||
pub fn set_id(&mut self, id: String) {
|
||||
// 对传入的 id 进行哈希处理
|
||||
let mut hasher = DefaultHasher::new();
|
||||
id.hash(&mut hasher);
|
||||
let hash_value = hasher.finish();
|
||||
self.id = format!("{:x}", hash_value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// 获取当前时间戳
|
||||
pub fn current_timestamp() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs() as i64
|
||||
}
|
||||
|
||||
/// 从base64字符串解码数据
|
||||
pub fn decode_base64(data: &str) -> Result<Vec<u8>, base64::DecodeError> {
|
||||
general_purpose::STANDARD.decode(data)
|
||||
}
|
||||
|
||||
/// 将数据编码为base64字符串
|
||||
pub fn encode_base64(data: &[u8]) -> String {
|
||||
general_purpose::STANDARD.encode(data)
|
||||
}
|
||||
|
||||
/// 从字节数组中提取鉴别器和剩余数据
|
||||
pub fn extract_discriminator(length: usize, data: &[u8]) -> Option<(&[u8], &[u8])> {
|
||||
if data.len() < length {
|
||||
return None;
|
||||
}
|
||||
Some((&data[..length], &data[length..]))
|
||||
}
|
||||
|
||||
/// 检查鉴别器是否匹配
|
||||
pub fn discriminator_matches(data: &str, expected: &str) -> bool {
|
||||
if data.len() < expected.len() {
|
||||
return false;
|
||||
}
|
||||
&data[..expected.len()] == expected
|
||||
}
|
||||
|
||||
/// 从日志中提取程序数据
|
||||
pub fn extract_program_data(log: &str) -> Option<&str> {
|
||||
const PROGRAM_DATA_PREFIX: &str = "Program data: ";
|
||||
log.strip_prefix(PROGRAM_DATA_PREFIX)
|
||||
}
|
||||
|
||||
/// 从日志中提取程序日志
|
||||
pub fn extract_program_log<'a>(log: &'a str, prefix: &str) -> Option<&'a str> {
|
||||
log.strip_prefix(prefix)
|
||||
}
|
||||
|
||||
/// 安全地从字节数组中读取u64
|
||||
pub fn read_u64_le(data: &[u8], offset: usize) -> Option<u64> {
|
||||
if data.len() < offset + 8 {
|
||||
return None;
|
||||
}
|
||||
let bytes: [u8; 8] = data[offset..offset + 8].try_into().ok()?;
|
||||
Some(u64::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
/// 安全地从字节数组中读取u32
|
||||
pub fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
|
||||
if data.len() < offset + 4 {
|
||||
return None;
|
||||
}
|
||||
let bytes: [u8; 4] = data[offset..offset + 4].try_into().ok()?;
|
||||
Some(u32::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
/// 安全地从字节数组中读取u16
|
||||
pub fn read_u16_le(data: &[u8], offset: usize) -> Option<u16> {
|
||||
if data.len() < offset + 2 {
|
||||
return None;
|
||||
}
|
||||
let bytes: [u8; 2] = data[offset..offset + 2].try_into().ok()?;
|
||||
Some(u16::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
/// 安全地从字节数组中读取u8
|
||||
pub fn read_u8(data: &[u8], offset: usize) -> Option<u8> {
|
||||
data.get(offset).copied()
|
||||
}
|
||||
|
||||
/// 验证账户索引的有效性
|
||||
pub fn validate_account_indices(indices: &[u8], account_count: usize) -> bool {
|
||||
indices.iter().all(|&idx| (idx as usize) < account_count)
|
||||
}
|
||||
|
||||
/// 格式化公钥为短字符串
|
||||
pub fn format_pubkey_short(pubkey: &solana_sdk::pubkey::Pubkey) -> String {
|
||||
let s = pubkey.to_string();
|
||||
if s.len() <= 8 {
|
||||
s
|
||||
} else {
|
||||
format!("{}...{}", &s[..4], &s[s.len() - 4..])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod traits;
|
||||
pub use traits::{EventParser, UnifiedEvent};
|
||||
@@ -0,0 +1,432 @@
|
||||
use anyhow::Result;
|
||||
use solana_sdk::{
|
||||
instruction::CompiledInstruction, pubkey::Pubkey, transaction::VersionedTransaction,
|
||||
};
|
||||
use solana_transaction_status::{
|
||||
EncodedTransactionWithStatusMeta, UiCompiledInstruction, UiInstruction,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::{
|
||||
error,
|
||||
event_parser::{common::{utils::*, EventMetadata, EventType, ProtocolType}, protocols::{pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent}, raydium_launchpad::{RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent}}},
|
||||
};
|
||||
|
||||
/// 统一事件接口 - 所有协议的事件都需要实现此trait
|
||||
pub trait UnifiedEvent: Debug + Send + Sync {
|
||||
/// 获取事件ID
|
||||
fn id(&self) -> &str;
|
||||
|
||||
/// 获取事件类型
|
||||
fn event_type(&self) -> EventType;
|
||||
|
||||
/// 获取交易签名
|
||||
fn signature(&self) -> &str;
|
||||
|
||||
/// 获取槽位号
|
||||
fn slot(&self) -> u64;
|
||||
|
||||
/// 获取程序接收的时间戳(毫秒)
|
||||
fn program_received_time_ms(&self) -> i64;
|
||||
|
||||
/// 将事件转换为Any以便向下转型
|
||||
fn as_any(&self) -> &dyn std::any::Any;
|
||||
|
||||
/// 将事件转换为可变Any以便向下转型
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
|
||||
|
||||
/// 克隆事件
|
||||
fn clone_boxed(&self) -> Box<dyn UnifiedEvent>;
|
||||
|
||||
/// 合并事件(可选实现)
|
||||
fn merge(&mut self, _other: Box<dyn UnifiedEvent>) {
|
||||
// 默认实现:不进行任何合并操作
|
||||
}
|
||||
}
|
||||
|
||||
/// 事件解析器trait - 定义了事件解析的核心方法
|
||||
#[async_trait::async_trait]
|
||||
pub trait EventParser: Send + Sync {
|
||||
/// 从内联指令中解析事件数据
|
||||
fn parse_events_from_inner_instruction(
|
||||
&self,
|
||||
instruction: &UiCompiledInstruction,
|
||||
signature: &str,
|
||||
slot: u64,
|
||||
) -> Vec<Box<dyn UnifiedEvent>>;
|
||||
|
||||
/// 从指令中解析事件数据
|
||||
fn parse_events_from_instruction(
|
||||
&self,
|
||||
instruction: &CompiledInstruction,
|
||||
accounts: &[Pubkey],
|
||||
signature: &str,
|
||||
slot: u64,
|
||||
) -> Vec<Box<dyn UnifiedEvent>>;
|
||||
|
||||
/// 从VersionedTransaction中解析指令事件的通用方法
|
||||
async fn parse_instruction_events_from_versioned_transaction(
|
||||
&self,
|
||||
versioned_tx: &VersionedTransaction,
|
||||
signature: &str,
|
||||
slot: Option<u64>,
|
||||
) -> Result<Vec<Box<dyn UnifiedEvent>>> {
|
||||
let mut instruction_events = Vec::new();
|
||||
// 获取交易的指令和账户
|
||||
let compiled_instructions = versioned_tx.message.instructions();
|
||||
let accounts = versioned_tx.message.static_account_keys();
|
||||
// 检查交易中是否包含程序
|
||||
let has_program = accounts.iter().any(|account| self.should_handle(account));
|
||||
if has_program {
|
||||
// 解析每个指令
|
||||
for instruction in compiled_instructions {
|
||||
if let Some(program_id) = accounts.get(instruction.program_id_index as usize) {
|
||||
if self.should_handle(program_id) {
|
||||
// 验证账户索引的有效性
|
||||
let all_accounts_valid = instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.all(|&acc_idx| (acc_idx as usize) < accounts.len());
|
||||
|
||||
if all_accounts_valid {
|
||||
if let Ok(events) = self
|
||||
.parse_instruction(instruction, accounts, signature, slot)
|
||||
.await
|
||||
{
|
||||
instruction_events.extend(events);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(instruction_events)
|
||||
}
|
||||
|
||||
async fn parse_versioned_transaction(
|
||||
&self,
|
||||
versioned_tx: &VersionedTransaction,
|
||||
signature: &str,
|
||||
slot: Option<u64>,
|
||||
bot_wallet: Option<Pubkey>,
|
||||
) -> Result<Vec<Box<dyn UnifiedEvent>>> {
|
||||
let events = self.parse_instruction_events_from_versioned_transaction(versioned_tx, signature, slot)
|
||||
.await.unwrap_or_else(|_e| vec![]);
|
||||
Ok(self.process_events(events, bot_wallet))
|
||||
}
|
||||
|
||||
async fn parse_transaction(
|
||||
&self,
|
||||
tx: EncodedTransactionWithStatusMeta,
|
||||
signature: &str,
|
||||
slot: Option<u64>,
|
||||
bot_wallet: Option<Pubkey>,
|
||||
) -> Result<Vec<Box<dyn UnifiedEvent>>> {
|
||||
let transaction = tx.transaction;
|
||||
let mut instruction_events = Vec::new();
|
||||
|
||||
// 解析指令事件
|
||||
if let Some(versioned_tx) = transaction.decode() {
|
||||
instruction_events = self
|
||||
.parse_instruction_events_from_versioned_transaction(&versioned_tx, signature, slot)
|
||||
.await
|
||||
.unwrap_or_else(|_e| vec![]);
|
||||
}
|
||||
|
||||
// 解析内联指令事件
|
||||
let mut inner_instruction_events = Vec::new();
|
||||
// 检查交易元数据
|
||||
let meta = tx
|
||||
.meta
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?;
|
||||
// 检查交易是否成功
|
||||
if meta.err.is_none() {
|
||||
let inner_instructions = meta.inner_instructions.as_ref().unwrap();
|
||||
for inner_instruction in inner_instructions {
|
||||
for instruction in &inner_instruction.instructions {
|
||||
match instruction {
|
||||
UiInstruction::Compiled(compiled) => {
|
||||
if let Ok(events) = self
|
||||
.parse_inner_instruction(compiled, signature, slot)
|
||||
.await
|
||||
{
|
||||
inner_instruction_events.extend(events);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if instruction_events.len() > 0 && inner_instruction_events.len() > 0 {
|
||||
for instruction_event in &mut instruction_events {
|
||||
for inner_instruction_event in &inner_instruction_events {
|
||||
if instruction_event.id() == inner_instruction_event.id()
|
||||
&& instruction_event.event_type() == inner_instruction_event.event_type()
|
||||
{
|
||||
instruction_event.merge(inner_instruction_event.clone_boxed());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(self.process_events(instruction_events, bot_wallet))
|
||||
}
|
||||
|
||||
fn process_events(&self, mut events: Vec<Box<dyn UnifiedEvent>>, bot_wallet: Option<Pubkey>) -> Vec<Box<dyn UnifiedEvent>> {
|
||||
let mut dev_address = None;
|
||||
let mut raydium_dev_address = None;
|
||||
for event in &mut events {
|
||||
if let Some(token_info) = event.as_any().downcast_ref::<PumpFunCreateTokenEvent>() {
|
||||
dev_address = Some(token_info.user);
|
||||
} else if let Some(trade_info) =
|
||||
event.as_any_mut().downcast_mut::<PumpFunTradeEvent>()
|
||||
{
|
||||
if Some(trade_info.user) == dev_address {
|
||||
trade_info.is_dev_create_token_trade = true;
|
||||
} else if Some(trade_info.user) == bot_wallet {
|
||||
trade_info.is_bot = true;
|
||||
} else {
|
||||
trade_info.is_dev_create_token_trade = false;
|
||||
}
|
||||
}
|
||||
if let Some(pool_info) = event
|
||||
.as_any()
|
||||
.downcast_ref::<RaydiumLaunchpadPoolCreateEvent>()
|
||||
{
|
||||
raydium_dev_address = Some(pool_info.creator);
|
||||
} else if let Some(trade_info) = event
|
||||
.as_any_mut()
|
||||
.downcast_mut::<RaydiumLaunchpadTradeEvent>()
|
||||
{
|
||||
if Some(trade_info.payer) == raydium_dev_address {
|
||||
trade_info.is_dev_create_token_trade = true;
|
||||
} else if Some(trade_info.payer) == bot_wallet {
|
||||
trade_info.is_bot = true;
|
||||
} else {
|
||||
trade_info.is_dev_create_token_trade = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
async fn parse_inner_instruction(
|
||||
&self,
|
||||
instruction: &UiCompiledInstruction,
|
||||
signature: &str,
|
||||
slot: Option<u64>,
|
||||
) -> Result<Vec<Box<dyn UnifiedEvent>>> {
|
||||
let slot = slot.unwrap_or(0);
|
||||
let events = self.parse_events_from_inner_instruction(instruction, signature, slot);
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
async fn parse_instruction(
|
||||
&self,
|
||||
instruction: &CompiledInstruction,
|
||||
accounts: &[Pubkey],
|
||||
signature: &str,
|
||||
slot: Option<u64>,
|
||||
) -> Result<Vec<Box<dyn UnifiedEvent>>> {
|
||||
let slot = slot.unwrap_or(0);
|
||||
let events = self.parse_events_from_instruction(instruction, accounts, signature, slot);
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
/// 检查是否应该处理此程序ID
|
||||
fn should_handle(&self, program_id: &Pubkey) -> bool;
|
||||
|
||||
/// 获取支持的程序ID列表
|
||||
fn supported_program_ids(&self) -> Vec<Pubkey>;
|
||||
}
|
||||
|
||||
// 为Box<dyn UnifiedEvent>实现Clone
|
||||
impl Clone for Box<dyn UnifiedEvent> {
|
||||
fn clone(&self) -> Self {
|
||||
self.clone_boxed()
|
||||
}
|
||||
}
|
||||
|
||||
/// 通用事件解析器配置
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GenericEventParseConfig {
|
||||
pub inner_instruction_discriminator: &'static str,
|
||||
pub instruction_discriminator: &'static [u8],
|
||||
pub event_type: EventType,
|
||||
pub inner_instruction_parser: InnerInstructionEventParser,
|
||||
pub instruction_parser: InstructionEventParser,
|
||||
}
|
||||
|
||||
/// 内联指令事件解析器
|
||||
pub type InnerInstructionEventParser =
|
||||
fn(data: &[u8], metadata: EventMetadata) -> Option<Box<dyn UnifiedEvent>>;
|
||||
|
||||
/// 指令事件解析器
|
||||
pub type InstructionEventParser =
|
||||
fn(data: &[u8], accounts: &[Pubkey], metadata: EventMetadata) -> Option<Box<dyn UnifiedEvent>>;
|
||||
|
||||
/// 通用事件解析器基类
|
||||
pub struct GenericEventParser {
|
||||
program_id: Pubkey,
|
||||
protocol_type: ProtocolType,
|
||||
inner_instruction_configs: HashMap<&'static str, Vec<GenericEventParseConfig>>,
|
||||
instruction_configs: HashMap<Vec<u8>, Vec<GenericEventParseConfig>>,
|
||||
}
|
||||
|
||||
impl GenericEventParser {
|
||||
/// 创建新的通用事件解析器
|
||||
pub fn new(
|
||||
program_id: Pubkey,
|
||||
protocol_type: ProtocolType,
|
||||
configs: Vec<GenericEventParseConfig>,
|
||||
) -> Self {
|
||||
let mut inner_instruction_configs = HashMap::new();
|
||||
let mut instruction_configs = HashMap::new();
|
||||
|
||||
for config in configs {
|
||||
inner_instruction_configs
|
||||
.entry(config.inner_instruction_discriminator)
|
||||
.or_insert(vec![])
|
||||
.push(config.clone());
|
||||
instruction_configs
|
||||
.entry(config.instruction_discriminator.to_vec())
|
||||
.or_insert(vec![])
|
||||
.push(config);
|
||||
}
|
||||
|
||||
Self {
|
||||
program_id,
|
||||
protocol_type,
|
||||
inner_instruction_configs,
|
||||
instruction_configs,
|
||||
}
|
||||
}
|
||||
|
||||
/// 通用的内联指令解析方法
|
||||
fn parse_inner_instruction_event(
|
||||
&self,
|
||||
config: &GenericEventParseConfig,
|
||||
data: &[u8],
|
||||
signature: &str,
|
||||
slot: u64,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
let metadata = EventMetadata::new(
|
||||
signature.to_string(),
|
||||
signature.to_string(),
|
||||
slot,
|
||||
self.protocol_type.clone(),
|
||||
config.event_type.clone(),
|
||||
self.program_id,
|
||||
);
|
||||
(config.inner_instruction_parser)(data, metadata)
|
||||
}
|
||||
|
||||
/// 通用的指令解析方法
|
||||
fn parse_instruction_event(
|
||||
&self,
|
||||
config: &GenericEventParseConfig,
|
||||
data: &[u8],
|
||||
account_pubkeys: &[Pubkey],
|
||||
signature: &str,
|
||||
slot: u64,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
let metadata = EventMetadata::new(
|
||||
signature.to_string(),
|
||||
signature.to_string(),
|
||||
slot,
|
||||
self.protocol_type.clone(),
|
||||
config.event_type.clone(),
|
||||
self.program_id,
|
||||
);
|
||||
(config.instruction_parser)(data, account_pubkeys, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl EventParser for GenericEventParser {
|
||||
/// 从内联指令中解析事件数据
|
||||
fn parse_events_from_inner_instruction(
|
||||
&self,
|
||||
inner_instruction: &UiCompiledInstruction,
|
||||
signature: &str,
|
||||
slot: u64,
|
||||
) -> Vec<Box<dyn UnifiedEvent>> {
|
||||
let inner_instruction_data = inner_instruction.data.clone();
|
||||
let inner_instruction_data_decoded =
|
||||
bs58::decode(inner_instruction_data).into_vec().unwrap();
|
||||
if inner_instruction_data_decoded.len() < 16 {
|
||||
return Vec::new();
|
||||
}
|
||||
let inner_instruction_data_decoded_str =
|
||||
format!("0x{}", hex::encode(&inner_instruction_data_decoded));
|
||||
let data = &inner_instruction_data_decoded[16..];
|
||||
let mut events = Vec::new();
|
||||
for (disc, configs) in &self.inner_instruction_configs {
|
||||
if discriminator_matches(&inner_instruction_data_decoded_str, disc) {
|
||||
for config in configs {
|
||||
if let Some(event) =
|
||||
self.parse_inner_instruction_event(config, data, signature, slot)
|
||||
{
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
/// 从指令中解析事件
|
||||
fn parse_events_from_instruction(
|
||||
&self,
|
||||
instruction: &CompiledInstruction,
|
||||
accounts: &[Pubkey],
|
||||
signature: &str,
|
||||
slot: u64,
|
||||
) -> Vec<Box<dyn UnifiedEvent>> {
|
||||
let mut events = Vec::new();
|
||||
for (disc, configs) in &self.instruction_configs {
|
||||
if instruction.data.len() < disc.len() {
|
||||
continue;
|
||||
}
|
||||
let discriminator = &instruction.data[..disc.len()];
|
||||
let data = &instruction.data[disc.len()..];
|
||||
if discriminator == disc {
|
||||
// 验证账户索引
|
||||
if !validate_account_indices(&instruction.accounts, accounts.len()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let account_pubkeys: Vec<Pubkey> = instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|&idx| accounts[idx as usize])
|
||||
.collect();
|
||||
for config in configs {
|
||||
if let Some(event) = self.parse_instruction_event(
|
||||
config,
|
||||
data,
|
||||
&account_pubkeys,
|
||||
signature,
|
||||
slot,
|
||||
) {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
events
|
||||
}
|
||||
|
||||
fn should_handle(&self, program_id: &Pubkey) -> bool {
|
||||
*program_id == self.program_id
|
||||
}
|
||||
|
||||
fn supported_program_ids(&self) -> Vec<Pubkey> {
|
||||
vec![self.program_id]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::event_parser::protocols::{
|
||||
pumpfun::parser::PUMPFUN_PROGRAM_ID, pumpswap::parser::PUMPSWAP_PROGRAM_ID, raydium_launchpad::parser::RAYDIUM_LAUNCHPAD_PROGRAM_ID, RaydiumLaunchpadEventParser,
|
||||
};
|
||||
|
||||
use super::{
|
||||
core::traits::EventParser,
|
||||
protocols::{pumpfun::PumpFunEventParser, pumpswap::PumpSwapEventParser},
|
||||
};
|
||||
|
||||
/// 支持的协议
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Protocol {
|
||||
PumpSwap,
|
||||
PumpFun,
|
||||
RaydiumLaunchpad,
|
||||
}
|
||||
|
||||
impl Protocol {
|
||||
pub fn get_program_id(&self) -> Vec<Pubkey> {
|
||||
match self {
|
||||
Protocol::PumpSwap => vec![PUMPSWAP_PROGRAM_ID],
|
||||
Protocol::PumpFun => vec![PUMPFUN_PROGRAM_ID],
|
||||
Protocol::RaydiumLaunchpad => vec![RAYDIUM_LAUNCHPAD_PROGRAM_ID],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Protocol {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Protocol::PumpSwap => write!(f, "PumpSwap"),
|
||||
Protocol::PumpFun => write!(f, "PumpFun"),
|
||||
Protocol::RaydiumLaunchpad => write!(f, "RaydiumLaunchpad"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Protocol {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"pumpswap" => Ok(Protocol::PumpSwap),
|
||||
"pumpfun" => Ok(Protocol::PumpFun),
|
||||
"raydiumlaunchpad" => Ok(Protocol::RaydiumLaunchpad),
|
||||
_ => Err(anyhow!("Unsupported protocol: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 事件解析器工厂 - 用于创建不同协议的事件解析器
|
||||
pub struct EventParserFactory;
|
||||
|
||||
impl EventParserFactory {
|
||||
/// 创建指定协议的事件解析器
|
||||
pub fn create_parser(protocol: Protocol) -> Arc<dyn EventParser> {
|
||||
match protocol {
|
||||
Protocol::PumpSwap => Arc::new(PumpSwapEventParser::new()),
|
||||
Protocol::PumpFun => Arc::new(PumpFunEventParser::new()),
|
||||
Protocol::RaydiumLaunchpad => Arc::new(RaydiumLaunchpadEventParser::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建所有协议的事件解析器
|
||||
pub fn create_all_parsers() -> Vec<Arc<dyn EventParser>> {
|
||||
Self::supported_protocols()
|
||||
.into_iter()
|
||||
.map(Self::create_parser)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 获取所有支持的协议
|
||||
pub fn supported_protocols() -> Vec<Protocol> {
|
||||
vec![Protocol::PumpSwap]
|
||||
}
|
||||
|
||||
/// 检查协议是否支持
|
||||
pub fn is_supported(protocol: &Protocol) -> bool {
|
||||
Self::supported_protocols().contains(protocol)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
pub mod common;
|
||||
pub mod core;
|
||||
pub mod factory;
|
||||
pub mod protocols;
|
||||
|
||||
pub use core::traits::{EventParser, UnifiedEvent};
|
||||
pub use factory::{EventParserFactory, Protocol};
|
||||
|
||||
/// 宏:简化 downcast_ref 模式匹配
|
||||
///
|
||||
/// # 使用示例
|
||||
/// ```
|
||||
/// use sol_trade_sdk::event_parser::match_event;
|
||||
///
|
||||
/// match_event!(event, {
|
||||
/// PumpSwapCreatePoolEvent => |typed_event| {
|
||||
/// println!("CreatePool event: {:?}", typed_event);
|
||||
/// },
|
||||
/// PumpSwapDepositEvent => |typed_event| {
|
||||
/// // 处理存款事件
|
||||
/// },
|
||||
/// });
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! match_event {
|
||||
($event:expr, {
|
||||
$($event_type:ty => $handler:expr),* $(,)?
|
||||
}) => {
|
||||
$(
|
||||
if let Some(typed_event) = $event.as_any().downcast_ref::<$event_type>() {
|
||||
$handler(typed_event.clone());
|
||||
} else
|
||||
)*
|
||||
{
|
||||
// 默认情况:什么都不做
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 重新导出宏以便于使用
|
||||
pub use match_event;
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod pumpfun;
|
||||
pub mod pumpswap;
|
||||
pub mod raydium_launchpad;
|
||||
|
||||
pub use pumpfun::PumpFunEventParser;
|
||||
pub use pumpswap::PumpSwapEventParser;
|
||||
pub use raydium_launchpad::RaydiumLaunchpadEventParser;
|
||||
@@ -0,0 +1,113 @@
|
||||
use borsh::BorshDeserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
use crate::event_parser::{common::EventMetadata, core::traits::UnifiedEvent};
|
||||
use crate::impl_unified_event;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct PumpFunCreateTokenEvent {
|
||||
#[borsh(skip)]
|
||||
pub metadata: EventMetadata,
|
||||
pub name: String,
|
||||
pub symbol: String,
|
||||
pub uri: String,
|
||||
pub mint: Pubkey,
|
||||
pub bonding_curve: Pubkey,
|
||||
pub user: Pubkey,
|
||||
pub creator: Pubkey,
|
||||
pub timestamp: i64,
|
||||
pub virtual_token_reserves: u64,
|
||||
pub virtual_sol_reserves: u64,
|
||||
pub real_token_reserves: u64,
|
||||
pub token_total_supply: u64,
|
||||
#[borsh(skip)]
|
||||
pub mint_authority: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub associated_bonding_curve: Pubkey,
|
||||
}
|
||||
|
||||
impl_unified_event!(
|
||||
PumpFunCreateTokenEvent,
|
||||
mint,
|
||||
bonding_curve,
|
||||
user,
|
||||
creator,
|
||||
timestamp,
|
||||
virtual_token_reserves,
|
||||
virtual_sol_reserves,
|
||||
real_token_reserves,
|
||||
token_total_supply
|
||||
);
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct PumpFunTradeEvent {
|
||||
#[borsh(skip)]
|
||||
pub metadata: EventMetadata,
|
||||
pub mint: Pubkey,
|
||||
pub sol_amount: u64,
|
||||
pub token_amount: u64,
|
||||
pub is_buy: bool,
|
||||
pub user: Pubkey,
|
||||
pub timestamp: i64,
|
||||
pub virtual_sol_reserves: u64,
|
||||
pub virtual_token_reserves: u64,
|
||||
pub real_sol_reserves: u64,
|
||||
pub real_token_reserves: u64,
|
||||
pub fee_recipient: Pubkey,
|
||||
pub fee_basis_points: u64,
|
||||
pub fee: u64,
|
||||
pub creator: Pubkey,
|
||||
pub creator_fee_basis_points: u64,
|
||||
pub creator_fee: u64,
|
||||
#[borsh(skip)]
|
||||
pub bonding_curve: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub associated_bonding_curve: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub associated_user: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub creator_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub max_sol_cost: u64,
|
||||
#[borsh(skip)]
|
||||
pub min_sol_output: u64,
|
||||
#[borsh(skip)]
|
||||
pub amount: u64,
|
||||
#[borsh(skip)]
|
||||
pub is_bot: bool,
|
||||
#[borsh(skip)]
|
||||
pub is_dev_create_token_trade: bool, // 是否是dev创建token的交易
|
||||
}
|
||||
|
||||
impl_unified_event!(
|
||||
PumpFunTradeEvent,
|
||||
mint,
|
||||
sol_amount,
|
||||
token_amount,
|
||||
is_buy,
|
||||
user,
|
||||
timestamp,
|
||||
virtual_sol_reserves,
|
||||
virtual_token_reserves,
|
||||
real_sol_reserves,
|
||||
real_token_reserves,
|
||||
fee_recipient,
|
||||
fee_basis_points,
|
||||
fee,
|
||||
creator,
|
||||
creator_fee_basis_points,
|
||||
creator_fee
|
||||
);
|
||||
|
||||
/// 事件鉴别器常量
|
||||
pub mod discriminators {
|
||||
// 事件鉴别器
|
||||
pub const CREATE_TOKEN_EVENT: &str = "0xe445a52e51cb9a1d1b72a94ddeeb6376";
|
||||
pub const TRADE_EVENT: &str = "0xe445a52e51cb9a1dbddb7fd34ee661ee";
|
||||
|
||||
// 指令鉴别器
|
||||
pub const CREATE_TOKEN_IX: &[u8] = &[24, 30, 200, 40, 5, 28, 7, 119];
|
||||
pub const BUY_IX: &[u8] = &[102, 6, 61, 18, 1, 218, 235, 234];
|
||||
pub const SELL_IX: &[u8] = &[51, 230, 133, 164, 1, 127, 131, 173];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod events;
|
||||
pub mod parser;
|
||||
|
||||
pub use events::*;
|
||||
pub use parser::PumpFunEventParser;
|
||||
@@ -0,0 +1,250 @@
|
||||
use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey};
|
||||
use solana_transaction_status::UiCompiledInstruction;
|
||||
|
||||
use crate::event_parser::{
|
||||
common::{utils::*, EventMetadata, EventType, ProtocolType},
|
||||
core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent},
|
||||
protocols::pumpfun::{discriminators, PumpFunCreateTokenEvent, PumpFunTradeEvent},
|
||||
};
|
||||
|
||||
/// PumpFun程序ID
|
||||
pub const PUMPFUN_PROGRAM_ID: Pubkey =
|
||||
solana_sdk::pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
||||
|
||||
/// PumpFun事件解析器
|
||||
pub struct PumpFunEventParser {
|
||||
inner: GenericEventParser,
|
||||
}
|
||||
|
||||
impl PumpFunEventParser {
|
||||
pub fn new() -> Self {
|
||||
// 配置所有事件类型
|
||||
let configs = vec![
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::CREATE_TOKEN_EVENT,
|
||||
instruction_discriminator: discriminators::CREATE_TOKEN_IX,
|
||||
event_type: EventType::PumpFunCreateToken,
|
||||
inner_instruction_parser: Self::parse_create_token_inner_instruction,
|
||||
instruction_parser: Self::parse_create_token_instruction,
|
||||
},
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::TRADE_EVENT,
|
||||
instruction_discriminator: discriminators::BUY_IX,
|
||||
event_type: EventType::PumpFunBuy,
|
||||
inner_instruction_parser: Self::parse_trade_inner_instruction,
|
||||
instruction_parser: Self::parse_buy_instruction,
|
||||
},
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::TRADE_EVENT,
|
||||
instruction_discriminator: discriminators::SELL_IX,
|
||||
event_type: EventType::PumpFunSell,
|
||||
inner_instruction_parser: Self::parse_trade_inner_instruction,
|
||||
instruction_parser: Self::parse_sell_instruction,
|
||||
},
|
||||
];
|
||||
|
||||
let inner = GenericEventParser::new(PUMPFUN_PROGRAM_ID, ProtocolType::PumpFun, configs);
|
||||
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
/// 解析创建代币日志事件
|
||||
fn parse_create_token_inner_instruction(
|
||||
data: &[u8],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if let Ok(event) = borsh::from_slice::<PumpFunCreateTokenEvent>(data) {
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature,
|
||||
event.name,
|
||||
event.symbol,
|
||||
event.mint.to_string()
|
||||
));
|
||||
Some(Box::new(PumpFunCreateTokenEvent {
|
||||
metadata: metadata,
|
||||
..event
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析交易事件
|
||||
fn parse_trade_inner_instruction(
|
||||
data: &[u8],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if let Ok(event) = borsh::from_slice::<PumpFunTradeEvent>(data) {
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature,
|
||||
event.mint.to_string(),
|
||||
event.user.to_string(),
|
||||
event.is_buy.to_string()
|
||||
));
|
||||
Some(Box::new(PumpFunTradeEvent {
|
||||
metadata: metadata,
|
||||
..event
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析创建代币指令事件
|
||||
fn parse_create_token_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let mut offset = 0;
|
||||
let name_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
|
||||
offset += 4;
|
||||
let name = String::from_utf8_lossy(&data[offset..offset + name_len]);
|
||||
offset += name_len;
|
||||
let symbol_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
|
||||
offset += 4;
|
||||
let symbol = String::from_utf8_lossy(&data[offset..offset + symbol_len]);
|
||||
offset += symbol_len;
|
||||
let uri_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
|
||||
offset += 4;
|
||||
let uri = String::from_utf8_lossy(&data[offset..offset + uri_len]);
|
||||
offset += uri_len;
|
||||
let creator = if offset + 32 <= data.len() {
|
||||
Pubkey::new_from_array(data[offset..offset + 32].try_into().ok()?)
|
||||
} else {
|
||||
Pubkey::default()
|
||||
};
|
||||
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature,
|
||||
name,
|
||||
symbol,
|
||||
accounts[0].to_string()
|
||||
));
|
||||
|
||||
Some(Box::new(PumpFunCreateTokenEvent {
|
||||
metadata,
|
||||
name: name.to_string(),
|
||||
symbol: symbol.to_string(),
|
||||
uri: uri.to_string(),
|
||||
creator,
|
||||
mint: accounts[0],
|
||||
mint_authority: accounts[1],
|
||||
bonding_curve: accounts[2],
|
||||
associated_bonding_curve: accounts[3],
|
||||
user: accounts[7],
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
// 解析买入指令事件
|
||||
fn parse_buy_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let amount = u64::from_le_bytes(data[0..8].try_into().unwrap());
|
||||
let max_sol_cost = u64::from_le_bytes(data[8..16].try_into().unwrap());
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature,
|
||||
accounts[2].to_string(),
|
||||
accounts[6].to_string(),
|
||||
true.to_string()
|
||||
));
|
||||
Some(Box::new(PumpFunTradeEvent {
|
||||
metadata,
|
||||
fee_recipient: accounts[1],
|
||||
mint: accounts[2],
|
||||
bonding_curve: accounts[3],
|
||||
associated_bonding_curve: accounts[4],
|
||||
associated_user: accounts[5],
|
||||
user: accounts[6],
|
||||
creator_vault: accounts[8],
|
||||
max_sol_cost,
|
||||
amount,
|
||||
is_buy: true,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
// 解析卖出指令事件
|
||||
fn parse_sell_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let amount = u64::from_le_bytes(data[0..8].try_into().unwrap());
|
||||
let min_sol_output = u64::from_le_bytes(data[8..16].try_into().unwrap());
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature,
|
||||
accounts[2].to_string(),
|
||||
accounts[6].to_string(),
|
||||
false.to_string()
|
||||
));
|
||||
Some(Box::new(PumpFunTradeEvent {
|
||||
metadata,
|
||||
fee_recipient: accounts[1],
|
||||
mint: accounts[2],
|
||||
bonding_curve: accounts[3],
|
||||
associated_bonding_curve: accounts[4],
|
||||
associated_user: accounts[5],
|
||||
user: accounts[6],
|
||||
creator_vault: accounts[8],
|
||||
min_sol_output,
|
||||
amount,
|
||||
is_buy: false,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl EventParser for PumpFunEventParser {
|
||||
fn parse_events_from_inner_instruction(
|
||||
&self,
|
||||
inner_instruction: &UiCompiledInstruction,
|
||||
signature: &str,
|
||||
slot: u64,
|
||||
) -> Vec<Box<dyn UnifiedEvent>> {
|
||||
self.inner
|
||||
.parse_events_from_inner_instruction(inner_instruction, signature, slot)
|
||||
}
|
||||
|
||||
fn parse_events_from_instruction(
|
||||
&self,
|
||||
instruction: &CompiledInstruction,
|
||||
accounts: &[Pubkey],
|
||||
signature: &str,
|
||||
slot: u64,
|
||||
) -> Vec<Box<dyn UnifiedEvent>> {
|
||||
self.inner
|
||||
.parse_events_from_instruction(instruction, accounts, signature, slot)
|
||||
}
|
||||
|
||||
fn should_handle(&self, program_id: &Pubkey) -> bool {
|
||||
self.inner.should_handle(program_id)
|
||||
}
|
||||
|
||||
fn supported_program_ids(&self) -> Vec<Pubkey> {
|
||||
self.inner.supported_program_ids()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
use borsh::BorshDeserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
use crate::event_parser::{common::EventMetadata, core::traits::UnifiedEvent};
|
||||
use crate::impl_unified_event;
|
||||
|
||||
/// 买入事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct PumpSwapBuyEvent {
|
||||
#[borsh(skip)]
|
||||
pub metadata: EventMetadata,
|
||||
pub timestamp: i64,
|
||||
pub base_amount_out: u64,
|
||||
pub max_quote_amount_in: u64,
|
||||
pub user_base_token_reserves: u64,
|
||||
pub user_quote_token_reserves: u64,
|
||||
pub pool_base_token_reserves: u64,
|
||||
pub pool_quote_token_reserves: u64,
|
||||
pub quote_amount_in: u64,
|
||||
pub lp_fee_basis_points: u64,
|
||||
pub lp_fee: u64,
|
||||
pub protocol_fee_basis_points: u64,
|
||||
pub protocol_fee: u64,
|
||||
pub quote_amount_in_with_lp_fee: u64,
|
||||
pub user_quote_amount_in: u64,
|
||||
pub pool: Pubkey,
|
||||
pub user: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
pub protocol_fee_recipient: Pubkey,
|
||||
pub protocol_fee_recipient_token_account: Pubkey,
|
||||
pub coin_creator: Pubkey,
|
||||
pub coin_creator_fee_basis_points: u64,
|
||||
pub coin_creator_fee: u64,
|
||||
#[borsh(skip)]
|
||||
pub base_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub quote_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_base_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub coin_creator_vault_ata: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub coin_creator_vault_authority: Pubkey,
|
||||
}
|
||||
|
||||
// 使用宏生成UnifiedEvent实现,指定需要合并的字段
|
||||
impl_unified_event!(
|
||||
PumpSwapBuyEvent,
|
||||
timestamp,
|
||||
base_amount_out,
|
||||
max_quote_amount_in,
|
||||
user_base_token_reserves,
|
||||
user_quote_token_reserves,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
quote_amount_in,
|
||||
lp_fee_basis_points,
|
||||
lp_fee,
|
||||
protocol_fee_basis_points,
|
||||
protocol_fee,
|
||||
quote_amount_in_with_lp_fee,
|
||||
user_quote_amount_in,
|
||||
pool,
|
||||
user,
|
||||
user_base_token_account,
|
||||
user_quote_token_account,
|
||||
protocol_fee_recipient,
|
||||
protocol_fee_recipient_token_account,
|
||||
coin_creator,
|
||||
coin_creator_fee_basis_points,
|
||||
coin_creator_fee
|
||||
);
|
||||
|
||||
/// 卖出事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct PumpSwapSellEvent {
|
||||
#[borsh(skip)]
|
||||
pub metadata: EventMetadata,
|
||||
pub timestamp: i64,
|
||||
pub base_amount_in: u64,
|
||||
pub min_quote_amount_out: u64,
|
||||
pub user_base_token_reserves: u64,
|
||||
pub user_quote_token_reserves: u64,
|
||||
pub pool_base_token_reserves: u64,
|
||||
pub pool_quote_token_reserves: u64,
|
||||
pub quote_amount_out: u64,
|
||||
pub lp_fee_basis_points: u64,
|
||||
pub lp_fee: u64,
|
||||
pub protocol_fee_basis_points: u64,
|
||||
pub protocol_fee: u64,
|
||||
pub quote_amount_out_without_lp_fee: u64,
|
||||
pub user_quote_amount_out: u64,
|
||||
pub pool: Pubkey,
|
||||
pub user: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
pub protocol_fee_recipient: Pubkey,
|
||||
pub protocol_fee_recipient_token_account: Pubkey,
|
||||
pub coin_creator: Pubkey,
|
||||
pub coin_creator_fee_basis_points: u64,
|
||||
pub coin_creator_fee: u64,
|
||||
#[borsh(skip)]
|
||||
pub base_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub quote_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_base_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub coin_creator_vault_ata: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub coin_creator_vault_authority: Pubkey,
|
||||
}
|
||||
|
||||
// 使用宏生成UnifiedEvent实现,指定需要合并的字段
|
||||
impl_unified_event!(
|
||||
PumpSwapSellEvent,
|
||||
timestamp,
|
||||
base_amount_in,
|
||||
min_quote_amount_out,
|
||||
user_base_token_reserves,
|
||||
user_quote_token_reserves,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
quote_amount_out,
|
||||
lp_fee_basis_points,
|
||||
lp_fee,
|
||||
protocol_fee_basis_points,
|
||||
protocol_fee,
|
||||
quote_amount_out_without_lp_fee,
|
||||
user_quote_amount_out,
|
||||
pool,
|
||||
user,
|
||||
user_base_token_account,
|
||||
user_quote_token_account,
|
||||
protocol_fee_recipient,
|
||||
protocol_fee_recipient_token_account,
|
||||
coin_creator,
|
||||
coin_creator_fee_basis_points,
|
||||
coin_creator_fee
|
||||
);
|
||||
|
||||
/// 创建池子事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct PumpSwapCreatePoolEvent {
|
||||
#[borsh(skip)]
|
||||
pub metadata: EventMetadata,
|
||||
pub timestamp: i64,
|
||||
pub index: u16,
|
||||
pub creator: Pubkey,
|
||||
pub base_mint: Pubkey,
|
||||
pub quote_mint: Pubkey,
|
||||
pub base_mint_decimals: u8,
|
||||
pub quote_mint_decimals: u8,
|
||||
pub base_amount_in: u64,
|
||||
pub quote_amount_in: u64,
|
||||
pub pool_base_amount: u64,
|
||||
pub pool_quote_amount: u64,
|
||||
pub minimum_liquidity: u64,
|
||||
pub initial_liquidity: u64,
|
||||
pub lp_token_amount_out: u64,
|
||||
pub pool_bump: u8,
|
||||
pub pool: Pubkey,
|
||||
pub lp_mint: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
pub coin_creator: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub user_pool_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_base_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
}
|
||||
|
||||
impl_unified_event!(
|
||||
PumpSwapCreatePoolEvent,
|
||||
timestamp,
|
||||
index,
|
||||
creator,
|
||||
base_mint,
|
||||
quote_mint,
|
||||
base_mint_decimals,
|
||||
quote_mint_decimals,
|
||||
base_amount_in,
|
||||
quote_amount_in,
|
||||
pool_base_amount,
|
||||
pool_quote_amount,
|
||||
minimum_liquidity,
|
||||
initial_liquidity,
|
||||
lp_token_amount_out,
|
||||
pool_bump,
|
||||
pool,
|
||||
lp_mint,
|
||||
user_base_token_account,
|
||||
user_quote_token_account,
|
||||
coin_creator
|
||||
);
|
||||
|
||||
/// 存款事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct PumpSwapDepositEvent {
|
||||
#[borsh(skip)]
|
||||
pub metadata: EventMetadata,
|
||||
pub timestamp: i64,
|
||||
pub lp_token_amount_out: u64,
|
||||
pub max_base_amount_in: u64,
|
||||
pub max_quote_amount_in: u64,
|
||||
pub user_base_token_reserves: u64,
|
||||
pub user_quote_token_reserves: u64,
|
||||
pub pool_base_token_reserves: u64,
|
||||
pub pool_quote_token_reserves: u64,
|
||||
pub base_amount_in: u64,
|
||||
pub quote_amount_in: u64,
|
||||
pub lp_mint_supply: u64,
|
||||
pub pool: Pubkey,
|
||||
pub user: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
pub user_pool_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub base_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub quote_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_base_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
}
|
||||
|
||||
impl_unified_event!(
|
||||
PumpSwapDepositEvent,
|
||||
timestamp,
|
||||
lp_token_amount_out,
|
||||
max_base_amount_in,
|
||||
max_quote_amount_in,
|
||||
user_base_token_reserves,
|
||||
user_quote_token_reserves,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
base_amount_in,
|
||||
quote_amount_in,
|
||||
lp_mint_supply,
|
||||
pool,
|
||||
user,
|
||||
user_base_token_account,
|
||||
user_quote_token_account,
|
||||
user_pool_token_account
|
||||
);
|
||||
|
||||
/// 提款事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct PumpSwapWithdrawEvent {
|
||||
#[borsh(skip)]
|
||||
pub metadata: EventMetadata,
|
||||
pub timestamp: i64,
|
||||
pub lp_token_amount_in: u64,
|
||||
pub min_base_amount_out: u64,
|
||||
pub min_quote_amount_out: u64,
|
||||
pub user_base_token_reserves: u64,
|
||||
pub user_quote_token_reserves: u64,
|
||||
pub pool_base_token_reserves: u64,
|
||||
pub pool_quote_token_reserves: u64,
|
||||
pub base_amount_out: u64,
|
||||
pub quote_amount_out: u64,
|
||||
pub lp_mint_supply: u64,
|
||||
pub pool: Pubkey,
|
||||
pub user: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
pub user_pool_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub base_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub quote_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_base_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
}
|
||||
|
||||
impl_unified_event!(
|
||||
PumpSwapWithdrawEvent,
|
||||
timestamp,
|
||||
lp_token_amount_in,
|
||||
min_base_amount_out,
|
||||
min_quote_amount_out,
|
||||
user_base_token_reserves,
|
||||
user_quote_token_reserves,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
base_amount_out,
|
||||
quote_amount_out,
|
||||
lp_mint_supply,
|
||||
pool,
|
||||
user,
|
||||
user_base_token_account,
|
||||
user_quote_token_account,
|
||||
user_pool_token_account
|
||||
);
|
||||
|
||||
/// 事件鉴别器常量
|
||||
pub mod discriminators {
|
||||
// 事件鉴别器
|
||||
pub const BUY_EVENT: &str = "0xe445a52e51cb9a1d67f4521f2cf57777";
|
||||
pub const SELL_EVENT: &str = "0xe445a52e51cb9a1d3e2f370aa503dc2a";
|
||||
pub const CREATE_POOL_EVENT: &str = "0xe445a52e51cb9a1db1310cd2a076a774";
|
||||
pub const DEPOSIT_EVENT: &str = "0xe445a52e51cb9a1d78f83d531f8e6b90";
|
||||
pub const WITHDRAW_EVENT: &str = "0xe445a52e51cb9a1d1609851aa02c47c0";
|
||||
|
||||
// 指令鉴别器
|
||||
pub const BUY_IX: &[u8] = &[102, 6, 61, 18, 1, 218, 235, 234];
|
||||
pub const SELL_IX: &[u8] = &[51, 230, 133, 164, 1, 127, 131, 173];
|
||||
pub const CREATE_POOL_IX: &[u8] = &[233, 146, 209, 142, 207, 104, 64, 188];
|
||||
pub const DEPOSIT_IX: &[u8] = &[242, 35, 198, 137, 82, 225, 242, 182];
|
||||
pub const WITHDRAW_IX: &[u8] = &[183, 18, 70, 156, 148, 109, 161, 34];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod events;
|
||||
pub mod parser;
|
||||
|
||||
pub use events::*;
|
||||
pub use parser::PumpSwapEventParser;
|
||||
@@ -0,0 +1,386 @@
|
||||
use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey};
|
||||
use solana_transaction_status::UiCompiledInstruction;
|
||||
|
||||
use crate::event_parser::{
|
||||
common::{utils::*, EventMetadata, EventType, ProtocolType},
|
||||
core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent},
|
||||
protocols::pumpswap::{
|
||||
discriminators, PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent,
|
||||
PumpSwapSellEvent, PumpSwapWithdrawEvent,
|
||||
},
|
||||
};
|
||||
|
||||
/// PumpSwap程序ID
|
||||
pub const PUMPSWAP_PROGRAM_ID: Pubkey =
|
||||
solana_sdk::pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA");
|
||||
|
||||
/// PumpSwap事件解析器
|
||||
pub struct PumpSwapEventParser {
|
||||
inner: GenericEventParser,
|
||||
}
|
||||
|
||||
impl PumpSwapEventParser {
|
||||
pub fn new() -> Self {
|
||||
// 配置所有事件类型
|
||||
let configs = vec![
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::BUY_EVENT,
|
||||
instruction_discriminator: discriminators::BUY_IX,
|
||||
event_type: EventType::PumpSwapBuy,
|
||||
inner_instruction_parser: Self::parse_buy_inner_instruction,
|
||||
instruction_parser: Self::parse_buy_instruction,
|
||||
},
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::SELL_EVENT,
|
||||
instruction_discriminator: discriminators::SELL_IX,
|
||||
event_type: EventType::PumpSwapSell,
|
||||
inner_instruction_parser: Self::parse_sell_inner_instruction,
|
||||
instruction_parser: Self::parse_sell_instruction,
|
||||
},
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::CREATE_POOL_EVENT,
|
||||
instruction_discriminator: discriminators::CREATE_POOL_IX,
|
||||
event_type: EventType::PumpSwapCreatePool,
|
||||
inner_instruction_parser: Self::parse_create_pool_inner_instruction,
|
||||
instruction_parser: Self::parse_create_pool_instruction,
|
||||
},
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::DEPOSIT_EVENT,
|
||||
instruction_discriminator: discriminators::DEPOSIT_IX,
|
||||
event_type: EventType::PumpSwapDeposit,
|
||||
inner_instruction_parser: Self::parse_deposit_inner_instruction,
|
||||
instruction_parser: Self::parse_deposit_instruction,
|
||||
},
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::WITHDRAW_EVENT,
|
||||
instruction_discriminator: discriminators::WITHDRAW_IX,
|
||||
event_type: EventType::PumpSwapWithdraw,
|
||||
inner_instruction_parser: Self::parse_withdraw_inner_instruction,
|
||||
instruction_parser: Self::parse_withdraw_instruction,
|
||||
},
|
||||
];
|
||||
|
||||
let inner = GenericEventParser::new(PUMPSWAP_PROGRAM_ID, ProtocolType::PumpSwap, configs);
|
||||
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
/// 解析买入日志事件
|
||||
fn parse_buy_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if let Ok(event) = borsh::from_slice::<PumpSwapBuyEvent>(data) {
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature, event.user, event.pool, event.base_amount_out
|
||||
));
|
||||
Some(Box::new(PumpSwapBuyEvent {
|
||||
metadata: metadata,
|
||||
..event
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析卖出日志事件
|
||||
fn parse_sell_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if let Ok(event) = borsh::from_slice::<PumpSwapSellEvent>(data) {
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature, event.user, event.pool, event.base_amount_in
|
||||
));
|
||||
Some(Box::new(PumpSwapSellEvent {
|
||||
metadata: metadata,
|
||||
..event
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析创建池子日志事件
|
||||
fn parse_create_pool_inner_instruction(
|
||||
data: &[u8],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if let Ok(event) = borsh::from_slice::<PumpSwapCreatePoolEvent>(data) {
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature, event.pool, event.creator, event.base_amount_in
|
||||
));
|
||||
Some(Box::new(PumpSwapCreatePoolEvent {
|
||||
metadata: metadata,
|
||||
..event
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析存款日志事件
|
||||
fn parse_deposit_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if let Ok(event) = borsh::from_slice::<PumpSwapDepositEvent>(data) {
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature, event.pool, event.user, event.lp_token_amount_out
|
||||
));
|
||||
Some(Box::new(PumpSwapDepositEvent {
|
||||
metadata: metadata,
|
||||
..event
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析提款日志事件
|
||||
fn parse_withdraw_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if let Ok(event) = borsh::from_slice::<PumpSwapWithdrawEvent>(data) {
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature, event.pool, event.user, event.lp_token_amount_in
|
||||
));
|
||||
Some(Box::new(PumpSwapWithdrawEvent {
|
||||
metadata: metadata,
|
||||
..event
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析买入指令事件
|
||||
fn parse_buy_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let base_amount_out = read_u64_le(data, 0)?;
|
||||
let max_quote_amount_in = read_u64_le(data, 8)?;
|
||||
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature, accounts[1], accounts[0], base_amount_out
|
||||
));
|
||||
|
||||
Some(Box::new(PumpSwapBuyEvent {
|
||||
metadata,
|
||||
base_amount_out,
|
||||
max_quote_amount_in,
|
||||
pool: accounts[0],
|
||||
user: accounts[1],
|
||||
base_mint: accounts[3],
|
||||
quote_mint: accounts[4],
|
||||
user_base_token_account: accounts[5],
|
||||
user_quote_token_account: accounts[6],
|
||||
pool_base_token_account: accounts[7],
|
||||
pool_quote_token_account: accounts[8],
|
||||
protocol_fee_recipient: accounts[9],
|
||||
protocol_fee_recipient_token_account: accounts[10],
|
||||
coin_creator_vault_ata: accounts.get(17).copied().unwrap_or_default(),
|
||||
coin_creator_vault_authority: accounts.get(18).copied().unwrap_or_default(),
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
/// 解析卖出指令事件
|
||||
fn parse_sell_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let base_amount_in = read_u64_le(data, 0)?;
|
||||
let min_quote_amount_out = read_u64_le(data, 8)?;
|
||||
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature, accounts[1], accounts[0], base_amount_in
|
||||
));
|
||||
|
||||
Some(Box::new(PumpSwapSellEvent {
|
||||
metadata,
|
||||
base_amount_in,
|
||||
min_quote_amount_out,
|
||||
pool: accounts[0],
|
||||
user: accounts[1],
|
||||
base_mint: accounts[3],
|
||||
quote_mint: accounts[4],
|
||||
user_base_token_account: accounts[5],
|
||||
user_quote_token_account: accounts[6],
|
||||
pool_base_token_account: accounts[7],
|
||||
pool_quote_token_account: accounts[8],
|
||||
protocol_fee_recipient: accounts[9],
|
||||
protocol_fee_recipient_token_account: accounts[10],
|
||||
coin_creator_vault_ata: accounts.get(17).copied().unwrap_or_default(),
|
||||
coin_creator_vault_authority: accounts.get(18).copied().unwrap_or_default(),
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
/// 解析创建池子指令事件
|
||||
fn parse_create_pool_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 18 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let index = u16::from_le_bytes(data[0..2].try_into().ok()?);
|
||||
let base_amount_in = u64::from_le_bytes(data[2..10].try_into().ok()?);
|
||||
let quote_amount_in = u64::from_le_bytes(data[10..18].try_into().ok()?);
|
||||
let coin_creator = if data.len() >= 50 {
|
||||
Pubkey::new_from_array(data[18..50].try_into().ok()?)
|
||||
} else {
|
||||
Pubkey::default()
|
||||
};
|
||||
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature, accounts[0], accounts[2], base_amount_in
|
||||
));
|
||||
|
||||
Some(Box::new(PumpSwapCreatePoolEvent {
|
||||
metadata,
|
||||
index,
|
||||
base_amount_in,
|
||||
quote_amount_in,
|
||||
pool: accounts[0],
|
||||
creator: accounts[2],
|
||||
base_mint: accounts[3],
|
||||
quote_mint: accounts[4],
|
||||
lp_mint: accounts[5],
|
||||
user_base_token_account: accounts[6],
|
||||
user_quote_token_account: accounts[7],
|
||||
user_pool_token_account: accounts[8],
|
||||
pool_base_token_account: accounts[9],
|
||||
pool_quote_token_account: accounts[10],
|
||||
coin_creator,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
/// 解析存款指令事件
|
||||
fn parse_deposit_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 24 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let lp_token_amount_out = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let max_base_amount_in = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
let max_quote_amount_in = u64::from_le_bytes(data[16..24].try_into().ok()?);
|
||||
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature, accounts[0], accounts[2], lp_token_amount_out
|
||||
));
|
||||
|
||||
Some(Box::new(PumpSwapDepositEvent {
|
||||
metadata,
|
||||
lp_token_amount_out,
|
||||
max_base_amount_in,
|
||||
max_quote_amount_in,
|
||||
pool: accounts[0],
|
||||
user: accounts[2],
|
||||
base_mint: accounts[3],
|
||||
quote_mint: accounts[4],
|
||||
user_base_token_account: accounts[6],
|
||||
user_quote_token_account: accounts[7],
|
||||
user_pool_token_account: accounts[8],
|
||||
pool_base_token_account: accounts[9],
|
||||
pool_quote_token_account: accounts[10],
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
/// 解析提款指令事件
|
||||
fn parse_withdraw_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 24 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let lp_token_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let min_base_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
let min_quote_amount_out = u64::from_le_bytes(data[16..24].try_into().ok()?);
|
||||
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}-{}-{}",
|
||||
metadata.signature, accounts[0], accounts[2], lp_token_amount_in
|
||||
));
|
||||
|
||||
Some(Box::new(PumpSwapWithdrawEvent {
|
||||
metadata,
|
||||
lp_token_amount_in,
|
||||
min_base_amount_out,
|
||||
min_quote_amount_out,
|
||||
pool: accounts[0],
|
||||
user: accounts[2],
|
||||
base_mint: accounts[3],
|
||||
quote_mint: accounts[4],
|
||||
user_base_token_account: accounts[6],
|
||||
user_quote_token_account: accounts[7],
|
||||
user_pool_token_account: accounts[8],
|
||||
pool_base_token_account: accounts[9],
|
||||
pool_quote_token_account: accounts[10],
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl EventParser for PumpSwapEventParser {
|
||||
fn parse_events_from_inner_instruction(
|
||||
&self,
|
||||
inner_instruction: &UiCompiledInstruction,
|
||||
signature: &str,
|
||||
slot: u64,
|
||||
) -> Vec<Box<dyn UnifiedEvent>> {
|
||||
self.inner
|
||||
.parse_events_from_inner_instruction(inner_instruction, signature, slot)
|
||||
}
|
||||
|
||||
fn parse_events_from_instruction(
|
||||
&self,
|
||||
instruction: &CompiledInstruction,
|
||||
accounts: &[Pubkey],
|
||||
signature: &str,
|
||||
slot: u64,
|
||||
) -> Vec<Box<dyn UnifiedEvent>> {
|
||||
self.inner
|
||||
.parse_events_from_instruction(instruction, accounts, signature, slot)
|
||||
}
|
||||
|
||||
fn should_handle(&self, program_id: &Pubkey) -> bool {
|
||||
self.inner.should_handle(program_id)
|
||||
}
|
||||
|
||||
fn supported_program_ids(&self) -> Vec<Pubkey> {
|
||||
self.inner.supported_program_ids()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
use crate::event_parser::protocols::raydium_launchpad::types::{
|
||||
CurveParams, MintParams, PoolStatus, TradeDirection, VestingParams,
|
||||
};
|
||||
use crate::event_parser::{common::EventMetadata, core::traits::UnifiedEvent};
|
||||
use crate::impl_unified_event;
|
||||
use borsh::BorshDeserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// 买入事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct RaydiumLaunchpadTradeEvent {
|
||||
#[borsh(skip)]
|
||||
pub metadata: EventMetadata,
|
||||
pub pool_state: Pubkey,
|
||||
pub total_base_sell: u64,
|
||||
pub virtual_base: u64,
|
||||
pub virtual_quote: u64,
|
||||
pub real_base_before: u64,
|
||||
pub real_quote_before: u64,
|
||||
pub real_base_after: u64,
|
||||
pub real_quote_after: u64,
|
||||
pub amount_in: u64,
|
||||
pub amount_out: u64,
|
||||
pub protocol_fee: u64,
|
||||
pub platform_fee: u64,
|
||||
pub share_fee: u64,
|
||||
pub trade_direction: TradeDirection,
|
||||
pub pool_status: PoolStatus,
|
||||
#[borsh(skip)]
|
||||
pub minimum_amount_out: u64,
|
||||
#[borsh(skip)]
|
||||
pub maximum_amount_in: u64,
|
||||
#[borsh(skip)]
|
||||
pub share_fee_rate: u64,
|
||||
#[borsh(skip)]
|
||||
pub payer: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub user_base_token: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub user_quote_token: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub base_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub quote_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub base_token_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub quote_token_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub is_dev_create_token_trade: bool,
|
||||
#[borsh(skip)]
|
||||
pub is_bot: bool,
|
||||
}
|
||||
|
||||
// 使用宏生成UnifiedEvent实现,指定需要合并的字段
|
||||
impl_unified_event!(
|
||||
RaydiumLaunchpadTradeEvent,
|
||||
pool_state,
|
||||
total_base_sell,
|
||||
virtual_base,
|
||||
virtual_quote,
|
||||
real_base_before,
|
||||
real_quote_before,
|
||||
real_base_after,
|
||||
real_quote_after,
|
||||
amount_in,
|
||||
amount_out,
|
||||
protocol_fee,
|
||||
platform_fee,
|
||||
share_fee,
|
||||
trade_direction,
|
||||
pool_status
|
||||
);
|
||||
|
||||
/// 创建池事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct RaydiumLaunchpadPoolCreateEvent {
|
||||
#[borsh(skip)]
|
||||
pub metadata: EventMetadata,
|
||||
pub pool_state: Pubkey,
|
||||
pub creator: Pubkey,
|
||||
pub config: Pubkey,
|
||||
pub base_mint_param: MintParams,
|
||||
pub curve_param: CurveParams,
|
||||
pub vesting_param: VestingParams,
|
||||
#[borsh(skip)]
|
||||
pub payer: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub base_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub quote_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub base_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub quote_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub global_config: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub platform_config: Pubkey,
|
||||
}
|
||||
|
||||
// 使用宏生成UnifiedEvent实现,指定需要合并的字段
|
||||
impl_unified_event!(
|
||||
RaydiumLaunchpadPoolCreateEvent,
|
||||
pool_state,
|
||||
creator,
|
||||
config,
|
||||
base_mint_param,
|
||||
curve_param,
|
||||
vesting_param
|
||||
);
|
||||
|
||||
/// 事件鉴别器常量
|
||||
pub mod discriminators {
|
||||
// 事件鉴别器
|
||||
pub const TRADE_EVENT: &str = "0xe445a52e51cb9a1dbddb7fd34ee661ee";
|
||||
pub const POOL_CREATE_EVENT: &str = "0xe445a52e51cb9a1d97d7e20976a173ae";
|
||||
|
||||
// 指令鉴别器
|
||||
pub const BUY_EXACT_IN: &[u8] = &[250, 234, 13, 123, 213, 156, 19, 236];
|
||||
pub const BUY_EXACT_OUT: &[u8] = &[24, 211, 116, 40, 105, 3, 153, 56];
|
||||
pub const SELL_EXACT_IN: &[u8] = &[149, 39, 222, 155, 211, 124, 152, 26];
|
||||
pub const SELL_EXACT_OUT: &[u8] = &[95, 200, 71, 34, 8, 9, 11, 166];
|
||||
pub const INITIALIZE: &[u8] = &[175, 175, 109, 31, 13, 152, 155, 237];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod events;
|
||||
pub mod parser;
|
||||
pub mod types;
|
||||
|
||||
pub use events::*;
|
||||
pub use parser::RaydiumLaunchpadEventParser;
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,436 @@
|
||||
use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey};
|
||||
use solana_transaction_status::UiCompiledInstruction;
|
||||
|
||||
use crate::event_parser::{
|
||||
common::{utils::*, EventMetadata, EventType, ProtocolType},
|
||||
core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent},
|
||||
protocols::raydium_launchpad::{
|
||||
discriminators, ConstantCurve, CurveParams, FixedCurve, LinearCurve, MintParams,
|
||||
RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent, TradeDirection, VestingParams,
|
||||
},
|
||||
};
|
||||
|
||||
/// RaydiumLaunchpad程序ID
|
||||
pub const RAYDIUM_LAUNCHPAD_PROGRAM_ID: Pubkey =
|
||||
solana_sdk::pubkey!("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj");
|
||||
|
||||
/// RaydiumLaunchpad事件解析器
|
||||
pub struct RaydiumLaunchpadEventParser {
|
||||
inner: GenericEventParser,
|
||||
}
|
||||
|
||||
impl RaydiumLaunchpadEventParser {
|
||||
pub fn new() -> Self {
|
||||
// 配置所有事件类型
|
||||
let configs = vec![
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::TRADE_EVENT,
|
||||
instruction_discriminator: discriminators::BUY_EXACT_IN,
|
||||
event_type: EventType::RaydiumLaunchpadBuyExactIn,
|
||||
inner_instruction_parser: Self::parse_trade_inner_instruction,
|
||||
instruction_parser: Self::parse_buy_exact_in_instruction,
|
||||
},
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::TRADE_EVENT,
|
||||
instruction_discriminator: discriminators::BUY_EXACT_OUT,
|
||||
event_type: EventType::RaydiumLaunchpadBuyExactOut,
|
||||
inner_instruction_parser: Self::parse_trade_inner_instruction,
|
||||
instruction_parser: Self::parse_buy_exact_out_instruction,
|
||||
},
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::TRADE_EVENT,
|
||||
instruction_discriminator: discriminators::SELL_EXACT_IN,
|
||||
event_type: EventType::RaydiumLaunchpadSellExactIn,
|
||||
inner_instruction_parser: Self::parse_trade_inner_instruction,
|
||||
instruction_parser: Self::parse_sell_exact_in_instruction,
|
||||
},
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::TRADE_EVENT,
|
||||
instruction_discriminator: discriminators::SELL_EXACT_OUT,
|
||||
event_type: EventType::RaydiumLaunchpadSellExactOut,
|
||||
inner_instruction_parser: Self::parse_trade_inner_instruction,
|
||||
instruction_parser: Self::parse_sell_exact_out_instruction,
|
||||
},
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::POOL_CREATE_EVENT,
|
||||
instruction_discriminator: discriminators::INITIALIZE,
|
||||
event_type: EventType::RaydiumLaunchpadInitialize,
|
||||
inner_instruction_parser: Self::parse_pool_create_inner_instruction,
|
||||
instruction_parser: Self::parse_initialize_instruction,
|
||||
},
|
||||
];
|
||||
|
||||
let inner = GenericEventParser::new(
|
||||
RAYDIUM_LAUNCHPAD_PROGRAM_ID,
|
||||
ProtocolType::RaydiumLaunchpad,
|
||||
configs,
|
||||
);
|
||||
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
/// 解析创建池事件
|
||||
fn parse_pool_create_inner_instruction(
|
||||
data: &[u8],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if let Ok(event) = borsh::from_slice::<RaydiumLaunchpadPoolCreateEvent>(data) {
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!("{}", metadata.signature,));
|
||||
Some(Box::new(RaydiumLaunchpadPoolCreateEvent {
|
||||
metadata: metadata,
|
||||
..event
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析交易事件
|
||||
fn parse_trade_inner_instruction(
|
||||
data: &[u8],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if let Ok(event) = borsh::from_slice::<RaydiumLaunchpadTradeEvent>(data) {
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}",
|
||||
metadata.signature,
|
||||
event.pool_state.to_string()
|
||||
));
|
||||
Some(Box::new(RaydiumLaunchpadTradeEvent {
|
||||
metadata: metadata,
|
||||
..event
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析买入指令事件
|
||||
fn parse_buy_exact_in_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let amount_in = read_u64_le(data, 0)?;
|
||||
let minimum_amount_out = read_u64_le(data, 8)?;
|
||||
let share_fee_rate = read_u64_le(data, 16)?;
|
||||
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!("{}-{}", metadata.signature, accounts[4]));
|
||||
|
||||
Some(Box::new(RaydiumLaunchpadTradeEvent {
|
||||
metadata,
|
||||
amount_in,
|
||||
minimum_amount_out,
|
||||
share_fee_rate,
|
||||
payer: accounts[0],
|
||||
pool_state: accounts[4],
|
||||
user_base_token: accounts[5],
|
||||
user_quote_token: accounts[6],
|
||||
base_vault: accounts[7],
|
||||
quote_vault: accounts[8],
|
||||
base_token_mint: accounts[9],
|
||||
quote_token_mint: accounts[10],
|
||||
trade_direction: TradeDirection::Buy,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_buy_exact_out_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let amount_out = read_u64_le(data, 0)?;
|
||||
let maximum_amount_in = read_u64_le(data, 8)?;
|
||||
let share_fee_rate = read_u64_le(data, 16)?;
|
||||
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!("{}-{}", metadata.signature, accounts[4]));
|
||||
|
||||
Some(Box::new(RaydiumLaunchpadTradeEvent {
|
||||
metadata,
|
||||
amount_out,
|
||||
maximum_amount_in,
|
||||
share_fee_rate,
|
||||
payer: accounts[0],
|
||||
pool_state: accounts[4],
|
||||
user_base_token: accounts[5],
|
||||
user_quote_token: accounts[6],
|
||||
base_vault: accounts[7],
|
||||
quote_vault: accounts[8],
|
||||
base_token_mint: accounts[9],
|
||||
quote_token_mint: accounts[10],
|
||||
trade_direction: TradeDirection::Buy,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_sell_exact_in_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let amount_in = read_u64_le(data, 0)?;
|
||||
let minimum_amount_out = read_u64_le(data, 8)?;
|
||||
let share_fee_rate = read_u64_le(data, 16)?;
|
||||
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!("{}-{}", metadata.signature, accounts[4]));
|
||||
|
||||
Some(Box::new(RaydiumLaunchpadTradeEvent {
|
||||
metadata,
|
||||
amount_in,
|
||||
minimum_amount_out,
|
||||
share_fee_rate,
|
||||
payer: accounts[0],
|
||||
pool_state: accounts[4],
|
||||
user_base_token: accounts[5],
|
||||
user_quote_token: accounts[6],
|
||||
base_vault: accounts[7],
|
||||
quote_vault: accounts[8],
|
||||
base_token_mint: accounts[9],
|
||||
quote_token_mint: accounts[10],
|
||||
trade_direction: TradeDirection::Sell,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_sell_exact_out_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let amount_out = read_u64_le(data, 0)?;
|
||||
let maximum_amount_in = read_u64_le(data, 8)?;
|
||||
let share_fee_rate = read_u64_le(data, 16)?;
|
||||
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!("{}-{}", metadata.signature, accounts[4]));
|
||||
|
||||
Some(Box::new(RaydiumLaunchpadTradeEvent {
|
||||
metadata,
|
||||
amount_out,
|
||||
maximum_amount_in,
|
||||
share_fee_rate,
|
||||
payer: accounts[0],
|
||||
pool_state: accounts[4],
|
||||
user_base_token: accounts[5],
|
||||
user_quote_token: accounts[6],
|
||||
base_vault: accounts[7],
|
||||
quote_vault: accounts[8],
|
||||
base_token_mint: accounts[9],
|
||||
quote_token_mint: accounts[10],
|
||||
trade_direction: TradeDirection::Sell,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
/// 解析初始化事件
|
||||
fn parse_initialize_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 24 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut offset = 0;
|
||||
let base_mint_param = Self::parse_mint_params(data, &mut offset)?;
|
||||
let curve_param = Self::parse_curve_params(data, &mut offset)?;
|
||||
let vesting_param = Self::parse_vesting_params(data, &mut offset)?;
|
||||
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!("{}", metadata.signature));
|
||||
|
||||
Some(Box::new(RaydiumLaunchpadPoolCreateEvent {
|
||||
metadata,
|
||||
payer: accounts[0],
|
||||
creator: accounts[1],
|
||||
global_config: accounts[2],
|
||||
platform_config: accounts[3],
|
||||
pool_state: accounts[5],
|
||||
base_mint: accounts[6],
|
||||
quote_mint: accounts[7],
|
||||
base_vault: accounts[8],
|
||||
quote_vault: accounts[9],
|
||||
base_mint_param,
|
||||
curve_param,
|
||||
vesting_param,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
/// 解析 MintParams 结构
|
||||
fn parse_mint_params(data: &[u8], offset: &mut usize) -> Option<MintParams> {
|
||||
// 读取decimals (1字节)
|
||||
let decimals = read_u8(data, *offset)?;
|
||||
*offset += 1;
|
||||
|
||||
// 读取name字符串长度和内容
|
||||
let name_len = read_u32_le(data, *offset)? as usize;
|
||||
*offset += 4;
|
||||
if data.len() < *offset + name_len {
|
||||
return None;
|
||||
}
|
||||
let name = String::from_utf8(data[*offset..*offset + name_len].to_vec()).ok()?;
|
||||
*offset += name_len;
|
||||
|
||||
// 读取symbol字符串长度和内容
|
||||
let symbol_len = read_u32_le(data, *offset)? as usize;
|
||||
*offset += 4;
|
||||
if data.len() < *offset + symbol_len {
|
||||
return None;
|
||||
}
|
||||
let symbol = String::from_utf8(data[*offset..*offset + symbol_len].to_vec()).ok()?;
|
||||
*offset += symbol_len;
|
||||
|
||||
// 读取uri字符串长度和内容
|
||||
let uri_len = read_u32_le(data, *offset)? as usize;
|
||||
*offset += 4;
|
||||
if data.len() < *offset + uri_len {
|
||||
return None;
|
||||
}
|
||||
let uri = String::from_utf8(data[*offset..*offset + uri_len].to_vec()).ok()?;
|
||||
*offset += uri_len;
|
||||
|
||||
Some(MintParams {
|
||||
decimals,
|
||||
name,
|
||||
symbol,
|
||||
uri,
|
||||
})
|
||||
}
|
||||
|
||||
/// 解析 CurveParams 结构
|
||||
fn parse_curve_params(data: &[u8], offset: &mut usize) -> Option<CurveParams> {
|
||||
// 读取curve类型标识符 (1字节)
|
||||
let curve_type = read_u8(data, *offset)?;
|
||||
*offset += 1;
|
||||
|
||||
match curve_type {
|
||||
0 => {
|
||||
// Constant curve
|
||||
let supply = read_u64_le(data, *offset)?;
|
||||
*offset += 8;
|
||||
let total_base_sell = read_u64_le(data, *offset)?;
|
||||
*offset += 8;
|
||||
let total_quote_fund_raising = read_u64_le(data, *offset)?;
|
||||
*offset += 8;
|
||||
let migrate_type = read_u8(data, *offset)?;
|
||||
*offset += 1;
|
||||
|
||||
Some(CurveParams::Constant {
|
||||
data: ConstantCurve {
|
||||
supply,
|
||||
total_base_sell,
|
||||
total_quote_fund_raising,
|
||||
migrate_type,
|
||||
},
|
||||
})
|
||||
}
|
||||
1 => {
|
||||
// Fixed curve
|
||||
let supply = read_u64_le(data, *offset)?;
|
||||
*offset += 8;
|
||||
let total_quote_fund_raising = read_u64_le(data, *offset)?;
|
||||
*offset += 8;
|
||||
let migrate_type = read_u8(data, *offset)?;
|
||||
*offset += 1;
|
||||
|
||||
Some(CurveParams::Fixed {
|
||||
data: FixedCurve {
|
||||
supply,
|
||||
total_quote_fund_raising,
|
||||
migrate_type,
|
||||
},
|
||||
})
|
||||
}
|
||||
2 => {
|
||||
// Linear curve
|
||||
let supply = read_u64_le(data, *offset)?;
|
||||
*offset += 8;
|
||||
let total_quote_fund_raising = read_u64_le(data, *offset)?;
|
||||
*offset += 8;
|
||||
let migrate_type = read_u8(data, *offset)?;
|
||||
*offset += 1;
|
||||
|
||||
Some(CurveParams::Linear {
|
||||
data: LinearCurve {
|
||||
supply,
|
||||
total_quote_fund_raising,
|
||||
migrate_type,
|
||||
},
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析 VestingParams 结构
|
||||
fn parse_vesting_params(data: &[u8], offset: &mut usize) -> Option<VestingParams> {
|
||||
let total_locked_amount = read_u64_le(data, *offset)?;
|
||||
*offset += 8;
|
||||
let cliff_period = read_u64_le(data, *offset)?;
|
||||
*offset += 8;
|
||||
let unlock_period = read_u64_le(data, *offset)?;
|
||||
*offset += 8;
|
||||
|
||||
Some(VestingParams {
|
||||
total_locked_amount,
|
||||
cliff_period,
|
||||
unlock_period,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl EventParser for RaydiumLaunchpadEventParser {
|
||||
fn parse_events_from_inner_instruction(
|
||||
&self,
|
||||
inner_instruction: &UiCompiledInstruction,
|
||||
signature: &str,
|
||||
slot: u64,
|
||||
) -> Vec<Box<dyn UnifiedEvent>> {
|
||||
self.inner
|
||||
.parse_events_from_inner_instruction(inner_instruction, signature, slot)
|
||||
}
|
||||
|
||||
fn parse_events_from_instruction(
|
||||
&self,
|
||||
instruction: &CompiledInstruction,
|
||||
accounts: &[Pubkey],
|
||||
signature: &str,
|
||||
slot: u64,
|
||||
) -> Vec<Box<dyn UnifiedEvent>> {
|
||||
self.inner
|
||||
.parse_events_from_instruction(instruction, accounts, signature, slot)
|
||||
}
|
||||
|
||||
fn should_handle(&self, program_id: &Pubkey) -> bool {
|
||||
self.inner.should_handle(program_id)
|
||||
}
|
||||
|
||||
fn supported_program_ids(&self) -> Vec<Pubkey> {
|
||||
self.inner.supported_program_ids()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use borsh::BorshDeserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub enum TradeDirection {
|
||||
#[default]
|
||||
Buy,
|
||||
Sell,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub enum PoolStatus {
|
||||
#[default]
|
||||
Fund,
|
||||
Migrate,
|
||||
Trade,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct MintParams {
|
||||
pub decimals: u8,
|
||||
pub name: String,
|
||||
pub symbol: String,
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct VestingParams {
|
||||
pub total_locked_amount: u64,
|
||||
pub cliff_period: u64,
|
||||
pub unlock_period: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct ConstantCurve {
|
||||
pub supply: u64,
|
||||
pub total_base_sell: u64,
|
||||
pub total_quote_fund_raising: u64,
|
||||
pub migrate_type: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct FixedCurve {
|
||||
pub supply: u64,
|
||||
pub total_quote_fund_raising: u64,
|
||||
pub migrate_type: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct LinearCurve {
|
||||
pub supply: u64,
|
||||
pub total_quote_fund_raising: u64,
|
||||
pub migrate_type: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub enum CurveParams {
|
||||
Constant { data: ConstantCurve },
|
||||
Fixed { data: FixedCurve },
|
||||
Linear { data: LinearCurve },
|
||||
}
|
||||
|
||||
impl Default for CurveParams {
|
||||
fn default() -> Self {
|
||||
Self::Constant {
|
||||
data: ConstantCurve::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
-209
@@ -7,19 +7,16 @@ use tonic::transport::Channel;
|
||||
use log::error;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
|
||||
use crate::common::pumpswap::PumpSwapInstruction;
|
||||
use crate::common::raydium::{RaydiumEvent, RaydiumInstruction};
|
||||
use crate::common::AnyResult;
|
||||
use crate::event_parser::protocols::pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent};
|
||||
use crate::event_parser::protocols::raydium_launchpad::{
|
||||
RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent,
|
||||
};
|
||||
use crate::event_parser::{EventParserFactory, Protocol, UnifiedEvent};
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use crate::common::pumpfun::logs_data::DexInstruction;
|
||||
use crate::common::pumpfun::logs_events::PumpfunEvent;
|
||||
use crate::common::pumpswap::logs_events::PumpSwapEvent;
|
||||
use crate::common::pumpfun::logs_filters::LogFilter;
|
||||
use crate::common::pumpswap::logs_filters::LogFilter as PumpswapLogFilter;
|
||||
use crate::common::raydium::logs_filters::LogFilter as RaydiumLogFilter;
|
||||
use crate::protos::shredstream::shredstream_proxy_client::ShredstreamProxyClient;
|
||||
use crate::protos::shredstream::SubscribeEntriesRequest;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
const CHANNEL_SIZE: usize = 1000;
|
||||
|
||||
@@ -32,18 +29,22 @@ struct TransactionWithSlot {
|
||||
slot: u64,
|
||||
}
|
||||
|
||||
|
||||
impl ShredStreamGrpc {
|
||||
pub async fn new(endpoint: String) -> AnyResult<Self> {
|
||||
let shredstream_client = ShredstreamProxyClient::connect(endpoint.clone()).await?;
|
||||
Ok(Self {
|
||||
shredstream_client: Arc::new(shredstream_client)
|
||||
Ok(Self {
|
||||
shredstream_client: Arc::new(shredstream_client),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn shredstream_subscribe<F>(&self, callback: F, bot_wallet: Option<Pubkey>) -> AnyResult<()>
|
||||
pub async fn shredstream_subscribe<F>(
|
||||
&self,
|
||||
protocols: Vec<Protocol>,
|
||||
bot_wallet: Option<Pubkey>,
|
||||
callback: F,
|
||||
) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(PumpfunEvent) + Send + Sync + 'static,
|
||||
F: Fn(Box<dyn UnifiedEvent>) + Send + Sync + 'static,
|
||||
{
|
||||
let request = tonic::Request::new(SubscribeEntriesRequest {});
|
||||
let mut client = (*self.shredstream_client).clone();
|
||||
@@ -74,215 +75,50 @@ impl ShredStreamGrpc {
|
||||
});
|
||||
|
||||
while let Some(transaction_with_slot) = rx.next().await {
|
||||
if let Err(e) = Self::process_pumpfun_transaction(transaction_with_slot, &*callback, bot_wallet).await {
|
||||
if let Err(e) = Self::process_transaction(
|
||||
transaction_with_slot,
|
||||
protocols.clone(),
|
||||
bot_wallet,
|
||||
&*callback,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shredstream_subscribe_pumpswap<F>(&self, callback: F) -> AnyResult<()>
|
||||
async fn process_transaction<F>(
|
||||
transaction_with_slot: TransactionWithSlot,
|
||||
protocols: Vec<Protocol>,
|
||||
bot_wallet: Option<Pubkey>,
|
||||
callback: &F,
|
||||
) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(PumpSwapEvent) + Send + Sync + 'static,
|
||||
{
|
||||
let request = tonic::Request::new(SubscribeEntriesRequest {});
|
||||
let mut client = (*self.shredstream_client).clone();
|
||||
let mut stream = client.subscribe_entries(request).await?.into_inner();
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionWithSlot>(CHANNEL_SIZE);
|
||||
let callback = Box::new(callback);
|
||||
tokio::spawn(async move {
|
||||
while let Some(message) = stream.next().await {
|
||||
match message {
|
||||
Ok(msg) => {
|
||||
if let Ok(entries) = bincode::deserialize::<Vec<Entry>>(&msg.entries) {
|
||||
for entry in entries {
|
||||
for transaction in entry.transactions {
|
||||
let _ = tx.try_send(TransactionWithSlot {
|
||||
transaction: transaction.clone(),
|
||||
slot: msg.slot,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Stream error: {error:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while let Some(transaction_with_slot) = rx.next().await {
|
||||
if let Err(e) = Self::process_pumpswap_transaction(transaction_with_slot, &*callback).await {
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shredstream_subscribe_raydium<F>(&self, callback: F) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(RaydiumEvent) + Send + Sync + 'static,
|
||||
{
|
||||
let request = tonic::Request::new(SubscribeEntriesRequest {});
|
||||
let mut client = (*self.shredstream_client).clone();
|
||||
let mut stream = client.subscribe_entries(request).await?.into_inner();
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionWithSlot>(CHANNEL_SIZE);
|
||||
let callback = Box::new(callback);
|
||||
tokio::spawn(async move {
|
||||
while let Some(message) = stream.next().await {
|
||||
match message {
|
||||
Ok(msg) => {
|
||||
if let Ok(entries) = bincode::deserialize::<Vec<Entry>>(&msg.entries) {
|
||||
for entry in entries {
|
||||
for transaction in entry.transactions {
|
||||
let _ = tx.try_send(TransactionWithSlot {
|
||||
transaction: transaction.clone(),
|
||||
slot: msg.slot,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Stream error: {error:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while let Some(transaction_with_slot) = rx.next().await {
|
||||
if let Err(e) = Self::process_raydium_transaction(transaction_with_slot, &*callback).await {
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_pumpfun_transaction<F>(transaction_with_slot: TransactionWithSlot, callback: &F, bot_wallet: Option<Pubkey>) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(PumpfunEvent) + Send + Sync,
|
||||
F: Fn(Box<dyn UnifiedEvent>) + Send + Sync,
|
||||
{
|
||||
let slot = transaction_with_slot.slot;
|
||||
let versioned_tx = transaction_with_slot.transaction;
|
||||
let mut dev_address: Option<Pubkey> = None;
|
||||
// let hash = versioned_tx.signatures[0].to_string();
|
||||
let instructions = LogFilter::parse_compiled_instruction(versioned_tx, bot_wallet).unwrap();
|
||||
for instruction in instructions {
|
||||
// println!("hash: {}\ninstruction: {:?}\n\n", hash, instruction);
|
||||
match instruction {
|
||||
DexInstruction::CreateToken(mut token_info) => {
|
||||
token_info.slot = slot;
|
||||
dev_address = Some(token_info.user);
|
||||
callback(PumpfunEvent::NewToken(token_info));
|
||||
}
|
||||
DexInstruction::UserTrade(mut trade_info) => {
|
||||
trade_info.slot = slot;
|
||||
if Some(trade_info.user) == dev_address {
|
||||
callback(PumpfunEvent::NewDevTrade(trade_info));
|
||||
} else {
|
||||
callback(PumpfunEvent::NewUserTrade(trade_info));
|
||||
}
|
||||
}
|
||||
DexInstruction::BotTrade(mut trade_info) => {
|
||||
trade_info.slot = slot;
|
||||
callback(PumpfunEvent::NewBotTrade(trade_info));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
let signature = versioned_tx.signatures[0];
|
||||
|
||||
async fn process_pumpswap_transaction<F>(transaction_with_slot: TransactionWithSlot, callback: &F) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(PumpSwapEvent) + Send + Sync,
|
||||
{
|
||||
let slot = transaction_with_slot.slot;
|
||||
let versioned_tx = transaction_with_slot.transaction;
|
||||
let signature = versioned_tx.signatures[0].to_string();
|
||||
let instructions = PumpswapLogFilter::parse_pumpswap_compiled_instruction(versioned_tx).unwrap();
|
||||
for instruction in instructions {
|
||||
match instruction {
|
||||
PumpSwapInstruction::CreatePool(mut create_event) => {
|
||||
create_event.slot = slot;
|
||||
create_event.signature = signature.clone();
|
||||
callback(PumpSwapEvent::CreatePool(create_event));
|
||||
}
|
||||
PumpSwapInstruction::Deposit(mut deposit_event) => {
|
||||
deposit_event.slot = slot;
|
||||
deposit_event.signature = signature.clone();
|
||||
callback(PumpSwapEvent::Deposit(deposit_event));
|
||||
}
|
||||
PumpSwapInstruction::Withdraw(mut withdraw_event) => {
|
||||
withdraw_event.slot = slot;
|
||||
withdraw_event.signature = signature.clone();
|
||||
callback(PumpSwapEvent::Withdraw(withdraw_event));
|
||||
}
|
||||
PumpSwapInstruction::Buy(mut buy_event) => {
|
||||
buy_event.slot = slot;
|
||||
buy_event.signature = signature.clone();
|
||||
callback(PumpSwapEvent::Buy(buy_event));
|
||||
}
|
||||
PumpSwapInstruction::Sell(mut sell_event) => {
|
||||
sell_event.slot = slot;
|
||||
sell_event.signature = signature.clone();
|
||||
callback(PumpSwapEvent::Sell(sell_event));
|
||||
}
|
||||
PumpSwapInstruction::UpdateFeeConfig(mut update_fee_event) => {
|
||||
update_fee_event.slot = slot;
|
||||
update_fee_event.signature = signature.clone();
|
||||
callback(PumpSwapEvent::UpdateFeeConfig(update_fee_event));
|
||||
}
|
||||
PumpSwapInstruction::UpdateAdmin(mut update_admin_event) => {
|
||||
update_admin_event.slot = slot;
|
||||
update_admin_event.signature = signature.clone();
|
||||
callback(PumpSwapEvent::UpdateAdmin(update_admin_event));
|
||||
}
|
||||
PumpSwapInstruction::Disable(mut disable_event) => {
|
||||
disable_event.slot = slot;
|
||||
disable_event.signature = signature.clone();
|
||||
callback(PumpSwapEvent::Disable(disable_event));
|
||||
}
|
||||
_ => {}
|
||||
for protocol in protocols {
|
||||
let parser = EventParserFactory::create_parser(protocol.clone());
|
||||
let events = parser
|
||||
.parse_versioned_transaction(
|
||||
&versioned_tx,
|
||||
&signature.to_string(),
|
||||
Some(slot),
|
||||
bot_wallet.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|_e| vec![]);
|
||||
for event in events {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_raydium_transaction<F>(transaction_with_slot: TransactionWithSlot, callback: &F) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(RaydiumEvent) + Send + Sync,
|
||||
{
|
||||
let slot = transaction_with_slot.slot;
|
||||
let versioned_tx = transaction_with_slot.transaction;
|
||||
let signature = versioned_tx.signatures[0].to_string();
|
||||
let instructions: Vec<RaydiumInstruction> = RaydiumLogFilter::parse_raydium_compiled_instruction(versioned_tx).unwrap();
|
||||
for instruction in instructions {
|
||||
match instruction {
|
||||
RaydiumInstruction::V4Swap(mut v4_swap_event) => {
|
||||
v4_swap_event.slot = slot;
|
||||
v4_swap_event.signature = signature.clone();
|
||||
callback(RaydiumEvent::V4Swap(v4_swap_event));
|
||||
}
|
||||
RaydiumInstruction::SwapBaseInput(mut swap_base_input_event) => {
|
||||
swap_base_input_event.slot = slot;
|
||||
swap_base_input_event.signature = signature.clone();
|
||||
callback(RaydiumEvent::SwapBaseInput(swap_base_input_event));
|
||||
}
|
||||
RaydiumInstruction::SwapBaseOutput(mut swap_base_output_event) => {
|
||||
swap_base_output_event.slot = slot;
|
||||
swap_base_output_event.signature = signature.clone();
|
||||
callback(RaydiumEvent::SwapBaseOutput(swap_base_output_event));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+70
-479
@@ -1,24 +1,31 @@
|
||||
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, Interceptor};
|
||||
use yellowstone_grpc_proto::geyser::{
|
||||
CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions, SubscribeUpdate,
|
||||
SubscribeUpdateTransaction, subscribe_update::UpdateOneof, SubscribeRequestPing,
|
||||
};
|
||||
use log::{error, info};
|
||||
use chrono::Local;
|
||||
use futures::{channel::mpsc, sink::Sink, SinkExt, Stream, StreamExt};
|
||||
use log::{error, info};
|
||||
use rustls::crypto::{ring::default_provider, CryptoProvider};
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey, signature::Signature};
|
||||
use solana_transaction_status::{
|
||||
option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiTransactionEncoding,
|
||||
};
|
||||
use tonic::{transport::channel::ClientTlsConfig, Status};
|
||||
use yellowstone_grpc_client::{GeyserGrpcClient, Interceptor};
|
||||
use yellowstone_grpc_proto::geyser::{
|
||||
subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest,
|
||||
SubscribeRequestFilterTransactions, SubscribeRequestPing, SubscribeUpdate,
|
||||
SubscribeUpdateTransaction,
|
||||
};
|
||||
|
||||
use crate::common::pumpfun::logs_data::{DexInstruction, TransferInfo};
|
||||
use crate::common::pumpfun::logs_events::{PumpfunEvent, SystemEvent};
|
||||
use crate::common::pumpfun::logs_filters::LogFilter;
|
||||
// use crate::common::pumpfun::logs_data::{DexInstruction, TransferInfo};
|
||||
// use crate::common::pumpfun::logs_events::{PumpfunEvent, SystemEvent};
|
||||
// use crate::common::pumpfun::logs_filters::LogFilter;
|
||||
use crate::common::AnyResult;
|
||||
use crate::constants::pumpfun::trade;
|
||||
use crate::event_parser::protocols::pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent};
|
||||
use crate::event_parser::protocols::raydium_launchpad::{
|
||||
RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent,
|
||||
};
|
||||
use crate::event_parser::{EventParserFactory, Protocol, UnifiedEvent};
|
||||
|
||||
type TransactionsFilterMap = HashMap<String, SubscribeRequestFilterTransactions>;
|
||||
|
||||
@@ -84,16 +91,10 @@ impl YellowstoneGrpc {
|
||||
.map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e))?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
endpoint,
|
||||
x_token,
|
||||
})
|
||||
Ok(Self { endpoint, x_token })
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
&self,
|
||||
) -> AnyResult<GeyserGrpcClient<impl Interceptor>>
|
||||
{
|
||||
pub async fn connect(&self) -> AnyResult<GeyserGrpcClient<impl Interceptor>> {
|
||||
let builder = GeyserGrpcClient::build_from_shared(self.endpoint.clone())?
|
||||
.x_token(self.x_token.clone())?
|
||||
.tls_config(ClientTlsConfig::new().with_native_roots())?
|
||||
@@ -118,7 +119,9 @@ impl YellowstoneGrpc {
|
||||
};
|
||||
|
||||
let mut client = self.connect().await?;
|
||||
let (sink, stream) = client.subscribe_with_request(Some(subscribe_request)).await?;
|
||||
let (sink, stream) = client
|
||||
.subscribe_with_request(Some(subscribe_request))
|
||||
.await?;
|
||||
Ok((sink, stream))
|
||||
}
|
||||
|
||||
@@ -170,434 +173,37 @@ impl YellowstoneGrpc {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe_pumpfun<F>(&self, callback: F, bot_wallet: Option<Pubkey>) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(PumpfunEvent) + Send + Sync + 'static,
|
||||
{
|
||||
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.subscribe_with_request(transactions).await?;
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(CHANNEL_SIZE);
|
||||
|
||||
let callback = Box::new(callback);
|
||||
|
||||
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_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await {
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe_pumpfun_with_filter<F>(&self, callback: F, bot_wallet: Option<Pubkey>, account_include: Option<Vec<String>>, account_exclude: Option<Vec<String>>) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(PumpfunEvent) + Send + Sync + 'static,
|
||||
{
|
||||
let addrs = vec![PUMP_PROGRAM_ID.to_string()];
|
||||
let account_include = account_include.unwrap_or_default();
|
||||
let account_exclude = account_exclude.unwrap_or_default();
|
||||
let transactions = self.get_subscribe_request_filter(account_include, account_exclude, addrs);
|
||||
let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?;
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(CHANNEL_SIZE);
|
||||
|
||||
let callback = Box::new(callback);
|
||||
|
||||
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_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await {
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_pumpfun_transaction<F>(transaction_pretty: TransactionPretty, callback: &F, bot_wallet: Option<Pubkey>) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(PumpfunEvent) + Send + Sync,
|
||||
{
|
||||
let slot = transaction_pretty.slot;
|
||||
let trade_raw: EncodedTransactionWithStatusMeta = transaction_pretty.tx;
|
||||
let meta = trade_raw.meta.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?;
|
||||
|
||||
if meta.err.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let logs = if let OptionSerializer::Some(logs) = &meta.log_messages {
|
||||
logs
|
||||
} else {
|
||||
&vec![]
|
||||
};
|
||||
|
||||
let mut dev_address: Option<Pubkey> = None;
|
||||
let instructions = LogFilter::parse_instruction(logs, bot_wallet).unwrap();
|
||||
for instruction in instructions {
|
||||
match instruction {
|
||||
DexInstruction::CreateToken(mut token_info) => {
|
||||
token_info.slot = slot;
|
||||
dev_address = Some(token_info.user);
|
||||
callback(PumpfunEvent::NewToken(token_info));
|
||||
}
|
||||
DexInstruction::UserTrade(mut trade_info) => {
|
||||
trade_info.slot = slot;
|
||||
if Some(trade_info.user) == dev_address {
|
||||
callback(PumpfunEvent::NewDevTrade(trade_info));
|
||||
} else {
|
||||
callback(PumpfunEvent::NewUserTrade(trade_info));
|
||||
}
|
||||
}
|
||||
DexInstruction::BotTrade(mut trade_info) => {
|
||||
trade_info.slot = slot;
|
||||
callback(PumpfunEvent::NewBotTrade(trade_info));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe_system<F>(&self, callback: F, account_include: Option<Vec<String>>, account_exclude: Option<Vec<String>>) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(SystemEvent) + Send + Sync + 'static,
|
||||
{
|
||||
let addrs = vec![SYSTEM_PROGRAM_ID.to_string()];
|
||||
let account_include = account_include.unwrap_or_default();
|
||||
let account_exclude = account_exclude.unwrap_or_default();
|
||||
let transactions = self.get_subscribe_request_filter(account_include, account_exclude, addrs);
|
||||
let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?;
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(CHANNEL_SIZE);
|
||||
|
||||
let callback = Box::new(callback);
|
||||
|
||||
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_system_transaction(transaction_pretty, &*callback).await {
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_system_transaction<F>(transaction_pretty: TransactionPretty, callback: &F) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(SystemEvent) + Send + Sync,
|
||||
{
|
||||
let trade_raw: EncodedTransactionWithStatusMeta = transaction_pretty.tx;
|
||||
let meta = trade_raw.meta.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?;
|
||||
|
||||
if meta.err.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
callback(SystemEvent::NewTransfer(TransferInfo {
|
||||
slot: transaction_pretty.slot,
|
||||
signature: transaction_pretty.signature.to_string(),
|
||||
tx: trade_raw.transaction.decode(),
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// PumpSwap
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/// 订阅PumpSwap事件
|
||||
pub async fn subscribe_pumpswap<F>(&self, callback: F) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(crate::common::pumpswap::logs_events::PumpSwapEvent) + Send + Sync + 'static,
|
||||
{
|
||||
// 使用constants中定义的AMM_PROGRAM
|
||||
let pump_program_id = crate::constants::pumpswap::accounts::AMM_PROGRAM;
|
||||
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.subscribe_with_request(transactions).await?;
|
||||
|
||||
// 创建通道
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(1000);
|
||||
|
||||
// 创建回调函数
|
||||
let callback = Box::new(callback);
|
||||
|
||||
// 启动处理流的任务
|
||||
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_pumpswap_transaction(transaction_pretty, &*callback).await {
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 使用过滤器订阅PumpSwap事件
|
||||
pub async fn subscribe_pumpswap_with_filter<F>(
|
||||
/// 订阅事件
|
||||
pub async fn subscribe_events<F>(
|
||||
&self,
|
||||
callback: F,
|
||||
protocols: Vec<Protocol>,
|
||||
bot_wallet: Option<Pubkey>,
|
||||
account_include: Option<Vec<String>>,
|
||||
account_exclude: Option<Vec<String>>
|
||||
) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(crate::common::pumpswap::logs_events::PumpSwapEvent) + Send + Sync + 'static,
|
||||
{
|
||||
// 使用constants中定义的AMM_PROGRAM
|
||||
let pump_program_id = crate::constants::pumpswap::accounts::AMM_PROGRAM;
|
||||
let addrs = vec![pump_program_id.to_string()];
|
||||
|
||||
// 创建过滤器
|
||||
let account_include = account_include.unwrap_or_default();
|
||||
let account_exclude = account_exclude.unwrap_or_default();
|
||||
let transactions = self.get_subscribe_request_filter(account_include, account_exclude, addrs);
|
||||
|
||||
// 订阅事件
|
||||
let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?;
|
||||
|
||||
// 创建通道
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(1000);
|
||||
|
||||
// 创建回调函数
|
||||
let callback = Box::new(callback);
|
||||
|
||||
// 启动处理流的任务
|
||||
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_pumpswap_transaction(transaction_pretty, &*callback).await {
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 处理PumpSwap交易
|
||||
async fn process_pumpswap_transaction<F>(
|
||||
transaction_pretty: TransactionPretty,
|
||||
callback: &F
|
||||
account_exclude: Option<Vec<String>>,
|
||||
callback: F,
|
||||
) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(crate::common::pumpswap::logs_events::PumpSwapEvent) + Send + Sync,
|
||||
F: Fn(Box<dyn UnifiedEvent>) + Send + Sync + 'static,
|
||||
{
|
||||
let slot = transaction_pretty.slot;
|
||||
let trade_raw: solana_transaction_status::EncodedTransactionWithStatusMeta = transaction_pretty.tx;
|
||||
|
||||
// 检查交易元数据
|
||||
let meta = trade_raw.meta.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?;
|
||||
|
||||
// 检查交易是否成功
|
||||
if meta.err.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut buy_instruction_events = vec![];
|
||||
let mut sell_instruction_events = vec![];
|
||||
|
||||
if let Some(versioned_tx) = trade_raw.transaction.decode() {
|
||||
let signature = versioned_tx.signatures[0].to_string();
|
||||
let instructions: Vec<crate::common::pumpswap::logs_data::PumpSwapInstruction> =
|
||||
crate::common::pumpswap::logs_filters::LogFilter::parse_pumpswap_compiled_instruction(versioned_tx).unwrap();
|
||||
for instruction in instructions {
|
||||
match instruction {
|
||||
crate::common::pumpswap::logs_data::PumpSwapInstruction::Buy(mut e) => {
|
||||
e.signature = signature.clone();
|
||||
e.slot = slot;
|
||||
buy_instruction_events.push(e);
|
||||
}
|
||||
crate::common::pumpswap::logs_data::PumpSwapInstruction::Sell(mut e) => {
|
||||
e.signature = signature.clone();
|
||||
e.slot = slot;
|
||||
sell_instruction_events.push(e);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取日志
|
||||
let logs = if let solana_transaction_status::option_serializer::OptionSerializer::Some(logs) = &meta.log_messages {
|
||||
logs
|
||||
} else {
|
||||
&vec![]
|
||||
};
|
||||
|
||||
// 解析PumpSwap事件
|
||||
let events = crate::common::pumpswap::logs_filters::LogFilter::parse_pumpswap_logs(logs);
|
||||
|
||||
// 处理事件
|
||||
for mut event in events {
|
||||
// 设置签名和slot
|
||||
match &mut event {
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::Buy(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
if let Some(ie) = buy_instruction_events.iter().find(|ie| ie.signature == e.signature && ie.slot == e.slot && ie.pool == e.pool && ie.user == e.user) {
|
||||
e.base_mint = ie.base_mint;
|
||||
e.quote_mint = ie.quote_mint;
|
||||
e.pool_base_token_account = ie.pool_base_token_account;
|
||||
e.pool_quote_token_account = ie.pool_quote_token_account;
|
||||
e.coin_creator_vault_ata = ie.coin_creator_vault_ata;
|
||||
e.coin_creator_vault_authority = ie.coin_creator_vault_authority;
|
||||
}
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::Sell(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
if let Some(ie) = sell_instruction_events.iter().find(|ie| ie.signature == e.signature && ie.slot == e.slot && ie.pool == e.pool && ie.user == e.user) {
|
||||
e.base_mint = ie.base_mint;
|
||||
e.quote_mint = ie.quote_mint;
|
||||
e.pool_base_token_account = ie.pool_base_token_account;
|
||||
e.pool_quote_token_account = ie.pool_quote_token_account;
|
||||
e.coin_creator_vault_ata = ie.coin_creator_vault_ata;
|
||||
e.coin_creator_vault_authority = ie.coin_creator_vault_authority;
|
||||
}
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::CreatePool(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::Deposit(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::Withdraw(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::Disable(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::UpdateAdmin(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::UpdateFeeConfig(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// 调用回调函数
|
||||
callback(event);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Raydium
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/// 订阅Raydium事件
|
||||
pub async fn subscribe_raydium<F>(&self, callback: F) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(crate::common::raydium::logs_events::RaydiumEvent) + Send + Sync + 'static,
|
||||
{
|
||||
// 使用constants中定义的AMM_PROGRAM
|
||||
let raydium_v4_program_id = crate::constants::raydium::accounts::AMMV4_PROGRAM;
|
||||
let raydium_cpmm_program_id = crate::constants::raydium::accounts::CPMM_PROGRAM;
|
||||
let addrs = vec![raydium_v4_program_id.to_string(), raydium_cpmm_program_id.to_string()];
|
||||
|
||||
// 创建过滤器
|
||||
let transactions = self.get_subscribe_request_filter(addrs, vec![], vec![]);
|
||||
let protocol_accounts = protocols
|
||||
.iter()
|
||||
.map(|p| p.get_program_id())
|
||||
.flatten()
|
||||
.map(|p| p.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
let mut account_include = account_include.unwrap_or_default();
|
||||
let account_exclude = account_exclude.unwrap_or_default();
|
||||
account_include.extend(protocol_accounts.clone());
|
||||
|
||||
let transactions =
|
||||
self.get_subscribe_request_filter(account_include, account_exclude, vec![]);
|
||||
|
||||
// 订阅事件
|
||||
let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?;
|
||||
|
||||
// 创建通道
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(1000);
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(CHANNEL_SIZE);
|
||||
|
||||
// 创建回调函数
|
||||
let callback = Box::new(callback);
|
||||
@@ -624,7 +230,13 @@ impl YellowstoneGrpc {
|
||||
|
||||
// 处理交易
|
||||
while let Some(transaction_pretty) = rx.next().await {
|
||||
if let Err(e) = Self::process_raydium_transaction(transaction_pretty, &*callback).await
|
||||
if let Err(e) = Self::process_event_transaction(
|
||||
transaction_pretty,
|
||||
&*callback,
|
||||
bot_wallet,
|
||||
protocols.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
@@ -633,52 +245,31 @@ impl YellowstoneGrpc {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 处理Raydium交易
|
||||
async fn process_raydium_transaction<F>(
|
||||
/// 处理事件交易
|
||||
async fn process_event_transaction<F>(
|
||||
transaction_pretty: TransactionPretty,
|
||||
callback: &F,
|
||||
bot_wallet: Option<Pubkey>,
|
||||
protocols: Vec<Protocol>,
|
||||
) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(crate::common::raydium::logs_events::RaydiumEvent) + Send + Sync,
|
||||
F: Fn(Box<dyn UnifiedEvent>) + Send + Sync,
|
||||
{
|
||||
let slot = transaction_pretty.slot;
|
||||
let trade_raw: solana_transaction_status::EncodedTransactionWithStatusMeta =
|
||||
transaction_pretty.tx;
|
||||
|
||||
// 检查交易元数据
|
||||
let meta = trade_raw
|
||||
.meta
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?;
|
||||
|
||||
// 检查交易是否成功
|
||||
if meta.err.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(versioned_tx) = trade_raw.transaction.decode() {
|
||||
let signature = versioned_tx.signatures[0].to_string();
|
||||
let instructions: Vec<crate::common::raydium::logs_data::RaydiumInstruction> =
|
||||
crate::common::raydium::logs_filters::LogFilter::parse_raydium_compiled_instruction(versioned_tx).unwrap();
|
||||
for instruction in instructions {
|
||||
match instruction {
|
||||
crate::common::raydium::logs_data::RaydiumInstruction::V4Swap(mut v4_swap_event) => {
|
||||
v4_swap_event.slot = slot;
|
||||
v4_swap_event.signature = signature.clone();
|
||||
callback(crate::common::raydium::logs_events::RaydiumEvent::V4Swap(v4_swap_event));
|
||||
}
|
||||
crate::common::raydium::logs_data::RaydiumInstruction::SwapBaseInput(mut swap_base_input_event) => {
|
||||
swap_base_input_event.slot = slot;
|
||||
swap_base_input_event.signature = signature.clone();
|
||||
callback(crate::common::raydium::logs_events::RaydiumEvent::SwapBaseInput(swap_base_input_event));
|
||||
}
|
||||
crate::common::raydium::logs_data::RaydiumInstruction::SwapBaseOutput(mut swap_base_output_event) => {
|
||||
swap_base_output_event.slot = slot;
|
||||
swap_base_output_event.signature = signature.clone();
|
||||
callback(crate::common::raydium::logs_events::RaydiumEvent::SwapBaseOutput(swap_base_output_event));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let signature = transaction_pretty.signature.to_string();
|
||||
for protocol in protocols {
|
||||
let parser = EventParserFactory::create_parser(protocol);
|
||||
let events = parser
|
||||
.parse_transaction(
|
||||
transaction_pretty.tx.clone(),
|
||||
&signature,
|
||||
Some(slot),
|
||||
bot_wallet.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|_e| vec![]);
|
||||
for event in events {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+259
-470
File diff suppressed because it is too large
Load Diff
+310
-341
@@ -1,299 +1,45 @@
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use sol_trade_sdk::{accounts::BondingCurveAccount, common::{pumpfun::PumpfunEvent, pumpswap::PumpSwapEvent, raydium::RaydiumEvent, AnyResult, PriorityFee, TradeConfig}, constants::pumpfun::global_constants::TOKEN_TOTAL_SUPPLY, grpc::{ShredStreamGrpc, YellowstoneGrpc}, pumpfun::common::get_bonding_curve_pda, swqos::{SwqosConfig, SwqosRegion, SwqosType}, SolanaTrade};
|
||||
use sol_trade_sdk::{
|
||||
accounts::BondingCurveAccount,
|
||||
common::{AnyResult, PriorityFee, TradeConfig},
|
||||
constants::{pumpfun::global_constants::TOKEN_TOTAL_SUPPLY, trade_type},
|
||||
event_parser::{
|
||||
protocols::{
|
||||
pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent},
|
||||
pumpswap::{
|
||||
PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent, PumpSwapSellEvent,
|
||||
PumpSwapWithdrawEvent,
|
||||
},
|
||||
raydium_launchpad::{RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent},
|
||||
},
|
||||
Protocol, UnifiedEvent,
|
||||
},
|
||||
grpc::{ShredStreamGrpc, YellowstoneGrpc},
|
||||
match_event,
|
||||
pumpfun::common::get_bonding_curve_account_v2,
|
||||
swqos::{SwqosConfig, SwqosRegion},
|
||||
trading::{
|
||||
core::params::{PumpFunParams, PumpFunSellParams, PumpSwapParams, RaydiumLaunchpadParams},
|
||||
BuyParams, SellParams,
|
||||
},
|
||||
SolanaTrade,
|
||||
};
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Keypair};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// test_pumpfun_with_shreds().await?;
|
||||
// test_pumpfun_with_grpc().await?;
|
||||
// test_pumpswap_with_shreds().await?;
|
||||
// test_pumpswap_with_grpc().await?;
|
||||
// test_raydium_with_shreds().await?;
|
||||
// test_raydium_with_grpc().await?;
|
||||
// test_pumpfun_sniper().await?;
|
||||
// test_pumpfun().await?;
|
||||
test_pumpswap().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_pumpfun_with_shreds() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let grpc = ShredStreamGrpc::new("http://127.0.0.1:10000".to_string()).await?;
|
||||
|
||||
let callback = |event: PumpfunEvent| {
|
||||
// TradeInfo 的 sol_amount 不是真实线上消费/获取的数量
|
||||
// 当 is_buy 为 true 时,sol_amount = max_sol_cost,代表用户愿意支付的最大金额
|
||||
// 当 is_buy 为 false 时,sol_amount = min_sol_output,代表用户愿意接受的最小金额
|
||||
//
|
||||
// timestamp 不是真实交易发生的时间,取的值为当前系统的时间
|
||||
//
|
||||
// 无法获取下面4个值
|
||||
// virtual_sol_reserves: 0,
|
||||
// virtual_token_reserves: 0,
|
||||
// real_sol_reserves: 0,
|
||||
// real_token_reserves: 0,
|
||||
match event {
|
||||
PumpfunEvent::NewDevTrade(trade_info) => {
|
||||
println!("Received new dev trade event: {:?}", trade_info);
|
||||
}
|
||||
PumpfunEvent::NewToken(token_info) => {
|
||||
println!("Received new token event: {:?}", token_info);
|
||||
}
|
||||
PumpfunEvent::NewUserTrade(trade_info) => {
|
||||
println!("Received new trade event: {:?}", trade_info);
|
||||
}
|
||||
PumpfunEvent::NewBotTrade(trade_info) => {
|
||||
println!("Received new bot trade event: {:?}", trade_info);
|
||||
}
|
||||
PumpfunEvent::Error(err) => {
|
||||
println!("Received error: {}", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
grpc.shredstream_subscribe(callback, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_pumpfun_with_grpc() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let grpc = YellowstoneGrpc::new(
|
||||
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let callback = |event: PumpfunEvent| match event {
|
||||
PumpfunEvent::NewDevTrade(trade_info) => {
|
||||
println!("Received new dev trade event: {:?}", trade_info);
|
||||
}
|
||||
PumpfunEvent::NewToken(token_info) => {
|
||||
println!("Received new token event: {:?}", token_info);
|
||||
}
|
||||
PumpfunEvent::NewUserTrade(trade_info) => {
|
||||
println!("Received new trade event: {:?}", trade_info);
|
||||
}
|
||||
PumpfunEvent::NewBotTrade(trade_info) => {
|
||||
println!("Received new bot trade event: {:?}", trade_info);
|
||||
}
|
||||
PumpfunEvent::Error(err) => {
|
||||
println!("Received error: {}", err);
|
||||
}
|
||||
};
|
||||
|
||||
grpc.subscribe_pumpfun(callback, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_pumpswap_with_shreds() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 使用 ShredStream 客户端订阅 PumpSwap 事件
|
||||
println!("正在订阅 PumpSwap ShredStream 事件...");
|
||||
|
||||
let grpc_client = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?;
|
||||
|
||||
// 定义回调函数处理 PumpSwap 事件
|
||||
let callback = |event: PumpSwapEvent| {
|
||||
match event {
|
||||
PumpSwapEvent::Buy(buy_event) => {
|
||||
// println!("buy_event: {:?}", buy_event);
|
||||
}
|
||||
PumpSwapEvent::Sell(sell_event) => {
|
||||
println!("sell_event: {:?}", sell_event);
|
||||
}
|
||||
PumpSwapEvent::CreatePool(create_event) => {
|
||||
// println!("create_event: {:?}", create_event);
|
||||
}
|
||||
PumpSwapEvent::Deposit(deposit_event) => {
|
||||
// println!("deposit_event: {:?}", deposit_event);
|
||||
}
|
||||
PumpSwapEvent::Withdraw(withdraw_event) => {
|
||||
// println!("withdraw_event: {:?}", withdraw_event);
|
||||
}
|
||||
PumpSwapEvent::Disable(disable_event) => {
|
||||
// println!("disable_event: {:?}", disable_event);
|
||||
}
|
||||
PumpSwapEvent::UpdateAdmin(update_admin_event) => {
|
||||
// println!("update_admin_event: {:?}", update_admin_event);
|
||||
}
|
||||
PumpSwapEvent::UpdateFeeConfig(update_fee_event) => {
|
||||
// println!("update_fee_event: {:?}", update_fee_event);
|
||||
}
|
||||
PumpSwapEvent::Error(err) => {
|
||||
println!("error: {}", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
// 订阅 PumpSwap 事件
|
||||
println!("开始监听 PumpSwap 事件,按 Ctrl+C 停止...");
|
||||
|
||||
grpc_client.shredstream_subscribe_pumpswap(callback).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_pumpswap_with_grpc() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 使用 GRPC 客户端订阅 PumpSwap 事件
|
||||
println!("正在订阅 PumpSwap GRPC 事件...");
|
||||
|
||||
let grpc = YellowstoneGrpc::new(
|
||||
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
||||
None,
|
||||
)?;
|
||||
|
||||
// 定义回调函数处理 PumpSwap 事件
|
||||
let callback = |event: PumpSwapEvent| match event {
|
||||
PumpSwapEvent::Buy(buy_event) => {
|
||||
println!("buy_event: {:?}", buy_event);
|
||||
}
|
||||
PumpSwapEvent::Sell(sell_event) => {
|
||||
println!("sell_event: {:?}", sell_event);
|
||||
}
|
||||
PumpSwapEvent::CreatePool(create_event) => {
|
||||
println!("create_event: {:?}", create_event);
|
||||
}
|
||||
PumpSwapEvent::Deposit(deposit_event) => {
|
||||
println!("deposit_event: {:?}", deposit_event);
|
||||
}
|
||||
PumpSwapEvent::Withdraw(withdraw_event) => {
|
||||
println!("withdraw_event: {:?}", withdraw_event);
|
||||
}
|
||||
PumpSwapEvent::Disable(disable_event) => {
|
||||
println!("disable_event: {:?}", disable_event);
|
||||
}
|
||||
PumpSwapEvent::UpdateAdmin(update_admin_event) => {
|
||||
println!("update_admin_event: {:?}", update_admin_event);
|
||||
}
|
||||
PumpSwapEvent::UpdateFeeConfig(update_fee_event) => {
|
||||
println!("update_fee_event: {:?}", update_fee_event);
|
||||
}
|
||||
PumpSwapEvent::Error(err) => {
|
||||
println!("error: {}", err);
|
||||
}
|
||||
};
|
||||
// 订阅 PumpSwap 事件
|
||||
println!("开始监听 PumpSwap 事件,按 Ctrl+C 停止...");
|
||||
|
||||
grpc.subscribe_pumpswap(callback).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_raydium_with_shreds() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 使用 ShredStream 客户端订阅 Raydium 事件
|
||||
println!("正在订阅 Raydium ShredStream 事件...");
|
||||
|
||||
let grpc_client = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?;
|
||||
|
||||
// 定义回调函数处理 Raydium 事件
|
||||
let callback = |event: RaydiumEvent| {
|
||||
match event {
|
||||
RaydiumEvent::V4Swap(v4_swap_event) => {
|
||||
println!("v4_swap_event: {:?}", v4_swap_event);
|
||||
}
|
||||
RaydiumEvent::SwapBaseInput(swap_base_input_event) => {
|
||||
println!("swap_base_input_event: {:?}", swap_base_input_event);
|
||||
}
|
||||
RaydiumEvent::SwapBaseOutput(swap_base_output_event) => {
|
||||
println!("swap_base_output_event: {:?}", swap_base_output_event);
|
||||
}
|
||||
RaydiumEvent::Error(err) => {
|
||||
println!("error: {}", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
// 订阅 Raydium 事件
|
||||
println!("开始监听 Raydium 事件,按 Ctrl+C 停止...");
|
||||
|
||||
grpc_client.shredstream_subscribe_raydium(callback).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_raydium_with_grpc() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 使用 GRPC 客户端订阅 Raydium 事件
|
||||
println!("正在订阅 Raydium GRPC 事件...");
|
||||
|
||||
let grpc = YellowstoneGrpc::new(
|
||||
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
||||
None,
|
||||
)?;
|
||||
|
||||
// 定义回调函数处理 PumpSwap 事件
|
||||
let callback = |event: RaydiumEvent| match event {
|
||||
RaydiumEvent::V4Swap(v4_swap_event) => {
|
||||
println!("v4_swap_event: {:?}", v4_swap_event);
|
||||
}
|
||||
RaydiumEvent::SwapBaseInput(swap_base_input_event) => {
|
||||
println!("swap_base_input_event: {:?}", swap_base_input_event);
|
||||
}
|
||||
RaydiumEvent::SwapBaseOutput(swap_base_output_event) => {
|
||||
println!("swap_base_output_event: {:?}", swap_base_output_event);
|
||||
}
|
||||
RaydiumEvent::Error(err) => {
|
||||
println!("error: {}", err);
|
||||
}
|
||||
};
|
||||
// 订阅 Raydium 事件
|
||||
println!("开始监听 Raydium 事件,按 Ctrl+C 停止...");
|
||||
|
||||
grpc.subscribe_raydium(callback).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_pumpfun_sniper() -> AnyResult<()> {
|
||||
// 创建一个随机账户作为交易者
|
||||
let payer = Keypair::new();
|
||||
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let swqos_configs = vec![
|
||||
SwqosConfig::Jito(SwqosRegion::Frankfurt),
|
||||
SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
];
|
||||
|
||||
// Define cluster configuration
|
||||
let trade_config = TradeConfig {
|
||||
rpc_url: rpc_url.clone(),
|
||||
commitment: CommitmentConfig::confirmed(),
|
||||
priority_fee: PriorityFee::default(),
|
||||
swqos_configs,
|
||||
lookup_table_key: None,
|
||||
};
|
||||
let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
|
||||
|
||||
let creator = Pubkey::from_str("xxx")?; // dev account
|
||||
let buy_sol_cost = 500_000; // 0.0005 SOL
|
||||
let slippage_basis_points = Some(100);
|
||||
let rpc = RpcClient::new(rpc_url);
|
||||
let recent_blockhash = rpc.get_latest_blockhash().unwrap();
|
||||
let mint_pubkey = Pubkey::from_str("xxx")?; // token mint
|
||||
println!("Sniping buy tokens from PumpFun...");
|
||||
// get bonding curve
|
||||
let dev_buy_token = 100_000; // test value
|
||||
let dev_cost_sol = 100_000; // test value
|
||||
let bonding_curve = BondingCurveAccount::new(&mint_pubkey, dev_buy_token, dev_cost_sol, creator);
|
||||
solana_trade_client
|
||||
.sniper_buy(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
buy_sol_cost,
|
||||
slippage_basis_points,
|
||||
recent_blockhash,
|
||||
Some(Arc::new(bonding_curve)),
|
||||
)
|
||||
.await?;
|
||||
test_pumpfun().await?;
|
||||
// test_pumpswap().await?;
|
||||
// test_raydium_launchpad().await?;
|
||||
// test_grpc().await?;
|
||||
// test_shreds().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_pumpfun() -> AnyResult<()> {
|
||||
let payer = Keypair::new();
|
||||
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let swqos_configs = vec![
|
||||
SwqosConfig::Jito(SwqosRegion::Frankfurt),
|
||||
@@ -303,7 +49,6 @@ async fn test_pumpfun() -> AnyResult<()> {
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
];
|
||||
|
||||
// Define cluster configuration
|
||||
let trade_config = TradeConfig {
|
||||
rpc_url: rpc_url.clone(),
|
||||
@@ -312,25 +57,24 @@ async fn test_pumpfun() -> AnyResult<()> {
|
||||
swqos_configs,
|
||||
lookup_table_key: None,
|
||||
};
|
||||
|
||||
let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
let creator = Pubkey::from_str("xxx")?; // dev account
|
||||
let buy_sol_cost = 500_000; // 0.0005 SOL
|
||||
let creator = Pubkey::from_str("xxxxxx")?; // dev account
|
||||
let buy_sol_cost = 100_000; // 0.0001 SOL
|
||||
let slippage_basis_points = Some(100);
|
||||
let rpc = RpcClient::new(rpc_url);
|
||||
let recent_blockhash = rpc.get_latest_blockhash().unwrap();
|
||||
let trade_platform = "pumpfun".to_string();
|
||||
let mint_pubkey = Pubkey::from_str("xxx")?; // token mint
|
||||
let mint_pubkey = Pubkey::from_str("xxxxxx")?; // token mint
|
||||
println!("Buying tokens from PumpFun...");
|
||||
// get bonding curve
|
||||
// Relevant on-chain information can be obtained from rpc/grpc
|
||||
let virtual_token_reserves = 0;
|
||||
let virtual_sol_reserves = 0;
|
||||
let real_token_reserves = 0;
|
||||
let real_sol_reserves = 0;
|
||||
let (bonding_curve, bonding_curve_pda) =
|
||||
get_bonding_curve_account_v2(&solana_trade_client.rpc, &mint_pubkey).await?;
|
||||
let virtual_token_reserves = bonding_curve.virtual_token_reserves;
|
||||
let virtual_sol_reserves = bonding_curve.virtual_sol_reserves;
|
||||
let real_token_reserves = bonding_curve.real_token_reserves;
|
||||
let real_sol_reserves = bonding_curve.real_sol_reserves;
|
||||
let bonding_curve = BondingCurveAccount {
|
||||
discriminator: 0,
|
||||
account: get_bonding_curve_pda(&mint_pubkey).unwrap(),
|
||||
discriminator: bonding_curve.discriminator,
|
||||
account: bonding_curve_pda,
|
||||
virtual_token_reserves: virtual_token_reserves,
|
||||
virtual_sol_reserves: virtual_sol_reserves,
|
||||
real_token_reserves: real_token_reserves,
|
||||
@@ -339,34 +83,57 @@ async fn test_pumpfun() -> AnyResult<()> {
|
||||
complete: false,
|
||||
creator: creator,
|
||||
};
|
||||
// 如果是狙击开发者
|
||||
// let bonding_curve =
|
||||
// BondingCurveAccount::new(&mint_pubkey, dev_buy_token, dev_cost_sol, creator);
|
||||
// buy
|
||||
let buy_protocol_params = PumpFunParams {
|
||||
trade_type: trade_type::COPY_BUY.to_string(),
|
||||
bonding_curve: Some(Arc::new(bonding_curve)),
|
||||
};
|
||||
let buy_params = BuyParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint_pubkey,
|
||||
creator: creator,
|
||||
amount_sol: buy_sol_cost,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
data_size_limit: 0,
|
||||
protocol_params: Box::new(buy_protocol_params.clone()),
|
||||
};
|
||||
let buy_with_tip_params = buy_params
|
||||
.clone()
|
||||
.with_tip(solana_trade_client.swqos_clients.clone());
|
||||
solana_trade_client
|
||||
.buy(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
buy_sol_cost,
|
||||
slippage_basis_points,
|
||||
recent_blockhash,
|
||||
Some(Arc::new(bonding_curve)),
|
||||
trade_platform.clone(),
|
||||
)
|
||||
.buy_use_buy_params(buy_with_tip_params, None)
|
||||
.await?;
|
||||
// sell
|
||||
println!("Selling tokens from PumpFun...");
|
||||
let sell_protocol_params = PumpFunSellParams {};
|
||||
let amount_token = 0; // 写上真实的amount_token
|
||||
let sell_params = SellParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint_pubkey,
|
||||
creator: creator,
|
||||
amount_token: Some(amount_token),
|
||||
slippage_basis_points: None,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
protocol_params: Box::new(sell_protocol_params.clone()),
|
||||
};
|
||||
solana_trade_client
|
||||
.sell_by_amount_use_sell_params(sell_params)
|
||||
.await?;
|
||||
// Sell 30% * amount_token quantity
|
||||
// solana_trade_client
|
||||
// .sell_by_percent(
|
||||
// mint_pubkey,
|
||||
// creator,
|
||||
// 30,
|
||||
// 100,
|
||||
// recent_blockhash,
|
||||
// trade_platform.clone(),
|
||||
// )
|
||||
// .await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_pumpswap() -> AnyResult<()> {
|
||||
let payer = Keypair::new();
|
||||
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let swqos_configs = vec![
|
||||
SwqosConfig::Jito(SwqosRegion::Frankfurt),
|
||||
@@ -376,7 +143,6 @@ async fn test_pumpswap() -> AnyResult<()> {
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
];
|
||||
|
||||
// Define cluster configuration
|
||||
let trade_config = TradeConfig {
|
||||
rpc_url: rpc_url.clone(),
|
||||
@@ -387,34 +153,237 @@ async fn test_pumpswap() -> AnyResult<()> {
|
||||
};
|
||||
let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
let creator = Pubkey::from_str("11111111111111111111111111111111")?; // dev account
|
||||
let buy_sol_cost = 500_000; // 0.0005 SOL
|
||||
let buy_sol_cost = 100_000; // 0.0001 SOL
|
||||
let slippage_basis_points = Some(100);
|
||||
let rpc = RpcClient::new(rpc_url);
|
||||
let recent_blockhash = rpc.get_latest_blockhash().unwrap();
|
||||
let trade_platform = "pumpswap".to_string();
|
||||
let mint_pubkey = Pubkey::from_str("2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv")?; // token mint
|
||||
println!("Buying tokens from PumpSwap...");
|
||||
// buy
|
||||
let protocol_params = PumpSwapParams {
|
||||
pool: None,
|
||||
pool_base_token_account: None,
|
||||
pool_quote_token_account: None,
|
||||
user_base_token_account: None,
|
||||
user_quote_token_account: None,
|
||||
auto_handle_wsol: true,
|
||||
};
|
||||
let buy_params = BuyParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint_pubkey,
|
||||
creator: creator,
|
||||
amount_sol: buy_sol_cost,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
data_size_limit: 0,
|
||||
protocol_params: Box::new(protocol_params.clone()),
|
||||
};
|
||||
let buy_with_tip_params = buy_params
|
||||
.clone()
|
||||
.with_tip(solana_trade_client.swqos_clients.clone());
|
||||
solana_trade_client
|
||||
.buy(
|
||||
mint_pubkey,
|
||||
creator,
|
||||
buy_sol_cost,
|
||||
slippage_basis_points,
|
||||
recent_blockhash,
|
||||
None,
|
||||
trade_platform.clone(),
|
||||
)
|
||||
.buy_use_buy_params(buy_with_tip_params, None)
|
||||
.await?;
|
||||
// sell
|
||||
println!("Selling tokens from PumpSwap...");
|
||||
let amount_token = 0; // 写上真实的amount_token
|
||||
let sell_params = SellParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint_pubkey,
|
||||
creator: creator,
|
||||
amount_token: Some(amount_token),
|
||||
slippage_basis_points: None,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
protocol_params: Box::new(protocol_params.clone()),
|
||||
};
|
||||
solana_trade_client
|
||||
.sell_by_amount_use_sell_params(sell_params)
|
||||
.await?;
|
||||
// Sell 30% * amount_token quantity
|
||||
// solana_trade_client
|
||||
// .sell_by_percent(
|
||||
// mint_pubkey,
|
||||
// creator,
|
||||
// 30,
|
||||
// 100,
|
||||
// recent_blockhash,
|
||||
// trade_platform.clone(),
|
||||
// )
|
||||
// .await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_raydium_launchpad() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 创建一个随机账户作为交易者
|
||||
let payer = Keypair::new();
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let swqos_configs = vec![
|
||||
SwqosConfig::Jito(SwqosRegion::Frankfurt),
|
||||
SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt),
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
];
|
||||
// Define cluster configuration
|
||||
let trade_config = TradeConfig {
|
||||
rpc_url: rpc_url.clone(),
|
||||
commitment: CommitmentConfig::confirmed(),
|
||||
priority_fee: PriorityFee::default(),
|
||||
swqos_configs,
|
||||
lookup_table_key: None,
|
||||
};
|
||||
let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
let amount = 100_000; // 0.0001 SOL
|
||||
let recent_blockhash = solana_trade_client.rpc.get_latest_blockhash().await?;
|
||||
let mint = Pubkey::from_str("xxxxxxx")?;
|
||||
let raydium_launchpad_params = RaydiumLaunchpadParams {
|
||||
virtual_base: None,
|
||||
virtual_quote: None,
|
||||
real_base_before: None,
|
||||
real_quote_before: None,
|
||||
auto_handle_wsol: true,
|
||||
};
|
||||
println!("Buying tokens from Raydium Launchpad...");
|
||||
// buy
|
||||
let buy_params = BuyParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint,
|
||||
creator: Pubkey::default(),
|
||||
amount_sol: amount,
|
||||
slippage_basis_points: None,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
data_size_limit: 0,
|
||||
protocol_params: Box::new(raydium_launchpad_params.clone()),
|
||||
};
|
||||
let buy_with_tip_params = buy_params
|
||||
.clone()
|
||||
.with_tip(solana_trade_client.swqos_clients.clone());
|
||||
solana_trade_client
|
||||
.buy_use_buy_params(buy_with_tip_params, None)
|
||||
.await?;
|
||||
// sell
|
||||
println!("Selling tokens from Raydium Launchpad...");
|
||||
let sell_params = SellParams {
|
||||
rpc: Some(solana_trade_client.rpc.clone()),
|
||||
payer: solana_trade_client.payer.clone(),
|
||||
mint: mint,
|
||||
creator: Pubkey::default(),
|
||||
amount_token: None,
|
||||
slippage_basis_points: None,
|
||||
priority_fee: solana_trade_client.trade_config.clone().priority_fee,
|
||||
lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key,
|
||||
recent_blockhash,
|
||||
protocol_params: Box::new(raydium_launchpad_params.clone()),
|
||||
};
|
||||
solana_trade_client
|
||||
.sell_by_amount_use_sell_params(sell_params)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 使用 GRPC 客户端订阅事件
|
||||
println!("正在订阅 GRPC 事件...");
|
||||
|
||||
let grpc = YellowstoneGrpc::new(
|
||||
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
||||
None,
|
||||
)?;
|
||||
|
||||
// 定义回调函数处理 PumpSwap 事件
|
||||
let callback = |event: Box<dyn UnifiedEvent>| {
|
||||
match_event!(event, {
|
||||
RaydiumLaunchpadPoolCreateEvent => |e: RaydiumLaunchpadPoolCreateEvent| {
|
||||
println!("RaydiumLaunchpadPoolCreateEvent: {:?}", e.base_mint_param.symbol);
|
||||
},
|
||||
RaydiumLaunchpadTradeEvent => |e: RaydiumLaunchpadTradeEvent| {
|
||||
println!("RaydiumLaunchpadTradeEvent: {:?}", e);
|
||||
},
|
||||
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
|
||||
println!("PumpFunTradeEvent: {:?}", e);
|
||||
},
|
||||
PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| {
|
||||
println!("PumpFunCreateTokenEvent: {:?}", e);
|
||||
},
|
||||
PumpSwapBuyEvent => |e: PumpSwapBuyEvent| {
|
||||
println!("Buy event: {:?}", e);
|
||||
},
|
||||
PumpSwapSellEvent => |e: PumpSwapSellEvent| {
|
||||
println!("Sell event: {:?}", e);
|
||||
},
|
||||
PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| {
|
||||
println!("CreatePool event: {:?}", e);
|
||||
},
|
||||
PumpSwapDepositEvent => |e: PumpSwapDepositEvent| {
|
||||
println!("Deposit event: {:?}", e);
|
||||
},
|
||||
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
|
||||
println!("Withdraw event: {:?}", e);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 订阅 PumpSwap 事件
|
||||
println!("开始监听事件,按 Ctrl+C 停止...");
|
||||
let protocols = vec![
|
||||
Protocol::PumpFun,
|
||||
Protocol::PumpSwap,
|
||||
Protocol::RaydiumLaunchpad,
|
||||
];
|
||||
grpc.subscribe_events(protocols, None, None, None, callback)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_shreds() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 使用 ShredStream 客户端订阅事件
|
||||
println!("正在订阅 ShredStream 事件...");
|
||||
|
||||
let shred_stream = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?;
|
||||
|
||||
// 定义回调函数处理 PumpSwap 事件
|
||||
let callback = |event: Box<dyn UnifiedEvent>| {
|
||||
match_event!(event, {
|
||||
RaydiumLaunchpadPoolCreateEvent => |e: RaydiumLaunchpadPoolCreateEvent| {
|
||||
println!("RaydiumLaunchpadPoolCreateEvent: {:?}", e.base_mint_param.symbol);
|
||||
},
|
||||
RaydiumLaunchpadTradeEvent => |e: RaydiumLaunchpadTradeEvent| {
|
||||
println!("RaydiumLaunchpadTradeEvent: {:?}", e);
|
||||
},
|
||||
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
|
||||
println!("PumpFunTradeEvent: {:?}", e);
|
||||
},
|
||||
PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| {
|
||||
println!("PumpFunCreateTokenEvent: {:?}", e);
|
||||
},
|
||||
PumpSwapBuyEvent => |e: PumpSwapBuyEvent| {
|
||||
println!("Buy event: {:?}", e);
|
||||
},
|
||||
PumpSwapSellEvent => |e: PumpSwapSellEvent| {
|
||||
println!("Sell event: {:?}", e);
|
||||
},
|
||||
PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| {
|
||||
println!("CreatePool event: {:?}", e);
|
||||
},
|
||||
PumpSwapDepositEvent => |e: PumpSwapDepositEvent| {
|
||||
println!("Deposit event: {:?}", e);
|
||||
},
|
||||
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
|
||||
println!("Withdraw event: {:?}", e);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 订阅 PumpSwap 事件
|
||||
println!("开始监听事件,按 Ctrl+C 停止...");
|
||||
let protocols = vec![
|
||||
Protocol::PumpFun,
|
||||
Protocol::PumpSwap,
|
||||
Protocol::RaydiumLaunchpad,
|
||||
];
|
||||
shred_stream
|
||||
.shredstream_subscribe(protocols, None, callback)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use solana_sdk::{
|
||||
};
|
||||
use spl_associated_token_account::get_associated_token_address;
|
||||
use pumpfun_program::accounts::BondingCurveAccount as PumpfunBondingCurveAccount;
|
||||
use crate::{accounts::{self, BondingCurveAccount}, common::{pumpfun::logs_data::TradeInfo, PriorityFee, SolanaRpcClient}, constants::{self, pumpfun::{self, global_constants::{CREATOR_FEE, FEE_BASIS_POINTS}, trade::DEFAULT_SLIPPAGE}}};
|
||||
use crate::{accounts::{self, BondingCurveAccount}, common::{PriorityFee, SolanaRpcClient}, constants::{self, pumpfun::{self, global_constants::{CREATOR_FEE, FEE_BASIS_POINTS}, trade::DEFAULT_SLIPPAGE}}, event_parser::protocols::pumpfun::PumpFunTradeEvent};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref ACCOUNT_CACHE: RwLock<HashMap<Pubkey, Arc<accounts::GlobalAccount>>> = RwLock::new(HashMap::new());
|
||||
@@ -327,7 +327,7 @@ pub fn get_token_price(virtual_sol_reserves: u64, virtual_token_reserves: u64) -
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_buy_price(amount: u64, trade_info: &TradeInfo) -> u64 {
|
||||
pub fn get_buy_price(amount: u64, trade_info: &PumpFunTradeEvent) -> u64 {
|
||||
if amount == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
use std::{str::FromStr, time::Instant, sync::Arc};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use solana_hash::Hash;
|
||||
use solana_sdk::{
|
||||
compute_budget::ComputeBudgetInstruction,
|
||||
instruction::Instruction, message::{v0, VersionedMessage},
|
||||
pubkey::Pubkey,
|
||||
native_token::sol_to_lamports,
|
||||
signature::Keypair,
|
||||
signer::Signer,
|
||||
system_instruction,
|
||||
transaction::{Transaction, VersionedTransaction}
|
||||
};
|
||||
use spl_associated_token_account::instruction::create_associated_token_account;
|
||||
|
||||
use crate::{
|
||||
common::{PriorityFee, SolanaRpcClient}, constants, instruction,
|
||||
ipfs::TokenMetadataIPFS, swqos::{SwqosClient, TradeType},
|
||||
};
|
||||
|
||||
use crate::pumpfun::common::{
|
||||
create_priority_fee_instructions,
|
||||
get_buy_amount_with_slippage, get_global_account
|
||||
};
|
||||
|
||||
use crate::common::tip_cache::TipCache;
|
||||
|
||||
use super::common::{get_bonding_curve_account, get_buy_token_amount, get_creator_vault_pda};
|
||||
|
||||
/// Create a new token
|
||||
pub async fn create(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Keypair,
|
||||
ipfs: TokenMetadataIPFS,
|
||||
priority_fee: PriorityFee,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut instructions = create_priority_fee_instructions(priority_fee);
|
||||
|
||||
instructions.push(instruction::create(
|
||||
payer.as_ref(),
|
||||
&mint,
|
||||
instruction::Create {
|
||||
_name: ipfs.metadata.name,
|
||||
_symbol: ipfs.metadata.symbol,
|
||||
_uri: ipfs.metadata_uri,
|
||||
_creator: payer.pubkey(),
|
||||
},
|
||||
));
|
||||
|
||||
let recent_blockhash = rpc.get_latest_blockhash().await?;
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&instructions,
|
||||
Some(&payer.pubkey()),
|
||||
&[payer.as_ref(), &mint],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
rpc.send_and_confirm_transaction(&transaction).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create and buy tokens in one transaction
|
||||
pub async fn create_and_buy(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Keypair,
|
||||
ipfs: TokenMetadataIPFS,
|
||||
buy_sol_cost: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
recent_blockhash: Hash,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
if buy_sol_cost == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
}
|
||||
|
||||
let mint = Arc::new(mint);
|
||||
let transaction = build_create_and_buy_transaction(rpc.clone(), payer.clone(), mint.clone(), ipfs, buy_sol_cost, slippage_basis_points, priority_fee.clone(), recent_blockhash).await?;
|
||||
rpc.send_and_confirm_transaction(&transaction).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_and_buy_with_tip(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
fee_clients: Vec<Arc<SwqosClient>>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Keypair,
|
||||
ipfs: TokenMetadataIPFS,
|
||||
buy_sol_cost: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
recent_blockhash: Hash,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let start_time = Instant::now();
|
||||
let mint = Arc::new(mint);
|
||||
let build_instructions = build_create_and_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), ipfs.clone(), buy_sol_cost, slippage_basis_points).await?;
|
||||
let mut handles = vec![];
|
||||
for fee_client in fee_clients {
|
||||
let tip_account = fee_client.get_tip_account()?;
|
||||
let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?);
|
||||
let transaction = build_create_and_buy_transaction_with_tip(/*rpc.clone(),*/ tip_account, payer.clone(), priority_fee.clone(), build_instructions.clone(), recent_blockhash).await?;
|
||||
let handle = tokio::spawn(async move {
|
||||
fee_client.send_transaction(TradeType::CreateAndBuy, &transaction).await.map_err(|e| anyhow!(e.to_string()))?;
|
||||
println!("Total Jito create and buy operation time: {:?}ms", start_time.elapsed().as_millis());
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
match handle.await {
|
||||
Ok(Ok(_)) => (),
|
||||
Ok(Err(e)) => println!("Error in task: {}", e),
|
||||
Err(e) => println!("Task join error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn build_create_and_buy_transaction(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Arc<Keypair>,
|
||||
ipfs: TokenMetadataIPFS,
|
||||
buy_sol_cost: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
recent_blockhash: Hash,
|
||||
) -> Result<Transaction, anyhow::Error> {
|
||||
let mut instructions = vec![
|
||||
ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price),
|
||||
ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit),
|
||||
];
|
||||
|
||||
let build_instructions = build_create_and_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), ipfs, buy_sol_cost, slippage_basis_points).await?;
|
||||
instructions.extend(build_instructions);
|
||||
|
||||
// let recent_blockhash = rpc.get_latest_blockhash().await?;
|
||||
// let recent_blockhash = Hash::default();
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&instructions,
|
||||
Some(&payer.pubkey()),
|
||||
&[payer.as_ref(), mint.as_ref()],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
pub async fn build_create_and_buy_transaction_with_tip(
|
||||
// rpc: Arc<SolanaRpcClient>,
|
||||
tip_account: Arc<Pubkey>,
|
||||
payer: Arc<Keypair>,
|
||||
priority_fee: PriorityFee,
|
||||
build_instructions: Vec<Instruction>,
|
||||
recent_blockhash: Hash,
|
||||
) -> Result<VersionedTransaction, anyhow::Error> {
|
||||
let tip_cache = TipCache::get_instance();
|
||||
let tip_amount = tip_cache.get_tip();
|
||||
|
||||
let mut instructions = vec![
|
||||
ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price),
|
||||
ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit),
|
||||
system_instruction::transfer(
|
||||
&payer.pubkey(),
|
||||
&tip_account,
|
||||
sol_to_lamports(tip_amount),
|
||||
),
|
||||
];
|
||||
instructions.extend(build_instructions);
|
||||
|
||||
// let recent_blockhash = rpc.get_latest_blockhash().await?;
|
||||
// let recent_blockhash = Hash::default();
|
||||
let v0_message: v0::Message =
|
||||
v0::Message::try_compile(&payer.pubkey(), &instructions, &[], recent_blockhash)?;
|
||||
|
||||
let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message);
|
||||
let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?;
|
||||
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
pub async fn build_create_and_buy_instructions(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Arc<Keypair>,
|
||||
ipfs: TokenMetadataIPFS,
|
||||
buy_sol_cost: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
) -> Result<Vec<Instruction>, anyhow::Error> {
|
||||
if buy_sol_cost == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
}
|
||||
|
||||
let (bonding_curve_account, bonding_curve_pda) = get_bonding_curve_account(&rpc, &mint.pubkey()).await?;
|
||||
let creator_vault_pda = get_creator_vault_pda(&bonding_curve_account.creator).unwrap();
|
||||
let (buy_token_amount, max_sol_cost) = get_buy_token_amount(&bonding_curve_account, buy_sol_cost, slippage_basis_points)?;
|
||||
|
||||
let mut instructions = vec![];
|
||||
|
||||
instructions.push(instruction::create(
|
||||
payer.as_ref(),
|
||||
mint.as_ref(),
|
||||
instruction::Create {
|
||||
_name: ipfs.metadata.name.clone(),
|
||||
_symbol: ipfs.metadata.symbol.clone(),
|
||||
_uri: ipfs.metadata_uri.clone(),
|
||||
_creator: payer.pubkey(),
|
||||
},
|
||||
));
|
||||
|
||||
instructions.push(create_associated_token_account(
|
||||
&payer.pubkey(),
|
||||
&payer.pubkey(),
|
||||
&mint.pubkey(),
|
||||
&constants::pumpfun::accounts::TOKEN_PROGRAM,
|
||||
));
|
||||
|
||||
instructions.push(instruction::buy(
|
||||
payer.as_ref(),
|
||||
&mint.pubkey(),
|
||||
&bonding_curve_pda,
|
||||
&creator_vault_pda,
|
||||
&constants::pumpfun::global_constants::FEE_RECIPIENT,
|
||||
instruction::Buy {
|
||||
_amount: buy_token_amount,
|
||||
_max_sol_cost: max_sol_cost,
|
||||
},
|
||||
));
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
@@ -81,9 +81,6 @@ pub async fn sell_by_amount(
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
recent_blockhash: Hash,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
if amount == 0 {
|
||||
return Err(anyhow!("Amount must be greater than 0"));
|
||||
}
|
||||
sell(
|
||||
rpc,
|
||||
payer,
|
||||
@@ -138,9 +135,6 @@ pub async fn sell_by_amount_with_tip(
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
recent_blockhash: Hash,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
if amount == 0 {
|
||||
return Err(anyhow!("Amount must be greater than 0"));
|
||||
}
|
||||
sell_with_tip(
|
||||
rpc,
|
||||
fee_clients,
|
||||
|
||||
+49
-23
@@ -1,7 +1,7 @@
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use crate::{common::SolanaRpcClient, constants::pumpswap::accounts};
|
||||
use anyhow::anyhow;
|
||||
use solana_account_decoder::UiAccountEncoding;
|
||||
use crate::{common::SolanaRpcClient, constants::pumpswap::accounts};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -29,16 +29,39 @@ impl Pool {
|
||||
let pool_bump = data[0];
|
||||
let index = u16::from_le_bytes([data[1], data[2]]);
|
||||
|
||||
let creator = Pubkey::new_from_array(data[3..35].try_into().map_err(|e| anyhow!("Failed to convert creator: {:?}", e))?);
|
||||
let base_mint = Pubkey::new_from_array(data[35..67].try_into().map_err(|e| anyhow!("Failed to convert base_mint: {:?}", e))?);
|
||||
let quote_mint = Pubkey::new_from_array(data[67..99].try_into().map_err(|e| anyhow!("Failed to convert quote_mint: {:?}", e))?);
|
||||
let lp_mint = Pubkey::new_from_array(data[99..131].try_into().map_err(|e| anyhow!("Failed to convert lp_mint: {:?}", e))?);
|
||||
let pool_base_token_account = Pubkey::new_from_array(data[131..163].try_into().map_err(|e| anyhow!("Failed to convert pool_base_token_account: {:?}", e))?);
|
||||
let pool_quote_token_account = Pubkey::new_from_array(data[163..195].try_into().map_err(|e| anyhow!("Failed to convert pool_quote_token_account: {:?}", e))?);
|
||||
let creator = Pubkey::new_from_array(
|
||||
data[3..35]
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("Failed to convert creator: {:?}", e))?,
|
||||
);
|
||||
let base_mint = Pubkey::new_from_array(
|
||||
data[35..67]
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("Failed to convert base_mint: {:?}", e))?,
|
||||
);
|
||||
let quote_mint = Pubkey::new_from_array(
|
||||
data[67..99]
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("Failed to convert quote_mint: {:?}", e))?,
|
||||
);
|
||||
let lp_mint = Pubkey::new_from_array(
|
||||
data[99..131]
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("Failed to convert lp_mint: {:?}", e))?,
|
||||
);
|
||||
let pool_base_token_account = Pubkey::new_from_array(
|
||||
data[131..163]
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("Failed to convert pool_base_token_account: {:?}", e))?,
|
||||
);
|
||||
let pool_quote_token_account = Pubkey::new_from_array(
|
||||
data[163..195]
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("Failed to convert pool_quote_token_account: {:?}", e))?,
|
||||
);
|
||||
|
||||
let lp_supply = u64::from_le_bytes([
|
||||
data[195], data[196], data[197], data[198],
|
||||
data[199], data[200], data[201], data[202],
|
||||
data[195], data[196], data[197], data[198], data[199], data[200], data[201], data[202],
|
||||
]);
|
||||
|
||||
Ok(Self {
|
||||
@@ -92,25 +115,21 @@ impl Pool {
|
||||
};
|
||||
|
||||
let program_id = crate::constants::pumpswap::accounts::AMM_PROGRAM;
|
||||
println!("program_id: {:?}", program_id);
|
||||
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
|
||||
let accounts = rpc
|
||||
.get_program_accounts_with_config(&program_id, config)
|
||||
.await?;
|
||||
|
||||
if accounts.is_empty() {
|
||||
return Err(anyhow!("No pool found for mint {}", mint));
|
||||
}
|
||||
|
||||
let mut pools: Vec<_> = accounts.into_iter()
|
||||
.filter_map(|(addr, acc)| {
|
||||
Self::from_bytes(&acc.data)
|
||||
.map(|pool| (addr, pool))
|
||||
.ok()
|
||||
})
|
||||
let mut pools: Vec<_> = accounts
|
||||
.into_iter()
|
||||
.filter_map(|(addr, acc)| Self::from_bytes(&acc.data).map(|pool| (addr, pool)).ok())
|
||||
.collect();
|
||||
pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply));
|
||||
|
||||
let (address, pool) = pools[0].clone();
|
||||
println!("pool: {:?}", pool);
|
||||
println!("address: {:?}", address);
|
||||
Ok((address, pool))
|
||||
}
|
||||
|
||||
@@ -118,11 +137,18 @@ impl Pool {
|
||||
&self,
|
||||
rpc: &SolanaRpcClient,
|
||||
) -> Result<(u64, u64), anyhow::Error> {
|
||||
let base_balance = rpc.get_token_account_balance(&self.pool_base_token_account).await?;
|
||||
let quote_balance = rpc.get_token_account_balance(&self.pool_quote_token_account).await?;
|
||||
let base_balance = rpc
|
||||
.get_token_account_balance(&self.pool_base_token_account)
|
||||
.await?;
|
||||
let quote_balance = rpc
|
||||
.get_token_account_balance(&self.pool_quote_token_account)
|
||||
.await?;
|
||||
|
||||
let base_amount = base_balance.amount.parse::<u64>().map_err(|e| anyhow!(e))?;
|
||||
let quote_amount = quote_balance.amount.parse::<u64>().map_err(|e| anyhow!(e))?;
|
||||
let quote_amount = quote_balance
|
||||
.amount
|
||||
.parse::<u64>()
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
|
||||
Ok((base_amount, quote_amount))
|
||||
}
|
||||
|
||||
@@ -114,10 +114,6 @@ pub async fn sell_by_amount(
|
||||
user_base_token_account: Option<Pubkey>,
|
||||
user_quote_token_account: Option<Pubkey>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
if amount == 0 {
|
||||
return Err(anyhow!("Amount must be greater than 0"));
|
||||
}
|
||||
|
||||
sell(
|
||||
rpc,
|
||||
payer,
|
||||
@@ -249,10 +245,6 @@ pub async fn sell_by_amount_with_tip(
|
||||
user_base_token_account: Option<Pubkey>,
|
||||
user_quote_token_account: Option<Pubkey>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
if amount == 0 {
|
||||
return Err(anyhow!("Amount must be greater than 0"));
|
||||
}
|
||||
|
||||
sell_with_tip(
|
||||
rpc,
|
||||
swqos_clients,
|
||||
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
use solana_hash::Hash;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::swqos::SwqosClient;
|
||||
use crate::trading::{
|
||||
core::params::{PumpSwapParams, RaydiumLaunchpadParams},
|
||||
factory::Protocol,
|
||||
BuyParams, TradeFactory,
|
||||
};
|
||||
use crate::{common::PriorityFee, SolanaRpcClient};
|
||||
|
||||
// Constants for compute budget
|
||||
// Increased from 64KB to 256KB to handle larger transactions
|
||||
const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 256 * 1024;
|
||||
|
||||
// Buy tokens from a Pumpswap pool
|
||||
pub async fn buy(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
virtual_base: u128,
|
||||
virtual_quote: u128,
|
||||
real_base_before: u128,
|
||||
real_quote_before: u128,
|
||||
amount_sol: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
recent_blockhash: Hash,
|
||||
auto_handle_wsol: bool,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
// 创建执行器
|
||||
let executor = TradeFactory::create_executor(Protocol::RaydiumLaunchpad);
|
||||
// 创建协议特定参数
|
||||
let protocol_params = Box::new(RaydiumLaunchpadParams {
|
||||
auto_handle_wsol: auto_handle_wsol,
|
||||
virtual_base: Some(virtual_base),
|
||||
virtual_quote: Some(virtual_quote),
|
||||
real_base_before: Some(real_base_before),
|
||||
real_quote_before: Some(real_quote_before),
|
||||
});
|
||||
// 创建买入参数
|
||||
let buy_params = BuyParams {
|
||||
rpc: Some(rpc.clone()),
|
||||
payer: payer,
|
||||
mint: mint,
|
||||
creator: Pubkey::default(),
|
||||
amount_sol: amount_sol,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
priority_fee: priority_fee,
|
||||
lookup_table_key: lookup_table_key,
|
||||
recent_blockhash: recent_blockhash,
|
||||
data_size_limit: MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT,
|
||||
protocol_params,
|
||||
};
|
||||
// 执行买入
|
||||
executor.buy(buy_params).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Buy tokens using a MEV service
|
||||
pub async fn buy_with_tip(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
swqos_clients: Vec<Arc<SwqosClient>>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
virtual_base: u128,
|
||||
virtual_quote: u128,
|
||||
real_base_before: u128,
|
||||
real_quote_before: u128,
|
||||
amount_sol: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
recent_blockhash: Hash,
|
||||
auto_handle_wsol: bool,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
// 创建执行器
|
||||
let executor = TradeFactory::create_executor(Protocol::RaydiumLaunchpad);
|
||||
// 创建协议特定参数
|
||||
let protocol_params = Box::new(RaydiumLaunchpadParams {
|
||||
auto_handle_wsol: auto_handle_wsol,
|
||||
virtual_base: Some(virtual_base),
|
||||
virtual_quote: Some(virtual_quote),
|
||||
real_base_before: Some(real_base_before),
|
||||
real_quote_before: Some(real_quote_before),
|
||||
});
|
||||
// 创建买入参数
|
||||
let buy_params = BuyParams {
|
||||
rpc: Some(rpc.clone()),
|
||||
payer: payer,
|
||||
mint: mint,
|
||||
creator: Pubkey::default(),
|
||||
amount_sol: amount_sol,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
priority_fee: priority_fee,
|
||||
lookup_table_key: lookup_table_key,
|
||||
recent_blockhash: recent_blockhash,
|
||||
data_size_limit: MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT,
|
||||
protocol_params,
|
||||
};
|
||||
let buy_with_tip_params = buy_params.with_tip(swqos_clients);
|
||||
// 执行买入
|
||||
executor.buy_with_tip(buy_with_tip_params).await?;
|
||||
Ok(())
|
||||
}
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
use anyhow::anyhow;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use spl_associated_token_account::get_associated_token_address;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants};
|
||||
|
||||
pub fn get_amount_out(
|
||||
amount_in: u64,
|
||||
protocol_fee_rate: u128,
|
||||
platform_fee_rate: u128,
|
||||
share_fee_rate: u128,
|
||||
virtual_base: u128,
|
||||
virtual_quote: u128,
|
||||
real_base_before: u128,
|
||||
real_quote_before: u128,
|
||||
slippage_basis_points: u128,
|
||||
) -> u64 {
|
||||
let amount_in_u128 = amount_in as u128;
|
||||
let protocol_fee = (amount_in_u128 * protocol_fee_rate / 10000) as u128;
|
||||
let platform_fee = (amount_in_u128 * platform_fee_rate / 10000) as u128;
|
||||
let share_fee = (amount_in_u128 * share_fee_rate / 10000) as u128;
|
||||
let amount_in_net = amount_in_u128
|
||||
.checked_sub(protocol_fee)
|
||||
.unwrap()
|
||||
.checked_sub(platform_fee)
|
||||
.unwrap()
|
||||
.checked_sub(share_fee)
|
||||
.unwrap();
|
||||
let input_reserve = virtual_quote.checked_add(real_quote_before).unwrap();
|
||||
let output_reserve = virtual_base.checked_sub(real_base_before).unwrap();
|
||||
let numerator = amount_in_net.checked_mul(output_reserve).unwrap();
|
||||
let denominator = input_reserve.checked_add(amount_in_net).unwrap();
|
||||
let mut amount_out = numerator.checked_div(denominator).unwrap();
|
||||
|
||||
amount_out = amount_out - (amount_out * slippage_basis_points) / 10000;
|
||||
amount_out as u64
|
||||
}
|
||||
|
||||
pub fn get_pool_pda(base_mint: &Pubkey, quote_mint: &Pubkey) -> Option<Pubkey> {
|
||||
let seeds: &[&[u8]; 3] = &[
|
||||
constants::raydium_launchpad::seeds::POOL_SEED,
|
||||
base_mint.as_ref(),
|
||||
quote_mint.as_ref(),
|
||||
];
|
||||
let program_id: &Pubkey = &constants::raydium_launchpad::accounts::LAUNCHPAD_PROGRAM;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
}
|
||||
|
||||
pub fn get_vault_pda(pool_state: &Pubkey, mint: &Pubkey) -> Option<Pubkey> {
|
||||
let seeds: &[&[u8]; 3] = &[
|
||||
constants::raydium_launchpad::seeds::POOL_VAULT_SEED,
|
||||
pool_state.as_ref(),
|
||||
mint.as_ref(),
|
||||
];
|
||||
let program_id: &Pubkey = &constants::raydium_launchpad::accounts::LAUNCHPAD_PROGRAM;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
}
|
||||
|
||||
pub async fn get_token_balance(
|
||||
rpc: &SolanaRpcClient,
|
||||
payer: &Pubkey,
|
||||
mint: &Pubkey,
|
||||
) -> Result<u64, anyhow::Error> {
|
||||
println!("payer: {:?}", payer);
|
||||
println!("mint: {:?}", mint);
|
||||
let ata = get_associated_token_address(payer, mint);
|
||||
let balance = rpc.get_token_account_balance(&ata).await?;
|
||||
let balance_u64 = balance
|
||||
.amount
|
||||
.parse::<u64>()
|
||||
.map_err(|_| anyhow!("Failed to parse token balance"))?;
|
||||
Ok(balance_u64)
|
||||
}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
pub mod buy;
|
||||
pub mod sell;
|
||||
pub mod common;
|
||||
pub mod pool;
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
use crate::{common::SolanaRpcClient, constants::raydium_launchpad::accounts};
|
||||
use anyhow::anyhow;
|
||||
use borsh::BorshDeserialize;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
#[derive(Debug, Clone, BorshDeserialize)]
|
||||
pub struct VestingSchedule {
|
||||
pub total_locked_amount: u64,
|
||||
pub cliff_period: u64,
|
||||
pub unlock_period: u64,
|
||||
pub start_time: u64,
|
||||
pub allocated_share_amount: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, BorshDeserialize)]
|
||||
pub struct Pool {
|
||||
pub epoch: u64,
|
||||
pub auth_bump: u8,
|
||||
pub status: u8,
|
||||
pub base_decimals: u8,
|
||||
pub quote_decimals: u8,
|
||||
pub migrate_type: u8,
|
||||
pub supply: u64,
|
||||
pub total_base_sell: u64,
|
||||
pub virtual_base: u64,
|
||||
pub virtual_quote: u64,
|
||||
pub real_base: u64,
|
||||
pub real_quote: u64,
|
||||
pub total_quote_fund_raising: u64,
|
||||
pub quote_protocol_fee: u64,
|
||||
pub platform_fee: u64,
|
||||
pub migrate_fee: u64,
|
||||
pub vesting_schedule: VestingSchedule,
|
||||
pub global_config: Pubkey,
|
||||
pub platform_config: Pubkey,
|
||||
pub base_mint: Pubkey,
|
||||
pub quote_mint: Pubkey,
|
||||
pub base_vault: Pubkey,
|
||||
pub quote_vault: Pubkey,
|
||||
pub creator: Pubkey,
|
||||
pub padding: [u64; 8],
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
pub fn from_bytes(data: &[u8]) -> Result<Self, anyhow::Error> {
|
||||
let pool = Pool::try_from_slice(&data[8..])?;
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub async fn fetch(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let account = rpc.get_account(pool_address).await?;
|
||||
|
||||
if account.owner != accounts::LAUNCHPAD_PROGRAM {
|
||||
return Err(anyhow!("Account is not owned by RaydiumLaunchpad program"));
|
||||
}
|
||||
|
||||
Self::from_bytes(&account.data)
|
||||
}
|
||||
}
|
||||
Executable
+240
@@ -0,0 +1,240 @@
|
||||
use anyhow::anyhow;
|
||||
use solana_hash::Hash;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::common::{PriorityFee, SolanaRpcClient};
|
||||
use crate::pumpswap::common::get_token_balance;
|
||||
use crate::swqos::SwqosClient;
|
||||
use crate::trading::{
|
||||
core::params::RaydiumLaunchpadParams, factory::Protocol, SellParams, TradeFactory,
|
||||
};
|
||||
|
||||
// Sell tokens to a Pumpswap pool
|
||||
pub async fn sell(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
virtual_base: u128,
|
||||
virtual_quote: u128,
|
||||
real_base_before: u128,
|
||||
real_quote_before: u128,
|
||||
amount_token: Option<u64>,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
recent_blockhash: Hash,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let executor = TradeFactory::create_executor(Protocol::RaydiumLaunchpad);
|
||||
// 创建PumpFun协议参数
|
||||
let protocol_params = Box::new(RaydiumLaunchpadParams {
|
||||
virtual_base: Some(virtual_base),
|
||||
virtual_quote: Some(virtual_quote),
|
||||
real_base_before: Some(real_base_before),
|
||||
real_quote_before: Some(real_quote_before),
|
||||
auto_handle_wsol: true,
|
||||
});
|
||||
// 创建卖出参数
|
||||
let sell_params = SellParams {
|
||||
rpc: Some(rpc.clone()),
|
||||
payer: payer.clone(),
|
||||
mint,
|
||||
creator: Pubkey::default(),
|
||||
amount_token: amount_token,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
priority_fee: priority_fee.clone(),
|
||||
lookup_table_key,
|
||||
recent_blockhash,
|
||||
protocol_params,
|
||||
};
|
||||
// 执行卖出交易
|
||||
executor.sell(sell_params).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Sell tokens by percentage
|
||||
pub async fn sell_by_percent(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
virtual_base: u128,
|
||||
virtual_quote: u128,
|
||||
real_base_before: u128,
|
||||
real_quote_before: u128,
|
||||
percent: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
recent_blockhash: Hash,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
if percent == 0 || percent > 100 {
|
||||
return Err(anyhow!("Percentage must be between 1 and 100"));
|
||||
}
|
||||
let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?;
|
||||
let amount = balance_u64 * percent / 100;
|
||||
sell(
|
||||
rpc,
|
||||
payer,
|
||||
mint,
|
||||
virtual_base,
|
||||
virtual_quote,
|
||||
real_base_before,
|
||||
real_quote_before,
|
||||
Some(amount),
|
||||
slippage_basis_points,
|
||||
priority_fee,
|
||||
lookup_table_key,
|
||||
recent_blockhash,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Sell tokens by amount
|
||||
pub async fn sell_by_amount(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
virtual_base: u128,
|
||||
virtual_quote: u128,
|
||||
real_base_before: u128,
|
||||
real_quote_before: u128,
|
||||
amount: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
recent_blockhash: Hash,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
sell(
|
||||
rpc,
|
||||
payer,
|
||||
mint,
|
||||
virtual_base,
|
||||
virtual_quote,
|
||||
real_base_before,
|
||||
real_quote_before,
|
||||
Some(amount),
|
||||
slippage_basis_points,
|
||||
priority_fee,
|
||||
lookup_table_key,
|
||||
recent_blockhash,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
// Sell tokens using a MEV service
|
||||
pub async fn sell_with_tip(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
swqos_clients: Vec<Arc<SwqosClient>>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
virtual_base: u128,
|
||||
virtual_quote: u128,
|
||||
real_base_before: u128,
|
||||
real_quote_before: u128,
|
||||
amount_token: Option<u64>,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
recent_blockhash: Hash,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let executor = TradeFactory::create_executor(Protocol::RaydiumLaunchpad);
|
||||
// 创建PumpFun协议参数
|
||||
let protocol_params = Box::new(RaydiumLaunchpadParams {
|
||||
virtual_base: Some(virtual_base),
|
||||
virtual_quote: Some(virtual_quote),
|
||||
real_base_before: Some(real_base_before),
|
||||
real_quote_before: Some(real_quote_before),
|
||||
auto_handle_wsol: true,
|
||||
});
|
||||
// 创建卖出参数
|
||||
let sell_params = SellParams {
|
||||
rpc: Some(rpc.clone()),
|
||||
payer: payer.clone(),
|
||||
mint,
|
||||
creator: Pubkey::default(),
|
||||
amount_token: amount_token,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
priority_fee: priority_fee.clone(),
|
||||
lookup_table_key,
|
||||
recent_blockhash,
|
||||
protocol_params,
|
||||
};
|
||||
let sell_with_tip_params = sell_params.with_tip(swqos_clients);
|
||||
// 执行卖出交易
|
||||
executor.sell_with_tip(sell_with_tip_params).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Sell tokens by percentage using a MEV service
|
||||
pub async fn sell_by_percent_with_tip(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
swqos_clients: Vec<Arc<SwqosClient>>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
virtual_base: u128,
|
||||
virtual_quote: u128,
|
||||
real_base_before: u128,
|
||||
real_quote_before: u128,
|
||||
percent: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
recent_blockhash: Hash,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
if percent == 0 || percent > 100 {
|
||||
return Err(anyhow!("Percentage must be between 1 and 100"));
|
||||
}
|
||||
|
||||
let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?;
|
||||
let amount = balance_u64 * percent / 100;
|
||||
sell_with_tip(
|
||||
rpc,
|
||||
swqos_clients,
|
||||
payer,
|
||||
mint,
|
||||
virtual_base,
|
||||
virtual_quote,
|
||||
real_base_before,
|
||||
real_quote_before,
|
||||
Some(amount),
|
||||
slippage_basis_points,
|
||||
priority_fee,
|
||||
lookup_table_key,
|
||||
recent_blockhash,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
// Sell tokens by amount using a MEV service
|
||||
pub async fn sell_by_amount_with_tip(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
swqos_clients: Vec<Arc<SwqosClient>>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
virtual_base: u128,
|
||||
virtual_quote: u128,
|
||||
real_base_before: u128,
|
||||
real_quote_before: u128,
|
||||
amount: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
recent_blockhash: Hash,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
sell_with_tip(
|
||||
rpc,
|
||||
swqos_clients,
|
||||
payer,
|
||||
mint,
|
||||
virtual_base,
|
||||
virtual_quote,
|
||||
real_base_before,
|
||||
real_quote_before,
|
||||
Some(amount),
|
||||
slippage_basis_points,
|
||||
priority_fee,
|
||||
lookup_table_key,
|
||||
recent_blockhash,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub const DEFAULT_SLIPPAGE_BASIS_POINTS: u64 = 100;
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod constants;
|
||||
pub mod params;
|
||||
pub mod traits;
|
||||
pub mod executor;
|
||||
|
||||
@@ -122,6 +122,26 @@ impl ProtocolParams for PumpSwapParams {
|
||||
}
|
||||
}
|
||||
|
||||
/// RaydiumLaunchpad协议特定参数
|
||||
#[derive(Clone)]
|
||||
pub struct RaydiumLaunchpadParams {
|
||||
pub virtual_base: Option<u128>,
|
||||
pub virtual_quote: Option<u128>,
|
||||
pub real_base_before: Option<u128>,
|
||||
pub real_quote_before: Option<u128>,
|
||||
pub auto_handle_wsol: bool,
|
||||
}
|
||||
|
||||
impl ProtocolParams for RaydiumLaunchpadParams {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn ProtocolParams> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl BuyParams {
|
||||
/// 转换为BuyWithTipParams
|
||||
pub fn with_tip(self, swqos_clients: Vec<Arc<SwqosClient>>) -> BuyWithTipParams {
|
||||
|
||||
+17
-30
@@ -1,6 +1,8 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::trading::protocols::raydium_launchpad::RaydiumLaunchpadInstructionBuilder;
|
||||
|
||||
use super::{
|
||||
core::{executor::GenericTradeExecutor, traits::TradeExecutor},
|
||||
protocols::{pumpfun::PumpFunInstructionBuilder, pumpswap::PumpSwapInstructionBuilder},
|
||||
@@ -11,6 +13,7 @@ use super::{
|
||||
pub enum Protocol {
|
||||
PumpFun,
|
||||
PumpSwap,
|
||||
RaydiumLaunchpad,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Protocol {
|
||||
@@ -18,6 +21,7 @@ impl std::fmt::Display for Protocol {
|
||||
match self {
|
||||
Protocol::PumpFun => write!(f, "PumpFun"),
|
||||
Protocol::PumpSwap => write!(f, "PumpSwap"),
|
||||
Protocol::RaydiumLaunchpad => write!(f, "RaydiumLaunchpad"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +33,7 @@ impl std::str::FromStr for Protocol {
|
||||
match s.to_lowercase().as_str() {
|
||||
"pumpfun" => Ok(Protocol::PumpFun),
|
||||
"pumpswap" => Ok(Protocol::PumpSwap),
|
||||
"raydiumlaunchpad" => Ok(Protocol::RaydiumLaunchpad),
|
||||
_ => Err(anyhow!("Unsupported protocol: {}", s)),
|
||||
}
|
||||
}
|
||||
@@ -49,12 +54,23 @@ impl TradeFactory {
|
||||
let instruction_builder = Arc::new(PumpSwapInstructionBuilder);
|
||||
Arc::new(GenericTradeExecutor::new(instruction_builder, "PumpSwap"))
|
||||
}
|
||||
Protocol::RaydiumLaunchpad => {
|
||||
let instruction_builder = Arc::new(RaydiumLaunchpadInstructionBuilder);
|
||||
Arc::new(GenericTradeExecutor::new(
|
||||
instruction_builder,
|
||||
"RaydiumLaunchpad",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取所有支持的协议
|
||||
pub fn supported_protocols() -> Vec<Protocol> {
|
||||
vec![Protocol::PumpFun, Protocol::PumpSwap]
|
||||
vec![
|
||||
Protocol::PumpFun,
|
||||
Protocol::PumpSwap,
|
||||
Protocol::RaydiumLaunchpad,
|
||||
]
|
||||
}
|
||||
|
||||
/// 检查协议是否支持
|
||||
@@ -62,32 +78,3 @@ impl TradeFactory {
|
||||
Self::supported_protocols().contains(protocol)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_protocol_from_str() {
|
||||
assert_eq!("pumpfun".parse::<Protocol>().unwrap(), Protocol::PumpFun);
|
||||
assert_eq!("pumpswap".parse::<Protocol>().unwrap(), Protocol::PumpSwap);
|
||||
assert_eq!("PUMPFUN".parse::<Protocol>().unwrap(), Protocol::PumpFun);
|
||||
assert!("unknown".parse::<Protocol>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_executor() {
|
||||
let pumpfun_executor = TradeFactory::create_executor(Protocol::PumpFun);
|
||||
assert_eq!(pumpfun_executor.protocol_name(), "PumpFun");
|
||||
|
||||
let pumpswap_executor = TradeFactory::create_executor(Protocol::PumpSwap);
|
||||
assert_eq!(pumpswap_executor.protocol_name(), "PumpSwap");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supported_protocols() {
|
||||
let protocols = TradeFactory::supported_protocols();
|
||||
assert!(protocols.contains(&Protocol::PumpFun));
|
||||
assert!(protocols.contains(&Protocol::PumpSwap));
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod pumpfun;
|
||||
pub mod pumpswap;
|
||||
pub mod pumpswap;
|
||||
pub mod raydium_launchpad;
|
||||
@@ -10,14 +10,13 @@ use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
accounts::BondingCurveAccount,
|
||||
constants::{self, pumpfun::global_constants::FEE_RECIPIENT, trade_type::SNIPER_BUY},
|
||||
constants::{self, pumpfun::{global_constants::FEE_RECIPIENT, trade::DEFAULT_SLIPPAGE}, trade_type::SNIPER_BUY},
|
||||
instruction,
|
||||
pumpfun::common::{
|
||||
calculate_with_slippage_buy, get_bonding_curve_account_v2, get_bonding_curve_pda,
|
||||
get_buy_token_amount_from_sol_amount, get_creator_vault_pda, init_bonding_curve_account,
|
||||
},
|
||||
trading::core::{
|
||||
constants::DEFAULT_SLIPPAGE_BASIS_POINTS,
|
||||
params::{BuyParams, PumpFunParams, SellParams},
|
||||
traits::InstructionBuilder,
|
||||
},
|
||||
@@ -50,7 +49,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
params.amount_sol,
|
||||
params
|
||||
.slippage_basis_points
|
||||
.unwrap_or(DEFAULT_SLIPPAGE_BASIS_POINTS),
|
||||
.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
let creator_vault_pda = bonding_curve.get_creator_vault_pda();
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ use crate::{
|
||||
get_token_balance,
|
||||
},
|
||||
trading::core::{
|
||||
constants::DEFAULT_SLIPPAGE_BASIS_POINTS,
|
||||
params::{BuyParams, PumpSwapParams, SellParams},
|
||||
traits::InstructionBuilder,
|
||||
},
|
||||
@@ -367,6 +366,17 @@ impl PumpSwapInstructionBuilder {
|
||||
|
||||
let mut instructions = vec![];
|
||||
|
||||
// 插入wsol
|
||||
instructions.push(
|
||||
// 创建wSOL ATA账户,如果不存在
|
||||
create_associated_token_account_idempotent(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
&accounts::WSOL_TOKEN_ACCOUNT,
|
||||
&accounts::TOKEN_PROGRAM,
|
||||
),
|
||||
);
|
||||
|
||||
// 创建用户的代币账户
|
||||
instructions.push(create_associated_token_account_idempotent(
|
||||
¶ms.payer.pubkey(),
|
||||
|
||||
Executable
+284
@@ -0,0 +1,284 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signer::Signer};
|
||||
use spl_associated_token_account::instruction::create_associated_token_account_idempotent;
|
||||
|
||||
use crate::{
|
||||
constants::raydium_launchpad::{
|
||||
accounts, trade::DEFAULT_SLIPPAGE, BUY_EXECT_IN_DISCRIMINATOR, SELL_EXECT_IN_DISCRIMINATOR,
|
||||
},
|
||||
raydium_launchpad::{
|
||||
common::{get_amount_out, get_pool_pda, get_token_balance, get_vault_pda},
|
||||
pool::Pool,
|
||||
},
|
||||
trading::core::{
|
||||
params::{BuyParams, RaydiumLaunchpadParams, SellParams},
|
||||
traits::InstructionBuilder,
|
||||
},
|
||||
};
|
||||
|
||||
/// RaydiumLaunchpad协议的指令构建器
|
||||
pub struct RaydiumLaunchpadInstructionBuilder;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InstructionBuilder for RaydiumLaunchpadInstructionBuilder {
|
||||
async fn build_buy_instructions(&self, params: &BuyParams) -> Result<Vec<Instruction>> {
|
||||
if params.amount_sol == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
}
|
||||
self.build_buy_instructions_with_accounts(params).await
|
||||
}
|
||||
|
||||
async fn build_sell_instructions(&self, params: &SellParams) -> Result<Vec<Instruction>> {
|
||||
self.build_sell_instructions_with_accounts(params).await
|
||||
}
|
||||
}
|
||||
|
||||
impl RaydiumLaunchpadInstructionBuilder {
|
||||
/// 使用提供的账户信息构建买入指令
|
||||
async fn build_buy_instructions_with_accounts(
|
||||
&self,
|
||||
params: &BuyParams,
|
||||
) -> Result<Vec<Instruction>> {
|
||||
let protocol_params = params
|
||||
.protocol_params
|
||||
.as_any()
|
||||
.downcast_ref::<RaydiumLaunchpadParams>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumLaunchpad"))?;
|
||||
|
||||
let pool_state = get_pool_pda(¶ms.mint, &accounts::WSOL_TOKEN_ACCOUNT).unwrap();
|
||||
|
||||
// 创建用户代币账户
|
||||
let user_base_token_account = spl_associated_token_account::get_associated_token_address(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.mint,
|
||||
);
|
||||
let user_quote_token_account = spl_associated_token_account::get_associated_token_address(
|
||||
¶ms.payer.pubkey(),
|
||||
&accounts::WSOL_TOKEN_ACCOUNT,
|
||||
);
|
||||
|
||||
// 获取池的代币账户
|
||||
let base_vault_account = get_vault_pda(&pool_state, ¶ms.mint).unwrap();
|
||||
let quote_vault_account =
|
||||
get_vault_pda(&pool_state, &accounts::WSOL_TOKEN_ACCOUNT).unwrap();
|
||||
|
||||
let mut virtual_base = protocol_params.virtual_base.unwrap_or(0);
|
||||
let mut virtual_quote = protocol_params.virtual_quote.unwrap_or(0);
|
||||
let mut real_base_before = protocol_params.real_base_before.unwrap_or(0);
|
||||
let mut real_quote_before = protocol_params.real_quote_before.unwrap_or(0);
|
||||
|
||||
if virtual_base == 0
|
||||
|| virtual_quote == 0
|
||||
|| real_base_before == 0
|
||||
|| real_quote_before == 0
|
||||
{
|
||||
let pool = Pool::fetch(params.rpc.as_ref().unwrap(), &pool_state).await?;
|
||||
virtual_base = pool.virtual_base as u128;
|
||||
virtual_quote = pool.virtual_quote as u128;
|
||||
real_base_before = pool.real_base as u128;
|
||||
real_quote_before = pool.real_quote as u128;
|
||||
}
|
||||
|
||||
let amount_in: u64 = params.amount_sol;
|
||||
let share_fee_rate: u64 = 0;
|
||||
let minimum_amount_out: u64 = get_amount_out(
|
||||
amount_in,
|
||||
accounts::PROTOCOL_FEE_RATE,
|
||||
accounts::PLATFORM_FEE_RATE,
|
||||
accounts::SHARE_FEE_RATE,
|
||||
virtual_base,
|
||||
virtual_quote,
|
||||
real_base_before,
|
||||
real_quote_before,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE) as u128,
|
||||
);
|
||||
|
||||
let mut instructions = vec![];
|
||||
|
||||
if protocol_params.auto_handle_wsol {
|
||||
// 插入wsol
|
||||
instructions.push(
|
||||
// 创建wSOL ATA账户,如果不存在
|
||||
create_associated_token_account_idempotent(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
&accounts::WSOL_TOKEN_ACCOUNT,
|
||||
&accounts::TOKEN_PROGRAM,
|
||||
),
|
||||
);
|
||||
instructions.push(
|
||||
// 将SOL转入wSOL ATA账户
|
||||
solana_sdk::system_instruction::transfer(
|
||||
¶ms.payer.pubkey(),
|
||||
&user_quote_token_account,
|
||||
amount_in,
|
||||
),
|
||||
);
|
||||
|
||||
// 同步wSOL余额
|
||||
instructions.push(
|
||||
spl_token::instruction::sync_native(
|
||||
&accounts::TOKEN_PROGRAM,
|
||||
&user_quote_token_account,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// 创建用户的基础代币账户
|
||||
instructions.push(create_associated_token_account_idempotent(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.mint,
|
||||
&accounts::TOKEN_PROGRAM,
|
||||
));
|
||||
|
||||
// 创建买入指令
|
||||
let accounts = vec![
|
||||
solana_sdk::instruction::AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::AUTHORITY, false), // Authority (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::GLOBAL_CONFIG, false), // Global Config (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::PLATFORM_CONFIG, false), // Platform Config (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new(pool_state, false), // Pool State
|
||||
solana_sdk::instruction::AccountMeta::new(user_base_token_account, false), // User Base Token
|
||||
solana_sdk::instruction::AccountMeta::new(user_quote_token_account, false), // User Quote Token
|
||||
solana_sdk::instruction::AccountMeta::new(base_vault_account, false), // Base Vault
|
||||
solana_sdk::instruction::AccountMeta::new(quote_vault_account, false), // Quote Vault
|
||||
solana_sdk::instruction::AccountMeta::new(params.mint, false), // Base Token Mint (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // Quote Token Mint (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Base Token Program (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Quote Token Program (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::EVENT_AUTHORITY, false), // Event Authority (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::LAUNCHPAD_PROGRAM, false), // Program (readonly)
|
||||
];
|
||||
// 创建指令数据
|
||||
let mut data = vec![];
|
||||
data.extend_from_slice(&BUY_EXECT_IN_DISCRIMINATOR);
|
||||
data.extend_from_slice(&amount_in.to_le_bytes());
|
||||
data.extend_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
data.extend_from_slice(&share_fee_rate.to_le_bytes());
|
||||
|
||||
instructions.push(Instruction {
|
||||
program_id: accounts::LAUNCHPAD_PROGRAM,
|
||||
accounts,
|
||||
data,
|
||||
});
|
||||
|
||||
if protocol_params.auto_handle_wsol {
|
||||
// 关闭wSOL ATA账户,回收租金
|
||||
instructions.push(
|
||||
spl_token::instruction::close_account(
|
||||
&accounts::TOKEN_PROGRAM,
|
||||
&user_quote_token_account,
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
&[],
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
/// 使用提供的账户信息构建卖出指令
|
||||
async fn build_sell_instructions_with_accounts(
|
||||
&self,
|
||||
params: &SellParams,
|
||||
) -> Result<Vec<Instruction>> {
|
||||
if params.rpc.is_none() {
|
||||
return Err(anyhow!("RPC is not set"));
|
||||
}
|
||||
let rpc = params.rpc.as_ref().unwrap().clone();
|
||||
|
||||
// 获取代币余额
|
||||
let mut amount = params.amount_token;
|
||||
if params.amount_token.is_none() || params.amount_token.unwrap_or(0) == 0 {
|
||||
let balance_u64 =
|
||||
get_token_balance(rpc.as_ref(), ¶ms.payer.pubkey(), ¶ms.mint).await?;
|
||||
amount = Some(balance_u64);
|
||||
}
|
||||
let amount = amount.unwrap_or(0);
|
||||
|
||||
if amount == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
}
|
||||
|
||||
// 计算预期的SOL数量
|
||||
let minimum_amount_out: u64 = 1;
|
||||
|
||||
let pool_state = get_pool_pda(¶ms.mint, &accounts::WSOL_TOKEN_ACCOUNT).unwrap();
|
||||
|
||||
// 创建用户代币账户
|
||||
let user_base_token_account = spl_associated_token_account::get_associated_token_address(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.mint,
|
||||
);
|
||||
let user_quote_token_account = spl_associated_token_account::get_associated_token_address(
|
||||
¶ms.payer.pubkey(),
|
||||
&accounts::WSOL_TOKEN_ACCOUNT,
|
||||
);
|
||||
|
||||
// 获取池的代币账户
|
||||
let base_vault_account = get_vault_pda(&pool_state, ¶ms.mint).unwrap();
|
||||
let quote_vault_account =
|
||||
get_vault_pda(&pool_state, &accounts::WSOL_TOKEN_ACCOUNT).unwrap();
|
||||
|
||||
let share_fee_rate: u64 = 0;
|
||||
|
||||
let mut instructions = vec![];
|
||||
|
||||
// 插入wsol
|
||||
instructions.push(
|
||||
// 创建wSOL ATA账户,如果不存在
|
||||
create_associated_token_account_idempotent(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
&accounts::WSOL_TOKEN_ACCOUNT,
|
||||
&accounts::TOKEN_PROGRAM,
|
||||
),
|
||||
);
|
||||
|
||||
// 创建用户的代币账户
|
||||
instructions.push(create_associated_token_account_idempotent(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.mint,
|
||||
&accounts::TOKEN_PROGRAM,
|
||||
));
|
||||
|
||||
// 创建卖出指令
|
||||
let accounts = vec![
|
||||
solana_sdk::instruction::AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::AUTHORITY, false), // Authority (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::GLOBAL_CONFIG, false), // Global Config (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::PLATFORM_CONFIG, false), // Platform Config (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new(pool_state, false), // Pool State
|
||||
solana_sdk::instruction::AccountMeta::new(user_base_token_account, false), // User Base Token
|
||||
solana_sdk::instruction::AccountMeta::new(user_quote_token_account, false), // User Quote Token
|
||||
solana_sdk::instruction::AccountMeta::new(base_vault_account, false), // Base Vault
|
||||
solana_sdk::instruction::AccountMeta::new(quote_vault_account, false), // Quote Vault
|
||||
solana_sdk::instruction::AccountMeta::new(params.mint, false), // Base Token Mint (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // Quote Token Mint (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Base Token Program (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Quote Token Program (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::EVENT_AUTHORITY, false), // Event Authority (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::LAUNCHPAD_PROGRAM, false), // Program (readonly)
|
||||
];
|
||||
|
||||
// 创建指令数据
|
||||
let mut data = vec![];
|
||||
data.extend_from_slice(&SELL_EXECT_IN_DISCRIMINATOR);
|
||||
data.extend_from_slice(&amount.to_le_bytes());
|
||||
data.extend_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
data.extend_from_slice(&share_fee_rate.to_le_bytes());
|
||||
|
||||
instructions.push(Instruction {
|
||||
program_id: accounts::LAUNCHPAD_PROGRAM,
|
||||
accounts,
|
||||
data,
|
||||
});
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user