feat: harden trading SDK examples
This commit is contained in:
@@ -281,27 +281,33 @@ Optional builder methods:
|
||||
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 |
|
||||
|-------------|-------------|-------------|
|
||||
| Simple buy/sell parameter API | `cargo run --package simple_trading` | [examples/simple_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/simple_trading/src/main.rs) |
|
||||
| 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
|
||||
|
||||
|
||||
+24
-18
@@ -280,27 +280,33 @@ client.buy_simple(buy_params).await?;
|
||||
当你使用 shred 订阅事件时,由于 shred 的特性,你无法获取到交易事件的完整信息。
|
||||
请你在使用时,确保你的交易逻辑依赖的参数,在shred中都能获取到。
|
||||
|
||||
客户端预热、blockhash/nonce、交易模式、事件状态新鲜度和 6040 重报价规则见 [低延迟 Bot 集成清单](docs/LOW_LATENCY_BOTS_CN.md)。
|
||||
|
||||
### 📊 使用示例汇总表格
|
||||
|
||||
| 描述 | 运行命令 | 源码路径 |
|
||||
完整双语索引和安全分类见 [`examples/README_CN.md`](examples/README_CN.md)。
|
||||
|
||||
| 描述 | 运行命令 | 使用说明 |
|
||||
|------|---------|----------|
|
||||
| 简化买卖参数 API | `cargo run --package simple_trading` | [examples/simple_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/simple_trading/src/main.rs) |
|
||||
| 创建和配置 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 服务配置说明
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
Reference examples:
|
||||
|
||||
- `fnzero-examples/pumpfun_grpc_sniper`
|
||||
- `fnzero-examples/pumpfun_shredstream_sniper`
|
||||
- `examples/pumpswap_trading`
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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 时才使用低层参数。
|
||||
|
||||
每个示例目录都提供内容对应的英文和中文文档。
|
||||
@@ -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).
|
||||
@@ -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)。
|
||||
@@ -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())];
|
||||
|
||||
@@ -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.
|
||||
@@ -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,不能复用触发快照。
|
||||
@@ -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,8 +15,6 @@ 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;
|
||||
@@ -96,7 +92,7 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
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);
|
||||
std::process::exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -109,7 +105,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 +143,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,
|
||||
@@ -182,22 +181,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 {
|
||||
@@ -206,21 +214,8 @@ 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,
|
||||
)),
|
||||
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,
|
||||
@@ -234,7 +229,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);
|
||||
|
||||
@@ -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.
|
||||
@@ -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。
|
||||
@@ -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,8 +9,6 @@ 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;
|
||||
@@ -64,7 +61,7 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
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);
|
||||
std::process::exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -77,7 +74,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 +110,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...");
|
||||
@@ -150,22 +150,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 {
|
||||
@@ -174,7 +177,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,
|
||||
@@ -195,7 +198,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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# SOL Trade CLI
|
||||
|
||||
[English](README.md)
|
||||
|
||||
一个用于在 Solana 上交易代币的命令行工具,支持多种 DEX 交易和钱包管理功能。
|
||||
|
||||
> **真实交易警告:**运行前设置 `PRIVATE_KEY`(base58 或 64 字节 JSON 数组)和 `RPC_URL`。CLI 不再创建临时交易钱包;下列命令可能提交真实主网交易。
|
||||
|
||||
## 功能
|
||||
|
||||
此 CLI 工具支持以下操作模式:
|
||||
|
||||
@@ -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)?;
|
||||
@@ -631,10 +619,10 @@ async fn handle_buy_pumpfun(
|
||||
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,
|
||||
@@ -664,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?;
|
||||
@@ -688,10 +676,10 @@ async fn handle_buy_pumpswap(
|
||||
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,
|
||||
@@ -720,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 =
|
||||
@@ -745,10 +733,10 @@ async fn handle_buy_bonk(
|
||||
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,
|
||||
@@ -779,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)?;
|
||||
@@ -806,10 +794,10 @@ async fn handle_buy_raydium_v4(
|
||||
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,
|
||||
@@ -840,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)?;
|
||||
@@ -868,10 +856,10 @@ async fn handle_buy_raydium_cpmm(
|
||||
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,
|
||||
@@ -1008,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?;
|
||||
@@ -1042,7 +1030,7 @@ async fn handle_sell_pumpfun(
|
||||
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,
|
||||
};
|
||||
@@ -1070,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)?;
|
||||
@@ -1102,7 +1090,7 @@ async fn handle_sell_pumpswap(
|
||||
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,
|
||||
};
|
||||
@@ -1129,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)?;
|
||||
@@ -1162,7 +1150,7 @@ async fn handle_sell_bonk(
|
||||
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,
|
||||
};
|
||||
@@ -1190,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)?;
|
||||
@@ -1225,7 +1213,7 @@ async fn handle_sell_raydium_v4(
|
||||
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,
|
||||
};
|
||||
@@ -1253,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)?;
|
||||
@@ -1289,7 +1277,7 @@ async fn handle_sell_raydium_cpmm(
|
||||
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,
|
||||
};
|
||||
@@ -1396,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)
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Gas Fee 策略
|
||||
|
||||
[English](README.md)
|
||||
|
||||
本地演示全局、按提交通道和高低两档 `GasFeeStrategy` 配置,并展示策略覆盖、删除和查看。
|
||||
|
||||
```bash
|
||||
cargo run --package gas_fee_strategy
|
||||
```
|
||||
|
||||
示例不会连接 Solana,也不会提交交易。代码中的费用仅用于说明,生产值应根据实时网络情况和明确成本上限设置。
|
||||
@@ -10,3 +10,4 @@ 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,10 +47,7 @@ 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?,
|
||||
),
|
||||
extension_params: DexParamEnum::MeteoraDammV2(pool_params),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
@@ -48,36 +55,36 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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)
|
||||
@@ -90,23 +97,36 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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。
|
||||
@@ -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())];
|
||||
|
||||
@@ -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).
|
||||
@@ -0,0 +1,17 @@
|
||||
# Durable Nonce
|
||||
|
||||
[English](README.md)
|
||||
|
||||
展示如何获取 durable nonce,并附加到由 `sol-parser-sdk` gRPC 事件触发的 PumpFun 交易。
|
||||
|
||||
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`,替换 `use_your_nonce_account_here`,并配置 RPC 和 gRPC;nonce 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)。
|
||||
@@ -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())];
|
||||
|
||||
@@ -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。
|
||||
@@ -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();
|
||||
@@ -140,6 +136,8 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
|
||||
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;
|
||||
@@ -180,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,
|
||||
@@ -200,25 +204,9 @@ 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.quote_mint,
|
||||
e.creator,
|
||||
e.creator_vault,
|
||||
e.virtual_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
e.real_token_reserves,
|
||||
real_quote_reserves,
|
||||
Some(true),
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
extension_params: DexParamEnum::PumpFun(sell_extension),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
@@ -231,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(())
|
||||
|
||||
@@ -9,3 +9,4 @@ sol-parser-sdk = "0.4.14"
|
||||
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 @@
|
||||
# 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)在订阅前完成预热,同时进行签名去重并刷新延迟卖出状态。
|
||||
@@ -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();
|
||||
@@ -118,15 +115,10 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
async fn pumpfun_sniper_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(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);
|
||||
@@ -169,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,
|
||||
@@ -183,25 +184,9 @@ 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.quote_mint,
|
||||
e.creator,
|
||||
e.creator_vault,
|
||||
e.virtual_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
e.real_token_reserves,
|
||||
real_quote_reserves,
|
||||
Some(true),
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
extension_params: DexParamEnum::PumpFun(sell_extension),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
@@ -214,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(())
|
||||
|
||||
@@ -9,3 +9,4 @@ solana-streamer-sdk = "0.5.0"
|
||||
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)。
|
||||
@@ -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,9 +45,7 @@ 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?,
|
||||
),
|
||||
extension_params: DexParamEnum::PumpSwap(pool_params),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
@@ -53,29 +59,29 @@ 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?,
|
||||
@@ -92,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(())
|
||||
}
|
||||
|
||||
@@ -102,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)
|
||||
|
||||
@@ -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
|
||||
@@ -9,4 +9,5 @@ solana-streamer-sdk = "0.5.0"
|
||||
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"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# 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 example records the pre-buy balance and sells only the confirmed balance increase. It 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.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,25 @@
|
||||
# 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。
|
||||
- 示例记录买前余额,只卖出确认后的余额增量;卖出前重新获取池状态和 blockhash。
|
||||
- 若业务必须精确花费 quote,应改用 `BuyAmount::ExactInput`。这会启用最小输出保护,在活跃池中更容易因状态变化而失败。
|
||||
|
||||
生产机器人还应增加持久化签名去重、持仓状态机、SWQoS 配置和有限次数的重新报价。不要通过把 `min_out` 设为零来处理滑点错误。
|
||||
@@ -1,5 +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::common::{clock::now_micros, SolanaRpcClient, TradeConfig};
|
||||
use sol_trade_sdk::TradeTokenType;
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
@@ -8,11 +7,10 @@ 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::{
|
||||
common::filter::EventTypeFilter, protocols::pumpswap::PumpSwapBuyEvent,
|
||||
};
|
||||
@@ -25,24 +23,132 @@ 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,
|
||||
};
|
||||
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)]
|
||||
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 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, selection);
|
||||
let protocols = vec![Protocol::PumpSwap];
|
||||
// Filter accounts
|
||||
let account_include = vec![
|
||||
@@ -83,8 +189,12 @@ 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>| {
|
||||
fn create_event_callback(
|
||||
client: Arc<SolanaTrade>,
|
||||
blockhash_cache: BlockhashCache,
|
||||
selection: EventSelection,
|
||||
) -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
move |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;
|
||||
@@ -92,13 +202,22 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
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.swap(true, Ordering::SeqCst) {
|
||||
let event_clone = e.clone();
|
||||
let client = client.clone();
|
||||
let blockhash_cache = blockhash_cache.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = pumpswap_trade_with_grpc_buy_event(event_clone).await {
|
||||
if let Err(err) = pumpswap_trade_with_grpc_buy_event(
|
||||
client,
|
||||
blockhash_cache,
|
||||
event_clone,
|
||||
).await {
|
||||
eprintln!("Error in trade: {:?}", err);
|
||||
std::process::exit(0);
|
||||
std::process::exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -109,13 +228,22 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
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.swap(true, Ordering::SeqCst) {
|
||||
let event_clone = e.clone();
|
||||
let client = client.clone();
|
||||
let blockhash_cache = blockhash_cache.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = pumpswap_trade_with_grpc_sell_event(event_clone).await {
|
||||
if let Err(err) = pumpswap_trade_with_grpc_sell_event(
|
||||
client,
|
||||
blockhash_cache,
|
||||
event_clone,
|
||||
).await {
|
||||
eprintln!("Error in trade: {:?}", err);
|
||||
std::process::exit(0);
|
||||
std::process::exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -127,9 +255,10 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
/// 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)
|
||||
@@ -141,12 +270,15 @@ 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?;
|
||||
async fn pumpswap_trade_with_grpc_buy_event(
|
||||
client: Arc<SolanaTrade>,
|
||||
blockhash_cache: BlockhashCache,
|
||||
trade_info: PumpSwapBuyEvent,
|
||||
) -> AnyResult<()> {
|
||||
let params = PumpSwapParams::from_trade_with_fee_basis_points(
|
||||
trade_info.pool,
|
||||
trade_info.base_mint,
|
||||
@@ -175,12 +307,16 @@ 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?;
|
||||
pumpswap_trade_with_grpc(&client, &blockhash_cache, trade_info.metadata.recv_us, mint, params)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> AnyResult<()> {
|
||||
let client = create_solana_trade_client().await?;
|
||||
async fn pumpswap_trade_with_grpc_sell_event(
|
||||
client: Arc<SolanaTrade>,
|
||||
blockhash_cache: BlockhashCache,
|
||||
trade_info: PumpSwapSellEvent,
|
||||
) -> AnyResult<()> {
|
||||
let params = PumpSwapParams::from_trade_with_fee_basis_points(
|
||||
trade_info.pool,
|
||||
trade_info.base_mint,
|
||||
@@ -209,92 +345,106 @@ 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?;
|
||||
pumpswap_trade_with_grpc(&client, &blockhash_cache, trade_info.metadata.recv_us, mint, params)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn pumpswap_trade_with_grpc(
|
||||
client: &SolanaTrade,
|
||||
blockhash_cache: &BlockhashCache,
|
||||
grpc_recv_us: i64,
|
||||
mint_pubkey: Pubkey,
|
||||
params: PumpSwapParams,
|
||||
) -> AnyResult<()> {
|
||||
println!("Testing PumpSwap trading...");
|
||||
let slippage_basis_points = Some(500);
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
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);
|
||||
|
||||
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 balance_before =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &program_id).await?;
|
||||
|
||||
// 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_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
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_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
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, ¶ms.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);
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,并刷新延迟卖出状态,不能复用触发快照。
|
||||
@@ -1,4 +1,3 @@
|
||||
use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed;
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
swqos::SwqosConfig,
|
||||
@@ -10,8 +9,6 @@ use sol_trade_sdk::{
|
||||
};
|
||||
use sol_trade_sdk::{common::TradeConfig, TradeTokenType};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::EventType;
|
||||
use solana_streamer_sdk::streaming::event_parser::protocols::raydium_amm_v4::parser::RAYDIUM_AMM_V4_PROGRAM_ID;
|
||||
@@ -89,7 +86,7 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
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);
|
||||
std::process::exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -102,7 +99,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 +144,9 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
let input_token_amount = 100_000;
|
||||
let is_wsol = params.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| params.coin_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let mint_token_program = client.infrastructure.rpc.get_account(&mint_pubkey).await?.owner;
|
||||
let balance_before =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &mint_token_program).await?;
|
||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||
dex_type: DexType::RaydiumAmmV4,
|
||||
input_token_type: if is_wsol { TradeTokenType::WSOL } else { TradeTokenType::USDC },
|
||||
@@ -168,22 +168,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 =
|
||||
@@ -195,7 +197,7 @@ 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_accounts: Vec::new(),
|
||||
@@ -210,7 +212,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);
|
||||
|
||||
@@ -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 参数映射。
|
||||
@@ -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,8 +5,6 @@ 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;
|
||||
@@ -90,7 +87,7 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
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);
|
||||
std::process::exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -103,7 +100,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 +144,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...");
|
||||
@@ -172,17 +178,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,
|
||||
@@ -197,7 +210,7 @@ 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_accounts: Vec::new(),
|
||||
@@ -212,7 +225,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);
|
||||
|
||||
@@ -8,4 +8,5 @@ sol-trade-sdk = { path = "../.." }
|
||||
solana-streamer-sdk = "0.5.0"
|
||||
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"
|
||||
|
||||
@@ -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.
|
||||
@@ -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;生产代码应在卖出前刷新。
|
||||
@@ -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,9 +45,7 @@ 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?,
|
||||
),
|
||||
extension_params: DexParamEnum::PumpSwap(pool_params),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
@@ -53,32 +59,31 @@ 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?,
|
||||
@@ -95,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(())
|
||||
}
|
||||
|
||||
@@ -105,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)
|
||||
|
||||
@@ -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 和提交通道时才共享基础设施;签名权限和持仓状态仍必须按钱包隔离。
|
||||
@@ -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).
|
||||
@@ -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)。
|
||||
@@ -18,11 +18,11 @@ use sol_trade_sdk::{
|
||||
SolanaTrade, TradeTokenType,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::{hash::Hash, pubkey::Pubkey, signature::Keypair, signer::Signer};
|
||||
use solana_sdk::{hash::Hash, pubkey::Pubkey, signer::Signer};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::processed();
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,13 @@
|
||||
# TradingClient 创建方式
|
||||
|
||||
[English](README.md)
|
||||
|
||||
展示两种客户端创建模式:单钱包使用 `TradingClient::new`,多钱包通过 `TradingClient::from_infrastructure` 共享 RPC 和 SWQoS 客户端。
|
||||
|
||||
这是配置模板。运行前设置 `PRIVATE_KEY`、RPC 和所有已启用服务商凭证;程序会初始化网络客户端,但不会提交交易。
|
||||
|
||||
```bash
|
||||
cargo run --package trading_client
|
||||
```
|
||||
|
||||
客户端应在事件订阅前完成初始化,不要在交易回调中创建。
|
||||
@@ -13,7 +13,6 @@ use sol_trade_sdk::{
|
||||
AstralaneTransport, TradingClient, TradingInfrastructure,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::main]
|
||||
@@ -35,7 +34,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
///
|
||||
/// Use this when you have a single wallet or don't need to share infrastructure.
|
||||
async fn create_trading_client_simple() -> AnyResult<TradingClient> {
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let commitment = CommitmentConfig::processed();
|
||||
|
||||
@@ -73,7 +72,7 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
|
||||
/// Use this when you have multiple wallets sharing the same configuration.
|
||||
/// The infrastructure (RPC client, SWQOS clients) is created once and shared.
|
||||
async fn create_trading_client_from_infrastructure() -> AnyResult<TradingClient> {
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let commitment = CommitmentConfig::processed();
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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,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())];
|
||||
|
||||
@@ -1348,6 +1348,12 @@ impl TradingClient {
|
||||
(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
|
||||
anyhow::Error,
|
||||
> {
|
||||
validate_trade_safety(
|
||||
"buy",
|
||||
params.input_token_amount,
|
||||
params.fixed_output_token_amount,
|
||||
params.slippage_basis_points,
|
||||
)?;
|
||||
if params.recent_blockhash.is_none() && params.durable_nonce.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Must provide either recent_blockhash or durable_nonce for buy (required for transaction validity)"
|
||||
@@ -1476,6 +1482,12 @@ impl TradingClient {
|
||||
(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
|
||||
anyhow::Error,
|
||||
> {
|
||||
validate_trade_safety(
|
||||
"sell",
|
||||
params.input_token_amount,
|
||||
params.fixed_output_token_amount,
|
||||
params.slippage_basis_points,
|
||||
)?;
|
||||
#[cfg(feature = "perf-trace")]
|
||||
if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() {
|
||||
debug!(
|
||||
@@ -1820,6 +1832,30 @@ impl TradingClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_trade_safety(
|
||||
side: &str,
|
||||
input_amount: u64,
|
||||
fixed_output_amount: Option<u64>,
|
||||
slippage_basis_points: Option<u64>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
if input_amount == 0 {
|
||||
return Err(anyhow::anyhow!("{} input amount must be greater than zero", side));
|
||||
}
|
||||
if fixed_output_amount == Some(0) {
|
||||
return Err(anyhow::anyhow!("{} fixed output amount must be greater than zero", side));
|
||||
}
|
||||
if let Some(bps) = slippage_basis_points {
|
||||
if bps >= 10_000 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"{} slippage_basis_points must be below 10000, got {}",
|
||||
side,
|
||||
bps
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1841,6 +1877,20 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trade_safety_rejects_zero_amounts_and_unbounded_slippage() {
|
||||
assert!(validate_trade_safety("buy", 0, None, Some(100)).is_err());
|
||||
assert!(validate_trade_safety("buy", 1, Some(0), Some(100)).is_err());
|
||||
assert!(validate_trade_safety("sell", 1, None, Some(10_000)).is_err());
|
||||
assert!(validate_trade_safety("sell", 1, None, Some(u64::MAX)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trade_safety_accepts_bounded_values() {
|
||||
assert!(validate_trade_safety("buy", 1, None, None).is_ok());
|
||||
assert!(validate_trade_safety("buy", 1, Some(1), Some(9_999)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_swqos_configs_adds_default_rpc_route() {
|
||||
let configs = vec![SwqosConfig::Jito("uuid".to_string(), SwqosRegion::Frankfurt, None)];
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
use anyhow::{Context, Result};
|
||||
use solana_sdk::signature::Keypair;
|
||||
|
||||
/// Load a Solana keypair from a base58 string or a 64-byte JSON array in an environment variable.
|
||||
pub fn load_keypair_from_env(name: &str) -> Result<Keypair> {
|
||||
let value = std::env::var(name).with_context(|| format!("{} is required", name))?;
|
||||
load_keypair_from_string(&value).with_context(|| format!("invalid {}", name))
|
||||
}
|
||||
|
||||
/// Parse a Solana keypair without the panic behavior of `Keypair::from_base58_string`.
|
||||
pub fn load_keypair_from_string(value: &str) -> Result<Keypair> {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
anyhow::bail!("keypair is empty");
|
||||
}
|
||||
|
||||
let bytes = if value.starts_with('[') {
|
||||
serde_json::from_str::<Vec<u8>>(value).context("keypair JSON must be a byte array")?
|
||||
} else {
|
||||
bs58::decode(value)
|
||||
.into_vec()
|
||||
.context("keypair must be valid base58 or a 64-byte JSON array")?
|
||||
};
|
||||
if bytes.len() != 64 {
|
||||
anyhow::bail!("keypair must contain 64 bytes, got {}", bytes.len());
|
||||
}
|
||||
Keypair::try_from(bytes.as_slice()).context("invalid 64-byte Solana keypair")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solana_sdk::signature::Signer;
|
||||
|
||||
#[test]
|
||||
fn parses_base58_and_json_keypairs() {
|
||||
let keypair = Keypair::new();
|
||||
let base58 = bs58::encode(keypair.to_bytes()).into_string();
|
||||
let json = serde_json::to_string(&keypair.to_bytes().to_vec()).unwrap();
|
||||
|
||||
assert_eq!(load_keypair_from_string(&base58).unwrap().pubkey(), keypair.pubkey());
|
||||
assert_eq!(load_keypair_from_string(&json).unwrap().pubkey(), keypair.pubkey());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_placeholders_and_wrong_lengths() {
|
||||
assert!(load_keypair_from_string("use_your_payer_keypair_here").is_err());
|
||||
assert!(load_keypair_from_string("[1,2,3]").is_err());
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod fast_fn;
|
||||
pub mod fast_timing;
|
||||
pub mod gas_fee_strategy;
|
||||
pub mod global;
|
||||
pub mod keypair;
|
||||
pub mod nonce_cache;
|
||||
pub mod sdk_log;
|
||||
pub mod seed;
|
||||
|
||||
Reference in New Issue
Block a user