Compare commits

...

34 Commits

Author SHA1 Message Date
0xfnzero c76c8d6ac2 Merge dev for sol-trade-sdk 5.0.0 2026-07-17 03:50:24 +08:00
0xfnzero dc3d2b8deb fix(pumpswap): harden quotes and low-latency examples 2026-07-17 03:22:52 +08:00
0xfnzero fb1ff176d0 fix(pumpswap): validate virtual reserve quotes 2026-07-17 02:15:20 +08:00
0xfnzero dd41dd4f87 feat(pumpswap): support virtual quote reserves 2026-07-16 20:58:56 +08:00
Wood 8bef655abb Merge pull request #108 from 0xfnzero/codex/lunar-lander-integration
feat(swqos): default Lunar Lander to QUIC
2026-07-12 18:17:11 +08:00
0xfnzero 8413baa3d9 feat(swqos): default Lunar Lander to QUIC 2026-07-12 18:16:22 +08:00
Wood 100156424c Merge pull request #107 from 0xfnzero/codex/lunar-lander-integration
feat(swqos): integrate Hello Moon Lunar Lander
2026-07-12 18:03:57 +08:00
0xfnzero 1d224770d8 fix: align Lunar Lander with official client 2026-07-12 18:03:08 +08:00
0xfnzero 1c78b3cab2 Merge pull request #96 from joe-hellomoon/feat/add-lunarlander-swqos
# Conflicts:
#	Cargo.toml
#	src/constants/swqos.rs
#	src/swqos/lunarlander.rs
#	src/swqos/mod.rs
2026-07-12 17:55:32 +08:00
0xfnzero ca306172ca Merge pull request #88 from samk-hellomoon/feat/add-lunarlander-swqos
# Conflicts:
#	README.md
#	README_CN.md
#	src/constants/swqos.rs
#	src/swqos/mod.rs
2026-07-12 17:44:05 +08:00
0xfnzero b09902c527 feat: harden trading SDK examples 2026-07-12 14:35:58 +08:00
0xfnzero 47cef59d15 fix: align PumpSwap dynamic fee parameters 2026-06-30 21:40:44 +08:00
0xfnzero 9af86c8397 Update SWQOS submit behavior 2026-06-23 04:32:50 +08:00
0xfnzero 30df893c30 docs: clarify SDK scope and current version 2026-06-20 00:56:14 +08:00
0xfnzero 22c1a149e7 Release v4.0.21 2026-06-18 01:26:54 +08:00
Wood 8657273ba6 Merge pull request #105 from civa/main
feat(swqos): update Solami endpoints + tip accounts
2026-06-17 13:01:17 +08:00
civa 99fb4ee604 feat(swqos): update Solami endpoints + tip accounts 2026-06-16 18:45:43 +00:00
0xfnzero 3719d4a9f4 Merge pull request #86 from civa/feat/add-solami
# Conflicts:
#	src/constants/swqos.rs
#	src/swqos/mod.rs
2026-06-17 01:16:07 +08:00
0xfnzero d7326b55f4 fix pumpfun native sol quote routing 2026-06-08 03:13:04 +08:00
0xfnzero 7471bcba09 Release v4.0.19 2026-06-08 00:00:32 +08:00
0xfnzero b8325b2b52 Release v4.0.18 2026-06-07 22:22:26 +08:00
0xfnzero e459f175f5 Release sol-trade-sdk v4.0.17 2026-06-07 01:42:08 +08:00
0xfnzero e5e58bef89 Release sol-trade-sdk v4.0.16 2026-06-06 12:31:08 +08:00
0xfnzero 9874cd5358 Release sol-trade-sdk v4.0.15 2026-06-01 14:57:49 +08:00
0xfnzero 1d3c0ff5da fix: force Token-2022 for pump suffix mints 2026-05-28 05:21:24 +08:00
Wood 34f400e89d Merge pull request #103 from HelvetiCrypt/wait-for-all-submits
Add SwapParams.wait_for_all_submits to restore all-routes signature collection
2026-05-28 02:46:39 +08:00
HelvetiCrypt bd1772bed2 Add SwapParams.wait_for_all_submits to restore all-routes signature collection
v4.0.11 switched the fast-submit (`wait_tx_confirmed = false`) result loop
from `wait_for_all_submitted` to `wait_for_first_submitted`. The new
function returns as soon as one SWQOS route's HTTP submit completes and
drains whatever signatures are in `ResultCollector.results` at that
moment — slower routes' signatures arrive after the caller has returned.

This is correct for fire-and-forget low-latency submits, but breaks
callers that do their own on-chain confirmation against a pinned durable
nonce. Only one of the submitted txs can land (nonce is consumed by
whichever lands first), but the caller has no way of knowing in advance
which route it will be. Their post-submit logic feeds every returned
signature to `getSignatureStatuses` to find the landed one — with only
the fastest HTTP responder returned (often not the one that lands), the
landed tx isn't in the polled set.

