Compare commits

...

57 Commits

Author SHA1 Message Date
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
0xfnzero 8ed2aef931 Fix trade execution correctness and release 4.0.11 2026-05-21 17:50:06 +08:00
0xfnzero 0cf0064e80 docs: clarify PumpFun V2 configuration 2026-05-21 14:48:43 +08:00
0xfnzero 4968c48a2e Release sol-trade-sdk v4.0.10
Sync Pump program IDLs with the latest definitions.
2026-05-20 19:41:16 +08:00
0xfnzero 5908c84798 Merge remote-tracking branch 'origin/main' into codex/swqos-dual-fee-mode 2026-05-19 06:21:44 +08:00
0xfnzero 6dab47f8f9 Optimize SWQOS dual fee lane submission 2026-05-19 06:21:37 +08:00
0xfnzero a076e0f8cd chore: flatten synced IDL files
Remove the redundant idl/bitquery source snapshot tree and keep synced Bitquery IDLs only under the SDK's canonical local IDL filenames.
2026-05-16 15:03:10 +08:00
0xfnzero 05ac079d6d release: update DEX protocol support for v4.0.9
Sync supported DEX IDLs from bitquery/solana-idl-lib, upgrade protocol builders, and keep trade submission hot paths free of RPC query calls.

Include exact-output coverage, token-account setup fixes for WSOL and stable pairs, and low-latency transaction build improvements.
2026-05-16 06:42:50 +08:00
0xfnzero f6db55c874 release: update Pump program metadata for v4.0.8 2026-05-15 01:08:33 +08:00
0xfnzero 69b4f9bb9f docs: add donation wallet address to README_CN 2026-05-14 19:26:31 +08:00
0xfnzero 532bc1195d docs: add donation wallet address to README 2026-05-14 19:24:53 +08:00
0xfnzero 8944c5455c Refine crate description wording 2026-05-14 00:25:44 +08:00
0xfnzero 0d846930ad fix(pumpswap): use live global fee recipients 2026-05-13 21:16:17 +08:00
0xfnzero 703ae5e4db fix(pumpfun): preserve observed fee recipient 2026-05-12 05:55:45 +08:00
0xfnzero 8e0210af97 fix(swqos): replace non-portable try_from_base58_string with bs58 decode + TryFrom
The patched solana-keypair in the main workspace adds try_from_base58_string,
but sol-safekey and other consumers use the standard crates.io version which
only provides from_base58_string (panics) and TryFrom<&[u8]> (fallible).

Use bs58::decode + Keypair::try_from to keep proper error handling while
remaining compatible with the standard solana-keypair crate.
2026-05-09 15:02:25 +08:00
0xfnzero e605485ecf fix(bonding-curve): tolerate extra trailing bytes in account data
BondingCurveAccount::try_from_slice requires the entire input slice to
be consumed, which fails with "Not all bytes read" when the on-chain
bonding curve account has been extended with new fields (e.g. after a
protocol upgrade). Switch to BorshDeserialize::deserialize which reads
the known fields and silently ignores any trailing bytes.
2026-05-09 14:46:52 +08:00
0xfnzero 8dec2f8d8c fix(sell): trust observed creator_vault directly to prevent Anchor 2006
In build_sell_v1/v2, the creator_vault was previously derived via
effective_creator_for_trade() passed to resolve_creator_vault_for_ix_with_fee_sharing.
When observed_trade_creator differs from bonding_curve.creator (e.g. creator
updated post-deployment), the derived vault PDA mismatches the on-chain
seeds constraint, causing Anchor error 2006 (ConstraintSeeds) on sell.

The pump.fun sell instruction validates creator_vault seeds as
PDA("creator-vault", bonding_curve.creator), but buy does not check seeds,
so buy succeeds while sell fails with 2006.

