mirror of
https://github.com/0xfnzero/solana-streamer.git
synced 2026-08-18 11:28:05 +00:00
grpc optimization
This commit is contained in:
+10
-6
@@ -1,12 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pumpfun-sdk"
|
name = "grpc-parsed"
|
||||||
version = "2.4.3"
|
version = "2.4.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["William <byteblock6@gmail.com>"]
|
authors = []
|
||||||
repository = "https://github.com/MiracleAI-Labs/pumpfun-sdk"
|
repository = ""
|
||||||
description = "Rust SDK to interact with the Pump.fun Solana program."
|
description = "Rust SDK to interact with the Pump.fun Solana program."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
keywords = ["solana", "memecoins", "pumpfun", "pumpfun-sdk", "pumpbot"]
|
keywords = ["solana", "memecoins", "pumpfun", "grpc-parsed", "pumpbot"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
@@ -21,8 +21,10 @@ solana-rpc-client-api = "2.1.16"
|
|||||||
solana-transaction-status = "2.1.16"
|
solana-transaction-status = "2.1.16"
|
||||||
solana-account-decoder = "2.1.16"
|
solana-account-decoder = "2.1.16"
|
||||||
solana-hash = "2.1.16"
|
solana-hash = "2.1.16"
|
||||||
solana-perf = "2.1.16"
|
|
||||||
solana-security-txt = "1.1.1"
|
solana-security-txt = "1.1.1"
|
||||||
|
solana-entry = "2.1.16"
|
||||||
|
solana-rpc-client-nonce-utils = "2.1.16"
|
||||||
|
solana-perf = "2.1.16"
|
||||||
|
|
||||||
spl-token = "8.0.0"
|
spl-token = "8.0.0"
|
||||||
spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] }
|
spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] }
|
||||||
@@ -48,8 +50,8 @@ tonic = { version = "0.12.3", features = ["tls", "tls-roots", "tls-webpki-roots"
|
|||||||
rustls = { version = "0.23.23", features = ["ring"] }
|
rustls = { version = "0.23.23", features = ["ring"] }
|
||||||
rustls-native-certs = "0.8.1"
|
rustls-native-certs = "0.8.1"
|
||||||
tokio-rustls = "0.26.1"
|
tokio-rustls = "0.26.1"
|
||||||
|
core_affinity = "0.8"
|
||||||
|
|
||||||
bytes = "1.4.0"
|
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
pretty_env_logger = "0.5.0"
|
pretty_env_logger = "0.5.0"
|
||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
@@ -85,6 +87,8 @@ tokio-tungstenite = { version = "0.26.1", features = ["native-tls"] }
|
|||||||
indicatif = "0.17.11"
|
indicatif = "0.17.11"
|
||||||
toml = "0.8.20"
|
toml = "0.8.20"
|
||||||
|
|
||||||
|
pumpfun = "4.2.0"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,127 +1,108 @@
|
|||||||
# PumpFun Rust SDK
|
# GrpcParsed
|
||||||
|
|
||||||
A comprehensive Rust SDK for seamless interaction with the PumpFun Solana program. This SDK provides a robust set of tools and interfaces to integrate PumpFun functionality into your applications.
|
A gRPC client implementation for subscribing to and processing Solana transaction data.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
# Explanation
|
- Real-time subscription to Solana transaction data via gRPC
|
||||||
1. Add `create, buy, sell` for pump.fun.
|
- Support for processing transaction entries and transactions
|
||||||
2. Add `logs_subscribe` to subscribe the logs of the PumpFun program.
|
- Asynchronous transaction data processing
|
||||||
3. Add `yellowstone grpc` to subscribe the logs of the PumpFun program.
|
- Custom callback function support for transaction events
|
||||||
4. Add `jito` to send transaction with Jito.
|
- Built-in error handling mechanism
|
||||||
5. Add `nextblock` to send transaction with nextblock.
|
|
||||||
6. Add `0slot` to send transaction with 0slot.
|
|
||||||
7. Submit a transaction using Jito, Nextblock, and 0slot simultaneously; the fastest one will succeed, while the others will fail.
|
|
||||||
|
|
||||||
## Usage
|
## Installation
|
||||||
```shell
|
|
||||||
cd `your project root directory`
|
Add the following to your `Cargo.toml`:
|
||||||
git clone https://github.com/0xfnzero/pumpfun-sdk
|
|
||||||
```
|
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# add to your Cargo.toml
|
[dependencies]
|
||||||
pumpfun-sdk = { path = "./pumpfun-sdk", version = "2.4.3" }
|
grpc-parsed = { path = ".", version = "0.1.0" }
|
||||||
```
|
```
|
||||||
|
|
||||||
### logs subscription for token create and trade transaction
|
## Usage Examples
|
||||||
|
|
||||||
|
### 1. Initializing the Client
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use pumpfun_sdk::{common::logs_events::PumpfunEvent, grpc::YellowstoneGrpc};
|
use grpc_parsed::grpc::YellowstoneGrpc;
|
||||||
|
|
||||||
// create grpc client
|
async fn setup_client() -> Result<YellowstoneGrpc, Box<dyn std::error::Error>> {
|
||||||
let grpc_url = "http://127.0.0.1:10000";
|
let endpoint = "https://solana-yellowstone-grpc.publicnode.com:443";
|
||||||
let client = YellowstoneGrpc::new(grpc_url);
|
let client = YellowstoneGrpc::new(endpoint.to_string(), None)?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
// Define callback function
|
### 2. Subscribing to Transaction Data
|
||||||
let callback = |event: PumpfunEvent| {
|
|
||||||
match event {
|
```rust
|
||||||
PumpfunEvent::NewToken(token_info) => {
|
use grpc_parsed::common::logs_events::PumpfunEvent;
|
||||||
println!("Received new token event: {:?}", token_info);
|
use solana_sdk::pubkey::Pubkey;
|
||||||
},
|
|
||||||
PumpfunEvent::NewDevTrade(trade_info) => {
|
async fn subscribe_to_transactions() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("Received dev trade event: {:?}", trade_info);
|
let client = YellowstoneGrpc::new(
|
||||||
},
|
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
||||||
PumpfunEvent::NewUserTrade(trade_info) => {
|
None,
|
||||||
println!("Received new trade event: {:?}", trade_info);
|
)?;
|
||||||
},
|
|
||||||
PumpfunEvent::NewBotTrade(trade_info) => {
|
let callback = |event: PumpfunEvent| {
|
||||||
println!("Received new bot trade event: {:?}", trade_info);
|
match event {
|
||||||
|
PumpfunEvent::NewToken(token_info) => {
|
||||||
|
println!("Received new token event: {:?}", token_info);
|
||||||
|
},
|
||||||
|
PumpfunEvent::NewDevTrade(trade_info) => {
|
||||||
|
println!("Received new dev trade event: {:?}", trade_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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PumpfunEvent::Error(err) => {
|
};
|
||||||
println!("Received error: {}", err);
|
|
||||||
|
// Optional: Specify bot wallet address for filtering
|
||||||
|
let bot_wallet = None;
|
||||||
|
client.subscribe_pumpfun(callback, bot_wallet).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use grpc_parsed::grpc::YellowstoneGrpc;
|
||||||
|
|
||||||
|
async fn handle_errors() {
|
||||||
|
match YellowstoneGrpc::new(
|
||||||
|
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(client) => {
|
||||||
|
println!("Client initialized successfully");
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to initialize client: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let payer_keypair = Keypair::from_base58_string("your private key");
|
|
||||||
client.subscribe_pumpfun(callback, Some(payer_keypair.pubkey())).await?;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Init pumpfun instance for configs
|
## Important Notes
|
||||||
```rust
|
|
||||||
use std::sync::Arc;
|
|
||||||
use pumpfun_sdk::{common::{Cluster, PriorityFee}, PumpFun};
|
|
||||||
use solana_sdk::{commitment_config::CommitmentConfig, signature::Keypair, signer::Signer};
|
|
||||||
|
|
||||||
let priority_fee = PriorityFee{
|
- Ensure the gRPC server address is correct and accessible
|
||||||
unit_limit: 190000,
|
- Callback functions should be thread-safe (Send + Sync)
|
||||||
unit_price: 1000000,
|
- Implement appropriate error retry mechanisms in production environments
|
||||||
buy_tip_fee: 0.001,
|
- Be mindful of memory usage when processing large volumes of transaction data
|
||||||
sell_tip_fee: 0.0001,
|
|
||||||
};
|
|
||||||
|
|
||||||
let cluster = Cluster {
|
## License
|
||||||
rpc_url: "https://api.mainnet-beta.solana.com".to_string(),
|
|
||||||
block_engine_url: "https://block-engine.example.com".to_string(),
|
|
||||||
nextblock_url: "https://nextblock.example.com".to_string(),
|
|
||||||
nextblock_auth_token: "nextblock_api_token".to_string(),
|
|
||||||
zeroslot_url: "https://zeroslot.example.com".to_string(),
|
|
||||||
zeroslot_auth_token: "zeroslot_api_token".to_string(),
|
|
||||||
use_jito: true,
|
|
||||||
use_nextblock: false,
|
|
||||||
use_zeroslot: false,
|
|
||||||
priority_fee,
|
|
||||||
commitment: CommitmentConfig::processed(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// create pumpfun instance
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
let payer = Keypair::from_base58_string("your private key");
|
|
||||||
let pumpfun = PumpFun::new(
|
|
||||||
Arc::new(payer),
|
|
||||||
&cluster,
|
|
||||||
).await;
|
|
||||||
```
|
|
||||||
|
|
||||||
### pumpfun buy token
|
|
||||||
```rust
|
|
||||||
use pumpfun_sdk::PumpFun;
|
|
||||||
use solana_sdk::{native_token::sol_to_lamports, signature::Keypair, signer::Signer};
|
|
||||||
|
|
||||||
// create pumpfun instance
|
|
||||||
let pumpfun = PumpFun::new(Arc::new(payer), &cluster).await;
|
|
||||||
|
|
||||||
// Mint keypair
|
|
||||||
let mint_pubkey: Keypair = Keypair::new();
|
|
||||||
|
|
||||||
// buy token with tip
|
|
||||||
pumpfun.buy_with_tip(mint_pubkey, 10000, None).await?;
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### pumpfun sell token
|
|
||||||
```rust
|
|
||||||
use pumpfun_sdk::PumpFun;
|
|
||||||
use solana_sdk::{native_token::sol_to_lamports, signature::Keypair, signer::Signer};
|
|
||||||
|
|
||||||
// create pumpfun instance
|
|
||||||
let pumpfun = PumpFun::new(Arc::new(payer), &cluster).await;
|
|
||||||
|
|
||||||
// sell token with tip
|
|
||||||
pumpfun.sell_with_tip(mint_pubkey, 100000, None).await?;
|
|
||||||
|
|
||||||
// sell token by percent with tip
|
|
||||||
pumpfun.sell_by_percent_with_tip(mint_pubkey, 100, None).await?;
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Telegram group:
|
### Telegram group:
|
||||||
https://t.me/fnzero_group
|
https://t.me/fnzero_group
|
||||||
|
|||||||
@@ -1,201 +0,0 @@
|
|||||||
//! Bonding curve account for the Pump.fun Solana Program
|
|
||||||
//!
|
|
||||||
//! This module contains the definition for the bonding curve account.
|
|
||||||
//!
|
|
||||||
//! # Bonding Curve Account
|
|
||||||
//!
|
|
||||||
//! The bonding curve account is used to manage token pricing and liquidity.
|
|
||||||
//!
|
|
||||||
//! # Fields
|
|
||||||
//!
|
|
||||||
//! - `discriminator`: Unique identifier for the bonding curve
|
|
||||||
//! - `virtual_token_reserves`: Virtual token reserves used for price calculations
|
|
||||||
//! - `virtual_sol_reserves`: Virtual SOL reserves used for price calculations
|
|
||||||
//! - `real_token_reserves`: Actual token reserves available for trading
|
|
||||||
//! - `real_sol_reserves`: Actual SOL reserves available for trading
|
|
||||||
//! - `token_total_supply`: Total supply of tokens
|
|
||||||
//! - `complete`: Whether the bonding curve is complete/finalized
|
|
||||||
//!
|
|
||||||
//! # Methods
|
|
||||||
//!
|
|
||||||
//! - `new`: Creates a new bonding curve instance
|
|
||||||
//! - `get_buy_price`: Calculates the amount of tokens received for a given SOL amount
|
|
||||||
//! - `get_sell_price`: Calculates the amount of SOL received for selling tokens
|
|
||||||
//! - `get_market_cap_sol`: Calculates the current market cap in SOL
|
|
||||||
//! - `get_final_market_cap_sol`: Calculates the final market cap in SOL after all tokens are sold
|
|
||||||
//! - `get_buy_out_price`: Calculates the price to buy out all remaining tokens
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use solana_sdk::pubkey::Pubkey;
|
|
||||||
|
|
||||||
/// Represents the global configuration account for token pricing and fees
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct BondingCurveAccount {
|
|
||||||
/// Unique identifier for the bonding curve
|
|
||||||
pub discriminator: u64,
|
|
||||||
/// Virtual token reserves used for price calculations
|
|
||||||
pub virtual_token_reserves: u64,
|
|
||||||
/// Virtual SOL reserves used for price calculations
|
|
||||||
pub virtual_sol_reserves: u64,
|
|
||||||
/// Actual token reserves available for trading
|
|
||||||
pub real_token_reserves: u64,
|
|
||||||
/// Actual SOL reserves available for trading
|
|
||||||
pub real_sol_reserves: u64,
|
|
||||||
/// Total supply of tokens
|
|
||||||
pub token_total_supply: u64,
|
|
||||||
/// Whether the bonding curve is complete/finalized
|
|
||||||
pub complete: bool,
|
|
||||||
/// Creator of the bonding curve
|
|
||||||
pub creator: Pubkey,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BondingCurveAccount {
|
|
||||||
/// Creates a new bonding curve instance
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `discriminator` - Unique identifier for the curve
|
|
||||||
/// * `virtual_token_reserves` - Virtual token reserves for price calculations
|
|
||||||
/// * `virtual_sol_reserves` - Virtual SOL reserves for price calculations
|
|
||||||
/// * `real_token_reserves` - Actual token reserves available
|
|
||||||
/// * `real_sol_reserves` - Actual SOL reserves available
|
|
||||||
/// * `token_total_supply` - Total supply of tokens
|
|
||||||
/// * `complete` - Whether the curve is complete
|
|
||||||
// pub fn new(mint: &Pubkey, dev_buy_token_amount: u64, dev_buy_sol_amount: u64) -> Self {
|
|
||||||
// Self {
|
|
||||||
// // account: get_bonding_curve_pda(mint).unwrap(),
|
|
||||||
// virtual_token_reserves: INITIAL_VIRTUAL_TOKEN_RESERVES - dev_buy_token_amount,
|
|
||||||
// virtual_sol_reserves: INITIAL_VIRTUAL_SOL_RESERVES + dev_buy_sol_amount,
|
|
||||||
// real_token_reserves: INITIAL_REAL_TOKEN_RESERVES - dev_buy_token_amount,
|
|
||||||
// real_sol_reserves: dev_buy_sol_amount,
|
|
||||||
// token_total_supply: TOKEN_TOTAL_SUPPLY,
|
|
||||||
// complete: false,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/// Calculates the amount of tokens received for a given SOL amount
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `amount` - Amount of SOL to spend
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// * `Ok(u64)` - Amount of tokens that would be received
|
|
||||||
/// * `Err(&str)` - Error message if curve is complete
|
|
||||||
pub fn get_buy_price(&self, amount: u64) -> Result<u64, &'static str> {
|
|
||||||
if self.complete {
|
|
||||||
return Err("Curve is complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
if amount == 0 {
|
|
||||||
return Ok(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the product of virtual reserves using u128 to avoid overflow
|
|
||||||
let n: u128 = (self.virtual_sol_reserves as u128) * (self.virtual_token_reserves as u128);
|
|
||||||
|
|
||||||
// Calculate the new virtual sol reserves after the purchase
|
|
||||||
let i: u128 = (self.virtual_sol_reserves as u128) + (amount as u128);
|
|
||||||
|
|
||||||
// Calculate the new virtual token reserves after the purchase
|
|
||||||
let r: u128 = n / i + 1;
|
|
||||||
|
|
||||||
// Calculate the amount of tokens to be purchased
|
|
||||||
let s: u128 = (self.virtual_token_reserves as u128) - r;
|
|
||||||
|
|
||||||
// Convert back to u64 and return the minimum of calculated tokens and real reserves
|
|
||||||
let s_u64 = s as u64;
|
|
||||||
Ok(if s_u64 < self.real_token_reserves {
|
|
||||||
s_u64
|
|
||||||
} else {
|
|
||||||
self.real_token_reserves
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the amount of SOL received for selling tokens
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `amount` - Amount of tokens to sell
|
|
||||||
/// * `fee_basis_points` - Fee in basis points (1/100th of a percent)
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// * `Ok(u64)` - Amount of SOL that would be received after fees
|
|
||||||
/// * `Err(&str)` - Error message if curve is complete
|
|
||||||
pub fn get_sell_price(&self, amount: u64, fee_basis_points: u64) -> Result<u64, &'static str> {
|
|
||||||
if self.complete {
|
|
||||||
return Err("Curve is complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
if amount == 0 {
|
|
||||||
return Ok(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the proportional amount of virtual sol reserves to be received using u128
|
|
||||||
let n: u128 = ((amount as u128) * (self.virtual_sol_reserves as u128))
|
|
||||||
/ ((self.virtual_token_reserves as u128) + (amount as u128));
|
|
||||||
|
|
||||||
// Calculate the fee amount in the same units
|
|
||||||
let a: u128 = (n * (fee_basis_points as u128)) / 10000;
|
|
||||||
|
|
||||||
// Return the net amount after deducting the fee, converting back to u64
|
|
||||||
Ok((n - a) as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the current market cap in SOL
|
|
||||||
pub fn get_market_cap_sol(&self) -> u64 {
|
|
||||||
if self.virtual_token_reserves == 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
((self.token_total_supply as u128) * (self.virtual_sol_reserves as u128)
|
|
||||||
/ (self.virtual_token_reserves as u128)) as u64
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the final market cap in SOL after all tokens are sold
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `fee_basis_points` - Fee in basis points (1/100th of a percent)
|
|
||||||
pub fn get_final_market_cap_sol(&self, fee_basis_points: u64) -> u64 {
|
|
||||||
let total_sell_value: u128 =
|
|
||||||
self.get_buy_out_price(self.real_token_reserves, fee_basis_points) as u128;
|
|
||||||
let total_virtual_value: u128 = (self.virtual_sol_reserves as u128) + total_sell_value;
|
|
||||||
let total_virtual_tokens: u128 =
|
|
||||||
(self.virtual_token_reserves as u128) - (self.real_token_reserves as u128);
|
|
||||||
|
|
||||||
if total_virtual_tokens == 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
((self.token_total_supply as u128) * total_virtual_value / total_virtual_tokens) as u64
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the price to buy out all remaining tokens
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `amount` - Amount of tokens to buy
|
|
||||||
/// * `fee_basis_points` - Fee in basis points (1/100th of a percent)
|
|
||||||
pub fn get_buy_out_price(&self, amount: u64, fee_basis_points: u64) -> u64 {
|
|
||||||
// Get the effective amount of sol tokens
|
|
||||||
let sol_tokens: u128 = if amount < self.real_sol_reserves {
|
|
||||||
self.real_sol_reserves as u128
|
|
||||||
} else {
|
|
||||||
amount as u128
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate total sell value
|
|
||||||
let total_sell_value: u128 = (sol_tokens * (self.virtual_sol_reserves as u128))
|
|
||||||
/ ((self.virtual_token_reserves as u128) - sol_tokens)
|
|
||||||
+ 1;
|
|
||||||
|
|
||||||
// Calculate fee
|
|
||||||
let fee: u128 = (total_sell_value * (fee_basis_points as u128)) / 10000;
|
|
||||||
|
|
||||||
// Return total including fee, converting back to u64
|
|
||||||
(total_sell_value + fee) as u64
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_token_price(&self) -> f64 {
|
|
||||||
let v_sol = self.virtual_sol_reserves as f64 / 100_000_000.0;
|
|
||||||
let v_tokens = self.virtual_token_reserves as f64 / 100_000.0;
|
|
||||||
let token_price = v_sol / v_tokens;
|
|
||||||
token_price
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
//! Global account for the Pump.fun Solana Program
|
|
||||||
//!
|
|
||||||
//! This module contains the definition for the global configuration account.
|
|
||||||
//!
|
|
||||||
//! # Global Account
|
|
||||||
//!
|
|
||||||
//! The global account is used to store the global configuration for the Pump.fun program.
|
|
||||||
//!
|
|
||||||
//! # Fields
|
|
||||||
//!
|
|
||||||
//! - `discriminator`: Unique identifier for the global account
|
|
||||||
//! - `initialized`: Whether the global account has been initialized
|
|
||||||
//! - `authority`: Authority pubkey that can modify settings
|
|
||||||
//! - `fee_recipient`: Account that receives fees
|
|
||||||
//! - `initial_virtual_token_reserves`: Initial virtual token reserves for price calculations
|
|
||||||
//! - `initial_virtual_sol_reserves`: Initial virtual SOL reserves for price calculations
|
|
||||||
//! - `initial_real_token_reserves`: Initial actual token reserves available for trading
|
|
||||||
//! - `token_total_supply`: Total supply of tokens
|
|
||||||
//! - `fee_basis_points`: Fee in basis points (1/100th of a percent)
|
|
||||||
//!
|
|
||||||
//! # Methods
|
|
||||||
//!
|
|
||||||
//! - `new`: Creates a new global account instance
|
|
||||||
//! - `get_initial_buy_price`: Calculates the initial amount of tokens received for a given SOL amount
|
|
||||||
|
|
||||||
use solana_sdk::pubkey::Pubkey;
|
|
||||||
use borsh::{BorshDeserialize, BorshSerialize};
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
/// Represents the global configuration account for token pricing and fees
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct GlobalAccount {
|
|
||||||
/// Unique identifier for the global account
|
|
||||||
pub discriminator: u64,
|
|
||||||
/// Whether the global account has been initialized
|
|
||||||
pub initialized: bool,
|
|
||||||
/// Authority that can modify global settings
|
|
||||||
pub authority: Pubkey,
|
|
||||||
/// Account that receives fees
|
|
||||||
pub fee_recipient: Pubkey,
|
|
||||||
/// Initial virtual token reserves for price calculations
|
|
||||||
pub initial_virtual_token_reserves: u64,
|
|
||||||
/// Initial virtual SOL reserves for price calculations
|
|
||||||
pub initial_virtual_sol_reserves: u64,
|
|
||||||
/// Initial actual token reserves available for trading
|
|
||||||
pub initial_real_token_reserves: u64,
|
|
||||||
/// Total supply of tokens
|
|
||||||
pub token_total_supply: u64,
|
|
||||||
/// Fee in basis points (1/100th of a percent)
|
|
||||||
pub fee_basis_points: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GlobalAccount {
|
|
||||||
/// Creates a new global account instance
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `discriminator` - Unique identifier for the account
|
|
||||||
/// * `initialized` - Whether the account is initialized
|
|
||||||
/// * `authority` - Authority pubkey that can modify settings
|
|
||||||
/// * `fee_recipient` - Account that receives fees
|
|
||||||
/// * `initial_virtual_token_reserves` - Initial virtual token reserves
|
|
||||||
/// * `initial_virtual_sol_reserves` - Initial virtual SOL reserves
|
|
||||||
/// * `initial_real_token_reserves` - Initial actual token reserves
|
|
||||||
/// * `token_total_supply` - Total supply of tokens
|
|
||||||
/// * `fee_basis_points` - Fee in basis points
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new(
|
|
||||||
discriminator: u64,
|
|
||||||
initialized: bool,
|
|
||||||
authority: Pubkey,
|
|
||||||
fee_recipient: Pubkey,
|
|
||||||
initial_virtual_token_reserves: u64,
|
|
||||||
initial_virtual_sol_reserves: u64,
|
|
||||||
initial_real_token_reserves: u64,
|
|
||||||
token_total_supply: u64,
|
|
||||||
fee_basis_points: u64,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
discriminator,
|
|
||||||
initialized,
|
|
||||||
authority,
|
|
||||||
fee_recipient,
|
|
||||||
initial_virtual_token_reserves,
|
|
||||||
initial_virtual_sol_reserves,
|
|
||||||
initial_real_token_reserves,
|
|
||||||
token_total_supply,
|
|
||||||
fee_basis_points,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the initial amount of tokens received for a given SOL amount
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `amount` - Amount of SOL to spend
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// Amount of tokens that would be received
|
|
||||||
pub fn get_initial_buy_price(&self, amount: u64) -> u64 {
|
|
||||||
if amount == 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let n: u128 = (self.initial_virtual_sol_reserves as u128)
|
|
||||||
* (self.initial_virtual_token_reserves as u128);
|
|
||||||
let i: u128 = (self.initial_virtual_sol_reserves as u128) + (amount as u128);
|
|
||||||
let r: u128 = n / i + 1;
|
|
||||||
let s: u128 = (self.initial_virtual_token_reserves as u128) - r;
|
|
||||||
|
|
||||||
if s < (self.initial_real_token_reserves as u128) {
|
|
||||||
s as u64
|
|
||||||
} else {
|
|
||||||
self.initial_real_token_reserves
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn get_global() -> GlobalAccount {
|
|
||||||
GlobalAccount::new(
|
|
||||||
1,
|
|
||||||
true,
|
|
||||||
Pubkey::new_unique(),
|
|
||||||
Pubkey::new_unique(),
|
|
||||||
1000,
|
|
||||||
1000,
|
|
||||||
500,
|
|
||||||
1000,
|
|
||||||
250,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_large_global() -> GlobalAccount {
|
|
||||||
GlobalAccount::new(
|
|
||||||
1,
|
|
||||||
true,
|
|
||||||
Pubkey::new_unique(),
|
|
||||||
Pubkey::new_unique(),
|
|
||||||
u64::MAX,
|
|
||||||
u64::MAX,
|
|
||||||
u64::MAX / 2,
|
|
||||||
u64::MAX,
|
|
||||||
250,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_global_account() {
|
|
||||||
let global: GlobalAccount = get_global();
|
|
||||||
|
|
||||||
// Test initial buy price calculation
|
|
||||||
assert_eq!(global.get_initial_buy_price(0), 0);
|
|
||||||
|
|
||||||
let price: u64 = global.get_initial_buy_price(100);
|
|
||||||
assert!(price > 0);
|
|
||||||
assert!(price <= global.initial_real_token_reserves);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_global_account_max_reserves() {
|
|
||||||
let mut global: GlobalAccount = get_global();
|
|
||||||
global.initial_real_token_reserves = 100;
|
|
||||||
|
|
||||||
// Test that returned amount is capped by real_token_reserves
|
|
||||||
let price: u64 = global.get_initial_buy_price(1000);
|
|
||||||
assert_eq!(price, global.initial_real_token_reserves);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_global_account_overflow() {
|
|
||||||
let global: GlobalAccount = get_large_global();
|
|
||||||
|
|
||||||
// Test with maximum possible SOL amount
|
|
||||||
let price: u64 = global.get_initial_buy_price(u64::MAX);
|
|
||||||
assert!(price > 0);
|
|
||||||
assert!(price <= global.initial_real_token_reserves);
|
|
||||||
|
|
||||||
// Test with large but not maximum SOL amount
|
|
||||||
let price: u64 = global.get_initial_buy_price(u64::MAX / 2);
|
|
||||||
assert!(price > 0);
|
|
||||||
assert!(price <= global.initial_real_token_reserves);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_global_account_overflow_edge_cases() {
|
|
||||||
let mut global: GlobalAccount = get_large_global();
|
|
||||||
global.initial_virtual_sol_reserves = u64::MAX - 1000;
|
|
||||||
global.initial_virtual_token_reserves = u64::MAX - 1000;
|
|
||||||
global.initial_real_token_reserves = u64::MAX / 4;
|
|
||||||
|
|
||||||
// Test with amounts near u64::MAX
|
|
||||||
let price: u64 = global.get_initial_buy_price(u64::MAX - 1);
|
|
||||||
assert!(price > 0);
|
|
||||||
assert!(price <= global.initial_real_token_reserves);
|
|
||||||
|
|
||||||
let price: u64 = global.get_initial_buy_price(u64::MAX - 1000);
|
|
||||||
assert!(price > 0);
|
|
||||||
assert!(price <= global.initial_real_token_reserves);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
//! Accounts for the Pump.fun Solana Program
|
|
||||||
//!
|
|
||||||
//! This module contains the definitions for the accounts used by the Pump.fun program.
|
|
||||||
//!
|
|
||||||
//! # Accounts
|
|
||||||
//!
|
|
||||||
//! - `BondingCurve`: Represents a bonding curve account.
|
|
||||||
//! - `Global`: Represents the global configuration account.
|
|
||||||
|
|
||||||
mod bonding_curve;
|
|
||||||
mod global;
|
|
||||||
|
|
||||||
pub use bonding_curve::*;
|
|
||||||
pub use global::*;
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use borsh::{BorshDeserialize, BorshSerialize};
|
use borsh::{BorshDeserialize, BorshSerialize};
|
||||||
use solana_sdk::pubkey::Pubkey;
|
use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction};
|
||||||
|
|
||||||
use crate::error::{ClientError, ClientResult};
|
use crate::error::{ClientError, ClientResult};
|
||||||
|
|
||||||
@@ -61,6 +61,13 @@ pub struct SwapBaseInLog {
|
|||||||
pub out_amount: u64,
|
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 {
|
pub trait EventTrait: Sized + std::fmt::Debug {
|
||||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self>;
|
fn from_bytes(bytes: &[u8]) -> ClientResult<Self>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use base64::engine::general_purpose;
|
use base64::engine::general_purpose;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use crate::common::logs_data::{CreateTokenInfo, TradeInfo, EventTrait};
|
use crate::common::logs_data::{CreateTokenInfo, TradeInfo, EventTrait, TransferInfo};
|
||||||
|
|
||||||
pub const PROGRAM_DATA: &str = "Program data: ";
|
pub const PROGRAM_DATA: &str = "Program data: ";
|
||||||
|
|
||||||
@@ -23,6 +23,12 @@ pub enum DexEvent {
|
|||||||
Error(String),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SystemEvent {
|
||||||
|
NewTransfer(TransferInfo),
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
// #[derive(Debug, Clone, Copy)]
|
// #[derive(Debug, Clone, Copy)]
|
||||||
// pub struct PumpEvent {}
|
// pub struct PumpEvent {}
|
||||||
|
|
||||||
@@ -67,9 +73,10 @@ impl RaydiumEvent {
|
|||||||
|
|
||||||
if !logs.is_empty() {
|
if !logs.is_empty() {
|
||||||
let logs_iter = logs.iter().peekable();
|
let logs_iter = logs.iter().peekable();
|
||||||
let re = Regex::new(r"ray_log: (?P<base64>[A-Za-z0-9+/=]+)").unwrap();
|
|
||||||
|
|
||||||
for l in logs_iter.rev() {
|
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(caps) = re.captures(l) {
|
||||||
if let Some(base64) = caps.name("base64") {
|
if let Some(base64) = caps.name("base64") {
|
||||||
let bytes = general_purpose::STANDARD.decode(base64.as_str()).unwrap();
|
let bytes = general_purpose::STANDARD.decode(base64.as_str()).unwrap();
|
||||||
|
|||||||
@@ -1,11 +1,75 @@
|
|||||||
use crate::common::logs_data::DexInstruction;
|
use crate::common::logs_data::DexInstruction;
|
||||||
use crate::common::logs_parser::{parse_create_token_data, parse_trade_data};
|
use crate::common::logs_parser::{parse_create_token_data, parse_trade_data, parse_instruction_create_token_data, parse_instruction_trade_data};
|
||||||
use crate::error::ClientResult;
|
use crate::error::ClientResult;
|
||||||
use solana_sdk::pubkey::Pubkey;
|
|
||||||
pub struct LogFilter;
|
pub struct LogFilter;
|
||||||
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use solana_sdk::transaction::VersionedTransaction;
|
||||||
|
|
||||||
impl LogFilter {
|
impl LogFilter {
|
||||||
const PROGRAM_ID: &'static str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
|
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
|
/// Parse transaction logs and return instruction type and data
|
||||||
pub fn parse_instruction(logs: &[String], bot_wallet: Option<Pubkey>) -> ClientResult<Vec<DexInstruction>> {
|
pub fn parse_instruction(logs: &[String], bot_wallet: Option<Pubkey>) -> ClientResult<Vec<DexInstruction>> {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ use crate::common::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use solana_sdk::pubkey::Pubkey;
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
use solana_sdk::instruction::CompiledInstruction;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
pub async fn process_logs<F>(
|
pub async fn process_logs<F>(
|
||||||
signature: &str,
|
signature: &str,
|
||||||
@@ -171,4 +173,64 @@ pub fn parse_trade_data(data: &str) -> ClientResult<TradeInfo> {
|
|||||||
real_sol_reserves,
|
real_sol_reserves,
|
||||||
real_token_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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -8,9 +8,9 @@ use std::sync::Arc;
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use crate::{constants, common::{
|
use crate::common::{
|
||||||
logs_data::DexInstruction, logs_events::DexEvent, logs_filters::LogFilter
|
logs_data::DexInstruction, logs_filters::LogFilter
|
||||||
}};
|
};
|
||||||
|
|
||||||
use super::logs_events::PumpfunEvent;
|
use super::logs_events::PumpfunEvent;
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ pub async fn tokens_subscription<F>(
|
|||||||
where
|
where
|
||||||
F: Fn(PumpfunEvent) + Send + Sync + 'static,
|
F: Fn(PumpfunEvent) + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
let program_address = constants::accounts::PUMPFUN.to_string();
|
let program_address = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P".to_string();
|
||||||
let logs_filter = RpcTransactionLogsFilter::Mentions(vec![program_address]);
|
let logs_filter = RpcTransactionLogsFilter::Mentions(vec![program_address]);
|
||||||
|
|
||||||
let logs_config = RpcTransactionLogsConfig {
|
let logs_config = RpcTransactionLogsConfig {
|
||||||
|
|||||||
@@ -3,6 +3,4 @@ pub mod logs_parser;
|
|||||||
pub mod logs_filters;
|
pub mod logs_filters;
|
||||||
pub mod logs_subscribe;
|
pub mod logs_subscribe;
|
||||||
pub mod logs_events;
|
pub mod logs_events;
|
||||||
pub mod types;
|
|
||||||
|
|
||||||
pub use types::*;
|
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use solana_client::rpc_client::RpcClient;
|
|
||||||
use solana_sdk::{commitment_config::CommitmentConfig, signature::Keypair};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use crate::{constants::trade::{DEFAULT_BUY_TIP_FEE, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SELL_TIP_FEE}, swqos::FeeClient};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum FeeType {
|
|
||||||
Jito,
|
|
||||||
NextBlock,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Cluster {
|
|
||||||
pub rpc_url: String,
|
|
||||||
pub block_engine_url: String,
|
|
||||||
pub nextblock_url: String,
|
|
||||||
pub nextblock_auth_token: String,
|
|
||||||
pub zeroslot_url: String,
|
|
||||||
pub zeroslot_auth_token: String,
|
|
||||||
pub use_jito: bool,
|
|
||||||
pub use_nextblock: bool,
|
|
||||||
pub use_zeroslot: bool,
|
|
||||||
pub priority_fee: PriorityFee,
|
|
||||||
pub commitment: CommitmentConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cluster {
|
|
||||||
pub fn new(
|
|
||||||
rpc_url: String,
|
|
||||||
block_engine_url:
|
|
||||||
String, nextblock_url:
|
|
||||||
String, nextblock_auth_token:
|
|
||||||
String, zeroslot_url: String,
|
|
||||||
zeroslot_auth_token: String,
|
|
||||||
priority_fee: PriorityFee,
|
|
||||||
commitment: CommitmentConfig,
|
|
||||||
use_jito: bool,
|
|
||||||
use_nextblock: bool,
|
|
||||||
use_zeroslot: bool
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
rpc_url,
|
|
||||||
block_engine_url,
|
|
||||||
nextblock_url,
|
|
||||||
nextblock_auth_token,
|
|
||||||
zeroslot_url,
|
|
||||||
zeroslot_auth_token,
|
|
||||||
priority_fee,
|
|
||||||
commitment,
|
|
||||||
use_jito,
|
|
||||||
use_nextblock,
|
|
||||||
use_zeroslot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone, Copy, PartialEq)]
|
|
||||||
|
|
||||||
pub struct PriorityFee {
|
|
||||||
pub unit_limit: u32,
|
|
||||||
pub unit_price: u64,
|
|
||||||
pub buy_tip_fee: f64,
|
|
||||||
pub sell_tip_fee: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PriorityFee {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
unit_limit: DEFAULT_COMPUTE_UNIT_LIMIT,
|
|
||||||
unit_price: DEFAULT_COMPUTE_UNIT_PRICE,
|
|
||||||
buy_tip_fee: DEFAULT_BUY_TIP_FEE,
|
|
||||||
sell_tip_fee: DEFAULT_SELL_TIP_FEE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient;
|
|
||||||
|
|
||||||
pub struct MethodArgs {
|
|
||||||
pub payer: Arc<Keypair>,
|
|
||||||
pub rpc: Arc<RpcClient>,
|
|
||||||
pub nonblocking_rpc: Arc<SolanaRpcClient>,
|
|
||||||
pub jito_client: Arc<FeeClient>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MethodArgs {
|
|
||||||
pub fn new(payer: Arc<Keypair>, rpc: Arc<RpcClient>, nonblocking_rpc: Arc<SolanaRpcClient>, jito_client: Arc<FeeClient>) -> Self {
|
|
||||||
Self { payer, rpc, nonblocking_rpc, jito_client }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type AnyResult<T> = anyhow::Result<T>;
|
|
||||||
|
|
||||||
@@ -1,174 +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";
|
|
||||||
|
|
||||||
/// Seed for the mint authority PDA
|
|
||||||
pub const MINT_AUTHORITY_SEED: &[u8] = b"mint-authority";
|
|
||||||
|
|
||||||
/// Seed for bonding curve PDAs
|
|
||||||
pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve";
|
|
||||||
|
|
||||||
/// Seed for creator vault PDAs
|
|
||||||
pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault";
|
|
||||||
|
|
||||||
/// Seed for metadata PDAs
|
|
||||||
pub const METADATA_SEED: &[u8] = b"metadata";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod global_constants {
|
|
||||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
|
||||||
|
|
||||||
pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000;
|
|
||||||
|
|
||||||
pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000;
|
|
||||||
|
|
||||||
pub const INITIAL_REAL_TOKEN_RESERVES: u64 = 793_100_000_000_000;
|
|
||||||
|
|
||||||
pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000;
|
|
||||||
|
|
||||||
pub const FEE_BASIS_POINTS: u64 = 95;
|
|
||||||
|
|
||||||
pub const ENABLE_MIGRATE: bool = false;
|
|
||||||
|
|
||||||
pub const POOL_MIGRATION_FEE: u64 = 15_000_001;
|
|
||||||
|
|
||||||
pub const CREATOR_FEE: u64 = 5;
|
|
||||||
|
|
||||||
pub const SCALE: u64 = 1_000_000; // 10^6 for token decimals
|
|
||||||
|
|
||||||
pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; // 10^9 for solana lamports
|
|
||||||
|
|
||||||
pub const TOTAL_SUPPLY: u64 = 1_000_000_000 * SCALE; // 1 billion tokens
|
|
||||||
|
|
||||||
pub const BONDING_CURVE_SUPPLY: u64 = 793_100_000 * SCALE; // total supply of bonding curve tokens
|
|
||||||
|
|
||||||
pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL; // ~ 85 SOL
|
|
||||||
|
|
||||||
/// Public key for the fee recipient
|
|
||||||
pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV");
|
|
||||||
|
|
||||||
/// Public key for the global PDA
|
|
||||||
pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf");
|
|
||||||
|
|
||||||
/// Public key for the authority
|
|
||||||
pub const AUTHORITY: Pubkey = pubkey!("FFWtrEQ4B4PKQoVuHYzZq8FabGkVatYzDpEVHsK5rrhF");
|
|
||||||
|
|
||||||
/// Public key for the withdraw authority
|
|
||||||
pub const WITHDRAW_AUTHORITY: Pubkey = pubkey!("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg");
|
|
||||||
|
|
||||||
pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"); // Pump.fun AMM: Protocol Fee 1
|
|
||||||
pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"); // Pump.fun AMM: Protocol Fee 2
|
|
||||||
pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"); // Pump.fun AMM: Protocol Fee 3
|
|
||||||
pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"); // Pump.fun AMM: Protocol Fee 4
|
|
||||||
pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"); // Pump.fun AMM: Protocol Fee 5
|
|
||||||
pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"); // Pump.fun AMM: Protocol Fee 6
|
|
||||||
pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP"); // Pump.fun AMM: Protocol Fee 7
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constants related to program accounts and authorities
|
|
||||||
pub mod accounts {
|
|
||||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
|
||||||
|
|
||||||
/// Public key for the Pump.fun program
|
|
||||||
pub const PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
|
||||||
|
|
||||||
/// Public key for the MPL Token Metadata program
|
|
||||||
pub const MPL_TOKEN_METADATA: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");
|
|
||||||
|
|
||||||
/// Authority for program events
|
|
||||||
pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1");
|
|
||||||
|
|
||||||
/// System Program ID
|
|
||||||
pub const SYSTEM_PROGRAM: Pubkey = pubkey!("11111111111111111111111111111111");
|
|
||||||
|
|
||||||
/// Token Program ID
|
|
||||||
pub const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
|
|
||||||
|
|
||||||
/// Associated Token Program ID
|
|
||||||
pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
|
||||||
|
|
||||||
/// Rent Sysvar ID
|
|
||||||
pub const RENT: Pubkey = pubkey!("SysvarRent111111111111111111111111111111111");
|
|
||||||
|
|
||||||
pub const JITO_TIP_ACCOUNTS: [&str; 8] = [
|
|
||||||
"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!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod trade {
|
|
||||||
pub const TRADER_TIP_AMOUNT: f64 = 0.0001;
|
|
||||||
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: f64 = 0.0006;
|
|
||||||
pub const DEFAULT_SELL_TIP_FEE: f64 = 0.0001;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Symbol;
|
|
||||||
|
|
||||||
impl Symbol {
|
|
||||||
pub const SOLANA: &'static str = "solana";
|
|
||||||
}
|
|
||||||
+2
-250
@@ -1,251 +1,3 @@
|
|||||||
use std::{collections::HashMap, fmt, time::Duration};
|
pub mod yellow_stone;
|
||||||
|
|
||||||
use futures::{channel::mpsc, sink::Sink, Stream, StreamExt, SinkExt};
|
pub use yellow_stone::YellowstoneGrpc;
|
||||||
use rustls::crypto::{ring::default_provider, CryptoProvider};
|
|
||||||
use tonic::{transport::channel::ClientTlsConfig, Status};
|
|
||||||
use yellowstone_grpc_client::{GeyserGrpcClient, GeyserGrpcClientResult};
|
|
||||||
use yellowstone_grpc_proto::geyser::{
|
|
||||||
CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions, SubscribeUpdate,
|
|
||||||
SubscribeUpdateTransaction, subscribe_update::UpdateOneof, SubscribeRequestPing,
|
|
||||||
};
|
|
||||||
use log::{error, info};
|
|
||||||
use chrono::Local;
|
|
||||||
use solana_sdk::{pubkey, pubkey::Pubkey, signature::Signature};
|
|
||||||
use solana_transaction_status::{
|
|
||||||
option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiTransactionEncoding,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::common::logs_data::DexInstruction;
|
|
||||||
use crate::common::logs_events::PumpfunEvent;
|
|
||||||
use crate::common::logs_filters::LogFilter;
|
|
||||||
use crate::error::{ClientError, ClientResult};
|
|
||||||
|
|
||||||
type TransactionsFilterMap = HashMap<String, SubscribeRequestFilterTransactions>;
|
|
||||||
|
|
||||||
const PUMP_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
|
||||||
const CONNECT_TIMEOUT: u64 = 10;
|
|
||||||
const REQUEST_TIMEOUT: u64 = 60;
|
|
||||||
const CHANNEL_SIZE: usize = 1000;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct TransactionPretty {
|
|
||||||
pub slot: u64,
|
|
||||||
pub signature: Signature,
|
|
||||||
pub is_vote: bool,
|
|
||||||
pub tx: EncodedTransactionWithStatusMeta,
|
|
||||||
// pub transaction: Option<Transaction>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for TransactionPretty {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
struct TxWrap<'a>(&'a EncodedTransactionWithStatusMeta);
|
|
||||||
impl<'a> fmt::Debug for TxWrap<'a> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let serialized = serde_json::to_string(self.0).expect("failed to serialize");
|
|
||||||
fmt::Display::fmt(&serialized, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f.debug_struct("TransactionPretty")
|
|
||||||
.field("slot", &self.slot)
|
|
||||||
.field("signature", &self.signature)
|
|
||||||
.field("is_vote", &self.is_vote)
|
|
||||||
.field("tx", &TxWrap(&self.tx))
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SubscribeUpdateTransaction> for TransactionPretty {
|
|
||||||
fn from(SubscribeUpdateTransaction { transaction, slot }: SubscribeUpdateTransaction) -> Self {
|
|
||||||
let tx = transaction.expect("should be defined");
|
|
||||||
// let transaction_info = tx.transaction.clone().unwrap();
|
|
||||||
Self {
|
|
||||||
slot,
|
|
||||||
signature: Signature::try_from(tx.signature.as_slice()).expect("valid signature"),
|
|
||||||
is_vote: tx.is_vote,
|
|
||||||
tx: yellowstone_grpc_proto::convert_from::create_tx_with_meta(tx)
|
|
||||||
.expect("valid tx with meta")
|
|
||||||
.encode(UiTransactionEncoding::Base64, Some(u8::MAX), true)
|
|
||||||
.expect("failed to encode"),
|
|
||||||
// transaction: Some(transaction_info),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct YellowstoneGrpc {
|
|
||||||
endpoint: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl YellowstoneGrpc {
|
|
||||||
pub fn new(endpoint: String) -> Self {
|
|
||||||
Self { endpoint }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn connect(
|
|
||||||
&self,
|
|
||||||
transactions: TransactionsFilterMap,
|
|
||||||
) -> ClientResult<
|
|
||||||
GeyserGrpcClientResult<(
|
|
||||||
impl Sink<SubscribeRequest, Error = mpsc::SendError>,
|
|
||||||
impl Stream<Item = Result<SubscribeUpdate, Status>>,
|
|
||||||
)>
|
|
||||||
> {
|
|
||||||
if CryptoProvider::get_default().is_none() {
|
|
||||||
default_provider()
|
|
||||||
.install_default()
|
|
||||||
.map_err(|e| ClientError::Other(format!("Failed to install crypto provider: {:?}", e)))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut client = GeyserGrpcClient::build_from_shared(self.endpoint.clone())
|
|
||||||
.map_err(|e| ClientError::Other(format!("Failed to build client: {:?}", e)))?
|
|
||||||
.tls_config(ClientTlsConfig::new().with_native_roots())
|
|
||||||
.map_err(|e| ClientError::Other(format!("Failed to build client: {:?}", e)))?
|
|
||||||
.connect_timeout(Duration::from_secs(CONNECT_TIMEOUT))
|
|
||||||
.timeout(Duration::from_secs(REQUEST_TIMEOUT))
|
|
||||||
.connect()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ClientError::Other(format!("Failed to connect: {:?}", e)))?;
|
|
||||||
|
|
||||||
let subscribe_request = SubscribeRequest {
|
|
||||||
transactions,
|
|
||||||
commitment: Some(CommitmentLevel::Processed.into()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(client.subscribe_with_request(Some(subscribe_request)).await)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_subscribe_request_filter(
|
|
||||||
&self,
|
|
||||||
account_include: Vec<String>,
|
|
||||||
account_exclude: Vec<String>,
|
|
||||||
account_required: Vec<String>,
|
|
||||||
) -> TransactionsFilterMap {
|
|
||||||
let mut transactions = HashMap::new();
|
|
||||||
transactions.insert(
|
|
||||||
"client".to_string(),
|
|
||||||
SubscribeRequestFilterTransactions {
|
|
||||||
vote: Some(false),
|
|
||||||
failed: Some(false),
|
|
||||||
signature: None,
|
|
||||||
account_include,
|
|
||||||
account_exclude,
|
|
||||||
account_required,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
transactions
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_stream_message(
|
|
||||||
msg: SubscribeUpdate,
|
|
||||||
tx: &mut mpsc::Sender<TransactionPretty>,
|
|
||||||
subscribe_tx: &mut (impl Sink<SubscribeRequest, Error = mpsc::SendError> + Unpin),
|
|
||||||
) -> ClientResult<()> {
|
|
||||||
match msg.update_oneof {
|
|
||||||
Some(UpdateOneof::Transaction(sut)) => {
|
|
||||||
let transaction_pretty = TransactionPretty::from(sut);
|
|
||||||
tx.try_send(transaction_pretty).map_err(|e| ClientError::Other(format!("Send error: {:?}", e)))?;
|
|
||||||
}
|
|
||||||
Some(UpdateOneof::Ping(_)) => {
|
|
||||||
subscribe_tx
|
|
||||||
.send(SubscribeRequest {
|
|
||||||
ping: Some(SubscribeRequestPing { id: 1 }),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|e| ClientError::Other(format!("Ping error: {:?}", e)))?;
|
|
||||||
info!("service is ping: {}", Local::now());
|
|
||||||
}
|
|
||||||
Some(UpdateOneof::Pong(_)) => {
|
|
||||||
info!("service is pong: {}", Local::now());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn subscribe_pumpfun<F>(&self, callback: F, bot_wallet: Option<Pubkey>) -> ClientResult<()>
|
|
||||||
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.connect(transactions).await?
|
|
||||||
.map_err(|e| ClientError::Other(format!("Failed to subscribe: {:?}", e)))?;
|
|
||||||
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>) -> ClientResult<()>
|
|
||||||
where
|
|
||||||
F: Fn(PumpfunEvent) + Send + Sync,
|
|
||||||
{
|
|
||||||
let slot = transaction_pretty.slot;
|
|
||||||
let trade_raw = transaction_pretty.tx;
|
|
||||||
let meta = trade_raw.meta.as_ref()
|
|
||||||
.ok_or_else(|| ClientError::Other("Missing transaction metadata".to_string()))?;
|
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Executable
+352
@@ -0,0 +1,352 @@
|
|||||||
|
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 solana_sdk::{pubkey, pubkey::Pubkey, signature::Signature};
|
||||||
|
use solana_transaction_status::{
|
||||||
|
option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiTransactionEncoding,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::common::logs_data::{DexInstruction, TransferInfo};
|
||||||
|
use crate::common::logs_events::{PumpfunEvent, SystemEvent};
|
||||||
|
use crate::common::logs_filters::LogFilter;
|
||||||
|
pub type AnyResult<T> = anyhow::Result<T>;
|
||||||
|
|
||||||
|
type TransactionsFilterMap = HashMap<String, SubscribeRequestFilterTransactions>;
|
||||||
|
|
||||||
|
const PUMP_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
||||||
|
const SYSTEM_PROGRAM_ID: Pubkey = pubkey!("11111111111111111111111111111111");
|
||||||
|
const CONNECT_TIMEOUT: u64 = 10;
|
||||||
|
const REQUEST_TIMEOUT: u64 = 60;
|
||||||
|
const CHANNEL_SIZE: usize = 1000;
|
||||||
|
const MAX_DECODING_MESSAGE_SIZE: usize = 1024 * 1024 * 10;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TransactionPretty {
|
||||||
|
pub slot: u64,
|
||||||
|
pub signature: Signature,
|
||||||
|
pub is_vote: bool,
|
||||||
|
pub tx: EncodedTransactionWithStatusMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for TransactionPretty {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
struct TxWrap<'a>(&'a EncodedTransactionWithStatusMeta);
|
||||||
|
impl<'a> fmt::Debug for TxWrap<'a> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let serialized = serde_json::to_string(self.0).expect("failed to serialize");
|
||||||
|
fmt::Display::fmt(&serialized, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.debug_struct("TransactionPretty")
|
||||||
|
.field("slot", &self.slot)
|
||||||
|
.field("signature", &self.signature)
|
||||||
|
.field("is_vote", &self.is_vote)
|
||||||
|
.field("tx", &TxWrap(&self.tx))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SubscribeUpdateTransaction> for TransactionPretty {
|
||||||
|
fn from(SubscribeUpdateTransaction { transaction, slot }: SubscribeUpdateTransaction) -> Self {
|
||||||
|
let tx = transaction.expect("should be defined");
|
||||||
|
Self {
|
||||||
|
slot,
|
||||||
|
signature: Signature::try_from(tx.signature.as_slice()).expect("valid signature"),
|
||||||
|
is_vote: tx.is_vote,
|
||||||
|
tx: yellowstone_grpc_proto::convert_from::create_tx_with_meta(tx)
|
||||||
|
.expect("valid tx with meta")
|
||||||
|
.encode(UiTransactionEncoding::Base64, Some(u8::MAX), true)
|
||||||
|
.expect("failed to encode"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct YellowstoneGrpc {
|
||||||
|
endpoint: String,
|
||||||
|
x_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneGrpc {
|
||||||
|
pub fn new(endpoint: String, x_token: Option<String>) -> AnyResult<Self> {
|
||||||
|
if CryptoProvider::get_default().is_none() {
|
||||||
|
default_provider()
|
||||||
|
.install_default()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
endpoint,
|
||||||
|
x_token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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())?
|
||||||
|
.max_decoding_message_size(MAX_DECODING_MESSAGE_SIZE)
|
||||||
|
.connect_timeout(Duration::from_secs(CONNECT_TIMEOUT))
|
||||||
|
.timeout(Duration::from_secs(REQUEST_TIMEOUT));
|
||||||
|
|
||||||
|
Ok(builder.connect().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn subscribe_with_request(
|
||||||
|
&self,
|
||||||
|
transactions: TransactionsFilterMap,
|
||||||
|
) -> AnyResult<(
|
||||||
|
impl Sink<SubscribeRequest, Error = mpsc::SendError>,
|
||||||
|
impl Stream<Item = Result<SubscribeUpdate, Status>>,
|
||||||
|
)> {
|
||||||
|
let subscribe_request = SubscribeRequest {
|
||||||
|
transactions,
|
||||||
|
commitment: Some(CommitmentLevel::Processed.into()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut client = self.connect().await?;
|
||||||
|
let (sink, stream) = client.subscribe_with_request(Some(subscribe_request)).await?;
|
||||||
|
Ok((sink, stream))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_subscribe_request_filter(
|
||||||
|
&self,
|
||||||
|
account_include: Vec<String>,
|
||||||
|
account_exclude: Vec<String>,
|
||||||
|
account_required: Vec<String>,
|
||||||
|
) -> TransactionsFilterMap {
|
||||||
|
let mut transactions = HashMap::new();
|
||||||
|
transactions.insert(
|
||||||
|
"client".to_string(),
|
||||||
|
SubscribeRequestFilterTransactions {
|
||||||
|
vote: Some(false),
|
||||||
|
failed: Some(false),
|
||||||
|
signature: None,
|
||||||
|
account_include,
|
||||||
|
account_exclude,
|
||||||
|
account_required,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
transactions
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_stream_message(
|
||||||
|
msg: SubscribeUpdate,
|
||||||
|
tx: &mut mpsc::Sender<TransactionPretty>,
|
||||||
|
subscribe_tx: &mut (impl Sink<SubscribeRequest, Error = mpsc::SendError> + Unpin),
|
||||||
|
) -> AnyResult<()> {
|
||||||
|
match msg.update_oneof {
|
||||||
|
Some(UpdateOneof::Transaction(sut)) => {
|
||||||
|
let transaction_pretty = TransactionPretty::from(sut);
|
||||||
|
tx.try_send(transaction_pretty)?;
|
||||||
|
}
|
||||||
|
Some(UpdateOneof::Ping(_)) => {
|
||||||
|
subscribe_tx
|
||||||
|
.send(SubscribeRequest {
|
||||||
|
ping: Some(SubscribeRequestPing { id: 1 }),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
info!("service is ping: {}", Local::now());
|
||||||
|
}
|
||||||
|
Some(UpdateOneof::Pong(_)) => {
|
||||||
|
info!("service is pong: {}", Local::now());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
//! Instructions for interacting with the Pump.fun program.
|
|
||||||
//!
|
|
||||||
//! This module contains instruction builders for creating Solana instructions to interact with the
|
|
||||||
//! Pump.fun program. Each function takes the required accounts and instruction data and returns a
|
|
||||||
//! properly formatted Solana instruction.
|
|
||||||
//!
|
|
||||||
//! # Instructions
|
|
||||||
//!
|
|
||||||
//! - `create`: Instruction to create a new token with an associated bonding curve.
|
|
||||||
//! - `buy`: Instruction to buy tokens from a bonding curve by providing SOL.
|
|
||||||
//! - `sell`: Instruction to sell tokens back to the bonding curve in exchange for SOL.
|
|
||||||
use crate::{
|
|
||||||
constants,
|
|
||||||
pumpfun::common::{
|
|
||||||
get_bonding_curve_pda, get_global_pda, get_metadata_pda, get_mint_authority_pda
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use spl_associated_token_account::get_associated_token_address;
|
|
||||||
|
|
||||||
use solana_sdk::{
|
|
||||||
instruction::{AccountMeta, Instruction},
|
|
||||||
pubkey::Pubkey,
|
|
||||||
signature::Keypair,
|
|
||||||
signer::Signer,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Buy {
|
|
||||||
pub _amount: u64,
|
|
||||||
pub _max_sol_cost: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Buy {
|
|
||||||
pub fn data(&self) -> Vec<u8> {
|
|
||||||
let mut data = Vec::with_capacity(8 + 8 + 8);
|
|
||||||
data.extend_from_slice(&[102, 6, 61, 18, 1, 218, 235, 234]); // discriminator
|
|
||||||
data.extend_from_slice(&self._amount.to_le_bytes());
|
|
||||||
data.extend_from_slice(&self._max_sol_cost.to_le_bytes());
|
|
||||||
data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Sell {
|
|
||||||
pub _amount: u64,
|
|
||||||
pub _min_sol_output: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sell {
|
|
||||||
pub fn data(&self) -> Vec<u8> {
|
|
||||||
let mut data = Vec::with_capacity(8 + 8 + 8);
|
|
||||||
data.extend_from_slice(&[51, 230, 133, 164, 1, 127, 131, 173]); // discriminator
|
|
||||||
data.extend_from_slice(&self._amount.to_le_bytes());
|
|
||||||
data.extend_from_slice(&self._min_sol_output.to_le_bytes());
|
|
||||||
data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an instruction to buy tokens from a bonding curve
|
|
||||||
///
|
|
||||||
/// Buys tokens by providing SOL. The amount of tokens received is calculated based on
|
|
||||||
/// the bonding curve formula. A portion of the SOL is taken as a fee and sent to the
|
|
||||||
/// fee recipient account.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `payer` - Keypair that will provide the SOL to buy tokens
|
|
||||||
/// * `mint` - Public key of the token mint to buy
|
|
||||||
/// * `fee_recipient` - Public key of the account that will receive the transaction fee
|
|
||||||
/// * `args` - Buy instruction data containing the SOL amount and maximum acceptable token price
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// Returns a Solana instruction that when executed will buy tokens from the bonding curve
|
|
||||||
pub fn buy(
|
|
||||||
payer: &Keypair,
|
|
||||||
mint: &Pubkey,
|
|
||||||
bonding_curve: &Pubkey,
|
|
||||||
creator_vault: &Pubkey,
|
|
||||||
fee_recipient: &Pubkey,
|
|
||||||
args: Buy,
|
|
||||||
) -> Instruction {
|
|
||||||
Instruction::new_with_bytes(
|
|
||||||
constants::accounts::PUMPFUN,
|
|
||||||
&args.data(),
|
|
||||||
vec![
|
|
||||||
AccountMeta::new_readonly(constants::global_constants::GLOBAL_ACCOUNT, false),
|
|
||||||
AccountMeta::new(*fee_recipient, false),
|
|
||||||
AccountMeta::new_readonly(*mint, false),
|
|
||||||
AccountMeta::new(*bonding_curve, false),
|
|
||||||
AccountMeta::new(get_associated_token_address(bonding_curve, mint), false),
|
|
||||||
AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false),
|
|
||||||
AccountMeta::new(payer.pubkey(), true),
|
|
||||||
AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false),
|
|
||||||
AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false),
|
|
||||||
AccountMeta::new(*creator_vault, false),
|
|
||||||
AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false),
|
|
||||||
AccountMeta::new_readonly(constants::accounts::PUMPFUN, false),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an instruction to sell tokens back to a bonding curve
|
|
||||||
///
|
|
||||||
/// Sells tokens back to the bonding curve in exchange for SOL. The amount of SOL received
|
|
||||||
/// is calculated based on the bonding curve formula. A portion of the SOL is taken as
|
|
||||||
/// a fee and sent to the fee recipient account.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `payer` - Keypair that owns the tokens to sell
|
|
||||||
/// * `mint` - Public key of the token mint to sell
|
|
||||||
/// * `fee_recipient` - Public key of the account that will receive the transaction fee
|
|
||||||
/// * `args` - Sell instruction data containing token amount and minimum acceptable SOL output
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// Returns a Solana instruction that when executed will sell tokens to the bonding curve
|
|
||||||
pub fn sell(
|
|
||||||
payer: &Keypair,
|
|
||||||
mint: &Pubkey,
|
|
||||||
bonding_curve: &Pubkey,
|
|
||||||
creator_vault: &Pubkey,
|
|
||||||
fee_recipient: &Pubkey,
|
|
||||||
args: Sell,
|
|
||||||
) -> Instruction {
|
|
||||||
Instruction::new_with_bytes(
|
|
||||||
constants::accounts::PUMPFUN,
|
|
||||||
&args.data(),
|
|
||||||
vec![
|
|
||||||
AccountMeta::new_readonly(constants::global_constants::GLOBAL_ACCOUNT, false),
|
|
||||||
AccountMeta::new(*fee_recipient, false),
|
|
||||||
AccountMeta::new_readonly(*mint, false),
|
|
||||||
AccountMeta::new(*bonding_curve, false),
|
|
||||||
AccountMeta::new(get_associated_token_address(&bonding_curve, mint), false),
|
|
||||||
AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false),
|
|
||||||
AccountMeta::new(payer.pubkey(), true),
|
|
||||||
AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false),
|
|
||||||
AccountMeta::new(*creator_vault, false),
|
|
||||||
AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false),
|
|
||||||
AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false),
|
|
||||||
AccountMeta::new_readonly(constants::accounts::PUMPFUN, false),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
-161
@@ -1,161 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use serde_json::Value;
|
|
||||||
use tokio::fs::File;
|
|
||||||
use tokio::io::AsyncReadExt;
|
|
||||||
use reqwest::Client;
|
|
||||||
use reqwest::multipart::{Form, Part};
|
|
||||||
use base64::{Engine as _, engine::general_purpose};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Metadata structure for a token, matching the format expected by Pump.fun.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct TokenMetadata {
|
|
||||||
/// Name of the token
|
|
||||||
pub name: String,
|
|
||||||
/// Token symbol (e.g. "BTC")
|
|
||||||
pub symbol: String,
|
|
||||||
/// Description of the token
|
|
||||||
pub description: String,
|
|
||||||
/// IPFS URL of the token's image
|
|
||||||
pub image: String,
|
|
||||||
/// Whether to display the token's name
|
|
||||||
pub show_name: bool,
|
|
||||||
/// Creation timestamp/source
|
|
||||||
pub created_on: String,
|
|
||||||
/// Twitter handle
|
|
||||||
pub twitter: Option<String>,
|
|
||||||
/// Telegram handle
|
|
||||||
pub telegram: Option<String>,
|
|
||||||
/// Website URL
|
|
||||||
pub website: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Response received after successfully uploading token metadata.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct TokenMetadataIPFS {
|
|
||||||
/// The uploaded token metadata
|
|
||||||
pub metadata: TokenMetadata,
|
|
||||||
/// IPFS URI where the metadata is stored
|
|
||||||
pub metadata_uri: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parameters for creating new token metadata.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CreateTokenMetadata {
|
|
||||||
/// Name of the token
|
|
||||||
pub name: String,
|
|
||||||
/// Token symbol (e.g. "BTC")
|
|
||||||
pub symbol: String,
|
|
||||||
/// Description of the token
|
|
||||||
pub description: String,
|
|
||||||
/// Path to the token's image file
|
|
||||||
pub file: String,
|
|
||||||
/// Optional Twitter handle
|
|
||||||
pub twitter: Option<String>,
|
|
||||||
/// Optional Telegram group
|
|
||||||
pub telegram: Option<String>,
|
|
||||||
/// Optional website URL
|
|
||||||
pub website: Option<String>,
|
|
||||||
|
|
||||||
pub metadata_uri: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_token_metadata(metadata: CreateTokenMetadata, jwt_token: &str) -> Result<TokenMetadataIPFS, anyhow::Error> {
|
|
||||||
let ipfs_url = if metadata.file.starts_with("http") || metadata.metadata_uri.is_some() {
|
|
||||||
metadata.file
|
|
||||||
} else {
|
|
||||||
let base64_string = file_to_base64(&metadata.file).await?;
|
|
||||||
upload_base64_file(&base64_string, jwt_token).await?
|
|
||||||
};
|
|
||||||
|
|
||||||
let token_metadata = TokenMetadata {
|
|
||||||
name: metadata.name,
|
|
||||||
symbol: metadata.symbol,
|
|
||||||
description: metadata.description,
|
|
||||||
image: ipfs_url,
|
|
||||||
show_name: true,
|
|
||||||
created_on: "https://pump.fun".to_string(),
|
|
||||||
twitter: metadata.twitter,
|
|
||||||
telegram: metadata.telegram,
|
|
||||||
website: metadata.website,
|
|
||||||
};
|
|
||||||
|
|
||||||
if metadata.metadata_uri.is_some() {
|
|
||||||
let token_metadata_ipfs = TokenMetadataIPFS {
|
|
||||||
metadata: token_metadata,
|
|
||||||
metadata_uri: metadata.metadata_uri.unwrap(),
|
|
||||||
};
|
|
||||||
Ok(token_metadata_ipfs)
|
|
||||||
} else {
|
|
||||||
let client = Client::new();
|
|
||||||
let response = client
|
|
||||||
.post("https://api.pinata.cloud/pinning/pinJSONToIPFS")
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("Authorization", format!("Bearer {}", jwt_token))
|
|
||||||
.json(&token_metadata)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// 确保请求成功
|
|
||||||
if response.status().is_success() {
|
|
||||||
let res_data: serde_json::Value = response.json().await?;
|
|
||||||
let ipfs_hash = res_data["IpfsHash"].as_str().unwrap();
|
|
||||||
let ipfs_url = format!("https://ipfs.io/ipfs/{}", ipfs_hash);
|
|
||||||
let token_metadata_ipfs = TokenMetadataIPFS {
|
|
||||||
metadata: token_metadata,
|
|
||||||
metadata_uri: ipfs_url,
|
|
||||||
};
|
|
||||||
Ok(token_metadata_ipfs)
|
|
||||||
} else {
|
|
||||||
eprintln!("Error: {:?}", response.status());
|
|
||||||
Err(anyhow::anyhow!("Failed to create token metadata"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn upload_base64_file(base64_string: &str, jwt_token: &str) -> Result<String, anyhow::Error> {
|
|
||||||
let decoded_bytes = general_purpose::STANDARD.decode(base64_string)?;
|
|
||||||
|
|
||||||
let client = Client::builder()
|
|
||||||
.timeout(Duration::from_secs(120)) // 增加超时时间到120秒
|
|
||||||
.pool_max_idle_per_host(0) // 禁用连接池
|
|
||||||
.pool_idle_timeout(None) // 禁用空闲超时
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let part = Part::bytes(decoded_bytes)
|
|
||||||
.file_name("file.png") // 添加文件扩展名
|
|
||||||
.mime_str("image/png")?; // 指定正确的MIME类型
|
|
||||||
|
|
||||||
let form = Form::new().part("file", part);
|
|
||||||
|
|
||||||
let response = client
|
|
||||||
.post("https://api.pinata.cloud/pinning/pinFileToIPFS")
|
|
||||||
.header("Authorization", format!("Bearer {}", jwt_token))
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.multipart(form)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
let response_json: Value = response.json().await.map_err(|e| anyhow::anyhow!("Failed to parse JSON: {}", e))?;
|
|
||||||
println!("{:#?}", response_json);
|
|
||||||
let ipfs_hash = response_json["IpfsHash"].as_str().unwrap();
|
|
||||||
let ipfs_url = format!("https://ipfs.io/ipfs/{}", ipfs_hash);
|
|
||||||
Ok(ipfs_url)
|
|
||||||
} else {
|
|
||||||
let error_text = response.text().await?;
|
|
||||||
eprintln!("Error: {:?}", error_text);
|
|
||||||
Err(anyhow::anyhow!("Failed to upload file to IPFS: {}", error_text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn file_to_base64(file_path: &str) -> Result<String, anyhow::Error> {
|
|
||||||
let mut file = File::open(file_path).await?;
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
file.read_to_end(&mut buffer).await?;
|
|
||||||
let base64_string = general_purpose::STANDARD.encode(&buffer);
|
|
||||||
Ok(base64_string)
|
|
||||||
}
|
|
||||||
+2
-260
@@ -1,261 +1,3 @@
|
|||||||
pub mod accounts;
|
|
||||||
pub mod constants;
|
|
||||||
pub mod error;
|
|
||||||
pub mod instruction;
|
|
||||||
pub mod grpc;
|
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod ipfs;
|
pub mod grpc;
|
||||||
pub mod swqos;
|
pub mod error;
|
||||||
pub mod pumpfun;
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use swqos::{FeeClient, JitoClient, NextBlockClient, ZeroSlotClient};
|
|
||||||
use rustls::crypto::{ring::default_provider, CryptoProvider};
|
|
||||||
use solana_sdk::{
|
|
||||||
commitment_config::CommitmentConfig,
|
|
||||||
pubkey::Pubkey,
|
|
||||||
signature::{Keypair, Signer},
|
|
||||||
};
|
|
||||||
|
|
||||||
use common::{logs_data::TradeInfo, logs_events::PumpfunEvent, logs_subscribe, Cluster, PriorityFee, SolanaRpcClient};
|
|
||||||
use common::logs_subscribe::SubscriptionHandle;
|
|
||||||
use ipfs::TokenMetadataIPFS;
|
|
||||||
|
|
||||||
pub struct PumpFun {
|
|
||||||
pub payer: Arc<Keypair>,
|
|
||||||
pub rpc: Arc<SolanaRpcClient>,
|
|
||||||
pub fee_clients: Vec<Arc<FeeClient>>,
|
|
||||||
pub priority_fee: PriorityFee,
|
|
||||||
pub cluster: Cluster,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for PumpFun {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
payer: self.payer.clone(),
|
|
||||||
rpc: self.rpc.clone(),
|
|
||||||
fee_clients: self.fee_clients.clone(),
|
|
||||||
priority_fee: self.priority_fee.clone(),
|
|
||||||
cluster: self.cluster.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PumpFun {
|
|
||||||
#[inline]
|
|
||||||
pub async fn new(
|
|
||||||
payer: Arc<Keypair>,
|
|
||||||
cluster: &Cluster,
|
|
||||||
) -> Self {
|
|
||||||
if CryptoProvider::get_default().is_none() {
|
|
||||||
let _ = default_provider()
|
|
||||||
.install_default()
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
let rpc = SolanaRpcClient::new_with_commitment(
|
|
||||||
cluster.clone().rpc_url,
|
|
||||||
cluster.clone().commitment
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut fee_clients: Vec<Arc<FeeClient>> = vec![];
|
|
||||||
if cluster.clone().use_jito {
|
|
||||||
let jito_client = JitoClient::new(
|
|
||||||
cluster.clone().rpc_url,
|
|
||||||
cluster.clone().block_engine_url
|
|
||||||
).await.expect("Failed to create Jito client");
|
|
||||||
|
|
||||||
fee_clients.push(Arc::new(jito_client));
|
|
||||||
}
|
|
||||||
|
|
||||||
if cluster.clone().use_zeroslot {
|
|
||||||
let zeroslot_client = ZeroSlotClient::new(
|
|
||||||
cluster.clone().rpc_url,
|
|
||||||
cluster.clone().zeroslot_url,
|
|
||||||
cluster.clone().zeroslot_auth_token
|
|
||||||
);
|
|
||||||
|
|
||||||
fee_clients.push(Arc::new(zeroslot_client));
|
|
||||||
}
|
|
||||||
|
|
||||||
if cluster.clone().use_nextblock {
|
|
||||||
let nextblock_client = NextBlockClient::new(
|
|
||||||
cluster.clone().rpc_url,
|
|
||||||
cluster.clone().nextblock_url,
|
|
||||||
cluster.clone().nextblock_auth_token
|
|
||||||
);
|
|
||||||
|
|
||||||
fee_clients.push(Arc::new(nextblock_client));
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
payer,
|
|
||||||
rpc: Arc::new(rpc),
|
|
||||||
fee_clients,
|
|
||||||
priority_fee: cluster.clone().priority_fee,
|
|
||||||
cluster: cluster.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Buy tokens
|
|
||||||
pub async fn buy(
|
|
||||||
&self,
|
|
||||||
mint: Pubkey,
|
|
||||||
amount_sol: u64,
|
|
||||||
slippage_basis_points: Option<u64>,
|
|
||||||
) -> Result<(), anyhow::Error> {
|
|
||||||
pumpfun::buy::buy(
|
|
||||||
self.rpc.clone(),
|
|
||||||
self.payer.clone(),
|
|
||||||
mint,
|
|
||||||
amount_sol,
|
|
||||||
slippage_basis_points,
|
|
||||||
self.priority_fee.clone(),
|
|
||||||
).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Buy tokens using Jito
|
|
||||||
pub async fn buy_with_tip(
|
|
||||||
&self,
|
|
||||||
mint: Pubkey,
|
|
||||||
amount_sol: u64,
|
|
||||||
slippage_basis_points: Option<u64>,
|
|
||||||
) -> Result<(), anyhow::Error> {
|
|
||||||
pumpfun::buy::buy_with_tip(
|
|
||||||
self.rpc.clone(),
|
|
||||||
self.fee_clients.clone(),
|
|
||||||
self.payer.clone(),
|
|
||||||
mint,
|
|
||||||
amount_sol,
|
|
||||||
slippage_basis_points,
|
|
||||||
self.priority_fee.clone(),
|
|
||||||
).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sell tokens
|
|
||||||
pub async fn sell(
|
|
||||||
&self,
|
|
||||||
mint: Pubkey,
|
|
||||||
amount_token: Option<u64>,
|
|
||||||
) -> Result<(), anyhow::Error> {
|
|
||||||
pumpfun::sell::sell(
|
|
||||||
self.rpc.clone(),
|
|
||||||
self.payer.clone(),
|
|
||||||
mint.clone(),
|
|
||||||
amount_token,
|
|
||||||
self.priority_fee.clone(),
|
|
||||||
).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sell tokens by percentage
|
|
||||||
pub async fn sell_by_percent(
|
|
||||||
&self,
|
|
||||||
mint: Pubkey,
|
|
||||||
percent: u64,
|
|
||||||
) -> Result<(), anyhow::Error> {
|
|
||||||
pumpfun::sell::sell_by_percent(
|
|
||||||
self.rpc.clone(),
|
|
||||||
self.payer.clone(),
|
|
||||||
mint.clone(),
|
|
||||||
percent,
|
|
||||||
self.priority_fee.clone(),
|
|
||||||
).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn sell_by_percent_with_tip(
|
|
||||||
&self,
|
|
||||||
mint: Pubkey,
|
|
||||||
percent: u64,
|
|
||||||
) -> Result<(), anyhow::Error> {
|
|
||||||
pumpfun::sell::sell_by_percent_with_tip(
|
|
||||||
self.rpc.clone(),
|
|
||||||
self.fee_clients.clone(),
|
|
||||||
self.payer.clone(),
|
|
||||||
mint,
|
|
||||||
percent,
|
|
||||||
self.priority_fee.clone(),
|
|
||||||
).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sell tokens using Jito
|
|
||||||
pub async fn sell_with_tip(
|
|
||||||
&self,
|
|
||||||
mint: Pubkey,
|
|
||||||
amount_token: Option<u64>,
|
|
||||||
) -> Result<(), anyhow::Error> {
|
|
||||||
pumpfun::sell::sell_with_tip(
|
|
||||||
self.rpc.clone(),
|
|
||||||
self.fee_clients.clone(),
|
|
||||||
self.payer.clone(),
|
|
||||||
mint,
|
|
||||||
amount_token,
|
|
||||||
self.priority_fee.clone(),
|
|
||||||
).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn tokens_subscription<F>(
|
|
||||||
&self,
|
|
||||||
ws_url: &str,
|
|
||||||
commitment: CommitmentConfig,
|
|
||||||
callback: F,
|
|
||||||
bot_wallet: Option<Pubkey>,
|
|
||||||
) -> Result<SubscriptionHandle, Box<dyn std::error::Error>>
|
|
||||||
where
|
|
||||||
F: Fn(PumpfunEvent) + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
logs_subscribe::tokens_subscription(ws_url, commitment, callback, bot_wallet).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn stop_subscription(&self, subscription_handle: SubscriptionHandle) {
|
|
||||||
subscription_handle.shutdown().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn get_sol_balance(&self, payer: &Pubkey) -> Result<u64, anyhow::Error> {
|
|
||||||
pumpfun::common::get_sol_balance(&self.rpc, payer).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn get_payer_sol_balance(&self) -> Result<u64, anyhow::Error> {
|
|
||||||
pumpfun::common::get_sol_balance(&self.rpc, &self.payer.pubkey()).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn get_token_balance(&self, payer: &Pubkey, mint: &Pubkey) -> Result<u64, anyhow::Error> {
|
|
||||||
println!("get_token_balance payer: {}, mint: {}, cluster: {}", payer, mint, self.cluster.rpc_url);
|
|
||||||
pumpfun::common::get_token_balance(&self.rpc, payer, mint).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn get_payer_token_balance(&self, mint: &Pubkey) -> Result<u64, anyhow::Error> {
|
|
||||||
pumpfun::common::get_token_balance(&self.rpc, &self.payer.pubkey(), mint).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn get_payer_pubkey(&self) -> Pubkey {
|
|
||||||
self.payer.pubkey()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn get_payer(&self) -> &Keypair {
|
|
||||||
self.payer.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn get_token_price(&self,virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 {
|
|
||||||
pumpfun::common::get_token_price(virtual_sol_reserves, virtual_token_reserves)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn get_buy_price(&self, amount: u64, trade_info: &TradeInfo) -> u64 {
|
|
||||||
pumpfun::common::get_buy_price(amount, trade_info)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn transfer_sol(&self, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> {
|
|
||||||
pumpfun::common::transfer_sol(&self.rpc, payer, receive_wallet, amount).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+15
-13
@@ -1,36 +1,38 @@
|
|||||||
use std::sync::Arc;
|
use grpc_parsed::{common::{
|
||||||
use pumpfun_sdk::{common::{logs_events::PumpfunEvent, Cluster, PriorityFee}, grpc::YellowstoneGrpc, ipfs, PumpFun};
|
logs_events::PumpfunEvent,
|
||||||
use solana_sdk::{commitment_config::CommitmentConfig, native_token::sol_to_lamports, signature::Keypair, signer::Signer};
|
}, grpc::YellowstoneGrpc};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// create grpc client
|
let grpc = YellowstoneGrpc::new(
|
||||||
let grpc_url = "http://127.0.0.1:10000";
|
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
||||||
let client = YellowstoneGrpc::new(grpc_url.to_string());
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
println!("Connected to the network");
|
||||||
|
|
||||||
// Define callback function
|
|
||||||
let callback = |event: PumpfunEvent| {
|
let callback = |event: PumpfunEvent| {
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
|
PumpfunEvent::NewDevTrade(trade_info) => {
|
||||||
|
println!("Received new dev trade event: {:?}", trade_info);
|
||||||
|
},
|
||||||
PumpfunEvent::NewToken(token_info) => {
|
PumpfunEvent::NewToken(token_info) => {
|
||||||
println!("Received new token event: {:?}", token_info);
|
println!("Received new token event: {:?}", token_info);
|
||||||
},
|
},
|
||||||
PumpfunEvent::NewDevTrade(trade_info) => {
|
|
||||||
println!("Received dev trade event: {:?}", trade_info);
|
|
||||||
},
|
|
||||||
PumpfunEvent::NewUserTrade(trade_info) => {
|
PumpfunEvent::NewUserTrade(trade_info) => {
|
||||||
println!("Received new trade event: {:?}", trade_info);
|
println!("Received new trade event: {:?}", trade_info);
|
||||||
},
|
},
|
||||||
PumpfunEvent::NewBotTrade(trade_info) => {
|
PumpfunEvent::NewBotTrade(trade_info) => {
|
||||||
println!("Received new bot trade event: {:?}", trade_info);
|
println!("Received new bot trade event: {:?}", trade_info);
|
||||||
}
|
},
|
||||||
PumpfunEvent::Error(err) => {
|
PumpfunEvent::Error(err) => {
|
||||||
println!("Received error: {}", err);
|
println!("Received error: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let payer_keypair = Keypair::from_base58_string("your private key");
|
grpc.subscribe_pumpfun(callback, None).await?;
|
||||||
client.subscribe_pumpfun(callback, Some(payer_keypair.pubkey())).await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
use anyhow::anyhow;
|
|
||||||
use solana_sdk::{
|
|
||||||
compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::{Transaction, VersionedTransaction}
|
|
||||||
};
|
|
||||||
use solana_hash::Hash;
|
|
||||||
use spl_associated_token_account::instruction::create_associated_token_account;
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use std::{str::FromStr, time::Instant, sync::Arc};
|
|
||||||
|
|
||||||
use crate::{common::{PriorityFee, SolanaRpcClient}, constants::{self, global_constants::FEE_RECIPIENT}, instruction, swqos::FeeClient};
|
|
||||||
|
|
||||||
const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 250000;
|
|
||||||
|
|
||||||
use super::common::{calculate_with_slippage_buy, get_bonding_curve_account, get_buy_token_amount_from_sol_amount, get_creator_vault_pda};
|
|
||||||
|
|
||||||
pub async fn buy(
|
|
||||||
rpc: Arc<SolanaRpcClient>,
|
|
||||||
payer: Arc<Keypair>,
|
|
||||||
mint: Pubkey,
|
|
||||||
amount_sol: u64,
|
|
||||||
slippage_basis_points: Option<u64>,
|
|
||||||
priority_fee: PriorityFee,
|
|
||||||
) -> Result<(), anyhow::Error> {
|
|
||||||
let transaction = build_buy_transaction(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points, priority_fee.clone()).await?;
|
|
||||||
rpc.send_and_confirm_transaction(&transaction).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Buy tokens using Jito
|
|
||||||
pub async fn buy_with_tip(
|
|
||||||
rpc: Arc<SolanaRpcClient>,
|
|
||||||
fee_clients: Vec<Arc<FeeClient>>,
|
|
||||||
payer: Arc<Keypair>,
|
|
||||||
mint: Pubkey,
|
|
||||||
amount_sol: u64,
|
|
||||||
slippage_basis_points: Option<u64>,
|
|
||||||
priority_fee: PriorityFee,
|
|
||||||
) -> Result<(), anyhow::Error> {
|
|
||||||
let start_time = Instant::now();
|
|
||||||
|
|
||||||
let mint = Arc::new(mint.clone());
|
|
||||||
let instructions = build_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points).await?;
|
|
||||||
|
|
||||||
let mut transactions = vec![];
|
|
||||||
let recent_blockhash = rpc.get_latest_blockhash().await?;
|
|
||||||
for fee_client in fee_clients.clone() {
|
|
||||||
let payer = payer.clone();
|
|
||||||
let priority_fee = priority_fee.clone();
|
|
||||||
let tip_account = fee_client.get_tip_account().await.map_err(|e| anyhow!(e.to_string()))?;
|
|
||||||
let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?);
|
|
||||||
|
|
||||||
let transaction = build_buy_transaction_with_tip(tip_account, payer, priority_fee, instructions.clone(), recent_blockhash).await?;
|
|
||||||
transactions.push(transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut handles: Vec<JoinHandle<Result<(), anyhow::Error>>> = vec![];
|
|
||||||
for i in 0..fee_clients.len() {
|
|
||||||
let fee_client = fee_clients[i].clone();
|
|
||||||
let transactions = transactions.clone();
|
|
||||||
let start_time = start_time.clone();
|
|
||||||
let transaction = transactions[i].clone();
|
|
||||||
let handle = tokio::spawn(async move {
|
|
||||||
fee_client.send_transaction(&transaction).await?;
|
|
||||||
println!("index: {}, Total Jito buy operation time: {:?}ms", i, 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_buy_transaction(
|
|
||||||
rpc: Arc<SolanaRpcClient>,
|
|
||||||
payer: Arc<Keypair>,
|
|
||||||
mint: Pubkey,
|
|
||||||
amount_sol: u64,
|
|
||||||
slippage_basis_points: Option<u64>,
|
|
||||||
priority_fee: PriorityFee,
|
|
||||||
) -> Result<Transaction, anyhow::Error> {
|
|
||||||
let mut instructions = vec![
|
|
||||||
ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT),
|
|
||||||
ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price),
|
|
||||||
ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit),
|
|
||||||
];
|
|
||||||
|
|
||||||
let build_instructions = build_buy_instructions(rpc.clone(), payer.clone(), Arc::new(mint), amount_sol, slippage_basis_points).await?;
|
|
||||||
instructions.extend(build_instructions);
|
|
||||||
|
|
||||||
let recent_blockhash = rpc.get_latest_blockhash().await?;
|
|
||||||
let transaction = Transaction::new_signed_with_payer(
|
|
||||||
&instructions,
|
|
||||||
Some(&payer.pubkey()),
|
|
||||||
&[payer],
|
|
||||||
recent_blockhash,
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(transaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn build_buy_transaction_with_tip(
|
|
||||||
tip_account: Arc<Pubkey>,
|
|
||||||
payer: Arc<Keypair>,
|
|
||||||
priority_fee: PriorityFee,
|
|
||||||
build_instructions: Vec<Instruction>,
|
|
||||||
blockhash: Hash,
|
|
||||||
) -> Result<VersionedTransaction, anyhow::Error> {
|
|
||||||
let mut instructions = vec![
|
|
||||||
ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT),
|
|
||||||
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(priority_fee.buy_tip_fee),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
instructions.extend(build_instructions);
|
|
||||||
|
|
||||||
let v0_message: v0::Message =
|
|
||||||
v0::Message::try_compile(&payer.pubkey(), &instructions, &[], blockhash)?;
|
|
||||||
let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message);
|
|
||||||
let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?;
|
|
||||||
|
|
||||||
Ok(transaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn build_buy_instructions(
|
|
||||||
rpc: Arc<SolanaRpcClient>,
|
|
||||||
payer: Arc<Keypair>,
|
|
||||||
mint: Arc<Pubkey>,
|
|
||||||
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, bonding_curve_pda) = get_bonding_curve_account(&rpc, &mint).await?;
|
|
||||||
let creator_vault_pda = get_creator_vault_pda(&bonding_curve.creator).unwrap();
|
|
||||||
let max_sol_cost = calculate_with_slippage_buy(buy_sol_cost, slippage_basis_points.unwrap_or(100));
|
|
||||||
|
|
||||||
let mut buy_token_amount = get_buy_token_amount_from_sol_amount(&bonding_curve, buy_sol_cost);
|
|
||||||
if buy_token_amount <= 100 * 1_000_000_u64 {
|
|
||||||
buy_token_amount = if max_sol_cost > sol_to_lamports(0.01) {
|
|
||||||
25547619 * 1_000_000_u64
|
|
||||||
} else {
|
|
||||||
255476 * 1_000_000_u64
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut instructions = vec![];
|
|
||||||
instructions.push(create_associated_token_account(
|
|
||||||
&payer.pubkey(),
|
|
||||||
&payer.pubkey(),
|
|
||||||
&mint,
|
|
||||||
&constants::accounts::TOKEN_PROGRAM,
|
|
||||||
));
|
|
||||||
|
|
||||||
instructions.push(instruction::buy(
|
|
||||||
payer.as_ref(),
|
|
||||||
&mint,
|
|
||||||
&bonding_curve_pda,
|
|
||||||
&creator_vault_pda,
|
|
||||||
&FEE_RECIPIENT,
|
|
||||||
instruction::Buy {
|
|
||||||
_amount: buy_token_amount,
|
|
||||||
_max_sol_cost: max_sol_cost,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
Ok(instructions)
|
|
||||||
}
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
use anyhow::anyhow;
|
|
||||||
use spl_token::state::Account;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use std::{collections::HashMap, sync::Arc};
|
|
||||||
use solana_sdk::{
|
|
||||||
commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::Transaction
|
|
||||||
};
|
|
||||||
use spl_associated_token_account::get_associated_token_address;
|
|
||||||
use crate::{accounts::{self, BondingCurveAccount}, common::{logs_data::TradeInfo, PriorityFee, SolanaRpcClient}, constants::{self, global_constants::{CREATOR_FEE, FEE_BASIS_POINTS}, trade::DEFAULT_SLIPPAGE}};
|
|
||||||
use borsh::BorshDeserialize;
|
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
|
||||||
static ref ACCOUNT_CACHE: RwLock<HashMap<Pubkey, Arc<accounts::GlobalAccount>>> = RwLock::new(HashMap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn transfer_sol(rpc: &SolanaRpcClient, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> {
|
|
||||||
if amount == 0 {
|
|
||||||
return Err(anyhow!("transfer_sol: Amount cannot be zero"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let balance = get_sol_balance(rpc, &payer.pubkey()).await?;
|
|
||||||
if balance < amount {
|
|
||||||
return Err(anyhow!("Insufficient balance"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let transfer_instruction = system_instruction::transfer(
|
|
||||||
&payer.pubkey(),
|
|
||||||
receive_wallet,
|
|
||||||
amount,
|
|
||||||
);
|
|
||||||
|
|
||||||
let recent_blockhash = rpc.get_latest_blockhash().await?;
|
|
||||||
|
|
||||||
let transaction = Transaction::new_signed_with_payer(
|
|
||||||
&[transfer_instruction],
|
|
||||||
Some(&payer.pubkey()),
|
|
||||||
&[payer],
|
|
||||||
recent_blockhash,
|
|
||||||
);
|
|
||||||
|
|
||||||
rpc.send_and_confirm_transaction(&transaction).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn create_priority_fee_instructions(priority_fee: PriorityFee) -> Vec<Instruction> {
|
|
||||||
let mut instructions = Vec::with_capacity(2);
|
|
||||||
instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit));
|
|
||||||
instructions.push(ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price));
|
|
||||||
|
|
||||||
instructions
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[inline]
|
|
||||||
pub async fn get_token_balance(rpc: &SolanaRpcClient, payer: &Pubkey, mint: &Pubkey) -> Result<u64, anyhow::Error> {
|
|
||||||
let ata = get_associated_token_address(payer, mint);
|
|
||||||
// let account_data = rpc.get_account_data(&ata).await?;
|
|
||||||
// let token_account = Account::unpack(&account_data.as_slice())?;
|
|
||||||
|
|
||||||
// Ok(token_account.amount)
|
|
||||||
|
|
||||||
// println!("get_token_balance ata: {}", ata);
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn get_token_balance_and_ata(rpc: &SolanaRpcClient, payer: &Keypair, mint: &Pubkey) -> Result<(u64, Pubkey), anyhow::Error> {
|
|
||||||
let ata = get_associated_token_address(&payer.pubkey(), mint);
|
|
||||||
// let account_data = rpc.get_account_data(&ata).await?;
|
|
||||||
// let token_account = Account::unpack(&account_data)?;
|
|
||||||
|
|
||||||
// Ok((token_account.amount, ata))
|
|
||||||
|
|
||||||
let balance = rpc.get_token_account_balance(&ata).await?;
|
|
||||||
let balance_u64 = balance.amount.parse::<u64>()
|
|
||||||
.map_err(|_| anyhow!("Failed to parse token balance"))?;
|
|
||||||
|
|
||||||
if balance_u64 == 0 {
|
|
||||||
return Err(anyhow!("Balance is 0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((balance_u64, ata))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn get_sol_balance(rpc: &SolanaRpcClient, account: &Pubkey) -> Result<u64, anyhow::Error> {
|
|
||||||
let balance = rpc.get_balance(account).await?;
|
|
||||||
Ok(balance)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn get_global_pda() -> Pubkey {
|
|
||||||
static GLOBAL_PDA: once_cell::sync::Lazy<Pubkey> = once_cell::sync::Lazy::new(|| {
|
|
||||||
Pubkey::find_program_address(&[constants::seeds::GLOBAL_SEED], &constants::accounts::PUMPFUN).0
|
|
||||||
});
|
|
||||||
*GLOBAL_PDA
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn get_mint_authority_pda() -> Pubkey {
|
|
||||||
static MINT_AUTHORITY_PDA: once_cell::sync::Lazy<Pubkey> = once_cell::sync::Lazy::new(|| {
|
|
||||||
Pubkey::find_program_address(&[constants::seeds::MINT_AUTHORITY_SEED], &constants::accounts::PUMPFUN).0
|
|
||||||
});
|
|
||||||
*MINT_AUTHORITY_PDA
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
|
|
||||||
let seeds: &[&[u8]; 2] = &[constants::seeds::BONDING_CURVE_SEED, mint.as_ref()];
|
|
||||||
let program_id: &Pubkey = &constants::accounts::PUMPFUN;
|
|
||||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
|
||||||
pda.map(|pubkey| pubkey.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn get_creator_vault_pda(creator: &Pubkey) -> Option<Pubkey> {
|
|
||||||
let seeds: &[&[u8]; 2] = &[constants::seeds::CREATOR_VAULT_SEED, creator.as_ref()];
|
|
||||||
let program_id: &Pubkey = &constants::accounts::PUMPFUN;
|
|
||||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
|
||||||
pda.map(|pubkey| pubkey.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn get_metadata_pda(mint: &Pubkey) -> Pubkey {
|
|
||||||
Pubkey::find_program_address(
|
|
||||||
&[
|
|
||||||
constants::seeds::METADATA_SEED,
|
|
||||||
constants::accounts::MPL_TOKEN_METADATA.as_ref(),
|
|
||||||
mint.as_ref(),
|
|
||||||
],
|
|
||||||
&constants::accounts::MPL_TOKEN_METADATA
|
|
||||||
).0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn get_global_account(rpc: &SolanaRpcClient) -> Result<Arc<accounts::GlobalAccount>, anyhow::Error> {
|
|
||||||
let global = get_global_pda();
|
|
||||||
if let Some(account) = ACCOUNT_CACHE.read().await.get(&global) {
|
|
||||||
return Ok(account.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let account = rpc.get_account(&global).await?;
|
|
||||||
let global_account = bincode::deserialize::<accounts::GlobalAccount>(&account.data)?;
|
|
||||||
let global_account = Arc::new(global_account);
|
|
||||||
|
|
||||||
ACCOUNT_CACHE.write().await.insert(global, global_account.clone());
|
|
||||||
Ok(global_account)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn get_initial_buy_price(global_account: &Arc<accounts::GlobalAccount>, amount_sol: u64) -> Result<u64, anyhow::Error> {
|
|
||||||
let buy_amount = global_account.get_initial_buy_price(amount_sol);
|
|
||||||
Ok(buy_amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn get_bonding_curve_account(
|
|
||||||
rpc: &SolanaRpcClient,
|
|
||||||
mint: &Pubkey,
|
|
||||||
) -> Result<(Arc<accounts::BondingCurveAccount>, Pubkey), anyhow::Error> {
|
|
||||||
let bonding_curve_pda = get_bonding_curve_pda(mint)
|
|
||||||
.ok_or(anyhow!("Bonding curve not found"))?;
|
|
||||||
|
|
||||||
let account = rpc.get_account(&bonding_curve_pda).await?;
|
|
||||||
if account.data.is_empty() {
|
|
||||||
return Err(anyhow!("Bonding curve not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let bonding_curve = Arc::new(bincode::deserialize::<accounts::BondingCurveAccount>(&account.data)?);
|
|
||||||
|
|
||||||
Ok((bonding_curve, bonding_curve_pda))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn get_buy_token_amount(
|
|
||||||
bonding_curve_account: &Arc<accounts::BondingCurveAccount>,
|
|
||||||
buy_sol_cost: u64,
|
|
||||||
slippage_basis_points: Option<u64>,
|
|
||||||
) -> anyhow::Result<(u64, u64)> {
|
|
||||||
let buy_token = bonding_curve_account.get_buy_price(buy_sol_cost).map_err(|e| anyhow!(e))?;
|
|
||||||
|
|
||||||
let max_sol_cost = calculate_with_slippage_buy(buy_sol_cost, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE));
|
|
||||||
|
|
||||||
Ok((buy_token, max_sol_cost))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_buy_token_amount_from_sol_amount(
|
|
||||||
bonding_curve: &BondingCurveAccount,
|
|
||||||
amount: u64,
|
|
||||||
) -> u64 {
|
|
||||||
if amount == 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if bonding_curve.virtual_token_reserves == 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let total_fee_basis_points = FEE_BASIS_POINTS
|
|
||||||
+ if bonding_curve.creator != Pubkey::default() {
|
|
||||||
CREATOR_FEE
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
// 转为 u128 防止溢出
|
|
||||||
let amount_128 = amount as u128;
|
|
||||||
let total_fee_basis_points_128 = total_fee_basis_points as u128;
|
|
||||||
let input_amount = amount_128
|
|
||||||
.checked_mul(10_000)
|
|
||||||
.unwrap()
|
|
||||||
.checked_div(total_fee_basis_points_128 + 10_000)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let virtual_token_reserves = bonding_curve.virtual_token_reserves as u128;
|
|
||||||
let virtual_sol_reserves = bonding_curve.virtual_sol_reserves as u128;
|
|
||||||
let real_token_reserves = bonding_curve.real_token_reserves as u128;
|
|
||||||
|
|
||||||
let denominator = virtual_sol_reserves + input_amount;
|
|
||||||
|
|
||||||
let tokens_received = input_amount
|
|
||||||
.checked_mul(virtual_token_reserves)
|
|
||||||
.unwrap()
|
|
||||||
.checked_div(denominator)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
tokens_received.min(real_token_reserves) as u64
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn get_buy_amount_with_slippage(amount_sol: u64, slippage_basis_points: Option<u64>) -> u64 {
|
|
||||||
let slippage = slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE);
|
|
||||||
amount_sol + (amount_sol * slippage / 10000)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn get_token_price(virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 {
|
|
||||||
let v_sol = virtual_sol_reserves as f64 / 100_000_000.0;
|
|
||||||
let v_tokens = virtual_token_reserves as f64 / 100_000.0;
|
|
||||||
v_sol / v_tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn get_buy_price(amount: u64, trade_info: &TradeInfo) -> u64 {
|
|
||||||
if amount == 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let n: u128 = (trade_info.virtual_sol_reserves as u128) * (trade_info.virtual_token_reserves as u128);
|
|
||||||
let i: u128 = (trade_info.virtual_sol_reserves as u128) + (amount as u128);
|
|
||||||
let r: u128 = n / i + 1;
|
|
||||||
let s: u128 = (trade_info.virtual_token_reserves as u128) - r;
|
|
||||||
let s_u64 = s as u64;
|
|
||||||
|
|
||||||
s_u64.min(trade_info.real_token_reserves)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64 {
|
|
||||||
amount + (amount * basis_points) / 10000
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 {
|
|
||||||
amount - (amount * basis_points) / 10000
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
pub mod buy;
|
|
||||||
pub mod sell;
|
|
||||||
pub mod common;
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
use anyhow::anyhow;
|
|
||||||
use solana_sdk::{
|
|
||||||
compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::{Transaction, VersionedTransaction}
|
|
||||||
};
|
|
||||||
use solana_hash::Hash;
|
|
||||||
use spl_associated_token_account::get_associated_token_address;
|
|
||||||
use spl_token::instruction::close_account;
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
|
|
||||||
use std::{str::FromStr, time::Instant, sync::Arc};
|
|
||||||
|
|
||||||
use crate::{common::{PriorityFee, SolanaRpcClient}, instruction, swqos::FeeClient};
|
|
||||||
|
|
||||||
use super::common::{get_bonding_curve_account, get_creator_vault_pda, get_global_account};
|
|
||||||
|
|
||||||
async fn get_token_balance(rpc: &SolanaRpcClient, payer: &Keypair, mint: &Pubkey) -> Result<(u64, Pubkey), anyhow::Error> {
|
|
||||||
let ata = get_associated_token_address(&payer.pubkey(), 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"))?;
|
|
||||||
|
|
||||||
if balance_u64 == 0 {
|
|
||||||
return Err(anyhow!("Balance is 0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((balance_u64, ata))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn sell(
|
|
||||||
rpc: Arc<SolanaRpcClient>,
|
|
||||||
payer: Arc<Keypair>,
|
|
||||||
mint: Pubkey,
|
|
||||||
amount_token: Option<u64>,
|
|
||||||
priority_fee: PriorityFee,
|
|
||||||
) -> Result<(), anyhow::Error> {
|
|
||||||
let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token).await?;
|
|
||||||
let transaction = build_sell_transaction(rpc.clone(), payer.clone(), priority_fee, instructions).await?;
|
|
||||||
rpc.send_and_confirm_transaction(&transaction).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sell tokens by percentage
|
|
||||||
pub async fn sell_by_percent(
|
|
||||||
rpc: Arc<SolanaRpcClient>,
|
|
||||||
payer: Arc<Keypair>,
|
|
||||||
mint: Pubkey,
|
|
||||||
percent: u64,
|
|
||||||
priority_fee: PriorityFee,
|
|
||||||
) -> 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, Some(amount), priority_fee).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn sell_by_percent_with_tip(
|
|
||||||
rpc: Arc<SolanaRpcClient>,
|
|
||||||
fee_clients: Vec<Arc<FeeClient>>,
|
|
||||||
payer: Arc<Keypair>,
|
|
||||||
mint: Pubkey,
|
|
||||||
percent: u64,
|
|
||||||
priority_fee: PriorityFee,
|
|
||||||
) -> 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, fee_clients, payer, mint, Some(amount), priority_fee).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sell tokens using Jito
|
|
||||||
pub async fn sell_with_tip(
|
|
||||||
rpc: Arc<SolanaRpcClient>,
|
|
||||||
fee_clients: Vec<Arc<FeeClient>>,
|
|
||||||
payer: Arc<Keypair>,
|
|
||||||
mint: Pubkey,
|
|
||||||
amount_token: Option<u64>,
|
|
||||||
priority_fee: PriorityFee,
|
|
||||||
) -> Result<(), anyhow::Error> {
|
|
||||||
let start_time = Instant::now();
|
|
||||||
|
|
||||||
let mut transactions = vec![];
|
|
||||||
let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token).await?;
|
|
||||||
|
|
||||||
let recent_blockhash = rpc.get_latest_blockhash().await?;
|
|
||||||
for fee_client in fee_clients.clone() {
|
|
||||||
let payer = payer.clone();
|
|
||||||
let priority_fee = priority_fee.clone();
|
|
||||||
let tip_account = fee_client.get_tip_account().await.map_err(|e| anyhow!(e.to_string()))?;
|
|
||||||
let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?);
|
|
||||||
|
|
||||||
let transaction = build_sell_transaction_with_tip(tip_account, payer, priority_fee, instructions.clone(), recent_blockhash).await?;
|
|
||||||
transactions.push(transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut handles = vec![];
|
|
||||||
for i in 0..fee_clients.len() {
|
|
||||||
let fee_client = fee_clients[i].clone();
|
|
||||||
let transaction = transactions[i].clone();
|
|
||||||
let handle: JoinHandle<Result<(), anyhow::Error>> = tokio::spawn(async move {
|
|
||||||
fee_client.send_transaction(&transaction).await?;
|
|
||||||
println!("index: {}, Total Jito sell operation time: {:?}ms", i, start_time.elapsed().as_millis());
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Total Jito sell operation time: {:?}ms", start_time.elapsed().as_millis());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn build_sell_transaction(
|
|
||||||
rpc: Arc<SolanaRpcClient>,
|
|
||||||
payer: Arc<Keypair>,
|
|
||||||
priority_fee: PriorityFee,
|
|
||||||
build_instructions: Vec<Instruction>
|
|
||||||
) -> 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),
|
|
||||||
];
|
|
||||||
|
|
||||||
instructions.extend(build_instructions);
|
|
||||||
|
|
||||||
let recent_blockhash = rpc.get_latest_blockhash().await?;
|
|
||||||
let transaction = Transaction::new_signed_with_payer(
|
|
||||||
&instructions,
|
|
||||||
Some(&payer.pubkey()),
|
|
||||||
&[payer.as_ref()],
|
|
||||||
recent_blockhash,
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(transaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn build_sell_transaction_with_tip(
|
|
||||||
tip_account: Arc<Pubkey>,
|
|
||||||
payer: Arc<Keypair>,
|
|
||||||
priority_fee: PriorityFee,
|
|
||||||
build_instructions: Vec<Instruction>,
|
|
||||||
blockhash: Hash,
|
|
||||||
) -> Result<VersionedTransaction, anyhow::Error> {
|
|
||||||
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(priority_fee.sell_tip_fee),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
instructions.extend(build_instructions);
|
|
||||||
|
|
||||||
let v0_message: v0::Message =
|
|
||||||
v0::Message::try_compile(&payer.pubkey(), &instructions, &[], blockhash)?;
|
|
||||||
let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message);
|
|
||||||
|
|
||||||
let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?;
|
|
||||||
|
|
||||||
Ok(transaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn build_sell_instructions(
|
|
||||||
rpc: Arc<SolanaRpcClient>,
|
|
||||||
payer: Arc<Keypair>,
|
|
||||||
mint: Pubkey,
|
|
||||||
amount_token: Option<u64>,
|
|
||||||
) -> Result<Vec<Instruction>, anyhow::Error> {
|
|
||||||
let (balance_u64, ata) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?;
|
|
||||||
let amount = amount_token.unwrap_or(balance_u64);
|
|
||||||
|
|
||||||
if amount == 0 {
|
|
||||||
return Err(anyhow!("Amount cannot be zero"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let global_account = get_global_account(rpc.as_ref()).await?;
|
|
||||||
let (bonding_curve_account, bonding_curve_pda) = get_bonding_curve_account(&rpc, &mint).await?;
|
|
||||||
let creator_vault_pda = get_creator_vault_pda(&bonding_curve_account.creator).unwrap();
|
|
||||||
|
|
||||||
let instructions = vec![
|
|
||||||
instruction::sell(
|
|
||||||
payer.as_ref(),
|
|
||||||
&mint,
|
|
||||||
&bonding_curve_pda,
|
|
||||||
&creator_vault_pda,
|
|
||||||
&global_account.fee_recipient,
|
|
||||||
instruction::Sell {
|
|
||||||
_amount: amount,
|
|
||||||
_min_sol_output: 0,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
close_account(
|
|
||||||
&spl_token::ID,
|
|
||||||
&ata,
|
|
||||||
&payer.pubkey(),
|
|
||||||
&payer.pubkey(),
|
|
||||||
&[&payer.pubkey()],
|
|
||||||
)?
|
|
||||||
];
|
|
||||||
|
|
||||||
Ok(instructions)
|
|
||||||
}
|
|
||||||
@@ -1,462 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PostSubmitRequest {
|
|
||||||
#[prost(message, optional, tag = "1")]
|
|
||||||
pub transaction: ::core::option::Option<TransactionMessage>,
|
|
||||||
#[prost(bool, tag = "2")]
|
|
||||||
pub skip_pre_flight: bool,
|
|
||||||
#[prost(bool, optional, tag = "3")]
|
|
||||||
pub front_running_protection: ::core::option::Option<bool>,
|
|
||||||
#[prost(bool, optional, tag = "8")]
|
|
||||||
pub experimental_front_running_protection: ::core::option::Option<bool>,
|
|
||||||
#[prost(bool, optional, tag = "9")]
|
|
||||||
pub snipe_transaction: ::core::option::Option<bool>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PostSubmitRequestEntry {
|
|
||||||
#[prost(message, optional, tag = "1")]
|
|
||||||
pub transaction: ::core::option::Option<TransactionMessage>,
|
|
||||||
#[prost(bool, tag = "2")]
|
|
||||||
pub skip_pre_flight: bool,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PostSubmitBatchRequest {
|
|
||||||
#[prost(message, repeated, tag = "1")]
|
|
||||||
pub entries: ::prost::alloc::vec::Vec<PostSubmitRequestEntry>,
|
|
||||||
#[prost(enumeration = "SubmitStrategy", tag = "2")]
|
|
||||||
pub submit_strategy: i32,
|
|
||||||
#[prost(bool, optional, tag = "3")]
|
|
||||||
pub use_bundle: ::core::option::Option<bool>,
|
|
||||||
#[prost(bool, optional, tag = "4")]
|
|
||||||
pub front_running_protection: ::core::option::Option<bool>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PostSubmitBatchResponseEntry {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub signature: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub error: ::prost::alloc::string::String,
|
|
||||||
#[prost(bool, tag = "3")]
|
|
||||||
pub submitted: bool,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PostSubmitBatchResponse {
|
|
||||||
#[prost(message, repeated, tag = "1")]
|
|
||||||
pub transactions: ::prost::alloc::vec::Vec<PostSubmitBatchResponseEntry>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PostSubmitResponse {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub signature: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct TransactionMessage {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub content: ::prost::alloc::string::String,
|
|
||||||
#[prost(bool, tag = "2")]
|
|
||||||
pub is_cleanup: bool,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct TransactionMessageV2 {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub content: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum SubmitStrategy {
|
|
||||||
PUknown = 0,
|
|
||||||
PSubmitAll = 1,
|
|
||||||
PAbortOnFirstError = 2,
|
|
||||||
PWaitForConfirmation = 3,
|
|
||||||
}
|
|
||||||
impl SubmitStrategy {
|
|
||||||
/// String value of the enum field names used in the ProtoBuf definition.
|
|
||||||
///
|
|
||||||
/// The values are not transformed in any way and thus are considered stable
|
|
||||||
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
|
|
||||||
pub fn as_str_name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::PUknown => "P_UKNOWN",
|
|
||||||
Self::PSubmitAll => "P_SUBMIT_ALL",
|
|
||||||
Self::PAbortOnFirstError => "P_ABORT_ON_FIRST_ERROR",
|
|
||||||
Self::PWaitForConfirmation => "P_WAIT_FOR_CONFIRMATION",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
|
||||||
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
|
||||||
match value {
|
|
||||||
"P_UKNOWN" => Some(Self::PUknown),
|
|
||||||
"P_SUBMIT_ALL" => Some(Self::PSubmitAll),
|
|
||||||
"P_ABORT_ON_FIRST_ERROR" => Some(Self::PAbortOnFirstError),
|
|
||||||
"P_WAIT_FOR_CONFIRMATION" => Some(Self::PWaitForConfirmation),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated client implementations.
|
|
||||||
pub mod api_client {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
use tonic::codegen::http::Uri;
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ApiClient<T> {
|
|
||||||
inner: tonic::client::Grpc<T>,
|
|
||||||
}
|
|
||||||
impl ApiClient<tonic::transport::Channel> {
|
|
||||||
/// Attempt to create a new client by connecting to a given endpoint.
|
|
||||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
|
||||||
where
|
|
||||||
D: std::convert::TryInto<tonic::transport::Endpoint>,
|
|
||||||
D::Error: Into<StdError>,
|
|
||||||
{
|
|
||||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
|
||||||
Ok(Self::new(conn))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> ApiClient<T>
|
|
||||||
where
|
|
||||||
T: tonic::client::GrpcService<tonic::body::BoxBody>,
|
|
||||||
T::Error: Into<StdError>,
|
|
||||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
|
||||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
|
||||||
{
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::new(inner);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> ApiClient<InterceptedService<T, F>>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
T::ResponseBody: Default,
|
|
||||||
T: tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::BoxBody>,
|
|
||||||
Response = http::Response<
|
|
||||||
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
<T as tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::BoxBody>,
|
|
||||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
|
||||||
{
|
|
||||||
ApiClient::new(InterceptedService::new(inner, interceptor))
|
|
||||||
}
|
|
||||||
/// Compress requests with the given encoding.
|
|
||||||
///
|
|
||||||
/// This requires the server to support it otherwise it might respond with an
|
|
||||||
/// error.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.send_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Enable decompressing responses.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.accept_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_decoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_encoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub async fn post_submit_v2(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::PostSubmitRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::PostSubmitResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static("/api.Api/PostSubmitV2");
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut().insert(GrpcMethod::new("api.Api", "PostSubmitV2"));
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn post_submit_batch_v2(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::PostSubmitBatchRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::PostSubmitBatchResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/api.Api/PostSubmitBatchV2",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut().insert(GrpcMethod::new("api.Api", "PostSubmitBatchV2"));
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated server implementations.
|
|
||||||
pub mod api_server {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
/// Generated trait containing gRPC methods that should be implemented for use with ApiServer.
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Api: std::marker::Send + std::marker::Sync + 'static {
|
|
||||||
async fn post_submit_v2(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::PostSubmitRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::PostSubmitResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn post_submit_batch_v2(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::PostSubmitBatchRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::PostSubmitBatchResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ApiServer<T> {
|
|
||||||
inner: Arc<T>,
|
|
||||||
accept_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
send_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
max_decoding_message_size: Option<usize>,
|
|
||||||
max_encoding_message_size: Option<usize>,
|
|
||||||
}
|
|
||||||
impl<T> ApiServer<T> {
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
Self::from_arc(Arc::new(inner))
|
|
||||||
}
|
|
||||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: Default::default(),
|
|
||||||
send_compression_encodings: Default::default(),
|
|
||||||
max_decoding_message_size: None,
|
|
||||||
max_encoding_message_size: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> InterceptedService<Self, F>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
{
|
|
||||||
InterceptedService::new(Self::new(inner), interceptor)
|
|
||||||
}
|
|
||||||
/// Enable decompressing requests with the given encoding.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.accept_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Compress responses with the given encoding, if the client supports it.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.send_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_decoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_encoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for ApiServer<T>
|
|
||||||
where
|
|
||||||
T: Api,
|
|
||||||
B: Body + std::marker::Send + 'static,
|
|
||||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
|
||||||
{
|
|
||||||
type Response = http::Response<tonic::body::BoxBody>;
|
|
||||||
type Error = std::convert::Infallible;
|
|
||||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
|
||||||
fn poll_ready(
|
|
||||||
&mut self,
|
|
||||||
_cx: &mut Context<'_>,
|
|
||||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
|
||||||
match req.uri().path() {
|
|
||||||
"/api.Api/PostSubmitV2" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct PostSubmitV2Svc<T: Api>(pub Arc<T>);
|
|
||||||
impl<T: Api> tonic::server::UnaryService<super::PostSubmitRequest>
|
|
||||||
for PostSubmitV2Svc<T> {
|
|
||||||
type Response = super::PostSubmitResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::PostSubmitRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Api>::post_submit_v2(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = PostSubmitV2Svc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/api.Api/PostSubmitBatchV2" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct PostSubmitBatchV2Svc<T: Api>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: Api,
|
|
||||||
> tonic::server::UnaryService<super::PostSubmitBatchRequest>
|
|
||||||
for PostSubmitBatchV2Svc<T> {
|
|
||||||
type Response = super::PostSubmitBatchResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::PostSubmitBatchRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Api>::post_submit_batch_v2(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = PostSubmitBatchV2Svc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
Box::pin(async move {
|
|
||||||
let mut response = http::Response::new(empty_body());
|
|
||||||
let headers = response.headers_mut();
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
tonic::Status::GRPC_STATUS,
|
|
||||||
(tonic::Code::Unimplemented as i32).into(),
|
|
||||||
);
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
http::header::CONTENT_TYPE,
|
|
||||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
|
||||||
);
|
|
||||||
Ok(response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Clone for ApiServer<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: self.accept_compression_encodings,
|
|
||||||
send_compression_encodings: self.send_compression_encodings,
|
|
||||||
max_decoding_message_size: self.max_decoding_message_size,
|
|
||||||
max_encoding_message_size: self.max_encoding_message_size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated gRPC service name
|
|
||||||
pub const SERVICE_NAME: &str = "api.Api";
|
|
||||||
impl<T> tonic::server::NamedService for ApiServer<T> {
|
|
||||||
const NAME: &'static str = SERVICE_NAME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
use bincode::serialize;
|
|
||||||
use serde_json::json;
|
|
||||||
use solana_client::rpc_client::SerializableTransaction;
|
|
||||||
use solana_sdk::signature::Signature;
|
|
||||||
use solana_sdk::transaction::Transaction;
|
|
||||||
use solana_transaction_status::{TransactionConfirmationStatus, UiTransactionEncoding};
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use tokio::time::sleep;
|
|
||||||
use crate::common::types::SolanaRpcClient;
|
|
||||||
use anyhow::Result;
|
|
||||||
use base64::Engine;
|
|
||||||
use base64::engine::general_purpose::STANDARD;
|
|
||||||
use reqwest::Client;
|
|
||||||
|
|
||||||
pub async fn poll_transaction_confirmation(rpc: &SolanaRpcClient, txt_sig: Signature) -> Result<Signature> {
|
|
||||||
// 15 second timeout
|
|
||||||
let timeout: Duration = Duration::from_secs(5);
|
|
||||||
// 5 second retry interval
|
|
||||||
let interval: Duration = Duration::from_millis(300);
|
|
||||||
let start: Instant = Instant::now();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if start.elapsed() >= timeout {
|
|
||||||
return Err(anyhow::anyhow!("Transaction {}'s confirmation timed out", txt_sig));
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = rpc.get_signature_statuses(&[txt_sig]).await?;
|
|
||||||
|
|
||||||
match status.value[0].clone() {
|
|
||||||
Some(status) => {
|
|
||||||
if status.err.is_none()
|
|
||||||
&& (status.confirmation_status == Some(TransactionConfirmationStatus::Confirmed)
|
|
||||||
|| status.confirmation_status == Some(TransactionConfirmationStatus::Finalized))
|
|
||||||
{
|
|
||||||
return Ok(txt_sig);
|
|
||||||
}
|
|
||||||
if status.err.is_some() {
|
|
||||||
return Err(anyhow::anyhow!(status.err.unwrap()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
sleep(interval).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &str, transaction: &Transaction) -> Result<Signature, anyhow::Error> {
|
|
||||||
// 序列化交易
|
|
||||||
let serialized = bincode::serialize(transaction)
|
|
||||||
.map_err(|e| anyhow::anyhow!("序列化交易失败: {}", e))?;
|
|
||||||
|
|
||||||
// Base64编码
|
|
||||||
let encoded = STANDARD.encode(serialized);
|
|
||||||
|
|
||||||
let request_data = json!({
|
|
||||||
"transaction": {
|
|
||||||
"content": encoded
|
|
||||||
},
|
|
||||||
"frontRunningProtection": true
|
|
||||||
});
|
|
||||||
|
|
||||||
let url = format!("{}/api/v2/submit", endpoint);
|
|
||||||
let response = client
|
|
||||||
.post(url)
|
|
||||||
.header("Authorization", auth_token)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.json(&request_data)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| anyhow::anyhow!("请求失败: {}", e))?;
|
|
||||||
|
|
||||||
let resp = response.json::<serde_json::Value>().await
|
|
||||||
.map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?;
|
|
||||||
|
|
||||||
if let Some(reason) = resp["reason"].as_str() {
|
|
||||||
return Err(anyhow::anyhow!(reason.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let signature = resp["signature"].as_str()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("响应中缺少signature字段"))?;
|
|
||||||
|
|
||||||
let signature = Signature::from_str(signature)
|
|
||||||
.map_err(|e| anyhow::anyhow!("无效的签名: {}", e))?;
|
|
||||||
|
|
||||||
Ok(signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn serialize_and_encode(
|
|
||||||
transaction: &Vec<u8>,
|
|
||||||
encoding: UiTransactionEncoding,
|
|
||||||
) -> Result<String> {
|
|
||||||
let serialized = match encoding {
|
|
||||||
UiTransactionEncoding::Base58 => bs58::encode(transaction).into_string(),
|
|
||||||
UiTransactionEncoding::Base64 => STANDARD.encode(transaction),
|
|
||||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
|
||||||
};
|
|
||||||
Ok(serialized)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn serialize_transaction_and_encode(
|
|
||||||
transaction: &impl SerializableTransaction,
|
|
||||||
encoding: UiTransactionEncoding,
|
|
||||||
) -> Result<String> {
|
|
||||||
let serialized_tx = serialize(transaction)?;
|
|
||||||
let serialized = match encoding {
|
|
||||||
UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(),
|
|
||||||
UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx),
|
|
||||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
|
||||||
};
|
|
||||||
Ok(serialized)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn serialize_smart_transaction_and_encode(
|
|
||||||
transaction: &impl SerializableTransaction,
|
|
||||||
encoding: UiTransactionEncoding,
|
|
||||||
) -> Result<(String, Signature)> {
|
|
||||||
let signature = transaction.get_signature();
|
|
||||||
let serialized_tx = serialize(transaction)?;
|
|
||||||
let serialized = match encoding {
|
|
||||||
UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(),
|
|
||||||
UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx),
|
|
||||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
|
||||||
};
|
|
||||||
Ok((serialized, *signature))
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Bundle {
|
|
||||||
#[prost(message, optional, tag = "2")]
|
|
||||||
pub header: ::core::option::Option<super::shared::Header>,
|
|
||||||
#[prost(message, repeated, tag = "3")]
|
|
||||||
pub packets: ::prost::alloc::vec::Vec<super::packet::Packet>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct BundleUuid {
|
|
||||||
#[prost(message, optional, tag = "1")]
|
|
||||||
pub bundle: ::core::option::Option<Bundle>,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub uuid: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
/// Indicates the bundle was accepted and forwarded to a validator.
|
|
||||||
/// NOTE: A single bundle may have multiple events emitted if forwarded to many validators.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Accepted {
|
|
||||||
/// Slot at which bundle was forwarded.
|
|
||||||
#[prost(uint64, tag = "1")]
|
|
||||||
pub slot: u64,
|
|
||||||
/// Validator identity bundle was forwarded to.
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub validator_identity: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
/// Indicates the bundle was dropped and therefore not forwarded to any validator.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Rejected {
|
|
||||||
#[prost(oneof = "rejected::Reason", tags = "1, 2, 3, 4, 5")]
|
|
||||||
pub reason: ::core::option::Option<rejected::Reason>,
|
|
||||||
}
|
|
||||||
/// Nested message and enum types in `Rejected`.
|
|
||||||
pub mod rejected {
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
|
||||||
pub enum Reason {
|
|
||||||
#[prost(message, tag = "1")]
|
|
||||||
StateAuctionBidRejected(super::StateAuctionBidRejected),
|
|
||||||
#[prost(message, tag = "2")]
|
|
||||||
WinningBatchBidRejected(super::WinningBatchBidRejected),
|
|
||||||
#[prost(message, tag = "3")]
|
|
||||||
SimulationFailure(super::SimulationFailure),
|
|
||||||
#[prost(message, tag = "4")]
|
|
||||||
InternalError(super::InternalError),
|
|
||||||
#[prost(message, tag = "5")]
|
|
||||||
DroppedBundle(super::DroppedBundle),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Indicates the bundle's bid was high enough to win its state auction.
|
|
||||||
/// However, not high enough relative to other state auction winners and therefore excluded from being forwarded.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct WinningBatchBidRejected {
|
|
||||||
/// Auction's unique identifier.
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub auction_id: ::prost::alloc::string::String,
|
|
||||||
/// Bundle's simulated bid.
|
|
||||||
#[prost(uint64, tag = "2")]
|
|
||||||
pub simulated_bid_lamports: u64,
|
|
||||||
#[prost(string, optional, tag = "3")]
|
|
||||||
pub msg: ::core::option::Option<::prost::alloc::string::String>,
|
|
||||||
}
|
|
||||||
/// Indicates the bundle's bid was __not__ high enough to be included in its state auction's set of winners.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct StateAuctionBidRejected {
|
|
||||||
/// Auction's unique identifier.
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub auction_id: ::prost::alloc::string::String,
|
|
||||||
/// Bundle's simulated bid.
|
|
||||||
#[prost(uint64, tag = "2")]
|
|
||||||
pub simulated_bid_lamports: u64,
|
|
||||||
#[prost(string, optional, tag = "3")]
|
|
||||||
pub msg: ::core::option::Option<::prost::alloc::string::String>,
|
|
||||||
}
|
|
||||||
/// Bundle dropped due to simulation failure.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct SimulationFailure {
|
|
||||||
/// Signature of the offending transaction.
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub tx_signature: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, optional, tag = "2")]
|
|
||||||
pub msg: ::core::option::Option<::prost::alloc::string::String>,
|
|
||||||
}
|
|
||||||
/// Bundle dropped due to an internal error.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct InternalError {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub msg: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
/// Bundle dropped (e.g. because no leader upcoming)
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct DroppedBundle {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub msg: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Finalized {}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Processed {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub validator_identity: ::prost::alloc::string::String,
|
|
||||||
#[prost(uint64, tag = "2")]
|
|
||||||
pub slot: u64,
|
|
||||||
/// / Index within the block.
|
|
||||||
#[prost(uint64, tag = "3")]
|
|
||||||
pub bundle_index: u64,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Dropped {
|
|
||||||
#[prost(enumeration = "DroppedReason", tag = "1")]
|
|
||||||
pub reason: i32,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct BundleResult {
|
|
||||||
/// Bundle's Uuid.
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub bundle_id: ::prost::alloc::string::String,
|
|
||||||
#[prost(oneof = "bundle_result::Result", tags = "2, 3, 4, 5, 6")]
|
|
||||||
pub result: ::core::option::Option<bundle_result::Result>,
|
|
||||||
}
|
|
||||||
/// Nested message and enum types in `BundleResult`.
|
|
||||||
pub mod bundle_result {
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
|
||||||
pub enum Result {
|
|
||||||
/// Indicated accepted by the block-engine and forwarded to a jito-solana validator.
|
|
||||||
#[prost(message, tag = "2")]
|
|
||||||
Accepted(super::Accepted),
|
|
||||||
/// Rejected by the block-engine.
|
|
||||||
#[prost(message, tag = "3")]
|
|
||||||
Rejected(super::Rejected),
|
|
||||||
/// Reached finalized commitment level.
|
|
||||||
#[prost(message, tag = "4")]
|
|
||||||
Finalized(super::Finalized),
|
|
||||||
/// Reached a processed commitment level.
|
|
||||||
#[prost(message, tag = "5")]
|
|
||||||
Processed(super::Processed),
|
|
||||||
/// Was accepted and forwarded by the block-engine but never landed on-chain.
|
|
||||||
#[prost(message, tag = "6")]
|
|
||||||
Dropped(super::Dropped),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum DroppedReason {
|
|
||||||
BlockhashExpired = 0,
|
|
||||||
/// One or more transactions in the bundle landed on-chain, invalidating the bundle.
|
|
||||||
PartiallyProcessed = 1,
|
|
||||||
/// This indicates bundle was processed but not finalized. This could occur during forks.
|
|
||||||
NotFinalized = 2,
|
|
||||||
}
|
|
||||||
impl DroppedReason {
|
|
||||||
/// String value of the enum field names used in the ProtoBuf definition.
|
|
||||||
///
|
|
||||||
/// The values are not transformed in any way and thus are considered stable
|
|
||||||
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
|
|
||||||
pub fn as_str_name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::BlockhashExpired => "BlockhashExpired",
|
|
||||||
Self::PartiallyProcessed => "PartiallyProcessed",
|
|
||||||
Self::NotFinalized => "NotFinalized",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
|
||||||
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
|
||||||
match value {
|
|
||||||
"BlockhashExpired" => Some(Self::BlockhashExpired),
|
|
||||||
"PartiallyProcessed" => Some(Self::PartiallyProcessed),
|
|
||||||
"NotFinalized" => Some(Self::NotFinalized),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
use std::{
|
|
||||||
cmp::min,
|
|
||||||
net::{AddrParseError, IpAddr, Ipv4Addr, SocketAddr},
|
|
||||||
str::FromStr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use bincode::serialize;
|
|
||||||
use solana_perf::packet::{Packet, PacketBatch, PACKET_DATA_SIZE};
|
|
||||||
use solana_sdk::{
|
|
||||||
packet::{Meta, PacketFlags},
|
|
||||||
transaction::VersionedTransaction,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::swqos::jito_grpc::packet::{
|
|
||||||
Meta as ProtoMeta, Packet as ProtoPacket, PacketBatch as ProtoPacketBatch,
|
|
||||||
PacketFlags as ProtoPacketFlags,
|
|
||||||
};
|
|
||||||
use crate::swqos::jito_grpc::shared::Socket;
|
|
||||||
|
|
||||||
/// Converts a Solana packet to a protobuf packet
|
|
||||||
/// NOTE: the packet.data() function will filter packets marked for discard
|
|
||||||
pub fn packet_to_proto_packet(p: &Packet) -> Option<ProtoPacket> {
|
|
||||||
Some(ProtoPacket {
|
|
||||||
data: p.data(..)?.to_vec(),
|
|
||||||
meta: Some(ProtoMeta {
|
|
||||||
size: p.meta().size as u64,
|
|
||||||
addr: p.meta().addr.to_string(),
|
|
||||||
port: p.meta().port as u32,
|
|
||||||
flags: Some(ProtoPacketFlags {
|
|
||||||
discard: p.meta().discard(),
|
|
||||||
forwarded: p.meta().forwarded(),
|
|
||||||
repair: p.meta().repair(),
|
|
||||||
simple_vote_tx: p.meta().is_simple_vote_tx(),
|
|
||||||
tracer_packet: p.meta().is_perf_track_packet(),
|
|
||||||
from_staked_node: p.meta().is_from_staked_node(),
|
|
||||||
}),
|
|
||||||
sender_stake: 0,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn packet_batches_to_proto_packets(
|
|
||||||
batches: &[PacketBatch],
|
|
||||||
) -> impl Iterator<Item = ProtoPacket> + '_ {
|
|
||||||
batches
|
|
||||||
.iter()
|
|
||||||
.flat_map(|b| b.iter().filter_map(packet_to_proto_packet))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// converts from a protobuf packet to packet
|
|
||||||
pub fn proto_packet_to_packet(p: &ProtoPacket) -> Packet {
|
|
||||||
let mut data = [0u8; PACKET_DATA_SIZE];
|
|
||||||
let copy_len = min(data.len(), p.data.len());
|
|
||||||
data[..copy_len].copy_from_slice(&p.data[..copy_len]);
|
|
||||||
let mut packet = Packet::new(data, Meta::default());
|
|
||||||
if let Some(meta) = &p.meta {
|
|
||||||
packet.meta_mut().size = meta.size as usize;
|
|
||||||
packet.meta_mut().addr = meta
|
|
||||||
.addr
|
|
||||||
.parse()
|
|
||||||
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
|
||||||
packet.meta_mut().port = meta.port as u16;
|
|
||||||
if let Some(flags) = &meta.flags {
|
|
||||||
if flags.simple_vote_tx {
|
|
||||||
packet.meta_mut().flags.insert(PacketFlags::SIMPLE_VOTE_TX);
|
|
||||||
}
|
|
||||||
if flags.forwarded {
|
|
||||||
packet.meta_mut().flags.insert(PacketFlags::FORWARDED);
|
|
||||||
}
|
|
||||||
if flags.tracer_packet {
|
|
||||||
packet.meta_mut().flags.insert(PacketFlags::PERF_TRACK_PACKET);
|
|
||||||
}
|
|
||||||
if flags.repair {
|
|
||||||
packet.meta_mut().flags.insert(PacketFlags::REPAIR);
|
|
||||||
}
|
|
||||||
if flags.discard {
|
|
||||||
packet.meta_mut().flags.insert(PacketFlags::DISCARD);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
packet
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn proto_packet_batch_to_packets(
|
|
||||||
packet_batch: ProtoPacketBatch,
|
|
||||||
) -> impl Iterator<Item = Packet> {
|
|
||||||
packet_batch
|
|
||||||
.packets
|
|
||||||
.into_iter()
|
|
||||||
.map(|proto_packet| proto_packet_to_packet(&proto_packet))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts a protobuf packet to a VersionedTransaction
|
|
||||||
pub fn versioned_tx_from_packet(p: &ProtoPacket) -> Option<VersionedTransaction> {
|
|
||||||
let mut data = [0; PACKET_DATA_SIZE];
|
|
||||||
let copy_len = min(data.len(), p.data.len());
|
|
||||||
data[..copy_len].copy_from_slice(&p.data[..copy_len]);
|
|
||||||
let mut packet = Packet::new(data, Default::default());
|
|
||||||
if let Some(meta) = &p.meta {
|
|
||||||
packet.meta_mut().size = meta.size as usize;
|
|
||||||
}
|
|
||||||
packet.deserialize_slice(..).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Coverts a VersionedTransaction to packet
|
|
||||||
pub fn packet_from_versioned_tx(tx: VersionedTransaction) -> Packet {
|
|
||||||
let tx_data = serialize(&tx).expect("serializes");
|
|
||||||
let mut data = [0; PACKET_DATA_SIZE];
|
|
||||||
let copy_len = min(tx_data.len(), data.len());
|
|
||||||
data[..copy_len].copy_from_slice(&tx_data[..copy_len]);
|
|
||||||
let mut packet = Packet::new(data, Default::default());
|
|
||||||
packet.meta_mut().size = copy_len;
|
|
||||||
packet
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts a VersionedTransaction to a protobuf packet
|
|
||||||
pub fn proto_packet_from_versioned_tx(tx: &VersionedTransaction) -> ProtoPacket {
|
|
||||||
let data = serialize(tx).expect("serializes");
|
|
||||||
let size = data.len() as u64;
|
|
||||||
ProtoPacket {
|
|
||||||
data,
|
|
||||||
meta: Some(ProtoMeta {
|
|
||||||
size,
|
|
||||||
addr: "".to_string(),
|
|
||||||
port: 0,
|
|
||||||
flags: None,
|
|
||||||
sender_stake: 0,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts a GRPC Socket to stdlib SocketAddr
|
|
||||||
impl TryFrom<&Socket> for SocketAddr {
|
|
||||||
type Error = AddrParseError;
|
|
||||||
|
|
||||||
fn try_from(value: &Socket) -> Result<Self, Self::Error> {
|
|
||||||
IpAddr::from_str(&value.ip).map(|ip| SocketAddr::new(ip, value.port as u16))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[cfg(test)]
|
|
||||||
// mod tests {
|
|
||||||
// use solana_perf::test_tx::test_tx;
|
|
||||||
// use solana_sdk::transaction::VersionedTransaction;
|
|
||||||
|
|
||||||
// use crate::convert::{proto_packet_from_versioned_tx, versioned_tx_from_packet};
|
|
||||||
|
|
||||||
// #[test]
|
|
||||||
// fn test_proto_to_packet() {
|
|
||||||
// let tx_before = VersionedTransaction::from(test_tx());
|
|
||||||
// let tx_after = versioned_tx_from_packet(&proto_packet_from_versioned_tx(&tx_before))
|
|
||||||
// .expect("tx_after");
|
|
||||||
|
|
||||||
// assert_eq!(tx_before, tx_after);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
pub mod bundle;
|
|
||||||
pub mod packet;
|
|
||||||
pub mod searcher;
|
|
||||||
pub mod shared;
|
|
||||||
pub mod convert;
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PacketBatch {
|
|
||||||
#[prost(message, repeated, tag = "1")]
|
|
||||||
pub packets: ::prost::alloc::vec::Vec<Packet>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Packet {
|
|
||||||
#[prost(bytes = "vec", tag = "1")]
|
|
||||||
pub data: ::prost::alloc::vec::Vec<u8>,
|
|
||||||
#[prost(message, optional, tag = "2")]
|
|
||||||
pub meta: ::core::option::Option<Meta>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Meta {
|
|
||||||
#[prost(uint64, tag = "1")]
|
|
||||||
pub size: u64,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub addr: ::prost::alloc::string::String,
|
|
||||||
#[prost(uint32, tag = "3")]
|
|
||||||
pub port: u32,
|
|
||||||
#[prost(message, optional, tag = "4")]
|
|
||||||
pub flags: ::core::option::Option<PacketFlags>,
|
|
||||||
#[prost(uint64, tag = "5")]
|
|
||||||
pub sender_stake: u64,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PacketFlags {
|
|
||||||
#[prost(bool, tag = "1")]
|
|
||||||
pub discard: bool,
|
|
||||||
#[prost(bool, tag = "2")]
|
|
||||||
pub forwarded: bool,
|
|
||||||
#[prost(bool, tag = "3")]
|
|
||||||
pub repair: bool,
|
|
||||||
#[prost(bool, tag = "4")]
|
|
||||||
pub simple_vote_tx: bool,
|
|
||||||
#[prost(bool, tag = "5")]
|
|
||||||
pub tracer_packet: bool,
|
|
||||||
#[prost(bool, tag = "6")]
|
|
||||||
pub from_staked_node: bool,
|
|
||||||
}
|
|
||||||
@@ -1,363 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct SlotList {
|
|
||||||
#[prost(uint64, repeated, tag = "1")]
|
|
||||||
pub slots: ::prost::alloc::vec::Vec<u64>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct ConnectedLeadersResponse {
|
|
||||||
/// Mapping of validator pubkey to leader slots for the current epoch.
|
|
||||||
#[prost(map = "string, message", tag = "1")]
|
|
||||||
pub connected_validators: ::std::collections::HashMap<
|
|
||||||
::prost::alloc::string::String,
|
|
||||||
SlotList,
|
|
||||||
>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct SendBundleRequest {
|
|
||||||
#[prost(message, optional, tag = "1")]
|
|
||||||
pub bundle: ::core::option::Option<super::bundle::Bundle>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct SendBundleResponse {
|
|
||||||
/// server uuid for the bundle
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub uuid: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct NextScheduledLeaderRequest {
|
|
||||||
/// Defaults to the currently connected region if no region provided.
|
|
||||||
#[prost(string, repeated, tag = "1")]
|
|
||||||
pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct NextScheduledLeaderResponse {
|
|
||||||
/// the current slot the backend is on
|
|
||||||
#[prost(uint64, tag = "1")]
|
|
||||||
pub current_slot: u64,
|
|
||||||
/// the slot of the next leader
|
|
||||||
#[prost(uint64, tag = "2")]
|
|
||||||
pub next_leader_slot: u64,
|
|
||||||
/// the identity pubkey (base58) of the next leader
|
|
||||||
#[prost(string, tag = "3")]
|
|
||||||
pub next_leader_identity: ::prost::alloc::string::String,
|
|
||||||
/// the block engine region of the next leader
|
|
||||||
#[prost(string, tag = "4")]
|
|
||||||
pub next_leader_region: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct ConnectedLeadersRequest {}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct ConnectedLeadersRegionedRequest {
|
|
||||||
/// Defaults to the currently connected region if no region provided.
|
|
||||||
#[prost(string, repeated, tag = "1")]
|
|
||||||
pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct ConnectedLeadersRegionedResponse {
|
|
||||||
#[prost(map = "string, message", tag = "1")]
|
|
||||||
pub connected_validators: ::std::collections::HashMap<
|
|
||||||
::prost::alloc::string::String,
|
|
||||||
ConnectedLeadersResponse,
|
|
||||||
>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct GetTipAccountsRequest {}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct GetTipAccountsResponse {
|
|
||||||
#[prost(string, repeated, tag = "1")]
|
|
||||||
pub accounts: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct SubscribeBundleResultsRequest {}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct GetRegionsRequest {}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct GetRegionsResponse {
|
|
||||||
/// The region the client is currently connected to
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub current_region: ::prost::alloc::string::String,
|
|
||||||
/// Regions that are online and ready for connections
|
|
||||||
/// All regions: <https://jito-labs.gitbook.io/mev/systems/connecting/mainnet>
|
|
||||||
#[prost(string, repeated, tag = "2")]
|
|
||||||
pub available_regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
|
||||||
}
|
|
||||||
/// Generated client implementations.
|
|
||||||
pub mod searcher_service_client {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
use tonic::codegen::http::Uri;
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SearcherServiceClient<T> {
|
|
||||||
inner: tonic::client::Grpc<T>,
|
|
||||||
}
|
|
||||||
impl SearcherServiceClient<tonic::transport::Channel> {
|
|
||||||
/// Attempt to create a new client by connecting to a given endpoint.
|
|
||||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
|
||||||
where
|
|
||||||
D: TryInto<tonic::transport::Endpoint>,
|
|
||||||
D::Error: Into<StdError>,
|
|
||||||
{
|
|
||||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
|
||||||
Ok(Self::new(conn))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> SearcherServiceClient<T>
|
|
||||||
where
|
|
||||||
T: tonic::client::GrpcService<tonic::body::BoxBody>,
|
|
||||||
T::Error: Into<StdError>,
|
|
||||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
|
||||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
|
||||||
{
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::new(inner);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> SearcherServiceClient<InterceptedService<T, F>>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
T::ResponseBody: Default,
|
|
||||||
T: tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::BoxBody>,
|
|
||||||
Response = http::Response<
|
|
||||||
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
<T as tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::BoxBody>,
|
|
||||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
|
||||||
{
|
|
||||||
SearcherServiceClient::new(InterceptedService::new(inner, interceptor))
|
|
||||||
}
|
|
||||||
/// Compress requests with the given encoding.
|
|
||||||
///
|
|
||||||
/// This requires the server to support it otherwise it might respond with an
|
|
||||||
/// error.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.send_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Enable decompressing responses.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.accept_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_decoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_encoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Searchers can invoke this endpoint to subscribe to their respective bundle results.
|
|
||||||
/// A success result would indicate the bundle won its state auction and was submitted to the validator.
|
|
||||||
pub async fn subscribe_bundle_results(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::SubscribeBundleResultsRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<tonic::codec::Streaming<super::super::bundle::BundleResult>>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/searcher.SearcherService/SubscribeBundleResults",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new("searcher.SearcherService", "SubscribeBundleResults"),
|
|
||||||
);
|
|
||||||
self.inner.server_streaming(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn send_bundle(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::SendBundleRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::SendBundleResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/searcher.SearcherService/SendBundle",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(GrpcMethod::new("searcher.SearcherService", "SendBundle"));
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
/// Returns the next scheduled leader connected to the block engine.
|
|
||||||
pub async fn get_next_scheduled_leader(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::NextScheduledLeaderRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::NextScheduledLeaderResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/searcher.SearcherService/GetNextScheduledLeader",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new("searcher.SearcherService", "GetNextScheduledLeader"),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
/// Returns leader slots for connected jito validators during the current epoch. Only returns data for this region.
|
|
||||||
pub async fn get_connected_leaders(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::ConnectedLeadersRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::ConnectedLeadersResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/searcher.SearcherService/GetConnectedLeaders",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new("searcher.SearcherService", "GetConnectedLeaders"),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
/// Returns leader slots for connected jito validators during the current epoch.
|
|
||||||
pub async fn get_connected_leaders_regioned(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::ConnectedLeadersRegionedRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::ConnectedLeadersRegionedResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/searcher.SearcherService/GetConnectedLeadersRegioned",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new(
|
|
||||||
"searcher.SearcherService",
|
|
||||||
"GetConnectedLeadersRegioned",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
/// Returns the tip accounts searchers shall transfer funds to for the leader to claim.
|
|
||||||
pub async fn get_tip_accounts(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::GetTipAccountsRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::GetTipAccountsResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/searcher.SearcherService/GetTipAccounts",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(GrpcMethod::new("searcher.SearcherService", "GetTipAccounts"));
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
/// Returns region the client is directly connected to, along with all available regions
|
|
||||||
pub async fn get_regions(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::GetRegionsRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::GetRegionsResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/searcher.SearcherService/GetRegions",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(GrpcMethod::new("searcher.SearcherService", "GetRegions"));
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Header {
|
|
||||||
#[prost(message, optional, tag = "1")]
|
|
||||||
pub ts: ::core::option::Option<::prost_types::Timestamp>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Heartbeat {
|
|
||||||
#[prost(uint64, tag = "1")]
|
|
||||||
pub count: u64,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Socket {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub ip: ::prost::alloc::string::String,
|
|
||||||
#[prost(int64, tag = "2")]
|
|
||||||
pub port: i64,
|
|
||||||
}
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
use api::api_client::ApiClient;
|
|
||||||
use common::{poll_transaction_confirmation, serialize_smart_transaction_and_encode};
|
|
||||||
use crate::swqos::jito_grpc::searcher::searcher_service_client::SearcherServiceClient;
|
|
||||||
use reqwest::Client;
|
|
||||||
use searcher_client::{get_searcher_client_no_auth, send_bundle_with_confirmation};
|
|
||||||
use serde_json::json;
|
|
||||||
use tonic::transport::Channel;
|
|
||||||
use yellowstone_grpc_client::Interceptor;
|
|
||||||
use std::{sync::Arc, time::Instant};
|
|
||||||
use tokio::sync::{Mutex, RwLock};
|
|
||||||
|
|
||||||
use solana_sdk::signature::Signature;
|
|
||||||
|
|
||||||
use std::str::FromStr;
|
|
||||||
use rustls::crypto::{ring::default_provider, CryptoProvider};
|
|
||||||
|
|
||||||
use tonic::{service::interceptor::InterceptedService, transport::Uri, Status};
|
|
||||||
use std::time::Duration;
|
|
||||||
use solana_transaction_status::UiTransactionEncoding;
|
|
||||||
use tonic::transport::ClientTlsConfig;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use rand::{rng, seq::{IndexedRandom, IteratorRandom}};
|
|
||||||
use solana_sdk::transaction::VersionedTransaction;
|
|
||||||
|
|
||||||
use crate::{common::SolanaRpcClient, constants::accounts::{JITO_TIP_ACCOUNTS, NEXTBLOCK_TIP_ACCOUNTS, ZEROSLOT_TIP_ACCOUNTS}};
|
|
||||||
|
|
||||||
pub mod common;
|
|
||||||
pub mod searcher_client;
|
|
||||||
pub mod api;
|
|
||||||
pub mod jito_grpc;
|
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
|
||||||
static ref TIP_ACCOUNT_CACHE: RwLock<Vec<String>> = RwLock::new(Vec::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum ClientType {
|
|
||||||
Jito,
|
|
||||||
NextBlock,
|
|
||||||
ZeroSlot,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type FeeClient = dyn FeeClientTrait + Send + Sync + 'static;
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
pub trait FeeClientTrait {
|
|
||||||
async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature>;
|
|
||||||
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>>;
|
|
||||||
async fn get_tip_account(&self) -> Result<String>;
|
|
||||||
async fn get_client_type(&self) -> ClientType;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct JitoClient {
|
|
||||||
pub rpc_client: Arc<SolanaRpcClient>,
|
|
||||||
pub searcher_client: Arc<Mutex<SearcherServiceClient<Channel>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl FeeClientTrait for JitoClient {
|
|
||||||
async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
|
|
||||||
self.send_bundle_with_confirmation(&vec![transaction.clone()]).await?.first().cloned().ok_or(anyhow!("Failed to send transaction"))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
|
|
||||||
self.send_bundle_with_confirmation(transactions).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_tip_account(&self) -> Result<String, anyhow::Error> {
|
|
||||||
if let Some(acc) = JITO_TIP_ACCOUNTS.iter().choose(&mut rng()) {
|
|
||||||
Ok(acc.to_string())
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("no valid tip accounts found"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_client_type(&self) -> ClientType {
|
|
||||||
ClientType::Jito
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JitoClient {
|
|
||||||
pub async fn new(rpc_url: String, block_engine_url: String) -> Result<Self> {
|
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
|
||||||
let searcher_client = get_searcher_client_no_auth(block_engine_url.as_str()).await?;
|
|
||||||
Ok(Self { rpc_client: Arc::new(rpc_client), searcher_client: Arc::new(Mutex::new(searcher_client)) })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_bundle_with_confirmation(
|
|
||||||
&self,
|
|
||||||
transactions: &Vec<VersionedTransaction>,
|
|
||||||
) -> Result<Vec<Signature>, anyhow::Error> {
|
|
||||||
send_bundle_with_confirmation(self.rpc_client.clone(), &transactions, self.searcher_client.clone()).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_bundle_no_wait(
|
|
||||||
&self,
|
|
||||||
transactions: &Vec<VersionedTransaction>,
|
|
||||||
) -> Result<Vec<Signature>, anyhow::Error> {
|
|
||||||
searcher_client::send_bundle_no_wait(&transactions, self.searcher_client.clone()).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct MyInterceptor {
|
|
||||||
auth_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MyInterceptor {
|
|
||||||
pub fn new(auth_token: String) -> Self {
|
|
||||||
Self { auth_token }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Interceptor for MyInterceptor {
|
|
||||||
fn call(&mut self, mut request: tonic::Request<()>) -> Result<tonic::Request<()>, Status> {
|
|
||||||
request.metadata_mut().insert(
|
|
||||||
"authorization",
|
|
||||||
tonic::metadata::MetadataValue::from_str(&self.auth_token)
|
|
||||||
.map_err(|_| Status::invalid_argument("Invalid auth token"))?
|
|
||||||
);
|
|
||||||
Ok(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct NextBlockClient {
|
|
||||||
pub rpc_client: Arc<SolanaRpcClient>,
|
|
||||||
pub client: ApiClient<InterceptedService<Channel, MyInterceptor>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl FeeClientTrait for NextBlockClient {
|
|
||||||
async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
|
|
||||||
self.send_transaction(transaction).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
|
|
||||||
self.send_transactions(transactions).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_tip_account(&self) -> Result<String> {
|
|
||||||
let tip_account = self.get_tip_account().await?;
|
|
||||||
Ok(tip_account)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_client_type(&self) -> ClientType {
|
|
||||||
ClientType::NextBlock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NextBlockClient {
|
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
|
||||||
if CryptoProvider::get_default().is_none() {
|
|
||||||
let _ = default_provider()
|
|
||||||
.install_default()
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
let endpoint = endpoint.parse::<Uri>().unwrap();
|
|
||||||
let tls = ClientTlsConfig::new().with_native_roots();
|
|
||||||
let channel = Channel::builder(endpoint)
|
|
||||||
.tls_config(tls).expect("Failed to create TLS config")
|
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60)))
|
|
||||||
.http2_keep_alive_interval(Duration::from_secs(30))
|
|
||||||
.keep_alive_while_idle(true)
|
|
||||||
.timeout(Duration::from_secs(30))
|
|
||||||
.connect_timeout(Duration::from_secs(10))
|
|
||||||
.connect_lazy();
|
|
||||||
|
|
||||||
let client = ApiClient::with_interceptor(channel, MyInterceptor::new(auth_token));
|
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
|
||||||
Self { rpc_client: Arc::new(rpc_client), client }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
|
|
||||||
let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
|
||||||
|
|
||||||
self.client.clone().post_submit_v2(api::PostSubmitRequest {
|
|
||||||
transaction: Some(api::TransactionMessage {
|
|
||||||
content,
|
|
||||||
is_cleanup: false,
|
|
||||||
}),
|
|
||||||
skip_pre_flight: true,
|
|
||||||
front_running_protection: Some(true),
|
|
||||||
experimental_front_running_protection: Some(true),
|
|
||||||
snipe_transaction: Some(true),
|
|
||||||
}).await?;
|
|
||||||
|
|
||||||
let timeout: Duration = Duration::from_secs(10);
|
|
||||||
let start_time: Instant = Instant::now();
|
|
||||||
while Instant::now().duration_since(start_time) < timeout {
|
|
||||||
match poll_transaction_confirmation(&self.rpc_client, signature).await {
|
|
||||||
Ok(sig) => return Ok(sig),
|
|
||||||
Err(_) => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
let encoding = UiTransactionEncoding::Base64;
|
|
||||||
|
|
||||||
let mut signatures = Vec::new();
|
|
||||||
for transaction in transactions {
|
|
||||||
let (content, signature) = serialize_smart_transaction_and_encode(transaction, encoding).await?;
|
|
||||||
entries.push(api::PostSubmitRequestEntry {
|
|
||||||
transaction: Some(api::TransactionMessage {
|
|
||||||
content,
|
|
||||||
is_cleanup: false,
|
|
||||||
}),
|
|
||||||
skip_pre_flight: true,
|
|
||||||
});
|
|
||||||
signatures.push(signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.client.clone().post_submit_batch_v2(api::PostSubmitBatchRequest {
|
|
||||||
entries,
|
|
||||||
submit_strategy: api::SubmitStrategy::PSubmitAll as i32,
|
|
||||||
use_bundle: Some(true),
|
|
||||||
front_running_protection: Some(true),
|
|
||||||
}).await?;
|
|
||||||
|
|
||||||
let timeout: Duration = Duration::from_secs(10);
|
|
||||||
let start_time: Instant = Instant::now();
|
|
||||||
while Instant::now().duration_since(start_time) < timeout {
|
|
||||||
for signature in signatures.clone() {
|
|
||||||
match poll_transaction_confirmation(&self.rpc_client, signature).await {
|
|
||||||
Ok(sig) => signatures.push(sig),
|
|
||||||
Err(_) => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(signatures)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_tip_account(&self) -> Result<String> {
|
|
||||||
let tip_account = *NEXTBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap();
|
|
||||||
Ok(tip_account.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ZeroSlotClient {
|
|
||||||
pub endpoint: String,
|
|
||||||
pub auth_token: String,
|
|
||||||
pub rpc_client: Arc<SolanaRpcClient>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl FeeClientTrait for ZeroSlotClient {
|
|
||||||
async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
|
|
||||||
self.send_transaction(transaction).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
|
|
||||||
self.send_transactions(transactions).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_tip_account(&self) -> Result<String> {
|
|
||||||
let tip_account = self.get_tip_account().await?;
|
|
||||||
Ok(tip_account)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_client_type(&self) -> ClientType {
|
|
||||||
ClientType::ZeroSlot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ZeroSlotClient {
|
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
|
||||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
|
|
||||||
let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
|
||||||
|
|
||||||
let client = Client::new();
|
|
||||||
let request_body = json!({
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "sendTransaction",
|
|
||||||
"params": [
|
|
||||||
content,
|
|
||||||
{
|
|
||||||
"encoding": "base64",
|
|
||||||
"skipPreflight": true,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send the request
|
|
||||||
let response = client.post(format!("{}/?api-key={}", self.endpoint, self.auth_token))
|
|
||||||
.json(&request_body)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Parse the response
|
|
||||||
let response_json: serde_json::Value = response.json().await?;
|
|
||||||
if let Some(result) = response_json.get("result") {
|
|
||||||
println!("Transaction sent successfully: {}", result);
|
|
||||||
} else if let Some(error) = response_json.get("error") {
|
|
||||||
eprintln!("Failed to send transaction: {}", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeout: Duration = Duration::from_secs(10);
|
|
||||||
let start_time: Instant = Instant::now();
|
|
||||||
while Instant::now().duration_since(start_time) < timeout {
|
|
||||||
match poll_transaction_confirmation(&self.rpc_client, signature).await {
|
|
||||||
Ok(sig) => return Ok(sig),
|
|
||||||
Err(_) => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
|
|
||||||
let mut signatures = Vec::new();
|
|
||||||
for transaction in transactions {
|
|
||||||
let signature = self.send_transaction(transaction).await?;
|
|
||||||
signatures.push(signature);
|
|
||||||
}
|
|
||||||
Ok(signatures)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_tip_account(&self) -> Result<String> {
|
|
||||||
let tip_account = *ZEROSLOT_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap();
|
|
||||||
Ok(tip_account.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
use std::{
|
|
||||||
sync::Arc,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::swqos::jito_grpc::{
|
|
||||||
bundle::{
|
|
||||||
Bundle, BundleResult,
|
|
||||||
},
|
|
||||||
convert::proto_packet_from_versioned_tx,
|
|
||||||
searcher::{
|
|
||||||
searcher_service_client::SearcherServiceClient, SendBundleRequest, SubscribeBundleResultsRequest,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use solana_sdk::{
|
|
||||||
signature::Signature,
|
|
||||||
transaction::VersionedTransaction,
|
|
||||||
};
|
|
||||||
use thiserror::Error;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use tonic::{transport::{self, Channel, Endpoint}, Status};
|
|
||||||
use yellowstone_grpc_client::ClientTlsConfig;
|
|
||||||
|
|
||||||
use crate::swqos::common::poll_transaction_confirmation;
|
|
||||||
use crate::common::SolanaRpcClient;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum BlockEngineConnectionError {
|
|
||||||
#[error("transport error {0}")]
|
|
||||||
TransportError(#[from] transport::Error),
|
|
||||||
#[error("client error {0}")]
|
|
||||||
ClientError(#[from] Status),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum BundleRejectionError {
|
|
||||||
#[error("bundle lost state auction, auction: {0}, tip {1} lamports")]
|
|
||||||
StateAuctionBidRejected(String, u64),
|
|
||||||
#[error("bundle won state auction but failed global auction, auction {0}, tip {1} lamports")]
|
|
||||||
WinningBatchBidRejected(String, u64),
|
|
||||||
#[error("bundle simulation failure on tx {0}, message: {1:?}")]
|
|
||||||
SimulationFailure(String, Option<String>),
|
|
||||||
#[error("internal error {0}")]
|
|
||||||
InternalError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type BlockEngineConnectionResult<T> = Result<T, BlockEngineConnectionError>;
|
|
||||||
|
|
||||||
pub async fn get_searcher_client_no_auth(
|
|
||||||
block_engine_url: &str,
|
|
||||||
) -> BlockEngineConnectionResult<SearcherServiceClient<Channel>> {
|
|
||||||
let searcher_channel = create_grpc_channel(block_engine_url).await?;
|
|
||||||
let searcher_client = SearcherServiceClient::new(searcher_channel);
|
|
||||||
Ok(searcher_client)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_grpc_channel(url: &str) -> BlockEngineConnectionResult<Channel> {
|
|
||||||
let mut endpoint = Endpoint::from_shared(url.to_string()).expect("invalid url");
|
|
||||||
if url.starts_with("https") {
|
|
||||||
endpoint = endpoint.tls_config(ClientTlsConfig::new().with_native_roots())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint = endpoint.tcp_nodelay(true);
|
|
||||||
endpoint = endpoint.tcp_keepalive(Some(Duration::from_secs(10)));
|
|
||||||
endpoint = endpoint.connect_timeout(Duration::from_secs(20));
|
|
||||||
endpoint = endpoint.http2_keep_alive_interval(Duration::from_secs(10));
|
|
||||||
|
|
||||||
Ok(endpoint.connect().await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn subscribe_bundle_results(
|
|
||||||
searcher_client: Arc<Mutex<SearcherServiceClient<Channel>>>,
|
|
||||||
request: impl tonic::IntoRequest<SubscribeBundleResultsRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<tonic::codec::Streaming<BundleResult>>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
let mut searcher = searcher_client.lock().await;
|
|
||||||
searcher.subscribe_bundle_results(request).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_bundle_with_confirmation(
|
|
||||||
rpc: Arc<SolanaRpcClient>,
|
|
||||||
transactions: &Vec<VersionedTransaction>,
|
|
||||||
searcher_client: Arc<Mutex<SearcherServiceClient<Channel>>>,
|
|
||||||
) -> Result<Vec<Signature>, anyhow::Error> {
|
|
||||||
let mut signatures = send_bundle_no_wait(transactions, searcher_client).await?;
|
|
||||||
|
|
||||||
let timeout: Duration = Duration::from_secs(10);
|
|
||||||
let start_time: Instant = Instant::now();
|
|
||||||
while Instant::now().duration_since(start_time) < timeout {
|
|
||||||
for signature in signatures.clone() {
|
|
||||||
match poll_transaction_confirmation(&rpc, signature).await {
|
|
||||||
Ok(sig) => signatures.push(sig),
|
|
||||||
Err(_) => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(signatures)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_bundle_no_wait(
|
|
||||||
transactions: &Vec<VersionedTransaction>,
|
|
||||||
searcher_client: Arc<Mutex<SearcherServiceClient<Channel>>>,
|
|
||||||
) -> Result<Vec<Signature>, anyhow::Error> {
|
|
||||||
let mut packets = vec![];
|
|
||||||
let mut signatures = vec![];
|
|
||||||
for transaction in transactions {
|
|
||||||
let packet = proto_packet_from_versioned_tx(transaction);
|
|
||||||
packets.push(packet);
|
|
||||||
signatures.push(transaction.signatures[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut searcher = searcher_client.lock().await;
|
|
||||||
searcher
|
|
||||||
.send_bundle(SendBundleRequest {
|
|
||||||
bundle: Some(Bundle {
|
|
||||||
header: None,
|
|
||||||
packets,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(signatures)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user