Adds an opt-in `SwapParams.wait_for_all_submits: bool` (default false,
plumbed through `TradeBuyParams` / `TradeSellParams`). When true and
`wait_tx_confirmed = false`, `execute_parallel` uses
`wait_for_all_submitted` (still in the codebase, was
`#[allow(dead_code)]`) so every submitted signature is returned. Zero
behaviour change for current users.
2026-05-27 08:02:49 +00:00
0xfnzero 5449d51591 Release sol-trade-sdk 4.0.14 2026-05-25 22:01:15 +08:00
0xfnzero b973061f92 Update DEX protocol support for 4.0.13 2026-05-25 01:47:48 +08:00
0xfnzero 57e4c5d221 Fix PumpFun V2 buyback fee mutability 2026-05-22 08:56:20 +08:00
Joe Abbey 70d1da0b13 feat(swqos): add Lunar Lander (HelloMoon) provider with binary HTTP and QUIC support
- HTTP binary mode: POST /send-bin with bincode body, x-api-key header, keepalive ping
- QUIC mode: port 16888 via lunar-lander-quic-client crate, fire-and-forget uni streams
- Dual transport: SwqosConfig::LunarLander(key, region, url, transport) like Astralane
- 10 moon-prefix tip accounts, 0.001 SOL minimum tip
- Regional endpoints: NYC, Frankfurt, Amsterdam, Ashburn, Tokyo
2026-03-30 13:43:26 -04:00
Sam Kerr 8c6557257e Add a test for Lunar Lander 2026-03-09 17:05:23 -04:00
Sam Kerr 77232a2b70 Add Hello Moon Lunar Lander support 2026-03-09 17:05:23 -04:00
civa f19f8a5d1c feat(swqos): add Solami 2026-03-02 03:00:49 +00:00
130 changed files with 58165 additions and 1307 deletions
+6 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.11"
version = "5.0.0"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
@@ -34,6 +34,7 @@ members = [
"examples/cli_trading",
"examples/gas_fee_strategy",
"examples/meteora_damm_v2_direct_trading",
"examples/simple_trading",
]
[lib]
@@ -74,6 +75,9 @@ futures = "0.3.31"
futures-util = "0.3.31"
base64 = "0.22.1"
bs58 = "0.5.1"
# `five8` 1.0 permits five8_core 0.1.x, whose DecodeError implements
# std::error::Error only with this feature. Solana keypair decoding requires it.
five8_core = { version = "0.1.2", features = ["std"] }
rand = "0.9.0"
bincode = "1.3.3"
anyhow = "1.0.90"
@@ -111,6 +115,7 @@ sha2 = "0.10"
tonic-prost = "0.14.2"
# 须含 runtime-tokio,否则 quinn::Endpoint::client 报 no async runtime foundQUICSpeedlanding/Soyas)无法初始化
quinn = { version = "0.11", default-features = false, features = ["rustls", "runtime-tokio"] }
lunar-lander-quic-client = "0.4"
rcgen = "0.13"
uuid = "1.11"
+182 -78
View File
@@ -79,19 +79,37 @@ This SDK is available in multiple languages:
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | Async/await native support |
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | Concurrent-safe with goroutine support |
## What This SDK Is For
`sol-trade-sdk` is the Rust implementation of the FnZero Solana trading SDK family. It focuses on low-latency transaction construction and submission for Solana DEX trading bots, copy-trading systems, sniper bots, arbitrage strategies, and private trading infrastructure.
| Area | Coverage |
|------|----------|
| DEX protocols | PumpFun, PumpSwap, Bonk, Meteora DAMM v2, Raydium AMM v4, Raydium CPMM |
| Submit lanes | Default Solana RPC plus Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane, SpeedLanding, and other SWQoS providers |
| Trading workflows | Buy/sell, exact input/output, copy trading, sniper trading, address lookup tables, durable nonce, middleware, shared infrastructure |
| Hot-path design | Caller supplies recent blockhash or durable nonce; trade execution avoids RPC reads for blockhash, account, or balance data |
## 🔖 Current Release
**Rust crate:** `sol-trade-sdk = "4.0.23"`
This release updates PumpSwap for the July 2026 virtual quote reserve rollout. Pool and event schemas include `virtual_quote_reserves`, and all PumpSwap buy, sell, pricing, and dynamic-fee calculations use `quote_vault_balance + virtual_quote_reserves`.
## ✨ Features
1. **PumpFun Trading**: Support for `buy`, `sell`, `buy_exact_sol_in`, and the new unified `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` instructions (SOL + USDC)
1. **PumpFun Trading**: Unified SDK-side `buy`, `sell`, and `buy_exact_quote_in` flow, preferring V1 for native SOL and selecting V2 for USDC/non-native quote mints or explicit WSOL settlement
2. **PumpSwap Trading**: Support for PumpSwap pool trading operations
3. **Bonk Trading**: Support for Bonk trading operations
4. **Raydium CPMM Trading**: Support for Raydium CPMM (Concentrated Pool Market Maker) trading operations
5. **Raydium AMM V4 Trading**: Support for Raydium AMM V4 (Automated Market Maker) trading operations
6. **Meteora DAMM V2 Trading**: Support for Meteora DAMM V2 (Dynamic AMM) trading operations
7. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane and other services
8. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail
7. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane, LunarLander and other services
8. **Concurrent Trading**: Submit through every configured SWQoS provider plus the default RPC lane; the first accepted result can return early while slower routes continue submitting
9. **Unified Trading Interface**: Use unified trading protocol enums for trading operations
10. **Middleware System**: Support for custom instruction middleware to modify, add, or remove instructions before transaction execution
11. **Shared Infrastructure**: Share expensive RPC and SWQoS clients across multiple wallets for reduced resource usage
12. **Hot-Path RPC Boundary**: Trade execution uses caller-supplied blockhash or durable nonce and never queries RPC for blockhash, account, or balance data
## 📦 Installation
@@ -108,14 +126,14 @@ Add the dependency to your `Cargo.toml`:
```toml
# Add to your Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.9" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.23" }
```
### Use crates.io
```toml
# Add to your Cargo.toml
sol-trade-sdk = "4.0.9"
sol-trade-sdk = "4.0.23"
```
## 🛠️ Usage Examples
@@ -143,6 +161,14 @@ let swqos_configs: Vec<SwqosConfig> = vec![
// Astralane: 4th param = AstralaneTransport — Binary (default), Plain (/iris), or Quic
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary HTTP /irisb
SwqosConfig::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None),
// Lunar Lander: 4th param None = QUIC (default); Some(SwqosTransport::Http) = binary HTTP
SwqosConfig::LunarLander("your_hellomoon_api_key".to_string(), SwqosRegion::Frankfurt, None, None),
SwqosConfig::LunarLander(
"your_hellomoon_api_key".to_string(),
SwqosRegion::Frankfurt,
None,
Some(SwqosTransport::Http),
),
];
// Create TradeConfig instance
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
@@ -152,7 +178,6 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .check_min_tip(false) // default: false - filter SWQOS below min tip
// .swqos_cores_from_end(false) // default: false - bind SWQOS to last N CPU cores
// .mev_protection(false) // default: false - MEV (Astralane QUIC :9000 or HTTP mev-protect / BlockRazor)
// .use_pumpfun_v2(true) // PumpFun V2 (27 accounts, quote_mint); required for USDC-paired PumpFun coins
.build();
// Create TradingClient
@@ -190,66 +215,107 @@ gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001,
For detailed information about all trading parameters, see the [Trading Parameters Reference](docs/TRADING_PARAMETERS.md).
```rust
// Import DexParamEnum for protocol-specific parameters
use sol_trade_sdk::trading::core::params::DexParamEnum;
let buy_params = sol_trade_sdk::TradeBuyParams {
dex_type: DexType::PumpSwap,
input_token_type: TradeTokenType::WSOL,
mint: mint_pubkey,
input_token_amount: buy_sol_amount,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
// Use DexParamEnum for type-safe protocol parameters (zero-overhead abstraction)
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
durable_nonce: None,
fixed_output_token_amount: None, // Optional: specify exact output amount
gas_fee_strategy: gas_fee_strategy.clone(), // Gas fee strategy configuration
simulate: false, // Set to true for simulation only
use_exact_sol_amount: None, // Use exact SOL input for PumpFun/PumpSwap (defaults to true)
use sol_trade_sdk::{
AccountPolicy, BuyAmount, DexType, SimpleBuyParams, TradeTokenType,
trading::core::params::DexParamEnum,
};
let buy_params = SimpleBuyParams::new(
DexType::PumpFun,
// Token used to pay. For PumpFun V2 SOL/WSOL quote pools, keep this as SOL
// when you want to spend native SOL; the SDK will still use V2 accounts.
TradeTokenType::SOL,
// Mint of the meme/token you want to buy.
mint_pubkey,
// Regular PumpFun/PumpSwap buy. The SDK estimates token output and applies
// slippage to the maximum quote cost.
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
// Protocol state from parser/RPC cache, for example PumpFunParams::from_trade(...).
DexParamEnum::PumpFun(pumpfun_params),
// Pass a cached recent blockhash; the SDK does not fetch it on the hot path.
recent_blockhash,
gas_fee_strategy.clone(),
)
// 300 = 3%.
.slippage_basis_points(300)
// For bots/sniping: assume ATAs are already prepared and keep the tx small.
.account_policy(AccountPolicy::HotPathMinimal);
```
#### 4. Execute Trading
```rust
client.buy(buy_params).await?;
client.buy_simple(buy_params).await?;
```
### ⚡ Trading Parameters
For comprehensive information about all trading parameters including `TradeBuyParams` and `TradeSellParams`, see the dedicated [Trading Parameters Reference](docs/TRADING_PARAMETERS.md).
Use `SimpleBuyParams` / `SimpleSellParams` for new integrations. They describe trading intent and hide low-level ATA flags. Most users only choose:
- `pay_with` / `receive_as`: quote token direction. Use `SOL` when the wallet spends or receives native SOL. For PumpFun V2 SOL-paired pools whose quote mint is WSOL, still use `SOL` if you want native SOL settlement.
- `amount`: trade sizing intent. Pick one enum variant instead of combining `input_token_amount`, `fixed_output_token_amount`, and `use_exact_sol_amount`.
- `account_policy`: account creation behavior. Bots usually use `HotPathMinimal`; normal apps can keep the default `Auto`.
| Parameter | Meaning | Recommendation |
|---|---|---|
| `BuyAmount::ExactInput(amount)` | Spend exactly this quote amount; slippage protects minimum output. | Normal swaps |
| `BuyAmount::WithMaxInput { quote_amount }` | Regular PumpFun/PumpSwap buy with slippage applied to max quote cost. | Sniping/arbitrage |
| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | Buy an exact token amount with a max quote budget. | Exact-output workflows |
| `SellAmount::ExactInput(amount)` | Sell exactly this token amount. | Normal sells |
| `SellAmount::ExactOutput { output_amount, max_input_amount }` | Receive an exact quote amount while limiting token input, where the DEX supports it. | Exact-output sells |
| `AccountPolicy::Auto` | SDK creates practical ATAs when needed. | General usage |
| `AccountPolicy::HotPathMinimal` | Avoid ATA create/close instructions in the trade tx. | Bots, sniping, latency-sensitive flows |
| `AccountPolicy::CreateMissing` | Include ATA creation instructions where possible. | Convenience over transaction size |
| `AccountPolicy::AssumePrepared` | Caller prepared every required ATA. | Deterministic advanced flows |
Optional builder methods:
| Method | Meaning |
|---|---|
| `.slippage_basis_points(300)` | Set slippage. `300` means 3%. |
| `.address_lookup_table_account(alt)` | Attach an ALT to reduce transaction size. Useful for large PumpFun V2 transactions. |
| `.wait_tx_confirmed(true)` | Return only after confirmation. Usually disabled for fastest submit paths. |
| `.wait_for_all_submits(true)` | Wait for all SWQoS lane responses and return submitted signatures. Recent-blockhash route variants are not mutually exclusive; durable nonce variants are. |
| `.simulate(true)` | Build and simulate the transaction instead of sending it. |
| `.grpc_recv_us(ts)` | Attach upstream receive timestamp for latency tracing. |
| `.durable_nonce(nonce_info)` | Use durable nonce and clear `recent_blockhash`. Recommended when you start from `SimpleBuyParams::new(...)` / `SimpleSellParams::new(...)`. |
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | Construct params directly with durable nonce instead of `recent_blockhash`. |
| `SimpleSellParams::with_tip(false)` | Disable relay tips for sells. Buys use the gas fee strategy/tip settings. |
`TradeBuyParams` and `TradeSellParams` remain available as advanced low-level APIs. See the dedicated [Trading Parameters Reference](docs/TRADING_PARAMETERS.md).
#### About ShredStream
When using shred to subscribe to events, due to the nature of shreds, you cannot get complete information about transaction events.
Please ensure that the parameters your trading logic depends on are available in shreds when using them.
See the [low-latency bot integration checklist](docs/LOW_LATENCY_BOTS.md) for client warmup, blockhash/nonce handling, trade intent, event-state freshness, and bounded requoting.
### 📊 Usage Examples Summary Table
| Description | Run Command | Source Code |
The complete bilingual index and safety classification are available in [`examples/README.md`](examples/README.md).
| Description | Run Command | Guide |
|-------------|-------------|-------------|
| Create and configure TradingClient instance | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) |
| Share infrastructure across multiple wallets | `cargo run --package shared_infrastructure` | [examples/shared_infrastructure](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/shared_infrastructure/src/main.rs) |
| PumpFun token sniping trading | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) |
| PumpFun token copy trading | `cargo run --package pumpfun_copy_trading` | [examples/pumpfun_copy_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_copy_trading/src/main.rs) |
| PumpSwap trading operations | `cargo run --package pumpswap_trading` | [examples/pumpswap_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpswap_trading/src/main.rs) |
| Raydium CPMM trading operations | `cargo run --package raydium_cpmm_trading` | [examples/raydium_cpmm_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/raydium_cpmm_trading/src/main.rs) |
| Raydium AMM V4 trading operations | `cargo run --package raydium_amm_v4_trading` | [examples/raydium_amm_v4_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/raydium_amm_v4_trading/src/main.rs) |
| Meteora DAMM V2 trading operations | `cargo run --package meteora_damm_v2_direct_trading` | [examples/meteora_damm_v2_direct_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/meteora_damm_v2_direct_trading/src/main.rs) |
| Bonk token sniping trading | `cargo run --package bonk_sniper_trading` | [examples/bonk_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/bonk_sniper_trading/src/main.rs) |
| Bonk token copy trading | `cargo run --package bonk_copy_trading` | [examples/bonk_copy_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/bonk_copy_trading/src/main.rs) |
| Custom instruction middleware example | `cargo run --package middleware_system` | [examples/middleware_system](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/middleware_system/src/main.rs) |
| Address lookup table example | `cargo run --package address_lookup` | [examples/address_lookup](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/address_lookup/src/main.rs) |
| Nonce example | `cargo run --package nonce_cache` | [examples/nonce_cache](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/nonce_cache/src/main.rs) |
| Wrap/unwrap SOL to/from WSOL example | `cargo run --package wsol_wrapper` | [examples/wsol_wrapper](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/wsol_wrapper/src/main.rs) |
| Seed trading example | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) |
| Gas fee strategy example | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
| Simple buy/sell parameter API | `cargo run --package simple_trading` | [README](examples/simple_trading/README.md) |
| Create and configure TradingClient | `cargo run --package trading_client` | [README](examples/trading_client/README.md) |
| Share infrastructure across wallets | `cargo run --package shared_infrastructure` | [README](examples/shared_infrastructure/README.md) |
| PumpFun sniper | `cargo run --package pumpfun_sniper_trading` | [README](examples/pumpfun_sniper_trading/README.md) |
| PumpFun copy trading | `cargo run --package pumpfun_copy_trading` | [README](examples/pumpfun_copy_trading/README.md) |
| PumpSwap low-latency stream | `cargo run --package pumpswap_trading` | [README](examples/pumpswap_trading/README.md) |
| PumpSwap direct RPC flow | `cargo run --package pumpswap_direct_trading` | [README](examples/pumpswap_direct_trading/README.md) |
| Raydium CPMM | `cargo run --package raydium_cpmm_trading` | [README](examples/raydium_cpmm_trading/README.md) |
| Raydium AMM V4 | `cargo run --package raydium_amm_v4_trading` | [README](examples/raydium_amm_v4_trading/README.md) |
| Meteora DAMM V2 | `cargo run --package meteora_damm_v2_direct_trading` | [README](examples/meteora_damm_v2_direct_trading/README.md) |
| Bonk sniper | `cargo run --package bonk_sniper_trading` | [README](examples/bonk_sniper_trading/README.md) |
| Bonk copy trading | `cargo run --package bonk_copy_trading` | [README](examples/bonk_copy_trading/README.md) |
| Instruction middleware | `cargo run --package middleware_system` | [README](examples/middleware_system/README.md) |
| Address lookup tables | `cargo run --package address_lookup` | [README](examples/address_lookup/README.md) |
| Durable nonce | `cargo run --package nonce_cache` | [README](examples/nonce_cache/README.md) |
| Wrap/unwrap SOL and WSOL | `cargo run --package wsol_wrapper` | [README](examples/wsol_wrapper/README.md) |
| Seed optimization | `cargo run --package seed_trading` | [README](examples/seed_trading/README.md) |
| Gas fee strategy | `cargo run --package gas_fee_strategy` | [README](examples/gas_fee_strategy/README.md) |
| Multi-DEX CLI template | `cargo run --package cli_trading` | [README](examples/cli_trading/README.md) |
### ⚙️ SWQoS Service Configuration
@@ -283,7 +349,29 @@ let temporal_config = SwqosConfig::Temporal(
- If no custom URL is provided (`None`), the system will use the default endpoint for the specified `SwqosRegion`
- This allows for maximum flexibility while maintaining backward compatibility
When using multiple MEV services, you need to use `Durable Nonce`. You need to use the `fetch_nonce_info` function to get the latest `nonce` value, and use it as the `durable_nonce` when trading.
When using multiple MEV services, you need to use `Durable Nonce`. Fetch the latest nonce value and attach it to the high-level buy/sell params:
```rust
use sol_trade_sdk::{fetch_nonce_info, AccountPolicy, BuyAmount, SimpleBuyParams};
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
.await
.expect("nonce account must be initialized");
let buy_params = SimpleBuyParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint_pubkey,
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
DexParamEnum::PumpFun(pumpfun_params),
recent_blockhash, // will be cleared by `.durable_nonce(...)`
gas_fee_strategy.clone(),
)
.durable_nonce(nonce_info)
.account_policy(AccountPolicy::HotPathMinimal);
client.buy_simple(buy_params).await?;
```
#### Astralane (Binary / Plain HTTP / QUIC)
@@ -335,7 +423,7 @@ PumpFun and PumpSwap support **cashback** for eligible tokens: part of the tradi
- **When params come from RPC**: If you use `PumpFunParams::from_mint_by_rpc` or `PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc`, the SDK reads `is_cashback_coin` from chain—no extra step.
- **When params come from event/parser**: If you build params from trade events (e.g. [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk)), you **must** pass the cashback flag into the SDK:
- **PumpFun**: `PumpFunParams::from_trade(..., is_cashback_coin)` and `PumpFunParams::from_dev_trade(..., is_cashback_coin)` take an `is_cashback_coin` parameter. Set it from the parsed event (e.g. CreateEvents `is_cashback_enabled` or BondingCurves `is_cashback_coin`).
- **PumpFun**: `PumpFunParams::from_trade(..., mint, quote_mint, creator, ..., is_cashback_coin, mayhem_mode)` and `PumpFunParams::from_dev_trade(..., is_cashback_coin)` take an `is_cashback_coin` parameter. Set it from the parsed event (e.g. CreateEvents `is_cashback_enabled` or BondingCurves `is_cashback_coin`).
- **PumpSwap**: `PumpSwapParams` has a field `is_cashback_coin`. When constructing params manually (e.g. from pool/trade events), set it from the parsed pool or event data.
- The **pumpfun_copy_trading** and **pumpfun_sniper_trading** examples use sol-parser-sdk for gRPC subscription and pass `e.is_cashback_coin` when building params.
- **Claim**: Use `client.claim_cashback_pumpfun()` and `client.claim_cashback_pumpswap(...)` to claim accumulated cashback.
@@ -355,7 +443,7 @@ Some PumpFun coins use **Creator Rewards Sharing**, so the on-chain `creator_vau
The SDK does not fetch creator_vault from RPC on every sell (to avoid latency); pass the up-to-date vault from gRPC/events when available.
#### PumpFun V1 vs V2 Instructions
#### PumpFun Unified Buy/Sell With V1/V2 Instructions
PumpFun has two instruction sets for bonding-curve trading:
@@ -363,47 +451,53 @@ PumpFun has two instruction sets for bonding-curve trading:
|---|---|---|
| Instructions | `buy` / `buy_exact_sol_in` / `sell` | `buy_v2` / `buy_exact_quote_in_v2` / `sell_v2` |
| Account metas | 18 | 27 |
| Quote mint | SOL only (legacy) | SOL or USDC (via `quote_mint` field) |
| Transaction size | Smaller (fits `PACKET_DATA_SIZE` without LUT) | Larger (requires LUT for most transactions) |
| Quote mint | Native SOL (`default`, Solscan SOL sentinel, or WSOL sentinel) | Non-native quote mint, or explicit WSOL settlement |
| Transaction size | Smaller (preferred hot path) | Larger (may require LUT for nonce/tip/ATA-heavy transactions) |
**Default: V1** (`use_pumpfun_v2 = false`). The SDK uses V1 instructions which produce smaller transactions that fit within the 1232-byte `PACKET_DATA_SIZE` limit without requiring an Address Lookup Table.
The SDK-side builder is version-neutral: callers use the normal buy/sell flow, and `quote_mint` plus the requested settlement token (`pay_with` / `receive_as`) select the correct on-chain discriminator and account layout internally. There is no user-facing V2 switch required.
**Default: V1**. When `quote_mint` is `Pubkey::default()`, the Solscan SOL sentinel (`So11111111111111111111111111111111111111111`), or `WSOL_TOKEN_ACCOUNT` (`So11111111111111111111111111111111111111112`), the SDK treats the curve as native SOL-paired and uses V1 instructions when `pay_with` / `receive_as` is `SOL`. This is the preferred hot path because it avoids the 27-account V2 layout. Passing USDC or another real quote mint selects V2. Passing `WSOL` as the buy input or sell output selects V2 only when you intentionally want to settle through an existing WSOL ATA.
**Key changes in v2 instructions:**
- `quote_mint` parameter — pass wrapped SOL for SOL-paired, or USDC mint for USDC-paired
- `quote_mint` parameter — native SOL-paired curves may appear as default, Solscan SOL, or WSOL; USDC/non-native quote mints select V2
- 27 fixed accounts (buy) / 26 fixed accounts (sell) — **no optional accounts**
- `buyback_fee_recipient`, `sharing_config`, and 6 `associated_quote_*` ATAs are now mandatory
- Same pricing and cost as legacy instructions for SOL-paired coins
- USDC-paired coins must be bought with USDC and sell back to USDC. The SDK rejects SOL input for USDC quote pools before transaction submission.
**How to enable V2:**
**Pass `quote_mint` into `PumpFunParams::from_trade`**:
**Method 1 — Global runtime flag** (recommended for `TradingClient`; required for USDC-paired coins):
When using event/parser data, pass the event's `quote_mint` right after `mint`. `Pubkey::default()`, Solscan SOL (`So11111111111111111111111111111111111111111`), and `WSOL_TOKEN_ACCOUNT` all mean a native SOL-paired curve and default to V1 for normal SOL settlement. USDC means USDC V2.
```rust
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
.use_pumpfun_v2(true) // Switch all PumpFun trades to V2 instructions (27 accounts)
.build();
// quote_mint is not a PDA. It is the quote SPL mint carried by parser/gRPC events:
// - Native SOL pool: Pubkey::default(), Solscan SOL, or WSOL sentinel from parser data
// - USDC/non-native pool: actual quote SPL mint
let quote_mint = e.quote_mint;
let params = PumpFunParams::from_trade(
e.bonding_curve,
e.associated_bonding_curve,
e.mint,
quote_mint,
e.creator,
e.creator_vault,
e.virtual_token_reserves,
e.virtual_quote_reserves,
e.real_token_reserves,
e.real_quote_reserves,
close_token_account_when_sell,
e.fee_recipient,
e.token_program,
e.is_cashback_coin,
Some(e.mayhem_mode),
);
```
**Method 2 — Set the quote mint on `PumpFunParams`**:
For USDC-paired coins, pass `USDC_TOKEN_ACCOUNT` as the buy `input_mint` and sell `output_mint`; SOL/WSOL is only valid for SOL-paired PumpFun curves. For SOL-paired curves, use `SOL` for the normal fast path; use `WSOL` only if you intentionally want V2 settlement through an existing WSOL ATA.
When consuming parser events, map `quoteMint`, `virtualQuoteReserves`, and `realQuoteReserves` into `PumpFunParams::from_trade(...)`; USDC pools use `4_292_000_000` as the initial virtual quote reserve.
For legacy SOL events where `quote_mint` is `Pubkey::default()` or Solscan SOL, use `virtual_sol_reserves` / `real_sol_reserves` when the quote-reserve fields are absent or zero.
When using the high-level `TradingClient`, keep `.use_pumpfun_v2(true)` enabled in `TradeConfig` and set `quote_mint` on the PumpFun params to select the pair:
```rust
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
// SOL-paired coin with v2 layout
let params = PumpFunParams::from_trade(/* ... */)
.with_quote_mint(WSOL_TOKEN_ACCOUNT);
// USDC-paired coin (requires v2)
let params = PumpFunParams::from_trade(/* ... */)
.with_quote_mint(USDC_TOKEN_ACCOUNT);
```
`with_quote_mint(...)` also marks the params as V2-capable for lower-level instruction builders, but the high-level `TradingClient` uses the client-level `use_pumpfun_v2` runtime flag when choosing V1 vs V2.
> **Note**: V2 transactions with ATA creation + durable nonce may exceed `PACKET_DATA_SIZE`. Enable an Address Lookup Table (`address_lookup_table_account`) when using V2.
> **Note**: V2 transactions with ATA creation + durable nonce/tip may exceed `PACKET_DATA_SIZE`. The SDK reports this locally and does not remove compute-budget or tip instructions because that changes priority semantics. Use V1 when the curve is native SOL-paired, pre-create ATAs, or enable an Address Lookup Table (`address_lookup_table_account`) when using V2.
#### PumpSwap: coin_creator_vault from events (no RPC)
@@ -412,6 +506,14 @@ For **PumpSwap** (Pump AMM), `coin_creator_vault_ata` and `coin_creator_vault_au
- **sol-parser-sdk**: Instruction parser sets them from accounts 17 and 18; the account filler also fills them when the event comes from logs. Use `PumpSwapParams::from_trade(..., e.coin_creator_vault_ata, e.coin_creator_vault_authority, ...)` with the buy/sell event `e`.
- **solana-streamer**: Instruction parser sets them from `accounts.get(17)` and `accounts.get(18)`. Use the same `from_trade` with the event's `coin_creator_vault_ata` and `coin_creator_vault_authority`.
#### PumpSwap: virtual quote reserves
PumpSwap quotes must use `effective_quote_reserves = pool_quote_token_account.amount + virtual_quote_reserves`. The Pool account and BuyEvent/SellEvent encode `virtual_quote_reserves` as `i128`.
- RPC constructors such as `PumpSwapParams::from_pool_address_by_rpc` read and apply the Pool field automatically.
- Event fast paths must pass the event's raw `pool_quote_token_reserves` and `virtual_quote_reserves` separately to `PumpSwapParams::from_trade(...)` or `from_trade_with_fee_basis_points(...)`. Do not add them before calling the constructor.
- The SDK uses effective reserves for buys, sells, prices, and dynamic fee-tier selection. Invalid signed sums return an error instead of wrapping.
## 🛡️ MEV Protection Services
You can apply for a key through the official website: [Community Website](https://fnzero.dev/swqos)
@@ -422,6 +524,8 @@ You can apply for a key through the official website: [Community Website](https:
- **BlockRazor**: High-speed transaction execution with API key authentication
- **Astralane**: Blockchain network acceleration (Binary/Plain HTTP and QUIC)
- **SpeedLanding**: High-speed transaction execution with API key authentication
- **Node1**: High-speed transaction execution with API key authentication
- **LunarLander**: HelloMoon transaction landing service (minimum tip: 0.001 SOL)
## 📁 Project Structure
+180 -75
View File
@@ -79,19 +79,37 @@
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | 原生 async/await 支持 |
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | 并发安全,goroutine 支持 |
## 这个 SDK 适合什么场景
`sol-trade-sdk` 是 FnZero Solana 交易 SDK 系列的 Rust 版本,重点服务于低延迟交易构建和提交,适合 Solana DEX 交易机器人、跟单系统、狙击机器人、套利策略和私有交易基础设施。
| 方向 | 覆盖范围 |
|------|----------|
| DEX 协议 | PumpFun、PumpSwap、Bonk、Meteora DAMM v2、Raydium AMM v4、Raydium CPMM |
| 提交通道 | 默认 Solana RPC,以及 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane、SpeedLanding 等 SWQoS 服务 |
| 交易流程 | 买入/卖出、精确输入/输出、跟单交易、狙击交易、地址查找表、durable nonce、中间件、共享基础设施 |
| 热路径设计 | 调用方传入 recent blockhash 或 durable nonce;交易执行阶段不再查询 RPC 获取 blockhash、账户或余额 |
## 🔖 当前版本
**Rust crate:** `sol-trade-sdk = "4.0.23"`
本版本适配 PumpSwap 2026 年 7 月的虚拟 quote 储备升级。Pool 与事件 schema 新增 `virtual_quote_reserves`,PumpSwap 买入、卖出、报价和动态费率计算统一使用 `quote_vault_balance + virtual_quote_reserves`
## ✨ 项目特性
1. **PumpFun 交易**: 支持 `buy``sell``buy_exact_sol_in` 以及全新的统一 `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` 指令(SOL + USDC
1. **PumpFun 交易**: SDK 侧统一 `buy``sell``buy_exact_quote_in` 流程,native SOL 优先走 V1USDC/非 SOL quote 或显式 WSOL 结算才走 V2
2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作
3. **Bonk 交易**: 支持 Bonk 的交易操作
4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作
5. **Raydium AMM V4 交易**: 支持 Raydium AMM V4 (Automated Market Maker) 的交易操作
6. **Meteora DAMM V2 交易**: 支持 Meteora DAMM V2 (Dynamic AMM) 的交易操作
7. **多种 MEV 保护**: 支持 Jito、Temporal、FlashBlock、BlockRazor、Astralane、SpeedLanding 等服务
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
7. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane、SpeedLanding、LunarLander 等服务
8. **并发交易**: 所有已配置的 SWQoS 通道和默认 RPC 通道都会发出提交;首个成功只影响返回,较慢通道会继续提交
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
11. **共享基础设施**: 多钱包可共享同一套 RPC 与 SWQoS 客户端,降低资源占用
12. **热路径 RPC 边界**: 交易执行使用调用方传入的 blockhash 或 durable nonce,不在热路径查询 blockhash、账户或余额
## 📦 安装
@@ -108,14 +126,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.9" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.23" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = "4.0.9"
sol-trade-sdk = "4.0.23"
```
## 🛠️ 使用示例
@@ -143,6 +161,14 @@ let swqos_configs: Vec<SwqosConfig> = vec![
// Astralane:第4个参数为 AstralaneTransport — Binary(默认)、Plain/iris)或 Quic
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary /irisb
SwqosConfig::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None),
// Lunar Lander:第4个参数 None 为 QUIC(默认);Some(SwqosTransport::Http) 为 binary HTTP
SwqosConfig::LunarLander("your_hellomoon_api_key".to_string(), SwqosRegion::Frankfurt, None, None),
SwqosConfig::LunarLander(
"your_hellomoon_api_key".to_string(),
SwqosRegion::Frankfurt,
None,
Some(SwqosTransport::Http),
),
];
// 创建 TradeConfig 实例
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
@@ -152,7 +178,6 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .check_min_tip(false) // 默认: false - 过滤低于最低小费的 SWQOS
// .swqos_cores_from_end(false) // 默认: false - 将 SWQOS 绑定到末尾 N 个 CPU 核心
// .mev_protection(false) // 默认: false - MEVAstralane QUIC :9000 或 HTTP mev-protect / BlockRazor
// .use_pumpfun_v2(true) // PumpFun V227 个账户,quote_mint);USDC 配对 PumpFun 币必须开启
.build();
// 创建 TradingClient
@@ -189,66 +214,107 @@ gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001,
有关所有交易参数的详细信息,请参阅 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。
```rust
// 导入 DexParamEnum 用于协议特定参数
use sol_trade_sdk::trading::core::params::DexParamEnum;
let buy_params = sol_trade_sdk::TradeBuyParams {
dex_type: DexType::PumpSwap,
input_token_type: TradeTokenType::WSOL,
mint: mint_pubkey,
input_token_amount: buy_sol_amount,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
// 使用 DexParamEnum 实现类型安全的协议参数(零开销抽象)
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
durable_nonce: None,
fixed_output_token_amount: None, // 可选:指定精确输出数量
gas_fee_strategy: gas_fee_strategy.clone(), // Gas 费用策略配置
simulate: false, // 设为 true 仅进行模拟
use_exact_sol_amount: None, // 对 PumpFun/PumpSwap 使用精确 SOL 输入(默认为 true)
use sol_trade_sdk::{
AccountPolicy, BuyAmount, DexType, SimpleBuyParams, TradeTokenType,
trading::core::params::DexParamEnum,
};
let buy_params = SimpleBuyParams::new(
DexType::PumpFun,
// 支付币种。PumpFun V2 的 SOL/WSOL quote 池,如果你想花原生 SOL,
// 这里仍然传 SOL;SDK 内部会按 V2 账户布局处理。
TradeTokenType::SOL,
// 要买入的 meme/token mint。
mint_pubkey,
// 常规 PumpFun/PumpSwap buy。SDK 先估算能买到多少 token,
// 再把滑点应用到最大 quote 成本上。
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
// 协议状态参数,通常来自 parser/RPC 缓存,例如 PumpFunParams::from_trade(...)。
DexParamEnum::PumpFun(pumpfun_params),
// 传入外部缓存的 recent_blockhashSDK 不在热路径里临时获取。
recent_blockhash,
gas_fee_strategy.clone(),
)
// 300 = 3%。
.slippage_basis_points(300)
// Bot/狙击推荐:假设 ATA 已提前准备好,交易内不创建/关闭 ATA,体积更小。
.account_policy(AccountPolicy::HotPathMinimal);
```
#### 4. 执行交易
```rust
client.buy(buy_params).await?;
client.buy_simple(buy_params).await?;
```
### ⚡ 交易参数
有关所有交易参数(包括 `TradeBuyParams` `TradeSellParams`)的详细信息,请参阅专门的 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。
新接入建议优先使用 `SimpleBuyParams` / `SimpleSellParams`。它们描述交易意图,SDK 内部处理底层 ATA 参数。多数用户只需要选择:
- `pay_with` / `receive_as`:买入时用什么 quote 支付,卖出时收什么 quote。钱包实际花/收原生 SOL 就传 `SOL`。PumpFun V2 的 SOL 配对池虽然 `quote_mint` 是 WSOL,但你想用原生 SOL 结算时这里仍传 `SOL`
- `amount`:交易数量语义。用一个枚举表达意图,不再同时理解 `input_token_amount``fixed_output_token_amount``use_exact_sol_amount`
- `account_policy`:账户创建策略。Bot 通常用 `HotPathMinimal`;普通应用可以保留默认 `Auto`
| 参数 | 含义 | 推荐场景 |
|---|---|---|
| `BuyAmount::ExactInput(amount)` | 精确花费指定 quote 数量;滑点保护最小买到数量。 | 普通买入 |
| `BuyAmount::WithMaxInput { quote_amount }` | PumpFun/PumpSwap 常规 buy,滑点作用在最大 quote 成本上。 | 狙击、套利 |
| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | 精确买到指定 token 数量,并限制最大 quote 成本。 | 精确输出 |
| `SellAmount::ExactInput(amount)` | 精确卖出指定 token 数量。 | 普通卖出 |
| `SellAmount::ExactOutput { output_amount, max_input_amount }` | 精确收到指定 quote 数量,并限制最多卖出多少 token;取决于 DEX 是否支持。 | 精确输出卖出 |
| `AccountPolicy::Auto` | SDK 按交易路径创建必要 ATA。 | 普通用户 |
| `AccountPolicy::HotPathMinimal` | 交易内避免创建/关闭 ATA。 | Bot、狙击、低延迟 |
| `AccountPolicy::CreateMissing` | 尽量在交易内创建缺失 ATA。 | 优先方便,不追求最小交易体积 |
| `AccountPolicy::AssumePrepared` | 调用方保证所有 ATA 已准备好。 | 高级确定性流程 |
可选 builder 方法:
| 方法 | 含义 |
|---|---|
| `.slippage_basis_points(300)` | 设置滑点。`300` 表示 3%。 |
| `.address_lookup_table_account(alt)` | 传入 ALT 以减少交易体积。PumpFun V2 交易较大时很有用。 |
| `.wait_tx_confirmed(true)` | 等链上确认后再返回。追求最快提交时通常关闭。 |
| `.wait_for_all_submits(true)` | 等待所有 SWQoS 通道返回,并拿到已提交签名。recent blockhash 多路交易不互斥;durable nonce 多路交易互斥。 |
| `.simulate(true)` | 只构建并模拟交易,不真正发送。 |
| `.grpc_recv_us(ts)` | 传入上游收到事件的微秒时间戳,用于延迟追踪。 |
| `.durable_nonce(nonce_info)` | 使用 durable nonce,并清空 `recent_blockhash`。如果你从 `SimpleBuyParams::new(...)` / `SimpleSellParams::new(...)` 开始构造,推荐用这个。 |
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | 直接用 durable nonce 构造参数,不使用 `recent_blockhash`。 |
| `SimpleSellParams::with_tip(false)` | 关闭卖出交易 relay tip。买入的 tip 使用 gas fee strategy 控制。 |
`TradeBuyParams``TradeSellParams` 仍保留为高级低层接口。详细说明见 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。
#### 关于shredstream
当你使用 shred 订阅事件时,由于 shred 的特性,你无法获取到交易事件的完整信息。
请你在使用时,确保你的交易逻辑依赖的参数,在shred中都能获取到。
客户端预热、blockhash/nonce、交易模式、事件状态新鲜度和 6040 重报价规则见 [低延迟 Bot 集成清单](docs/LOW_LATENCY_BOTS_CN.md)。
### 📊 使用示例汇总表格
| 描述 | 运行命令 | 源码路径 |
完整双语索引和安全分类见 [`examples/README_CN.md`](examples/README_CN.md)。
| 描述 | 运行命令 | 使用说明 |
|------|---------|----------|
| 创建和配置 TradingClient 实例 | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) |
| 多钱包共享基础设施 | `cargo run --package shared_infrastructure` | [examples/shared_infrastructure](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/shared_infrastructure/src/main.rs) |
| PumpFun 代币狙击交易 | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) |
| PumpFun 代币跟单交易 | `cargo run --package pumpfun_copy_trading` | [examples/pumpfun_copy_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_copy_trading/src/main.rs) |
| PumpSwap 交易操作 | `cargo run --package pumpswap_trading` | [examples/pumpswap_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpswap_trading/src/main.rs) |
| Raydium CPMM 交易操作 | `cargo run --package raydium_cpmm_trading` | [examples/raydium_cpmm_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/raydium_cpmm_trading/src/main.rs) |
| Raydium AMM V4 交易操作 | `cargo run --package raydium_amm_v4_trading` | [examples/raydium_amm_v4_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/raydium_amm_v4_trading/src/main.rs) |
| Meteora DAMM V2 交易操作 | `cargo run --package meteora_damm_v2_direct_trading` | [examples/meteora_damm_v2_direct_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/meteora_damm_v2_direct_trading/src/main.rs) |
| Bonk 代币狙击交易 | `cargo run --package bonk_sniper_trading` | [examples/bonk_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/bonk_sniper_trading/src/main.rs) |
| Bonk 代币跟单交易 | `cargo run --package bonk_copy_trading` | [examples/bonk_copy_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/bonk_copy_trading/src/main.rs) |
| 自定义指令中间件示例 | `cargo run --package middleware_system` | [examples/middleware_system](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/middleware_system/src/main.rs) |
| 地址查找表示例 | `cargo run --package address_lookup` | [examples/address_lookup](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/address_lookup/src/main.rs) |
| Nonce示例 | `cargo run --package nonce_cache` | [examples/nonce_cache](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/nonce_cache/src/main.rs) |
| SOL与WSOL相互转换示例 | `cargo run --package wsol_wrapper` | [examples/wsol_wrapper](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/wsol_wrapper/src/main.rs) |
| Seed 优化交易示例 | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) |
| Gas费用策略示例 | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
| 简化买卖参数 API | `cargo run --package simple_trading` | [README](examples/simple_trading/README_CN.md) |
| 创建和配置 TradingClient | `cargo run --package trading_client` | [README](examples/trading_client/README_CN.md) |
| 多钱包共享基础设施 | `cargo run --package shared_infrastructure` | [README](examples/shared_infrastructure/README_CN.md) |
| PumpFun 狙击 | `cargo run --package pumpfun_sniper_trading` | [README](examples/pumpfun_sniper_trading/README_CN.md) |
| PumpFun 跟单 | `cargo run --package pumpfun_copy_trading` | [README](examples/pumpfun_copy_trading/README_CN.md) |
| PumpSwap 低延迟事件流 | `cargo run --package pumpswap_trading` | [README](examples/pumpswap_trading/README_CN.md) |
| PumpSwap 直接 RPC 流程 | `cargo run --package pumpswap_direct_trading` | [README](examples/pumpswap_direct_trading/README_CN.md) |
| Raydium CPMM | `cargo run --package raydium_cpmm_trading` | [README](examples/raydium_cpmm_trading/README_CN.md) |
| Raydium AMM V4 | `cargo run --package raydium_amm_v4_trading` | [README](examples/raydium_amm_v4_trading/README_CN.md) |
| Meteora DAMM V2 | `cargo run --package meteora_damm_v2_direct_trading` | [README](examples/meteora_damm_v2_direct_trading/README_CN.md) |
| Bonk 狙击 | `cargo run --package bonk_sniper_trading` | [README](examples/bonk_sniper_trading/README_CN.md) |
| Bonk 跟单 | `cargo run --package bonk_copy_trading` | [README](examples/bonk_copy_trading/README_CN.md) |
| 指令中间件 | `cargo run --package middleware_system` | [README](examples/middleware_system/README_CN.md) |
| 地址查找表 | `cargo run --package address_lookup` | [README](examples/address_lookup/README_CN.md) |
| Durable nonce | `cargo run --package nonce_cache` | [README](examples/nonce_cache/README_CN.md) |
| SOL/WSOL 包装与解包 | `cargo run --package wsol_wrapper` | [README](examples/wsol_wrapper/README_CN.md) |
| Seed 优化 | `cargo run --package seed_trading` | [README](examples/seed_trading/README_CN.md) |
| Gas fee 策略 | `cargo run --package gas_fee_strategy` | [README](examples/gas_fee_strategy/README_CN.md) |
| 多 DEX CLI 模板 | `cargo run --package cli_trading` | [README](examples/cli_trading/README_CN.md) |
### ⚙️ SWQoS 服务配置说明
@@ -282,7 +348,29 @@ let temporal_config = SwqosConfig::Temporal(
- 如果没有提供自定义 URL`None`),系统将使用指定 `SwqosRegion` 的默认端点
- 这提供了最大的灵活性,同时保持向后兼容性
当使用多个MEV服务时,需要使用`Durable Nonce`你需要使用`fetch_nonce_info`函数获取最新`nonce`值,并在交易的时候将`durable_nonce`填入交易参数。
当使用多个 MEV 服务时,需要使用 `Durable Nonce`获取最新 nonce,再挂到新的 buy/sell 参数上:
```rust
use sol_trade_sdk::{fetch_nonce_info, AccountPolicy, BuyAmount, SimpleBuyParams};
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
.await
.expect("nonce account must be initialized");
let buy_params = SimpleBuyParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint_pubkey,
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
DexParamEnum::PumpFun(pumpfun_params),
recent_blockhash, // 会被 `.durable_nonce(...)` 清空
gas_fee_strategy.clone(),
)
.durable_nonce(nonce_info)
.account_policy(AccountPolicy::HotPathMinimal);
client.buy_simple(buy_params).await?;
```
#### AstralaneBinary / Plain / QUIC
@@ -334,7 +422,7 @@ PumpFun 与 PumpSwap 支持**返现(Cashback**:部分手续费可返还
- **参数来自 RPC 时**:使用 `PumpFunParams::from_mint_by_rpc``PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc` 时,SDK 会从链上读取 `is_cashback_coin`,无需额外传入。
- **参数来自事件/解析器时**:若根据交易事件(如 [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk))构建参数,**必须**把返现标志传给 SDK:
- **PumpFun**`PumpFunParams::from_trade(..., is_cashback_coin)``PumpFunParams::from_dev_trade(..., is_cashback_coin)` 最后一个参数为 `is_cashback_coin`。从解析出的事件传入(如 sol-parser-sdk 的 `PumpFunTradeEvent.is_cashback_coin`)。
- **PumpFun**`PumpFunParams::from_trade(..., mint, quote_mint, creator, ..., is_cashback_coin, mayhem_mode)``PumpFunParams::from_dev_trade(..., is_cashback_coin)` 都需要传入 `is_cashback_coin`。从解析出的事件传入(如 sol-parser-sdk 的 `PumpFunTradeEvent.is_cashback_coin`)。
- **PumpSwap**`PumpSwapParams` 有字段 `is_cashback_coin`。手动构造参数(如从池/交易事件)时,从解析到的池或事件数据中设置该字段。
- **pumpfun_copy_trading**、**pumpfun_sniper_trading** 示例使用 sol-parser-sdk 订阅 gRPC 事件,并在构造参数时传入 `e.is_cashback_coin`
- **领取返现**:使用 `client.claim_cashback_pumpfun()``client.claim_cashback_pumpswap(...)` 领取累计的返现。
@@ -361,52 +449,67 @@ SDK 不会在每次卖出时通过 RPC 拉取 creator_vault(以避免延迟)
- **sol-parser-sdk**:指令解析从账户 17、18 写入;若事件来自日志,账户填充器也会从指令补全。用 `PumpSwapParams::from_trade(..., e.coin_creator_vault_ata, e.coin_creator_vault_authority, ...)` 即可。
- **solana-streamer**:指令解析从 `accounts.get(17)``accounts.get(18)` 写入。同样用事件的 `coin_creator_vault_ata``coin_creator_vault_authority` 调用 `from_trade`
### Pump.fun Bonding Curve v2buy_v2 / sell_v2 / buy_exact_quote_in_v2
### Pump.fun Bonding Curve 统一买卖入口与 v2 指令
Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过固定账户布局同时支持 SOL 和 USDC 配对币。旧版 `buy`/`sell`/`buy_exact_sol_in` 仍可用于 SOL 配对币,且保持为默认选项。
SDK 侧调用入口保持统一:正常使用 `buy` / `sell` 流程即可,SDK 会根据 `quote_mint` 和买/卖的结算 mint 自动选择正确的链上 discriminator 和账户布局。能用 V1 的 native SOL 池会优先用 V1。
**v2 指令关键变化:**
- 新增 `quote_mint` 参数 — SOL 配对传包装 SOL`So11111111111111111111111111111111111111112`USDC 配对传 USDC mint
- 新增 `quote_mint` 参数 — native SOL 配对可能表现为默认值、Solscan SOL sentinel`So11111111111111111111111111111111111111111`或 WSOL sentinel`So11111111111111111111111111111111111111112`);USDC/其他真实 quote mint 才选择 V2
- 27 个固定账户(buy)/ 26 个固定账户(sell)— **无可选账户**
- `buyback_fee_recipient``sharing_config` 和 6 个 `associated_quote_*` ATA 变为强制账户
- SOL 配对币的报价和成本与旧版一致,无额外开销
**使用方式:**
高层 `TradingClient` 需要先在 `TradeConfig` 开启 PumpFun V2USDC 配对币必须开启
把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade``quote_mint` 不是 PDA,它就是 quote SPL mint 或 native SOL sentinel`Pubkey::default()`、Solscan SOL`So11111111111111111111111111111111111111111`)和 `WSOL_TOKEN_ACCOUNT` 都表示 native SOL 配对,正常用 SOL 结算时默认走旧版 V1;USDC 表示 USDC V2
```rust
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
.use_pumpfun_v2(true)
.build();
```
// native SOL 池:可能是 Pubkey::default()、Solscan SOL sentinel 或 WSOL sentinel
// USDC / 非 SOL 池:就是实际 quote SPL mint
let quote_mint = e.quote_mint;
然后在 `PumpFunParams` 设置 `quote_mint` 来选择 SOL/USDC 配对:
```rust
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
// SOL 配对币 — 传包装 SOL mint
let params = PumpFunParams::from_trade(/* ... */)
.with_quote_mint(WSOL_TOKEN_ACCOUNT);
// USDC 配对币(即将开放 — 必须使用 v2)
let params = PumpFunParams::from_trade(/* ... */)
.with_quote_mint(USDC_TOKEN_ACCOUNT);
let params = PumpFunParams::from_trade(
e.bonding_curve,
e.associated_bonding_curve,
e.mint,
quote_mint,
e.creator,
e.creator_vault,
e.virtual_token_reserves,
e.virtual_quote_reserves,
e.real_token_reserves,
e.real_quote_reserves,
close_token_account_when_sell,
e.fee_recipient,
e.token_program,
e.is_cashback_coin,
Some(e.mayhem_mode),
);
// 之后正常交易
client.buy(buy_params).await?;
client.sell(sell_params).await?;
```
`with_quote_mint(...)` 会把参数标记为可使用 V2,底层 instruction builder 可据此走 V2;但高层 `TradingClient` 选择 V1/V2 时使用的是 client 级别的 `use_pumpfun_v2` 运行时开关
USDC 配对币必须用 USDC 买入、卖出也结算为 USDCSOL/WSOL 只适用于 SOL 配对的 PumpFun 曲线。SOL 配对的普通热路径请传 `SOL`SDK 会用 V1;只有你明确传 `WSOL` 作为买入输入或卖出输出、希望通过已有 WSOL ATA 结算时,才会选择 V2
SDK 会在提交前拒绝 USDC quote 池的 SOL 输入,避免链上 6063 失败。
消费 parser 事件时,需要把 `quoteMint``virtualQuoteReserves``realQuoteReserves` 传进 `PumpFunParams::from_trade(...)`USDC 池初始虚拟 quote reserve 是 `4_292_000_000`
legacy SOL 事件里如果 `quote_mint` 是默认值或 Solscan SOL,并且 quote reserve 字段缺失/为 0,应回退使用 `virtual_sol_reserves` / `real_sol_reserves`
| quote_mint | use_v2_ix | 实际使用的指令 | 说明 |
|-----------|-------------|---------|------|
| 未设置(默认) | `false` | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL |
| `WSOL_TOKEN_ACCOUNT` | `true` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | SOL 配对,统一布局 |
| `USDC_TOKEN_ACCOUNT` | `true` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2 |
| quote_mint | 实际使用的指令 | 说明 |
|-----------|---------|------|
| 未设置(默认)/ `SOL_TOKEN_ACCOUNT` (`So111...11111`) / `WSOL_TOKEN_ACCOUNT` (`So111...11112`) | 优先旧版 `buy`/`sell`/`buy_exact_sol_in` | native SOL 配对;普通 SOL 结算走 V1,显式 WSOL 结算才走 V2 |
| `USDC_TOKEN_ACCOUNT` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2 |
#### PumpSwap:虚拟 quote 储备
PumpSwap 报价必须使用 `effective_quote_reserves = pool_quote_token_account.amount + virtual_quote_reserves`。Pool 账户以及 BuyEvent/SellEvent 中的 `virtual_quote_reserves` 类型均为 `i128`
- `PumpSwapParams::from_pool_address_by_rpc` 等 RPC 构造器会自动读取并应用 Pool 字段。
- 事件热路径必须把事件中的原始 `pool_quote_token_reserves``virtual_quote_reserves` 分别传给 `PumpSwapParams::from_trade(...)``from_trade_with_fee_basis_points(...)`,不要在调用前自行相加。
- SDK 在买入、卖出、报价和动态费率分层中统一使用有效储备;无效的有符号结果会返回错误,不会发生整数回绕。
## 🛡️ MEV 保护服务
@@ -418,6 +521,8 @@ client.sell(sell_params).await?;
- **BlockRazor**: 高速交易执行,支持 API 密钥认证
- **Astralane**: 区块链网络加速(Binary/Plain HTTP 与 QUIC
- **SpeedLanding**: 高速交易执行,支持 API 密钥认证
- **Node1**: 高速交易执行,支持 API 密钥认证
- **LunarLander**: HelloMoon 交易着陆服务(最低小费:0.001 SOL)
## 📁 项目结构
+23 -3
View File
@@ -19,7 +19,7 @@ Include lookup tables in your trade parameters:
```rust
let lookup_table_key = Pubkey::from_str("use_your_lookup_table_key_here").unwrap();
let address_lookup_table_account = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key).await.ok();
let alt = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key).await?;
// Include lookup table in trade parameters
let buy_params = sol_trade_sdk::TradeBuyParams {
@@ -29,7 +29,7 @@ let buy_params = sol_trade_sdk::TradeBuyParams {
slippage_basis_points: Some(100),
recent_blockhash: Some(recent_blockhash),
extension_params: Box::new(PumpFunParams::from_trade(&trade_info, None)),
address_lookup_table_account: address_lookup_table_account, // Include lookup table
address_lookup_table_accounts: vec![alt], // One ALT
wait_transaction_confirmed: true,
create_wsol_ata: false,
close_wsol_ata: false,
@@ -41,6 +41,26 @@ let buy_params = sol_trade_sdk::TradeBuyParams {
client.buy(buy_params).await?;
```
Multiple lookup tables are also supported:
```rust
let alt1 = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key_1).await?;
let alt2 = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key_2).await?;
let params = SimpleBuyParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint_pubkey,
BuyAmount::ExactInput(buy_lamports),
extension_params,
recent_blockhash,
gas_fee_strategy,
)
.address_lookup_table_accounts(vec![alt1, alt2]);
```
Use the same `address_lookup_table_accounts` field for one or many ALTs: `vec![alt]` for a single ALT, `vec![alt1, alt2]` for multiple ALTs.
## 📊 Performance Comparison
| Aspect | Without ALT | With ALT | Improvement |
@@ -64,4 +84,4 @@ client.buy(buy_params).await?;
## 📚 External Resources
- [Solana Address Lookup Tables Documentation](https://docs.solana.com/developing/lookup-tables)
- [Solana Address Lookup Tables Documentation](https://docs.solana.com/developing/lookup-tables)
+23 -3
View File
@@ -19,7 +19,7 @@
```rust
let lookup_table_key = Pubkey::from_str("use_your_lookup_table_key_here").unwrap();
let address_lookup_table_account = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key).await.ok();
let alt = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key).await?;
// 在交易参数中包含查找表
let buy_params = sol_trade_sdk::TradeBuyParams {
@@ -29,7 +29,7 @@ let buy_params = sol_trade_sdk::TradeBuyParams {
slippage_basis_points: Some(100),
recent_blockhash: Some(recent_blockhash),
extension_params: Box::new(PumpFunParams::from_trade(&trade_info, None)),
address_lookup_table_account: address_lookup_table_account, // 包含查找表
address_lookup_table_accounts: vec![alt], // 1 个 ALT
wait_transaction_confirmed: true,
create_wsol_ata: false,
close_wsol_ata: false,
@@ -41,6 +41,26 @@ let buy_params = sol_trade_sdk::TradeBuyParams {
client.buy(buy_params).await?;
```
也支持同时传入多个查找表:
```rust
let alt1 = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key_1).await?;
let alt2 = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key_2).await?;
let params = SimpleBuyParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint_pubkey,
BuyAmount::ExactInput(buy_lamports),
extension_params,
recent_blockhash,
gas_fee_strategy,
)
.address_lookup_table_accounts(vec![alt1, alt2]);
```
单 ALT 和多 ALT 都使用同一个 `address_lookup_table_accounts` 字段:单 ALT 传 `vec![alt]`,多 ALT 传 `vec![alt1, alt2]`
## 📊 性能对比
| 方面 | 不使用 ALT | 使用 ALT | 改进幅度 |
@@ -64,4 +84,4 @@ client.buy(buy_params).await?;
## 📚 外部资源
- [Solana 地址查找表文档](https://docs.solana.com/developing/lookup-tables)
- [Solana 地址查找表文档](https://docs.solana.com/developing/lookup-tables)
+34
View File
@@ -0,0 +1,34 @@
# Low-Latency Bot Integration Checklist
Before subscription, initialize and warm `SolanaTrade`, RPC and SWQoS clients, a background blockhash cache or durable nonce pool, known ATAs, and ALTs. Restore signature/instruction deduplication and position state before accepting events.
The event hot path should be limited to:
```text
filter -> deduplicate -> reject stale event -> map post-trade state -> Simple*Params -> sign -> submit
```
Do not initialize clients, synchronously fetch a blockhash, query balances, or search for pools in this path. An RPC fallback is valid for incomplete shred data but is no longer a purely low-latency path.
## Trade intent
| Goal | Parameter |
|---|---|
| Exact spend with minimum-output protection | `BuyAmount::ExactInput` |
| Fill-priority sniping/arbitrage with maximum-cost protection | `BuyAmount::WithMaxInput` |
| Exact token output with maximum-input protection | `BuyAmount::ExactOutput` |
| Sell an exact token amount | `SellAmount::ExactInput` |
`WithMaxInput` still enforces slippage. Never use `min_out = 0` as routine error handling.
Exact-output support is protocol- and pool-direction-specific. PumpSwap exposes exact output through its on-chain `buy` instruction, but its `sell` instruction accepts exact base input plus minimum quote output; the SDK rejects `SellAmount::ExactOutput` when that direction would require `sell`.
Use post-trade event reserves. Preserve PumpFun quote mint, creator/vault, token program, cashback, and mayhem fields. PumpSwap event integrations should use `from_trade_with_fee_basis_points`. Refresh delayed sells because the triggering trade and your own buy both change pool state. Durable nonce extends transaction validity, not quote validity.
For `BuySlippageBelowMinBaseAmountOut`, discard the old transaction, obtain newer reserves and fee rates, enforce a quote-age limit, and rebuild only within a bounded retry policy.
After a submit timeout or ambiguous relay error, reconcile the signature and position before retrying. A retry policy may rebuild quotes automatically only when the previous transaction is known not to have been submitted.
Reference examples:
- `fnzero-examples/pumpfun_grpc_sniper`
- `fnzero-examples/pumpfun_shredstream_sniper`
- `examples/pumpswap_trading`
+45
View File
@@ -0,0 +1,45 @@
# 低延迟 Bot 集成清单
## 启动阶段
- 创建并预热 `SolanaTrade`、RPC 和全部 SWQoS 客户端。
- 启动后台 blockhash cache,或准备并持续刷新 durable nonce pool。
- 对已知 mint 准备 ATA、WSOL ATA 和 ALT。
- 建立签名 + 指令索引去重,并恢复持仓状态。
## 事件热路径
```text
过滤 -> 去重 -> 事件年龄检查 -> 映射成交后状态 -> Simple*Params -> 签名 -> 提交
```
事件处理期间不要初始化客户端、同步查询 blockhash、查余额或搜索池。Shred 缺少必需参数时可以 RPC 回退,但要明确该路径不再是纯低延迟路径。
## 交易意图
| 目标 | 参数 |
|---|---|
| 固定花费,保护最小输出 | `BuyAmount::ExactInput` |
| 狙击/套利优先成交,保护最大成本 | `BuyAmount::WithMaxInput` |
| 固定买到数量,限制最大输入 | `BuyAmount::ExactOutput` |
| 卖出固定 token 数量 | `SellAmount::ExactInput` |
`WithMaxInput` 仍有滑点保护。不要通过设置 `min_out = 0` 处理滑点错误。
## 状态和费率
- 使用解析事件中的成交后 reserves。
- PumpFun 保留 quote mint、creator、creator vault、token program、cashback 和 mayhem 字段。
- PumpSwap 使用 `from_trade_with_fee_basis_points` 传入最新储备和动态费率。
- 自己的买入会改变池状态;延迟卖出前必须使用更新后的流缓存或 RPC 快照。
- durable nonce 只延长交易有效期,不会延长报价有效期。
## 6040 重报价
发生 `BuySlippageBelowMinBaseAmountOut` 时,丢弃旧交易,取得更新的储备和费率,检查报价年龄,然后在业务配置的有限次数内重建。没有新状态时,不要通过不断放大滑点重发同一报价。
参考示例:
- `fnzero-examples/pumpfun_grpc_sniper`
- `fnzero-examples/pumpfun_shredstream_sniper`
- `examples/pumpswap_trading`
-1
View File
@@ -49,7 +49,6 @@ let buy_params = sol_trade_sdk::TradeBuyParams {
slippage_basis_points: Some(100),
recent_blockhash: Some(recent_blockhash),
extension_params: Box::new(PumpFunParams::from_trade(&trade_info, None)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
create_wsol_ata: false,
close_wsol_ata: false,
-1
View File
@@ -49,7 +49,6 @@ let buy_params = sol_trade_sdk::TradeBuyParams {
slippage_basis_points: Some(100),
recent_blockhash: Some(recent_blockhash),
extension_params: Box::new(PumpFunParams::from_trade(&trade_info, None)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
create_wsol_ata: false,
close_wsol_ata: false,
+93 -5
View File
@@ -4,14 +4,104 @@ This document provides a comprehensive reference for all trading parameters used
## 📋 Table of Contents
- [SimpleBuyParams / SimpleSellParams](#simplebuyparams--simplesellparams)
- [TradeBuyParams](#tradebuyparams)
- [TradeSellParams](#tradesellparams)
- [Parameter Categories](#parameter-categories)
- [Important Notes](#important-notes)
## SimpleBuyParams / SimpleSellParams
Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the public API focused on trading intent and map to the lower-level `TradeBuyParams` / `TradeSellParams` internally.
### SimpleBuyParams
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `dex_type` | `DexType` | ✅ | Protocol to trade through, for example `DexType::PumpFun`. |
| `pay_with` | `TradeTokenType` | ✅ | Quote token used to pay for the buy. Use `SOL` when the wallet spends native SOL. For PumpFun V2 SOL/WSOL quote pools, still use `SOL` if you want native SOL settlement. |
| `mint` | `Pubkey` | ✅ | Mint of the token being bought. |
| `amount` | `BuyAmount` | ✅ | Buy sizing intent. Choose one enum variant instead of combining low-level amount flags. |
| `extension_params` | `DexParamEnum` | ✅ | Protocol state from parser/RPC cache, such as `DexParamEnum::PumpFun(PumpFunParams::from_trade(...))`. |
| `recent_blockhash` | `Hash` | ✅ for `new` | Cached recent blockhash for non-nonce transactions. The SDK does not fetch this on the hot path. |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Compute unit price/limit and relay tip configuration. |
| `slippage_basis_points` | `Option<u64>` | ❌ | Optional slippage override. `100` means 1%. |
| `account_policy` | `AccountPolicy` | ❌ | ATA creation/close behavior. Default is `Auto`. |
| `address_lookup_table_accounts` | `Vec<AddressLookupTableAccount>` | ❌ | Optional ALT list. Pass one element for a single ALT or multiple elements for multi-ALT to reduce transaction size. |
| `wait_tx_confirmed` | `bool` | ❌ | Whether to wait for chain confirmation before returning. Default is `false`. |
| `wait_for_all_submits` | `bool` | ❌ | Wait for every SWQoS lane response and return submitted signatures; useful for poll-any confirmation or external monitoring. Recent-blockhash route variants are not mutually exclusive; durable nonce variants are. |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `.durable_nonce(nonce_info)` or `SimpleBuyParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
| `simulate` | `bool` | ❌ | Build and simulate instead of submitting. Default is `false`. |
| `grpc_recv_us` | `Option<i64>` | ❌ | Upstream receive timestamp in microseconds for latency tracing. |
### SimpleSellParams
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `dex_type` | `DexType` | ✅ | Protocol to trade through, for example `DexType::PumpFun`. |
| `receive_as` | `TradeTokenType` | ✅ | Quote token to receive from the sell. Use `SOL` when you want native SOL output. |
| `mint` | `Pubkey` | ✅ | Mint of the token being sold. |
| `amount` | `SellAmount` | ✅ | Sell sizing intent. |
| `extension_params` | `DexParamEnum` | ✅ | Protocol state from parser/RPC cache. |
| `recent_blockhash` | `Hash` | ✅ for `new` | Cached recent blockhash for non-nonce transactions. |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Compute unit price/limit and relay tip configuration. |
| `slippage_basis_points` | `Option<u64>` | ❌ | Optional slippage override. `100` means 1%. |
| `account_policy` | `AccountPolicy` | ❌ | ATA creation/close behavior. Default is `Auto`. |
| `address_lookup_table_accounts` | `Vec<AddressLookupTableAccount>` | ❌ | Optional ALT list. Pass one element for a single ALT or multiple elements for multi-ALT to reduce transaction size. |
| `wait_tx_confirmed` | `bool` | ❌ | Whether to wait for chain confirmation before returning. Default is `false`. |
| `wait_for_all_submits` | `bool` | ❌ | Wait for every SWQoS lane response and return submitted signatures; useful for poll-any confirmation or external monitoring. Recent-blockhash route variants are not mutually exclusive; durable nonce variants are. |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `.durable_nonce(nonce_info)` or `SimpleSellParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
| `simulate` | `bool` | ❌ | Build and simulate instead of submitting. Default is `false`. |
| `with_tip` | `bool` | ❌ | Whether sells include relay tips. Default is `true`; set with `.with_tip(false)`. |
| `grpc_recv_us` | `Option<i64>` | ❌ | Upstream receive timestamp in microseconds for latency tracing. |
### Amount Selection
| Variant | Meaning | Low-level mapping |
|---------|---------|-------------------|
| `BuyAmount::ExactInput(amount)` | Spend exactly this quote amount; slippage protects minimum token output. | `input_token_amount = amount`, `use_exact_sol_amount = Some(true)` |
| `BuyAmount::WithMaxInput { quote_amount }` | Regular PumpFun/PumpSwap buy. The SDK estimates output and applies slippage to max quote cost. | `input_token_amount = quote_amount`, `use_exact_sol_amount = Some(false)` |
| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | Buy an exact token amount while limiting max quote input. | `fixed_output_token_amount = Some(output_amount)`, `input_token_amount = max_input_amount` |
| `SellAmount::ExactInput(amount)` | Sell exactly this token amount; slippage protects minimum quote output. | `input_token_amount = amount` |
| `SellAmount::ExactOutput { output_amount, max_input_amount }` | Receive an exact quote amount while limiting token input, where supported. | `fixed_output_token_amount = Some(output_amount)`, `input_token_amount = max_input_amount` |
### AccountPolicy
| Variant | Behavior | Use when |
|---------|----------|----------|
| `Auto` | SDK creates practical ATAs when needed. Buy creates the target mint ATA; sell creates the output ATA for non-SOL outputs. | Normal apps and manual trading tools. |
| `HotPathMinimal` | No ATA create/close instructions in the trade transaction. | Bots, sniping, arbitrage, and any path sensitive to transaction size. |
| `CreateMissing` | Include ATA creation where possible. | Convenience matters more than smallest transaction size. |
| `AssumePrepared` | Do not create or close token accounts; caller prepared everything. | Advanced deterministic flows. |
### Durable Nonce With Simple Params
`fetch_nonce_info` and `DurableNonceInfo` are re-exported from the crate root:
```rust
use sol_trade_sdk::{fetch_nonce_info, SimpleBuyParams};
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
.await
.expect("nonce account must be initialized");
let buy_params = SimpleBuyParams::new(
dex_type,
pay_with,
mint,
amount,
extension_params,
recent_blockhash,
gas_fee_strategy,
)
.durable_nonce(nonce_info);
```
Calling `.durable_nonce(...)` clears `recent_blockhash`; nonce transactions use the nonce value as the transaction blockhash.
## TradeBuyParams
The `TradeBuyParams` struct contains all parameters required for executing buy orders across different DEX protocols.
`TradeBuyParams` is the advanced low-level buy API. New integrations should prefer `SimpleBuyParams` unless they need direct control over individual ATA flags.
### Basic Trading Parameters
@@ -29,7 +119,6 @@ The `TradeBuyParams` struct contains all parameters required for executing buy o
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Address lookup table for transaction optimization |
| `wait_tx_confirmed` | `bool` | ✅ | Whether to wait for transaction confirmation |
| `create_input_token_ata` | `bool` | ✅ | Whether to create input token Associated Token Account |
| `close_input_token_ata` | `bool` | ✅ | Whether to close input token ATA after transaction |
@@ -61,7 +150,6 @@ The `TradeSellParams` struct contains all parameters required for executing sell
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `address_lookup_table_account` | `Option<Pubkey>` | ❌ | Address lookup table for transaction optimization |
| `wait_tx_confirmed` | `bool` | ✅ | Whether to wait for transaction confirmation |
| `create_output_token_ata` | `bool` | ✅ | Whether to create output token Associated Token Account |
| `close_output_token_ata` | `bool` | ✅ | Whether to close output token ATA after transaction |
@@ -102,7 +190,7 @@ These parameters control automatic account creation and management:
These parameters enable advanced optimizations:
- **address_lookup_table_account**: Use address lookup tables for reduced transaction size
- **address_lookup_table_accounts**: Use one or more address lookup tables for reduced transaction size
### 🔄 Token Type Parameters
@@ -146,7 +234,7 @@ The account management parameters provide granular control:
### 🔍 Address Lookup Tables
Before using `address_lookup_table_account`:
Before using `address_lookup_table_accounts`:
- Lookup tables reduce transaction size and improve success rates
- Particularly beneficial for complex transactions with many account references
+93 -5
View File
@@ -4,14 +4,104 @@
## 📋 目录
- [SimpleBuyParams / SimpleSellParams](#simplebuyparams--simplesellparams)
- [TradeBuyParams](#tradebuyparams)
- [TradeSellParams](#tradesellparams)
- [参数分类](#参数分类)
- [重要说明](#重要说明)
## SimpleBuyParams / SimpleSellParams
新接入优先使用 `SimpleBuyParams``SimpleSellParams`。这两个结构体描述交易意图,SDK 内部会转换成低层 `TradeBuyParams` / `TradeSellParams`
### SimpleBuyParams
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `dex_type` | `DexType` | ✅ | 使用哪个协议交易,例如 `DexType::PumpFun`。 |
| `pay_with` | `TradeTokenType` | ✅ | 买入时用什么 quote 支付。钱包实际花原生 SOL 就传 `SOL`。PumpFun V2 的 SOL/WSOL quote 池,如果你想用原生 SOL 结算,也仍然传 `SOL`。 |
| `mint` | `Pubkey` | ✅ | 要买入的 token mint。 |
| `amount` | `BuyAmount` | ✅ | 买入数量语义。选择一个枚举,不再组合多个低层数量字段。 |
| `extension_params` | `DexParamEnum` | ✅ | 协议状态参数,来自 parser/RPC 缓存,例如 `DexParamEnum::PumpFun(PumpFunParams::from_trade(...))`。 |
| `recent_blockhash` | `Hash` | ✅,使用 `new` 时 | 非 nonce 交易使用的 recent blockhash。SDK 不会在热路径临时获取。 |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | CU price/limit 和 relay tip 配置。 |
| `slippage_basis_points` | `Option<u64>` | ❌ | 可选滑点覆盖。`100` 表示 1%。 |
| `account_policy` | `AccountPolicy` | ❌ | ATA 创建/关闭策略。默认 `Auto`。 |
| `address_lookup_table_accounts` | `Vec<AddressLookupTableAccount>` | ❌ | 可选 ALT 列表。传 1 个元素表示单 ALT,传多个元素表示多 ALT,用于减少交易体积。 |
| `wait_tx_confirmed` | `bool` | ❌ | 是否等链上确认后再返回。默认 `false`。 |
| `wait_for_all_submits` | `bool` | ❌ | 是否等待所有 SWQoS 通道返回并拿到已提交签名;适合 poll-any 确认或外部监控。recent blockhash 多路交易不互斥;durable nonce 多路交易互斥。 |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `.durable_nonce(nonce_info)``SimpleBuyParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 |
| `grpc_recv_us` | `Option<i64>` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 |
### SimpleSellParams
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `dex_type` | `DexType` | ✅ | 使用哪个协议交易,例如 `DexType::PumpFun`。 |
| `receive_as` | `TradeTokenType` | ✅ | 卖出后接收什么 quote。想收原生 SOL 就传 `SOL`。 |
| `mint` | `Pubkey` | ✅ | 要卖出的 token mint。 |
| `amount` | `SellAmount` | ✅ | 卖出数量语义。 |
| `extension_params` | `DexParamEnum` | ✅ | 协议状态参数,来自 parser/RPC 缓存。 |
| `recent_blockhash` | `Hash` | ✅,使用 `new` 时 | 非 nonce 交易使用的 recent blockhash。 |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | CU price/limit 和 relay tip 配置。 |
| `slippage_basis_points` | `Option<u64>` | ❌ | 可选滑点覆盖。`100` 表示 1%。 |
| `account_policy` | `AccountPolicy` | ❌ | ATA 创建/关闭策略。默认 `Auto`。 |
| `address_lookup_table_accounts` | `Vec<AddressLookupTableAccount>` | ❌ | 可选 ALT 列表。传 1 个元素表示单 ALT,传多个元素表示多 ALT,用于减少交易体积。 |
| `wait_tx_confirmed` | `bool` | ❌ | 是否等链上确认后再返回。默认 `false`。 |
| `wait_for_all_submits` | `bool` | ❌ | 是否等待所有 SWQoS 通道返回并拿到已提交签名;适合 poll-any 确认或外部监控。recent blockhash 多路交易不互斥;durable nonce 多路交易互斥。 |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `.durable_nonce(nonce_info)``SimpleSellParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 |
| `with_tip` | `bool` | ❌ | 卖出交易是否带 relay tip。默认 `true`,可通过 `.with_tip(false)` 关闭。 |
| `grpc_recv_us` | `Option<i64>` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 |
### 数量如何选择
| 枚举 | 含义 | 底层映射 |
|------|------|----------|
| `BuyAmount::ExactInput(amount)` | 精确花费指定 quote 数量,滑点保护最小买到 token 数量。 | `input_token_amount = amount``use_exact_sol_amount = Some(true)` |
| `BuyAmount::WithMaxInput { quote_amount }` | 常规 PumpFun/PumpSwap buy。SDK 估算输出,并把滑点作用在最大 quote 成本上。 | `input_token_amount = quote_amount``use_exact_sol_amount = Some(false)` |
| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | 精确买到指定 token 数量,并限制最多花多少 quote。 | `fixed_output_token_amount = Some(output_amount)``input_token_amount = max_input_amount` |
| `SellAmount::ExactInput(amount)` | 精确卖出指定 token 数量,滑点保护最少收到 quote 数量。 | `input_token_amount = amount` |
| `SellAmount::ExactOutput { output_amount, max_input_amount }` | 精确收到指定 quote 数量,并限制最多卖出多少 token;取决于 DEX 是否支持。 | `fixed_output_token_amount = Some(output_amount)``input_token_amount = max_input_amount` |
### AccountPolicy
| 枚举 | 行为 | 适用场景 |
|------|------|----------|
| `Auto` | SDK 按实际路径创建必要 ATA。买入会创建目标 token ATA;卖出接收非 SOL 时会创建输出 ATA。 | 普通应用、手动交易工具。 |
| `HotPathMinimal` | 交易内不创建/关闭 ATA。 | Bot、狙击、套利、对交易体积敏感的路径。 |
| `CreateMissing` | 尽量在交易内创建缺失 ATA。 | 更重视方便,不追求最小交易体积。 |
| `AssumePrepared` | 不创建也不关闭 token account,调用方保证都已准备好。 | 高级确定性流程。 |
### Simple 参数使用 Durable Nonce
`fetch_nonce_info``DurableNonceInfo` 已从 crate root 重新导出:
```rust
use sol_trade_sdk::{fetch_nonce_info, SimpleBuyParams};
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
.await
.expect("nonce account must be initialized");
let buy_params = SimpleBuyParams::new(
dex_type,
pay_with,
mint,
amount,
extension_params,
recent_blockhash,
gas_fee_strategy,
)
.durable_nonce(nonce_info);
```
调用 `.durable_nonce(...)` 会清空 `recent_blockhash`nonce 交易会使用 nonce value 作为 transaction blockhash。
## TradeBuyParams
`TradeBuyParams` 结构体包含在不同 DEX 协议上执行买入订单所需的所有参数
`TradeBuyParams` 是高级低层买入 API。新接入建议优先使用 `SimpleBuyParams`,只有需要直接控制单个 ATA flag 时再使用它
### 基础交易参数
@@ -29,7 +119,6 @@
| 参数 | 类型 | 必需 | 描述 |
|------|------|------|------|
| `address_lookup_table_account` | `Option<Pubkey>` | ❌ | 用于交易优化的地址查找表 |
| `wait_tx_confirmed` | `bool` | ✅ | 是否等待交易确认 |
| `create_input_token_ata` | `bool` | ✅ | 是否创建输入代币关联代币账户 |
| `close_input_token_ata` | `bool` | ✅ | 交易后是否关闭输入代币 ATA |
@@ -61,7 +150,6 @@
| 参数 | 类型 | 必需 | 描述 |
|------|------|------|------|
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 用于交易优化的地址查找表 |
| `wait_tx_confirmed` | `bool` | ✅ | 是否等待交易确认 |
| `create_output_token_ata` | `bool` | ✅ | 是否创建输出代币关联代币账户 |
| `close_output_token_ata` | `bool` | ✅ | 交易后是否关闭输出代币 ATA |
@@ -102,7 +190,7 @@
这些参数启用高级优化:
- **address_lookup_table_account**: 使用地址查找表减少交易大小
- **address_lookup_table_accounts**: 使用一个或多个地址查找表减少交易大小
### 🔄 代币类型参数
@@ -146,7 +234,7 @@ let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
### 🔍 地址查找表
使用 `address_lookup_table_account` 之前:
使用 `address_lookup_table_accounts` 之前:
- 查找表减少交易大小并提高成功率
- 对于有许多账户引用的复杂交易特别有益
+26
View File
@@ -0,0 +1,26 @@
# sol-trade-sdk Examples
[中文](README_CN.md)
Run commands from the repository root with `cargo run --package <name>`. Read the example's README before running it: many protocol examples contain placeholder keys or addresses and submit real mainnet transactions after configuration.
## Choose an example
| Category | Examples |
|---|---|
| Start here | `simple_trading`, `trading_client`, `shared_infrastructure` |
| Low latency | `pumpswap_trading`; also read `../docs/LOW_LATENCY_BOTS.md` |
| PumpFun streams | `pumpfun_sniper_trading`, `pumpfun_copy_trading` |
| Other DEXs | `pumpswap_direct_trading`, `bonk_*`, `raydium_*`, `meteora_*` |
| Transaction construction | `address_lookup`, `nonce_cache`, `middleware_system`, `seed_trading` |
| Utilities | `gas_fee_strategy`, `wsol_wrapper`, `cli_trading` |
## Safety boundary
- Files containing `use_your_*` or `your_*_here` are templates. Replace every placeholder before running.
- `simple_trading` and `gas_fee_strategy` do not submit a swap as shipped. Most other trading examples can.
- Event-driven templates that create the client or fetch blockhash/state inside the callback demonstrate protocol mapping, not the final low-latency architecture.
- Never commit private keys. Use environment variables or a secure keystore in real applications.
- Use `SimpleBuyParams` / `SimpleSellParams` for new integrations unless low-level account flags are specifically required.
Each example directory contains matching English and Chinese documentation.
+26
View File
@@ -0,0 +1,26 @@
# sol-trade-sdk 示例索引
[English](README.md)
请在仓库根目录使用 `cargo run --package <name>`。运行前先阅读对应示例的 README:很多协议示例包含占位私钥或地址,配置完成后会提交真实主网交易。
## 如何选择
| 分类 | 示例 |
|---|---|
| 入门 | `simple_trading``trading_client``shared_infrastructure` |
| 低延迟 | `pumpswap_trading`;同时阅读 `../docs/LOW_LATENCY_BOTS_CN.md` |
| PumpFun 事件流 | `pumpfun_sniper_trading``pumpfun_copy_trading` |
| 其他 DEX | `pumpswap_direct_trading``bonk_*``raydium_*``meteora_*` |
| 交易构造 | `address_lookup``nonce_cache``middleware_system``seed_trading` |
| 工具 | `gas_fee_strategy``wsol_wrapper``cli_trading` |
## 安全边界
- 含 `use_your_*``your_*_here` 的源码都是模板,运行前必须替换全部占位值。
- `simple_trading``gas_fee_strategy` 默认不会提交 swap;多数其他交易示例配置后会发真实交易。
- 若事件回调内仍创建客户端或同步查询 blockhash/状态,该示例只展示协议参数映射,不代表最终低延迟架构。
- 不要提交私钥。真实应用应使用环境变量或安全 keystore。
- 新接入优先使用 `SimpleBuyParams` / `SimpleSellParams`,只有必须控制低层账户 flag 时才使用低层参数。
每个示例目录都提供内容对应的英文和中文文档。
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = "0.2.2"
sol-parser-sdk = { version = "0.6.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "995d88991b56234a23fc1d0911fdd33caa063c67" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+17
View File
@@ -0,0 +1,17 @@
# Address Lookup Table
[中文](README_CN.md)
Shows how to fetch an Address Lookup Table (ALT), attach it to a PumpFun transaction, and submit after receiving a `sol-parser-sdk` gRPC event.
> This is a live-transaction template. Set `PRIVATE_KEY`, replace `use_your_lookup_table_key_here`, and configure RPC and gRPC before running.
```bash
export GRPC_ENDPOINT=https://your-yellowstone.example
export GRPC_AUTH_TOKEN=optional-token
cargo run --package address_lookup
```
An ALT reduces message size but does not make stale pool state valid. Fetch and cache known ALTs before the event hot path. The current example initializes the trading client after an event and is therefore an API demonstration, not the recommended low-latency structure.
See [Address Lookup Tables](../../docs/ADDRESS_LOOKUP_TABLE.md).
+17
View File
@@ -0,0 +1,17 @@
# 地址查找表(ALT
[English](README.md)
展示如何获取 Address Lookup Table、附加到 PumpFun 交易,并在收到 `sol-parser-sdk` gRPC 事件后提交。
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`,替换 `use_your_lookup_table_key_here`,并配置 RPC 和 gRPC。
```bash
export GRPC_ENDPOINT=https://your-yellowstone.example
export GRPC_AUTH_TOKEN=optional-token
cargo run --package address_lookup
```
ALT 只能缩小消息体积,不能让过期池状态重新有效。已知 ALT 应在事件热路径前获取并缓存。当前示例在事件后初始化交易客户端,因此只用于展示 API,不代表推荐低延迟结构。
参见[地址查找表说明](../../docs/ADDRESS_LOOKUP_TABLE_CN.md)。
+19 -9
View File
@@ -24,7 +24,6 @@ use sol_trade_sdk::{
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
@@ -105,7 +104,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
@@ -130,14 +129,23 @@ async fn pumpfun_copy_trade_with_grpc(
let client = create_solana_trade_client().await?;
let mint_pubkey = trade_info.mint;
let virtual_quote_reserves = if trade_info.virtual_quote_reserves != 0 {
trade_info.virtual_quote_reserves
} else {
trade_info.virtual_sol_reserves
};
let real_quote_reserves = if trade_info.virtual_quote_reserves != 0 {
trade_info.real_quote_reserves
} else {
trade_info.real_sol_reserves
};
let slippage_basis_points = Some(100);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let lookup_table_key = Pubkey::from_str("use_your_lookup_table_key_here").unwrap();
let address_lookup_table_account =
fetch_address_lookup_table_account(&client.infrastructure.rpc, &lookup_table_key)
.await
.ok();
let alt = fetch_address_lookup_table_account(&client.infrastructure.rpc, &lookup_table_key)
.await
.ok();
let gas_fee_strategy = GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
@@ -154,20 +162,22 @@ async fn pumpfun_copy_trade_with_grpc(
trade_info.bonding_curve,
trade_info.associated_bonding_curve,
trade_info.mint,
trade_info.quote_mint,
trade_info.creator,
trade_info.creator_vault,
trade_info.virtual_token_reserves,
trade_info.virtual_sol_reserves,
virtual_quote_reserves,
trade_info.real_token_reserves,
trade_info.real_sol_reserves,
real_quote_reserves,
None,
trade_info.fee_recipient,
trade_info.token_program,
trade_info.is_cashback_coin,
Some(trade_info.mayhem_mode),
)),
address_lookup_table_account,
address_lookup_table_accounts: alt.into_iter().collect(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+13
View File
@@ -0,0 +1,13 @@
# Bonk Copy Trading
[中文](README_CN.md)
Consumes Bonk events through `solana-streamer-sdk`, maps event state to SDK protocol params, then performs one follow-up buy/sell flow.
> This is a live-transaction template. Set `PRIVATE_KEY`, RPC/stream configuration, target policy, amounts, slippage, and fee limits before running.
```bash
cargo run --package bonk_copy_trading
```
Add signature/instruction deduplication, event-age checks, wallet and mint allowlists, position isolation, and maximum input/loss limits. Delayed sells require current state and a fresh blockhash; do not reuse the trigger snapshot.
+13
View File
@@ -0,0 +1,13 @@
# Bonk 跟单交易
[English](README.md)
通过 `solana-streamer-sdk` 消费 Bonk 事件,把事件状态映射为 SDK 协议参数,再执行一次跟随买卖流程。
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`、RPC/stream 配置、目标策略、金额、滑点和费用上限。
```bash
cargo run --package bonk_copy_trading
```
必须增加签名/指令去重、事件年龄检查、钱包和 mint allowlist、持仓隔离及最大输入/亏损限制。延迟卖出需要最新状态和新 blockhash,不能复用触发快照。
+59 -65
View File
@@ -3,10 +3,8 @@ use std::sync::{
Arc,
};
use sol_trade_sdk::common::GasFeeStrategy;
use sol_trade_sdk::common::TradeConfig;
use sol_trade_sdk::common::{
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, GasFeeStrategy,
};
use sol_trade_sdk::{
common::AnyResult,
swqos::SwqosConfig,
@@ -17,15 +15,12 @@ use sol_trade_sdk::{
SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer;
use solana_streamer_sdk::match_event;
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
use solana_streamer_sdk::streaming::event_parser::common::EventType;
use solana_streamer_sdk::streaming::event_parser::protocols::bonk::parser::BONK_PROGRAM_ID;
use solana_streamer_sdk::streaming::event_parser::protocols::bonk::BonkTradeEvent;
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
use solana_streamer_sdk::streaming::yellowstone_grpc::TransactionFilter;
use solana_streamer_sdk::streaming::YellowstoneGrpc;
// Global static flag to ensure transaction is executed only once
@@ -56,24 +51,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
account_required,
};
// Listen to account data belonging to owner programs -> account event monitoring
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
// listen to specific event type
let event_type_filter = EventTypeFilter {
include: vec![
EventType::BonkBuyExactIn,
EventType::BonkSellExactIn,
EventType::BonkBuyExactOut,
EventType::BonkSellExactOut,
],
};
let event_type_filter = EventTypeFilter::include_only(vec![
EventType::BonkBuyExactIn,
EventType::BonkSellExactIn,
EventType::BonkBuyExactOut,
EventType::BonkSellExactOut,
]);
grpc.subscribe_events_immediate(
protocols,
None,
vec![transaction_filter],
vec![account_filter],
vec![],
Some(event_type_filter),
None,
callback,
@@ -81,27 +71,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?;
tokio::signal::ctrl_c().await?;
grpc.stop().await;
Ok(())
}
/// Create an event callback function that handles different types of events
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
match_event!(event, {
BonkTradeEvent => |e: BonkTradeEvent| {
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let event_clone = e.clone();
tokio::spawn(async move {
if let Err(err) = bonk_copy_trade_with_grpc(event_clone).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(0);
}
});
fn create_event_callback() -> impl Fn(DexEvent) {
|event: DexEvent| {
let DexEvent::BonkTradeEvent(event) = event else {
return;
};
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
tokio::spawn(async move {
if let Err(err) = bonk_copy_trade_with_grpc(event).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(1);
}
},
});
});
}
}
}
@@ -109,7 +97,7 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
/// Initializes a new SolanaTrade client with configuration
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
@@ -147,6 +135,9 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
} else {
sol_trade_sdk::TradeTokenType::SOL
};
let balance_before = client
.get_payer_token_balance_with_program(&mint_pubkey, &trade_info.base_token_program)
.await?;
let buy_sol_amount = 100_000;
let buy_params = sol_trade_sdk::TradeBuyParams {
dex_type: DexType::Bonk,
@@ -169,8 +160,9 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
trade_info.creator_associated_account,
trade_info.global_config,
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: true,
@@ -181,22 +173,31 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
let (ok, sigs, err, _) = client.buy(buy_params).await?;
if !ok {
return Err(
std::io::Error::other(format!("buy failed: {:?}; sigs: {:?}", err, sigs)).into()
);
}
// Sell tokens
println!("Selling tokens from Bonk...");
let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey();
let account = get_associated_token_address_with_program_id_fast_use_seed(
&payer,
let balance_after = client
.get_payer_token_balance_with_program(&mint_pubkey, &trade_info.base_token_program)
.await?;
let amount_token = balance_after
.checked_sub(balance_before)
.ok_or_else(|| std::io::Error::other("token balance decreased after buy"))?;
if amount_token == 0 {
return Err(std::io::Error::other("confirmed buy did not increase token balance").into());
}
let sell_extension = BonkParams::from_mint_by_rpc(
&client.infrastructure.rpc,
&mint_pubkey,
&trade_info.base_token_program,
client.use_seed_optimize,
);
let balance = rpc.get_token_account_balance(&account).await?;
println!("Balance: {:?}", balance);
let amount_token = balance.amount.parse::<u64>().unwrap();
trade_info.quote_token_mint == sol_trade_sdk::constants::USD1_TOKEN_ACCOUNT,
)
.await?;
println!("Selling {} tokens", amount_token);
let sell_params = sol_trade_sdk::TradeSellParams {
@@ -205,23 +206,11 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
mint: mint_pubkey,
input_token_amount: amount_token,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::Bonk(BonkParams::from_trade(
trade_info.virtual_base,
trade_info.virtual_quote,
trade_info.real_base_after,
trade_info.real_quote_after,
trade_info.pool_state,
trade_info.base_vault,
trade_info.quote_vault,
trade_info.base_token_program,
trade_info.platform_config,
trade_info.platform_associated_account,
trade_info.creator_associated_account,
trade_info.global_config,
)),
address_lookup_table_account: None,
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
extension_params: DexParamEnum::Bonk(sell_extension),
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
with_tip: false,
durable_nonce: None,
create_output_token_ata: false,
@@ -232,7 +221,12 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
let (ok, sigs, err, _) = client.sell(sell_params).await?;
if !ok {
return Err(
std::io::Error::other(format!("sell failed: {:?}; sigs: {:?}", err, sigs)).into()
);
}
// Exit program
std::process::exit(0);
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+13
View File
@@ -0,0 +1,13 @@
# Bonk Sniper Trading
[中文](README_CN.md)
Uses `solana-streamer-sdk` ShredStream events to detect Bonk launch activity and execute one buy/sell flow.
> This is a live-transaction template. Replace `use_your_shred_stream_url_here`, set `PRIVATE_KEY`, RPC, target filters, amounts, slippage, and fees before running.
```bash
cargo run --package bonk_sniper_trading
```
Shreds may not contain every log-derived field. Only trade when every required account, reserve, token-program, and fee field is present and current; otherwise use a correctness-first RPC fallback. Prewarm the trading client and blockhash cache before subscription in a production bot.
+13
View File
@@ -0,0 +1,13 @@
# Bonk 狙击交易
[English](README.md)
使用 `solana-streamer-sdk` ShredStream 事件识别 Bonk launch 活动,并执行一次买卖流程。
> 这是会发送真实交易的模板。运行前替换 `use_your_shred_stream_url_here`,设置 `PRIVATE_KEY`、RPC、目标过滤、金额、滑点和费用。
```bash
cargo run --package bonk_sniper_trading
```
Shred 不一定包含所有日志字段。只有必需账户、储备、token program 和费率字段完整且新鲜时才能交易,否则应使用正确性优先的 RPC 回退。生产 Bot 应在订阅前预热交易客户端和 blockhash cache。
+55 -52
View File
@@ -1,4 +1,3 @@
use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed;
use sol_trade_sdk::common::TradeConfig;
use sol_trade_sdk::{
common::AnyResult,
@@ -10,13 +9,11 @@ use sol_trade_sdk::{
SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer;
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
use solana_streamer_sdk::streaming::event_parser::common::EventType;
use solana_streamer_sdk::streaming::event_parser::protocols::bonk::BonkTradeEvent;
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::{match_event, streaming::ShredStreamGrpc};
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
use solana_streamer_sdk::streaming::ShredStreamGrpc;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -32,16 +29,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let shred_stream = ShredStreamGrpc::new("use_your_shred_stream_url_here".to_string()).await?;
let callback = create_event_callback();
let protocols = vec![Protocol::Bonk];
let event_type_filter = EventTypeFilter {
include: vec![
EventType::BonkBuyExactIn,
EventType::BonkBuyExactOut,
EventType::BonkSellExactIn,
EventType::BonkSellExactOut,
EventType::BonkInitialize,
EventType::BonkInitializeV2,
],
};
let event_type_filter = EventTypeFilter::include_only(vec![
EventType::BonkBuyExactIn,
EventType::BonkBuyExactOut,
EventType::BonkSellExactIn,
EventType::BonkSellExactOut,
EventType::BonkInitialize,
EventType::BonkInitializeV2,
]);
println!("Starting to listen for events, press Ctrl+C to stop...");
shred_stream.shredstream_subscribe(protocols, None, Some(event_type_filter), callback).await?;
tokio::signal::ctrl_c().await?;
@@ -49,27 +44,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
/// Create an event callback function that handles different types of events
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
match_event!(event, {
BonkTradeEvent => |e: BonkTradeEvent| {
// Only process developer token creation events
if !e.is_dev_create_token_trade {
return;
fn create_event_callback() -> impl Fn(DexEvent) {
|event: DexEvent| {
let DexEvent::BonkTradeEvent(event) = event else {
return;
};
if !event.is_dev_create_token_trade {
return;
}
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
tokio::spawn(async move {
if let Err(err) = bonk_sniper_trade_with_shreds(event).await {
eprintln!("Error in sniper trade: {:?}", err);
std::process::exit(1);
}
// Ensure we only execute the trade once using atomic compare-and-swap
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let event_clone = e.clone();
// Spawn a new task to handle the trading operation
tokio::spawn(async move {
if let Err(err) = bonk_sniper_trade_with_shreds(event_clone).await {
eprintln!("Error in sniper trade: {:?}", err);
std::process::exit(0);
}
});
}
},
});
});
}
}
}
@@ -77,7 +67,7 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
/// Initializes a new SolanaTrade client with configuration
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
@@ -113,6 +103,9 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
} else {
sol_trade_sdk::TradeTokenType::SOL
};
let balance_before = client
.get_payer_token_balance_with_program(&mint_pubkey, &trade_info.base_token_program)
.await?;
// Buy tokens
println!("Buying tokens from Bonk...");
@@ -137,8 +130,9 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
trade_info.creator_associated_account,
trade_info.global_config,
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -149,22 +143,25 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
let (ok, sigs, err, _) = client.buy(buy_params).await?;
if !ok {
return Err(
std::io::Error::other(format!("buy failed: {:?}; sigs: {:?}", err, sigs)).into()
);
}
// Sell tokens
println!("Selling tokens from Bonk...");
let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey();
let account = get_associated_token_address_with_program_id_fast_use_seed(
&payer,
&mint_pubkey,
&trade_info.base_token_program,
client.use_seed_optimize,
);
let balance = rpc.get_token_account_balance(&account).await?;
println!("Balance: {:?}", balance);
let amount_token = balance.amount.parse::<u64>().unwrap();
let balance_after = client
.get_payer_token_balance_with_program(&mint_pubkey, &trade_info.base_token_program)
.await?;
let amount_token = balance_after
.checked_sub(balance_before)
.ok_or_else(|| std::io::Error::other("token balance decreased after buy"))?;
if amount_token == 0 {
return Err(std::io::Error::other("confirmed buy did not increase token balance").into());
}
println!("Selling {} tokens", amount_token);
let sell_params = sol_trade_sdk::TradeSellParams {
@@ -173,7 +170,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
mint: mint_pubkey,
input_token_amount: amount_token,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
extension_params: DexParamEnum::Bonk(BonkParams::immediate_sell(
trade_info.base_token_program,
trade_info.platform_config,
@@ -181,8 +178,9 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
trade_info.creator_associated_account,
trade_info.global_config,
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
@@ -193,7 +191,12 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
let (ok, sigs, err, _) = client.sell(sell_params).await?;
if !ok {
return Err(
std::io::Error::other(format!("sell failed: {:?}; sigs: {:?}", err, sigs)).into()
);
}
// Exit program after completing the trade
std::process::exit(0);
+4
View File
@@ -1,7 +1,11 @@
# SOL Trade CLI
[中文](README_CN.md)
A command-line tool for trading tokens on Solana, supporting multiple DEX trading and wallet management features.
> **Live transaction warning:** set `PRIVATE_KEY` (base58 or a 64-byte JSON array) and `RPC_URL` before running. The CLI no longer creates a temporary trading wallet. Commands below can submit real mainnet transactions.
## Features
This CLI tool supports the following operation modes:
+4
View File
@@ -1,7 +1,11 @@
# SOL Trade CLI
[English](README.md)
一个用于在 Solana 上交易代币的命令行工具,支持多种 DEX 交易和钱包管理功能。
> **真实交易警告:**运行前设置 `PRIVATE_KEY`base58 或 64 字节 JSON 数组)和 `RPC_URL`。CLI 不再创建临时交易钱包;下列命令可能提交真实主网交易。
## 功能
此 CLI 工具支持以下操作模式:
+99 -102
View File
@@ -24,22 +24,21 @@ use solana_sdk::{
message::{AccountMeta, Instruction},
native_token::sol_str_to_lamports,
pubkey::Pubkey,
signature::Keypair,
signer::Signer,
};
use solana_system_interface::instruction::transfer;
use std::sync::{Arc, LazyLock};
use std::sync::Arc;
use std::{
io::{self, Write},
str::FromStr,
};
// 设置 payer
static PAYER: LazyLock<Keypair> = LazyLock::new(|| Keypair::new());
// 设置 rpc url
static RPC_URL: &str = "https://api.mainnet-beta.solana.com";
static DEXS: &[&str] = &["pumpfun", "pumpswap", "bonk", "raydium_v4", "raydium_cpmm"];
fn rpc_url() -> String {
std::env::var("RPC_URL")
.or_else(|_| std::env::var("SOLANA_RPC_URL"))
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string())
}
#[derive(Parser)]
#[command(name = "sol-trade-cli")]
#[command(about = "SOL Trade CLI - A command line interface for trading tokens on Solana")]
@@ -197,7 +196,7 @@ async fn show_startup_info() {
println!("\n📋 STARTUP INFORMATION");
println!("══════════════════════════════════════");
println!("🌐 RPC URL: {}", RPC_URL);
println!("🌐 RPC URL: {}", rpc_url());
// Try to initialize client to show wallet info
match initialize_real_client().await {
@@ -214,14 +213,9 @@ async fn show_startup_info() {
}
}
}
Err(_) => {
// Generate a temporary keypair to show the format
let temp_keypair = solana_sdk::signature::Keypair::new();
println!(
"👛 Wallet Address: {} (temporary - set SOLANA_RPC_URL for real wallet)",
temp_keypair.pubkey()
);
println!("💰 SOL Balance: Unable to fetch (no valid RPC connection)");
Err(err) => {
println!("👛 Wallet: unavailable ({})", err);
println!("💰 SOL Balance: Unable to fetch");
}
}
@@ -268,21 +262,21 @@ async fn run_interactive_mode() -> Result<(), Box<dyn std::error::Error>> {
handle_close_wsol().await?;
}
_ => {
if input.starts_with("raydium_cpmm_buy ") {
handle_interactive_raydium_cpmm_buy(&input[16..]).await?;
} else if input.starts_with("raydium_cpmm_sell ") {
handle_interactive_raydium_cpmm_sell(&input[17..]).await?;
} else if input.starts_with("raydium_v4_buy ") {
handle_interactive_raydium_v4_buy(&input[14..]).await?;
} else if input.starts_with("raydium_v4_sell ") {
handle_interactive_raydium_v4_sell(&input[15..]).await?;
} else if input.starts_with("buy ") {
handle_interactive_buy(&input[4..]).await?;
} else if input.starts_with("sell ") {
handle_interactive_sell(&input[5..]).await?;
} else if input.starts_with("wrap_sol ") || input.starts_with("wrap-sol ") {
let amount_str =
if input.starts_with("wrap_sol ") { &input[9..] } else { &input[9..] };
if let Some(args) = input.strip_prefix("raydium_cpmm_buy ") {
handle_interactive_raydium_cpmm_buy(args).await?;
} else if let Some(args) = input.strip_prefix("raydium_cpmm_sell ") {
handle_interactive_raydium_cpmm_sell(args).await?;
} else if let Some(args) = input.strip_prefix("raydium_v4_buy ") {
handle_interactive_raydium_v4_buy(args).await?;
} else if let Some(args) = input.strip_prefix("raydium_v4_sell ") {
handle_interactive_raydium_v4_sell(args).await?;
} else if let Some(args) = input.strip_prefix("buy ") {
handle_interactive_buy(args).await?;
} else if let Some(args) = input.strip_prefix("sell ") {
handle_interactive_sell(args).await?;
} else if let Some(amount_str) =
input.strip_prefix("wrap_sol ").or_else(|| input.strip_prefix("wrap-sol "))
{
if let Ok(amount) = amount_str.parse::<f64>() {
handle_wrap_sol(amount).await?;
} else {
@@ -477,23 +471,20 @@ async fn check_mint_ata(
let mint_pubkey = Pubkey::from_str(mint).unwrap();
if let Ok(mint_info) = client.infrastructure.rpc.get_account(&mint_pubkey).await {
let owner_pubkey = mint_info.owner.clone();
let owner_pubkey = mint_info.owner;
let mint_ata = get_associated_token_address_with_program_id_fast_use_seed(
&client.get_payer_pubkey(),
&mint_pubkey,
&owner_pubkey,
false,
);
match client.infrastructure.rpc.get_token_account_balance(&mint_ata).await {
Ok(balance) => {
let amount = balance.ui_amount.unwrap_or(0.0);
decimals = balance.decimals;
amount_f64 = amount as f64 * 10_f64.powi(decimals as i32);
if let Ok(balance) = client.infrastructure.rpc.get_token_account_balance(&mint_ata).await {
let amount = balance.ui_amount.unwrap_or(0.0);
decimals = balance.decimals;
amount_f64 = amount * 10_f64.powi(decimals as i32);
create_mint_ata = false;
use_seed = false;
}
Err(_) => {}
create_mint_ata = false;
use_seed = false;
}
if !create_mint_ata {
return Ok((create_mint_ata, use_seed, owner_pubkey, amount_f64, decimals));
@@ -505,16 +496,13 @@ async fn check_mint_ata(
&owner_pubkey,
true,
);
match client.infrastructure.rpc.get_token_account_balance(&mint_ata).await {
Ok(_) => {
create_mint_ata = false;
use_seed = true;
}
Err(_) => {}
if client.infrastructure.rpc.get_token_account_balance(&mint_ata).await.is_ok() {
create_mint_ata = false;
use_seed = true;
}
return Ok((create_mint_ata, use_seed, owner_pubkey, amount_f64, decimals));
}
return Err("Mint account not found".to_string().into());
Err("Mint account not found".to_string().into())
}
// Buy and sell functions - currently in demo mode since trading logic is complex
@@ -606,8 +594,8 @@ async fn handle_buy_pumpfun(
println!("🔥 BUY PUMPFUN COMMAND");
println!(" Token Mint: {}", mint);
println!(" SOL Amount: {} SOL", sol_amount);
if slippage.is_some() {
println!(" Slippage: {}", slippage.unwrap());
if let Some(slippage) = slippage {
println!(" Slippage: {}", slippage);
}
let client = initialize_real_client().await?;
let mint_pubkey = Pubkey::from_str(mint)?;
@@ -626,14 +614,15 @@ async fn handle_buy_pumpfun(
slippage_basis_points: slippage,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
create_mint_ata,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
@@ -663,8 +652,8 @@ async fn handle_buy_pumpswap(
println!("🔥 BUY PUMPSWAP COMMAND");
println!(" Token Mint: {}", mint);
println!(" SOL Amount: {} SOL", sol_amount);
if slippage.is_some() {
println!(" Slippage: {}%", slippage.unwrap());
if let Some(slippage) = slippage {
println!(" Slippage: {}%", slippage);
}
let mint_pubkey = Pubkey::from_str(mint)?;
let param = PumpSwapParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey).await?;
@@ -682,14 +671,15 @@ async fn handle_buy_pumpswap(
slippage_basis_points: slippage,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
create_mint_ata,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
@@ -718,8 +708,8 @@ async fn handle_buy_bonk(
println!("🔥 BUY BONK COMMAND");
println!(" Token Mint: {}", mint);
println!(" SOL Amount: {} SOL", sol_amount);
if slippage.is_some() {
println!(" Slippage: {}%", slippage.unwrap());
if let Some(slippage) = slippage {
println!(" Slippage: {}%", slippage);
}
let mint_pubkey = Pubkey::from_str(mint)?;
let param =
@@ -738,14 +728,15 @@ async fn handle_buy_bonk(
slippage_basis_points: slippage,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
create_mint_ata,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
@@ -776,8 +767,8 @@ async fn handle_buy_raydium_v4(
println!(" Token Mint: {}", mint);
println!(" AMM: {}", amm);
println!(" SOL Amount: {} SOL", sol_amount);
if slippage.is_some() {
println!(" Slippage: {}%", slippage.unwrap());
if let Some(slippage) = slippage {
println!(" Slippage: {}%", slippage);
}
let mint_pubkey = Pubkey::from_str(mint)?;
@@ -798,14 +789,15 @@ async fn handle_buy_raydium_v4(
slippage_basis_points: slippage,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumAmmV4(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
create_mint_ata,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
@@ -836,8 +828,8 @@ async fn handle_buy_raydium_cpmm(
println!(" Pool Address: {}", pool_address);
println!(" Token Mint: {}", mint);
println!(" SOL Amount: {} SOL", sol_amount);
if slippage.is_some() {
println!(" Slippage: {}%", slippage.unwrap());
if let Some(slippage) = slippage {
println!(" Slippage: {}%", slippage);
}
let mint_pubkey = Pubkey::from_str(mint)?;
@@ -859,14 +851,15 @@ async fn handle_buy_raydium_cpmm(
slippage_basis_points: slippage,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumCpmm(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
create_mint_ata,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
@@ -1003,13 +996,13 @@ async fn handle_sell_pumpfun(
amount_f64: f64,
_decimals: u8,
) -> Result<(), Box<dyn std::error::Error>> {
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
let amount = token_amount.unwrap_or(amount_f64);
println!("🔥 SELL PUMPFUN COMMAND");
println!(" Token Mint: {}", mint);
println!(" Token Amount: {} ", amount);
if slippage.is_some() {
println!(" Slippage: {}%", slippage.unwrap());
if let Some(slippage) = slippage {
println!(" Slippage: {}%", slippage);
}
let client = initialize_real_client().await?;
@@ -1029,14 +1022,15 @@ async fn handle_sell_pumpfun(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
@@ -1064,12 +1058,12 @@ async fn handle_sell_pumpswap(
amount_f64: f64,
_decimals: u8,
) -> Result<(), Box<dyn std::error::Error>> {
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
let amount = token_amount.unwrap_or(amount_f64);
println!("🔥 SELL PUMPSWAP COMMAND");
println!(" Token Mint: {}", mint);
println!(" Token Amount: {}", amount);
if slippage.is_some() {
println!(" Slippage: {}", slippage.unwrap());
if let Some(slippage) = slippage {
println!(" Slippage: {}", slippage);
}
let client = initialize_real_client().await?;
let mint_pubkey = Pubkey::from_str(mint)?;
@@ -1088,14 +1082,15 @@ async fn handle_sell_pumpswap(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
@@ -1122,12 +1117,12 @@ async fn handle_sell_bonk(
amount_f64: f64,
_decimals: u8,
) -> Result<(), Box<dyn std::error::Error>> {
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
let amount = token_amount.unwrap_or(amount_f64);
println!("🔥 SELL PUMPSWAP COMMAND");
println!(" Token Mint: {}", mint);
println!(" Token Amount: {}", amount);
if slippage.is_some() {
println!(" Slippage: {}", slippage.unwrap());
if let Some(slippage) = slippage {
println!(" Slippage: {}", slippage);
}
let client = initialize_real_client().await?;
let mint_pubkey = Pubkey::from_str(mint)?;
@@ -1147,14 +1142,15 @@ async fn handle_sell_bonk(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
@@ -1182,13 +1178,13 @@ async fn handle_sell_raydium_v4(
amount_f64: f64,
_decimals: u8,
) -> Result<(), Box<dyn std::error::Error>> {
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
let amount = token_amount.unwrap_or(amount_f64);
println!("🔥 SELL RAYDIUM V4 COMMAND");
println!(" AMM: {}", amm);
println!(" Token Mint: {}", mint);
println!(" Token Amount: {}", amount);
if slippage.is_some() {
println!(" Slippage: {}", slippage.unwrap());
if let Some(slippage) = slippage {
println!(" Slippage: {}", slippage);
}
let client = initialize_real_client().await?;
let amm_pubkey = Pubkey::from_str(amm)?;
@@ -1209,14 +1205,15 @@ async fn handle_sell_raydium_v4(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::RaydiumAmmV4(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
@@ -1244,13 +1241,13 @@ async fn handle_sell_raydium_cpmm(
amount_f64: f64,
_decimals: u8,
) -> Result<(), Box<dyn std::error::Error>> {
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
let amount = token_amount.unwrap_or(amount_f64);
println!("🔥 SELL RAYDIUM CPMM COMMAND");
println!(" Pool Address: {}", pool_address);
println!(" Token Mint: {}", mint);
println!(" Token Amount: {}", amount);
if slippage.is_some() {
println!(" Slippage: {}", slippage.unwrap());
if let Some(slippage) = slippage {
println!(" Slippage: {}", slippage);
}
let client = initialize_real_client().await?;
let pool_pubkey = Pubkey::from_str(pool_address)?;
@@ -1272,14 +1269,15 @@ async fn handle_sell_raydium_cpmm(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::RaydiumCpmm(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
@@ -1386,10 +1384,9 @@ async fn handle_wallet() -> Result<(), Box<dyn std::error::Error>> {
// Real implementation functions
async fn initialize_real_client() -> AnyResult<SolanaTrade> {
// You need to update this with a real RPC URL
println!("🚀 Initializing SolanaTrade client...");
let payer = Arc::new(Keypair::try_from(&PAYER.to_bytes()[..]).unwrap());
let rpc_url = RPC_URL.to_string();
let payer = Arc::new(sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?);
let rpc_url = rpc_url();
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
+11
View File
@@ -0,0 +1,11 @@
# Gas Fee Strategy
[中文](README_CN.md)
Local, non-transaction demo of global, per-route, and high/low `GasFeeStrategy` configuration. It also shows replacement, deletion, and inspection of strategies.
```bash
cargo run --package gas_fee_strategy
```
The example does not connect to Solana or submit a transaction. Fee values are illustrative; production values must come from current network conditions and explicit cost limits.
+11
View File
@@ -0,0 +1,11 @@
# Gas Fee 策略
[English](README.md)
本地演示全局、按提交通道和高低两档 `GasFeeStrategy` 配置,并展示策略覆盖、删除和查看。
```bash
cargo run --package gas_fee_strategy
```
示例不会连接 Solana,也不会提交交易。代码中的费用仅用于说明,生产值应根据实时网络情况和明确成本上限设置。
@@ -5,8 +5,9 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] }
anyhow = "1"
@@ -0,0 +1,16 @@
# Meteora DAMM V2 Direct Trading
[中文](README_CN.md)
Demonstrates a direct Meteora DAMM V2 buy/sell flow using pool data supplied by `solana-streamer-sdk` types and SDK trading params.
> This is a live mainnet template. Set `PRIVATE_KEY`, `RPC_URL`, and real raw-unit
> `MIN_BUY_OUTPUT_AMOUNT` / `MIN_SELL_OUTPUT_AMOUNT` quotes before running.
```bash
cargo run --package meteora_damm_v2_direct_trading
```
Token program selection, pool orientation, dynamic fee state, and remaining accounts must be current. Refresh pool state and blockhash before a delayed sell. Use Token-2022-aware balance/account handling where required.
`MIN_*_OUTPUT_AMOUNT` is the on-chain minimum output, not a token UI amount. Derive it from a current quote and your slippage tolerance. The example intentionally refuses to use `1`, because that provides almost no price protection in partial-fill mode.
@@ -0,0 +1,15 @@
# Meteora DAMM V2 直接交易
[English](README.md)
展示使用 `solana-streamer-sdk` 类型提供的池数据和 SDK 参数执行 Meteora DAMM V2 直接买卖流程。
> 这是会发送真实主网交易的模板。运行前必须设置 `PRIVATE_KEY``RPC_URL`,以及以原始单位表示的真实报价 `MIN_BUY_OUTPUT_AMOUNT` / `MIN_SELL_OUTPUT_AMOUNT`
```bash
cargo run --package meteora_damm_v2_direct_trading
```
Token program、池方向、动态费率状态和 remaining accounts 必须保持最新。延迟卖出前刷新池状态和 blockhash;需要时使用兼容 Token-2022 的余额与账户处理。
`MIN_*_OUTPUT_AMOUNT` 是链上的最低输出原始数量,不是 UI 数量。应根据当前报价和滑点容忍度计算。示例会拒绝使用 `1`,因为在 partial-fill 模式下它几乎不提供价格保护。
@@ -1,7 +1,5 @@
use sol_trade_sdk::{
common::{
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, AnyResult, TradeConfig,
},
common::{AnyResult, TradeConfig},
swqos::SwqosConfig,
trading::{
core::params::{DexParamEnum, MeteoraDammV2Params},
@@ -10,12 +8,11 @@ use sol_trade_sdk::{
SolanaTrade, TradeTokenType,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use solana_sdk::{pubkey::Pubkey, signer::Signer};
use solana_sdk::pubkey::Pubkey;
use std::{str::FromStr, sync::Arc};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn main() -> AnyResult<()> {
println!("Testing Metaora Damm V2 trading...");
let client = create_solana_trade_client().await?;
@@ -23,9 +20,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let pool = Pubkey::from_str("7dVri3qjYD3uobSZL3Zth8vSCgU6r6R2nvFsh7uVfDte").unwrap();
let mint_pubkey = Pubkey::from_str("PRVT6TB7uss3FrUd2D9xs2zqDBsa3GbMJMwCQsgmeta").unwrap();
let min_buy_output = required_u64_env("MIN_BUY_OUTPUT_AMOUNT")?;
let min_sell_output = required_u64_env("MIN_SELL_OUTPUT_AMOUNT")?;
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
let pool_params =
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?;
let token_program = if pool_params.token_a_mint == mint_pubkey {
pool_params.token_a_program
} else if pool_params.token_b_mint == mint_pubkey {
pool_params.token_b_program
} else {
anyhow::bail!("target mint does not belong to the configured Meteora pool");
};
let balance_before =
client.get_payer_token_balance_with_program(&mint_pubkey, &token_program).await?;
// Buy tokens
println!("Buying tokens from Metaora Damm V2...");
@@ -37,74 +47,86 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
input_token_amount: input_token_amount,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::MeteoraDammV2(
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool)
.await?,
),
address_lookup_table_account: None,
extension_params: DexParamEnum::MeteoraDammV2(pool_params),
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false, //if input token is SOL/WSOL,set to true,if input token is USDC,set to false.
close_input_token_ata: false, //if input token is SOL/WSOL,set to true,if input token is USDC,set to false.
create_mint_ata: true,
durable_nonce: None,
fixed_output_token_amount: Some(1),
fixed_output_token_amount: Some(min_buy_output),
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
let (ok, sigs, err, _) = client.buy(buy_params).await?;
if !ok {
anyhow::bail!("buy failed: {:?}; signatures: {:?}", err, sigs);
}
// Sell tokens
println!("Selling tokens from Metaora Damm V2...");
let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey();
let program_id = sol_trade_sdk::constants::TOKEN_PROGRAM;
let account = get_associated_token_address_with_program_id_fast_use_seed(
&payer,
&mint_pubkey,
&program_id,
client.use_seed_optimize,
);
let balance = rpc.get_token_account_balance(&account).await?;
let amount_token = balance.amount.parse::<u64>().unwrap();
println!("Token balance: {}", amount_token);
let balance_after =
client.get_payer_token_balance_with_program(&mint_pubkey, &token_program).await?;
let amount_token = balance_after.checked_sub(balance_before).ok_or_else(|| {
anyhow::anyhow!("token balance decreased after buy; refusing to sell existing holdings")
})?;
if amount_token == 0 {
anyhow::bail!("confirmed buy did not increase token balance");
}
println!("Position acquired by this run: {}", amount_token);
let sell_params = sol_trade_sdk::TradeSellParams {
dex_type: DexType::MeteoraDammV2,
output_token_type: TradeTokenType::USDC,
mint: mint_pubkey,
input_token_amount: amount_token,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
with_tip: false,
extension_params: DexParamEnum::MeteoraDammV2(
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool)
.await?,
),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: false, //if output token is SOL/WSOL,set to true,if output token is USDC,set to false.
close_output_token_ata: false, //if output token is SOL/WSOL,set to true,if output token is USDC,set to false.
close_mint_token_ata: false,
durable_nonce: None,
fixed_output_token_amount: Some(1),
fixed_output_token_amount: Some(min_sell_output),
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
let (ok, sigs, err, _) = client.sell(sell_params).await?;
if !ok {
anyhow::bail!("sell failed: {:?}; signatures: {:?}", err, sigs);
}
Ok(())
}
// Exit program
std::process::exit(0);
fn required_u64_env(name: &str) -> AnyResult<u64> {
let value = std::env::var(name).map_err(|_| {
anyhow::anyhow!("{} is required and must be a real raw-unit minimum output quote", name)
})?;
let amount = value.parse::<u64>().map_err(|_| anyhow::anyhow!("{} must be u64", name))?;
if amount == 0 {
anyhow::bail!("{} must be greater than zero", name);
}
Ok(amount)
}
/// Create SolanaTrade client
/// Initializes a new SolanaTrade client with configuration
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("your_payer_keypair_here");
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = std::env::var("RPC_URL")
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
+2 -2
View File
@@ -5,8 +5,8 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0.79"
anyhow = "1.0.79"
+13
View File
@@ -0,0 +1,13 @@
# Instruction Middleware
[中文](README_CN.md)
Demonstrates implementing `InstructionMiddleware`, installing it in a `MiddlewareManager`, and observing or modifying instructions before submission.
> The example loads `PRIVATE_KEY` and calls `client.buy`. After configuration it can submit a real PumpSwap mainnet transaction.
```bash
cargo run --package middleware_system
```
Middleware must preserve signer, account-order, compute-budget, tip, nonce, and transaction-size semantics. Keep latency-sensitive middleware deterministic and free of blocking I/O.
+13
View File
@@ -0,0 +1,13 @@
# 指令中间件
[English](README.md)
展示如何实现 `InstructionMiddleware`、安装到 `MiddlewareManager`,并在提交前观察或修改指令。
> 示例从 `PRIVATE_KEY` 加载钱包并调用 `client.buy`;配置完成后可能提交真实 PumpSwap 主网交易。
```bash
cargo run --package middleware_system
```
中间件必须保持 signer、账户顺序、compute budget、tip、nonce 和交易体积语义。低延迟中间件应确定性执行,不能包含阻塞 I/O。
+4 -3
View File
@@ -10,7 +10,7 @@ use sol_trade_sdk::{
SolanaTrade, TradeTokenType,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signature::Keypair};
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
use std::{str::FromStr, sync::Arc};
#[tokio::main]
@@ -59,7 +59,7 @@ impl InstructionMiddleware for CustomMiddleware {
/// Initializes a new SolanaTrade client with configuration
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
@@ -102,8 +102,9 @@ async fn test_middleware() -> AnyResult<()> {
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_address)
.await?,
),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = "0.2.2"
sol-parser-sdk = { version = "0.6.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "995d88991b56234a23fc1d0911fdd33caa063c67" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
spl-associated-token-account = "7.0.0"
+17
View File
@@ -0,0 +1,17 @@
# Durable Nonce
[中文](README_CN.md)
Shows how to fetch a durable nonce and attach it to a PumpFun transaction triggered by a `sol-parser-sdk` gRPC event.
> This is a live-transaction template. Set `PRIVATE_KEY`, replace `use_your_nonce_account_here`, and configure RPC and gRPC before running. The nonce authority must match the signer.
```bash
export GRPC_ENDPOINT=https://your-yellowstone.example
export GRPC_AUTH_TOKEN=optional-token
cargo run --package nonce_cache
```
Do not set both a recent blockhash and a durable nonce in new code; use `SimpleBuyParams::with_durable_nonce`. Refresh nonce state immediately before construction, and never reuse consumed nonce state. A nonce extends transaction validity but does not preserve quote freshness.
See [Nonce Cache](../../docs/NONCE_CACHE.md).
+17
View File
@@ -0,0 +1,17 @@
# Durable Nonce
[English](README.md)
展示如何获取 durable nonce,并附加到由 `sol-parser-sdk` gRPC 事件触发的 PumpFun 交易。
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`,替换 `use_your_nonce_account_here`,并配置 RPC 和 gRPCnonce authority 必须与签名者匹配。
```bash
export GRPC_ENDPOINT=https://your-yellowstone.example
export GRPC_AUTH_TOKEN=optional-token
cargo run --package nonce_cache
```
新代码不要同时设置 recent blockhash 和 durable nonce,应使用 `SimpleBuyParams::with_durable_nonce`。构建前刷新 nonce,已消费状态不可复用。Nonce 只延长交易有效期,不会保持报价新鲜。
参见[Nonce Cache 说明](../../docs/NONCE_CACHE_CN.md)。
+17 -5
View File
@@ -23,7 +23,7 @@ use sol_trade_sdk::{
SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
use solana_sdk::pubkey::Pubkey;
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
@@ -104,7 +104,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
@@ -129,6 +129,16 @@ async fn pumpfun_copy_trade_with_grpc(
let client = create_solana_trade_client().await?;
let mint_pubkey = trade_info.mint;
let virtual_quote_reserves = if trade_info.virtual_quote_reserves != 0 {
trade_info.virtual_quote_reserves
} else {
trade_info.virtual_sol_reserves
};
let real_quote_reserves = if trade_info.virtual_quote_reserves != 0 {
trade_info.real_quote_reserves
} else {
trade_info.real_sol_reserves
};
let slippage_basis_points = Some(100);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
@@ -150,20 +160,22 @@ async fn pumpfun_copy_trade_with_grpc(
trade_info.bonding_curve,
trade_info.associated_bonding_curve,
trade_info.mint,
trade_info.quote_mint,
trade_info.creator,
trade_info.creator_vault,
trade_info.virtual_token_reserves,
trade_info.virtual_sol_reserves,
virtual_quote_reserves,
trade_info.real_token_reserves,
trade_info.real_sol_reserves,
real_quote_reserves,
None,
trade_info.fee_recipient,
trade_info.token_program,
trade_info.is_cashback_coin,
Some(trade_info.mayhem_mode),
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = "0.2.2"
sol-parser-sdk = { version = "0.6.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "995d88991b56234a23fc1d0911fdd33caa063c67" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+15
View File
@@ -0,0 +1,15 @@
# PumpFun Copy Trading
[中文](README_CN.md)
Consumes PumpFun buy/sell events through `sol-parser-sdk` Yellowstone gRPC and maps the event's post-trade protocol fields into one follow-up buy/sell flow.
> This is a live-transaction template. Set `PRIVATE_KEY` and configure RPC, gRPC, target filtering, amounts, slippage, and fee limits before running.
```bash
export RPC_URL=https://your-rpc.example
export GRPC_ENDPOINT=https://your-yellowstone.example
cargo run --package pumpfun_copy_trading
```
Copying every event is unsafe. Add wallet/mint allowlists, signature plus instruction-index deduplication, event-age limits, position isolation, and maximum input/loss controls. Prewarm the client and blockhash cache before subscription for low latency.
@@ -0,0 +1,15 @@
# PumpFun 跟单交易
[English](README.md)
通过 `sol-parser-sdk` Yellowstone gRPC 消费 PumpFun 买卖事件,把事件中的成交后协议字段映射到一次跟随买卖流程。
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`,并配置 RPC、gRPC、目标过滤、金额、滑点和费用上限。
```bash
export RPC_URL=https://your-rpc.example
export GRPC_ENDPOINT=https://your-yellowstone.example
cargo run --package pumpfun_copy_trading
```
无条件复制全部事件是不安全的。必须增加钱包/mint allowlist、签名加指令索引去重、事件年龄上限、持仓隔离和最大输入/亏损控制。低延迟场景应在订阅前预热客户端和 blockhash cache。
+43 -39
View File
@@ -12,9 +12,7 @@ use sol_parser_sdk::grpc::{
TransactionFilter, YellowstoneGrpc,
};
use sol_parser_sdk::DexEvent;
use sol_trade_sdk::common::{
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, TradeConfig,
};
use sol_trade_sdk::common::TradeConfig;
use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::{
common::AnyResult,
@@ -26,8 +24,6 @@ use sol_trade_sdk::{
SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer;
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
@@ -109,7 +105,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = std::env::var("RPC_URL")
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::confirmed();
@@ -128,11 +124,20 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent) -> AnyResult<()> {
let client = create_solana_trade_client().await?;
let mint_pubkey = e.mint;
let virtual_quote_reserves = if e.virtual_quote_reserves != 0 {
e.virtual_quote_reserves
} else {
e.virtual_sol_reserves
};
let real_quote_reserves =
if e.virtual_quote_reserves != 0 { e.real_quote_reserves } else { e.real_sol_reserves };
let slippage_basis_points = Some(100u64);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
let balance_before =
client.get_payer_token_balance_with_program(&mint_pubkey, &e.token_program).await?;
// 买入:使用事件参数,含 is_cashback_coin(来自 sol-parser-sdk 解析)
let buy_sol_amount = 100_000u64;
@@ -147,20 +152,22 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
e.bonding_curve,
e.associated_bonding_curve,
e.mint,
e.quote_mint,
e.creator,
e.creator_vault,
e.virtual_token_reserves,
e.virtual_sol_reserves,
virtual_quote_reserves,
e.real_token_reserves,
e.real_sol_reserves,
real_quote_reserves,
None,
e.fee_recipient,
e.token_program,
e.is_cashback_coin,
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
@@ -171,19 +178,25 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
let (ok, sigs, err, _) = client.buy(buy_params).await?;
if !ok {
return Err(
std::io::Error::other(format!("buy failed: {:?}; sigs: {:?}", err, sigs)).into()
);
}
// 卖出:查询余额后卖出,同样传入 is_cashback_coin
let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey();
let account = get_associated_token_address_with_program_id_fast_use_seed(
&payer,
&mint_pubkey,
&e.token_program,
client.use_seed_optimize,
);
let balance = rpc.get_token_account_balance(&account).await?;
let amount_token = balance.amount.parse::<u64>().unwrap();
let balance_after =
client.get_payer_token_balance_with_program(&mint_pubkey, &e.token_program).await?;
let amount_token = balance_after
.checked_sub(balance_before)
.ok_or_else(|| std::io::Error::other("token balance decreased after buy"))?;
if amount_token == 0 {
return Err(std::io::Error::other("confirmed buy did not increase token balance").into());
}
let sell_extension = PumpFunParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey)
.await?
.with_creator_vault(e.creator_vault);
let sell_params = sol_trade_sdk::TradeSellParams {
dex_type: DexType::PumpFun,
@@ -191,26 +204,12 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
mint: mint_pubkey,
input_token_amount: amount_token,
slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
with_tip: false,
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
e.bonding_curve,
e.associated_bonding_curve,
e.mint,
e.creator,
e.creator_vault,
e.virtual_token_reserves,
e.virtual_sol_reserves,
e.real_token_reserves,
e.real_sol_reserves,
Some(true),
e.fee_recipient,
e.token_program,
e.is_cashback_coin,
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
extension_params: DexParamEnum::PumpFun(sell_extension),
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: false,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -220,7 +219,12 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
let (ok, sigs, err, _) = client.sell(sell_params).await?;
if !ok {
return Err(
std::io::Error::other(format!("sell failed: {:?}; sigs: {:?}", err, sigs)).into()
);
}
println!("跟单一次买+卖完成");
Ok(())
+2 -1
View File
@@ -5,7 +5,8 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = "0.2.2"
sol-parser-sdk = { version = "0.6.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "995d88991b56234a23fc1d0911fdd33caa063c67" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
+15
View File
@@ -0,0 +1,15 @@
# PumpFun Sniper Trading
[中文](README_CN.md)
Uses `sol-parser-sdk` Yellowstone gRPC and selects creator-first-buy events (`is_created_buy`). Event fields are mapped into PumpFun params, including quote reserves, token program, cashback, and mayhem state, before one buy/sell flow.
> This is a live-transaction template. Set `PRIVATE_KEY`, `RPC_URL`, `GRPC_ENDPOINT`, optional `GRPC_AUTH_TOKEN`, target policy, amounts, and fees.
```bash
export RPC_URL=https://your-rpc.example
export GRPC_ENDPOINT=https://your-yellowstone.example
cargo run --package pumpfun_sniper_trading
```
The current source creates `SolanaTrade` and fetches blockhash inside event handling. For production latency, initialize both before subscription as shown in the [low-latency guide](../../docs/LOW_LATENCY_BOTS.md). Deduplicate signatures and refresh state before delayed sells.
@@ -0,0 +1,15 @@
# PumpFun 狙击交易
[English](README.md)
使用 `sol-parser-sdk` Yellowstone gRPC,只选择创建者首次买入事件 `is_created_buy`。示例将 quote 储备、token program、cashback 和 mayhem 等事件字段映射到 PumpFun 参数,再执行一次买卖流程。
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY``RPC_URL``GRPC_ENDPOINT`、可选 `GRPC_AUTH_TOKEN`、目标策略、金额和费用。
```bash
export RPC_URL=https://your-rpc.example
export GRPC_ENDPOINT=https://your-yellowstone.example
cargo run --package pumpfun_sniper_trading
```
当前源码在事件处理中创建 `SolanaTrade` 并查询 blockhash。生产低延迟实现应按[低延迟指南](../../docs/LOW_LATENCY_BOTS_CN.md)在订阅前完成预热,同时进行签名去重并刷新延迟卖出状态。
+28 -30
View File
@@ -13,7 +13,6 @@ use sol_parser_sdk::grpc::{
TransactionFilter, YellowstoneGrpc,
};
use sol_parser_sdk::DexEvent;
use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address;
use sol_trade_sdk::common::TradeConfig;
use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::{
@@ -26,8 +25,6 @@ use sol_trade_sdk::{
SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer;
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
@@ -99,7 +96,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = std::env::var("RPC_URL")
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::confirmed();
@@ -120,6 +117,8 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
let mint_pubkey = e.mint;
let slippage_basis_points = Some(300u64);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let balance_before =
client.get_payer_token_balance_with_program(&mint_pubkey, &e.token_program).await?;
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
@@ -149,8 +148,9 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
e.is_cashback_coin,
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -161,13 +161,22 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
let (ok, sigs, err, _) = client.buy(buy_params).await?;
if !ok {
anyhow::bail!("buy failed: {:?}; signatures: {:?}", err, sigs);
}
let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey();
let account = get_associated_token_address(&payer, &mint_pubkey);
let balance = rpc.get_token_account_balance(&account).await?;
let amount_token = balance.amount.parse::<u64>().unwrap();
let balance_after =
client.get_payer_token_balance_with_program(&mint_pubkey, &e.token_program).await?;
let amount_token = balance_after.checked_sub(balance_before).ok_or_else(|| {
anyhow::anyhow!("token balance decreased after buy; refusing to sell existing holdings")
})?;
if amount_token == 0 {
anyhow::bail!("confirmed buy did not increase token balance");
}
let sell_extension = PumpFunParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey)
.await?
.with_creator_vault(e.creator_vault);
let sell_params = sol_trade_sdk::TradeSellParams {
dex_type: DexType::PumpFun,
@@ -175,26 +184,12 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
mint: mint_pubkey,
input_token_amount: amount_token,
slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
with_tip: false,
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
e.bonding_curve,
e.associated_bonding_curve,
e.mint,
e.creator,
e.creator_vault,
e.virtual_token_reserves,
e.virtual_sol_reserves,
e.real_token_reserves,
e.real_sol_reserves,
Some(true),
e.fee_recipient,
e.token_program,
e.is_cashback_coin,
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
extension_params: DexParamEnum::PumpFun(sell_extension),
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
@@ -204,7 +199,10 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
let (ok, sigs, err, _) = client.sell(sell_params).await?;
if !ok {
anyhow::bail!("sell failed: {:?}; signatures: {:?}", err, sigs);
}
println!("狙击一次买+卖完成");
Ok(())
+2 -1
View File
@@ -5,7 +5,8 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
@@ -0,0 +1,15 @@
# PumpSwap Direct Trading
[中文](README_CN.md)
RPC-driven PumpSwap buy/wait/sell example without an event stream. It resolves pool state through RPC, buys, reads the wallet token balance, refreshes pool state, and sells.
> This template submits real mainnet transactions. Set `PRIVATE_KEY` and `RPC_URL`, replace mint and pool, then review amounts, slippage, fee strategy, ATA flags, and whether WSOL accounts should be closed.
```bash
cargo run --package pumpswap_direct_trading
```
For new code, prefer `SimpleBuyParams` with `BuyAmount::WithMaxInput` or an explicitly selected intent. Fetch a fresh blockhash before the delayed sell; do not reuse pre-buy reserves.
For an event-driven version, see [`pumpswap_trading`](../pumpswap_trading/README.md).
@@ -0,0 +1,15 @@
# PumpSwap 直接交易
[English](README.md)
不依赖事件流的 RPC 驱动 PumpSwap 买入/等待/卖出示例:通过 RPC 读取池状态,买入后查询钱包 token 余额,刷新池状态再卖出。
> 模板会提交真实主网交易。运行前设置 `PRIVATE_KEY``RPC_URL`,替换 mint 和 pool,并检查金额、滑点、费用策略、ATA flag 及是否关闭 WSOL 账户。
```bash
cargo run --package pumpswap_direct_trading
```
新代码优先使用 `SimpleBuyParams`,并明确选择 `BuyAmount::WithMaxInput` 或其他交易意图。延迟卖出前必须获取新 blockhash,不能复用买入前储备。
事件驱动版本见 [`pumpswap_trading`](../pumpswap_trading/README_CN.md)。
+39 -29
View File
@@ -1,7 +1,5 @@
use sol_trade_sdk::{
common::{
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, AnyResult, TradeConfig,
},
common::{AnyResult, TradeConfig},
swqos::SwqosConfig,
trading::{
core::params::{DexParamEnum, PumpSwapParams},
@@ -10,12 +8,11 @@ use sol_trade_sdk::{
SolanaTrade, TradeTokenType,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use solana_sdk::{pubkey::Pubkey, signer::Signer};
use solana_sdk::pubkey::Pubkey;
use std::{str::FromStr, sync::Arc};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn main() -> AnyResult<()> {
println!("Testing PumpSwap trading...");
let client = create_solana_trade_client().await?;
@@ -26,6 +23,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
let pool_params =
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?;
let token_program = if pool_params.base_mint == mint_pubkey {
pool_params.base_token_program
} else if pool_params.quote_mint == mint_pubkey {
pool_params.quote_token_program
} else {
anyhow::bail!("target mint does not belong to the configured pool");
};
let balance_before =
client.get_payer_token_balance_with_program(&mint_pubkey, &token_program).await?;
// Buy tokens
println!("Buying tokens from PumpSwap...");
@@ -37,11 +45,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
input_token_amount: buy_sol_amount,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpSwap(
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
extension_params: DexParamEnum::PumpSwap(pool_params),
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -52,35 +59,36 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
let (ok, sigs, err, _) = client.buy(buy_params).await?;
if !ok {
anyhow::bail!("buy failed: {:?}; signatures: {:?}", err, sigs);
}
// Sell tokens
println!("Selling tokens from PumpSwap...");
let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey();
let program_id = sol_trade_sdk::constants::TOKEN_PROGRAM_2022;
let account = get_associated_token_address_with_program_id_fast_use_seed(
&payer,
&mint_pubkey,
&program_id,
client.use_seed_optimize,
);
let balance = rpc.get_token_account_balance(&account).await?;
let amount_token = balance.amount.parse::<u64>().unwrap();
let balance_after =
client.get_payer_token_balance_with_program(&mint_pubkey, &token_program).await?;
let amount_token = balance_after.checked_sub(balance_before).ok_or_else(|| {
anyhow::anyhow!("token balance decreased after buy; refusing to sell existing holdings")
})?;
if amount_token == 0 {
anyhow::bail!("confirmed buy did not increase token balance");
}
let sell_params = sol_trade_sdk::TradeSellParams {
dex_type: DexType::PumpSwap,
output_token_type: TradeTokenType::SOL,
mint: mint_pubkey,
input_token_amount: amount_token,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
with_tip: false,
extension_params: DexParamEnum::PumpSwap(
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
@@ -90,9 +98,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
gas_fee_strategy: gas_fee_strategy,
simulate: false,
};
client.sell(sell_params).await?;
tokio::signal::ctrl_c().await?;
let (ok, sigs, err, _) = client.sell(sell_params).await?;
if !ok {
anyhow::bail!("sell failed: {:?}; signatures: {:?}", err, sigs);
}
Ok(())
}
@@ -100,8 +109,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// Initializes a new SolanaTrade client with configuration
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = std::env::var("RPC_URL")
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
+9
View File
@@ -0,0 +1,9 @@
PRIVATE_KEY=
RPC_URL=https://api.mainnet-beta.solana.com
GRPC_ENDPOINT=https://solana-yellowstone-grpc.publicnode.com:443
GRPC_AUTH_TOKEN=
# At least one target is required. If both are set, both must match.
TARGET_MINT=
TARGET_POOL=
MAX_EVENT_AGE_MS=1000
+3 -2
View File
@@ -5,8 +5,9 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] }
spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] }
anyhow = "1"
+29
View File
@@ -0,0 +1,29 @@
# PumpSwap Low-Latency gRPC Example
[中文](README_CN.md)
This example consumes PumpSwap events through `solana-streamer-sdk` and builds one follow-up buy from the event's post-trade reserves and dynamic fee rates. The trading client and blockhash cache are initialized before subscription, so the event hot path does not synchronously fetch a blockhash.
## Run
```bash
cp .env.example .env
# Edit .env, then load it into the current shell:
set -a; source .env; set +a
export PRIVATE_KEY=your_base58_private_key
export RPC_URL=https://your-rpc.example
cargo run --release --package pumpswap_trading
```
`GRPC_ENDPOINT` and `GRPC_AUTH_TOKEN` are optional. `TARGET_MINT` or `TARGET_POOL` is required; when both are set, both must match. `MAX_EVENT_AGE_MS` defaults to 1000. The binary reads environment variables but does not load `.env` itself, so source the file first when using it.
## Trade semantics
- The buy uses `BuyAmount::WithMaxInput`, which applies slippage to maximum quote cost and is appropriate when fill priority matters.
- Buy parameters use post-trade reserves and LP/protocol/creator fee bps from the event.
- The event's raw and virtual quote reserves come from the same transaction snapshot. The hot path does not fetch the Pool account, avoiding both added latency and mixed-slot quotes.
- The first matching event asynchronously records the pre-buy balance; the next fresh event performs the trade without a balance RPC in the submission hot path. It sells only the confirmed balance increase and refreshes pool state and blockhash before selling.
- Use `BuyAmount::ExactInput` when the quote spend must be exact. That mode protects minimum output and can fail more often in an active pool.
- If baseline warmup fails, the example waits for another event. Once transaction execution starts, an error keeps the one-shot guard locked because submission or position state may be uncertain; inspect the signatures and account state before retrying.
Production bots should also add durable signature deduplication, a position state machine, SWQoS configuration, and bounded requoting. Do not solve slippage errors by setting `min_out` to zero.
+27
View File
@@ -0,0 +1,27 @@
# PumpSwap 低延迟 gRPC 示例
[English](README.md)
该示例通过 `solana-streamer-sdk` 监听 PumpSwap 买卖事件,并用事件中的成交后储备和动态费率构建一次跟随买入。交易客户端和 blockhash cache 会在订阅前初始化,事件热路径不会同步查询 blockhash。
## 运行
```bash
cp .env.example .env
# 编辑 .env,然后载入当前 shell:
set -a; source .env; set +a
cargo run --release --package pumpswap_trading
```
也可以直接导出环境变量。`TARGET_MINT``TARGET_POOL` 至少设置一个;两者同时设置时必须都匹配。`MAX_EVENT_AGE_MS` 默认 1000。程序只读取环境变量,不会自行加载 `.env`
## 交易语义
- 买入使用 `BuyAmount::WithMaxInput`,适合优先成交的跟单/狙击场景,滑点限制最大 quote 成本。
- 买入参数使用事件中的成交后储备和 LP/protocol/creator fee bps。
- 原始 quote 储备和虚拟 quote 储备均来自同一笔交易的事件快照;热路径不再查询 Pool 账户,避免额外延迟和跨 slot 混合报价。
- 第一个匹配事件用于异步记录买前余额,下一条新鲜事件才会交易,因此提交热路径不再查询余额;示例只卖出确认后的余额增量,并在卖出前重新获取池状态和 blockhash。
- 若业务必须精确花费 quote,应改用 `BuyAmount::ExactInput`。这会启用最小输出保护,在活跃池中更容易因状态变化而失败。
- 基线预热失败时会等待后续事件;一旦进入交易阶段,错误会保持单次执行锁定,因为提交状态或持仓可能不确定,必须先核对签名和账户状态再重试。
生产机器人还应增加持久化签名去重、持仓状态机、SWQoS 配置和有限次数的重新报价。不要通过把 `min_out` 设为零来处理滑点错误。
+427 -139
View File
@@ -1,6 +1,4 @@
use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed;
use sol_trade_sdk::common::TradeConfig;
use sol_trade_sdk::instruction::utils::pumpswap::fetch_pool;
use sol_trade_sdk::common::{clock::now_micros, SolanaRpcClient, TradeConfig};
use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::{
common::AnyResult,
@@ -9,41 +7,160 @@ use sol_trade_sdk::{
core::params::{DexParamEnum, PumpSwapParams},
factory::DexType,
},
SolanaTrade,
AccountPolicy, BuyAmount, SellAmount, SimpleBuyParams, SimpleSellParams, SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use solana_sdk::{pubkey::Pubkey, signer::Signer};
use solana_sdk::{hash::Hash, pubkey::Pubkey};
use solana_streamer_sdk::streaming::event_parser::protocols::pumpswap::parser::PUMPSWAP_PROGRAM_ID;
use solana_streamer_sdk::streaming::event_parser::{
common::filter::EventTypeFilter, protocols::pumpswap::PumpSwapBuyEvent,
};
use solana_streamer_sdk::streaming::event_parser::{
common::EventType, protocols::pumpswap::PumpSwapSellEvent,
};
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
use solana_streamer_sdk::streaming::YellowstoneGrpc;
use solana_streamer_sdk::{
match_event, streaming::event_parser::protocols::pumpswap::parser::PUMPSWAP_PROGRAM_ID,
};
use std::str::FromStr;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
Arc, RwLock,
};
use std::time::{Duration, Instant};
use tokio::sync::watch;
// Global static flag to ensure transaction is executed only once
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
const BLOCKHASH_REFRESH_INTERVAL: Duration = Duration::from_millis(400);
const MAX_BLOCKHASH_AGE: Duration = Duration::from_secs(20);
#[derive(Clone, Copy)]
struct EventSelection {
target_mint: Option<Pubkey>,
target_pool: Option<Pubkey>,
max_event_age_ms: u64,
}
impl EventSelection {
fn from_env() -> AnyResult<Self> {
let target_mint = parse_optional_pubkey("TARGET_MINT")?;
let target_pool = parse_optional_pubkey("TARGET_POOL")?;
if target_mint.is_none() && target_pool.is_none() {
anyhow::bail!("set TARGET_MINT or TARGET_POOL before running this live example");
}
let max_event_age_ms = std::env::var("MAX_EVENT_AGE_MS")
.unwrap_or_else(|_| "1000".to_string())
.parse::<u64>()
.map_err(|_| anyhow::anyhow!("MAX_EVENT_AGE_MS must be a positive integer"))?;
if max_event_age_ms == 0 || max_event_age_ms > i64::MAX as u64 / 1_000 {
anyhow::bail!("MAX_EVENT_AGE_MS is outside the supported range");
}
Ok(Self { target_mint, target_pool, max_event_age_ms })
}
fn matches(self, pool: Pubkey, base_mint: Pubkey, quote_mint: Pubkey, recv_us: i64) -> bool {
if self.target_pool.is_some_and(|target| target != pool) {
return false;
}
if self.target_mint.is_some_and(|target| target != base_mint && target != quote_mint) {
return false;
}
is_event_fresh(recv_us, now_micros(), self.max_event_age_ms)
}
}
fn parse_optional_pubkey(key: &str) -> AnyResult<Option<Pubkey>> {
std::env::var(key)
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.map(|value| Pubkey::from_str(&value).map_err(anyhow::Error::from))
.transpose()
}
fn is_event_fresh(recv_us: i64, now_us: i64, max_age_ms: u64) -> bool {
recv_us > 0
&& now_us >= recv_us
&& now_us.saturating_sub(recv_us) <= (max_age_ms as i64).saturating_mul(1_000)
}
#[derive(Clone)]
struct CachedBlockhash {
hash: Hash,
fetched_at: Instant,
}
#[derive(Clone, Copy)]
struct PositionBaseline {
mint: Pubkey,
token_program: Pubkey,
amount: u64,
}
enum EventAction {
BaselineWarmed,
TradeCompleted,
}
#[derive(Clone)]
struct BlockhashCache {
receiver: watch::Receiver<CachedBlockhash>,
}
impl BlockhashCache {
async fn start(rpc: Arc<SolanaRpcClient>) -> AnyResult<Self> {
let initial =
CachedBlockhash { hash: rpc.get_latest_blockhash().await?, fetched_at: Instant::now() };
let (sender, receiver) = watch::channel(initial);
tokio::spawn(async move {
let mut interval = tokio::time::interval(BLOCKHASH_REFRESH_INTERVAL);
interval.tick().await;
loop {
interval.tick().await;
match rpc.get_latest_blockhash().await {
Ok(hash) => {
if sender
.send(CachedBlockhash { hash, fetched_at: Instant::now() })
.is_err()
{
break;
}
}
Err(err) => eprintln!("warning: blockhash refresh failed: {err}"),
}
}
});
Ok(Self { receiver })
}
fn latest(&self) -> AnyResult<Hash> {
let cached = self.receiver.borrow().clone();
if cached.fetched_at.elapsed() > MAX_BLOCKHASH_AGE {
anyhow::bail!("cached blockhash is older than {} seconds", MAX_BLOCKHASH_AGE.as_secs());
}
Ok(cached.hash)
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Subscribing to GRPC events...");
let selection = EventSelection::from_env()?;
let trade_client = Arc::new(create_solana_trade_client().await?);
let blockhash_cache = BlockhashCache::start(trade_client.infrastructure.rpc.clone()).await?;
let position_baseline = Arc::new(RwLock::new(None));
let grpc = YellowstoneGrpc::new(
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
None,
std::env::var("GRPC_ENDPOINT")
.unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()),
std::env::var("GRPC_AUTH_TOKEN").ok(),
)?;
let callback = create_event_callback();
let callback =
create_event_callback(trade_client, blockhash_cache, position_baseline, selection);
let protocols = vec![Protocol::PumpSwap];
// Filter accounts
let account_include = vec![
@@ -64,8 +181,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
// listen to specific event type
let event_type_filter =
EventTypeFilter { include: vec![EventType::PumpSwapBuy, EventType::PumpSwapSell] };
let event_type_filter = EventTypeFilter {
include: vec![EventType::PumpSwapBuy, EventType::PumpSwapSell],
..Default::default()
};
grpc.subscribe_events_immediate(
protocols,
@@ -84,53 +203,120 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
/// Create an event callback function that handles different types of events
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
match_event!(event, {
PumpSwapBuyEvent => |e: PumpSwapBuyEvent| {
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return;
}
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let event_clone = e.clone();
tokio::spawn(async move {
if let Err(err) = pumpswap_trade_with_grpc_buy_event(event_clone).await {
eprintln!("Error in trade: {:?}", err);
std::process::exit(0);
}
});
}
},
PumpSwapSellEvent => |e: PumpSwapSellEvent| {
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return;
}
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let event_clone = e.clone();
tokio::spawn(async move {
if let Err(err) = pumpswap_trade_with_grpc_sell_event(event_clone).await {
eprintln!("Error in trade: {:?}", err);
std::process::exit(0);
}
});
}
fn create_event_callback(
client: Arc<SolanaTrade>,
blockhash_cache: BlockhashCache,
position_baseline: Arc<RwLock<Option<PositionBaseline>>>,
selection: EventSelection,
) -> impl Fn(DexEvent) {
move |event: DexEvent| match event {
DexEvent::PumpSwapBuyEvent(e) => {
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return;
}
});
if !selection.matches(e.pool, e.base_mint, e.quote_mint, e.metadata.recv_us) {
return;
}
// Test code, only test one transaction
if ALREADY_EXECUTED
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
let client = client.clone();
let blockhash_cache = blockhash_cache.clone();
let position_baseline = position_baseline.clone();
let was_preparing =
position_baseline.read().map(|baseline| baseline.is_none()).unwrap_or(true);
tokio::spawn(async move {
match pumpswap_trade_with_grpc_buy_event(
client,
blockhash_cache,
position_baseline,
selection,
e,
)
.await
{
Ok(EventAction::BaselineWarmed) => {
ALREADY_EXECUTED.store(false, Ordering::Release);
}
Ok(EventAction::TradeCompleted) => {}
Err(err) if was_preparing => {
eprintln!("baseline warmup failed; waiting for a later event: {err:?}");
ALREADY_EXECUTED.store(false, Ordering::Release);
}
Err(err) => {
eprintln!(
"trade failed after entering execution state: {err:?}; automatic retry is disabled because submission status or position state may be uncertain"
);
}
}
});
}
}
DexEvent::PumpSwapSellEvent(e) => {
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return;
}
if !selection.matches(e.pool, e.base_mint, e.quote_mint, e.metadata.recv_us) {
return;
}
// Test code, only test one transaction
if ALREADY_EXECUTED
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
let client = client.clone();
let blockhash_cache = blockhash_cache.clone();
let position_baseline = position_baseline.clone();
let was_preparing =
position_baseline.read().map(|baseline| baseline.is_none()).unwrap_or(true);
tokio::spawn(async move {
match pumpswap_trade_with_grpc_sell_event(
client,
blockhash_cache,
position_baseline,
selection,
e,
)
.await
{
Ok(EventAction::BaselineWarmed) => {
ALREADY_EXECUTED.store(false, Ordering::Release);
}
Ok(EventAction::TradeCompleted) => {}
Err(err) if was_preparing => {
eprintln!("baseline warmup failed; waiting for a later event: {err:?}");
ALREADY_EXECUTED.store(false, Ordering::Release);
}
Err(err) => {
eprintln!(
"trade failed after entering execution state: {err:?}; automatic retry is disabled because submission status or position state may be uncertain"
);
}
}
});
}
}
_ => {}
}
}
/// Create SolanaTrade client
/// Initializes a new SolanaTrade client with configuration
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("your_payer_keypair_here");
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
println!("Initializing SolanaTrade client...");
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = std::env::var("RPC_URL")
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
@@ -142,14 +328,18 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
// .mev_protection(false) // default: false
.build();
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
println!("SolanaTrade client initialized successfully!");
println!("SolanaTrade client initialized successfully");
Ok(solana_trade)
}
async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> AnyResult<()> {
let client = create_solana_trade_client().await?;
let pool_data = fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?;
let params = PumpSwapParams::from_trade(
async fn pumpswap_trade_with_grpc_buy_event(
client: Arc<SolanaTrade>,
blockhash_cache: BlockhashCache,
position_baseline: Arc<RwLock<Option<PositionBaseline>>>,
selection: EventSelection,
trade_info: PumpSwapBuyEvent,
) -> AnyResult<EventAction> {
let params = PumpSwapParams::from_trade_with_fee_basis_points(
trade_info.pool,
trade_info.base_mint,
trade_info.quote_mint,
@@ -157,14 +347,19 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
trade_info.pool_quote_token_account,
trade_info.pool_base_token_reserves,
trade_info.pool_quote_token_reserves,
trade_info.virtual_quote_reserves,
trade_info.coin_creator_vault_ata,
trade_info.coin_creator_vault_authority,
trade_info.base_token_program,
trade_info.quote_token_program,
trade_info.protocol_fee_recipient,
pool_data.coin_creator,
pool_data.is_cashback_coin,
0,
Pubkey::default(),
trade_info.coin_creator,
trade_info.cashback_fee_basis_points != 0 || trade_info.cashback != 0,
trade_info.cashback_fee_basis_points,
trade_info.lp_fee_basis_points,
trade_info.protocol_fee_basis_points,
trade_info.coin_creator_fee_basis_points,
);
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
@@ -173,14 +368,26 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
} else {
trade_info.base_mint
};
pumpswap_trade_with_grpc(&client, mint, params).await?;
Ok(())
pumpswap_trade_with_grpc(
&client,
&blockhash_cache,
&position_baseline,
trade_info.metadata.recv_us,
selection.max_event_age_ms,
mint,
params,
)
.await
}
async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> AnyResult<()> {
let client = create_solana_trade_client().await?;
let pool_data = fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?;
let params = PumpSwapParams::from_trade(
async fn pumpswap_trade_with_grpc_sell_event(
client: Arc<SolanaTrade>,
blockhash_cache: BlockhashCache,
position_baseline: Arc<RwLock<Option<PositionBaseline>>>,
selection: EventSelection,
trade_info: PumpSwapSellEvent,
) -> AnyResult<EventAction> {
let params = PumpSwapParams::from_trade_with_fee_basis_points(
trade_info.pool,
trade_info.base_mint,
trade_info.quote_mint,
@@ -188,14 +395,19 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
trade_info.pool_quote_token_account,
trade_info.pool_base_token_reserves,
trade_info.pool_quote_token_reserves,
trade_info.virtual_quote_reserves,
trade_info.coin_creator_vault_ata,
trade_info.coin_creator_vault_authority,
trade_info.base_token_program,
trade_info.quote_token_program,
trade_info.protocol_fee_recipient,
pool_data.coin_creator,
pool_data.is_cashback_coin,
0,
Pubkey::default(),
trade_info.coin_creator,
trade_info.cashback_fee_basis_points != 0 || trade_info.cashback != 0,
trade_info.cashback_fee_basis_points,
trade_info.lp_fee_basis_points,
trade_info.protocol_fee_basis_points,
trade_info.coin_creator_fee_basis_points,
);
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
@@ -204,90 +416,166 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
} else {
trade_info.base_mint
};
pumpswap_trade_with_grpc(&client, mint, params).await?;
Ok(())
pumpswap_trade_with_grpc(
&client,
&blockhash_cache,
&position_baseline,
trade_info.metadata.recv_us,
selection.max_event_age_ms,
mint,
params,
)
.await
}
async fn pumpswap_trade_with_grpc(
client: &SolanaTrade,
blockhash_cache: &BlockhashCache,
position_baseline: &Arc<RwLock<Option<PositionBaseline>>>,
grpc_recv_us: i64,
max_event_age_ms: u64,
mint_pubkey: Pubkey,
params: PumpSwapParams,
) -> AnyResult<()> {
) -> AnyResult<EventAction> {
println!("Testing PumpSwap trading...");
validate_pumpswap_snapshot(&params)?;
if !is_event_fresh(grpc_recv_us, now_micros(), max_event_age_ms) {
anyhow::bail!("event became stale before transaction construction");
}
let slippage_basis_points = Some(500);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
let is_sol = params.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| params.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let program_id = if params.base_mint == mint_pubkey {
params.base_token_program
} else if params.quote_mint == mint_pubkey {
params.quote_token_program
} else {
anyhow::bail!("target mint {} does not belong to pool {}", mint_pubkey, params.pool);
};
let baseline = position_baseline
.read()
.map_err(|_| anyhow::anyhow!("position baseline lock is poisoned"))?
.as_ref()
.copied();
let balance_before = if let Some(baseline) = baseline {
if baseline.mint != mint_pubkey || baseline.token_program != program_id {
anyhow::bail!("cached position baseline belongs to a different mint or token program");
}
baseline.amount
} else {
let amount = client.get_payer_token_balance_with_program(&mint_pubkey, &program_id).await?;
let mut baseline = position_baseline
.write()
.map_err(|_| anyhow::anyhow!("position baseline lock is poisoned"))?;
*baseline = Some(PositionBaseline { mint: mint_pubkey, token_program: program_id, amount });
println!(
"Position baseline warmed at {} base units; waiting for the next fresh matching event",
amount
);
return Ok(EventAction::BaselineWarmed);
};
let recent_blockhash = blockhash_cache.latest()?;
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
if !is_event_fresh(grpc_recv_us, now_micros(), max_event_age_ms) {
anyhow::bail!("event became stale while preparing the transaction");
}
// Buy tokens
println!("Buying tokens from PumpSwap...");
let buy_token_amount = 300_000;
let buy_params = sol_trade_sdk::TradeBuyParams {
dex_type: DexType::PumpSwap,
input_token_type: if is_sol { TradeTokenType::SOL } else { TradeTokenType::USDC },
mint: mint_pubkey,
input_token_amount: buy_token_amount,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_tx_confirmed: true,
create_input_token_ata: is_sol,
close_input_token_ata: is_sol,
create_mint_ata: true,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
let buy_params = SimpleBuyParams::new(
DexType::PumpSwap,
if is_sol { TradeTokenType::SOL } else { TradeTokenType::USDC },
mint_pubkey,
BuyAmount::WithMaxInput { quote_amount: buy_token_amount },
DexParamEnum::PumpSwap(params.clone()),
recent_blockhash,
gas_fee_strategy.clone(),
)
.slippage_basis_points(slippage_basis_points.unwrap_or(500))
.account_policy(AccountPolicy::Auto)
.wait_tx_confirmed(true)
.grpc_recv_us(grpc_recv_us);
let (ok, sigs, err, _) = client.buy_simple(buy_params).await?;
if !ok {
anyhow::bail!("buy failed: {:?}; signatures: {:?}", err, sigs);
}
// Sell tokens
println!("Selling tokens from PumpSwap...");
let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey();
let program_id = if params.base_mint == mint_pubkey {
params.base_token_program
} else {
params.quote_token_program
};
let account = get_associated_token_address_with_program_id_fast_use_seed(
&payer,
&mint_pubkey,
&program_id,
client.use_seed_optimize,
);
let balance = rpc.get_token_account_balance(&account).await?;
let amount_token = balance.amount.parse::<u64>().unwrap();
let sell_params = sol_trade_sdk::TradeSellParams {
dex_type: DexType::PumpSwap,
output_token_type: if is_sol { TradeTokenType::SOL } else { TradeTokenType::USDC },
mint: mint_pubkey,
input_token_amount: amount_token,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_tx_confirmed: true,
create_output_token_ata: is_sol,
close_output_token_ata: is_sol,
close_mint_token_ata: false,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
let balance_after =
client.get_payer_token_balance_with_program(&mint_pubkey, &program_id).await?;
let position_amount = balance_after.checked_sub(balance_before).ok_or_else(|| {
anyhow::anyhow!(
"token balance decreased from {} to {}; refusing to sell existing holdings",
balance_before,
balance_after
)
})?;
if position_amount == 0 {
anyhow::bail!("confirmed buy did not increase token balance; refusing to sell");
}
let sell_params_from_rpc =
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &params.pool).await?;
let sell_params = SimpleSellParams::new(
DexType::PumpSwap,
if is_sol { TradeTokenType::SOL } else { TradeTokenType::USDC },
mint_pubkey,
SellAmount::ExactInput(position_amount),
DexParamEnum::PumpSwap(sell_params_from_rpc),
blockhash_cache.latest()?,
gas_fee_strategy,
)
.slippage_basis_points(slippage_basis_points.unwrap_or(500))
.account_policy(AccountPolicy::Auto)
.wait_tx_confirmed(true)
.with_tip(false);
let (ok, sigs, err, _) = client.sell_simple(sell_params).await?;
if !ok {
anyhow::bail!("sell failed: {:?}; signatures: {:?}", err, sigs);
}
// Exit program
std::process::exit(0);
println!("Round-trip example completed; further matching events remain locked out");
Ok(EventAction::TradeCompleted)
}
fn validate_pumpswap_snapshot(params: &PumpSwapParams) -> AnyResult<()> {
let required = [
("pool", params.pool),
("base_mint", params.base_mint),
("quote_mint", params.quote_mint),
("pool_base_token_account", params.pool_base_token_account),
("pool_quote_token_account", params.pool_quote_token_account),
("coin_creator_vault_ata", params.coin_creator_vault_ata),
("coin_creator_vault_authority", params.coin_creator_vault_authority),
("base_token_program", params.base_token_program),
("quote_token_program", params.quote_token_program),
];
for (name, value) in required {
if value == Pubkey::default() {
anyhow::bail!("event snapshot is missing {name}");
}
}
if params.pool_base_token_reserves == 0 || params.pool_quote_token_reserves == 0 {
anyhow::bail!("event snapshot has an empty raw pool reserve");
}
params.effective_quote_reserves()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::is_event_fresh;
#[test]
fn event_freshness_has_a_strict_boundary() {
assert!(!is_event_fresh(0, 1_000_000, 100));
assert!(!is_event_fresh(1_000_001, 1_000_000, 100));
assert!(!is_event_fresh(899_999, 1_000_000, 100));
assert!(is_event_fresh(900_000, 1_000_000, 100));
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+13
View File
@@ -0,0 +1,13 @@
# Raydium AMM V4 Trading
[中文](README_CN.md)
Consumes Raydium AMM V4 events from `solana-streamer-sdk`, builds AMM params, and executes one buy/sell flow.
> This is a live-transaction template. Set `PRIVATE_KEY`, RPC/stream configuration, target filters, token amounts, and fees before running.
```bash
cargo run --package raydium_amm_v4_trading
```
AMM address, market accounts, vaults, token programs, and reserve direction must match the event. For low latency, prewarm the client and blockhash cache and refresh delayed-sell state rather than reusing the triggering snapshot.
@@ -0,0 +1,13 @@
# Raydium AMM V4 交易
[English](README.md)
通过 `solana-streamer-sdk` 消费 Raydium AMM V4 事件,构建 AMM 参数并执行一次买卖流程。
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`、RPC/stream 配置、目标过滤、token 数量和费用。
```bash
cargo run --package raydium_amm_v4_trading
```
AMM 地址、market 账户、vault、token program 和储备方向必须与事件一致。低延迟场景应预热客户端和 blockhash cache,并刷新延迟卖出状态,不能复用触发快照。
+50 -47
View File
@@ -1,4 +1,3 @@
use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed;
use sol_trade_sdk::{
common::AnyResult,
swqos::SwqosConfig,
@@ -10,17 +9,13 @@ use sol_trade_sdk::{
};
use sol_trade_sdk::{common::TradeConfig, TradeTokenType};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer;
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
use solana_streamer_sdk::streaming::event_parser::common::EventType;
use solana_streamer_sdk::streaming::event_parser::protocols::raydium_amm_v4::parser::RAYDIUM_AMM_V4_PROGRAM_ID;
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
use solana_streamer_sdk::streaming::event_parser::protocols::raydium_amm_v4::RaydiumAmmV4SwapEvent;
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
use solana_streamer_sdk::streaming::yellowstone_grpc::TransactionFilter;
use solana_streamer_sdk::streaming::YellowstoneGrpc;
use solana_streamer_sdk::{
match_event, streaming::event_parser::protocols::raydium_amm_v4::RaydiumAmmV4SwapEvent,
};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -54,19 +49,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
account_required,
};
// Listen to account data belonging to owner programs -> account event monitoring
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
// listen to specific event type
let event_type_filter = EventTypeFilter {
include: vec![EventType::RaydiumAmmV4SwapBaseIn, EventType::RaydiumAmmV4SwapBaseOut],
};
let event_type_filter = EventTypeFilter::include_only(vec![
EventType::RaydiumAmmV4SwapBaseIn,
EventType::RaydiumAmmV4SwapBaseOut,
]);
grpc.subscribe_events_immediate(
protocols,
None,
vec![transaction_filter],
vec![account_filter],
vec![],
Some(event_type_filter),
None,
callback,
@@ -74,27 +67,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?;
tokio::signal::ctrl_c().await?;
grpc.stop().await;
Ok(())
}
/// Create an event callback function that handles different types of events
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
match_event!(event, {
RaydiumAmmV4SwapEvent => |e: RaydiumAmmV4SwapEvent| {
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let event_clone = e.clone();
tokio::spawn(async move {
if let Err(err) = raydium_amm_v4_copy_trade_with_grpc(event_clone).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(0);
}
});
fn create_event_callback() -> impl Fn(DexEvent) {
|event: DexEvent| {
let DexEvent::RaydiumAmmV4SwapEvent(event) = event else {
return;
};
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
tokio::spawn(async move {
if let Err(err) = raydium_amm_v4_copy_trade_with_grpc(event).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(1);
}
},
});
});
}
}
}
@@ -102,7 +93,7 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
/// Initializes a new SolanaTrade client with configuration
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("your_payer_keypair_here");
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
@@ -147,6 +138,9 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
let input_token_amount = 100_000;
let is_wsol = params.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| params.coin_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let mint_token_program = client.infrastructure.rpc.get_account(&mint_pubkey).await?.owner;
let balance_before =
client.get_payer_token_balance_with_program(&mint_pubkey, &mint_token_program).await?;
let buy_params = sol_trade_sdk::TradeBuyParams {
dex_type: DexType::RaydiumAmmV4,
input_token_type: if is_wsol { TradeTokenType::WSOL } else { TradeTokenType::USDC },
@@ -155,8 +149,9 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumAmmV4(params),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_wsol,
close_input_token_ata: is_wsol,
create_mint_ata: true,
@@ -167,22 +162,24 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
let (ok, sigs, err, _) = client.buy(buy_params).await?;
if !ok {
return Err(
std::io::Error::other(format!("buy failed: {:?}; sigs: {:?}", err, sigs)).into()
);
}
// Sell tokens
println!("Selling tokens from Raydium_amm_v4...");
let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey();
let account = get_associated_token_address_with_program_id_fast_use_seed(
&payer,
&mint_pubkey,
&trade_info.token_program,
client.use_seed_optimize,
);
let balance = rpc.get_token_account_balance(&account).await?;
println!("Balance: {:?}", balance);
let amount_token = balance.amount.parse::<u64>().unwrap();
let balance_after =
client.get_payer_token_balance_with_program(&mint_pubkey, &mint_token_program).await?;
let amount_token = balance_after
.checked_sub(balance_before)
.ok_or_else(|| std::io::Error::other("token balance decreased after buy"))?;
if amount_token == 0 {
return Err(std::io::Error::other("confirmed buy did not increase token balance").into());
}
println!("Selling {} tokens", amount_token);
let params =
@@ -194,11 +191,12 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
mint: mint_pubkey,
input_token_amount: amount_token,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
with_tip: false,
extension_params: DexParamEnum::RaydiumAmmV4(params),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_wsol,
close_output_token_ata: is_wsol,
close_mint_token_ata: false,
@@ -208,7 +206,12 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
let (ok, sigs, err, _) = client.sell(sell_params).await?;
if !ok {
return Err(
std::io::Error::other(format!("sell failed: {:?}; sigs: {:?}", err, sigs)).into()
);
}
// Exit program
std::process::exit(0);
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+13
View File
@@ -0,0 +1,13 @@
# Raydium CPMM Trading
[中文](README_CN.md)
Consumes Raydium CPMM events from `solana-streamer-sdk`, maps event pool data into SDK params, then performs one buy and sell.
> This is a live-transaction template. Set `PRIVATE_KEY`, RPC/stream configuration, target filters, amounts, and fee settings before running.
```bash
cargo run --package raydium_cpmm_trading
```
Production bots must initialize the trading client and blockhash cache before subscription, filter and deduplicate events, reject stale events, and refresh state before delayed sells. The current code focuses on CPMM parameter mapping.
@@ -0,0 +1,13 @@
# Raydium CPMM 交易
[English](README.md)
通过 `solana-streamer-sdk` 消费 Raydium CPMM 事件,将事件池数据映射为 SDK 参数,然后执行一次买入和卖出。
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`、RPC/stream 配置、目标过滤、金额和费用设置。
```bash
cargo run --package raydium_cpmm_trading
```
生产 Bot 必须在订阅前初始化交易客户端和 blockhash cache,过滤并去重事件、拒绝过期事件,并在延迟卖出前刷新状态。当前代码重点展示 CPMM 参数映射。
+63 -47
View File
@@ -1,4 +1,3 @@
use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address;
use sol_trade_sdk::common::TradeConfig;
use sol_trade_sdk::constants::{USDC_TOKEN_ACCOUNT, WSOL_TOKEN_ACCOUNT};
use sol_trade_sdk::trading::core::params::{DexParamEnum, RaydiumCpmmParams};
@@ -6,17 +5,13 @@ use sol_trade_sdk::trading::factory::DexType;
use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::{common::AnyResult, swqos::SwqosConfig, SolanaTrade};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer;
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
use solana_streamer_sdk::streaming::event_parser::common::EventType;
use solana_streamer_sdk::streaming::event_parser::protocols::raydium_cpmm::parser::RAYDIUM_CPMM_PROGRAM_ID;
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
use solana_streamer_sdk::streaming::event_parser::protocols::raydium_cpmm::RaydiumCpmmSwapEvent;
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
use solana_streamer_sdk::streaming::yellowstone_grpc::TransactionFilter;
use solana_streamer_sdk::streaming::YellowstoneGrpc;
use solana_streamer_sdk::{
match_event, streaming::event_parser::protocols::raydium_cpmm::RaydiumCpmmSwapEvent,
};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -50,19 +45,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
account_required,
};
// Listen to account data belonging to owner programs -> account event monitoring
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
// listen to specific event type
let event_type_filter = EventTypeFilter {
include: vec![EventType::RaydiumCpmmSwapBaseInput, EventType::RaydiumCpmmSwapBaseOutput],
};
let event_type_filter = EventTypeFilter::include_only(vec![
EventType::RaydiumCpmmSwapBaseInput,
EventType::RaydiumCpmmSwapBaseOutput,
]);
grpc.subscribe_events_immediate(
protocols,
None,
vec![transaction_filter],
vec![account_filter],
vec![],
Some(event_type_filter),
None,
callback,
@@ -70,32 +63,32 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?;
tokio::signal::ctrl_c().await?;
grpc.stop().await;
Ok(())
}
/// Create an event callback function that handles different types of events
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
match_event!(event, {
RaydiumCpmmSwapEvent => |e: RaydiumCpmmSwapEvent| {
let is_wsol = e.input_token_mint == WSOL_TOKEN_ACCOUNT || e.output_token_mint == WSOL_TOKEN_ACCOUNT;
let is_usdc = e.input_token_mint == USDC_TOKEN_ACCOUNT || e.output_token_mint == USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return;
fn create_event_callback() -> impl Fn(DexEvent) {
|event: DexEvent| {
let DexEvent::RaydiumCpmmSwapEvent(event) = event else {
return;
};
let is_wsol = event.input_token_mint == WSOL_TOKEN_ACCOUNT
|| event.output_token_mint == WSOL_TOKEN_ACCOUNT;
let is_usdc = event.input_token_mint == USDC_TOKEN_ACCOUNT
|| event.output_token_mint == USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return;
}
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
tokio::spawn(async move {
if let Err(err) = raydium_cpmm_copy_trade_with_grpc(event).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(1);
}
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let event_clone = e.clone();
tokio::spawn(async move {
if let Err(err) = raydium_cpmm_copy_trade_with_grpc(event_clone).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(0);
}
});
}
},
});
});
}
}
}
@@ -103,7 +96,7 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
/// Initializes a new SolanaTrade client with configuration
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("your_payer_keypair_here");
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
@@ -147,6 +140,15 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
let is_wsol = trade_info.input_token_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| trade_info.output_token_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let mint_token_program = if buy_params.base_mint == mint_pubkey {
buy_params.base_token_program
} else if buy_params.quote_mint == mint_pubkey {
buy_params.quote_token_program
} else {
return Err(std::io::Error::other("target mint does not belong to pool").into());
};
let balance_before =
client.get_payer_token_balance_with_program(&mint_pubkey, &mint_token_program).await?;
// Buy tokens
println!("Buying tokens from Raydium_cpmm...");
@@ -159,8 +161,9 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumCpmm(buy_params),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_wsol,
close_input_token_ata: is_wsol,
create_mint_ata: true,
@@ -171,17 +174,24 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
let (ok, sigs, err, _) = client.buy(buy_params).await?;
if !ok {
return Err(
std::io::Error::other(format!("buy failed: {:?}; sigs: {:?}", err, sigs)).into()
);
}
// Sell tokens
println!("Selling tokens from Raydium_cpmm...");
let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey();
let account = get_associated_token_address(&payer, &mint_pubkey);
let balance = rpc.get_token_account_balance(&account).await?;
println!("Balance: {:?}", balance);
let amount_token = balance.amount.parse::<u64>().unwrap();
let balance_after =
client.get_payer_token_balance_with_program(&mint_pubkey, &mint_token_program).await?;
let amount_token = balance_after
.checked_sub(balance_before)
.ok_or_else(|| std::io::Error::other("token balance decreased after buy"))?;
if amount_token == 0 {
return Err(std::io::Error::other("confirmed buy did not increase token balance").into());
}
let sell_params = RaydiumCpmmParams::from_pool_address_by_rpc(
&client.infrastructure.rpc,
@@ -196,11 +206,12 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
mint: mint_pubkey,
input_token_amount: amount_token,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
with_tip: false,
extension_params: DexParamEnum::RaydiumCpmm(sell_params),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_wsol,
close_output_token_ata: is_wsol,
close_mint_token_ata: false,
@@ -210,7 +221,12 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
let (ok, sigs, err, _) = client.sell(sell_params).await?;
if !ok {
return Err(
std::io::Error::other(format!("sell failed: {:?}; sigs: {:?}", err, sigs)).into()
);
}
// Exit program
std::process::exit(0);
+3 -2
View File
@@ -5,7 +5,8 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
+13
View File
@@ -0,0 +1,13 @@
# Seed-Optimized Trading
[中文](README_CN.md)
Demonstrates PumpSwap buy/sell account derivation with seed optimization enabled, including the matching token-account lookup used before selling.
> This is a live mainnet transaction template. Set `PRIVATE_KEY` and `RPC_URL`, replace mint and pool values, and review amounts, slippage, fees, and account-close flags before running.
```bash
cargo run --package seed_trading
```
The same `use_seed_optimize` policy must be used when deriving accounts and building transactions. The example reuses a blockhash across a delay; production code should refresh it before the sell.
+13
View File
@@ -0,0 +1,13 @@
# Seed 优化交易
[English](README.md)
展示开启 seed 优化后的 PumpSwap 买卖账户派生,以及卖出前使用相同规则查询 token 账户余额。
> 这是会发送真实主网交易的模板。运行前设置 `PRIVATE_KEY``RPC_URL`,替换 mint 和 pool,并检查金额、滑点、费用及账户关闭 flag。
```bash
cargo run --package seed_trading
```
派生账户和构建交易必须使用一致的 `use_seed_optimize` 策略。示例在等待后复用 blockhash;生产代码应在卖出前刷新。
+39 -30
View File
@@ -1,7 +1,5 @@
use sol_trade_sdk::{
common::{
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, AnyResult, TradeConfig,
},
common::{AnyResult, TradeConfig},
swqos::SwqosConfig,
trading::{
core::params::{DexParamEnum, PumpSwapParams},
@@ -10,12 +8,11 @@ use sol_trade_sdk::{
SolanaTrade, TradeTokenType,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use solana_sdk::{pubkey::Pubkey, signer::Signer};
use solana_sdk::pubkey::Pubkey;
use std::{str::FromStr, sync::Arc};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn main() -> AnyResult<()> {
println!("Testing PumpSwap trading...");
let client = create_solana_trade_client().await?;
@@ -26,6 +23,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
let pool_params =
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?;
let token_program = if pool_params.base_mint == mint_pubkey {
pool_params.base_token_program
} else if pool_params.quote_mint == mint_pubkey {
pool_params.quote_token_program
} else {
anyhow::bail!("target mint does not belong to the configured pool");
};
let balance_before =
client.get_payer_token_balance_with_program(&mint_pubkey, &token_program).await?;
// Buy tokens
println!("Buying tokens from PumpSwap...");
@@ -37,11 +45,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
input_token_amount: buy_sol_amount,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpSwap(
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
extension_params: DexParamEnum::PumpSwap(pool_params),
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -52,38 +59,38 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
let (ok, sigs, err, _) = client.buy(buy_params).await?;
if !ok {
anyhow::bail!("buy failed: {:?}; signatures: {:?}", err, sigs);
}
tokio::time::sleep(std::time::Duration::from_secs(4)).await;
// Sell tokens
println!("Selling tokens from PumpSwap...");
let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey();
let program_id = sol_trade_sdk::constants::TOKEN_PROGRAM;
// ❗️❗️❗️❗️ Must use the 'use seed' method to get the ATA account, otherwise the transaction will fail
let account = get_associated_token_address_with_program_id_fast_use_seed(
&payer,
&mint_pubkey,
&program_id,
true,
);
let balance = rpc.get_token_account_balance(&account).await?;
let amount_token = balance.amount.parse::<u64>().unwrap();
let balance_after =
client.get_payer_token_balance_with_program(&mint_pubkey, &token_program).await?;
let amount_token = balance_after.checked_sub(balance_before).ok_or_else(|| {
anyhow::anyhow!("token balance decreased after buy; refusing to sell existing holdings")
})?;
if amount_token == 0 {
anyhow::bail!("confirmed buy did not increase token balance");
}
let sell_params = sol_trade_sdk::TradeSellParams {
dex_type: DexType::PumpSwap,
output_token_type: TradeTokenType::WSOL,
mint: mint_pubkey,
input_token_amount: amount_token,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
with_tip: false,
extension_params: DexParamEnum::PumpSwap(
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
@@ -93,9 +100,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
tokio::signal::ctrl_c().await?;
let (ok, sigs, err, _) = client.sell(sell_params).await?;
if !ok {
anyhow::bail!("sell failed: {:?}; signatures: {:?}", err, sigs);
}
Ok(())
}
@@ -103,8 +111,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// Initializes a new SolanaTrade client with configuration
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = std::env::var("RPC_URL")
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
+13
View File
@@ -0,0 +1,13 @@
# Shared Infrastructure
[中文](README_CN.md)
Demonstrates sharing one `TradingInfrastructure` across multiple wallet-specific `TradingClient` values. This reduces connection, memory, and initialization overhead in multi-wallet services.
Replace the RPC, SWQoS credentials, and all wallet placeholders in `src/main.rs`. The example initializes clients and prints sharing information; it does not submit a trade.
```bash
cargo run --package shared_infrastructure
```
Only share infrastructure when wallets use the same RPC, commitment, and submission routes. Keep wallet signing and position state isolated.
@@ -0,0 +1,13 @@
# 多钱包共享基础设施
[English](README.md)
展示多个钱包专属 `TradingClient` 共享一份 `TradingInfrastructure`,降低多钱包服务的连接数、内存和初始化开销。
运行前替换 `src/main.rs` 中的 RPC、SWQoS 凭证和全部钱包占位值。示例只初始化客户端并打印共享信息,不提交交易。
```bash
cargo run --package shared_infrastructure
```
只有钱包使用相同 RPC、commitment 和提交通道时才共享基础设施;签名权限和持仓状态仍必须按钱包隔离。
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "simple_trading"
version = "0.1.0"
edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
solana-sdk = "3.0.0"
tokio = { version = "1", features = ["full"] }
+16
View File
@@ -0,0 +1,16 @@
# Simple Trading API
[中文](README_CN.md)
This is the recommended starting point for `SimpleBuyParams`, `SimpleSellParams`, `BuyAmount`, `SellAmount`, account policies, recent blockhashes, and durable nonces.
The example constructs parameters but does not submit the commented buy/sell calls. Set `PRIVATE_KEY` and replace protocol-state placeholders before adapting it to a live integration.
```bash
export RPC_URL=https://your-rpc.example
cargo run --package simple_trading
```
`WithMaxInput` protects maximum quote cost and is generally appropriate when fill priority matters. `ExactInput` protects minimum output and may fail more often in active markets. `HotPathMinimal` requires all needed token accounts to exist; normal integrations should use `AccountPolicy::Auto`.
See [Trading Parameters](../../docs/TRADING_PARAMETERS.md) and [Low-Latency Bots](../../docs/LOW_LATENCY_BOTS.md).
+16
View File
@@ -0,0 +1,16 @@
# 简化交易 API
[English](README.md)
这是学习 `SimpleBuyParams``SimpleSellParams``BuyAmount``SellAmount`、账户策略、recent blockhash 和 durable nonce 的推荐入口。
示例只构造参数,买卖调用保持注释状态。用于真实集成前必须设置 `PRIVATE_KEY` 并替换协议状态占位值。
```bash
export RPC_URL=https://your-rpc.example
cargo run --package simple_trading
```
`WithMaxInput` 保护最大 quote 成本,适合优先成交;`ExactInput` 保护最小输出,在活跃市场更容易失败。`HotPathMinimal` 要求所需 token 账户均已存在,普通接入应使用 `AccountPolicy::Auto`
参见[交易参数说明](../../docs/TRADING_PARAMETERS_CN.md)和[低延迟 Bot 指南](../../docs/LOW_LATENCY_BOTS_CN.md)。
+138
View File
@@ -0,0 +1,138 @@
//! Simple high-level trading API example.
//!
//! This example focuses on parameter construction. Replace the placeholder
//! PumpFun params with real params from your parser/RPC cache before sending.
use std::sync::Arc;
use sol_trade_sdk::{
common::{GasFeeStrategy, TradeConfig},
constants::{TOKEN_PROGRAM_2022, WSOL_TOKEN_ACCOUNT},
instruction::utils::pumpfun::global_constants,
swqos::SwqosConfig,
trading::{
core::params::{DexParamEnum, PumpFunParams},
factory::DexType,
},
AccountPolicy, BuyAmount, DurableNonceInfo, SellAmount, SimpleBuyParams, SimpleSellParams,
SolanaTrade, TradeTokenType,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{hash::Hash, pubkey::Pubkey, signer::Signer};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = std::env::var("RPC_URL")
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::processed();
let swqos_configs = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// Default is true. WSOL ATA is prepared on startup, not in the hot trade tx.
.create_wsol_ata_on_startup(true)
.build();
let client = SolanaTrade::new(Arc::new(payer), trade_config).await;
let mint = Pubkey::new_unique();
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let gas_fee_strategy = GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150_000, 150_000, 500_000, 500_000, 0.001, 0.001);
// In production, fill these fields from your parser/RPC cache. They are
// protocol state, not user preferences.
let pumpfun_params = DexParamEnum::PumpFun(PumpFunParams::from_trade(
Pubkey::new_unique(), // bonding_curve
Pubkey::new_unique(), // associated_bonding_curve
mint,
// WSOL quote_mint selects PumpFun V2 SOL layout. Users still pay with
// native SOL below by setting `TradeTokenType::SOL`.
WSOL_TOKEN_ACCOUNT,
Pubkey::new_unique(), // creator from event/cache
Pubkey::default(), // creator_vault; SDK derives it if unavailable
1_073_000_000_000_000, // virtual_token_reserves
30_000_000_000, // virtual_quote_reserves
793_100_000_000_000, // real_token_reserves
0, // real_quote_reserves
None, // close_token_account_when_sell
global_constants::FEE_RECIPIENT,
// If parser/cache does not know the mint owner, PumpFun now defaults to
// Token-2022. Passing it explicitly makes the example easier to read.
TOKEN_PROGRAM_2022,
false, // is_cashback_coin
Some(false), // mayhem_mode
));
let buy_params = SimpleBuyParams::new(
DexType::PumpFun,
// For PumpFun V2 SOL-paired coins, keep this as SOL even though V2
// account metas use the WSOL mint as quote_mint.
TradeTokenType::SOL,
mint,
// Regular PumpFun/PumpSwap buy. The SDK estimates expected output and
// applies slippage to the maximum quote cost.
BuyAmount::WithMaxInput { quote_amount: 100_000 },
pumpfun_params.clone(),
recent_blockhash,
gas_fee_strategy.clone(),
)
.slippage_basis_points(300)
// Best for bots: do not create/close ATAs in the hot transaction.
// Use AccountPolicy::Auto for normal integrations.
.account_policy(AccountPolicy::HotPathMinimal);
// client.buy_simple(buy_params).await?;
let _ = buy_params;
// Durable nonce is supported by the same high-level params. In production,
// fetch it with `sol_trade_sdk::fetch_nonce_info(...)` immediately before
// building the transaction.
let nonce_buy_params = SimpleBuyParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint,
BuyAmount::WithMaxInput { quote_amount: 100_000 },
pumpfun_params.clone(),
recent_blockhash,
gas_fee_strategy.clone(),
)
.durable_nonce(DurableNonceInfo {
nonce_account: Some(Pubkey::new_unique()),
current_nonce: Some(Hash::new_unique()),
})
.account_policy(AccountPolicy::HotPathMinimal);
let _ = nonce_buy_params;
let sell_params = SimpleSellParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint,
SellAmount::ExactInput(1_000_000),
pumpfun_params.clone(),
client.infrastructure.rpc.get_latest_blockhash().await?,
gas_fee_strategy.clone(),
)
.slippage_basis_points(300)
.account_policy(AccountPolicy::HotPathMinimal);
// client.sell_simple(sell_params).await?;
let _ = sell_params;
let nonce_sell_params = SimpleSellParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint,
SellAmount::ExactInput(1_000_000),
pumpfun_params,
client.infrastructure.rpc.get_latest_blockhash().await?,
gas_fee_strategy,
)
.durable_nonce(DurableNonceInfo {
nonce_account: Some(Pubkey::new_unique()),
current_nonce: Some(Hash::new_unique()),
})
.account_policy(AccountPolicy::HotPathMinimal);
let _ = nonce_sell_params;
println!("Built simple buy/sell params for payer {}", client.payer.pubkey());
Ok(())
}
+13
View File
@@ -0,0 +1,13 @@
# Trading Client Construction
[中文](README_CN.md)
Shows two client-construction patterns: `TradingClient::new` for one wallet and `TradingClient::from_infrastructure` for wallets sharing RPC and SWQoS clients.
This is a configuration template. Set `PRIVATE_KEY`, RPC, and every enabled provider credential; it initializes network clients but does not submit a trade.
```bash
cargo run --package trading_client
```
Initialize the client before subscribing to events. Do not construct it in a trading callback.
+13
View File
@@ -0,0 +1,13 @@
# TradingClient 创建方式
[English](README.md)
展示两种客户端创建模式:单钱包使用 `TradingClient::new`,多钱包通过 `TradingClient::from_infrastructure` 共享 RPC 和 SWQoS 客户端。
这是配置模板。运行前设置 `PRIVATE_KEY`、RPC 和所有已启用服务商凭证;程序会初始化网络客户端,但不会提交交易。
```bash
cargo run --package trading_client
```
客户端应在事件订阅前完成初始化,不要在交易回调中创建。
+2 -3
View File
@@ -13,7 +13,6 @@ use sol_trade_sdk::{
AstralaneTransport, TradingClient, TradingInfrastructure,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use std::sync::Arc;
#[tokio::main]
@@ -35,7 +34,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
///
/// Use this when you have a single wallet or don't need to share infrastructure.
async fn create_trading_client_simple() -> AnyResult<TradingClient> {
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
let commitment = CommitmentConfig::processed();
@@ -73,7 +72,7 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
/// Use this when you have multiple wallets sharing the same configuration.
/// The infrastructure (RPC client, SWQOS clients) is created once and shared.
async fn create_trading_client_from_infrastructure() -> AnyResult<TradingClient> {
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
let commitment = CommitmentConfig::processed();
+13
View File
@@ -0,0 +1,13 @@
# SOL / WSOL Operations
[中文](README_CN.md)
Demonstrates wrapping SOL, partially unwrapping WSOL through the SDK seed account, and closing the WSOL account to recover the remaining SOL.
> All three steps submit real transactions. Set `PRIVATE_KEY` and RPC, use a test wallet first, and reduce amounts as appropriate.
```bash
cargo run --package wsol_wrapper
```
Closing WSOL unwraps the account's full remaining balance and removes the account. Do not run that step when another workflow expects the same WSOL account to remain available.
+13
View File
@@ -0,0 +1,13 @@
# SOL / WSOL 操作
[English](README.md)
展示包装 SOL、通过 SDK seed 账户部分解包 WSOL,以及关闭 WSOL 账户取回剩余 SOL。
> 三个步骤都会提交真实交易。运行前设置 `PRIVATE_KEY` 和 RPC,先使用测试钱包,并按需降低金额。
```bash
cargo run --package wsol_wrapper
```
关闭 WSOL 会解包账户中的全部剩余余额并删除该账户。如果其他流程仍依赖该 WSOL 账户,不要执行关闭步骤。
+1 -2
View File
@@ -1,6 +1,5 @@
use sol_trade_sdk::{common::TradeConfig, swqos::SwqosConfig, SolanaTrade};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use std::sync::Arc;
#[tokio::main]
@@ -80,7 +79,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// Create and initialize SolanaTrade client
async fn create_solana_trade_client() -> Result<SolanaTrade, Box<dyn std::error::Error>> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
+2483
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+742
View File
@@ -288,6 +288,213 @@
}
]
},
{
"name": "boost_buy_and_burn",
"discriminator": [
105,
68,
6,
175,
0,
7,
35,
162
],
"accounts": [
{
"name": "pool"
},
{
"name": "authority",
"writable": true,
"signer": true
},
{
"name": "global_config",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
103,
108,
111,
98,
97,
108,
95,
99,
111,
110,
102,
105,
103
]
}
]
}
},
{
"name": "base_mint",
"writable": true,
"relations": [
"pool"
]
},
{
"name": "quote_mint",
"relations": [
"pool"
]
},
{
"name": "pool_base_token_account",
"writable": true,
"relations": [
"pool"
]
},
{
"name": "pool_quote_token_account",
"writable": true,
"relations": [
"pool"
]
},
{
"name": "boost_vault_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
111,
111,
115,
116,
95,
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "pool"
}
]
}
},
{
"name": "boost_vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "boost_vault_authority"
},
{
"kind": "account",
"path": "quote_token_program"
},
{
"kind": "account",
"path": "quote_mint"
}
],
"program": {
"kind": "const",
"value": [
140,
151,
37,
143,
78,
36,
137,
241,
187,
61,
16,
41,
20,
142,
13,
131,
11,
90,
19,
153,
218,
255,
16,
132,
4,
142,
123,
216,
219,
233,
248,
89
]
}
}
},
{
"name": "base_token_program"
},
{
"name": "quote_token_program"
},
{
"name": "event_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
95,
95,
101,
118,
101,
110,
116,
95,
97,
117,
116,
104,
111,
114,
105,
116,
121
]
}
]
}
},
{
"name": "program"
}
],
"args": [
{
"name": "quote_amount_in",
"type": "u64"
},
{
"name": "min_base_amount_burned",
"type": "u64"
}
]
},
{
"name": "buy",
"docs": [
@@ -2587,6 +2794,208 @@
],
"args": []
},
{
"name": "init_boost",
"discriminator": [
140,
233,
33,
94,
132,
90,
194,
143
],
"accounts": [
{
"name": "pool",
"writable": true
},
{
"name": "global_config",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
103,
108,
111,
98,
97,
108,
95,
99,
111,
110,
102,
105,
103
]
}
]
}
},
{
"name": "creator",
"writable": true,
"signer": true
},
{
"name": "base_mint",
"relations": [
"pool"
]
},
{
"name": "quote_mint",
"relations": [
"pool"
]
},
{
"name": "pool_base_token_account",
"relations": [
"pool"
]
},
{
"name": "pool_quote_token_account",
"writable": true,
"relations": [
"pool"
]
},
{
"name": "boost_vault_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
111,
111,
115,
116,
95,
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "pool"
}
]
}
},
{
"name": "boost_vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "boost_vault_authority"
},
{
"kind": "account",
"path": "quote_token_program"
},
{
"kind": "account",
"path": "quote_mint"
}
],
"program": {
"kind": "const",
"value": [
140,
151,
37,
143,
78,
36,
137,
241,
187,
61,
16,
41,
20,
142,
13,
131,
11,
90,
19,
153,
218,
255,
16,
132,
4,
142,
123,
216,
219,
233,
248,
89
]
}
}
},
{
"name": "quote_token_program"
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
},
{
"name": "associated_token_program",
"address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
},
{
"name": "event_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
95,
95,
101,
118,
101,
110,
116,
95,
97,
117,
116,
104,
111,
114,
105,
116,
121
]
}
]
}
},
{
"name": "program"
}
],
"args": []
},
{
"name": "init_user_volume_accumulator",
"discriminator": [
@@ -3175,6 +3584,72 @@
}
]
},
{
"name": "set_boost_authority",
"discriminator": [
227,
149,
76,
42,
130,
39,
234,
205
],
"accounts": [
{
"name": "admin",
"signer": true,
"relations": [
"global_config"
]
},
{
"name": "global_config",
"writable": true
},
{
"name": "boost_authority"
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
},
{
"name": "event_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
95,
95,
101,
118,
101,
110,
116,
95,
97,
117,
116,
104,
111,
114,
105,
116,
121
]
}
]
}
},
{
"name": "program"
}
],
"args": []
},
{
"name": "set_coin_creator",
"docs": [
@@ -3610,6 +4085,38 @@
],
"args": []
},
{
"name": "toggle_boost",
"discriminator": [
117,
161,
160,
74,
223,
137,
118,
99
],
"accounts": [
{
"name": "admin",
"signer": true,
"relations": [
"global_config"
]
},
{
"name": "global_config",
"writable": true
}
],
"args": [
{
"name": "enabled",
"type": "bool"
}
]
},
{
"name": "toggle_cashback_enabled",
"discriminator": [
@@ -4672,6 +5179,19 @@
222
]
},
{
"name": "BoostBuyAndBurnEvent",
"discriminator": [
63,
69,
28,
22,
48,
92,
194,
185
]
},
{
"name": "BuyEvent",
"discriminator": [
@@ -4802,6 +5322,19 @@
124
]
},
{
"name": "InitBoostEvent",
"discriminator": [
174,
124,
74,
249,
4,
81,
246,
17
]
},
{
"name": "InitUserVolumeAccumulatorEvent",
"discriminator": [
@@ -4867,6 +5400,19 @@
211
]
},
{
"name": "SetBoostAuthorityEvent",
"discriminator": [
89,
128,
240,
141,
91,
202,
71,
105
]
},
{
"name": "SetMetaplexCoinCreatorEvent",
"discriminator": [
@@ -5177,6 +5723,51 @@
{
"code": 6058,
"name": "BuybackFeeRecipientMissing"
},
{
"code": 6059,
"name": "MissingCashbackAccounts",
"msg": "Cashback trade is missing the required remaining accounts"
},
{
"code": 6060,
"name": "InvalidCashbackAccumulator",
"msg": "Cashback user_volume_accumulator account is invalid"
},
{
"code": 6061,
"name": "InvalidCashbackAccumulatorAta",
"msg": "Cashback user_volume_accumulator ATA is missing or invalid"
},
{
"code": 6062,
"name": "InvalidPoolV2",
"msg": "pool_v2 remaining account is missing or invalid"
},
{
"code": 6063,
"name": "InsufficientRealQuoteReserves",
"msg": "BOOST: sell output exceeds the real quote vault. effective = real + virtual is pricing-only; payout is capped at real_vault, so quote min(out, real_vault)"
},
{
"code": 6064,
"name": "BoostPoolLiquidityUnsupported",
"msg": "BOOST: deposit/withdraw don't apply to boost pools"
},
{
"code": 6065,
"name": "PoolCannotBoost",
"msg": "BOOST: pool cannot be boosted (no virtual reserves)"
},
{
"code": 6066,
"name": "BoostDisabled",
"msg": "BOOST: boost is disabled"
},
{
"code": 6067,
"name": "SeedLockViolation",
"msg": "BOOST: lp_supply must never drop below the circulating LP mint supply"
}
],
"types": [
@@ -5292,6 +5883,62 @@
]
}
},
{
"name": "BoostBuyAndBurnEvent",
"type": {
"kind": "struct",
"fields": [
{
"name": "timestamp",
"type": "i64"
},
{
"name": "mint",
"type": "pubkey"
},
{
"name": "bonding_curve",
"type": "pubkey"
},
{
"name": "pool",
"type": "pubkey"
},
{
"name": "authority",
"type": "pubkey"
},
{
"name": "quote_amount_in_requested",
"type": "u64"
},
{
"name": "quote_amount_in_used",
"type": "u64"
},
{
"name": "base_amount_burned",
"type": "u64"
},
{
"name": "virtual_quote_reserves",
"type": "i128"
},
{
"name": "real_quote_reserves_after",
"type": "u64"
},
{
"name": "base_reserves_after",
"type": "u64"
},
{
"name": "boost_vault_remaining",
"type": "u64"
}
]
}
},
{
"name": "BuyEvent",
"docs": [
@@ -5435,6 +6082,18 @@
{
"name": "buyback_fee",
"type": "u64"
},
{
"name": "virtual_quote_reserves",
"type": "i128"
},
{
"name": "can_boost",
"type": "bool"
},
{
"name": "base_supply",
"type": "u64"
}
]
}
@@ -6018,6 +6677,14 @@
{
"name": "buyback_basis_points",
"type": "u64"
},
{
"name": "boost_authority",
"type": "pubkey"
},
{
"name": "boost_enabled",
"type": "bool"
}
]
}
@@ -6064,6 +6731,38 @@
]
}
},
{
"name": "InitBoostEvent",
"type": {
"kind": "struct",
"fields": [
{
"name": "timestamp",
"type": "i64"
},
{
"name": "mint",
"type": "pubkey"
},
{
"name": "bonding_curve",
"type": "pubkey"
},
{
"name": "pool",
"type": "pubkey"
},
{
"name": "virtual_quote_reserves",
"type": "i128"
},
{
"name": "real_quote_reserves_after",
"type": "u64"
}
]
}
},
{
"name": "InitUserVolumeAccumulatorEvent",
"type": {
@@ -6180,6 +6879,13 @@
{
"name": "is_cashback_coin",
"type": "bool"
},
{
"name": "virtual_quote_reserves",
"docs": [
"For non-boost pools, value is 0, so the behavior is identical to legacy pools."
],
"type": "i128"
}
]
}
@@ -6321,6 +7027,18 @@
{
"name": "buyback_fee",
"type": "u64"
},
{
"name": "virtual_quote_reserves",
"type": "i128"
},
{
"name": "can_boost",
"type": "bool"
},
{
"name": "base_supply",
"type": "u64"
}
]
}
@@ -6353,6 +7071,30 @@
]
}
},
{
"name": "SetBoostAuthorityEvent",
"type": {
"kind": "struct",
"fields": [
{
"name": "timestamp",
"type": "i64"
},
{
"name": "admin",
"type": "pubkey"
},
{
"name": "old_boost_authority",
"type": "pubkey"
},
{
"name": "new_boost_authority",
"type": "pubkey"
}
]
}
},
{
"name": "SetMetaplexCoinCreatorEvent",
"type": {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+863 -16
View File
File diff suppressed because it is too large Load Diff
+108 -5
View File
@@ -31,7 +31,7 @@ use solana_sdk::pubkey::Pubkey;
use crate::instruction::utils::pumpfun::global_constants::{
INITIAL_REAL_TOKEN_RESERVES, INITIAL_VIRTUAL_SOL_RESERVES, INITIAL_VIRTUAL_TOKEN_RESERVES,
TOKEN_TOTAL_SUPPLY,
INITIAL_VIRTUAL_USDC_RESERVES, TOKEN_TOTAL_SUPPLY,
};
use crate::instruction::utils::pumpfun::{get_bonding_curve_pda, get_creator_vault_pda};
@@ -62,9 +62,57 @@ pub struct BondingCurveAccount {
pub is_mayhem_mode: bool,
/// Whether this coin has cashback enabled (creator fee redirected to users)
pub is_cashback_coin: bool,
/// Quote mint for V2 curves. Defaults to WSOL for legacy/event payloads that do not expose it.
pub quote_mint: Pubkey,
}
impl BondingCurveAccount {
#[inline]
pub fn normalize_quote_mint(quote_mint: Pubkey) -> Pubkey {
if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT {
crate::constants::WSOL_TOKEN_ACCOUNT
} else {
quote_mint
}
}
#[inline]
pub fn effective_quote_mint(&self) -> Pubkey {
Self::normalize_quote_mint(self.quote_mint)
}
#[inline]
pub fn initial_virtual_quote_reserves_for_quote_mint(quote_mint: &Pubkey) -> u64 {
if *quote_mint == crate::constants::USDC_TOKEN_ACCOUNT {
INITIAL_VIRTUAL_USDC_RESERVES
} else {
INITIAL_VIRTUAL_SOL_RESERVES
}
}
#[inline]
pub fn virtual_quote_reserves(&self) -> u64 {
self.virtual_sol_reserves
}
#[inline]
pub fn real_quote_reserves(&self) -> u64 {
self.real_sol_reserves
}
#[inline]
pub fn with_quote_mint(mut self, quote_mint: Pubkey) -> Self {
let quote_mint = Self::normalize_quote_mint(quote_mint);
let old_initial =
Self::initial_virtual_quote_reserves_for_quote_mint(&self.effective_quote_mint());
let new_initial = Self::initial_virtual_quote_reserves_for_quote_mint(&quote_mint);
if self.virtual_sol_reserves == old_initial.saturating_add(self.real_sol_reserves) {
self.virtual_sol_reserves = new_initial.saturating_add(self.real_sol_reserves);
}
self.quote_mint = quote_mint;
self
}
/// When building from event/parser data (e.g. sol-parser-sdk), pass the token's cashback flag
/// so that sell instructions include the correct remaining accounts. From RPC use `from_mint_by_rpc` instead.
pub fn from_dev_trade(
@@ -75,24 +123,50 @@ impl BondingCurveAccount {
creator: Pubkey,
is_mayhem_mode: bool,
is_cashback_coin: bool,
) -> Self {
Self::from_dev_trade_with_quote_mint(
bonding_curve,
mint,
dev_token_amount,
dev_sol_amount,
creator,
is_mayhem_mode,
is_cashback_coin,
crate::constants::WSOL_TOKEN_ACCOUNT,
)
}
/// Same as [`Self::from_dev_trade`], but quote-aware for V2 pools such as USDC.
pub fn from_dev_trade_with_quote_mint(
bonding_curve: Pubkey,
mint: &Pubkey,
dev_token_amount: u64,
dev_quote_amount: u64,
creator: Pubkey,
is_mayhem_mode: bool,
is_cashback_coin: bool,
quote_mint: Pubkey,
) -> Self {
let account = if bonding_curve != Pubkey::default() {
bonding_curve
} else {
get_bonding_curve_pda(&mint).unwrap()
};
let quote_mint = Self::normalize_quote_mint(quote_mint);
Self {
discriminator: 0,
account: account,
virtual_token_reserves: INITIAL_VIRTUAL_TOKEN_RESERVES - dev_token_amount,
virtual_sol_reserves: INITIAL_VIRTUAL_SOL_RESERVES + dev_sol_amount,
virtual_sol_reserves: Self::initial_virtual_quote_reserves_for_quote_mint(&quote_mint)
+ dev_quote_amount,
real_token_reserves: INITIAL_REAL_TOKEN_RESERVES - dev_token_amount,
real_sol_reserves: dev_sol_amount,
real_sol_reserves: dev_quote_amount,
token_total_supply: TOKEN_TOTAL_SUPPLY,
complete: false,
creator: creator,
is_mayhem_mode: is_mayhem_mode,
is_cashback_coin,
quote_mint,
}
}
@@ -108,24 +182,53 @@ impl BondingCurveAccount {
real_sol_reserves: u64,
is_mayhem_mode: bool,
is_cashback_coin: bool,
) -> Self {
Self::from_trade_with_quote_mint(
bonding_curve,
mint,
creator,
virtual_token_reserves,
virtual_sol_reserves,
real_token_reserves,
real_sol_reserves,
is_mayhem_mode,
is_cashback_coin,
crate::constants::WSOL_TOKEN_ACCOUNT,
)
}
/// Same as [`Self::from_trade`], but carries the V2 quote mint alongside quote reserves.
pub fn from_trade_with_quote_mint(
bonding_curve: Pubkey,
mint: Pubkey,
creator: Pubkey,
virtual_token_reserves: u64,
virtual_quote_reserves: u64,
real_token_reserves: u64,
real_quote_reserves: u64,
is_mayhem_mode: bool,
is_cashback_coin: bool,
quote_mint: Pubkey,
) -> Self {
let account = if bonding_curve != Pubkey::default() {
bonding_curve
} else {
get_bonding_curve_pda(&mint).unwrap()
};
let quote_mint = Self::normalize_quote_mint(quote_mint);
Self {
discriminator: 0,
account: account,
virtual_token_reserves: virtual_token_reserves,
virtual_sol_reserves: virtual_sol_reserves,
virtual_sol_reserves: virtual_quote_reserves,
real_token_reserves: real_token_reserves,
real_sol_reserves: real_sol_reserves,
real_sol_reserves: real_quote_reserves,
token_total_supply: TOKEN_TOTAL_SUPPLY,
complete: false,
creator: creator,
is_mayhem_mode: is_mayhem_mode,
is_cashback_coin,
quote_mint,
}
}

Some files were not shown because too many files have changed in this diff Show More