Fix: when protocol_params.creator_vault is a valid non-default/non-phantom
value (observed from gRPC / update_fee_shares), use it directly without
re-derivation. Only fall back to resolve via bonding_curve.creator when
creator_vault is missing. This ensures the authoritative observed vault
is never overridden by a stale or mismatched creator.
2026-05-09 14:41:22 +08:00
0xfnzero b505bebdac fix(pumpfun): use is_cashback_coin instead of is_mayhem_mode for sell user_volume_accumulator
The Pump.fun IDL explicitly states that user_volume_accumulator is
passed as remaining_accounts[0] for cashback coins during sell.
Using is_mayhem_mode incorrectly omitted the account for cashback
tokens, causing the program to misread bonding_curve_v2 as
user_volume_accumulator and resulting in Custom(6024) Overflow.
2026-05-09 08:18:58 +08:00
0xfnzero 1cc051a874 feat(pumpfun): runtime V2 flag, buyback fee pool fix, legacy ix encoding
- Replace compile-time `pumpfun-v2` feature flag with runtime
  `TradeConfig::use_pumpfun_v2(bool)` for flexible V1/V2 switching
- Fix BUYBACK_FEE_RECIPIENTS pool: replace wrong addresses (was reusing
  standard pool + FEE_CONFIG) with official buyback pool from
  FEE_RECIPIENTS.md
- Fix legacy buy/buy_exact_sol_in ix data encoding: use 2-byte
  Option<bool> (option tag + value) matching official Pump SDK, 26 bytes
  total instead of broken 25
- Add `SwapParams.use_pumpfun_v2` field and wire through TradingClient
  buy/sell paths
- V2 instructions read `quote_mint` from `PumpFunParams` (not
  BondingCurveAccount which lacks the field)
- Fix `fetch_bonding_curve_account` to use BorshDeserialize instead of
  non-existent `decode_from_chain_account_data`
- Update all examples with `use_pumpfun_v2: false` field
- Update README with unified V1/V2 section showing both enabling methods
2026-05-09 07:15:07 +08:00
0xfnzero 0311a0b876 docs(readme): update SWQoS partner list to Jito, Temporal, FlashBlock, BlockRazor, Astralane, SpeedLanding
Remove non-partner services (Nextblock, ZeroSlot, Bloxroute, Node1).
Update code examples to use partner service configs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 00:05:53 +08:00
0xfnzero b49c1c803e fix(pump): restore 26-byte legacy buy instruction data encoding
The refactored ix data encoder was producing 25-byte buy instructions
(1-byte track_volume), but the on-chain Pump program expects Anchor's
2-byte Option<bool> encoding (option tag + value), totaling 26 bytes.
The 1-byte shortfall caused instruction data under-read that manifested
as error 0xbbf (ConstraintOwner) for all legacy buy/buy_exact_sol_in.

Restore the 2-byte encoding: option tag (1u8) followed by the cashback
flag as the bool value, matching the pre-refactoring inline encoder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 23:37:07 +08:00
0xfnzero ce8fb99626 Revert "fix(pump): default token program to legacy SPL Token instead of Token-2022"
This reverts commit 49843d8a20.
2026-05-08 23:28:21 +08:00
0xfnzero 49843d8a20 fix(pump): default token program to legacy SPL Token instead of Token-2022
The `effective_pump_mint_token_program` helper was defaulting to
TOKEN_PROGRAM_2022 when `token_program` was Pubkey::default(), causing
Anchor error 0xbbf (account owned by wrong program) for all legacy
PumpFun coins whose ATAs are owned by the original Token Program.

Fall back to TOKEN_PROGRAM (legacy) when no explicit token program is
provided. Token-2022 must be set explicitly for create_v2 coins.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 23:23:39 +08:00
0xfnzero 60d1db4755 feat(pump): add buy_v2, sell_v2, buy_exact_quote_in_v2 instruction builders
Support Pump.fun Bonding Curve v2 unified trading interface with quote_mint
parameter for SOL-paired (WSOL) and USDC-paired coins.

