Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75bfbf6d54 | |||
| 184f1cc583 | |||
| 55c13e2f25 | |||
| fda211ea87 | |||
| 85d2c602f3 | |||
| 02d939b3cf | |||
| e8ec9103ab | |||
| 0fe54f0e94 | |||
| 7ac07247a3 | |||
| 06ed710869 | |||
| d711346c55 | |||
| 06ef2fed84 |
+4
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sol-trade-sdk"
|
||||
version = "4.0.3"
|
||||
version = "4.0.7"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"William <byteblock6@gmail.com>",
|
||||
@@ -37,7 +37,7 @@ members = [
|
||||
]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
@@ -109,7 +109,8 @@ parking_lot = "0.12"
|
||||
arc-swap = "1.7"
|
||||
sha2 = "0.10"
|
||||
tonic-prost = "0.14.2"
|
||||
quinn = { version = "0.11", default-features = false, features = ["rustls"] }
|
||||
# 须含 runtime-tokio,否则 quinn::Endpoint::client 报 no async runtime found,QUIC(Speedlanding/Soyas)无法初始化
|
||||
quinn = { version = "0.11", default-features = false, features = ["rustls", "runtime-tokio"] }
|
||||
rcgen = "0.13"
|
||||
uuid = "1.11"
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
- [⚡ Trading Parameters](#-trading-parameters)
|
||||
- [📊 Usage Examples Summary Table](#-usage-examples-summary-table)
|
||||
- [⚙️ SWQoS Service Configuration](#️-swqos-service-configuration)
|
||||
- [Astralane QUIC (Low-Latency)](#astralane-quic-low-latency)
|
||||
- [Astralane (Binary / Plain / QUIC)](#astralane-binary--plain--quic)
|
||||
- [🔧 Middleware System](#-middleware-system)
|
||||
- [🔍 Address Lookup Tables](#-address-lookup-tables)
|
||||
- [🔍 Nonce Cache](#-nonce-cache)
|
||||
@@ -131,13 +131,13 @@ let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
// Astralane: HTTP (4th param None) or QUIC (Some(SwqosTransport::Quic)); same API key
|
||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // HTTP
|
||||
// Astralane: 4th param = AstralaneTransport — Binary (default), Plain (/iris), or Quic
|
||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary HTTP /irisb
|
||||
SwqosConfig::Astralane(
|
||||
"your_astralane_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Quic),
|
||||
Some(AstralaneTransport::Quic),
|
||||
), // QUIC
|
||||
];
|
||||
// Create TradeConfig instance
|
||||
@@ -147,7 +147,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .log_enabled(true) // default: true - SDK timing / SWQOS logs
|
||||
// .check_min_tip(false) // default: false - filter SWQOS below min tip
|
||||
// .swqos_cores_from_end(false) // default: false - bind SWQOS to last N CPU cores
|
||||
// .mev_protection(false) // default: false - MEV protection (Astralane port 9000 / BlockRazor sandwichMitigation)
|
||||
// .mev_protection(false) // default: false - MEV (Astralane QUIC :9000 or HTTP mev-protect / BlockRazor)
|
||||
.build();
|
||||
|
||||
// Create TradingClient
|
||||
@@ -280,28 +280,28 @@ let bloxroute_config = SwqosConfig::Bloxroute(
|
||||
|
||||
When using multiple MEV services, you need to use `Durable Nonce`. You need to use the `fetch_nonce_info` function to get the latest `nonce` value, and use it as the `durable_nonce` when trading.
|
||||
|
||||
#### Astralane QUIC (Low-Latency)
|
||||
#### Astralane (Binary / Plain HTTP / QUIC)
|
||||
|
||||
Astralane supports both HTTP and **QUIC** transport. QUIC reduces connection overhead and can lower submission latency. To use the QUIC channel, pass `Some(SwqosTransport::Quic)` as the fourth parameter of `SwqosConfig::Astralane`. Astralane’s QUIC service uses a **single endpoint** (no per-region endpoints); the SDK ignores the `region` (and optional custom URL) when QUIC is selected. You can pass the same region as your other SWQoS configs for consistency.
|
||||
Astralane supports **Binary** HTTP (`/irisb`), **Plain** HTTP (`/iris`), and **QUIC** (`host:7000`, or `:9000` when global `mev_protection` is true). Pass `Some(AstralaneTransport::Plain)`, `Some(AstralaneTransport::Quic)`, or use `None` / omit for **Binary** (default). Global `mev_protection` adds `mev-protect=true` on HTTP or selects QUIC port 9000.
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{SwqosConfig, SwqosRegion, SwqosTransport};
|
||||
use sol_trade_sdk::{SwqosConfig, SwqosRegion, AstralaneTransport};
|
||||
|
||||
// Astralane over QUIC (low-latency); region is ignored (single QUIC endpoint)
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Astralane(
|
||||
"your_astralane_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt, // same as other services; ignored for QUIC
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Quic),
|
||||
Some(AstralaneTransport::Quic),
|
||||
),
|
||||
];
|
||||
// Then create TradeConfig / TradingClient as usual with swqos_configs
|
||||
```
|
||||
|
||||
- **HTTP** (default): use `None` or `Some(SwqosTransport::Http)`; region and optional custom URL apply.
|
||||
- **QUIC**: use `Some(SwqosTransport::Quic)`; the SDK uses a single QUIC endpoint and ignores region. Same API key as HTTP.
|
||||
- **Binary** (default): `None` or `Some(AstralaneTransport::Binary)` — `/irisb`, bincode body.
|
||||
- **Plain**: `Some(AstralaneTransport::Plain)` — `/iris`.
|
||||
- **QUIC**: `Some(AstralaneTransport::Quic)` — regional `host:7000` / `:9000` (MEV); same API key.
|
||||
|
||||
---
|
||||
|
||||
@@ -335,6 +335,10 @@ PumpFun and PumpSwap support **cashback** for eligible tokens: part of the tradi
|
||||
- The **pumpfun_copy_trading** and **pumpfun_sniper_trading** examples use sol-parser-sdk for gRPC subscription and pass `e.is_cashback_coin` when building params.
|
||||
- **Claim**: Use `client.claim_cashback_pumpfun()` and `client.claim_cashback_pumpswap(...)` to claim accumulated cashback.
|
||||
|
||||
#### PumpFun: troubleshooting (on-chain errors)
|
||||
|
||||
For **Anchor 2006 / `NotAuthorized` (6000) / wrong token program / BuyZeroAmount (6020) / slippage (6042)** and related issues, see **[docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)** (Chinese). An English appendix may be added later.
|
||||
|
||||
#### PumpFun: Creator Rewards Sharing (creator_vault)
|
||||
|
||||
Some PumpFun coins use **Creator Rewards Sharing**, so the on-chain `creator_vault` can differ from the default derivation. If you reuse cached params from a **buy** when **selling**, you may see program error **2006 (seeds constraint violated)**. To avoid this:
|
||||
@@ -364,7 +368,7 @@ You can apply for a key through the official website: [Community Website](https:
|
||||
- **FlashBlock**: High-speed transaction execution with API key authentication
|
||||
- **BlockRazor**: High-speed transaction execution with API key authentication
|
||||
- **Node1**: High-speed transaction execution with API key authentication
|
||||
- **Astralane**: Blockchain network acceleration (supports HTTP and QUIC; see [Astralane QUIC](#astralane-quic-low-latency) above)
|
||||
- **Astralane**: Blockchain network acceleration (Binary/Plain HTTP and QUIC; see [Astralane](#astralane-binary--plain--quic) above)
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
|
||||
+19
-14
@@ -48,11 +48,12 @@
|
||||
- [⚡ 交易参数](#-交易参数)
|
||||
- [📊 使用示例汇总表格](#-使用示例汇总表格)
|
||||
- [⚙️ SWQoS 服务配置说明](#️-swqos-服务配置说明)
|
||||
- [Astralane QUIC(低延迟)](#astralane-quic低延迟)
|
||||
- [Astralane(Binary / Plain / QUIC)](#astralanebinary--plain--quic)
|
||||
- [🔧 中间件系统说明](#-中间件系统说明)
|
||||
- [🔍 地址查找表](#-地址查找表)
|
||||
- [🔍 Nonce 缓存](#-nonce-缓存)
|
||||
- [💰 Cashback 支持(PumpFun / PumpSwap)](#-cashback-支持pumpfun--pumpswap)
|
||||
- [Pump.fun 常见链上错误与排错(文档)](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)
|
||||
- [🛡️ MEV 保护服务](#️-mev-保护服务)
|
||||
- [📁 项目结构](#-项目结构)
|
||||
- [📄 许可证](#-许可证)
|
||||
@@ -131,13 +132,13 @@ let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
// Astralane:第4个参数 None 为 HTTP,Some(SwqosTransport::Quic) 为 QUIC;同一 API key
|
||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // HTTP
|
||||
// Astralane:第4个参数为 AstralaneTransport — Binary(默认)、Plain(/iris)或 Quic
|
||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary /irisb
|
||||
SwqosConfig::Astralane(
|
||||
"your_astralane_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Quic),
|
||||
Some(AstralaneTransport::Quic),
|
||||
), // QUIC
|
||||
];
|
||||
// 创建 TradeConfig 实例
|
||||
@@ -147,7 +148,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .log_enabled(true) // 默认: true - SDK 计时 / SWQOS 日志
|
||||
// .check_min_tip(false) // 默认: false - 过滤低于最低小费的 SWQOS
|
||||
// .swqos_cores_from_end(false) // 默认: false - 将 SWQOS 绑定到末尾 N 个 CPU 核心
|
||||
// .mev_protection(false) // 默认: false - MEV 保护(Astralane 端口 9000 / BlockRazor sandwichMitigation)
|
||||
// .mev_protection(false) // 默认: false - MEV(Astralane QUIC :9000 或 HTTP mev-protect / BlockRazor)
|
||||
.build();
|
||||
|
||||
// 创建 TradingClient
|
||||
@@ -279,28 +280,28 @@ let bloxroute_config = SwqosConfig::Bloxroute(
|
||||
|
||||
当使用多个MEV服务时,需要使用`Durable Nonce`。你需要使用`fetch_nonce_info`函数获取最新的`nonce`值,并在交易的时候将`durable_nonce`填入交易参数。
|
||||
|
||||
#### Astralane QUIC(低延迟)
|
||||
#### Astralane(Binary / Plain / QUIC)
|
||||
|
||||
Astralane 支持 **HTTP** 与 **QUIC** 两种传输方式。QUIC 可减少连接开销,降低提交延迟。使用 QUIC 时,将 `SwqosConfig::Astralane` 的第四个参数设为 `Some(SwqosTransport::Quic)`。Astralane 的 QUIC 服务使用**单一端点**(无分区域端点),选 QUIC 时 SDK 会忽略 `region` 与可选自定义 URL;为与其他 SWQoS 配置一致,可传入相同 region。
|
||||
Astralane 支持 **Binary** HTTP(`/irisb`)、**Plain** HTTP(`/iris`)与 **QUIC**(`host:7000`,全局 `mev_protection` 为 true 时用 `:9000`)。第四个参数:`Some(AstralaneTransport::Plain)`、`Some(AstralaneTransport::Quic)`,或 `None` 表示 **Binary**(默认)。全局 `mev_protection` 会在 HTTP 上附加 `mev-protect=true`,或为 QUIC 选择 9000 端口。
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{SwqosConfig, SwqosRegion, SwqosTransport};
|
||||
use sol_trade_sdk::{SwqosConfig, SwqosRegion, AstralaneTransport};
|
||||
|
||||
// Astralane 使用 QUIC(低延迟);region 会被忽略(QUIC 单一端点)
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Astralane(
|
||||
"your_astralane_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt, // 与其他服务一致即可;QUIC 时会被忽略
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Quic),
|
||||
Some(AstralaneTransport::Quic),
|
||||
),
|
||||
];
|
||||
// 然后照常使用 swqos_configs 创建 TradeConfig / TradingClient
|
||||
```
|
||||
|
||||
- **HTTP**(默认):第四个参数为 `None` 或 `Some(SwqosTransport::Http)`;region 与可选自定义 URL 生效。
|
||||
- **QUIC**:第四个参数为 `Some(SwqosTransport::Quic)`;SDK 使用单一 QUIC 端点并忽略 region。与 HTTP 使用同一 API key。
|
||||
- **Binary**(默认):`None` 或 `Some(AstralaneTransport::Binary)` — `/irisb`,bincode 正文。
|
||||
- **Plain**:`Some(AstralaneTransport::Plain)` — `/iris`。
|
||||
- **QUIC**:`Some(AstralaneTransport::Quic)` — 按区域的 `host:7000` / `:9000`(MEV);同一 API key。
|
||||
|
||||
---
|
||||
|
||||
@@ -334,6 +335,10 @@ PumpFun 与 PumpSwap 支持**返现(Cashback)**:部分手续费可返还
|
||||
- **pumpfun_copy_trading**、**pumpfun_sniper_trading** 示例使用 sol-parser-sdk 订阅 gRPC 事件,并在构造参数时传入 `e.is_cashback_coin`。
|
||||
- **领取返现**:使用 `client.claim_cashback_pumpfun()` 和 `client.claim_cashback_pumpswap(...)` 领取累计的返现。
|
||||
|
||||
#### PumpFun:常见错误与排错思路
|
||||
|
||||
实盘集成时若遇 **Anchor 2006、`NotAuthorized`(6000)、Token program 不匹配、6020/6042** 等,请参阅专门文档:**[Pump.fun 常见链上错误与处理思路(中文)](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)**。
|
||||
|
||||
#### PumpFun:Creator Rewards Sharing(creator_vault)
|
||||
|
||||
部分 PumpFun 代币启用了 **Creator Rewards Sharing**,链上 `creator_vault` 可能与默认推导结果不同。若在**卖出**时复用**买入**时缓存的 params,可能触发程序错误 **2006(seeds constraint violated)**。建议:
|
||||
@@ -363,7 +368,7 @@ SDK 不会在每次卖出时通过 RPC 拉取 creator_vault(以避免延迟)
|
||||
- **FlashBlock**: 高速交易执行,支持 API 密钥认证
|
||||
- **BlockRazor**: 高速交易执行,支持 API 密钥认证
|
||||
- **Node1**: 高速交易执行,支持 API 密钥认证
|
||||
- **Astralane**: 区块链网络加速(支持 HTTP 与 QUIC,见上方 [Astralane QUIC](#astralane-quic低延迟))
|
||||
- **Astralane**: 区块链网络加速(Binary/Plain HTTP 与 QUIC,见 [Astralane](#astralanebinary--plain--quic))
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
# Pump.fun(Bonding Curve)常见链上错误与处理思路
|
||||
|
||||
本文档面向使用 **sol-trade-sdk** 组装 Pump.fun Program(`6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`)买卖交易的集成方,汇总**实战中高频**的失败形态、日志特征,以及与 SDK 参数的对应关系和**推荐处理方式**。
|
||||
|
||||
> 自定义错误码以仓库内 `idl/pump.json`、`idl/pump_fees.json` 为准;**2006** 等部分错误来自 **Anchor 框架**,不在 Pump 自定义枚举里。
|
||||
|
||||
---
|
||||
|
||||
## 1. Anchor `2006` / `ConstraintSeeds`(`creator_vault`)
|
||||
|
||||
### 现象
|
||||
|
||||
- Solana Explorer / `simulateTransaction`:**Program Error: custom program error: 2006**,或 Anchor 文案 **A seeds constraint was violated**。
|
||||
- 日志里常见于账户 **`creator_vault`**:打印 **Left**(你传入的 pubkey)与 **Right**(程序按当前 curve 推导的 PDA)。
|
||||
|
||||
### 含义
|
||||
|
||||
Pump 校验 `creator_vault` 必须满足程序的 **PDAs seeds**(与 bonding curve 上记录的 **creator / fee-sharing 布局**一致)。传入地址与程序期望不一致即失败。
|
||||
|
||||
### 常见成因
|
||||
|
||||
1. **`bonding_curve.creator` 与链上不符**:事件/缓存里用的是 create 交易的 `creator` 字段、`user`,或陈旧快照,与 curve 账户内真实 creator 不一致;据此推导或缓存的 vault 会错。
|
||||
2. **买单侧「旧 vault」被沿用到卖单**:买入时 ix 里的 `creator_vault` 在后续 trade 语境下已不再与程序约束一致(例如 creator / sharing 语义在链上演进),卖单仍填旧地址 → **Left ≠ Right**。
|
||||
3. **Creator Rewards / pump-fees 分成布局**:部分 mint 使用 `sharing-config` 相关 seeds,单靠 `PDA(["creator-vault"], bonding_curve.creator)` 不足以覆盖全部历史状态;Stale 的 offline hint 也会把 resolve 引向错误 vault。
|
||||
4. **Phantom vault**:历史上若用 `Pubkey::default()` 推导出的占位 vault(SDK 常量 `phantom_default_creator_vault`),链上必定失败。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
| 方向 | 做法 |
|
||||
|------|------|
|
||||
| **买入 / 卖出指令**(同一套解析) | 使用 `resolve_creator_vault_for_ix_with_fee_sharing`(`src/instruction/utils/pumpfun.rs`):有 **非 default、非 phantom** 的 ix / 解析器回填 **`creator_vault` 时原样采用**(**不会**再根据 `creator` 做 `get_creator_vault_pda` 覆盖费分成等非传统布局);**未传 ix vault** 时依次:`fee_sharing_creator_vault_if_active` hint → `PDA(effective_creator)`。`PumpFunInstructionBuilder` 买卖均走此逻辑。 |
|
||||
| **权威对齐** | 低延迟路径外,可 **`PumpFunParams::from_mint_by_rpc`** 或由解析器 **`fill_trade_accounts`**(如 sol-parser-sdk)持续刷新 `creator` / `creator_vault`;必要时对 **fee-sharing** 使用 `fetch_fee_sharing_creator_vault_if_active` / `refresh_fee_sharing_creator_vault_from_rpc`。 |
|
||||
| **bonding_curve 账户地址** | 指令构建时使用 **`get_bonding_curve_pda(mint)`** 作为 canonical bonding curve pubkey,避免缓存中的曲线地址错位导致读到错误 **creator**。 |
|
||||
|
||||
集成方若在 **bot** 侧维护持仓快照,建议在**每笔**解析到的 Pump trade 后刷新缓存/仓位中的 `creator` 与 `creator_vault`,避免「只写一次建仓快照」。
|
||||
|
||||
---
|
||||
|
||||
## 2. Pump `6000` `NotAuthorized`(常见:`feeRecipient`)
|
||||
|
||||
### 现象
|
||||
|
||||
- 日志:`AnchorError thrown in programs/pump/src/fee_recipient.rs` 或 **`Error Code: NotAuthorized`**(与 Global 授权的 fee recipient 池有关)。
|
||||
|
||||
### 含义
|
||||
|
||||
账户 **#2 fee recipient** 不是当前 Pump **Global / 协议**允许使用的收款地址之一,或与 **Mayhem / 非 Mayhem** 池不一致。
|
||||
|
||||
### 常见成因
|
||||
|
||||
1. 使用了**过期**或**错误池**的 fee recipient(静态列表落后于主网轮换)。
|
||||
2. **`mayhem_mode` 与 `fee_recipient` 不匹配**:声明 Mayhem 却传普通池地址,或相反(见 `reconcile_mayhem_mode_for_trade`,`src/instruction/utils/pumpfun.rs`)。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
| 方向 | 做法 |
|
||||
|------|------|
|
||||
| **优先事件** | 使用 gRPC / 解析器里的 **`tradeEvent.feeRecipient`** 或同笔 **create_v2 + buy** 观测到的 fee recipient。 |
|
||||
| **纠偏** | `PumpFunParams::from_trade` 会对 `mayhem_mode` 与 `fee_recipient` 做池一致性纠偏;发单前若事件缺省,可走 `pump_fun_fee_recipient_meta`(按 `is_mayhem_mode` 从静态池选)。 |
|
||||
| **提交前清空** | 若你明确希望与 npm / 官方 SDK 一致的「按池随机」,可在业务层 **`fee_recipient = default`**,由 builder 再走静态池(与 README 中的 Cashback/Mayhem 说明一致)。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. SPL:Token **`initializeAccount3`**——`incorrect program id for instruction`
|
||||
|
||||
### 现象
|
||||
|
||||
- 内联指令里 **`Token Program: initializeAccount3`**(或 Token-2022 等价指令)报错 **`incorrect program id for instruction`**。
|
||||
|
||||
### 含义
|
||||
|
||||
为 **Mint** 创建用户 ATA 时,使用的 **token program(Legacy SPL vs Token-2022)** 与 **Mint 的实际 owner(`mint.owner`)** 不一致。
|
||||
|
||||
### 常见成因
|
||||
|
||||
1. Pump 新发多为 **Token-2022**,但代码写死 **`Tokenkeg…`**。
|
||||
2. 少数 Legacy mint(`Tokenkeg…`),却被强制按 Token-2022 建账。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- **`PumpFunParams::token_program`** 必须与非 default 的 **mint owner** 一致;从 **gRPC / 解析结果** 带入,**勿**在已明确 program 时再用「仅按 `.pump` 后缀猜 Token-2022」覆盖(业务层若做后缀启发,应仅在 `token_program == default` 时生效)。
|
||||
|
||||
---
|
||||
|
||||
## 4. Pump `6020` `BuyZeroAmount`
|
||||
|
||||
### 现象
|
||||
|
||||
- `buy` / `buy_exact_sol_in` 报 **Buy zero amount**。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- `min_tokens_out == 0`(或等价路径算出可买 **0 枚**),协议直接拒绝。
|
||||
- 使用 **Create / Shred** 事件构造曲线时 **virtual / real 储备全 0**,本地定价算出 **0**。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- 对「首买 / 无储备」场景用 **`PumpFunParams::from_dev_trade`** 或按协议初值回填虚拟储备(与 `global_constants` 一致),再算 **`min_tokens_out`**。
|
||||
- 适当 **放宽买入滑点**(`slippage_basis_points`),避免估算代币量略小于链上。
|
||||
|
||||
---
|
||||
|
||||
## 5. Pump `6042` `BuySlippageBelowMinTokensOut`
|
||||
|
||||
### 现象
|
||||
|
||||
- 文案:**Slippage: Would buy less tokens than expected min_tokens_out**。
|
||||
|
||||
### 含义
|
||||
|
||||
链上实际可成交代币数量 **小于** 指令参数 **`min_tokens_out`**。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- 市价波动、SOL 竞价导致曲线状态与本地快照不一致。
|
||||
- 本地 **`get_buy_token_amount_from_sol_amount`** 所用 **creator / 费率假设**与链上 **pfee CPI**(动态费率)不一致,**预估偏多**。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- **提高滑点容忍**(或降低 **`min_tokens_out`**)。
|
||||
- 尽量用 **较新**的 **virtual/real reserves**(来自最近一次 trade 解析或简短 RPC)。
|
||||
- 若运行在 **狙击手**等极端延迟场景,需接受:**保守的 min_out**(更大滑点)换成功率。
|
||||
|
||||
---
|
||||
|
||||
## 6. Pump `6024` `Overflow` 与其它算术类错误
|
||||
|
||||
### 现象
|
||||
|
||||
- `6024 Overflow`、`6025 Truncation`、`6026 DivisionByZero`(见 IDL)。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- 指令参数 **`amount` / SOL / lamports** 与曲线状态组合不合法(例如极端大卖、或为 0 与后续计算冲突)。
|
||||
- SDK 外传入了 **不合理的储备快照**。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- 校验 **买入/卖出金额 > 0**、与余额一致。
|
||||
- 使用 **`from_mint_by_rpc`** 或与链一致的储备后再算 **`min_sol_output` / `min_tokens_out`**。
|
||||
|
||||
---
|
||||
|
||||
## 7. Pump `6027` `NotEnoughRemainingAccounts`(返现等)
|
||||
|
||||
### 现象
|
||||
|
||||
- 返现代币等路径要求 **remaining accounts**(例如 `UserVolumeAccumulator`),数量不足。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- **`is_cashback_coin`(或等价标志)为 true**,但组装指令时 **未追加**所需账户。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- **`PumpFunParams::from_trade` / `from_dev_trade`** 传入正确的 **`is_cashback_coin`**(来自事件)。
|
||||
- README 中与 **Cashback** 章节一致:**事件路径必须带标志**,不能默认 false。
|
||||
|
||||
---
|
||||
|
||||
## 8. Pump `6022` `SellZeroAmount`
|
||||
|
||||
### 含义
|
||||
|
||||
卖出代币数量为 **0**。在业务层过滤即可。
|
||||
|
||||
---
|
||||
|
||||
## 9. 与 pump-fees / Creator 迁移相关的错误(`6049`–`6053` 等)
|
||||
|
||||
IDL 中例如:
|
||||
|
||||
- **`6049` `CreatorMigratedToSharingConfig`**
|
||||
- **`6050` `UnableToDistributeCreatorVaultMigratedToSharingConfig`**
|
||||
- **`6053` `BondingCurveAndSharingConfigCreatorMismatch`**
|
||||
|
||||
### 思路
|
||||
|
||||
这些是 **creator / fee-sharing** 生命周期中的**专用分支**,与一般买卖路径不同。若仿真或清算类指令触发:
|
||||
|
||||
- 以 **Pump / pump-fees 官方文档** 为准使用 **`distribute_creator_fees`**、**reset_fee_sharing_config** 等;
|
||||
- **`creator_vault` resolve** 需结合 **`fetch_fee_sharing_creator_vault_if_active`** 与链上 **`SharingConfig` 状态**,避免离线 deduce 过时。
|
||||
|
||||
---
|
||||
|
||||
## 10. 调试清单(推荐给集成方)
|
||||
|
||||
1. **记下失败指令索引** + **Simulation / explorer 展开的账户列表**,重点核对:**mint、bonding_curve、associated_bonding_curve、creator_vault、fee_recipient、token_program**。
|
||||
2. **对比 Anchor 日志里的 Left / Right**(针对 2006)与本地 `PumpFunParams` 打印是否一致。
|
||||
3. **`mint.owner`** 与 **`PumpFunParams.token_program`** 是否一致。
|
||||
4. **`bonding_curve` 地址**是否与 **`get_bonding_curve_pda(mint)`** 一致。
|
||||
5. **`mayhem_mode` ↔ `fee_recipient`** 是否同池。
|
||||
6. 低延迟不足以覆盖 **creator 演进** 时,是否在卖前引入了 **RPC 或最新 trade** 刷新。
|
||||
|
||||
---
|
||||
|
||||
## 参考代码入口(本仓库)
|
||||
|
||||
| 主题 | 路径 |
|
||||
|------|------|
|
||||
| Buy / Sell vault resolve(显式 ix `creator_vault` → `fee_sharing` hint → `PDA(effective_creator)`) | `src/instruction/utils/pumpfun.rs` — `resolve_creator_vault_for_ix_with_fee_sharing`;`effective_creator_for_trade` 见 `src/trading/core/params/pumpfun.rs` |
|
||||
| Fee recipient / Mayhem | `src/instruction/utils/pumpfun.rs` — `pump_fun_fee_recipient_meta`, `reconcile_mayhem_mode_for_trade` |
|
||||
| 指令构建 | `src/instruction/pumpfun.rs` — `PumpFunInstructionBuilder` |
|
||||
| Params | `src/trading/core/params/pumpfun.rs` — `PumpFunParams::{from_trade, from_dev_trade, from_mint_by_rpc, refresh_fee_sharing_creator_vault_from_rpc}` |
|
||||
|
||||
---
|
||||
|
||||
如需英文版或对 PumpSwap/其它 DEX 的同类文档,可在 `docs/` 下按相同结构扩展。
|
||||
@@ -162,7 +162,9 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
|
||||
trade_info.base_token_program,
|
||||
trade_info.quote_token_program,
|
||||
trade_info.protocol_fee_recipient,
|
||||
pool_data.coin_creator,
|
||||
pool_data.is_cashback_coin,
|
||||
trade_info.cashback_fee_basis_points,
|
||||
);
|
||||
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
@@ -191,7 +193,9 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
|
||||
trade_info.base_token_program,
|
||||
trade_info.quote_token_program,
|
||||
trade_info.protocol_fee_recipient,
|
||||
pool_data.coin_creator,
|
||||
pool_data.is_cashback_coin,
|
||||
trade_info.cashback_fee_basis_points,
|
||||
);
|
||||
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
use sol_trade_sdk::{
|
||||
common::{AnyResult, InfrastructureConfig, TradeConfig},
|
||||
swqos::{SwqosConfig, SwqosRegion},
|
||||
SwqosTransport, TradingClient, TradingInfrastructure,
|
||||
AstralaneTransport, TradingClient, TradingInfrastructure,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
@@ -52,8 +52,8 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
|
||||
"your_api_token".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Quic),
|
||||
), // QUIC; use None for HTTP
|
||||
Some(AstralaneTransport::Quic),
|
||||
), // QUIC;None / Some(Binary) / Some(Plain) 为 HTTP
|
||||
// Helius Sender: 4th param swqos_only Some(true) => min tip 0.000005 SOL; None => 0.0002 SOL
|
||||
SwqosConfig::Helius("".to_string(), SwqosRegion::Default, None, Some(true)),
|
||||
];
|
||||
|
||||
+1222
File diff suppressed because it is too large
Load Diff
@@ -154,6 +154,8 @@ pub enum PdaCacheKey {
|
||||
PumpFunUserVolume(Pubkey),
|
||||
PumpFunBondingCurve(Pubkey),
|
||||
PumpFunBondingCurveV2(Pubkey),
|
||||
/// Pump-fees program `sharing-config` PDA for a mint (`feeSharingConfigPda`).
|
||||
PumpFunFeeSharingConfig(Pubkey),
|
||||
PumpFunCreatorVault(Pubkey),
|
||||
BonkPool(Pubkey, Pubkey),
|
||||
BonkVault(Pubkey, Pubkey),
|
||||
|
||||
+4
-4
@@ -12,7 +12,7 @@ pub struct InfrastructureConfig {
|
||||
/// When true, SWQOS sender threads use the *last* N cores instead of the first N. Reduces contention with main thread / default tokio workers that often use low-numbered cores. Default false.
|
||||
pub swqos_cores_from_end: bool,
|
||||
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
|
||||
/// (Astralane QUIC port 9000, BlockRazor revert_protection) will use their MEV-protected
|
||||
/// (Astralane QUIC `:9000` or HTTP `mev-protect=true`, BlockRazor) use MEV-protected
|
||||
/// endpoints/modes. Default false.
|
||||
pub mev_protection: bool,
|
||||
}
|
||||
@@ -92,8 +92,8 @@ pub struct TradeConfig {
|
||||
/// When true, SWQOS uses the *last* N cores (instead of the first N). Use when main thread / tokio use low-numbered cores to reduce CPU contention. Default false.
|
||||
pub swqos_cores_from_end: bool,
|
||||
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
|
||||
/// (Astralane QUIC port 9000, BlockRazor sandwichMitigation mode) will use their
|
||||
/// MEV-protected endpoints/modes. Default false (no MEV protection, lower latency).
|
||||
/// (Astralane QUIC `:9000` or Plain/Binary HTTP `mev-protect=true`, BlockRazor sandwichMitigation)
|
||||
/// use their MEV-protected endpoints/modes. Default false (no MEV protection, lower latency).
|
||||
pub mev_protection: bool,
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ impl TradeConfigBuilder {
|
||||
}
|
||||
|
||||
/// Enable global MEV protection. When `true`:
|
||||
/// - **Astralane QUIC** uses port `9000` (MEV-protected endpoint)
|
||||
/// - **Astralane QUIC** uses port `9000`; **Astralane HTTP** adds `mev-protect=true`
|
||||
/// - **BlockRazor** uses `mode=sandwichMitigation` (skips blacklisted Leader slots)
|
||||
///
|
||||
/// May reduce landing speed. Default: `false`.
|
||||
|
||||
+214
-92
@@ -171,215 +171,267 @@ pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"),
|
||||
];
|
||||
|
||||
// NewYork,
|
||||
// Frankfurt,
|
||||
// Amsterdam,
|
||||
// SLC,
|
||||
// Tokyo,
|
||||
// London,
|
||||
// LosAngeles,
|
||||
// Default,
|
||||
// `SwqosRegion` 与下列各 `SWQOS_ENDPOINTS_*` 下标严格对应(共 10 项):
|
||||
// 0 NewYork, 1 Frankfurt, 2 Amsterdam, 3 Dublin, 4 SLC, 5 Tokyo, 6 Singapore, 7 London, 8 LosAngeles, 9 Default。
|
||||
//
|
||||
// **地理就近(用户语义)**:当某枚举区域没有该服务商**独立公布**的 PoP 时,在**该服务商已出现的端点集合内**,按真实地理位置选**大圆距离最近**的一项作为填充;行尾注释说明依据。
|
||||
// **例外**:`SwqosRegion::Default`(下标 9)不表示地球上的点,表中为全局 URL 或文档默认枢纽,**不适用**地理就近,仅表示「未指定区域时的回退」。
|
||||
// 若某区域仅有一种「大区」级入口(例如全美只有一个美东 PoP),则地理上非最优但只能复用,注释会标明「受服务商可用区限制」。
|
||||
|
||||
pub const SWQOS_ENDPOINTS_JITO: [&str; 8] = [
|
||||
/// Jito mainnet block engines (`https://<region>.mainnet.block-engine.jito.wtf`).
|
||||
/// There is no Los Angeles engine → use Salt Lake City for `LosAngeles`; `SwqosRegion::Default` uses the global mainnet URL.
|
||||
pub const SWQOS_ENDPOINTS_JITO: [&str; 10] = [
|
||||
"https://ny.mainnet.block-engine.jito.wtf",
|
||||
"https://frankfurt.mainnet.block-engine.jito.wtf",
|
||||
"https://amsterdam.mainnet.block-engine.jito.wtf",
|
||||
"https://dublin.mainnet.block-engine.jito.wtf",
|
||||
"https://slc.mainnet.block-engine.jito.wtf",
|
||||
"https://tokyo.mainnet.block-engine.jito.wtf",
|
||||
"https://singapore.mainnet.block-engine.jito.wtf",
|
||||
"https://london.mainnet.block-engine.jito.wtf",
|
||||
"https://ny.mainnet.block-engine.jito.wtf",
|
||||
"https://slc.mainnet.block-engine.jito.wtf", // LosAngeles: no LA PoP; nearest US-West is SLC
|
||||
"https://mainnet.block-engine.jito.wtf",
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_NEXTBLOCK: [&str; 8] = [
|
||||
/// NextBlock regional HTTP hosts (see provider docs). `SwqosRegion` order; no dedicated LA PoP → SLC as US-West fallback.
|
||||
pub const SWQOS_ENDPOINTS_NEXTBLOCK: [&str; 10] = [
|
||||
"http://ny.nextblock.io",
|
||||
"http://frankfurt.nextblock.io",
|
||||
"http://amsterdam.nextblock.io",
|
||||
"http://fra.nextblock.io",
|
||||
"http://ams.nextblock.io",
|
||||
"http://dublin.nextblock.io",
|
||||
"http://slc.nextblock.io",
|
||||
"http://tokyo.nextblock.io",
|
||||
"http://sgp.nextblock.io",
|
||||
"http://london.nextblock.io",
|
||||
"http://singapore.nextblock.io",
|
||||
"http://frankfurt.nextblock.io",
|
||||
"http://slc.nextblock.io",
|
||||
"http://fra.nextblock.io", // Default: 非地理区域;服务商无「全局」主机名时用 EU 枢纽作未选区回退
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_ZERO_SLOT: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_ZERO_SLOT: [&str; 10] = [
|
||||
"http://ny.0slot.trade",
|
||||
"http://de2.0slot.trade", // Use de2 for TSW, and de1 for OVH
|
||||
"http://ams.0slot.trade",
|
||||
"http://ny.0slot.trade",
|
||||
"http://ams.0slot.trade", // Dublin: 无 IE 专用;在已公布 EU 点中选距爱尔兰最近的 ams(相对 de2 等)
|
||||
"http://la.0slot.trade", // SLC: no UT PoP; nearest US-West published host
|
||||
"http://jp.0slot.trade",
|
||||
"http://ams.0slot.trade",
|
||||
"http://jp.0slot.trade", // SG: 无本地 PoP;已公布 APAC 仅 jp,为表中离新加坡最近的大圆距离
|
||||
"http://ams.0slot.trade", // London: 无 UK 专用;已公布 EU 点中 ams 距伦敦最近之一
|
||||
"http://la.0slot.trade",
|
||||
"http://de2.0slot.trade", // Use de2 for TSW, and de1 for OVH
|
||||
"http://de2.0slot.trade", // Default: 非地理区域;EU 枢纽 de2 作未选区回退
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_TEMPORAL: [&str; 8] = [
|
||||
"http://ewr1.nozomi.temporal.xyz",
|
||||
/// Nozomi Direct regions: ewr1, fra2, ams1, lon1, lax1, tyo1, sgp1, …
|
||||
pub const SWQOS_ENDPOINTS_TEMPORAL: [&str; 10] = [
|
||||
"http://ewr1.nozomi.temporal.xyz", // NewYork → Newark
|
||||
"http://fra2.nozomi.temporal.xyz",
|
||||
"http://ams1.nozomi.temporal.xyz",
|
||||
"http://ewr1.nozomi.temporal.xyz",
|
||||
"http://lon1.nozomi.temporal.xyz", // Dublin: no IE host; UK nearest Direct PoP
|
||||
"http://lax1.nozomi.temporal.xyz", // SLC: US-West
|
||||
"http://tyo1.nozomi.temporal.xyz",
|
||||
"http://sgp1.nozomi.temporal.xyz",
|
||||
"http://pit1.nozomi.temporal.xyz",
|
||||
"http://fra2.nozomi.temporal.xyz",
|
||||
"http://lon1.nozomi.temporal.xyz",
|
||||
"http://lax1.nozomi.temporal.xyz",
|
||||
"http://fra2.nozomi.temporal.xyz", // Default: 非地理区域;EU Direct 枢纽
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_BLOX: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_BLOX: [&str; 10] = [
|
||||
"https://ny.solana.dex.blxrbdn.com",
|
||||
"https://germany.solana.dex.blxrbdn.com",
|
||||
"https://amsterdam.solana.dex.blxrbdn.com",
|
||||
"https://ny.solana.dex.blxrbdn.com",
|
||||
"https://uk.solana.dex.blxrbdn.com", // Dublin: IE/UK edge
|
||||
"https://la.solana.dex.blxrbdn.com", // SLC: no Mountain PoP; US-West LA
|
||||
"https://tokyo.solana.dex.blxrbdn.com",
|
||||
"https://tokyo.solana.dex.blxrbdn.com", // SG: 文档无 SGP 区域;已公布 APAC 仅 Tokyo,为距 SG 最近选项
|
||||
"https://uk.solana.dex.blxrbdn.com",
|
||||
"https://la.solana.dex.blxrbdn.com",
|
||||
"https://global.solana.dex.blxrbdn.com",
|
||||
"https://global.solana.dex.blxrbdn.com", // Default: 非地理区域;全球任播
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_NODE1: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_NODE1: [&str; 10] = [
|
||||
"http://ny.node1.me",
|
||||
"http://fra.node1.me",
|
||||
"http://ams.node1.me",
|
||||
"http://ny.node1.me",
|
||||
"http://lon.node1.me", // Dublin: 已公布中英爱区域用 lon(地理上近爱尔兰)
|
||||
"http://ny.node1.me", // SLC: 已公布美国仅 ny;美西无 PoP,受可用区限制复用美东
|
||||
"http://tk.node1.me",
|
||||
"http://tk.node1.me", // SG: 已公布 APAC 仅 tk;地理上为表中离 SG 最近
|
||||
"http://lon.node1.me",
|
||||
"http://ny.node1.me",
|
||||
"http://fra.node1.me",
|
||||
"http://ny.node1.me", // LosAngeles: 同上,美国仅 ny 入口
|
||||
"http://fra.node1.me", // Default: 非地理区域;与 QUIC 对齐为 EU 枢纽
|
||||
];
|
||||
|
||||
/// Node1 QUIC: port 16666. Region order: NewYork, Frankfurt, Amsterdam, SLC→ny, Tokyo, London, LosAngeles→ny, Default→ny.
|
||||
/// Node1 QUIC: port 16666. Region order matches [`SwqosRegion`].
|
||||
/// server_name = host part (e.g. ny.node1.me). Auth: first bi stream = 16-byte UUID; each tx = new bi stream, bincode body.
|
||||
pub const SWQOS_ENDPOINTS_NODE1_QUIC: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_NODE1_QUIC: [&str; 10] = [
|
||||
"ny.node1.me:16666",
|
||||
"fra.node1.me:16666",
|
||||
"ams.node1.me:16666",
|
||||
"ny.node1.me:16666", // SLC → ny
|
||||
"lon.node1.me:16666",
|
||||
"ny.node1.me:16666",
|
||||
"tk.node1.me:16666",
|
||||
"tk.node1.me:16666",
|
||||
"lon.node1.me:16666",
|
||||
"ny.node1.me:16666", // LA → ny
|
||||
"ny.node1.me:16666", // Default → ny
|
||||
"ny.node1.me:16666",
|
||||
"fra.node1.me:16666", // Default: 非地理区域;与 HTTP 对齐为 EU 枢纽
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_FLASHBLOCK: [&str; 8] = [
|
||||
/// Published: ny, slc, ams, fra, singapore, london, tokyo (no IE/UK split → london for Dublin).
|
||||
pub const SWQOS_ENDPOINTS_FLASHBLOCK: [&str; 10] = [
|
||||
"http://ny.flashblock.trade",
|
||||
"http://fra.flashblock.trade",
|
||||
"http://ams.flashblock.trade",
|
||||
"http://london.flashblock.trade", // Dublin: no IE host; UK nearest
|
||||
"http://slc.flashblock.trade",
|
||||
"http://tokyo.flashblock.trade",
|
||||
"http://singapore.flashblock.trade",
|
||||
"http://london.flashblock.trade",
|
||||
"http://ny.flashblock.trade",
|
||||
"http://ny.flashblock.trade",
|
||||
"http://slc.flashblock.trade",
|
||||
"http://fra.flashblock.trade", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
/// BlockRazor Send Transaction v2: plain-text Base64 body, auth in URI, Content-Type: text/plain. Keep-alive: POST /v2/health.
|
||||
/// 若 HTTP 返回 500,可尝试 HTTPS:https://<region>.solana.blockrazor.io/v2/sendTransaction(Frankfurt/NewYork/Tokyo),通过 custom_url 覆盖。
|
||||
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 10] = [
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://amsterdam.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://london.solana.blockrazor.xyz:443/v2/sendTransaction", // Dublin: UK nearest published
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction", // SLC: 文档无美西;美国仅 NY,受可用区限制
|
||||
"http://tokyo.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://tokyo.solana.blockrazor.xyz:443/v2/sendTransaction", // SG: 已公布 APAC 仅 Tokyo,为距 SG 最近
|
||||
"http://london.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction", // LosAngeles: 无美西入口;美国仅 NY
|
||||
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
/// BlockRazor gRPC endpoints. Region order: NewYork, Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles, Default.
|
||||
/// BlockRazor gRPC endpoints. Region order matches [`SwqosRegion`].
|
||||
/// Port 80 for gRPC protocol. Auth: apikey metadata in gRPC headers.
|
||||
pub const SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC: [&str; 10] = [
|
||||
"http://newyork.solana-grpc.blockrazor.xyz:80",
|
||||
"http://frankfurt.solana-grpc.blockrazor.xyz:80",
|
||||
"http://amsterdam.solana-grpc.blockrazor.xyz:80",
|
||||
"http://newyork.solana-grpc.blockrazor.xyz:80",
|
||||
"http://london.solana-grpc.blockrazor.xyz:80",
|
||||
"http://newyork.solana-grpc.blockrazor.xyz:80", // SLC: 与 HTTP 一致;美国仅 NY
|
||||
"http://tokyo.solana-grpc.blockrazor.xyz:80",
|
||||
"http://tokyo.solana-grpc.blockrazor.xyz:80",
|
||||
"http://london.solana-grpc.blockrazor.xyz:80",
|
||||
"http://newyork.solana-grpc.blockrazor.xyz:80",
|
||||
"http://frankfurt.solana-grpc.blockrazor.xyz:80",
|
||||
"http://newyork.solana-grpc.blockrazor.xyz:80", // LosAngeles: 与 HTTP 一致
|
||||
"http://frankfurt.solana-grpc.blockrazor.xyz:80", // Default: 非地理区域
|
||||
];
|
||||
|
||||
/// Astralane binary API path (no Base64; use with ?api-key=...&method=sendTransaction|getHealth).
|
||||
/// Plain HTTP API path (`/iris?api-key=…&method=…`).
|
||||
pub const ASTRALANE_PATH_IRIS: &str = "iris";
|
||||
/// Binary HTTP API path (`/irisb?api-key=…&method=sendTransaction`, raw bincode body).
|
||||
pub const ASTRALANE_PATH_IRISB: &str = "irisb";
|
||||
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE: [&str; 8] = [
|
||||
/// Astralane **Plain** HTTP gateways (`/iris`). Pair with [`ASTRALANE_PATH_IRIS`].
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_PLAIN: [&str; 10] = [
|
||||
"http://ny.gateway.astralane.io/iris",
|
||||
"http://fr.gateway.astralane.io/iris",
|
||||
"http://ams.gateway.astralane.io/iris",
|
||||
"http://ams.gateway.astralane.io/iris", // Dublin: 无 IE 专用;在已公布 EU 点中选距爱尔兰最近的 ams
|
||||
"http://la.gateway.astralane.io/iris",
|
||||
"http://jp.gateway.astralane.io/iris",
|
||||
"http://sg.gateway.astralane.io/iris",
|
||||
"http://ams.gateway.astralane.io/iris", // London: 无 UK 专用;在已公布 EU 点中选距英国最近的 ams
|
||||
"http://la.gateway.astralane.io/iris",
|
||||
"https://edge.astralane.io/iris", // Default: 非地理区域;全局任播边缘
|
||||
];
|
||||
|
||||
/// Astralane **Binary** HTTP gateways (`/irisb`). Pair with [`ASTRALANE_PATH_IRISB`].
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_BINARY: [&str; 10] = [
|
||||
"http://ny.gateway.astralane.io/irisb",
|
||||
"http://fr.gateway.astralane.io/irisb",
|
||||
"http://ams.gateway.astralane.io/irisb",
|
||||
"http://ny.gateway.astralane.io/irisb",
|
||||
"http://ams.gateway.astralane.io/irisb", // Dublin: 同 Plain
|
||||
"http://la.gateway.astralane.io/irisb",
|
||||
"http://jp.gateway.astralane.io/irisb",
|
||||
"http://ny.gateway.astralane.io/irisb",
|
||||
"http://lax.gateway.astralane.io/irisb",
|
||||
"http://lim.gateway.astralane.io/irisb",
|
||||
"http://sg.gateway.astralane.io/irisb",
|
||||
"http://ams.gateway.astralane.io/irisb", // London: 同 Plain
|
||||
"http://la.gateway.astralane.io/irisb",
|
||||
"https://edge.astralane.io/irisb", // Default: 同 Plain
|
||||
];
|
||||
|
||||
/// Astralane QUIC endpoints (port 7000). Region order: NewYork, Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles, Default.
|
||||
/// See: https://github.com/Astralane/astralane-quic-client. We use fr, ams, la, ny, lim, sg only (avoid ams2/fr2 for lower latency).
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 8] = [
|
||||
"ny.gateway.astralane.io:7000", // NewYork
|
||||
"fr.gateway.astralane.io:7000", // Frankfurt
|
||||
"ams.gateway.astralane.io:7000", // Amsterdam
|
||||
"lim.gateway.astralane.io:7000", // SLC (no slc, use lim)
|
||||
"sg.gateway.astralane.io:7000", // Tokyo (Asia)
|
||||
"ams.gateway.astralane.io:7000", // London (Europe, avoid ams2)
|
||||
"la.gateway.astralane.io:7000", // LosAngeles
|
||||
"lim.gateway.astralane.io:7000", // Default
|
||||
/// Astralane QUIC endpoints (port 7000). Region order matches [`SwqosRegion`].
|
||||
/// See: https://github.com/Astralane/astralane-quic-client.
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 10] = [
|
||||
"ny.gateway.astralane.io:7000",
|
||||
"fr.gateway.astralane.io:7000",
|
||||
"ams.gateway.astralane.io:7000",
|
||||
"ams.gateway.astralane.io:7000", // Dublin: 同 HTTP
|
||||
"la.gateway.astralane.io:7000", // SLC: 美西 la 为最近已公布美区入口
|
||||
"jp.gateway.astralane.io:7000",
|
||||
"sg.gateway.astralane.io:7000",
|
||||
"ams.gateway.astralane.io:7000", // London: 同 HTTP
|
||||
"la.gateway.astralane.io:7000",
|
||||
"lim.gateway.astralane.io:7000", // Default: 非地理区域;全局边缘
|
||||
];
|
||||
|
||||
/// Astralane QUIC MEV-protected endpoints (port 9000). Same region order as SWQOS_ENDPOINTS_ASTRALANE_QUIC.
|
||||
/// Use these when mev_protection=true to route through Astralane's MEV-protected path.
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV: [&str; 8] = [
|
||||
"ny.gateway.astralane.io:9000", // NewYork
|
||||
"fr.gateway.astralane.io:9000", // Frankfurt
|
||||
"ams.gateway.astralane.io:9000", // Amsterdam
|
||||
"lim.gateway.astralane.io:9000", // SLC (no slc, use lim)
|
||||
"sg.gateway.astralane.io:9000", // Tokyo (Asia)
|
||||
"ams.gateway.astralane.io:9000", // London (Europe)
|
||||
"la.gateway.astralane.io:9000", // LosAngeles
|
||||
"lim.gateway.astralane.io:9000", // Default
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV: [&str; 10] = [
|
||||
"ny.gateway.astralane.io:9000",
|
||||
"fr.gateway.astralane.io:9000",
|
||||
"ams.gateway.astralane.io:9000",
|
||||
"ams.gateway.astralane.io:9000",
|
||||
"la.gateway.astralane.io:9000",
|
||||
"jp.gateway.astralane.io:9000",
|
||||
"sg.gateway.astralane.io:9000",
|
||||
"ams.gateway.astralane.io:9000",
|
||||
"la.gateway.astralane.io:9000",
|
||||
"lim.gateway.astralane.io:9000",
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 10] = [
|
||||
"http://ewr1.flashrpc.com",
|
||||
"http://fra1.flashrpc.com",
|
||||
"http://ams1.flashrpc.com",
|
||||
"http://ewr1.flashrpc.com",
|
||||
"http://lhr1.flashrpc.com", // Dublin: 已公布 UK 用 lhr;地理上近爱尔兰
|
||||
"http://ewr1.flashrpc.com", // SLC: 已公布美国仅 ewr;无美西 PoP,受可用区限制
|
||||
"http://tyo1.flashrpc.com",
|
||||
"http://tyo1.flashrpc.com", // SG: 表中无 SGP;APAC 仅 tyo,为距 SG 最近
|
||||
"http://lhr1.flashrpc.com",
|
||||
"http://ewr1.flashrpc.com",
|
||||
"http://fra1.flashrpc.com",
|
||||
"http://ewr1.flashrpc.com", // LosAngeles: 同上,美国仅 ewr
|
||||
"http://fra1.flashrpc.com", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_SOYAS: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_SOYAS: [&str; 10] = [
|
||||
"nyc.landing.soyas.xyz:9000",
|
||||
"fra.landing.soyas.xyz:9000",
|
||||
"ams.landing.soyas.xyz:9000",
|
||||
"nyc.landing.soyas.xyz:9000",
|
||||
"lon.landing.soyas.xyz:9000", // Dublin: 已公布用 lon;地理近爱尔兰
|
||||
"nyc.landing.soyas.xyz:9000", // SLC: 已公布美国仅 nyc;无美西
|
||||
"tyo.landing.soyas.xyz:9000",
|
||||
"tyo.landing.soyas.xyz:9000", // SG: 表中 APAC 仅 tyo
|
||||
"lon.landing.soyas.xyz:9000",
|
||||
"nyc.landing.soyas.xyz:9000",
|
||||
"fra.landing.soyas.xyz:9000",
|
||||
"nyc.landing.soyas.xyz:9000", // LosAngeles: 同上
|
||||
"fra.landing.soyas.xyz:9000", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 10] = [
|
||||
"nyc.speedlanding.trade:17778",
|
||||
"fra.speedlanding.trade:17778",
|
||||
"ams.speedlanding.trade:17778",
|
||||
"nyc.speedlanding.trade:17778",
|
||||
"ams.speedlanding.trade:17778", // Dublin: 已公布 EU 点中 ams 地理近爱尔兰
|
||||
"nyc.speedlanding.trade:17778", // SLC: 表中美国仅 nyc;无美西 PoP,受可用区限制
|
||||
"tyo.speedlanding.trade:17778",
|
||||
"fra.speedlanding.trade:17778",
|
||||
"nyc.speedlanding.trade:17778",
|
||||
"fra.speedlanding.trade:17778",
|
||||
"sgp.speedlanding.trade:17778",
|
||||
"ams.speedlanding.trade:17778", // London: 已公布 EU 中 ams 距英国最近之一
|
||||
"nyc.speedlanding.trade:17778", // LosAngeles: 同上,美国仅 nyc
|
||||
"fra.speedlanding.trade:17778", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
/// Helius Sender: POST /fast, dual routing to validators and Jito. API key optional (custom TPS only).
|
||||
/// Region order: NewYork(EWR), Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles(SG), Default(Global).
|
||||
pub const SWQOS_ENDPOINTS_HELIUS: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_HELIUS: [&str; 10] = [
|
||||
"http://ewr-sender.helius-rpc.com/fast",
|
||||
"http://fra-sender.helius-rpc.com/fast",
|
||||
"http://ams-sender.helius-rpc.com/fast",
|
||||
"http://lon-sender.helius-rpc.com/fast", // Dublin: IE → UK/EU routing
|
||||
"http://slc-sender.helius-rpc.com/fast",
|
||||
"http://tyo-sender.helius-rpc.com/fast",
|
||||
"http://lon-sender.helius-rpc.com/fast",
|
||||
"http://sg-sender.helius-rpc.com/fast",
|
||||
"https://sender.helius-rpc.com/fast",
|
||||
"http://lon-sender.helius-rpc.com/fast",
|
||||
"http://slc-sender.helius-rpc.com/fast",
|
||||
"https://sender.helius-rpc.com/fast", // Default: 非地理区域;全局 Sender
|
||||
];
|
||||
|
||||
pub const SWQOS_MIN_TIP_DEFAULT: f64 = 0.00001; // 其它SWQOS默认最低小费
|
||||
@@ -400,3 +452,73 @@ pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires mini
|
||||
pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002;
|
||||
/// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
|
||||
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SWQOS_REGION_ENDPOINT_TABLES: &[&[&str]] = &[
|
||||
&SWQOS_ENDPOINTS_JITO,
|
||||
&SWQOS_ENDPOINTS_NEXTBLOCK,
|
||||
&SWQOS_ENDPOINTS_ZERO_SLOT,
|
||||
&SWQOS_ENDPOINTS_TEMPORAL,
|
||||
&SWQOS_ENDPOINTS_BLOX,
|
||||
&SWQOS_ENDPOINTS_NODE1,
|
||||
&SWQOS_ENDPOINTS_NODE1_QUIC,
|
||||
&SWQOS_ENDPOINTS_FLASHBLOCK,
|
||||
&SWQOS_ENDPOINTS_BLOCKRAZOR,
|
||||
&SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC,
|
||||
&SWQOS_ENDPOINTS_ASTRALANE_PLAIN,
|
||||
&SWQOS_ENDPOINTS_ASTRALANE_BINARY,
|
||||
&SWQOS_ENDPOINTS_ASTRALANE_QUIC,
|
||||
&SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV,
|
||||
&SWQOS_ENDPOINTS_STELLIUM,
|
||||
&SWQOS_ENDPOINTS_SOYAS,
|
||||
&SWQOS_ENDPOINTS_SPEEDLANDING,
|
||||
&SWQOS_ENDPOINTS_HELIUS,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn all_swqos_endpoint_tables_align_with_swqos_region() {
|
||||
const EXPECT: usize = 10;
|
||||
for (idx, table) in SWQOS_REGION_ENDPOINT_TABLES.iter().enumerate() {
|
||||
assert_eq!(
|
||||
table.len(),
|
||||
EXPECT,
|
||||
"SWQOS endpoint table index {} length must match SwqosRegion (10 variants)",
|
||||
idx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn astralane_quic_hosts_match_mev_row_by_row() {
|
||||
for i in 0..10 {
|
||||
let base = SWQOS_ENDPOINTS_ASTRALANE_QUIC[i].trim_end_matches(":7000");
|
||||
let mev = SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[i].trim_end_matches(":9000");
|
||||
assert_eq!(base, mev, "Astralane QUIC vs MEV host mismatch at index {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node1_http_host_matches_quic_without_port() {
|
||||
for i in 0..10 {
|
||||
let http_host = SWQOS_ENDPOINTS_NODE1[i]
|
||||
.strip_prefix("http://")
|
||||
.expect("NODE1 HTTP URL");
|
||||
let quic_host = SWQOS_ENDPOINTS_NODE1_QUIC[i]
|
||||
.strip_suffix(":16666")
|
||||
.expect("NODE1 QUIC endpoint");
|
||||
assert_eq!(http_host, quic_host, "Node1 HTTP vs QUIC host mismatch at index {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn astralane_plain_and_binary_same_origin_per_region() {
|
||||
for i in 0..10 {
|
||||
let plain = SWQOS_ENDPOINTS_ASTRALANE_PLAIN[i].trim_end_matches("/iris");
|
||||
let binary = SWQOS_ENDPOINTS_ASTRALANE_BINARY[i].trim_end_matches("/irisb");
|
||||
assert_eq!(plain, binary, "Astralane Plain vs Binary base URL mismatch at index {}", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,11 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
if is_a_in {
|
||||
&protocol_params.token_b_program
|
||||
} else {
|
||||
&protocol_params.token_a_program
|
||||
},
|
||||
params.open_seed_optimize,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod bonk;
|
||||
pub mod meteora_damm_v2;
|
||||
pub(crate) mod pumpfun_ix_data;
|
||||
pub(crate) mod pumpswap_ix_data;
|
||||
pub mod pumpfun;
|
||||
pub mod pumpswap;
|
||||
pub mod raydium_amm_v4;
|
||||
|
||||
Executable → Regular
+82
-92
@@ -1,3 +1,5 @@
|
||||
//! Pump.fun bonding-curve swap ix assembly ([`SwapParams`](crate::trading::core::params::SwapParams)).
|
||||
|
||||
use crate::{
|
||||
common::spl_token::close_account,
|
||||
constants::{trade::trade::DEFAULT_SLIPPAGE, TOKEN_PROGRAM_2022},
|
||||
@@ -7,12 +9,15 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use crate::{
|
||||
instruction::pumpfun_ix_data::{
|
||||
encode_pumpfun_buy_exact_sol_in_ix_data, encode_pumpfun_buy_ix_data,
|
||||
encode_pumpfun_sell_ix_data, TRACK_VOLUME_TRUE,
|
||||
},
|
||||
instruction::utils::pumpfun::{
|
||||
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda,
|
||||
get_user_volume_accumulator_pda, pump_fun_fee_recipient_meta,
|
||||
resolve_creator_vault_for_ix,
|
||||
get_protocol_extra_fee_recipient_random, get_user_volume_accumulator_pda,
|
||||
pump_fun_fee_recipient_meta, resolve_creator_vault_for_ix_with_fee_sharing,
|
||||
global_constants::{self},
|
||||
BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||
},
|
||||
utils::calc::{
|
||||
common::{calculate_with_slippage_buy, calculate_with_slippage_sell},
|
||||
@@ -23,33 +28,41 @@ use anyhow::{anyhow, Result};
|
||||
use solana_sdk::instruction::AccountMeta;
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signer::Signer};
|
||||
|
||||
/// Instruction builder for PumpFun protocol
|
||||
#[inline]
|
||||
fn effective_pump_mint_token_program(protocol_params: &PumpFunParams) -> Pubkey {
|
||||
let tp = protocol_params.token_program;
|
||||
if tp == Pubkey::default() {
|
||||
TOKEN_PROGRAM_2022
|
||||
} else {
|
||||
tp
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PumpFunInstructionBuilder;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
// ========================================
|
||||
// Parameter validation and basic data preparation
|
||||
// ========================================
|
||||
let protocol_params = params
|
||||
.protocol_params
|
||||
.as_any()
|
||||
.downcast_ref::<PumpFunParams>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?;
|
||||
|
||||
if params.input_amount.unwrap_or(0) == 0 {
|
||||
let lamports_in = params.input_amount.unwrap_or(0);
|
||||
if lamports_in == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
}
|
||||
|
||||
let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE);
|
||||
|
||||
let bonding_curve = &protocol_params.bonding_curve;
|
||||
// creator_vault must be PDA(creator) per bonding curve. Event vault: use only if == derived;
|
||||
// if stream sends a mismatched vault (wrong token / stale), fall back to derived.
|
||||
let creator = bonding_curve.creator;
|
||||
let creator_vault_pda = resolve_creator_vault_for_ix(
|
||||
let creator = protocol_params.effective_creator_for_trade();
|
||||
let creator_vault_account = resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&creator,
|
||||
protocol_params.creator_vault,
|
||||
¶ms.output_mint,
|
||||
protocol_params.fee_sharing_creator_vault_if_active,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
@@ -58,9 +71,6 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
)
|
||||
})?;
|
||||
|
||||
// ========================================
|
||||
// Trade calculation and account address preparation
|
||||
// ========================================
|
||||
let buy_token_amount = match params.fixed_output_amount {
|
||||
Some(amount) => amount,
|
||||
None => get_buy_token_amount_from_sol_amount(
|
||||
@@ -68,25 +78,19 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
bonding_curve.virtual_sol_reserves as u128,
|
||||
bonding_curve.real_token_reserves as u128,
|
||||
creator,
|
||||
params.input_amount.unwrap_or(0),
|
||||
lamports_in,
|
||||
),
|
||||
};
|
||||
|
||||
let max_sol_cost = calculate_with_slippage_buy(
|
||||
params.input_amount.unwrap_or(0),
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp);
|
||||
|
||||
// 始终用 mint 推导 canonical bonding curve PDA。缓存里的 `bonding_curve.account` 可能指向其它池子,
|
||||
// 会导致链上读到错误 `creator`,从而 creator_vault seeds 与传入的 vault 不一致(Anchor 2006)。
|
||||
let bonding_curve_addr = get_bonding_curve_pda(¶ms.output_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint)
|
||||
})?;
|
||||
|
||||
// Determine token program based on mayhem mode
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
let token_program = protocol_params.token_program;
|
||||
let token_program_meta = if protocol_params.token_program == TOKEN_PROGRAM_2022 {
|
||||
let token_program = effective_pump_mint_token_program(protocol_params);
|
||||
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_META
|
||||
@@ -110,14 +114,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey())
|
||||
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
|
||||
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
// Hot path: no RPC here (latency). For legacy curves <151 bytes, use
|
||||
// `extend_bonding_curve_account_instruction` from a cold path or separate tx.
|
||||
let mut instructions = Vec::with_capacity(2);
|
||||
|
||||
// Create associated token account
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
@@ -130,35 +128,24 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
);
|
||||
}
|
||||
|
||||
// IDL: buy/buy_exact_sol_in 第三参数 track_volume: OptionBool,仅代币支持返现时传 Some(true)
|
||||
let track_volume = if bonding_curve.is_cashback_coin { [1u8, 1u8] } else { [1u8, 0u8] }; // Some(true) / Some(false)
|
||||
let mut buy_data = [0u8; 26];
|
||||
if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
// buy_exact_sol_in(spendable_sol_in: u64, min_tokens_out: u64, track_volume)
|
||||
let min_tokens_out = calculate_with_slippage_sell(
|
||||
buy_token_amount,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
buy_data[..8].copy_from_slice(&BUY_EXACT_SOL_IN_DISCRIMINATOR);
|
||||
buy_data[8..16].copy_from_slice(¶ms.input_amount.unwrap_or(0).to_le_bytes());
|
||||
buy_data[16..24].copy_from_slice(&min_tokens_out.to_le_bytes());
|
||||
buy_data[24..26].copy_from_slice(&track_volume);
|
||||
let buy_data = if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp);
|
||||
encode_pumpfun_buy_exact_sol_in_ix_data(
|
||||
lamports_in,
|
||||
min_tokens_out,
|
||||
TRACK_VOLUME_TRUE,
|
||||
)
|
||||
} else {
|
||||
// buy(token_amount: u64, max_sol_cost: u64, track_volume)
|
||||
buy_data[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
buy_data[8..16].copy_from_slice(&buy_token_amount.to_le_bytes());
|
||||
buy_data[16..24].copy_from_slice(&max_sol_cost.to_le_bytes());
|
||||
buy_data[24..26].copy_from_slice(&track_volume);
|
||||
}
|
||||
encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, TRACK_VOLUME_TRUE)
|
||||
};
|
||||
|
||||
// Fee recipient: gRPC/ShredStream 填入的 `PumpFunParams.fee_recipient`(同笔 create_v2+buy 或 trade 日志)优先;热路径无 RPC。
|
||||
let fee_recipient_meta =
|
||||
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
|
||||
|
||||
let bonding_curve_v2 = get_bonding_curve_v2_pda(¶ms.output_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.output_mint)
|
||||
})?;
|
||||
let mut accounts: Vec<AccountMeta> = vec![
|
||||
let mut metas: Vec<AccountMeta> = vec![
|
||||
global_constants::GLOBAL_ACCOUNT_META,
|
||||
fee_recipient_meta,
|
||||
AccountMeta::new_readonly(params.output_mint, false),
|
||||
@@ -168,7 +155,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
AccountMeta::new(params.payer.pubkey(), true),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
token_program_meta,
|
||||
AccountMeta::new(creator_vault_pda, false),
|
||||
AccountMeta::new(creator_vault_account, false),
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
accounts::GLOBAL_VOLUME_ACCUMULATOR_META,
|
||||
@@ -176,17 +163,22 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
accounts::FEE_CONFIG_META,
|
||||
accounts::FEE_PROGRAM_META,
|
||||
];
|
||||
accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false)); // remainingAccounts: @pump-fun/pump-sdk 要求末尾传 bondingCurveV2Pda(mint),勿删
|
||||
metas.push(AccountMeta::new_readonly(bonding_curve_v2, false));
|
||||
metas.push(AccountMeta::new(
|
||||
get_protocol_extra_fee_recipient_random(),
|
||||
false,
|
||||
));
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &buy_data, accounts));
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::PUMPFUN,
|
||||
&buy_data,
|
||||
metas,
|
||||
));
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
async fn build_sell_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
// ========================================
|
||||
// Parameter validation and basic data preparation
|
||||
// ========================================
|
||||
let protocol_params = params
|
||||
.protocol_params
|
||||
.as_any()
|
||||
@@ -202,12 +194,15 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
return Err(anyhow!("Amount token is required"));
|
||||
};
|
||||
|
||||
let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE);
|
||||
|
||||
let bonding_curve = &protocol_params.bonding_curve;
|
||||
let creator = bonding_curve.creator;
|
||||
let creator_vault_pda = resolve_creator_vault_for_ix(
|
||||
let creator = protocol_params.effective_creator_for_trade();
|
||||
let creator_vault_account = resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&creator,
|
||||
protocol_params.creator_vault,
|
||||
¶ms.input_mint,
|
||||
protocol_params.fee_sharing_creator_vault_if_active,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
@@ -216,9 +211,6 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
)
|
||||
})?;
|
||||
|
||||
// ========================================
|
||||
// Trade calculation and account address preparation
|
||||
// ========================================
|
||||
let sol_amount = get_sell_sol_amount_from_token_amount(
|
||||
bonding_curve.virtual_token_reserves as u128,
|
||||
bonding_curve.virtual_sol_reserves as u128,
|
||||
@@ -228,20 +220,16 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
|
||||
let min_sol_output = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => calculate_with_slippage_sell(
|
||||
sol_amount,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
),
|
||||
None => calculate_with_slippage_sell(sol_amount, slippage_bp),
|
||||
};
|
||||
|
||||
let bonding_curve_addr = get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint)
|
||||
})?;
|
||||
|
||||
// Determine token program based on mayhem mode
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
let token_program = protocol_params.token_program;
|
||||
let token_program_meta = if protocol_params.token_program == TOKEN_PROGRAM_2022 {
|
||||
let token_program = effective_pump_mint_token_program(protocol_params);
|
||||
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_META
|
||||
@@ -262,20 +250,12 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(2);
|
||||
|
||||
let mut sell_data = [0u8; 24];
|
||||
sell_data[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
sell_data[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
sell_data[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
|
||||
|
||||
let sell_data = encode_pumpfun_sell_ix_data(token_amount, min_sol_output);
|
||||
let fee_recipient_meta =
|
||||
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
|
||||
|
||||
let mut accounts: Vec<AccountMeta> = vec![
|
||||
let mut metas: Vec<AccountMeta> = vec![
|
||||
global_constants::GLOBAL_ACCOUNT_META,
|
||||
fee_recipient_meta,
|
||||
AccountMeta::new_readonly(params.input_mint, false),
|
||||
@@ -284,7 +264,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
AccountMeta::new(user_token_account, false),
|
||||
AccountMeta::new(params.payer.pubkey(), true),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
AccountMeta::new(creator_vault_pda, false),
|
||||
AccountMeta::new(creator_vault_account, false),
|
||||
token_program_meta,
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
@@ -292,22 +272,28 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
accounts::FEE_PROGRAM_META,
|
||||
];
|
||||
|
||||
// Cashback: Bonding Curve Sell expects UserVolumeAccumulator PDA at 0th remaining account (writable)
|
||||
if bonding_curve.is_cashback_coin {
|
||||
let user_volume_accumulator =
|
||||
get_user_volume_accumulator_pda(¶ms.payer.pubkey())
|
||||
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
|
||||
accounts.push(AccountMeta::new(user_volume_accumulator, false));
|
||||
metas.push(AccountMeta::new(user_volume_accumulator, false));
|
||||
}
|
||||
// remainingAccounts: @pump-fun/pump-sdk sell 要求末尾传 bondingCurveV2Pda(mint)(cashback 时在 user_volume_accumulator 之后),勿删
|
||||
|
||||
let bonding_curve_v2 = get_bonding_curve_v2_pda(¶ms.input_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.input_mint)
|
||||
})?;
|
||||
accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false));
|
||||
metas.push(AccountMeta::new_readonly(bonding_curve_v2, false));
|
||||
metas.push(AccountMeta::new(
|
||||
get_protocol_extra_fee_recipient_random(),
|
||||
false,
|
||||
));
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &sell_data, accounts));
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::PUMPFUN,
|
||||
&sell_data,
|
||||
metas,
|
||||
));
|
||||
|
||||
// Optional: Close token account
|
||||
if protocol_params.close_token_account_when_sell.unwrap_or(false)
|
||||
|| params.close_input_mint_ata
|
||||
{
|
||||
@@ -324,16 +310,20 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Claim cashback for Bonding Curve (Pump program). Transfers native lamports from UserVolumeAccumulator to user.
|
||||
/// Claim cashback (UserVolumeAccumulator → user lamports).
|
||||
pub fn claim_cashback_pumpfun_instruction(payer: &Pubkey) -> Option<Instruction> {
|
||||
const CLAIM_CASHBACK_DISCRIMINATOR: [u8; 8] = [37, 58, 35, 126, 190, 53, 228, 197];
|
||||
let user_volume_accumulator = get_user_volume_accumulator_pda(payer)?;
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*payer, true), // user (signer, writable)
|
||||
AccountMeta::new(user_volume_accumulator, false), // user_volume_accumulator (writable, not signer)
|
||||
let ix_accounts = vec![
|
||||
AccountMeta::new(*payer, true),
|
||||
AccountMeta::new(user_volume_accumulator, false),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
];
|
||||
Some(Instruction::new_with_bytes(accounts::PUMPFUN, &CLAIM_CASHBACK_DISCRIMINATOR, accounts))
|
||||
Some(Instruction::new_with_bytes(
|
||||
accounts::PUMPFUN,
|
||||
&CLAIM_CASHBACK_DISCRIMINATOR,
|
||||
ix_accounts,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//! Pump.fun 曲线 `buy` / `buy_exact_sol_in` / `sell` 的 **instruction data** 栈上编码(热路径零堆分配)。
|
||||
//!
|
||||
//! 与 `@pump-fun/pump-sdk` Anchor `coder.instruction.encode` 对齐:`OptionBool` 在 ix 参数中为 **1 字节**。
|
||||
|
||||
use crate::instruction::utils::pumpfun::{
|
||||
BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||
};
|
||||
|
||||
/// 与官方 `getBuyInstructionInternal` 一致:`track_volume = true`。
|
||||
pub const TRACK_VOLUME_TRUE: u8 = 1;
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_buy_ix_data(
|
||||
token_amount: u64,
|
||||
max_sol_cost: u64,
|
||||
track_volume: u8,
|
||||
) -> [u8; 25] {
|
||||
let mut d = [0u8; 25];
|
||||
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_sol_cost.to_le_bytes());
|
||||
d[24] = track_volume;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_buy_exact_sol_in_ix_data(
|
||||
spendable_sol_in: u64,
|
||||
min_tokens_out: u64,
|
||||
track_volume: u8,
|
||||
) -> [u8; 25] {
|
||||
let mut d = [0u8; 25];
|
||||
d[..8].copy_from_slice(&BUY_EXACT_SOL_IN_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&spendable_sol_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes());
|
||||
d[24] = track_volume;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_sell_ix_data(token_amount: u64, min_sol_output: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
|
||||
d
|
||||
}
|
||||
+71
-56
@@ -1,10 +1,13 @@
|
||||
use crate::{
|
||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||
instruction::pumpswap_ix_data::{
|
||||
encode_pumpswap_buy_exact_quote_in_ix_data, encode_pumpswap_buy_ix_data,
|
||||
encode_pumpswap_buy_two_args, encode_pumpswap_sell_ix_data,
|
||||
},
|
||||
instruction::utils::pumpswap::{
|
||||
accounts, fee_recipient_ata, get_mayhem_fee_recipient_random, get_pool_v2_pda,
|
||||
get_user_volume_accumulator_pda, get_user_volume_accumulator_quote_ata,
|
||||
get_user_volume_accumulator_wsol_ata, BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR,
|
||||
SELL_DISCRIMINATOR,
|
||||
get_protocol_extra_fee_recipient_random, get_user_volume_accumulator_pda,
|
||||
get_user_volume_accumulator_quote_ata, get_user_volume_accumulator_wsol_ata,
|
||||
},
|
||||
trading::{
|
||||
common::wsol_manager,
|
||||
@@ -76,6 +79,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
||||
creator = params_coin_creator_vault_authority;
|
||||
}
|
||||
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
|
||||
|
||||
let (mut token_amount, sol_amount) = if quote_is_wsol_or_usdc {
|
||||
let result = buy_quote_input_internal(
|
||||
@@ -84,6 +88,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
&creator,
|
||||
cashback_fee_bps,
|
||||
)
|
||||
.unwrap();
|
||||
// base_amount_out, max_quote_amount_in
|
||||
@@ -95,6 +100,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
&creator,
|
||||
cashback_fee_bps,
|
||||
)
|
||||
.unwrap();
|
||||
// min_quote_amount_out, base_amount_in
|
||||
@@ -166,7 +172,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
}
|
||||
|
||||
// Create buy instruction
|
||||
let mut accounts = Vec::with_capacity(23);
|
||||
let mut accounts = Vec::with_capacity(28);
|
||||
accounts.extend([
|
||||
AccountMeta::new(pool, false), // pool_id
|
||||
AccountMeta::new(params.payer.pubkey(), true), // user (signer)
|
||||
@@ -202,40 +208,51 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
accounts.push(AccountMeta::new(wsol_ata, false));
|
||||
}
|
||||
}
|
||||
// remainingAccounts: @pump-fun/pump-swap-sdk 要求末尾传 poolV2Pda(baseMint),勿删
|
||||
let pool_v2 = get_pool_v2_pda(&base_mint)
|
||||
.ok_or_else(|| anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint))?;
|
||||
accounts.push(AccountMeta::new_readonly(pool_v2, false));
|
||||
// `pool-v2` only when coin_creator ≠ default (@pump-fun/pump-swap-sdk remainingAccounts);
|
||||
// 否则多出的一格会把 buyback pubkey 错位,触发 BuybackFeeRecipientNotAuthorized(6053)。
|
||||
if protocol_params.coin_creator != Pubkey::default() {
|
||||
let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| {
|
||||
anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint)
|
||||
})?;
|
||||
accounts.push(AccountMeta::new_readonly(pool_v2, false));
|
||||
}
|
||||
// Trailing accounts: GlobalConfig.buyback_fee_recipients 中任 pubkey + quote ATA(与 pump-swap-sdk 静态池对齐;轮换时需查链上)。
|
||||
let protocol_extra = get_protocol_extra_fee_recipient_random();
|
||||
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
|
||||
accounts.push(AccountMeta::new(
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
|
||||
false,
|
||||
));
|
||||
|
||||
// Create instruction data(buy/buy_exact_quote_in 第三参数 track_volume: OptionBool,仅代币支持返现时传 Some(true);sell 仅两参数)
|
||||
let track_volume = if protocol_params.is_cashback_coin { [1u8, 1u8] } else { [1u8, 0u8] }; // Some(true) / Some(false)
|
||||
let data: Vec<u8> = if quote_is_wsol_or_usdc {
|
||||
let mut buf = [0u8; 26];
|
||||
if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
// buy / buy_exact_quote_in:栈上 `[u8;25]` + `new_with_bytes`,避免每笔 `Vec` 堆分配。
|
||||
let track_volume: u8 = if protocol_params.is_cashback_coin { 1 } else { 0 };
|
||||
if quote_is_wsol_or_usdc {
|
||||
let ix_data = if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let min_base_amount_out = crate::utils::calc::common::calculate_with_slippage_sell(
|
||||
token_amount,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
buf[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_DISCRIMINATOR);
|
||||
buf[8..16].copy_from_slice(¶ms.input_amount.unwrap_or(0).to_le_bytes());
|
||||
buf[16..24].copy_from_slice(&min_base_amount_out.to_le_bytes());
|
||||
buf[24..26].copy_from_slice(&track_volume);
|
||||
encode_pumpswap_buy_exact_quote_in_ix_data(
|
||||
params.input_amount.unwrap_or(0),
|
||||
min_base_amount_out,
|
||||
track_volume,
|
||||
)
|
||||
} else {
|
||||
buf[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
buf[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
buf[16..24].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
buf[24..26].copy_from_slice(&track_volume);
|
||||
}
|
||||
buf.to_vec()
|
||||
encode_pumpswap_buy_ix_data(token_amount, sol_amount, track_volume)
|
||||
};
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
} else {
|
||||
let mut buf = [0u8; 24];
|
||||
buf[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
buf[8..16].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
buf[16..24].copy_from_slice(&token_amount.to_le_bytes());
|
||||
buf.to_vec()
|
||||
};
|
||||
|
||||
instructions.push(Instruction { program_id: accounts::AMM_PROGRAM, accounts, data });
|
||||
let ix_data = encode_pumpswap_sell_ix_data(sol_amount, token_amount);
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
}
|
||||
if close_wsol_ata {
|
||||
// Close wSOL ATA account, reclaim rent
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
@@ -292,6 +309,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
||||
creator = params_coin_creator_vault_authority;
|
||||
}
|
||||
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
|
||||
|
||||
let (token_amount, mut sol_amount) = if quote_is_wsol_or_usdc {
|
||||
let result = sell_base_input_internal(
|
||||
@@ -300,6 +318,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
&creator,
|
||||
cashback_fee_bps,
|
||||
)
|
||||
.unwrap();
|
||||
// base_amount_in, min_quote_amount_out
|
||||
@@ -311,6 +330,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
&creator,
|
||||
cashback_fee_bps,
|
||||
)
|
||||
.unwrap();
|
||||
// max_quote_amount_in, base_amount_out
|
||||
@@ -359,7 +379,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
}
|
||||
|
||||
// Create sell instruction
|
||||
let mut accounts = Vec::with_capacity(23);
|
||||
let mut accounts = Vec::with_capacity(28);
|
||||
accounts.extend([
|
||||
AccountMeta::new(pool, false), // pool_id
|
||||
AccountMeta::new(params.payer.pubkey(), true), // user (signer)
|
||||
@@ -403,32 +423,27 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
accounts.push(AccountMeta::new(accumulator, false));
|
||||
}
|
||||
}
|
||||
// remainingAccounts: @pump-fun/pump-swap-sdk sell 要求末尾传 poolV2Pda(baseMint),勿删
|
||||
let pool_v2 = get_pool_v2_pda(&base_mint)
|
||||
.ok_or_else(|| anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint))?;
|
||||
accounts.push(AccountMeta::new_readonly(pool_v2, false));
|
||||
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 24];
|
||||
if quote_is_wsol_or_usdc {
|
||||
data[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
// base_amount_in
|
||||
data[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
// min_quote_amount_out
|
||||
data[16..24].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
} else {
|
||||
data[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
// base_amount_out
|
||||
data[8..16].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
// max_quote_amount_in
|
||||
data[16..24].copy_from_slice(&token_amount.to_le_bytes());
|
||||
if protocol_params.coin_creator != Pubkey::default() {
|
||||
let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| {
|
||||
anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint)
|
||||
})?;
|
||||
accounts.push(AccountMeta::new_readonly(pool_v2, false));
|
||||
}
|
||||
let protocol_extra = get_protocol_extra_fee_recipient_random();
|
||||
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
|
||||
accounts.push(AccountMeta::new(
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
|
||||
false,
|
||||
));
|
||||
|
||||
instructions.push(Instruction {
|
||||
program_id: accounts::AMM_PROGRAM,
|
||||
accounts,
|
||||
data: data.to_vec(),
|
||||
});
|
||||
// 栈数组 + `new_with_bytes`,避免 `data.to_vec()`。
|
||||
let ix_data = if quote_is_wsol_or_usdc {
|
||||
encode_pumpswap_sell_ix_data(token_amount, sol_amount)
|
||||
} else {
|
||||
encode_pumpswap_buy_two_args(sol_amount, token_amount)
|
||||
};
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::AMM_PROGRAM, &ix_data, accounts));
|
||||
|
||||
if close_wsol_ata {
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//! PumpSwap AMM `buy` / `buy_exact_quote_in` / `sell` instruction **data**(栈数组、无 `Vec` 分配)。
|
||||
|
||||
use crate::instruction::utils::pumpswap::{
|
||||
BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||
};
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_two_args(base_amount_out: u64, max_quote_amount_in: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&base_amount_out.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_quote_amount_in.to_le_bytes());
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_ix_data(
|
||||
base_amount_out: u64,
|
||||
max_quote_amount_in: u64,
|
||||
track_volume: u8,
|
||||
) -> [u8; 25] {
|
||||
let mut d = [0u8; 25];
|
||||
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&base_amount_out.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_quote_amount_in.to_le_bytes());
|
||||
d[24] = track_volume;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_exact_quote_in_ix_data(
|
||||
spendable_quote_in: u64,
|
||||
min_base_amount_out: u64,
|
||||
track_volume: u8,
|
||||
) -> [u8; 25] {
|
||||
let mut d = [0u8; 25];
|
||||
d[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&spendable_quote_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_base_amount_out.to_le_bytes());
|
||||
d[24] = track_volume;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_sell_ix_data(base_amount_in: u64, min_quote_amount_out: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&base_amount_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_quote_amount_out.to_le_bytes());
|
||||
d
|
||||
}
|
||||
+437
-257
@@ -1,3 +1,7 @@
|
||||
//! Pump.fun bonding-curve utilities (flat module): PDAs, fee `#2`, pump-fees `SharingConfig`, cold RPC.
|
||||
//!
|
||||
//! Hot swap ix assembly stays sync; async helpers at file bottom. Layout matches `@pump-fun/pump-sdk`.
|
||||
|
||||
use crate::common::{bonding_curve::BondingCurveAccount, SolanaRpcClient};
|
||||
use anyhow::anyhow;
|
||||
use rand::seq::IndexedRandom;
|
||||
@@ -7,212 +11,181 @@ use solana_sdk::{
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
// --- Aligned with official `@pump-fun/pump-sdk` (npm) ---
|
||||
// - `src/fees.ts` `getFeeRecipient(global, mayhemMode)` — fee recipient pools
|
||||
// - `src/bondingCurve.ts` `CURRENT_FEE_RECIPIENTS` / `getStaticRandomFeeRecipient`
|
||||
// - `src/sdk.ts` `BONDING_CURVE_NEW_SIZE` (151) + `extendAccountInstruction` — **not** called from the
|
||||
// trade hot path here (no RPC in `PumpFunInstructionBuilder`); use these helpers from a cold path if needed.
|
||||
|
||||
/// Minimum bonding curve account data length after protocol upgrades (`sdk.ts` `BONDING_CURVE_NEW_SIZE`).
|
||||
pub const PUMP_BONDING_CURVE_MIN_DATA_LEN: usize = 151;
|
||||
// --- seeds -------------------------------------------------------------
|
||||
|
||||
/// Anchor discriminator for `extend_account` (`pump.json`); same as `PumpSdk.extendAccountInstruction`.
|
||||
pub const EXTEND_ACCOUNT_DISCRIMINATOR: [u8; 8] = [234, 102, 194, 203, 150, 72, 62, 229];
|
||||
|
||||
/// Build `extend_account` for bonding curve (cold path / separate tx only — do not add RPC to hot-path builds).
|
||||
#[inline]
|
||||
pub fn extend_bonding_curve_account_instruction(bonding_curve: &Pubkey, user: &Pubkey) -> Instruction {
|
||||
Instruction {
|
||||
program_id: accounts::PUMPFUN,
|
||||
accounts: vec![
|
||||
AccountMeta::new(*bonding_curve, false),
|
||||
AccountMeta::new(*user, true),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
],
|
||||
data: EXTEND_ACCOUNT_DISCRIMINATOR.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
pub mod seeds {
|
||||
/// Seed for bonding curve PDAs
|
||||
pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve";
|
||||
/// Seed for bonding curve v2 PDA (required by program upgrade, readonly at end of account list)
|
||||
pub const BONDING_CURVE_V2_SEED: &[u8] = b"bonding-curve-v2";
|
||||
|
||||
/// Seed for creator vault PDAs
|
||||
pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault";
|
||||
|
||||
/// Seed for metadata PDAs
|
||||
pub const METADATA_SEED: &[u8] = b"metadata";
|
||||
|
||||
/// Seed for user volume accumulator PDAs
|
||||
pub const USER_VOLUME_ACCUMULATOR_SEED: &[u8] = b"user_volume_accumulator";
|
||||
|
||||
/// Seed for global volume accumulator PDAs
|
||||
pub const GLOBAL_VOLUME_ACCUMULATOR_SEED: &[u8] = b"global_volume_accumulator";
|
||||
|
||||
pub const FEE_CONFIG_SEED: &[u8] = b"fee_config";
|
||||
|
||||
/// `feeSharingConfig` PDA under pump-fees (`@pump-fun/pump-sdk` `feeSharingConfigPda`)
|
||||
pub const SHARING_CONFIG_SEED: &[u8] = b"sharing-config";
|
||||
/// Seed for bonding curve PDAs (`["bonding-curve", mint]`).
|
||||
pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve";
|
||||
/// Seed for bonding curve v2 PDA (`["bonding-curve-v2", mint]`).
|
||||
pub const BONDING_CURVE_V2_SEED: &[u8] = b"bonding-curve-v2";
|
||||
/// Creator vault PDA seeds prefix (`["creator-vault", authority]`).
|
||||
pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault";
|
||||
/// Metadata PDA seeds prefix.
|
||||
pub const METADATA_SEED: &[u8] = b"metadata";
|
||||
/// User volume accumulator for cashback / bonding-curve UX.
|
||||
pub const USER_VOLUME_ACCUMULATOR_SEED: &[u8] = b"user_volume_accumulator";
|
||||
/// Global volume accumulator.
|
||||
pub const GLOBAL_VOLUME_ACCUMULATOR_SEED: &[u8] = b"global_volume_accumulator";
|
||||
pub const FEE_CONFIG_SEED: &[u8] = b"fee_config";
|
||||
/// `feeSharingConfig` PDA under pump-fees (`feeSharingConfigPda`).
|
||||
pub const SHARING_CONFIG_SEED: &[u8] = b"sharing-config";
|
||||
}
|
||||
|
||||
|
||||
pub mod global_constants {
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000;
|
||||
|
||||
pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000;
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
pub const INITIAL_REAL_TOKEN_RESERVES: u64 = 793_100_000_000_000;
|
||||
pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000;
|
||||
pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000;
|
||||
pub const INITIAL_REAL_TOKEN_RESERVES: u64 = 793_100_000_000_000;
|
||||
pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000;
|
||||
pub const FEE_BASIS_POINTS: u64 = 95;
|
||||
pub const ENABLE_MIGRATE: bool = false;
|
||||
pub const POOL_MIGRATION_FEE: u64 = 15_000_001;
|
||||
pub const CREATOR_FEE: u64 = 30;
|
||||
pub const SCALE: u64 = 1_000_000;
|
||||
pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
|
||||
pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL;
|
||||
|
||||
pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000;
|
||||
pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV");
|
||||
pub const FEE_RECIPIENT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: FEE_RECIPIENT,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
};
|
||||
|
||||
pub const FEE_BASIS_POINTS: u64 = 95;
|
||||
pub const MAYHEM_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"),
|
||||
pubkey!("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"),
|
||||
pubkey!("8SBKzEQU4nLSzcwF4a74F2iaUDQyTfjGndn6qUWBnrpR"),
|
||||
pubkey!("4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH"),
|
||||
pubkey!("8sNeir4QsLsJdYpc9RZacohhK1Y5FLU3nC5LXgYB4aa6"),
|
||||
pubkey!("Fh9HmeLNUMVCvejxCtCL2DbYaRyBFVJ5xrWkLnMH6fdk"),
|
||||
pubkey!("463MEnMeGyJekNZFQSTUABBEbLnvMTALbT6ZmsxAbAdq"),
|
||||
pubkey!("6AUH3WEHucYZyC61hqpqYUWVto5qA5hjHuNQ32GNnNxA"),
|
||||
];
|
||||
pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0];
|
||||
pub const MAYHEM_FEE_RECIPIENT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: MAYHEM_FEE_RECIPIENT,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
};
|
||||
|
||||
pub const ENABLE_MIGRATE: bool = false;
|
||||
pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf");
|
||||
pub const GLOBAL_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: GLOBAL_ACCOUNT,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
pub const POOL_MIGRATION_FEE: u64 = 15_000_001;
|
||||
pub const AUTHORITY: Pubkey = pubkey!("FFWtrEQ4B4PKQoVuHYzZq8FabGkVatYzDpEVHsK5rrhF");
|
||||
pub const WITHDRAW_AUTHORITY: Pubkey = pubkey!("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg");
|
||||
|
||||
pub const CREATOR_FEE: u64 = 30;
|
||||
pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ");
|
||||
pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX");
|
||||
pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz");
|
||||
pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY");
|
||||
pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM");
|
||||
pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz");
|
||||
pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP");
|
||||
|
||||
pub const SCALE: u64 = 1_000_000; // 10^6 for token decimals
|
||||
|
||||
pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; // 10^9 for solana lamports
|
||||
|
||||
pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL; // ~ 85 SOL
|
||||
|
||||
/// Public key for the fee recipient
|
||||
pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV");
|
||||
pub const FEE_RECIPIENT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: FEE_RECIPIENT,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
};
|
||||
|
||||
/// Mayhem fee recipients (pump-public-docs: use any one randomly)
|
||||
pub const MAYHEM_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"),
|
||||
pubkey!("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"),
|
||||
pubkey!("8SBKzEQU4nLSzcwF4a74F2iaUDQyTfjGndn6qUWBnrpR"),
|
||||
pubkey!("4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH"),
|
||||
pubkey!("8sNeir4QsLsJdYpc9RZacohhK1Y5FLU3nC5LXgYB4aa6"),
|
||||
pubkey!("Fh9HmeLNUMVCvejxCtCL2DbYaRyBFVJ5xrWkLnMH6fdk"),
|
||||
pubkey!("463MEnMeGyJekNZFQSTUABBEbLnvMTALbT6ZmsxAbAdq"),
|
||||
pubkey!("6AUH3WEHucYZyC61hqpqYUWVto5qA5hjHuNQ32GNnNxA"),
|
||||
];
|
||||
pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0];
|
||||
pub const MAYHEM_FEE_RECIPIENT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: MAYHEM_FEE_RECIPIENT,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
};
|
||||
|
||||
/// Public key for the global PDA
|
||||
pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf");
|
||||
pub const GLOBAL_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: GLOBAL_ACCOUNT,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
/// Public key for the authority
|
||||
pub const AUTHORITY: Pubkey = pubkey!("FFWtrEQ4B4PKQoVuHYzZq8FabGkVatYzDpEVHsK5rrhF");
|
||||
|
||||
/// Public key for the withdraw authority
|
||||
pub const WITHDRAW_AUTHORITY: Pubkey = pubkey!("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg");
|
||||
|
||||
pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"); // Pump.fun AMM: Protocol Fee 1
|
||||
pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"); // Pump.fun AMM: Protocol Fee 2
|
||||
pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"); // Pump.fun AMM: Protocol Fee 3
|
||||
pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"); // Pump.fun AMM: Protocol Fee 4
|
||||
pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"); // Pump.fun AMM: Protocol Fee 5
|
||||
pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"); // Pump.fun AMM: Protocol Fee 6
|
||||
pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP");
|
||||
// Pump.fun AMM: Protocol Fee 7
|
||||
pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
|
||||
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
|
||||
pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
|
||||
pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
|
||||
pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
|
||||
pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
|
||||
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
|
||||
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
|
||||
];
|
||||
}
|
||||
|
||||
/// Constants related to program accounts and authorities
|
||||
|
||||
pub mod accounts {
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
/// Public key for the Pump.fun program
|
||||
pub const PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
||||
|
||||
/// Public key for the MPL Token Metadata program
|
||||
pub const MPL_TOKEN_METADATA: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
/// Authority for program events
|
||||
pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1");
|
||||
pub const PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
||||
pub const MPL_TOKEN_METADATA: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");
|
||||
pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1");
|
||||
pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey =
|
||||
pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
||||
pub const AMM_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
|
||||
pub const FEE_PROGRAM: Pubkey = pubkey!("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ");
|
||||
pub const GLOBAL_VOLUME_ACCUMULATOR: Pubkey =
|
||||
pubkey!("Hq2wp8uJ9jCPsYgNHex8RtqdvMPfVGoYwjvF1ATiwn2Y");
|
||||
pub const FEE_CONFIG: Pubkey = pubkey!("8Wf5TiAheLUqBrKXeYg2JtAFFMWtKdG2BSFgqUcPVwTt");
|
||||
|
||||
/// Associated Token Program ID
|
||||
pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey =
|
||||
pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
||||
pub const PUMPFUN_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: PUMPFUN,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
pub const AMM_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
|
||||
pub const EVENT_AUTHORITY_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: EVENT_AUTHORITY,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
pub const FEE_PROGRAM: Pubkey = pubkey!("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ");
|
||||
pub const FEE_PROGRAM_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: FEE_PROGRAM,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
pub const GLOBAL_VOLUME_ACCUMULATOR: Pubkey =
|
||||
pubkey!("Hq2wp8uJ9jCPsYgNHex8RtqdvMPfVGoYwjvF1ATiwn2Y");
|
||||
pub const GLOBAL_VOLUME_ACCUMULATOR_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: GLOBAL_VOLUME_ACCUMULATOR,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
};
|
||||
|
||||
pub const FEE_CONFIG: Pubkey = pubkey!("8Wf5TiAheLUqBrKXeYg2JtAFFMWtKdG2BSFgqUcPVwTt");
|
||||
|
||||
// META
|
||||
pub const PUMPFUN_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: PUMPFUN,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
pub const EVENT_AUTHORITY_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: EVENT_AUTHORITY,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
pub const FEE_PROGRAM_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: FEE_PROGRAM,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
pub const GLOBAL_VOLUME_ACCUMULATOR_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: GLOBAL_VOLUME_ACCUMULATOR,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
};
|
||||
|
||||
pub const FEE_CONFIG_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: FEE_CONFIG,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
pub const FEE_CONFIG_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: FEE_CONFIG,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Instruction discriminators for PumpFun program
|
||||
|
||||
// --- Anchor / layout constants ---------------------------------------
|
||||
|
||||
|
||||
/// Minimum bonding curve account data length (`sdk.ts` `BONDING_CURVE_NEW_SIZE`).
|
||||
pub const PUMP_BONDING_CURVE_MIN_DATA_LEN: usize = 151;
|
||||
|
||||
pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
|
||||
pub const BUY_EXACT_SOL_IN_DISCRIMINATOR: [u8; 8] = [56, 252, 116, 8, 158, 223, 205, 95];
|
||||
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
||||
|
||||
/// Check if a pubkey is one of the Mayhem fee recipients
|
||||
pub const EXTEND_ACCOUNT_DISCRIMINATOR: [u8; 8] = [234, 102, 194, 203, 150, 72, 62, 229];
|
||||
|
||||
pub const SHARING_CONFIG_ACCOUNT_DISCRIMINATOR: [u8; 8] = [216, 74, 9, 0, 56, 140, 93, 75];
|
||||
|
||||
pub(crate) const SHARING_CONFIG_STATUS_ACTIVE: u8 = 1;
|
||||
|
||||
|
||||
// --- Fee recipient pools -----------------------------------------------
|
||||
|
||||
#[inline]
|
||||
pub fn is_mayhem_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
global_constants::MAYHEM_FEE_RECIPIENTS.iter().any(|p| p == pubkey)
|
||||
global_constants::MAYHEM_FEE_RECIPIENTS.contains(pubkey)
|
||||
}
|
||||
|
||||
/// Check if a pubkey is a Pump.fun AMM protocol fee recipient (PUMPFUN_AMM_FEE_1..7)
|
||||
#[inline]
|
||||
pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
pubkey == &global_constants::PUMPFUN_AMM_FEE_1
|
||||
@@ -224,18 +197,58 @@ pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_7
|
||||
}
|
||||
|
||||
/// Mayhem: random among `Global.reservedFeeRecipient` + `Global.reservedFeeRecipients` (`fees.ts` `getFeeRecipient` when `mayhemMode === true`).
|
||||
/// Uses hardcoded `MAYHEM_FEE_RECIPIENTS`; prefer gRPC/event `PumpFunParams.fee_recipient` when set.
|
||||
#[inline]
|
||||
pub fn is_standard_bonding_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
*pubkey == global_constants::FEE_RECIPIENT || is_amm_fee_recipient(pubkey)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn reconcile_mayhem_mode_for_trade(
|
||||
mayhem_from_event: Option<bool>,
|
||||
fee_recipient: &Pubkey,
|
||||
) -> bool {
|
||||
if *fee_recipient == Pubkey::default() {
|
||||
return mayhem_from_event.unwrap_or(false);
|
||||
}
|
||||
let fee_m = is_mayhem_fee_recipient(fee_recipient);
|
||||
let fee_s = is_standard_bonding_fee_recipient(fee_recipient);
|
||||
match mayhem_from_event {
|
||||
Some(log_m) => {
|
||||
if fee_m && !log_m {
|
||||
true
|
||||
} else if fee_s && log_m && !fee_m {
|
||||
false
|
||||
} else {
|
||||
log_m
|
||||
}
|
||||
}
|
||||
None => fee_m,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn fee_recipient_ok_for_bonding_curve_mode(pk: &Pubkey, is_mayhem_mode: bool) -> bool {
|
||||
let is_m = is_mayhem_fee_recipient(pk);
|
||||
let is_s = is_standard_bonding_fee_recipient(pk);
|
||||
if is_mayhem_mode {
|
||||
!(is_s && !is_m)
|
||||
} else {
|
||||
!(is_m && !is_s)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
|
||||
let recipient = *global_constants::MAYHEM_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&global_constants::MAYHEM_FEE_RECIPIENTS[0]);
|
||||
AccountMeta { pubkey: recipient, is_signer: false, is_writable: true }
|
||||
AccountMeta {
|
||||
pubkey: recipient,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-mayhem: random among `Global::fee_recipient` + `Global::fee_recipients[0..7]`.
|
||||
/// Same pubkey set as `bondingCurve.ts` `CURRENT_FEE_RECIPIENTS` / `getStaticRandomFeeRecipient` and `fees.ts` `getFeeRecipient` when `mayhemMode === false`.
|
||||
#[inline]
|
||||
pub fn get_standard_fee_recipient_meta_random() -> AccountMeta {
|
||||
const POOL: &[Pubkey] = &[
|
||||
@@ -258,12 +271,20 @@ pub fn get_standard_fee_recipient_meta_random() -> AccountMeta {
|
||||
}
|
||||
}
|
||||
|
||||
/// 账户 #2 fee recipient:优先使用 gRPC/ShredStream 解析值(同笔 create_v2+buy 的 `observed_fee_recipient` 或 `tradeEvent.feeRecipient`);未提供时按 mayhem 从静态池随机。
|
||||
#[inline]
|
||||
pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) -> AccountMeta {
|
||||
if from_stream != Pubkey::default() {
|
||||
pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
|
||||
*global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pump_fun_fee_recipient_meta(observed_fee_recipient: Pubkey, is_mayhem_mode: bool) -> AccountMeta {
|
||||
let trust_observation = observed_fee_recipient != Pubkey::default()
|
||||
&& fee_recipient_ok_for_bonding_curve_mode(&observed_fee_recipient, is_mayhem_mode);
|
||||
if trust_observation {
|
||||
AccountMeta {
|
||||
pubkey: from_stream,
|
||||
pubkey: observed_fee_recipient,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
}
|
||||
@@ -274,12 +295,29 @@ pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) ->
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Symbol;
|
||||
// --- Extend bonding curve (cold path) --------------------------------
|
||||
|
||||
impl Symbol {
|
||||
pub const SOLANA: &'static str = "solana";
|
||||
#[inline]
|
||||
pub fn extend_bonding_curve_account_instruction(
|
||||
bonding_curve: &Pubkey,
|
||||
user: &Pubkey,
|
||||
) -> Instruction {
|
||||
Instruction::new_with_bytes(
|
||||
accounts::PUMPFUN,
|
||||
&EXTEND_ACCOUNT_DISCRIMINATOR,
|
||||
vec![
|
||||
AccountMeta::new(*bonding_curve, false),
|
||||
AccountMeta::new(*user, true),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// --- Cached PDAs + creator_vault resolve ------------------------------
|
||||
|
||||
#[inline]
|
||||
pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
@@ -287,13 +325,11 @@ pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::BONDING_CURVE_SEED, mint.as_ref()];
|
||||
let program_id: &Pubkey = &accounts::PUMPFUN;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Bonding curve v2 PDA (seeds: ["bonding-curve-v2", mint]). Required at end of buy/sell/buy_exact_sol_in accounts.
|
||||
#[inline]
|
||||
pub fn get_bonding_curve_v2_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
@@ -301,8 +337,7 @@ pub fn get_bonding_curve_v2_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::BONDING_CURVE_V2_SEED, mint.as_ref()];
|
||||
let program_id: &Pubkey = &accounts::PUMPFUN;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -312,7 +347,6 @@ pub fn get_creator(creator_vault_pda: &Pubkey) -> Pubkey {
|
||||
if creator_vault_pda.eq(&Pubkey::default()) {
|
||||
Pubkey::default()
|
||||
} else {
|
||||
// Fast check against cached default creator vault
|
||||
static DEFAULT_CREATOR_VAULT: std::sync::LazyLock<Option<Pubkey>> =
|
||||
std::sync::LazyLock::new(|| get_creator_vault_pda(&Pubkey::default()));
|
||||
match DEFAULT_CREATOR_VAULT.as_ref() {
|
||||
@@ -329,24 +363,25 @@ pub fn get_creator_vault_pda(creator: &Pubkey) -> Option<Pubkey> {
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::CREATOR_VAULT_SEED, creator.as_ref()];
|
||||
let program_id: &Pubkey = &accounts::PUMPFUN;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// `feeSharingConfig` PDA per mint (`pump-sdk` `feeSharingConfigPda` → `pump-fees` program).
|
||||
#[inline]
|
||||
pub fn get_fee_sharing_config_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
Pubkey::try_find_program_address(
|
||||
&[seeds::SHARING_CONFIG_SEED, mint.as_ref()],
|
||||
&accounts::FEE_PROGRAM,
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
crate::common::fast_fn::PdaCacheKey::PumpFunFeeSharingConfig(*mint),
|
||||
|| {
|
||||
Pubkey::try_find_program_address(
|
||||
&[seeds::SHARING_CONFIG_SEED, mint.as_ref()],
|
||||
&accounts::FEE_PROGRAM,
|
||||
)
|
||||
.map(|(p, _)| p)
|
||||
},
|
||||
)
|
||||
.map(|(p, _)| p)
|
||||
}
|
||||
|
||||
/// PDA of `["creator-vault", Pubkey::default()]`. Never use as a real vault — it is only produced when
|
||||
/// `creator` was missing and code incorrectly derived a vault; on-chain this fails with Anchor 2006.
|
||||
#[inline]
|
||||
pub fn phantom_default_creator_vault() -> Pubkey {
|
||||
solana_sdk::pubkey!("2DR3iqRPVThyRLVJnwjPW1qiGWrp8RUFfHVjMbZyhdNc")
|
||||
@@ -357,52 +392,47 @@ pub fn is_phantom_default_creator_vault(pk: &Pubkey) -> bool {
|
||||
*pk == phantom_default_creator_vault()
|
||||
}
|
||||
|
||||
/// Resolve `creator_vault` for Pump buy/sell account #10.
|
||||
///
|
||||
/// - If `creator` is **missing** in the outer trade-event borsh (`Pubkey::default()`) but
|
||||
/// `creator_vault` was filled from **instruction accounts** (e.g. `fill_trade_accounts` index 9),
|
||||
/// **trust that vault** — unless it equals [`phantom_default_creator_vault`] (bad derivation / cache).
|
||||
/// - If event `creator_vault` is **missing** → [`get_creator_vault_pda`]`(creator)` (never `PDA(default)`).
|
||||
/// - If it **matches** `PDA(creator)` or `PDA(fee_sharing_config(mint))` → use it (fast path, matches ix).
|
||||
/// - If it **does not match** either (e.g. stale vault but `creator` from tradeEvent is correct) → use
|
||||
/// [`get_creator_vault_pda`]`(creator)` so seeds match on-chain bonding curve (fixes 2006 Left≠Right).
|
||||
#[inline]
|
||||
pub fn resolve_creator_vault_for_ix(
|
||||
creator: &Pubkey,
|
||||
creator_vault_from_event: Pubkey,
|
||||
mint: &Pubkey,
|
||||
) -> Option<Pubkey> {
|
||||
resolve_creator_vault_for_ix_with_fee_sharing(creator, creator_vault_from_event, mint, None)
|
||||
}
|
||||
|
||||
/// Resolves Pump.fun bonding-curve buy/sell **account `#10` (`creator_vault`)** for ix assembly.
|
||||
///
|
||||
/// **Priority (highest first)**
|
||||
/// 1. **Explicit `creator_vault`** from ix / parser / cached observation when non-default and not the phantom
|
||||
/// sentinel — **always used as-is** (no remap to [`get_creator_vault_pda`] from `creator`);
|
||||
/// fee-sharing / multi-party layouts rely on upstream passing the vault the program expects (`pfee…` / `update_fee_shares`).
|
||||
/// 2. If ix vault missing: optional `fee_sharing_creator_vault_if_active` hint (non-default, non-phantom).
|
||||
/// 3. Else: [`get_creator_vault_pda`] from `creator` when `creator` is known.
|
||||
#[inline]
|
||||
pub fn resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
creator: &Pubkey,
|
||||
creator_vault_from_event: Pubkey,
|
||||
_mint: &Pubkey,
|
||||
fee_sharing_creator_vault_if_active: Option<Pubkey>,
|
||||
) -> Option<Pubkey> {
|
||||
let phantom = phantom_default_creator_vault();
|
||||
|
||||
if creator_vault_from_event != Pubkey::default() && creator_vault_from_event != phantom {
|
||||
return Some(creator_vault_from_event);
|
||||
}
|
||||
|
||||
if let Some(v) = fee_sharing_creator_vault_if_active {
|
||||
if v != Pubkey::default() && v != phantom {
|
||||
return Some(v);
|
||||
}
|
||||
}
|
||||
|
||||
if *creator == Pubkey::default() {
|
||||
if creator_vault_from_event == Pubkey::default() {
|
||||
return None;
|
||||
}
|
||||
if creator_vault_from_event == phantom {
|
||||
return None;
|
||||
}
|
||||
return Some(creator_vault_from_event);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Real creator: poisoned cache may hold phantom vault — always remap to PDA(creator).
|
||||
if creator_vault_from_event == phantom {
|
||||
return get_creator_vault_pda(creator);
|
||||
}
|
||||
|
||||
let v_derived = get_creator_vault_pda(creator)?;
|
||||
if creator_vault_from_event == Pubkey::default() {
|
||||
return Some(v_derived);
|
||||
}
|
||||
if creator_vault_from_event == v_derived {
|
||||
return Some(creator_vault_from_event);
|
||||
}
|
||||
if let Some(sharing) = get_fee_sharing_config_pda(mint) {
|
||||
let v_sharing = get_creator_vault_pda(&sharing)?;
|
||||
if creator_vault_from_event == v_sharing {
|
||||
return Some(creator_vault_from_event);
|
||||
}
|
||||
}
|
||||
Some(v_derived)
|
||||
get_creator_vault_pda(creator)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -410,34 +440,13 @@ pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
crate::common::fast_fn::PdaCacheKey::PumpFunUserVolume(*user),
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::USER_VOLUME_ACCUMULATOR_SEED, user.as_ref()];
|
||||
let seed: &[&[u8]; 2] = &[seeds::USER_VOLUME_ACCUMULATOR_SEED, user.as_ref()];
|
||||
let program_id: &Pubkey = &accounts::PUMPFUN;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
Pubkey::try_find_program_address(seed, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn fetch_bonding_curve_account(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<(Arc<BondingCurveAccount>, Pubkey), anyhow::Error> {
|
||||
let bonding_curve_pda: Pubkey =
|
||||
get_bonding_curve_pda(mint).ok_or(anyhow!("Bonding curve not found"))?;
|
||||
|
||||
let account = rpc.get_account(&bonding_curve_pda).await?;
|
||||
if account.data.is_empty() {
|
||||
return Err(anyhow!("Bonding curve not found"));
|
||||
}
|
||||
|
||||
let bonding_curve =
|
||||
solana_sdk::borsh1::try_from_slice_unchecked::<BondingCurveAccount>(&account.data[8..])
|
||||
.map_err(|e| anyhow::anyhow!("Failed to deserialize bonding curve account: {}", e))?;
|
||||
|
||||
Ok((Arc::new(bonding_curve), bonding_curve_pda))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_buy_price(
|
||||
amount: u64,
|
||||
@@ -458,9 +467,72 @@ pub fn get_buy_price(
|
||||
s_u64.min(real_token_reserves)
|
||||
}
|
||||
|
||||
|
||||
// --- RPC -------------------------------------------------------------
|
||||
|
||||
#[inline]
|
||||
pub async fn fetch_fee_sharing_creator_vault_if_active(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Option<Pubkey>, anyhow::Error> {
|
||||
let Some(config_pda) = get_fee_sharing_config_pda(mint) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let acc = match rpc.get_account(&config_pda).await {
|
||||
Ok(a) => a,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
if acc.owner != accounts::FEE_PROGRAM {
|
||||
return Ok(None);
|
||||
}
|
||||
let d = acc.data.as_slice();
|
||||
if d.len() < 43 || d[..8] != SHARING_CONFIG_ACCOUNT_DISCRIMINATOR {
|
||||
return Ok(None);
|
||||
}
|
||||
if d[10] != SHARING_CONFIG_STATUS_ACTIVE {
|
||||
return Ok(None);
|
||||
}
|
||||
let mint_on_chain = Pubkey::new_from_array(
|
||||
d[11..43]
|
||||
.try_into()
|
||||
.map_err(|_| anyhow!("SharingConfig mint slice"))?,
|
||||
);
|
||||
if mint_on_chain != *mint {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(get_creator_vault_pda(&config_pda))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn fetch_bonding_curve_account(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<(Arc<BondingCurveAccount>, Pubkey), anyhow::Error> {
|
||||
let bonding_curve_pda: Pubkey =
|
||||
get_bonding_curve_pda(mint).ok_or_else(|| anyhow!("Bonding curve not found"))?;
|
||||
|
||||
let account = rpc.get_account(&bonding_curve_pda).await?;
|
||||
if account.data.is_empty() {
|
||||
return Err(anyhow!("Bonding curve not found"));
|
||||
}
|
||||
|
||||
let bonding_curve =
|
||||
solana_sdk::borsh1::try_from_slice_unchecked::<BondingCurveAccount>(&account.data[8..])
|
||||
.map_err(|e| anyhow::anyhow!("Failed to deserialize bonding curve account: {}", e))?;
|
||||
|
||||
Ok((Arc::new(bonding_curve), bonding_curve_pda))
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::{
|
||||
global_constants,
|
||||
phantom_default_creator_vault, pump_fun_fee_recipient_meta,
|
||||
reconcile_mayhem_mode_for_trade, resolve_creator_vault_for_ix,
|
||||
resolve_creator_vault_for_ix_with_fee_sharing, *,
|
||||
};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
#[test]
|
||||
@@ -497,7 +569,11 @@ mod tests {
|
||||
#[test]
|
||||
fn default_creator_yields_fixed_creator_vault() {
|
||||
let v = get_creator_vault_pda(&Pubkey::default()).unwrap();
|
||||
assert_eq!(v, phantom_default_creator_vault(), "phantom vault constant must match PDA(default creator)");
|
||||
assert_eq!(
|
||||
v,
|
||||
phantom_default_creator_vault(),
|
||||
"phantom vault constant must match PDA(default creator)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -521,11 +597,53 @@ mod tests {
|
||||
fn resolve_rejects_phantom_vault_when_creator_borsh_is_default() {
|
||||
let mint = Pubkey::new_unique();
|
||||
assert_eq!(
|
||||
resolve_creator_vault_for_ix(&Pubkey::default(), phantom_default_creator_vault(), &mint),
|
||||
resolve_creator_vault_for_ix(
|
||||
&Pubkey::default(),
|
||||
phantom_default_creator_vault(),
|
||||
&mint,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_prefers_creator_pda_ix_vault_even_when_fee_sharing_hint_differs() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let v_derived = get_creator_vault_pda(&creator).unwrap();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
assert_ne!(v_derived, vs);
|
||||
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&creator,
|
||||
v_derived,
|
||||
&mint,
|
||||
Some(vs),
|
||||
);
|
||||
assert_eq!(
|
||||
resolved,
|
||||
Some(v_derived),
|
||||
"observed ix uses PDA(creator); stale hint must not override → wrong vault / Anchor 2006"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_trusts_ix_fee_sharing_vault_when_creator_known() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
let creator_vault = get_creator_vault_pda(&creator).unwrap();
|
||||
assert_ne!(vs, creator_vault);
|
||||
let resolved =
|
||||
resolve_creator_vault_for_ix_with_fee_sharing(&creator, vs, &mint, Some(vs));
|
||||
assert_eq!(
|
||||
resolved,
|
||||
Some(vs),
|
||||
"explicit ix creator_vault (e.g. fee-sharing #8) wins; no remap to PDA(creator)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_remaps_phantom_vault_when_creator_known() {
|
||||
let creator = Pubkey::new_unique();
|
||||
@@ -536,4 +654,66 @@ mod tests {
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_ix_vault_always_wins_when_non_default_even_if_creator_pda_differs() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let v_derived = get_creator_vault_pda(&creator).unwrap();
|
||||
let ix_other = Pubkey::new_unique();
|
||||
assert_ne!(ix_other, v_derived);
|
||||
let resolved =
|
||||
resolve_creator_vault_for_ix_with_fee_sharing(&creator, ix_other, &mint, None);
|
||||
assert_eq!(resolved, Some(ix_other));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_fee_sharing_ix_vault_used_as_is_even_without_hint() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let v_derived = get_creator_vault_pda(&creator).unwrap();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
assert_ne!(v_derived, vs);
|
||||
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(&creator, vs, &mint, None);
|
||||
assert_eq!(resolved, Some(vs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_falls_back_to_fee_sharing_hint_when_ix_vault_placeholder() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&creator,
|
||||
Pubkey::default(),
|
||||
&mint,
|
||||
Some(vs),
|
||||
);
|
||||
assert_eq!(resolved, Some(vs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mayhem_prefers_fee_when_log_says_false_but_fee_is_mayhem_pool() {
|
||||
let fee = global_constants::MAYHEM_FEE_RECIPIENTS[0];
|
||||
assert!(reconcile_mayhem_mode_for_trade(Some(false), &fee));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mayhem_prefers_fee_when_log_says_true_but_fee_is_standard_pool() {
|
||||
let fee = global_constants::PUMPFUN_AMM_FEE_4;
|
||||
assert!(!reconcile_mayhem_mode_for_trade(Some(true), &fee));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_fee_meta_rejects_standard_fee_when_building_mayhem_ix() {
|
||||
let fee = global_constants::PUMPFUN_AMM_FEE_4;
|
||||
let m = pump_fun_fee_recipient_meta(fee, true);
|
||||
assert!(
|
||||
global_constants::MAYHEM_FEE_RECIPIENTS.contains(&m.pubkey),
|
||||
"expected fallback to mayhem pool, got {}",
|
||||
m.pubkey
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,20 @@ pub mod accounts {
|
||||
/// Default Mayhem fee recipient (first of MAYHEM_FEE_RECIPIENTS)
|
||||
pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0];
|
||||
|
||||
/// Buyback trailing fee recipients (`GlobalConfig.buyback_fee_recipients` on Pump AMM).
|
||||
/// Must match one of these for the pubkey passed after optional `pool-v2` (`@pump-fun/pump-swap-sdk` `getBuybackFeeRecipient`).
|
||||
/// Static mirror of pump-public-docs; if protocol rotates configs, decode global_config from RPC.
|
||||
pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
|
||||
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
|
||||
pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
|
||||
pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
|
||||
pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
|
||||
pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
|
||||
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
|
||||
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
|
||||
];
|
||||
|
||||
// META
|
||||
|
||||
pub const GLOBAL_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
|
||||
@@ -171,6 +185,14 @@ pub fn get_mayhem_fee_recipient_random() -> (Pubkey, AccountMeta) {
|
||||
(recipient, meta)
|
||||
}
|
||||
|
||||
/// Random entry from [`accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS`] (readonly; paired with [`fee_recipient_ata`] as last account).
|
||||
#[inline]
|
||||
pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
|
||||
*accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
|
||||
}
|
||||
|
||||
/// Pool v2 PDA (seeds: ["pool-v2", base_mint]). Required at end of buy/sell/buy_exact_quote_in accounts.
|
||||
#[inline]
|
||||
pub fn get_pool_v2_pda(base_mint: &Pubkey) -> Option<Pubkey> {
|
||||
|
||||
+7
-1165
File diff suppressed because it is too large
Load Diff
@@ -447,23 +447,23 @@ impl BranchOptimizer {
|
||||
|
||||
/// Prefetch: load cache line at ptr into L1. Caller must ensure ptr is valid, read-only, no concurrent write. 预取:将 ptr 所在缓存行加载到 L1;调用方需保证有效、只读、无并发写。
|
||||
#[inline(always)]
|
||||
pub unsafe fn prefetch_read_data<T>(ptr: *const T) {
|
||||
pub unsafe fn prefetch_read_data<T>(_ptr: *const T) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::_mm_prefetch;
|
||||
use std::arch::x86_64::_MM_HINT_T0;
|
||||
_mm_prefetch(ptr as *const i8, _MM_HINT_T0);
|
||||
_mm_prefetch(_ptr as *const i8, _MM_HINT_T0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Prefetch for write (T1 hint). 写预取(T1 提示)。
|
||||
#[inline(always)]
|
||||
pub unsafe fn prefetch_write_data<T>(ptr: *const T) {
|
||||
pub unsafe fn prefetch_write_data<T>(_ptr: *const T) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::_mm_prefetch;
|
||||
use std::arch::x86_64::_MM_HINT_T1;
|
||||
_mm_prefetch(ptr as *const i8, _MM_HINT_T1);
|
||||
_mm_prefetch(_ptr as *const i8, _MM_HINT_T1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-5
@@ -27,6 +27,8 @@ pub enum AstralaneBackend {
|
||||
Http {
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
/// Mirrors global `mev_protection`: adds `mev-protect=true` on HTTP sends (QUIC uses :9000 instead).
|
||||
mev_http: bool,
|
||||
http_client: Client,
|
||||
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
|
||||
stop_ping: Arc<AtomicBool>,
|
||||
@@ -77,8 +79,8 @@ impl SwqosClientTrait for AstralaneClient {
|
||||
}
|
||||
|
||||
impl AstralaneClient {
|
||||
/// 使用 HTTP(irisb)提交。
|
||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||
/// HTTP 提交:`/iris`(Plain)或 `/irisb`(Binary),由 `endpoint` URL 路径区分;`mev_http` 为 true 时附加 `mev-protect=true`。
|
||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String, mev_http: bool) -> Self {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = default_http_client_builder().build().unwrap();
|
||||
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
|
||||
@@ -89,6 +91,7 @@ impl AstralaneClient {
|
||||
backend: AstralaneBackend::Http {
|
||||
endpoint,
|
||||
auth_token,
|
||||
mev_http,
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
@@ -119,6 +122,7 @@ impl AstralaneClient {
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
..
|
||||
} => {
|
||||
let endpoint = endpoint.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
@@ -182,10 +186,14 @@ impl AstralaneClient {
|
||||
.map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
|
||||
|
||||
match &self.backend {
|
||||
AstralaneBackend::Http { endpoint, auth_token, http_client, .. } => {
|
||||
let response = http_client
|
||||
AstralaneBackend::Http { endpoint, auth_token, mev_http, http_client, .. } => {
|
||||
let mut req = http_client
|
||||
.post(endpoint)
|
||||
.query(&[("api-key", auth_token.as_str()), ("method", "sendTransaction")])
|
||||
.query(&[("api-key", auth_token.as_str()), ("method", "sendTransaction")]);
|
||||
if *mev_http {
|
||||
req = req.query(&[("mev-protect", "true")]);
|
||||
}
|
||||
let response = req
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.body(body_bytes)
|
||||
.send()
|
||||
|
||||
@@ -429,7 +429,7 @@ impl BlockRazorClient {
|
||||
let (content, _signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let mut query_params: Vec<(&str, &str)> = vec![
|
||||
let query_params: Vec<(&str, &str)> = vec![
|
||||
("auth", auth_token.as_str()),
|
||||
// mev_protection=true: sandwichMitigation mode skips blacklisted Leader slots (MEV protection).
|
||||
// revertProtection is unrelated to MEV; not set.
|
||||
|
||||
+69
-40
@@ -28,8 +28,9 @@ use anyhow::Result;
|
||||
use crate::{
|
||||
common::SolanaRpcClient,
|
||||
constants::swqos::{
|
||||
SWQOS_ENDPOINTS_ASTRALANE, SWQOS_ENDPOINTS_ASTRALANE_QUIC,
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV, SWQOS_ENDPOINTS_BLOCKRAZOR,
|
||||
SWQOS_ENDPOINTS_ASTRALANE_BINARY, SWQOS_ENDPOINTS_ASTRALANE_PLAIN,
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV,
|
||||
SWQOS_ENDPOINTS_BLOCKRAZOR,
|
||||
SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX, SWQOS_ENDPOINTS_FLASHBLOCK,
|
||||
SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO, SWQOS_ENDPOINTS_NEXTBLOCK,
|
||||
SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOYAS,
|
||||
@@ -63,7 +64,7 @@ pub const SWQOS_BLACKLIST: &[SwqosType] = &[
|
||||
|
||||
/// SWQOS 提交通道:HTTP、gRPC 或 QUIC(低延迟)。
|
||||
/// BlockRazor 支持 gRPC 和 HTTP。
|
||||
/// Astralane 和 Node1 支持 QUIC。
|
||||
/// Node1 支持 QUIC。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub enum SwqosTransport {
|
||||
#[default]
|
||||
@@ -72,6 +73,19 @@ pub enum SwqosTransport {
|
||||
Quic,
|
||||
}
|
||||
|
||||
/// Astralane 三种提交方式:QUIC TPU、Plain HTTP(`/iris`)、Binary HTTP(`/irisb` + bincode)。
|
||||
/// 与全局 [`crate::common::TradeConfig::mev_protection`] 配合:HTTP 加 `mev-protect=true`;QUIC 选 `:9000` / `:7000`。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub enum AstralaneTransport {
|
||||
/// Binary over HTTP:`…/irisb?api-key=…&method=sendTransaction`(与 `AstralaneClient` 当前序列化一致)。
|
||||
#[default]
|
||||
Binary,
|
||||
/// Plain HTTP:`…/iris?…`(非 irisb 路径)。
|
||||
Plain,
|
||||
/// QUIC(`host:7000`;MEV 时 `host:9000`)。
|
||||
Quic,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TradeType {
|
||||
Create,
|
||||
@@ -196,15 +210,23 @@ pub trait SwqosClientTrait {
|
||||
}
|
||||
}
|
||||
|
||||
/// 地理区域,用于默认 SWQOS 端点下标(见 `constants::swqos`)。
|
||||
///
|
||||
/// 各服务商常量表在**缺独立 PoP**时,于**已公布的端点集合内**按地理距离选最近项;[`SwqosRegion::Default`] 不表示地球上的位置,表中为全局/枢纽回退,不适用地理就近。
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum SwqosRegion {
|
||||
NewYork,
|
||||
Frankfurt,
|
||||
Amsterdam,
|
||||
/// Ireland (EU); Jito publishes `dublin.mainnet.block-engine.jito.wtf`.
|
||||
Dublin,
|
||||
SLC,
|
||||
Tokyo,
|
||||
/// Southeast Asia (Singapore); not interchangeable with [`SwqosRegion::Tokyo`].
|
||||
Singapore,
|
||||
London,
|
||||
LosAngeles,
|
||||
/// 非地理区域:未指定区域时的回退,对应表中全局 URL 或枢纽,**不按地理距离选取**。
|
||||
Default,
|
||||
}
|
||||
|
||||
@@ -227,8 +249,8 @@ pub enum SwqosConfig {
|
||||
FlashBlock(String, SwqosRegion, Option<String>),
|
||||
/// BlockRazor(api_token, region, custom_url, transport). transport=None 或 Grpc => gRPC; Some(Http) => HTTP.
|
||||
BlockRazor(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
|
||||
/// Astralane(api_token, region, custom_url, transport). transport=None 表示 Http。
|
||||
Astralane(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
|
||||
/// Astralane(api_token, region, custom_url, mode). `None` => [`AstralaneTransport::Binary`](`/irisb`)。
|
||||
Astralane(String, SwqosRegion, Option<String>, Option<AstralaneTransport>),
|
||||
/// Stellium(api_token, region, custom_url)
|
||||
Stellium(String, SwqosRegion, Option<String>),
|
||||
/// Lightspeed(api_key, region, custom_url) - Solana Vibe Station
|
||||
@@ -285,7 +307,7 @@ impl SwqosConfig {
|
||||
SwqosType::Node1 => SWQOS_ENDPOINTS_NODE1[region as usize].to_string(),
|
||||
SwqosType::FlashBlock => SWQOS_ENDPOINTS_FLASHBLOCK[region as usize].to_string(),
|
||||
SwqosType::BlockRazor => SWQOS_ENDPOINTS_BLOCKRAZOR[region as usize].to_string(),
|
||||
SwqosType::Astralane => SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string(),
|
||||
SwqosType::Astralane => SWQOS_ENDPOINTS_ASTRALANE_BINARY[region as usize].to_string(),
|
||||
SwqosType::Stellium => SWQOS_ENDPOINTS_STELLIUM[region as usize].to_string(),
|
||||
SwqosType::Lightspeed => "".to_string(), // Lightspeed requires custom URL with api_key
|
||||
SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(),
|
||||
@@ -300,7 +322,7 @@ impl SwqosConfig {
|
||||
region: SwqosRegion,
|
||||
url: Option<String>,
|
||||
transport: Option<SwqosTransport>,
|
||||
mev_protection: bool,
|
||||
_mev_protection: bool,
|
||||
) -> String {
|
||||
if let Some(custom_url) = url {
|
||||
return custom_url;
|
||||
@@ -324,19 +346,6 @@ impl SwqosConfig {
|
||||
SWQOS_ENDPOINTS_NODE1[region as usize].to_string()
|
||||
}
|
||||
}
|
||||
SwqosType::Astralane => {
|
||||
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
|
||||
if use_quic {
|
||||
// MEV protection: port 9000; standard: port 7000
|
||||
if mev_protection {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
|
||||
} else {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
|
||||
}
|
||||
} else {
|
||||
SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string()
|
||||
}
|
||||
}
|
||||
_ => Self::get_endpoint(swqos_type, region, None),
|
||||
}
|
||||
}
|
||||
@@ -414,26 +423,46 @@ impl SwqosConfig {
|
||||
Ok(Arc::new(blockrazor_client))
|
||||
}
|
||||
}
|
||||
SwqosConfig::Astralane(auth_token, region, url, transport) => {
|
||||
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
|
||||
if use_quic {
|
||||
let quic_endpoint = url.unwrap_or_else(|| {
|
||||
// MEV protection: port 9000; standard: port 7000
|
||||
if mev_protection {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
|
||||
} else {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
|
||||
}
|
||||
});
|
||||
let astralane_client =
|
||||
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token)
|
||||
.await?;
|
||||
Ok(Arc::new(astralane_client))
|
||||
} else {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Astralane, region, url);
|
||||
let astralane_client =
|
||||
AstralaneClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
|
||||
Ok(Arc::new(astralane_client))
|
||||
SwqosConfig::Astralane(auth_token, region, url, mode) => {
|
||||
let mode = mode.unwrap_or_default();
|
||||
match mode {
|
||||
AstralaneTransport::Quic => {
|
||||
let quic_endpoint = url.unwrap_or_else(|| {
|
||||
if mev_protection {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
|
||||
} else {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
|
||||
}
|
||||
});
|
||||
let astralane_client =
|
||||
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token)
|
||||
.await?;
|
||||
Ok(Arc::new(astralane_client))
|
||||
}
|
||||
AstralaneTransport::Plain => {
|
||||
let endpoint = url.unwrap_or_else(|| {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_PLAIN[region as usize].to_string()
|
||||
});
|
||||
let astralane_client = AstralaneClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint,
|
||||
auth_token,
|
||||
mev_protection,
|
||||
);
|
||||
Ok(Arc::new(astralane_client))
|
||||
}
|
||||
AstralaneTransport::Binary => {
|
||||
let endpoint = url.unwrap_or_else(|| {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_BINARY[region as usize].to_string()
|
||||
});
|
||||
let astralane_client = AstralaneClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint,
|
||||
auth_token,
|
||||
mev_protection,
|
||||
);
|
||||
Ok(Arc::new(astralane_client))
|
||||
}
|
||||
}
|
||||
}
|
||||
SwqosConfig::Stellium(auth_token, region, url) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ use solana_transaction_status::UiTransactionEncoding;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::{
|
||||
common::SolanaRpcClient,
|
||||
common::{sdk_log, SolanaRpcClient},
|
||||
swqos::{common::poll_transaction_confirmation, SwqosType, TradeType},
|
||||
};
|
||||
use anyhow::Result;
|
||||
@@ -25,6 +25,7 @@ impl SwqosClientTrait for SolRpcClient {
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let submit_start = Instant::now();
|
||||
let signature = self
|
||||
.rpc_client
|
||||
.send_transaction_with_config(
|
||||
@@ -39,6 +40,8 @@ impl SwqosClientTrait for SolRpcClient {
|
||||
)
|
||||
.await?;
|
||||
|
||||
sdk_log::log_swqos_submitted("Default", trade_type, submit_start.elapsed());
|
||||
|
||||
let start_time = Instant::now();
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||
Ok(_) => (),
|
||||
|
||||
+21
-14
@@ -6,7 +6,10 @@ use quinn::{
|
||||
TransportConfig,
|
||||
};
|
||||
use rand::seq::IndexedRandom as _;
|
||||
use rcgen::{CertificateParams, KeyPair as RcgenKeyPair};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||
use solana_client::rpc_client::SerializableTransaction;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
|
||||
use std::time::Instant;
|
||||
use std::{
|
||||
@@ -69,18 +72,17 @@ impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate dummy self-signed certificate using rcgen
|
||||
fn generate_self_signed_cert(_keypair: &Keypair) -> Result<(rustls::pki_types::CertificateDer<'static>, rustls::pki_types::PrivateKeyDer<'static>)> {
|
||||
// Generate a new key pair for the certificate
|
||||
let key_pair = rcgen::KeyPair::generate()?;
|
||||
|
||||
let params = rcgen::CertificateParams::default();
|
||||
let cert = params.self_signed(&key_pair)?;
|
||||
|
||||
let cert_der = rustls::pki_types::CertificateDer::from(cert.der().to_vec());
|
||||
let key_der = rustls::pki_types::PrivateKeyDer::try_from(key_pair.serialize_der())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create private key: {:?}", e))?;
|
||||
|
||||
/// TLS 客户端证书:ECDSA P-256 + CN=钱包公钥(与 Speedlanding / Astralane QUIC 策略一致)。
|
||||
fn generate_client_tls_credentials(keypair: &Keypair) -> Result<(CertificateDer<'static>, PrivateKeyDer<'static>)> {
|
||||
let tls_key = RcgenKeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
|
||||
let mut cert_params = CertificateParams::new(vec![])?;
|
||||
cert_params.distinguished_name.push(
|
||||
rcgen::DnType::CommonName,
|
||||
rcgen::DnValue::Utf8String(keypair.pubkey().to_string()),
|
||||
);
|
||||
let cert = cert_params.self_signed(&tls_key)?;
|
||||
let cert_der = CertificateDer::from(cert.der().to_vec());
|
||||
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(tls_key.serialize_der()));
|
||||
Ok((cert_der, key_der))
|
||||
}
|
||||
|
||||
@@ -101,8 +103,13 @@ pub struct SoyasClient {
|
||||
impl SoyasClient {
|
||||
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let keypair = Keypair::from_base58_string(&api_key);
|
||||
let (cert, key) = generate_self_signed_cert(&keypair)?;
|
||||
let keypair = Keypair::try_from_base58_string(api_key.trim()).map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"Soyas api_token 无法解析为 Solana keypair base58(QUIC mTLS 用): {}",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
let (cert, key) = generate_client_tls_credentials(&keypair)?;
|
||||
let mut crypto = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(SkipServerVerification::new())
|
||||
|
||||
+61
-112
@@ -6,7 +6,9 @@ use quinn::{
|
||||
TransportConfig,
|
||||
};
|
||||
use rand::seq::IndexedRandom as _;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
|
||||
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
|
||||
use std::time::Instant;
|
||||
use std::{
|
||||
net::{SocketAddr, ToSocketAddrs as _},
|
||||
@@ -25,69 +27,9 @@ use crate::{
|
||||
swqos::{SwqosType, TradeType},
|
||||
};
|
||||
|
||||
// Skip server verification implementation
|
||||
#[derive(Debug)]
|
||||
struct SkipServerVerification;
|
||||
|
||||
impl SkipServerVerification {
|
||||
fn new() -> Arc<Self> {
|
||||
Arc::new(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
||||
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
|
||||
_server_name: &rustls::pki_types::ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: rustls::pki_types::UnixTime,
|
||||
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
||||
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
vec![rustls::SignatureScheme::ECDSA_NISTP256_SHA256]
|
||||
}
|
||||
}
|
||||
|
||||
// Generate dummy self-signed certificate using rcgen
|
||||
fn generate_self_signed_cert(_keypair: &Keypair) -> Result<(rustls::pki_types::CertificateDer<'static>, rustls::pki_types::PrivateKeyDer<'static>)> {
|
||||
// Generate a new key pair for the certificate
|
||||
let key_pair = rcgen::KeyPair::generate()?;
|
||||
|
||||
let params = rcgen::CertificateParams::default();
|
||||
let cert = params.self_signed(&key_pair)?;
|
||||
|
||||
let cert_der = rustls::pki_types::CertificateDer::from(cert.der().to_vec());
|
||||
let key_der = rustls::pki_types::PrivateKeyDer::try_from(key_pair.serialize_der())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create private key: {:?}", e))?;
|
||||
|
||||
Ok((cert_der, key_der))
|
||||
}
|
||||
|
||||
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
|
||||
/// Fallback SNI when endpoint is IP or cannot extract host (keeps legacy behavior).
|
||||
const SPEED_SERVER_FALLBACK: &str = "speed-landing";
|
||||
/// QUIC TLS SNI:与 Speedlanding 官方客户端一致,固定为 `speed-landing`(勿用 PoP 主机名,否则易握手失败)。
|
||||
const SPEED_SERVER: &str = "speed-landing";
|
||||
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
|
||||
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
@@ -98,34 +40,21 @@ pub struct SpeedlandingClient {
|
||||
endpoint: Endpoint,
|
||||
client_config: ClientConfig,
|
||||
addr: SocketAddr,
|
||||
/// TLS SNI: host from endpoint URL so server presents the right cert (e.g. nyc.speedlanding.trade).
|
||||
server_name: String,
|
||||
connection: ArcSwap<Connection>,
|
||||
reconnect: Mutex<()>,
|
||||
}
|
||||
|
||||
impl SpeedlandingClient {
|
||||
/// Extract TLS SNI (host) from endpoint URL. Uses fallback "speed-landing" for IP or when host cannot be determined.
|
||||
fn server_name_from_endpoint(endpoint: &str) -> String {
|
||||
let without_scheme = endpoint
|
||||
.strip_prefix("https://")
|
||||
.or_else(|| endpoint.strip_prefix("http://"))
|
||||
.unwrap_or(endpoint);
|
||||
let host = without_scheme.split(':').next().unwrap_or("").trim();
|
||||
if host.is_empty() {
|
||||
return SPEED_SERVER_FALLBACK.to_string();
|
||||
}
|
||||
if !host.chars().any(|c| c.is_ascii_alphabetic()) {
|
||||
return SPEED_SERVER_FALLBACK.to_string();
|
||||
}
|
||||
host.to_string()
|
||||
}
|
||||
|
||||
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let server_name = Self::server_name_from_endpoint(&endpoint_string);
|
||||
let keypair = Keypair::from_base58_string(&api_key);
|
||||
let (cert, key) = generate_self_signed_cert(&keypair)?;
|
||||
// Speedlanding QUIC:与官方一致使用 `solana_tls_utils::new_dummy_x509_certificate`(Ed25519 dummy cert)+ SNI `speed-landing`。
|
||||
let keypair = Keypair::try_from_base58_string(api_key.trim()).map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"Speedlanding api_token 无法解析为 Solana keypair base58(用于 mTLS);请确认粘贴的是机器人提供的密钥而非其它字符串: {}",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
let (cert, key) = new_dummy_x509_certificate(&keypair);
|
||||
let mut crypto = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(SkipServerVerification::new())
|
||||
@@ -148,18 +77,23 @@ impl SpeedlandingClient {
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Address not resolved"))?;
|
||||
let connecting = endpoint.connect(addr, &server_name)?;
|
||||
let connecting = endpoint.connect(addr, SPEED_SERVER)?;
|
||||
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
||||
.await
|
||||
.context("Speedlanding QUIC connect timeout")?
|
||||
.context("Speedlanding QUIC handshake failed")?;
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Speedlanding QUIC handshake failed(请确认:1) 机器人登记的身份与钱包公钥 {} 一致 2) 本机 UDP 可访问 {} 3) region 与 PoP 匹配)",
|
||||
keypair.pubkey(),
|
||||
endpoint_string
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
endpoint,
|
||||
client_config,
|
||||
addr,
|
||||
server_name,
|
||||
connection: ArcSwap::from_pointee(connection),
|
||||
reconnect: Mutex::new(()),
|
||||
})
|
||||
@@ -181,12 +115,17 @@ impl SpeedlandingClient {
|
||||
let connecting = self.endpoint.connect_with(
|
||||
self.client_config.clone(),
|
||||
self.addr,
|
||||
self.server_name.as_str(),
|
||||
SPEED_SERVER,
|
||||
)?;
|
||||
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
||||
.await
|
||||
.context("Speedlanding QUIC reconnect timeout")?
|
||||
.context("Speedlanding QUIC re-handshake failed")?;
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Speedlanding QUIC re-handshake failed(对端 {} SNI {})",
|
||||
self.addr, SPEED_SERVER
|
||||
)
|
||||
})?;
|
||||
self.connection.store(Arc::new(connection));
|
||||
return Ok(self.connection.load_full());
|
||||
}
|
||||
@@ -219,51 +158,61 @@ impl SwqosClientTrait for SpeedlandingClient {
|
||||
Ok(Err(_)) | Err(_) => true,
|
||||
};
|
||||
if need_retry {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(" [Speedlanding] {} submission failed after {:?}, reconnecting", trade_type, start_time.elapsed());
|
||||
}
|
||||
eprintln!(
|
||||
" [Speedlanding] {} QUIC 首次发送失败 {:?},正在重试",
|
||||
trade_type,
|
||||
start_time.elapsed()
|
||||
);
|
||||
let connection = self.ensure_connected().await?;
|
||||
send_result =
|
||||
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
|
||||
}
|
||||
match send_result.context("Speedlanding QUIC send timeout") {
|
||||
Ok(Ok(())) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("Speedlanding", trade_type, start_time.elapsed());
|
||||
}
|
||||
// 提交结果与「详细耗时/SDK 开关」无关,便于确认当前通道确实在执行
|
||||
crate::common::sdk_log::log_swqos_submitted("Speedlanding", trade_type, start_time.elapsed());
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Speedlanding", trade_type, start_time.elapsed(), &e);
|
||||
}
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
&e,
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Speedlanding", trade_type, start_time.elapsed(), "timeout");
|
||||
}
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"timeout",
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
println!(" signature: {:?}", signature);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
&e,
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "Speedlanding", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -23,7 +23,11 @@ use std::collections::HashMap;
|
||||
use std::hash::BuildHasherDefault;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::{str::FromStr, sync::Arc, time::Instant};
|
||||
use std::{
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use fnv::FnvHasher;
|
||||
@@ -402,14 +406,31 @@ impl ResultCollector {
|
||||
timeout_secs: u64,
|
||||
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
let start = Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(timeout_secs);
|
||||
let poll_interval = std::time::Duration::from_millis(2);
|
||||
let primary = Duration::from_secs(timeout_secs);
|
||||
let poll_interval = Duration::from_millis(2);
|
||||
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
|
||||
if start.elapsed() > timeout {
|
||||
if start.elapsed() > primary {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(poll_interval).await;
|
||||
}
|
||||
// 「不等待链上确认」仍会等各 SWQOS 的 HTTP 回包;主循环在收齐或触达 `timeout_secs` 后结束。
|
||||
// 若主窗口到时仍有未回包通道,晚到的 TaskResult 若立刻 drain 会丢签名——仅在该路径上拉长 grace。
|
||||
let all_submitted =
|
||||
self.completed_count.load(Ordering::Acquire) >= self.total_tasks;
|
||||
if all_submitted {
|
||||
// 全数已登记:仅留极短 settle,避免极端情况下最后一笔与计数可见性竞态。
|
||||
tokio::time::sleep(Duration::from_millis(35)).await;
|
||||
} else {
|
||||
tokio::time::sleep(Duration::from_millis(600)).await;
|
||||
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
|
||||
if start.elapsed() > primary + Duration::from_secs(6) {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(120)).await;
|
||||
}
|
||||
self.get_first()
|
||||
}
|
||||
}
|
||||
@@ -492,7 +513,11 @@ pub async fn execute_parallel(
|
||||
}
|
||||
|
||||
// Task preparation completed: one shared context (clone once per batch), then minimal per-task data.
|
||||
let collector = Arc::new(ResultCollector::new(task_configs.len()));
|
||||
let channel_count = task_configs.len().max(1);
|
||||
let collector = Arc::new(ResultCollector::new(channel_count));
|
||||
// 上限最多 5s:单路卡死时才会等满;若所有通道在窗口内回完,主循环会提前结束(不睡满秒数)。
|
||||
let submit_timeout_secs: u64 =
|
||||
(3u64 + (channel_count.min(16) as u64).div_ceil(2).min(6)).clamp(3, 5);
|
||||
let shared = Arc::new(SwqosSharedContext {
|
||||
payer,
|
||||
instructions,
|
||||
@@ -562,13 +587,20 @@ pub async fn execute_parallel(
|
||||
// All jobs enqueued (no spawn on hot path)
|
||||
|
||||
if !wait_transaction_confirmed {
|
||||
const SUBMIT_TIMEOUT_SECS: u64 = 2;//无需确认的交易,一般2秒合适了 一般2秒内发送全都返回 没返回的也不等了,没返回的就是太慢的swqos
|
||||
let ret = collector.wait_for_all_submitted(SUBMIT_TIMEOUT_SECS).await.unwrap_or((
|
||||
false,
|
||||
vec![],
|
||||
Some(anyhow!("No SWQOS result within {}s", SUBMIT_TIMEOUT_SECS)),
|
||||
vec![],
|
||||
));
|
||||
// submit_timeout_secs 为「等齐各 SWQOS HTTP 应答」的上限;收齐后会立刻进入短 settle,不会睡满整段秒数。
|
||||
// `wait_for_all_submitted` 仅在未收齐时追加长 grace,避免过早 drain 丢晚到签名。
|
||||
let ret = collector
|
||||
.wait_for_all_submitted(submit_timeout_secs)
|
||||
.await
|
||||
.unwrap_or((
|
||||
false,
|
||||
vec![],
|
||||
Some(anyhow!(
|
||||
"No SWQOS result within grace window (primary {}s)",
|
||||
submit_timeout_secs
|
||||
)),
|
||||
vec![],
|
||||
));
|
||||
let (success, signatures, last_error, submit_timings) = ret;
|
||||
return Ok((success, signatures, last_error, submit_timings));
|
||||
}
|
||||
|
||||
@@ -1,867 +0,0 @@
|
||||
use crate::common::bonding_curve::BondingCurveAccount;
|
||||
use crate::common::nonce_cache::DurableNonceInfo;
|
||||
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
|
||||
use crate::common::{GasFeeStrategy, SolanaRpcClient};
|
||||
use crate::constants::TOKEN_PROGRAM;
|
||||
use core_affinity::CoreId;
|
||||
use crate::instruction::utils::pumpfun::is_mayhem_fee_recipient;
|
||||
|
||||
/// Concurrency + core binding config for parallel submit (precomputed at SDK init, one param on hot path). Uses Arc so no borrow of SwapParams.
|
||||
#[derive(Clone)]
|
||||
pub struct SenderConcurrencyConfig {
|
||||
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
|
||||
pub effective_core_ids: Arc<Vec<CoreId>>,
|
||||
pub max_sender_concurrency: usize,
|
||||
}
|
||||
use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP;
|
||||
use crate::swqos::{SwqosClient, TradeType};
|
||||
use crate::trading::common::get_multi_token_balances;
|
||||
use crate::trading::MiddlewareManager;
|
||||
use solana_hash::Hash;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// DEX 参数枚举 - 零开销抽象替代 Box<dyn ProtocolParams>
|
||||
#[derive(Clone)]
|
||||
pub enum DexParamEnum {
|
||||
PumpFun(PumpFunParams),
|
||||
PumpSwap(PumpSwapParams),
|
||||
Bonk(BonkParams),
|
||||
RaydiumCpmm(RaydiumCpmmParams),
|
||||
RaydiumAmmV4(RaydiumAmmV4Params),
|
||||
MeteoraDammV2(MeteoraDammV2Params),
|
||||
}
|
||||
|
||||
impl DexParamEnum {
|
||||
/// 获取内部参数的 Any 引用,用于向后兼容的类型检查
|
||||
#[inline]
|
||||
pub fn as_any(&self) -> &dyn std::any::Any {
|
||||
match self {
|
||||
DexParamEnum::PumpFun(p) => p,
|
||||
DexParamEnum::PumpSwap(p) => p,
|
||||
DexParamEnum::Bonk(p) => p,
|
||||
DexParamEnum::RaydiumCpmm(p) => p,
|
||||
DexParamEnum::RaydiumAmmV4(p) => p,
|
||||
DexParamEnum::MeteoraDammV2(p) => p,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Swap parameters
|
||||
#[derive(Clone)]
|
||||
pub struct SwapParams {
|
||||
pub rpc: Option<Arc<SolanaRpcClient>>,
|
||||
pub payer: Arc<Keypair>,
|
||||
pub trade_type: TradeType,
|
||||
pub input_mint: Pubkey,
|
||||
pub input_token_program: Option<Pubkey>,
|
||||
pub output_mint: Pubkey,
|
||||
pub output_token_program: Option<Pubkey>,
|
||||
pub input_amount: Option<u64>,
|
||||
pub slippage_basis_points: Option<u64>,
|
||||
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
pub recent_blockhash: Option<Hash>,
|
||||
pub wait_tx_confirmed: bool,
|
||||
pub protocol_params: DexParamEnum,
|
||||
pub open_seed_optimize: bool,
|
||||
/// Arc<Vec<..>> so cloning from infrastructure is a single Arc clone.
|
||||
pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
|
||||
pub middleware_manager: Option<Arc<MiddlewareManager>>,
|
||||
pub durable_nonce: Option<DurableNonceInfo>,
|
||||
pub with_tip: bool,
|
||||
pub create_input_mint_ata: bool,
|
||||
pub close_input_mint_ata: bool,
|
||||
pub create_output_mint_ata: bool,
|
||||
pub close_output_mint_ata: bool,
|
||||
pub fixed_output_amount: Option<u64>,
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
pub simulate: bool,
|
||||
/// Whether to output SDK logs (from TradeConfig.log_enabled).
|
||||
pub log_enabled: bool,
|
||||
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
|
||||
pub use_dedicated_sender_threads: bool,
|
||||
/// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path.
|
||||
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
|
||||
/// Precomputed at SDK init: min(swqos_count, 2/3*cores). Avoids get_core_ids() on trade hot path.
|
||||
pub max_sender_concurrency: usize,
|
||||
/// Precomputed at SDK init: first max_sender_concurrency CoreIds for job affinity. Arc clone only.
|
||||
pub effective_core_ids: Arc<Vec<CoreId>>,
|
||||
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). When false, skip filter for lower latency.
|
||||
pub check_min_tip: bool,
|
||||
/// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled.
|
||||
pub grpc_recv_us: Option<i64>,
|
||||
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
|
||||
/// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens.
|
||||
/// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
|
||||
/// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes.
|
||||
pub use_exact_sol_amount: Option<bool>,
|
||||
}
|
||||
|
||||
impl SwapParams {
|
||||
/// One struct for execute_parallel: merges sender_thread_cores, effective_core_ids, max_sender_concurrency. Arc clone only.
|
||||
#[inline]
|
||||
pub fn sender_concurrency_config(&self) -> SenderConcurrencyConfig {
|
||||
SenderConcurrencyConfig {
|
||||
sender_thread_cores: self.sender_thread_cores.clone(),
|
||||
effective_core_ids: self.effective_core_ids.clone(),
|
||||
max_sender_concurrency: self.max_sender_concurrency,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SwapParams {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "SwapParams: ...")
|
||||
}
|
||||
}
|
||||
|
||||
/// PumpFun protocol specific parameters
|
||||
/// Configuration parameters specific to PumpFun trading protocol.
|
||||
///
|
||||
/// **Creator vault**: Pump buy/sell instructions always pass `creator_vault` =
|
||||
/// `PDA(["creator-vault", bonding_curve.creator])` derived from [`BondingCurveAccount::creator`].
|
||||
/// Keep `bonding_curve.creator` in sync with chain (gRPC / RPC); stale `creator_vault` in this struct
|
||||
/// does not affect ix building.
|
||||
#[derive(Clone)]
|
||||
pub struct PumpFunParams {
|
||||
pub bonding_curve: Arc<BondingCurveAccount>,
|
||||
pub associated_bonding_curve: Pubkey,
|
||||
/// Resolved by [`resolve_creator_vault_for_ix`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix): use ix vault when it matches `PDA(creator)` or fee-sharing vault; else `PDA(creator)`.
|
||||
pub creator_vault: Pubkey,
|
||||
pub token_program: Pubkey,
|
||||
/// Whether to close token account when selling, only effective during sell operations
|
||||
pub close_token_account_when_sell: Option<bool>,
|
||||
/// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。
|
||||
/// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)。
|
||||
pub fee_recipient: Pubkey,
|
||||
}
|
||||
|
||||
impl PumpFunParams {
|
||||
pub fn immediate_sell(
|
||||
creator_vault: Pubkey,
|
||||
token_program: Pubkey,
|
||||
close_token_account_when_sell: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
bonding_curve: Arc::new(BondingCurveAccount { ..Default::default() }),
|
||||
associated_bonding_curve: Pubkey::default(),
|
||||
creator_vault: creator_vault,
|
||||
token_program: token_program,
|
||||
close_token_account_when_sell: Some(close_token_account_when_sell),
|
||||
fee_recipient: Pubkey::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||
/// `mayhem_mode`: `Some` when known from Create/Trade event (`is_mayhem_mode` / `mayhem_mode`).
|
||||
/// `None` falls back to detecting Mayhem via reserved fee recipient pubkeys only (not AMM protocol fee accounts).
|
||||
pub fn from_dev_trade(
|
||||
mint: Pubkey,
|
||||
token_amount: u64,
|
||||
max_sol_cost: u64,
|
||||
creator: Pubkey,
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
close_token_account_when_sell: Option<bool>,
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
mayhem_mode: Option<bool>,
|
||||
) -> Self {
|
||||
let is_mayhem_mode =
|
||||
mayhem_mode.unwrap_or_else(|| is_mayhem_fee_recipient(&fee_recipient));
|
||||
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
|
||||
bonding_curve,
|
||||
&mint,
|
||||
token_amount,
|
||||
max_sol_cost,
|
||||
creator,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix(
|
||||
&bonding_curve_account.creator,
|
||||
creator_vault,
|
||||
&mint,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve_account),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
creator_vault: creator_vault_resolved,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
fee_recipient,
|
||||
}
|
||||
}
|
||||
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||
///
|
||||
/// `mayhem_mode`:
|
||||
/// - **`Some(v)`**(推荐):显式使用链上事件中的值。gRPC 日志解析对应 Explorer 的 `tradeEvent.mayhemMode`;
|
||||
/// **不会**再用 `fee_recipient` 覆盖。
|
||||
/// - **`None`**:无该字段时(例如 ShredStream 仅解外层指令、或冷路径),才用 `fee_recipient` 是否落在 Mayhem 静态列表上推断。
|
||||
pub fn from_trade(
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
mint: Pubkey,
|
||||
creator: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
virtual_token_reserves: u64,
|
||||
virtual_sol_reserves: u64,
|
||||
real_token_reserves: u64,
|
||||
real_sol_reserves: u64,
|
||||
close_token_account_when_sell: Option<bool>,
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
mayhem_mode: Option<bool>,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = match mayhem_mode {
|
||||
Some(v) => v,
|
||||
None => is_mayhem_fee_recipient(&fee_recipient),
|
||||
};
|
||||
let bonding_curve = BondingCurveAccount::from_trade(
|
||||
bonding_curve,
|
||||
mint,
|
||||
creator,
|
||||
virtual_token_reserves,
|
||||
virtual_sol_reserves,
|
||||
real_token_reserves,
|
||||
real_sol_reserves,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix(
|
||||
&bonding_curve.creator,
|
||||
creator_vault,
|
||||
&mint,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
creator_vault: creator_vault_resolved,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
fee_recipient,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let account =
|
||||
crate::instruction::utils::pumpfun::fetch_bonding_curve_account(rpc, mint).await?;
|
||||
let mint_account = rpc.get_account(&mint).await?;
|
||||
let bonding_curve = BondingCurveAccount {
|
||||
discriminator: 0,
|
||||
account: account.1,
|
||||
virtual_token_reserves: account.0.virtual_token_reserves,
|
||||
virtual_sol_reserves: account.0.virtual_sol_reserves,
|
||||
real_token_reserves: account.0.real_token_reserves,
|
||||
real_sol_reserves: account.0.real_sol_reserves,
|
||||
token_total_supply: account.0.token_total_supply,
|
||||
complete: account.0.complete,
|
||||
creator: account.0.creator,
|
||||
is_mayhem_mode: account.0.is_mayhem_mode,
|
||||
is_cashback_coin: account.0.is_cashback_coin,
|
||||
};
|
||||
let associated_bonding_curve = get_associated_token_address_with_program_id(
|
||||
&bonding_curve.account,
|
||||
mint,
|
||||
&mint_account.owner,
|
||||
);
|
||||
let creator_vault =
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator);
|
||||
Ok(Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
creator_vault: creator_vault.unwrap(),
|
||||
close_token_account_when_sell: None,
|
||||
token_program: mint_account.owner,
|
||||
fee_recipient: Pubkey::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates the cached `creator_vault` field only. Buy/sell ix use [`BondingCurveAccount::creator`].
|
||||
#[inline]
|
||||
pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self {
|
||||
self.creator_vault = creator_vault;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// PumpSwap Protocol Specific Parameters
|
||||
///
|
||||
/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information,
|
||||
/// token configuration, and transaction amounts.
|
||||
///
|
||||
/// **Performance Note**: If these parameters are not provided, the system will attempt to
|
||||
/// retrieve the relevant information from RPC, which will increase transaction time.
|
||||
/// For optimal performance, it is recommended to provide all necessary parameters in advance.
|
||||
#[derive(Clone)]
|
||||
pub struct PumpSwapParams {
|
||||
/// Liquidity pool address
|
||||
pub pool: Pubkey,
|
||||
/// Base token mint address
|
||||
/// The mint account address of the base token in the trading pair
|
||||
pub base_mint: Pubkey,
|
||||
/// Quote token mint address
|
||||
/// The mint account address of the quote token in the trading pair, usually SOL or USDC
|
||||
pub quote_mint: Pubkey,
|
||||
/// Pool base token account
|
||||
pub pool_base_token_account: Pubkey,
|
||||
/// Pool quote token account
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
/// Base token reserves in the pool
|
||||
pub pool_base_token_reserves: u64,
|
||||
/// Quote token reserves in the pool
|
||||
pub pool_quote_token_reserves: u64,
|
||||
/// Coin creator vault ATA
|
||||
pub coin_creator_vault_ata: Pubkey,
|
||||
/// Coin creator vault authority
|
||||
pub coin_creator_vault_authority: Pubkey,
|
||||
/// Token program ID
|
||||
pub base_token_program: Pubkey,
|
||||
/// Quote token program ID
|
||||
pub quote_token_program: Pubkey,
|
||||
/// Whether the pool is in mayhem mode
|
||||
pub is_mayhem_mode: bool,
|
||||
/// Whether the pool's coin has cashback enabled
|
||||
pub is_cashback_coin: bool,
|
||||
}
|
||||
|
||||
impl PumpSwapParams {
|
||||
pub fn new(
|
||||
pool: Pubkey,
|
||||
base_mint: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
fee_recipient: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP;
|
||||
Self {
|
||||
pool,
|
||||
base_mint,
|
||||
quote_mint,
|
||||
pool_base_token_account,
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
quote_token_program,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fast-path constructor for building PumpSwap parameters directly from decoded
|
||||
/// trade/event data and the accompanying instruction accounts, avoiding RPC
|
||||
/// lookups and associated latency. Token program IDs should be sourced from
|
||||
/// the instruction accounts themselves to respect Token Program vs Token-2022
|
||||
/// differences.
|
||||
///
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin`
|
||||
/// from the event so that buy/sell instructions include the correct remaining
|
||||
/// accounts for cashback.
|
||||
pub fn from_trade(
|
||||
pool: Pubkey,
|
||||
base_mint: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
fee_recipient: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
pool,
|
||||
base_mint,
|
||||
quote_mint,
|
||||
pool_base_token_account,
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
quote_token_program,
|
||||
fee_recipient,
|
||||
is_cashback_coin,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("No pool found for mint"));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_data = crate::instruction::utils::pumpswap::fetch_pool(rpc, pool_address).await?;
|
||||
Self::from_pool_data(rpc, pool_address, &pool_data).await
|
||||
}
|
||||
|
||||
/// Build params from an already-decoded Pool, only fetching token balances.
|
||||
///
|
||||
/// Saves 1 RPC `getAccount` call vs `from_pool_address_by_rpc` when pool data
|
||||
/// is already available (e.g. from `pumpswap::find_by_mint` which returns the
|
||||
/// decoded Pool).
|
||||
pub async fn from_pool_data(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
pool_data: &crate::instruction::utils::pumpswap_types::Pool,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let (pool_base_token_reserves, pool_quote_token_reserves) =
|
||||
crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?;
|
||||
let creator = pool_data.coin_creator;
|
||||
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
|
||||
creator,
|
||||
pool_data.quote_mint,
|
||||
);
|
||||
let coin_creator_vault_authority =
|
||||
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
|
||||
|
||||
let base_token_program_ata = get_associated_token_address_with_program_id(
|
||||
pool_address,
|
||||
&pool_data.base_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
let quote_token_program_ata = get_associated_token_address_with_program_id(
|
||||
pool_address,
|
||||
&pool_data.quote_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
pool: *pool_address,
|
||||
base_mint: pool_data.base_mint,
|
||||
quote_mint: pool_data.quote_mint,
|
||||
pool_base_token_account: pool_data.pool_base_token_account,
|
||||
pool_quote_token_account: pool_data.pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_2022
|
||||
},
|
||||
is_cashback_coin: pool_data.is_cashback_coin,
|
||||
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_2022
|
||||
},
|
||||
is_mayhem_mode: pool_data.is_mayhem_mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Bonk protocol specific parameters
|
||||
/// Configuration parameters specific to Bonk trading protocol
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BonkParams {
|
||||
pub virtual_base: u128,
|
||||
pub virtual_quote: u128,
|
||||
pub real_base: u128,
|
||||
pub real_quote: u128,
|
||||
pub pool_state: Pubkey,
|
||||
pub base_vault: Pubkey,
|
||||
pub quote_vault: Pubkey,
|
||||
/// Token program ID
|
||||
pub mint_token_program: Pubkey,
|
||||
pub platform_config: Pubkey,
|
||||
pub platform_associated_account: Pubkey,
|
||||
pub creator_associated_account: Pubkey,
|
||||
pub global_config: Pubkey,
|
||||
}
|
||||
|
||||
impl BonkParams {
|
||||
pub fn immediate_sell(
|
||||
mint_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
mint_token_program,
|
||||
platform_config,
|
||||
platform_associated_account,
|
||||
creator_associated_account,
|
||||
global_config,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn from_trade(
|
||||
virtual_base: u64,
|
||||
virtual_quote: u64,
|
||||
real_base_after: u64,
|
||||
real_quote_after: u64,
|
||||
pool_state: Pubkey,
|
||||
base_vault: Pubkey,
|
||||
quote_vault: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
virtual_base: virtual_base as u128,
|
||||
virtual_quote: virtual_quote as u128,
|
||||
real_base: real_base_after as u128,
|
||||
real_quote: real_quote_after as u128,
|
||||
pool_state: pool_state,
|
||||
base_vault: base_vault,
|
||||
quote_vault: quote_vault,
|
||||
mint_token_program: base_token_program,
|
||||
platform_config: platform_config,
|
||||
platform_associated_account: platform_associated_account,
|
||||
creator_associated_account: creator_associated_account,
|
||||
global_config: global_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_dev_trade(
|
||||
is_exact_in: bool,
|
||||
amount_in: u64,
|
||||
amount_out: u64,
|
||||
pool_state: Pubkey,
|
||||
base_vault: Pubkey,
|
||||
quote_vault: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
const DEFAULT_VIRTUAL_BASE: u128 = 1073025605596382;
|
||||
const DEFAULT_VIRTUAL_QUOTE: u128 = 30000852951;
|
||||
let _amount_in = if is_exact_in {
|
||||
amount_in
|
||||
} else {
|
||||
crate::instruction::utils::bonk::get_amount_in(
|
||||
amount_out,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
DEFAULT_VIRTUAL_BASE,
|
||||
DEFAULT_VIRTUAL_QUOTE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
let real_quote = crate::instruction::utils::bonk::get_amount_in_net(
|
||||
amount_in,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
) as u128;
|
||||
let _amount_out = if is_exact_in {
|
||||
crate::instruction::utils::bonk::get_amount_out(
|
||||
amount_in,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
DEFAULT_VIRTUAL_BASE,
|
||||
DEFAULT_VIRTUAL_QUOTE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
) as u128
|
||||
} else {
|
||||
amount_out as u128
|
||||
};
|
||||
let real_base = _amount_out;
|
||||
Self {
|
||||
virtual_base: DEFAULT_VIRTUAL_BASE,
|
||||
virtual_quote: DEFAULT_VIRTUAL_QUOTE,
|
||||
real_base: real_base,
|
||||
real_quote: real_quote,
|
||||
pool_state: pool_state,
|
||||
base_vault: base_vault,
|
||||
quote_vault: quote_vault,
|
||||
mint_token_program: base_token_program,
|
||||
platform_config: platform_config,
|
||||
platform_associated_account: platform_associated_account,
|
||||
creator_associated_account: creator_associated_account,
|
||||
global_config: global_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
usd1_pool: bool,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_address = crate::instruction::utils::bonk::get_pool_pda(
|
||||
mint,
|
||||
if usd1_pool {
|
||||
&crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let pool_data =
|
||||
crate::instruction::utils::bonk::fetch_pool_state(rpc, &pool_address).await?;
|
||||
let token_account = rpc.get_account(&pool_data.base_mint).await?;
|
||||
let platform_associated_account =
|
||||
crate::instruction::utils::bonk::get_platform_associated_account(
|
||||
&pool_data.platform_config,
|
||||
);
|
||||
let creator_associated_account =
|
||||
crate::instruction::utils::bonk::get_creator_associated_account(&pool_data.creator);
|
||||
let platform_associated_account = platform_associated_account.unwrap();
|
||||
let creator_associated_account = creator_associated_account.unwrap();
|
||||
Ok(Self {
|
||||
virtual_base: pool_data.virtual_base as u128,
|
||||
virtual_quote: pool_data.virtual_quote as u128,
|
||||
real_base: pool_data.real_base as u128,
|
||||
real_quote: pool_data.real_quote as u128,
|
||||
pool_state: pool_address,
|
||||
base_vault: pool_data.base_vault,
|
||||
quote_vault: pool_data.quote_vault,
|
||||
mint_token_program: token_account.owner,
|
||||
platform_config: pool_data.platform_config,
|
||||
platform_associated_account,
|
||||
creator_associated_account,
|
||||
global_config: pool_data.global_config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// RaydiumCpmm protocol specific parameters
|
||||
/// Configuration parameters specific to Raydium CPMM trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct RaydiumCpmmParams {
|
||||
/// Pool address
|
||||
pub pool_state: Pubkey,
|
||||
/// Amm config address
|
||||
pub amm_config: Pubkey,
|
||||
/// Base token mint address
|
||||
pub base_mint: Pubkey,
|
||||
/// Quote token mint address
|
||||
pub quote_mint: Pubkey,
|
||||
/// Base token reserve amount in the pool
|
||||
pub base_reserve: u64,
|
||||
/// Quote token reserve amount in the pool
|
||||
pub quote_reserve: u64,
|
||||
/// Base token vault address
|
||||
pub base_vault: Pubkey,
|
||||
/// Quote token vault address
|
||||
pub quote_vault: Pubkey,
|
||||
/// Base token program ID
|
||||
pub base_token_program: Pubkey,
|
||||
/// Quote token program ID
|
||||
pub quote_token_program: Pubkey,
|
||||
/// Observation state account
|
||||
pub observation_state: Pubkey,
|
||||
}
|
||||
|
||||
impl RaydiumCpmmParams {
|
||||
pub fn from_trade(
|
||||
pool_state: Pubkey,
|
||||
amm_config: Pubkey,
|
||||
input_token_mint: Pubkey,
|
||||
output_token_mint: Pubkey,
|
||||
input_vault: Pubkey,
|
||||
output_vault: Pubkey,
|
||||
input_token_program: Pubkey,
|
||||
output_token_program: Pubkey,
|
||||
observation_state: Pubkey,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
pool_state: pool_state,
|
||||
amm_config: amm_config,
|
||||
base_mint: input_token_mint,
|
||||
quote_mint: output_token_mint,
|
||||
base_reserve: base_reserve,
|
||||
quote_reserve: quote_reserve,
|
||||
base_vault: input_vault,
|
||||
quote_vault: output_vault,
|
||||
base_token_program: input_token_program,
|
||||
quote_token_program: output_token_program,
|
||||
observation_state: observation_state,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool =
|
||||
crate::instruction::utils::raydium_cpmm::fetch_pool_state(rpc, pool_address).await?;
|
||||
let (token0_balance, token1_balance) =
|
||||
crate::instruction::utils::raydium_cpmm::get_pool_token_balances(
|
||||
rpc,
|
||||
pool_address,
|
||||
&pool.token0_mint,
|
||||
&pool.token1_mint,
|
||||
)
|
||||
.await?;
|
||||
Ok(Self {
|
||||
pool_state: *pool_address,
|
||||
amm_config: pool.amm_config,
|
||||
base_mint: pool.token0_mint,
|
||||
quote_mint: pool.token1_mint,
|
||||
base_reserve: token0_balance,
|
||||
quote_reserve: token1_balance,
|
||||
base_vault: pool.token0_vault,
|
||||
quote_vault: pool.token1_vault,
|
||||
base_token_program: pool.token0_program,
|
||||
quote_token_program: pool.token1_program,
|
||||
observation_state: pool.observation_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// RaydiumCpmm protocol specific parameters
|
||||
/// Configuration parameters specific to Raydium CPMM trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct RaydiumAmmV4Params {
|
||||
/// AMM pool address
|
||||
pub amm: Pubkey,
|
||||
/// Base token (coin) mint address
|
||||
pub coin_mint: Pubkey,
|
||||
/// Quote token (pc) mint address
|
||||
pub pc_mint: Pubkey,
|
||||
/// Pool's coin token account address
|
||||
pub token_coin: Pubkey,
|
||||
/// Pool's pc token account address
|
||||
pub token_pc: Pubkey,
|
||||
/// Current coin reserve amount in the pool
|
||||
pub coin_reserve: u64,
|
||||
/// Current pc reserve amount in the pool
|
||||
pub pc_reserve: u64,
|
||||
}
|
||||
|
||||
impl RaydiumAmmV4Params {
|
||||
pub fn new(
|
||||
amm: Pubkey,
|
||||
coin_mint: Pubkey,
|
||||
pc_mint: Pubkey,
|
||||
token_coin: Pubkey,
|
||||
token_pc: Pubkey,
|
||||
coin_reserve: u64,
|
||||
pc_reserve: u64,
|
||||
) -> Self {
|
||||
Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve }
|
||||
}
|
||||
pub async fn from_amm_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
amm: Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?;
|
||||
let (coin_reserve, pc_reserve) =
|
||||
get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
|
||||
Ok(Self {
|
||||
amm,
|
||||
coin_mint: amm_info.coin_mint,
|
||||
pc_mint: amm_info.pc_mint,
|
||||
token_coin: amm_info.token_coin,
|
||||
token_pc: amm_info.token_pc,
|
||||
coin_reserve,
|
||||
pc_reserve,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// MeteoraDammV2 protocol specific parameters
|
||||
/// Configuration parameters specific to Meteora Damm V2 trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct MeteoraDammV2Params {
|
||||
pub pool: Pubkey,
|
||||
pub token_a_vault: Pubkey,
|
||||
pub token_b_vault: Pubkey,
|
||||
pub token_a_mint: Pubkey,
|
||||
pub token_b_mint: Pubkey,
|
||||
pub token_a_program: Pubkey,
|
||||
pub token_b_program: Pubkey,
|
||||
}
|
||||
|
||||
impl MeteoraDammV2Params {
|
||||
pub fn new(
|
||||
pool: Pubkey,
|
||||
token_a_vault: Pubkey,
|
||||
token_b_vault: Pubkey,
|
||||
token_a_mint: Pubkey,
|
||||
token_b_mint: Pubkey,
|
||||
token_a_program: Pubkey,
|
||||
token_b_program: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
pool,
|
||||
token_a_vault,
|
||||
token_b_vault,
|
||||
token_a_mint,
|
||||
token_b_mint,
|
||||
token_a_program,
|
||||
token_b_program,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_data =
|
||||
crate::instruction::utils::meteora_damm_v2::fetch_pool(rpc, pool_address).await?;
|
||||
Ok(Self {
|
||||
pool: *pool_address,
|
||||
token_a_vault: pool_data.token_a_vault,
|
||||
token_b_vault: pool_data.token_b_vault,
|
||||
token_a_mint: pool_data.token_a_mint,
|
||||
token_b_mint: pool_data.token_b_mint,
|
||||
token_a_program: TOKEN_PROGRAM,
|
||||
token_b_program: TOKEN_PROGRAM,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// Bonk protocol specific parameters
|
||||
/// Configuration parameters specific to Bonk trading protocol
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BonkParams {
|
||||
pub virtual_base: u128,
|
||||
pub virtual_quote: u128,
|
||||
pub real_base: u128,
|
||||
pub real_quote: u128,
|
||||
pub pool_state: Pubkey,
|
||||
pub base_vault: Pubkey,
|
||||
pub quote_vault: Pubkey,
|
||||
/// Token program ID
|
||||
pub mint_token_program: Pubkey,
|
||||
pub platform_config: Pubkey,
|
||||
pub platform_associated_account: Pubkey,
|
||||
pub creator_associated_account: Pubkey,
|
||||
pub global_config: Pubkey,
|
||||
}
|
||||
|
||||
impl BonkParams {
|
||||
pub fn immediate_sell(
|
||||
mint_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
mint_token_program,
|
||||
platform_config,
|
||||
platform_associated_account,
|
||||
creator_associated_account,
|
||||
global_config,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn from_trade(
|
||||
virtual_base: u64,
|
||||
virtual_quote: u64,
|
||||
real_base_after: u64,
|
||||
real_quote_after: u64,
|
||||
pool_state: Pubkey,
|
||||
base_vault: Pubkey,
|
||||
quote_vault: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
virtual_base: virtual_base as u128,
|
||||
virtual_quote: virtual_quote as u128,
|
||||
real_base: real_base_after as u128,
|
||||
real_quote: real_quote_after as u128,
|
||||
pool_state: pool_state,
|
||||
base_vault: base_vault,
|
||||
quote_vault: quote_vault,
|
||||
mint_token_program: base_token_program,
|
||||
platform_config: platform_config,
|
||||
platform_associated_account: platform_associated_account,
|
||||
creator_associated_account: creator_associated_account,
|
||||
global_config: global_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_dev_trade(
|
||||
is_exact_in: bool,
|
||||
amount_in: u64,
|
||||
amount_out: u64,
|
||||
pool_state: Pubkey,
|
||||
base_vault: Pubkey,
|
||||
quote_vault: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
const DEFAULT_VIRTUAL_BASE: u128 = 1073025605596382;
|
||||
const DEFAULT_VIRTUAL_QUOTE: u128 = 30000852951;
|
||||
let _amount_in = if is_exact_in {
|
||||
amount_in
|
||||
} else {
|
||||
crate::instruction::utils::bonk::get_amount_in(
|
||||
amount_out,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
DEFAULT_VIRTUAL_BASE,
|
||||
DEFAULT_VIRTUAL_QUOTE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
let real_quote = crate::instruction::utils::bonk::get_amount_in_net(
|
||||
amount_in,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
) as u128;
|
||||
let _amount_out = if is_exact_in {
|
||||
crate::instruction::utils::bonk::get_amount_out(
|
||||
amount_in,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
DEFAULT_VIRTUAL_BASE,
|
||||
DEFAULT_VIRTUAL_QUOTE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
) as u128
|
||||
} else {
|
||||
amount_out as u128
|
||||
};
|
||||
let real_base = _amount_out;
|
||||
Self {
|
||||
virtual_base: DEFAULT_VIRTUAL_BASE,
|
||||
virtual_quote: DEFAULT_VIRTUAL_QUOTE,
|
||||
real_base: real_base,
|
||||
real_quote: real_quote,
|
||||
pool_state: pool_state,
|
||||
base_vault: base_vault,
|
||||
quote_vault: quote_vault,
|
||||
mint_token_program: base_token_program,
|
||||
platform_config: platform_config,
|
||||
platform_associated_account: platform_associated_account,
|
||||
creator_associated_account: creator_associated_account,
|
||||
global_config: global_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
usd1_pool: bool,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_address = crate::instruction::utils::bonk::get_pool_pda(
|
||||
mint,
|
||||
if usd1_pool {
|
||||
&crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let pool_data =
|
||||
crate::instruction::utils::bonk::fetch_pool_state(rpc, &pool_address).await?;
|
||||
let token_account = rpc.get_account(&pool_data.base_mint).await?;
|
||||
let platform_associated_account =
|
||||
crate::instruction::utils::bonk::get_platform_associated_account(
|
||||
&pool_data.platform_config,
|
||||
);
|
||||
let creator_associated_account =
|
||||
crate::instruction::utils::bonk::get_creator_associated_account(&pool_data.creator);
|
||||
let platform_associated_account = platform_associated_account.unwrap();
|
||||
let creator_associated_account = creator_associated_account.unwrap();
|
||||
Ok(Self {
|
||||
virtual_base: pool_data.virtual_base as u128,
|
||||
virtual_quote: pool_data.virtual_quote as u128,
|
||||
real_base: pool_data.real_base as u128,
|
||||
real_quote: pool_data.real_quote as u128,
|
||||
pool_state: pool_address,
|
||||
base_vault: pool_data.base_vault,
|
||||
quote_vault: pool_data.quote_vault,
|
||||
mint_token_program: token_account.owner,
|
||||
platform_config: pool_data.platform_config,
|
||||
platform_associated_account,
|
||||
creator_associated_account,
|
||||
global_config: pool_data.global_config,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
use crate::common::nonce_cache::DurableNonceInfo;
|
||||
use crate::common::{GasFeeStrategy, SolanaRpcClient};
|
||||
use crate::swqos::{SwqosClient, TradeType};
|
||||
use crate::trading::MiddlewareManager;
|
||||
use core_affinity::CoreId;
|
||||
use solana_hash::Hash;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::bonk::BonkParams;
|
||||
use super::meteora_damm_v2::MeteoraDammV2Params;
|
||||
use super::pumpfun::PumpFunParams;
|
||||
use super::pumpswap::PumpSwapParams;
|
||||
use super::raydium_amm_v4::RaydiumAmmV4Params;
|
||||
use super::raydium_cpmm::RaydiumCpmmParams;
|
||||
|
||||
/// Concurrency + core binding config for parallel submit (precomputed at SDK init, one param on hot path). Uses Arc so no borrow of SwapParams.
|
||||
#[derive(Clone)]
|
||||
pub struct SenderConcurrencyConfig {
|
||||
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
|
||||
pub effective_core_ids: Arc<Vec<CoreId>>,
|
||||
pub max_sender_concurrency: usize,
|
||||
}
|
||||
|
||||
/// DEX 参数枚举 - 零开销抽象替代 Box<dyn ProtocolParams>
|
||||
#[derive(Clone)]
|
||||
pub enum DexParamEnum {
|
||||
PumpFun(PumpFunParams),
|
||||
PumpSwap(PumpSwapParams),
|
||||
Bonk(BonkParams),
|
||||
RaydiumCpmm(RaydiumCpmmParams),
|
||||
RaydiumAmmV4(RaydiumAmmV4Params),
|
||||
MeteoraDammV2(MeteoraDammV2Params),
|
||||
}
|
||||
|
||||
impl DexParamEnum {
|
||||
/// 获取内部参数的 Any 引用,用于向后兼容的类型检查
|
||||
#[inline]
|
||||
pub fn as_any(&self) -> &dyn std::any::Any {
|
||||
match self {
|
||||
DexParamEnum::PumpFun(p) => p,
|
||||
DexParamEnum::PumpSwap(p) => p,
|
||||
DexParamEnum::Bonk(p) => p,
|
||||
DexParamEnum::RaydiumCpmm(p) => p,
|
||||
DexParamEnum::RaydiumAmmV4(p) => p,
|
||||
DexParamEnum::MeteoraDammV2(p) => p,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Swap parameters
|
||||
#[derive(Clone)]
|
||||
pub struct SwapParams {
|
||||
pub rpc: Option<Arc<SolanaRpcClient>>,
|
||||
pub payer: Arc<Keypair>,
|
||||
pub trade_type: TradeType,
|
||||
pub input_mint: Pubkey,
|
||||
pub input_token_program: Option<Pubkey>,
|
||||
pub output_mint: Pubkey,
|
||||
pub output_token_program: Option<Pubkey>,
|
||||
pub input_amount: Option<u64>,
|
||||
pub slippage_basis_points: Option<u64>,
|
||||
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
pub recent_blockhash: Option<Hash>,
|
||||
pub wait_tx_confirmed: bool,
|
||||
pub protocol_params: DexParamEnum,
|
||||
pub open_seed_optimize: bool,
|
||||
/// Arc<Vec<..>> so cloning from infrastructure is a single Arc clone.
|
||||
pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
|
||||
pub middleware_manager: Option<Arc<MiddlewareManager>>,
|
||||
pub durable_nonce: Option<DurableNonceInfo>,
|
||||
pub with_tip: bool,
|
||||
pub create_input_mint_ata: bool,
|
||||
pub close_input_mint_ata: bool,
|
||||
pub create_output_mint_ata: bool,
|
||||
pub close_output_mint_ata: bool,
|
||||
pub fixed_output_amount: Option<u64>,
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
pub simulate: bool,
|
||||
/// Whether to output SDK logs (from TradeConfig.log_enabled).
|
||||
pub log_enabled: bool,
|
||||
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
|
||||
pub use_dedicated_sender_threads: bool,
|
||||
/// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path.
|
||||
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
|
||||
/// Precomputed at SDK init: min(swqos_count, 2/3*cores). Avoids get_core_ids() on trade hot path.
|
||||
pub max_sender_concurrency: usize,
|
||||
/// Precomputed at SDK init: first max_sender_concurrency CoreIds for job affinity. Arc clone only.
|
||||
pub effective_core_ids: Arc<Vec<CoreId>>,
|
||||
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). When false, skip filter for lower latency.
|
||||
pub check_min_tip: bool,
|
||||
/// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled.
|
||||
pub grpc_recv_us: Option<i64>,
|
||||
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
|
||||
/// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens.
|
||||
/// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
|
||||
/// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes.
|
||||
pub use_exact_sol_amount: Option<bool>,
|
||||
}
|
||||
|
||||
impl SwapParams {
|
||||
/// One struct for execute_parallel: merges sender_thread_cores, effective_core_ids, max_sender_concurrency. Arc clone only.
|
||||
#[inline]
|
||||
pub fn sender_concurrency_config(&self) -> SenderConcurrencyConfig {
|
||||
SenderConcurrencyConfig {
|
||||
sender_thread_cores: self.sender_thread_cores.clone(),
|
||||
effective_core_ids: self.effective_core_ids.clone(),
|
||||
max_sender_concurrency: self.max_sender_concurrency,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SwapParams {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "SwapParams: ...")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// MeteoraDammV2 protocol specific parameters
|
||||
/// Configuration parameters specific to Meteora Damm V2 trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct MeteoraDammV2Params {
|
||||
pub pool: Pubkey,
|
||||
pub token_a_vault: Pubkey,
|
||||
pub token_b_vault: Pubkey,
|
||||
pub token_a_mint: Pubkey,
|
||||
pub token_b_mint: Pubkey,
|
||||
pub token_a_program: Pubkey,
|
||||
pub token_b_program: Pubkey,
|
||||
}
|
||||
|
||||
impl MeteoraDammV2Params {
|
||||
pub fn new(
|
||||
pool: Pubkey,
|
||||
token_a_vault: Pubkey,
|
||||
token_b_vault: Pubkey,
|
||||
token_a_mint: Pubkey,
|
||||
token_b_mint: Pubkey,
|
||||
token_a_program: Pubkey,
|
||||
token_b_program: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
pool,
|
||||
token_a_vault,
|
||||
token_b_vault,
|
||||
token_a_mint,
|
||||
token_b_mint,
|
||||
token_a_program,
|
||||
token_b_program,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_data =
|
||||
crate::instruction::utils::meteora_damm_v2::fetch_pool(rpc, pool_address).await?;
|
||||
let mint_accounts = rpc
|
||||
.get_multiple_accounts(&[pool_data.token_a_mint, pool_data.token_b_mint])
|
||||
.await?;
|
||||
let token_a_program = mint_accounts
|
||||
.get(0)
|
||||
.and_then(|a| a.as_ref())
|
||||
.map(|a| a.owner)
|
||||
.ok_or_else(|| anyhow::anyhow!("Token A mint account not found"))?;
|
||||
let token_b_program = mint_accounts
|
||||
.get(1)
|
||||
.and_then(|a| a.as_ref())
|
||||
.map(|a| a.owner)
|
||||
.ok_or_else(|| anyhow::anyhow!("Token B mint account not found"))?;
|
||||
Ok(Self {
|
||||
pool: *pool_address,
|
||||
token_a_vault: pool_data.token_a_vault,
|
||||
token_b_vault: pool_data.token_b_vault,
|
||||
token_a_mint: pool_data.token_a_mint,
|
||||
token_b_mint: pool_data.token_b_mint,
|
||||
token_a_program,
|
||||
token_b_program,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//! DEX protocol parameter types and [`SwapParams`].
|
||||
|
||||
mod bonk;
|
||||
mod dex_swap;
|
||||
mod meteora_damm_v2;
|
||||
mod pumpfun;
|
||||
mod pumpswap;
|
||||
mod raydium_amm_v4;
|
||||
mod raydium_cpmm;
|
||||
|
||||
pub use bonk::BonkParams;
|
||||
pub use dex_swap::{DexParamEnum, SenderConcurrencyConfig, SwapParams};
|
||||
pub use meteora_damm_v2::MeteoraDammV2Params;
|
||||
pub use pumpfun::PumpFunParams;
|
||||
pub use pumpswap::PumpSwapParams;
|
||||
pub use raydium_amm_v4::RaydiumAmmV4Params;
|
||||
pub use raydium_cpmm::RaydiumCpmmParams;
|
||||
@@ -0,0 +1,270 @@
|
||||
use crate::common::bonding_curve::BondingCurveAccount;
|
||||
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
|
||||
use crate::common::SolanaRpcClient;
|
||||
use crate::instruction::utils::pumpfun::reconcile_mayhem_mode_for_trade;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
/// PumpFun protocol specific parameters
|
||||
/// Configuration parameters specific to PumpFun trading protocol.
|
||||
///
|
||||
/// **Creator vault**: Pump buy/sell pass `creator_vault` = `PDA(["creator-vault", authority])`.
|
||||
/// Usually `authority` is [`BondingCurveAccount::creator`]; with **Creator Rewards Sharing** it is
|
||||
/// `fee_sharing_config_pda(mint)` (see [`fetch_fee_sharing_creator_vault_if_active`](crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active)).
|
||||
/// **Buy/sell**:`creator_vault` 及(若可得)**`tradeEvent` / CPI 日志中的 `creator`** 优先于陈旧的曲线快照;
|
||||
/// ix 组装与链下询价见 [`Self::effective_creator_for_trade`]、[`crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing`]。
|
||||
#[derive(Clone)]
|
||||
pub struct PumpFunParams {
|
||||
pub bonding_curve: Arc<BondingCurveAccount>,
|
||||
pub associated_bonding_curve: Pubkey,
|
||||
/// 最新一笔可观测 trade 的 **`tradeEvent.creator`(日志)**。当 `Some` 且非 default 时,
|
||||
/// **优先于** `bonding_curve.creator` 用于链下 creator-fee 询价与 `creator_vault` 在缺省 ix 时的推导。
|
||||
/// Pump 上 creator 可能随交易推进,调用方应在每次解析到带 `creator` 的 trade 后更新(如 `.with_observed_trade_creator`)。
|
||||
pub observed_trade_creator: Option<Pubkey>,
|
||||
/// Resolved by [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing):
|
||||
/// **显式 `creator_vault`(非 default、非 phantom)永远优先并按原值使用**,不会再用 `creator` 重算覆盖;
|
||||
/// 未传时再按 `fee_sharing_creator_vault_if_active`、`PDA(effective_creator)`(见 [`Self::effective_creator_for_trade`])。
|
||||
pub creator_vault: Pubkey,
|
||||
/// `Some(PDA(["creator-vault", fee_sharing_config]))` when pump-fees `SharingConfig` is **Active**; set by `from_mint_by_rpc` / [`refresh_fee_sharing_creator_vault_from_rpc`](Self::refresh_fee_sharing_creator_vault_from_rpc).
|
||||
pub fee_sharing_creator_vault_if_active: Option<Pubkey>,
|
||||
/// SPL Token or Token-2022 program id owning the **mint** (from gRPC / parser / cache).
|
||||
/// **`Pubkey::default()`**:ix 构建时使用 SDK 默认 **Token-2022**(与多数 Pump.fun 新发一致);显式传入 Legacy 或 Token-2022 id 可覆盖该默认值。
|
||||
pub token_program: Pubkey,
|
||||
/// Whether to close token account when selling, only effective during sell operations
|
||||
pub close_token_account_when_sell: Option<bool>,
|
||||
/// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。
|
||||
/// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)。
|
||||
pub fee_recipient: Pubkey,
|
||||
}
|
||||
|
||||
impl PumpFunParams {
|
||||
pub fn immediate_sell(
|
||||
creator_vault: Pubkey,
|
||||
token_program: Pubkey,
|
||||
close_token_account_when_sell: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
bonding_curve: Arc::new(BondingCurveAccount { ..Default::default() }),
|
||||
associated_bonding_curve: Pubkey::default(),
|
||||
observed_trade_creator: None,
|
||||
creator_vault: creator_vault,
|
||||
fee_sharing_creator_vault_if_active: None,
|
||||
token_program: token_program,
|
||||
close_token_account_when_sell: Some(close_token_account_when_sell),
|
||||
fee_recipient: Pubkey::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||
/// `mayhem_mode`: `Some` when known from Create/Trade event (`is_mayhem_mode` / `mayhem_mode`).
|
||||
/// `None` falls back to detecting Mayhem via reserved fee recipient pubkeys only (not AMM protocol fee accounts).
|
||||
pub fn from_dev_trade(
|
||||
mint: Pubkey,
|
||||
token_amount: u64,
|
||||
max_sol_cost: u64,
|
||||
creator: Pubkey,
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
close_token_account_when_sell: Option<bool>,
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
mayhem_mode: Option<bool>,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient);
|
||||
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
|
||||
bonding_curve,
|
||||
&mint,
|
||||
token_amount,
|
||||
max_sol_cost,
|
||||
creator,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&bonding_curve_account.creator,
|
||||
creator_vault,
|
||||
&mint,
|
||||
None,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve_account),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
observed_trade_creator: (creator != Pubkey::default()).then_some(creator),
|
||||
creator_vault: creator_vault_resolved,
|
||||
fee_sharing_creator_vault_if_active: None,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
fee_recipient,
|
||||
}
|
||||
}
|
||||
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||
///
|
||||
/// `mayhem_mode`:
|
||||
/// - **`Some(v)`**:优先采用 gRPC / `tradeEvent`,但与 **`fee_recipient` 所属池**(Mayhem vs 普通,见 pump-public-docs)不一致时,以 fee 地址为准纠偏,避免链上 `NotAuthorized`。
|
||||
/// - **`None`**:用 `fee_recipient` 是否落在 Mayhem 静态列表推断。
|
||||
pub fn from_trade(
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
mint: Pubkey,
|
||||
creator: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
virtual_token_reserves: u64,
|
||||
virtual_sol_reserves: u64,
|
||||
real_token_reserves: u64,
|
||||
real_sol_reserves: u64,
|
||||
close_token_account_when_sell: Option<bool>,
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
mayhem_mode: Option<bool>,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient);
|
||||
let bonding_curve = BondingCurveAccount::from_trade(
|
||||
bonding_curve,
|
||||
mint,
|
||||
creator,
|
||||
virtual_token_reserves,
|
||||
virtual_sol_reserves,
|
||||
real_token_reserves,
|
||||
real_sol_reserves,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&bonding_curve.creator,
|
||||
creator_vault,
|
||||
&mint,
|
||||
None,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
observed_trade_creator: (creator != Pubkey::default()).then_some(creator),
|
||||
creator_vault: creator_vault_resolved,
|
||||
fee_sharing_creator_vault_if_active: None,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
fee_recipient,
|
||||
}
|
||||
}
|
||||
|
||||
/// 仅 RPC 读取曲线快照;[`Self::observed_trade_creator`] 为 `None`,便于 bot 缓存合并时用粘性的 trade 日志 creator 覆盖陈旧曲线推导。
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let account =
|
||||
crate::instruction::utils::pumpfun::fetch_bonding_curve_account(rpc, mint).await?;
|
||||
let mint_account = rpc.get_account(&mint).await?;
|
||||
let bonding_curve = BondingCurveAccount {
|
||||
discriminator: 0,
|
||||
account: account.1,
|
||||
virtual_token_reserves: account.0.virtual_token_reserves,
|
||||
virtual_sol_reserves: account.0.virtual_sol_reserves,
|
||||
real_token_reserves: account.0.real_token_reserves,
|
||||
real_sol_reserves: account.0.real_sol_reserves,
|
||||
token_total_supply: account.0.token_total_supply,
|
||||
complete: account.0.complete,
|
||||
creator: account.0.creator,
|
||||
is_mayhem_mode: account.0.is_mayhem_mode,
|
||||
is_cashback_coin: account.0.is_cashback_coin,
|
||||
};
|
||||
let associated_bonding_curve = get_associated_token_address_with_program_id(
|
||||
&bonding_curve.account,
|
||||
mint,
|
||||
&mint_account.owner,
|
||||
);
|
||||
let fee_sharing_creator_vault_if_active =
|
||||
crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(rpc, mint)
|
||||
.await?;
|
||||
let creator_vault = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&bonding_curve.creator,
|
||||
Pubkey::default(),
|
||||
mint,
|
||||
fee_sharing_creator_vault_if_active,
|
||||
)
|
||||
.or_else(|| crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator))
|
||||
.unwrap_or_default();
|
||||
Ok(Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
observed_trade_creator: None,
|
||||
creator_vault,
|
||||
fee_sharing_creator_vault_if_active,
|
||||
close_token_account_when_sell: None,
|
||||
token_program: mint_account.owner,
|
||||
fee_recipient: Pubkey::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 链下公式与 **`creator_vault` 推导回退**:**日志/事件 `creator`**(若已写入 `observed_trade_creator`)
|
||||
/// 优先,否则使用 `bonding_curve.creator`。
|
||||
#[inline]
|
||||
pub fn effective_creator_for_trade(&self) -> Pubkey {
|
||||
self.observed_trade_creator
|
||||
.filter(|c| *c != Pubkey::default())
|
||||
.unwrap_or(self.bonding_curve.creator)
|
||||
}
|
||||
|
||||
/// One `getAccount` on pump-fees `SharingConfig` + re-resolves [`Self::creator_vault`]. Call before sell
|
||||
/// when params come from gRPC/cache so migrated fee-sharing mints do not hit Anchor 2006.
|
||||
pub async fn refresh_fee_sharing_creator_vault_from_rpc(
|
||||
mut self,
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
self.fee_sharing_creator_vault_if_active =
|
||||
crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(rpc, mint)
|
||||
.await?;
|
||||
let c = self.effective_creator_for_trade();
|
||||
if let Some(v) =
|
||||
crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&c,
|
||||
self.creator_vault,
|
||||
mint,
|
||||
self.fee_sharing_creator_vault_if_active,
|
||||
)
|
||||
{
|
||||
self.creator_vault = v;
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Updates the cached `creator_vault` field only. Buy/sell ix use [`Self::effective_creator_for_trade`] + resolve.
|
||||
#[inline]
|
||||
pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self {
|
||||
self.creator_vault = creator_vault;
|
||||
self
|
||||
}
|
||||
|
||||
/// 覆盖 **最新一笔 trade 日志中的 `creator`**(`tradeEvent.creator`)。`None` 或 default 会清除覆盖。
|
||||
#[inline]
|
||||
pub fn with_observed_trade_creator(mut self, c: Option<Pubkey>) -> Self {
|
||||
self.observed_trade_creator = c.filter(|x| *x != Pubkey::default());
|
||||
self
|
||||
}
|
||||
|
||||
/// Override fee-sharing vault hint (e.g. from an off-chain indexer). `None` clears the hint.
|
||||
#[inline]
|
||||
pub fn with_fee_sharing_creator_vault_if_active(
|
||||
mut self,
|
||||
fee_sharing_creator_vault_if_active: Option<Pubkey>,
|
||||
) -> Self {
|
||||
self.fee_sharing_creator_vault_if_active = fee_sharing_creator_vault_if_active;
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
|
||||
use crate::common::SolanaRpcClient;
|
||||
use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// PumpSwap Protocol Specific Parameters
|
||||
///
|
||||
/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information,
|
||||
/// token configuration, and transaction amounts.
|
||||
///
|
||||
/// **Performance Note**: If these parameters are not provided, the system will attempt to
|
||||
/// retrieve the relevant information from RPC, which will increase transaction time.
|
||||
/// For optimal performance, it is recommended to provide all necessary parameters in advance.
|
||||
#[derive(Clone)]
|
||||
pub struct PumpSwapParams {
|
||||
/// Liquidity pool address
|
||||
pub pool: Pubkey,
|
||||
/// Base token mint address
|
||||
/// The mint account address of the base token in the trading pair
|
||||
pub base_mint: Pubkey,
|
||||
/// Quote token mint address
|
||||
/// The mint account address of the quote token in the trading pair, usually SOL or USDC
|
||||
pub quote_mint: Pubkey,
|
||||
/// Pool base token account
|
||||
pub pool_base_token_account: Pubkey,
|
||||
/// Pool quote token account
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
/// Base token reserves in the pool
|
||||
pub pool_base_token_reserves: u64,
|
||||
/// Quote token reserves in the pool
|
||||
pub pool_quote_token_reserves: u64,
|
||||
/// Coin creator vault ATA
|
||||
pub coin_creator_vault_ata: Pubkey,
|
||||
/// Coin creator vault authority
|
||||
pub coin_creator_vault_authority: Pubkey,
|
||||
/// Token program ID
|
||||
pub base_token_program: Pubkey,
|
||||
/// Quote token program ID
|
||||
pub quote_token_program: Pubkey,
|
||||
/// Whether the pool is in mayhem mode
|
||||
pub is_mayhem_mode: bool,
|
||||
/// Pool [`Pool::coin_creator`](crate::instruction::utils::pumpswap_types::Pool). Used for PumpSwap
|
||||
/// `remaining_accounts`: **`pool-v2` is appended only when this is not `Pubkey::default()`
|
||||
/// (matches `@pump-fun/pump-swap-sdk`); wrong flag causes buys to revert with buyback recipient errors (e.g. 6053).
|
||||
pub coin_creator: Pubkey,
|
||||
/// Whether the pool's coin has cashback enabled
|
||||
pub is_cashback_coin: bool,
|
||||
/// Cashback fee in basis points (from trade events / sol-parser-sdk). For quote-in buy and base-in sell
|
||||
/// math, this is summed with [`COIN_CREATOR_FEE_BASIS_POINTS`](crate::instruction::utils::pumpswap::accounts::COIN_CREATOR_FEE_BASIS_POINTS)
|
||||
/// when a creator vault applies — matching on-chain treating creator + cashback as one fee bucket.
|
||||
/// Use `0` when unknown (e.g. RPC-only pool decode has no per-mint cashback bps).
|
||||
pub cashback_fee_basis_points: u64,
|
||||
}
|
||||
|
||||
impl PumpSwapParams {
|
||||
pub fn new(
|
||||
pool: Pubkey,
|
||||
base_mint: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
fee_recipient: Pubkey,
|
||||
coin_creator: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP;
|
||||
Self {
|
||||
pool,
|
||||
base_mint,
|
||||
quote_mint,
|
||||
pool_base_token_account,
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
quote_token_program,
|
||||
is_mayhem_mode,
|
||||
coin_creator,
|
||||
is_cashback_coin,
|
||||
cashback_fee_basis_points,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fast-path constructor for building PumpSwap parameters directly from decoded
|
||||
/// trade/event data and the accompanying instruction accounts, avoiding RPC
|
||||
/// lookups and associated latency. Token program IDs should be sourced from
|
||||
/// the instruction accounts themselves to respect Token Program vs Token-2022
|
||||
/// differences.
|
||||
///
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin`
|
||||
/// from the event so that buy/sell instructions include the correct remaining
|
||||
/// accounts for cashback.
|
||||
pub fn from_trade(
|
||||
pool: Pubkey,
|
||||
base_mint: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
fee_recipient: Pubkey,
|
||||
coin_creator: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
pool,
|
||||
base_mint,
|
||||
quote_mint,
|
||||
pool_base_token_account,
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
quote_token_program,
|
||||
fee_recipient,
|
||||
coin_creator,
|
||||
is_cashback_coin,
|
||||
cashback_fee_basis_points,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("No pool found for mint"));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_data = crate::instruction::utils::pumpswap::fetch_pool(rpc, pool_address).await?;
|
||||
Self::from_pool_data(rpc, pool_address, &pool_data).await
|
||||
}
|
||||
|
||||
/// Build params from an already-decoded Pool, only fetching token balances.
|
||||
///
|
||||
/// Saves 1 RPC `getAccount` call vs `from_pool_address_by_rpc` when pool data
|
||||
/// is already available (e.g. from `pumpswap::find_by_mint` which returns the
|
||||
/// decoded Pool).
|
||||
pub async fn from_pool_data(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
pool_data: &crate::instruction::utils::pumpswap_types::Pool,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let (pool_base_token_reserves, pool_quote_token_reserves) =
|
||||
crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?;
|
||||
let creator = pool_data.coin_creator;
|
||||
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
|
||||
creator,
|
||||
pool_data.quote_mint,
|
||||
);
|
||||
let coin_creator_vault_authority =
|
||||
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
|
||||
|
||||
let base_token_program_ata = get_associated_token_address_with_program_id(
|
||||
pool_address,
|
||||
&pool_data.base_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
let quote_token_program_ata = get_associated_token_address_with_program_id(
|
||||
pool_address,
|
||||
&pool_data.quote_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
pool: *pool_address,
|
||||
base_mint: pool_data.base_mint,
|
||||
quote_mint: pool_data.quote_mint,
|
||||
pool_base_token_account: pool_data.pool_base_token_account,
|
||||
pool_quote_token_account: pool_data.pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_2022
|
||||
},
|
||||
is_cashback_coin: pool_data.is_cashback_coin,
|
||||
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_2022
|
||||
},
|
||||
is_mayhem_mode: pool_data.is_mayhem_mode,
|
||||
coin_creator: pool_data.coin_creator,
|
||||
cashback_fee_basis_points: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use crate::trading::common::get_multi_token_balances;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// RaydiumCpmm protocol specific parameters
|
||||
/// Configuration parameters specific to Raydium CPMM trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct RaydiumAmmV4Params {
|
||||
/// AMM pool address
|
||||
pub amm: Pubkey,
|
||||
/// Base token (coin) mint address
|
||||
pub coin_mint: Pubkey,
|
||||
/// Quote token (pc) mint address
|
||||
pub pc_mint: Pubkey,
|
||||
/// Pool's coin token account address
|
||||
pub token_coin: Pubkey,
|
||||
/// Pool's pc token account address
|
||||
pub token_pc: Pubkey,
|
||||
/// Current coin reserve amount in the pool
|
||||
pub coin_reserve: u64,
|
||||
/// Current pc reserve amount in the pool
|
||||
pub pc_reserve: u64,
|
||||
}
|
||||
|
||||
impl RaydiumAmmV4Params {
|
||||
pub fn new(
|
||||
amm: Pubkey,
|
||||
coin_mint: Pubkey,
|
||||
pc_mint: Pubkey,
|
||||
token_coin: Pubkey,
|
||||
token_pc: Pubkey,
|
||||
coin_reserve: u64,
|
||||
pc_reserve: u64,
|
||||
) -> Self {
|
||||
Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve }
|
||||
}
|
||||
pub async fn from_amm_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
amm: Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?;
|
||||
let (coin_reserve, pc_reserve) =
|
||||
get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
|
||||
Ok(Self {
|
||||
amm,
|
||||
coin_mint: amm_info.coin_mint,
|
||||
pc_mint: amm_info.pc_mint,
|
||||
token_coin: amm_info.token_coin,
|
||||
token_pc: amm_info.token_pc,
|
||||
coin_reserve,
|
||||
pc_reserve,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// RaydiumCpmm protocol specific parameters
|
||||
/// Configuration parameters specific to Raydium CPMM trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct RaydiumCpmmParams {
|
||||
/// Pool address
|
||||
pub pool_state: Pubkey,
|
||||
/// Amm config address
|
||||
pub amm_config: Pubkey,
|
||||
/// Base token mint address
|
||||
pub base_mint: Pubkey,
|
||||
/// Quote token mint address
|
||||
pub quote_mint: Pubkey,
|
||||
/// Base token reserve amount in the pool
|
||||
pub base_reserve: u64,
|
||||
/// Quote token reserve amount in the pool
|
||||
pub quote_reserve: u64,
|
||||
/// Base token vault address
|
||||
pub base_vault: Pubkey,
|
||||
/// Quote token vault address
|
||||
pub quote_vault: Pubkey,
|
||||
/// Base token program ID
|
||||
pub base_token_program: Pubkey,
|
||||
/// Quote token program ID
|
||||
pub quote_token_program: Pubkey,
|
||||
/// Observation state account
|
||||
pub observation_state: Pubkey,
|
||||
}
|
||||
|
||||
impl RaydiumCpmmParams {
|
||||
pub fn from_trade(
|
||||
pool_state: Pubkey,
|
||||
amm_config: Pubkey,
|
||||
input_token_mint: Pubkey,
|
||||
output_token_mint: Pubkey,
|
||||
input_vault: Pubkey,
|
||||
output_vault: Pubkey,
|
||||
input_token_program: Pubkey,
|
||||
output_token_program: Pubkey,
|
||||
observation_state: Pubkey,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
pool_state: pool_state,
|
||||
amm_config: amm_config,
|
||||
base_mint: input_token_mint,
|
||||
quote_mint: output_token_mint,
|
||||
base_reserve: base_reserve,
|
||||
quote_reserve: quote_reserve,
|
||||
base_vault: input_vault,
|
||||
quote_vault: output_vault,
|
||||
base_token_program: input_token_program,
|
||||
quote_token_program: output_token_program,
|
||||
observation_state: observation_state,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool =
|
||||
crate::instruction::utils::raydium_cpmm::fetch_pool_state(rpc, pool_address).await?;
|
||||
let (token0_balance, token1_balance) =
|
||||
crate::instruction::utils::raydium_cpmm::get_pool_token_balances(
|
||||
rpc,
|
||||
pool_address,
|
||||
&pool.token0_mint,
|
||||
&pool.token1_mint,
|
||||
)
|
||||
.await?;
|
||||
Ok(Self {
|
||||
pool_state: *pool_address,
|
||||
amm_config: pool.amm_config,
|
||||
base_mint: pool.token0_mint,
|
||||
quote_mint: pool.token1_mint,
|
||||
base_reserve: token0_balance,
|
||||
quote_reserve: token1_balance,
|
||||
base_vault: pool.token0_vault,
|
||||
quote_vault: pool.token1_vault,
|
||||
base_token_program: pool.token0_program,
|
||||
quote_token_program: pool.token1_program,
|
||||
observation_state: pool.observation_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,9 @@ pub const fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64
|
||||
/// * basis_points = 500 -> 5% slippage
|
||||
#[inline(always)]
|
||||
pub const fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 {
|
||||
if amount == 0 {
|
||||
return 0;
|
||||
}
|
||||
if amount <= basis_points / 10000 {
|
||||
1
|
||||
} else {
|
||||
|
||||
+31
-12
@@ -6,6 +6,21 @@ use crate::instruction::utils::pumpswap::accounts::{
|
||||
};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// Creator-side fee bps: fixed coin-creator fee when a creator vault applies, plus optional
|
||||
/// cashback fee bps for cashback-enabled coins (see Pump AMM / parser event field).
|
||||
#[inline]
|
||||
pub(crate) fn creator_side_fee_basis_points(
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> u64 {
|
||||
let creator_bps = if *coin_creator == Pubkey::default() {
|
||||
0
|
||||
} else {
|
||||
COIN_CREATOR_FEE_BASIS_POINTS
|
||||
};
|
||||
creator_bps.saturating_add(cashback_fee_basis_points)
|
||||
}
|
||||
|
||||
/// Result for buying base tokens with base amount input
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BuyBaseInputResult {
|
||||
@@ -58,6 +73,7 @@ pub struct SellQuoteInputResult {
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins (from on-chain / events); use `0` if unknown
|
||||
///
|
||||
/// # Returns
|
||||
/// * `BuyBaseInputResult` containing quote amounts and slippage calculations
|
||||
@@ -67,6 +83,7 @@ pub fn buy_base_input_internal(
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<BuyBaseInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
@@ -89,11 +106,9 @@ pub fn buy_base_input_internal(
|
||||
let lp_fee = compute_fee(quote_amount_in as u128, LP_FEE_BASIS_POINTS as u128) as u64;
|
||||
let protocol_fee =
|
||||
compute_fee(quote_amount_in as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64;
|
||||
let coin_creator_fee = if *coin_creator == Pubkey::default() {
|
||||
0
|
||||
} else {
|
||||
compute_fee(quote_amount_in as u128, COIN_CREATOR_FEE_BASIS_POINTS as u128) as u64
|
||||
};
|
||||
let creator_bps =
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128;
|
||||
let coin_creator_fee = compute_fee(quote_amount_in as u128, creator_bps) as u64;
|
||||
let total_quote = quote_amount_in + lp_fee + protocol_fee + coin_creator_fee;
|
||||
|
||||
// Calculate max quote with slippage
|
||||
@@ -114,6 +129,7 @@ pub fn buy_base_input_internal(
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||
///
|
||||
/// # Returns
|
||||
/// * `BuyQuoteInputResult` containing base amount and slippage calculations
|
||||
@@ -123,6 +139,7 @@ pub fn buy_quote_input_internal(
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<BuyQuoteInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
@@ -131,7 +148,7 @@ pub fn buy_quote_input_internal(
|
||||
// Calculate total fee basis points
|
||||
let total_fee_bps = LP_FEE_BASIS_POINTS
|
||||
+ PROTOCOL_FEE_BASIS_POINTS
|
||||
+ if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS };
|
||||
+ creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points);
|
||||
let denominator = 10_000 + total_fee_bps;
|
||||
|
||||
// Calculate effective quote amount after fees
|
||||
@@ -165,6 +182,7 @@ pub fn buy_quote_input_internal(
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||
///
|
||||
/// # Returns
|
||||
/// * `SellBaseInputResult` containing quote amounts and slippage calculations
|
||||
@@ -174,6 +192,7 @@ pub fn sell_base_input_internal(
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<SellBaseInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
@@ -187,11 +206,9 @@ pub fn sell_base_input_internal(
|
||||
let lp_fee = compute_fee(quote_amount_out as u128, LP_FEE_BASIS_POINTS as u128) as u64;
|
||||
let protocol_fee =
|
||||
compute_fee(quote_amount_out as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64;
|
||||
let coin_creator_fee = if *coin_creator == Pubkey::default() {
|
||||
0
|
||||
} else {
|
||||
compute_fee(quote_amount_out as u128, COIN_CREATOR_FEE_BASIS_POINTS as u128) as u64
|
||||
};
|
||||
let creator_bps =
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128;
|
||||
let coin_creator_fee = compute_fee(quote_amount_out as u128, creator_bps) as u64;
|
||||
|
||||
// Calculate final quote after fees
|
||||
let total_fees = lp_fee + protocol_fee + coin_creator_fee;
|
||||
@@ -234,6 +251,7 @@ fn calculate_quote_amount_out(
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||
///
|
||||
/// # Returns
|
||||
/// * `SellQuoteInputResult` containing base amount and slippage calculations
|
||||
@@ -243,6 +261,7 @@ pub fn sell_quote_input_internal(
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<SellQuoteInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
@@ -256,7 +275,7 @@ pub fn sell_quote_input_internal(
|
||||
quote,
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS },
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
|
||||
);
|
||||
|
||||
// Calculate base amount needed using inverse constant product formula
|
||||
|
||||
Reference in New Issue
Block a user