- Add BUY_V2, SELL_V2, BUY_EXACT_QUOTE_IN_V2 discriminators
- Implement 27/26-account v2 instruction layouts matching official IDL
- Add PumpFunParams::quote_mint and use_v2_ix fields (default legacy)
- Add with_quote_mint() builder, buyback fee recipient pool, and PDA helpers
- Add v2 ix data encoders in pumpfun_ix_data.rs
- Legacy instructions remain default for backward compatibility

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 20:48:11 +08:00
0xfnzero 7a8d2a9c17 docs(readme): bump crates.io examples to v4.0.7
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-06 00:40:09 +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
174 changed files with 81934 additions and 5667 deletions
+7 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.7"
version = "5.0.0"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
@@ -8,7 +8,7 @@ authors = [
"wei <1415121722@qq.com>",
]
repository = "https://github.com/0xfnzero/sol-trade-sdk"
description = "Rust SDK to interact with the dex trade Solana program."
description = "A high-performance Rust SDK for Solana DEX trading."
license = "MIT"
keywords = ["solana", "memecoins", "pumpfun", "pumpswap", "raydium"]
readme = "README.md"
@@ -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"
+216 -59
View File
@@ -39,6 +39,12 @@
<a href="https://discord.gg/vuazbGkqQE">Discord</a>
</p>
> ☕ **Support This Project**
>
> This SDK is completely free and open source. However, maintaining and continuously updating it requires significant AI computing resources and token consumption. If this SDK helps with your trading development, consider making a monthly SOL donation — any amount is appreciated and helps keep this project alive!
>
> **Donation Wallet:** `6oW7AXz1yRb57pYSxysuXnMs2aR1ha5rzGzReZ1MjPV8`
## 📋 Table of Contents
- [✨ Features](#-features)
@@ -53,6 +59,7 @@
- [🔍 Address Lookup Tables](#-address-lookup-tables)
- [🔍 Nonce Cache](#-nonce-cache)
- [💰 Cashback Support (PumpFun / PumpSwap)](#-cashback-support-pumpfun--pumpswap)
- [🔄 PumpFun V1 vs V2 Instructions](#-pumpfun-v1-vs-v2-instructions)
- [🛡️ MEV Protection Services](#-mev-protection-services)
- [📁 Project Structure](#-project-structure)
- [📄 License](#-license)
@@ -72,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` and `sell` operations
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
@@ -101,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.3" }
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.3"
sol-trade-sdk = "4.0.23"
```
## 🛠️ Usage Examples
@@ -130,15 +155,20 @@ let commitment = CommitmentConfig::processed();
let swqos_configs: Vec<SwqosConfig> = vec![
SwqosConfig::Default(rpc_url.clone()),
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt, None),
// 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::Astralane(
"your_astralane_api_key".to_string(),
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(AstralaneTransport::Quic),
), // QUIC
Some(SwqosTransport::Http),
),
];
// Create TradeConfig instance
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
@@ -185,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
@@ -266,7 +337,7 @@ let jito_config = SwqosConfig::Jito(
);
// Using default regional endpoint (third parameter is None)
let bloxroute_config = SwqosConfig::Bloxroute(
let temporal_config = SwqosConfig::Temporal(
"your_api_token".to_string(),
SwqosRegion::NewYork, // Will use the default endpoint for this region
None // No custom URL, uses SwqosRegion
@@ -278,7 +349,29 @@ let bloxroute_config = SwqosConfig::Bloxroute(
- 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)
@@ -330,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.
@@ -350,25 +443,89 @@ 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 Unified Buy/Sell With V1/V2 Instructions
PumpFun has two instruction sets for bonding-curve trading:
| | V1 (default) | V2 (opt-in) |
|---|---|---|
| Instructions | `buy` / `buy_exact_sol_in` / `sell` | `buy_v2` / `buy_exact_quote_in_v2` / `sell_v2` |
| Account metas | 18 | 27 |
| 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) |
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 — 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.
**Pass `quote_mint` into `PumpFunParams::from_trade`**:
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
// 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),
);
```
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.
> **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)
For **PumpSwap** (Pump AMM), `coin_creator_vault_ata` and `coin_creator_vault_authority` are required in buy/sell instructions. Both are available from parsed events without RPC:
- **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 events `coin_creator_vault_ata` and `coin_creator_vault_authority`.
- **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)
- **Jito**: High-performance block space
- **ZeroSlot**: Zero-latency transactions
- **Temporal**: Time-sensitive transactions
- **Bloxroute**: Blockchain network acceleration
- **FlashBlock**: High-speed transaction execution with API key authentication
- **BlockRazor**: High-speed transaction execution with API key authentication
- **Node1**: High-speed transaction execution with API key authentication
- **Astralane**: Blockchain network acceleration (Binary/Plain HTTP and QUIC; see [Astralane](#astralane-binary--plain--quic) above)
- **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
+211 -58
View File
@@ -39,6 +39,12 @@
<a href="https://discord.gg/vuazbGkqQE">Discord</a>
</p>
> ☕ **支持本项目**
>
> 本 SDK 完全免费且开源。但维护和持续更新需要消耗大量 AI 算力与 Token。如果这个 SDK 对您的开发有帮助,欢迎每月捐赠任意数量的 SOL,您的支持将帮助这个项目持续运行!
>
> **捐赠钱包:** `6oW7AXz1yRb57pYSxysuXnMs2aR1ha5rzGzReZ1MjPV8`
## 📋 目录
- [✨ 项目特性](#-项目特性)
@@ -73,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 交易**: 支持`购买``卖出`功能
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、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane 等服务
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、账户或余额
## 📦 安装
@@ -102,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.3" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.23" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = "4.0.3"
sol-trade-sdk = "4.0.23"
```
## 🛠️ 使用示例
@@ -131,15 +155,20 @@ let commitment = CommitmentConfig::processed();
let swqos_configs: Vec<SwqosConfig> = vec![
SwqosConfig::Default(rpc_url.clone()),
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt, None),
// Astralane:第4个参数为 AstralaneTransport — Binary(默认)、Plain/iris)或 Quic
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary /irisb
SwqosConfig::Astralane(
"your_astralane_api_key".to_string(),
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(AstralaneTransport::Quic),
), // QUIC
Some(SwqosTransport::Http),
),
];
// 创建 TradeConfig 实例
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
@@ -185,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 服务配置说明
@@ -266,7 +336,7 @@ let jito_config = SwqosConfig::Jito(
);
// 使用默认区域端点(第三个参数为 None)
let bloxroute_config = SwqosConfig::Bloxroute(
let temporal_config = SwqosConfig::Temporal(
"your_api_token".to_string(),
SwqosRegion::NewYork, // 将使用该区域的默认端点
None // 没有自定义 URL,使用 SwqosRegion
@@ -278,7 +348,29 @@ let bloxroute_config = SwqosConfig::Bloxroute(
- 如果没有提供自定义 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
@@ -330,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(...)` 领取累计的返现。
@@ -357,18 +449,80 @@ 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 统一买卖入口与 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` 参数 — 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 配对币的报价和成本与旧版一致,无额外开销
**使用方式:**
把事件里的 `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
// native SOL 池:可能是 Pubkey::default()、Solscan SOL sentinel 或 WSOL sentinel
// USDC / 非 SOL 池:就是实际 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),
);
// 之后正常交易
client.buy(buy_params).await?;
client.sell(sell_params).await?;
```
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 | 实际使用的指令 | 说明 |
|-----------|---------|------|
| 未设置(默认)/ `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 保护服务
可以通过官网申请密钥:[社区官网](https://fnzero.dev/swqos)
- **Jito**: 高性能区块空间
- **ZeroSlot**: 零延迟交易
- **Temporal**: 时间敏感交易
- **Bloxroute**: 区块链网络加速
- **FlashBlock**: 高速交易执行,支持 API 密钥认证
- **BlockRazor**: 高速交易执行,支持 API 密钥认证
- **Astralane**: 区块链网络加速(Binary/Plain HTTP 与 QUIC
- **SpeedLanding**: 高速交易执行,支持 API 密钥认证
- **Node1**: 高速交易执行,支持 API 密钥认证
- **Astralane**: 区块链网络加速(Binary/Plain HTTP 与 QUIC,见 [Astralane](#astralanebinary--plain--quic)
- **LunarLander**: HelloMoon 交易着陆服务(最低小费:0.001 SOL
## 📁 项目结构
@@ -408,4 +562,3 @@ MIT 许可证
3. 注意滑点设置避免交易失败
4. 监控余额和交易费用
5. 遵循相关法律法规
+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,
+1 -1
View File
@@ -57,7 +57,7 @@ Pump 校验 `creator_vault` 必须满足程序的 **PDAs seeds**(与 bonding c
|------|------|
| **优先事件** | 使用 gRPC / 解析器里的 **`tradeEvent.feeRecipient`** 或同笔 **create_v2 + buy** 观测到的 fee recipient。 |
| **纠偏** | `PumpFunParams::from_trade` 会对 `mayhem_mode``fee_recipient` 做池一致性纠偏;发单前若事件缺省,可走 `pump_fun_fee_recipient_meta`(按 `is_mayhem_mode` 从静态池选)。 |
| **提交前清空** | 若你明确希望与 npm / 官方 SDK 一致的「按池随机」,可在业务层 **`fee_recipient = default`**,由 builder 再走静态池(与 README 中的 Cashback/Mayhem 说明一致)。 |
| **提交前保留观测值** | 不要无条件把已观测的 `fee_recipient` 清成 default;否则会退回 SDK 内置静态池,静态池若落后于主网 Global 授权,仍可能触发 6000。只有缺少观测值时才让 builder 兜底。 |
---
+95 -7
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,13 +119,12 @@ 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 |
| `create_mint_ata` | `bool` | ✅ | Whether to create token mint ATA |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce information containing nonce account and current nonce value |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. If set, this value will be directly assigned to the output amount instead of being calculated (required for Meteora DAMM V2) |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. On exact-out capable DEXes, this uses the exact-out instruction and treats input_token_amount as the max input budget (required for Meteora DAMM V2) |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee strategy instance for controlling transaction fees and priorities |
| `simulate` | `bool` | ✅ | Whether to simulate the transaction instead of executing it. When true, the transaction will be simulated via RPC to validate and show detailed logs, compute units consumed, and potential errors without actually submitting to the blockchain |
@@ -61,13 +150,12 @@ 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 |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce information containing nonce account and current nonce value |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee strategy instance for controlling transaction fees and priorities |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. If set, this value will be directly assigned to the output amount instead of being calculated (required for Meteora DAMM V2) |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. On exact-out capable DEXes, this uses the exact-out instruction and treats input_token_amount as the max input budget (required for Meteora DAMM V2) |
| `simulate` | `bool` | ✅ | Whether to simulate the transaction instead of executing it. When true, the transaction will be simulated via RPC to validate and show detailed logs, compute units consumed, and potential errors without actually submitting to the blockchain |
@@ -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
+95 -7
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,13 +119,12 @@
| 参数 | 类型 | 必需 | 描述 |
|------|------|------|------|
| `address_lookup_table_account` | `Option<Pubkey>` | ❌ | 用于交易优化的地址查找表 |
| `wait_tx_confirmed` | `bool` | ✅ | 是否等待交易确认 |
| `create_input_token_ata` | `bool` | ✅ | 是否创建输入代币关联代币账户 |
| `close_input_token_ata` | `bool` | ✅ | 交易后是否关闭输入代币 ATA |
| `create_mint_ata` | `bool` | ✅ | 是否创建代币 mint ATA |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | 持久 nonce 信息,包含 nonce 账户和当前 nonce 值 |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。如果设置,此值将直接分配给输出数量而不是通过计算得出Meteora DAMM V2 必需) |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。对于支持 exact-out 的 DEX,会使用 exact-out 指令,并将 input_token_amount 作为最大输入预算Meteora DAMM V2 必需) |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee 策略实例,用于控制交易费用和优先级 |
| `simulate` | `bool` | ✅ | 是否模拟交易而不实际执行。当为 true 时,将通过 RPC 模拟交易以验证并显示详细日志、计算单元消耗和潜在错误,而不会实际提交到区块链 |
@@ -61,13 +150,12 @@
| 参数 | 类型 | 必需 | 描述 |
|------|------|------|------|
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 用于交易优化的地址查找表 |
| `wait_tx_confirmed` | `bool` | ✅ | 是否等待交易确认 |
| `create_output_token_ata` | `bool` | ✅ | 是否创建输出代币关联代币账户 |
| `close_output_token_ata` | `bool` | ✅ | 交易后是否关闭输出代币 ATA |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | 持久 nonce 信息,包含 nonce 账户和当前 nonce 值 |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee 策略实例,用于控制交易费用和优先级 |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。如果设置,此值将直接分配给输出数量而不是通过计算得出Meteora DAMM V2 必需) |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。对于支持 exact-out 的 DEX,会使用 exact-out 指令,并将 input_token_amount 作为最大输入预算Meteora DAMM V2 必需) |
| `simulate` | `bool` | ✅ | 是否模拟交易而不实际执行。当为 true 时,将通过 RPC 模拟交易以验证并显示详细日志、计算单元消耗和潜在错误,而不会实际提交到区块链 |
@@ -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)。
+20 -10
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,
wait_transaction_confirmed: true,
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,不能复用触发快照。
+61 -67
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,
wait_transaction_confirmed: true,
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,
wait_transaction_confirmed: true,
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。
+57 -54
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,
wait_transaction_confirmed: true,
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,
wait_transaction_confirmed: true,
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 工具支持以下操作模式:
+119 -122
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,20 +614,21 @@ async fn handle_buy_pumpfun(
slippage_basis_points: slippage,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
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,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from PumpFun!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -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,20 +671,21 @@ async fn handle_buy_pumpswap(
slippage_basis_points: slippage,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
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,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from PumpSwap!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -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,20 +728,21 @@ async fn handle_buy_bonk(
slippage_basis_points: slippage,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
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,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from Bonk!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -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,20 +789,21 @@ 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,
wait_transaction_confirmed: true,
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,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from Raydium V4!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -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,20 +851,21 @@ 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,
wait_transaction_confirmed: true,
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,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from Raydium CPMM!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -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,20 +1022,21 @@ async fn handle_sell_pumpfun(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
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,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from PumpFun!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -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,19 +1082,20 @@ async fn handle_sell_pumpswap(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
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,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from PumpSwap!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -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,19 +1142,20 @@ async fn handle_sell_bonk(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
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,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from Bonk!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -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,19 +1205,20 @@ async fn handle_sell_raydium_v4(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::RaydiumAmmV4(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
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,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from Raydium V4!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -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,19 +1269,20 @@ async fn handle_sell_raydium_cpmm(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::RaydiumCpmm(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
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,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from Raydium CPMM!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -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,
wait_transaction_confirmed: true,
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,
wait_transaction_confirmed: true,
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。
+5 -4
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,
wait_transaction_confirmed: true,
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)。
+18 -6
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,
wait_transaction_confirmed: true,
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。
+45 -41
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,
wait_transaction_confirmed: true,
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,
wait_transaction_confirmed: true,
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)在订阅前完成预热,同时进行签名去重并刷新延迟卖出状态。
+30 -32
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,
wait_transaction_confirmed: true,
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,
wait_transaction_confirmed: true,
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)。
+41 -31
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,
wait_transaction_confirmed: true,
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,
wait_transaction_confirmed: true,
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` 设为零来处理滑点错误。
+425 -137
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,
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,
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_transaction_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_transaction_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,并刷新延迟卖出状态,不能复用触发快照。
+62 -75
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,
@@ -8,22 +7,15 @@ use sol_trade_sdk::{
},
SolanaTrade,
};
use sol_trade_sdk::{
common::TradeConfig, instruction::utils::raydium_amm_v4::fetch_amm_info,
trading::common::get_multi_token_balances, TradeTokenType,
};
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,
@@ -57,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,
@@ -77,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);
}
},
});
});
}
}
}
@@ -105,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())];
@@ -131,29 +119,16 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
let slippage_basis_points = Some(100);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let amm_info = fetch_amm_info(&client.infrastructure.rpc, trade_info.amm).await?;
let (coin_reserve, pc_reserve) = get_multi_token_balances(
&client.infrastructure.rpc,
&amm_info.token_coin,
&amm_info.token_pc,
)
.await?;
let mint_pubkey = if amm_info.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| amm_info.pc_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
let params =
RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, trade_info.amm)
.await?;
let mint_pubkey = if params.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| params.pc_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
{
amm_info.coin_mint
params.coin_mint
} else {
amm_info.pc_mint
params.pc_mint
};
let params = RaydiumAmmV4Params::new(
trade_info.amm,
amm_info.coin_mint,
amm_info.pc_mint,
amm_info.token_coin,
amm_info.token_pc,
coin_reserve,
pc_reserve,
);
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);
@@ -161,8 +136,11 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
// Buy tokens
println!("Buying tokens from Raydium_amm_v4...");
let input_token_amount = 100_000;
let is_wsol = amm_info.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| amm_info.coin_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
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 },
@@ -171,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,
wait_transaction_confirmed: true,
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,
@@ -183,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 =
@@ -210,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,
wait_transaction_confirmed: true,
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,
@@ -224,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 参数映射。
+65 -49
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,
wait_transaction_confirmed: true,
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,
wait_transaction_confirmed: true,
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;生产代码应在卖出前刷新。
+41 -32
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,
wait_transaction_confirmed: true,
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,
wait_transaction_confirmed: true,
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
```
客户端应在事件订阅前完成初始化,不要在交易回调中创建。
+3 -4
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();
@@ -47,7 +46,7 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
SwqosConfig::Temporal("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::FlashBlock("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Node1("your_api_token".to_string(), SwqosRegion::Frankfurt, None, None),
SwqosConfig::BlockRazor("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::BlockRazor("your_api_token".to_string(), SwqosRegion::Frankfurt, None, None),
SwqosConfig::Astralane(
"your_api_token".to_string(),
SwqosRegion::Frankfurt,
@@ -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())];
+16
View File
@@ -0,0 +1,16 @@
# IDL files
Supported DEX IDLs are stored directly in this directory. The files below were
synced from `bitquery/solana-idl-lib` commit `f804b17` and written to the SDK's
canonical local filenames.
| SDK protocol | Local IDL | Source path |
| --- | --- | --- |
| Bonk / Raydium Launchpad | `bonk.json` | `raydium/launchpad.json` |
| Raydium CPMM | `raydium_cpmm.json` | `raydium/raydium_cp.json` |
| Raydium AMM v4 | `raydium_amm_v4.json` | `raydium/raydium_amm.json` |
| Meteora DAMM v2 | `meteora_damm_v2.json` | `meteora/cp_amm_016.json` |
| PumpFun | `pump.json` | `pumpfun/pump.json` |
| PumpSwap | `pump_amm.json` | `pumpswap/amm.json` |
No separate source snapshot tree is kept in the repository.
+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
File diff suppressed because it is too large Load Diff
+2611 -2896
View File
File diff suppressed because it is too large Load Diff
+1122
View File
File diff suppressed because it is too large Load Diff
+4612 -249
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

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