From b8325b2b52dfcec101e773e9b2da08a293e5ad9a Mon Sep 17 00:00:00 2001 From: 0xfnzero <0xfnzero@users.noreply.github.com> Date: Sun, 7 Jun 2026 22:22:26 +0800 Subject: [PATCH] Release v4.0.18 --- Cargo.toml | 3 +- README.md | 82 ++- README_CN.md | 82 ++- docs/TRADING_PARAMETERS.md | 67 ++- docs/TRADING_PARAMETERS_CN.md | 67 ++- examples/simple_trading/Cargo.toml | 10 + examples/simple_trading/src/main.rs | 103 ++++ src/client/mod.rs | 636 ++++++++++++++++++++++ src/instruction/pumpfun.rs | 84 ++- src/lib.rs | 5 +- src/trading/common/transaction_builder.rs | 71 +++ 11 files changed, 1140 insertions(+), 70 deletions(-) create mode 100644 examples/simple_trading/Cargo.toml create mode 100644 examples/simple_trading/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 34a8401..63da783 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "4.0.17" +version = "4.0.18" edition = "2021" authors = [ "William ", @@ -34,6 +34,7 @@ members = [ "examples/cli_trading", "examples/gas_fee_strategy", "examples/meteora_damm_v2_direct_trading", + "examples/simple_trading", ] [lib] diff --git a/README.md b/README.md index 5959867..978dc69 100644 --- a/README.md +++ b/README.md @@ -196,40 +196,73 @@ gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, For detailed information about all trading parameters, see the [Trading Parameters Reference](docs/TRADING_PARAMETERS.md). ```rust -// Import DexParamEnum for protocol-specific parameters -use sol_trade_sdk::trading::core::params::DexParamEnum; - -let buy_params = sol_trade_sdk::TradeBuyParams { - dex_type: DexType::PumpSwap, - input_token_type: TradeTokenType::WSOL, - mint: mint_pubkey, - input_token_amount: buy_sol_amount, - slippage_basis_points: slippage_basis_points, - recent_blockhash: Some(recent_blockhash), - // Use DexParamEnum for type-safe protocol parameters (zero-overhead abstraction) - extension_params: DexParamEnum::PumpSwap(params.clone()), - address_lookup_table_account: None, - wait_transaction_confirmed: true, - create_input_token_ata: true, - close_input_token_ata: true, - create_mint_ata: true, - durable_nonce: None, - fixed_output_token_amount: None, // Optional: specify exact output amount - gas_fee_strategy: gas_fee_strategy.clone(), // Gas fee strategy configuration - simulate: false, // Set to true for simulation only - use_exact_sol_amount: None, // Use exact SOL input for PumpFun/PumpSwap (defaults to true) +use sol_trade_sdk::{ + AccountPolicy, BuyAmount, DexType, SimpleBuyParams, TradeTokenType, + trading::core::params::DexParamEnum, }; + +let buy_params = SimpleBuyParams::new( + DexType::PumpFun, + // Token used to pay. For PumpFun V2 SOL/WSOL quote pools, keep this as SOL + // when you want to spend native SOL; the SDK will still use V2 accounts. + TradeTokenType::SOL, + // Mint of the meme/token you want to buy. + mint_pubkey, + // Regular PumpFun/PumpSwap buy. The SDK estimates token output and applies + // slippage to the maximum quote cost. + BuyAmount::WithMaxInput { quote_amount: buy_sol_amount }, + // Protocol state from parser/RPC cache, for example PumpFunParams::from_trade(...). + DexParamEnum::PumpFun(pumpfun_params), + // Pass a cached recent blockhash; the SDK does not fetch it on the hot path. + recent_blockhash, + gas_fee_strategy.clone(), +) +// 300 = 3%. +.slippage_basis_points(300) +// For bots/sniping: assume ATAs are already prepared and keep the tx small. +.account_policy(AccountPolicy::HotPathMinimal); ``` #### 4. Execute Trading ```rust -client.buy(buy_params).await?; +client.buy_simple(buy_params).await?; ``` ### ⚡ Trading Parameters -For comprehensive information about all trading parameters including `TradeBuyParams` and `TradeSellParams`, see the dedicated [Trading Parameters Reference](docs/TRADING_PARAMETERS.md). +Use `SimpleBuyParams` / `SimpleSellParams` for new integrations. They describe trading intent and hide low-level ATA flags. Most users only choose: + +- `pay_with` / `receive_as`: quote token direction. Use `SOL` when the wallet spends or receives native SOL. For PumpFun V2 SOL-paired pools whose quote mint is WSOL, still use `SOL` if you want native SOL settlement. +- `amount`: trade sizing intent. Pick one enum variant instead of combining `input_token_amount`, `fixed_output_token_amount`, and `use_exact_sol_amount`. +- `account_policy`: account creation behavior. Bots usually use `HotPathMinimal`; normal apps can keep the default `Auto`. + +| Parameter | Meaning | Recommendation | +|---|---|---| +| `BuyAmount::ExactInput(amount)` | Spend exactly this quote amount; slippage protects minimum output. | Normal swaps | +| `BuyAmount::WithMaxInput { quote_amount }` | Regular PumpFun/PumpSwap buy with slippage applied to max quote cost. | Sniping/arbitrage | +| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | Buy an exact token amount with a max quote budget. | Exact-output workflows | +| `SellAmount::ExactInput(amount)` | Sell exactly this token amount. | Normal sells | +| `SellAmount::ExactOutput { output_amount, max_input_amount }` | Receive an exact quote amount while limiting token input, where the DEX supports it. | Exact-output sells | +| `AccountPolicy::Auto` | SDK creates practical ATAs when needed. | General usage | +| `AccountPolicy::HotPathMinimal` | Avoid ATA create/close instructions in the trade tx. | Bots, sniping, latency-sensitive flows | +| `AccountPolicy::CreateMissing` | Include ATA creation instructions where possible. | Convenience over transaction size | +| `AccountPolicy::AssumePrepared` | Caller prepared every required ATA. | Deterministic advanced flows | + +Optional builder methods: + +| Method | Meaning | +|---|---| +| `.slippage_basis_points(300)` | Set slippage. `300` means 3%. | +| `.address_lookup_table_account(alt)` | Attach an ALT to reduce transaction size. Useful for large PumpFun V2 transactions. | +| `.wait_tx_confirmed(true)` | Return only after confirmation. Usually disabled for fastest submit paths. | +| `.wait_for_all_submits(true)` | In fast-submit mode, wait for all SWQoS lane responses and return all signatures. | +| `.simulate(true)` | Build and simulate the transaction instead of sending it. | +| `.grpc_recv_us(ts)` | Attach upstream receive timestamp for latency tracing. | +| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | Use durable nonce instead of `recent_blockhash`. | +| `SimpleSellParams::with_tip(false)` | Disable relay tips for sells. Buys use the gas fee strategy/tip settings. | + +`TradeBuyParams` and `TradeSellParams` remain available as advanced low-level APIs. See the dedicated [Trading Parameters Reference](docs/TRADING_PARAMETERS.md). #### About ShredStream @@ -240,6 +273,7 @@ Please ensure that the parameters your trading logic depends on are available in | Description | Run Command | Source Code | |-------------|-------------|-------------| +| Simple buy/sell parameter API | `cargo run --package simple_trading` | [examples/simple_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/simple_trading/src/main.rs) | | Create and configure TradingClient instance | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) | | Share infrastructure across multiple wallets | `cargo run --package shared_infrastructure` | [examples/shared_infrastructure](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/shared_infrastructure/src/main.rs) | | PumpFun token sniping trading | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) | diff --git a/README_CN.md b/README_CN.md index 64b5d41..c94d430 100755 --- a/README_CN.md +++ b/README_CN.md @@ -195,40 +195,73 @@ gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 有关所有交易参数的详细信息,请参阅 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。 ```rust -// 导入 DexParamEnum 用于协议特定参数 -use sol_trade_sdk::trading::core::params::DexParamEnum; - -let buy_params = sol_trade_sdk::TradeBuyParams { - dex_type: DexType::PumpSwap, - input_token_type: TradeTokenType::WSOL, - mint: mint_pubkey, - input_token_amount: buy_sol_amount, - slippage_basis_points: slippage_basis_points, - recent_blockhash: Some(recent_blockhash), - // 使用 DexParamEnum 实现类型安全的协议参数(零开销抽象) - extension_params: DexParamEnum::PumpSwap(params.clone()), - address_lookup_table_account: None, - wait_transaction_confirmed: true, - create_input_token_ata: true, - close_input_token_ata: true, - create_mint_ata: true, - durable_nonce: None, - fixed_output_token_amount: None, // 可选:指定精确输出数量 - gas_fee_strategy: gas_fee_strategy.clone(), // Gas 费用策略配置 - simulate: false, // 设为 true 仅进行模拟 - use_exact_sol_amount: None, // 对 PumpFun/PumpSwap 使用精确 SOL 输入(默认为 true) +use sol_trade_sdk::{ + AccountPolicy, BuyAmount, DexType, SimpleBuyParams, TradeTokenType, + trading::core::params::DexParamEnum, }; + +let buy_params = SimpleBuyParams::new( + DexType::PumpFun, + // 支付币种。PumpFun V2 的 SOL/WSOL quote 池,如果你想花原生 SOL, + // 这里仍然传 SOL;SDK 内部会按 V2 账户布局处理。 + TradeTokenType::SOL, + // 要买入的 meme/token mint。 + mint_pubkey, + // 常规 PumpFun/PumpSwap buy。SDK 先估算能买到多少 token, + // 再把滑点应用到最大 quote 成本上。 + BuyAmount::WithMaxInput { quote_amount: buy_sol_amount }, + // 协议状态参数,通常来自 parser/RPC 缓存,例如 PumpFunParams::from_trade(...)。 + DexParamEnum::PumpFun(pumpfun_params), + // 传入外部缓存的 recent_blockhash;SDK 不在热路径里临时获取。 + recent_blockhash, + gas_fee_strategy.clone(), +) +// 300 = 3%。 +.slippage_basis_points(300) +// Bot/狙击推荐:假设 ATA 已提前准备好,交易内不创建/关闭 ATA,体积更小。 +.account_policy(AccountPolicy::HotPathMinimal); ``` #### 4. 执行交易 ```rust -client.buy(buy_params).await?; +client.buy_simple(buy_params).await?; ``` ### ⚡ 交易参数 -有关所有交易参数(包括 `TradeBuyParams` 和 `TradeSellParams`)的详细信息,请参阅专门的 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。 +新接入建议优先使用 `SimpleBuyParams` / `SimpleSellParams`。它们描述交易意图,SDK 内部处理底层 ATA 参数。多数用户只需要选择: + +- `pay_with` / `receive_as`:买入时用什么 quote 支付,卖出时收什么 quote。钱包实际花/收原生 SOL 就传 `SOL`。PumpFun V2 的 SOL 配对池虽然 `quote_mint` 是 WSOL,但你想用原生 SOL 结算时这里仍传 `SOL`。 +- `amount`:交易数量语义。用一个枚举表达意图,不再同时理解 `input_token_amount`、`fixed_output_token_amount`、`use_exact_sol_amount`。 +- `account_policy`:账户创建策略。Bot 通常用 `HotPathMinimal`;普通应用可以保留默认 `Auto`。 + +| 参数 | 含义 | 推荐场景 | +|---|---|---| +| `BuyAmount::ExactInput(amount)` | 精确花费指定 quote 数量;滑点保护最小买到数量。 | 普通买入 | +| `BuyAmount::WithMaxInput { quote_amount }` | PumpFun/PumpSwap 常规 buy,滑点作用在最大 quote 成本上。 | 狙击、套利 | +| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | 精确买到指定 token 数量,并限制最大 quote 成本。 | 精确输出 | +| `SellAmount::ExactInput(amount)` | 精确卖出指定 token 数量。 | 普通卖出 | +| `SellAmount::ExactOutput { output_amount, max_input_amount }` | 精确收到指定 quote 数量,并限制最多卖出多少 token;取决于 DEX 是否支持。 | 精确输出卖出 | +| `AccountPolicy::Auto` | SDK 按交易路径创建必要 ATA。 | 普通用户 | +| `AccountPolicy::HotPathMinimal` | 交易内避免创建/关闭 ATA。 | Bot、狙击、低延迟 | +| `AccountPolicy::CreateMissing` | 尽量在交易内创建缺失 ATA。 | 优先方便,不追求最小交易体积 | +| `AccountPolicy::AssumePrepared` | 调用方保证所有 ATA 已准备好。 | 高级确定性流程 | + +可选 builder 方法: + +| 方法 | 含义 | +|---|---| +| `.slippage_basis_points(300)` | 设置滑点。`300` 表示 3%。 | +| `.address_lookup_table_account(alt)` | 传入 ALT 以减少交易体积。PumpFun V2 交易较大时很有用。 | +| `.wait_tx_confirmed(true)` | 等链上确认后再返回。追求最快提交时通常关闭。 | +| `.wait_for_all_submits(true)` | fast-submit 模式下等待所有 SWQoS 通道返回,并拿到全部签名。 | +| `.simulate(true)` | 只构建并模拟交易,不真正发送。 | +| `.grpc_recv_us(ts)` | 传入上游收到事件的微秒时间戳,用于延迟追踪。 | +| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | 使用 durable nonce,不使用 `recent_blockhash`。 | +| `SimpleSellParams::with_tip(false)` | 关闭卖出交易 relay tip。买入的 tip 使用 gas fee strategy 控制。 | + +`TradeBuyParams` 和 `TradeSellParams` 仍保留为高级低层接口。详细说明见 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。 #### 关于shredstream @@ -239,6 +272,7 @@ client.buy(buy_params).await?; | 描述 | 运行命令 | 源码路径 | |------|---------|----------| +| 简化买卖参数 API | `cargo run --package simple_trading` | [examples/simple_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/simple_trading/src/main.rs) | | 创建和配置 TradingClient 实例 | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) | | 多钱包共享基础设施 | `cargo run --package shared_infrastructure` | [examples/shared_infrastructure](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/shared_infrastructure/src/main.rs) | | PumpFun 代币狙击交易 | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) | diff --git a/docs/TRADING_PARAMETERS.md b/docs/TRADING_PARAMETERS.md index b1f3983..a74bc7f 100644 --- a/docs/TRADING_PARAMETERS.md +++ b/docs/TRADING_PARAMETERS.md @@ -4,14 +4,79 @@ This document provides a comprehensive reference for all trading parameters used ## 📋 Table of Contents +- [SimpleBuyParams / SimpleSellParams](#simplebuyparams--simplesellparams) - [TradeBuyParams](#tradebuyparams) - [TradeSellParams](#tradesellparams) - [Parameter Categories](#parameter-categories) - [Important Notes](#important-notes) +## SimpleBuyParams / SimpleSellParams + +Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the public API focused on trading intent and map to the lower-level `TradeBuyParams` / `TradeSellParams` internally. + +### SimpleBuyParams + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `dex_type` | `DexType` | ✅ | Protocol to trade through, for example `DexType::PumpFun`. | +| `pay_with` | `TradeTokenType` | ✅ | Quote token used to pay for the buy. Use `SOL` when the wallet spends native SOL. For PumpFun V2 SOL/WSOL quote pools, still use `SOL` if you want native SOL settlement. | +| `mint` | `Pubkey` | ✅ | Mint of the token being bought. | +| `amount` | `BuyAmount` | ✅ | Buy sizing intent. Choose one enum variant instead of combining low-level amount flags. | +| `extension_params` | `DexParamEnum` | ✅ | Protocol state from parser/RPC cache, such as `DexParamEnum::PumpFun(PumpFunParams::from_trade(...))`. | +| `recent_blockhash` | `Hash` | ✅ for `new` | Cached recent blockhash for non-nonce transactions. The SDK does not fetch this on the hot path. | +| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Compute unit price/limit and relay tip configuration. | +| `slippage_basis_points` | `Option` | ❌ | Optional slippage override. `100` means 1%. | +| `account_policy` | `AccountPolicy` | ❌ | ATA creation/close behavior. Default is `Auto`. | +| `address_lookup_table_account` | `Option` | ❌ | Optional ALT to reduce transaction size. | +| `wait_tx_confirmed` | `bool` | ❌ | Whether to wait for chain confirmation before returning. Default is `false`. | +| `wait_for_all_submits` | `bool` | ❌ | Fast-submit mode only: wait for every SWQoS lane response and return all signatures. | +| `durable_nonce` | `Option` | ❌ | Durable nonce info. Use `SimpleBuyParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. | +| `simulate` | `bool` | ❌ | Build and simulate instead of submitting. Default is `false`. | +| `grpc_recv_us` | `Option` | ❌ | Upstream receive timestamp in microseconds for latency tracing. | + +### SimpleSellParams + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `dex_type` | `DexType` | ✅ | Protocol to trade through, for example `DexType::PumpFun`. | +| `receive_as` | `TradeTokenType` | ✅ | Quote token to receive from the sell. Use `SOL` when you want native SOL output. | +| `mint` | `Pubkey` | ✅ | Mint of the token being sold. | +| `amount` | `SellAmount` | ✅ | Sell sizing intent. | +| `extension_params` | `DexParamEnum` | ✅ | Protocol state from parser/RPC cache. | +| `recent_blockhash` | `Hash` | ✅ for `new` | Cached recent blockhash for non-nonce transactions. | +| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Compute unit price/limit and relay tip configuration. | +| `slippage_basis_points` | `Option` | ❌ | Optional slippage override. `100` means 1%. | +| `account_policy` | `AccountPolicy` | ❌ | ATA creation/close behavior. Default is `Auto`. | +| `address_lookup_table_account` | `Option` | ❌ | Optional ALT to reduce transaction size. | +| `wait_tx_confirmed` | `bool` | ❌ | Whether to wait for chain confirmation before returning. Default is `false`. | +| `wait_for_all_submits` | `bool` | ❌ | Fast-submit mode only: wait for every SWQoS lane response and return all signatures. | +| `durable_nonce` | `Option` | ❌ | Durable nonce info. Use `SimpleSellParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. | +| `simulate` | `bool` | ❌ | Build and simulate instead of submitting. Default is `false`. | +| `with_tip` | `bool` | ❌ | Whether sells include relay tips. Default is `true`; set with `.with_tip(false)`. | +| `grpc_recv_us` | `Option` | ❌ | Upstream receive timestamp in microseconds for latency tracing. | + +### Amount Selection + +| Variant | Meaning | Low-level mapping | +|---------|---------|-------------------| +| `BuyAmount::ExactInput(amount)` | Spend exactly this quote amount; slippage protects minimum token output. | `input_token_amount = amount`, `use_exact_sol_amount = Some(true)` | +| `BuyAmount::WithMaxInput { quote_amount }` | Regular PumpFun/PumpSwap buy. The SDK estimates output and applies slippage to max quote cost. | `input_token_amount = quote_amount`, `use_exact_sol_amount = Some(false)` | +| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | Buy an exact token amount while limiting max quote input. | `fixed_output_token_amount = Some(output_amount)`, `input_token_amount = max_input_amount` | +| `SellAmount::ExactInput(amount)` | Sell exactly this token amount; slippage protects minimum quote output. | `input_token_amount = amount` | +| `SellAmount::ExactOutput { output_amount, max_input_amount }` | Receive an exact quote amount while limiting token input, where supported. | `fixed_output_token_amount = Some(output_amount)`, `input_token_amount = max_input_amount` | + +### AccountPolicy + +| Variant | Behavior | Use when | +|---------|----------|----------| +| `Auto` | SDK creates practical ATAs when needed. Buy creates the target mint ATA; sell creates the output ATA for non-SOL outputs. | Normal apps and manual trading tools. | +| `HotPathMinimal` | No ATA create/close instructions in the trade transaction. | Bots, sniping, arbitrage, and any path sensitive to transaction size. | +| `CreateMissing` | Include ATA creation where possible. | Convenience matters more than smallest transaction size. | +| `AssumePrepared` | Do not create or close token accounts; caller prepared everything. | Advanced deterministic flows. | + ## TradeBuyParams -The `TradeBuyParams` struct contains all parameters required for executing buy orders across different DEX protocols. +`TradeBuyParams` is the advanced low-level buy API. New integrations should prefer `SimpleBuyParams` unless they need direct control over individual ATA flags. ### Basic Trading Parameters diff --git a/docs/TRADING_PARAMETERS_CN.md b/docs/TRADING_PARAMETERS_CN.md index f644329..2c0fd57 100644 --- a/docs/TRADING_PARAMETERS_CN.md +++ b/docs/TRADING_PARAMETERS_CN.md @@ -4,14 +4,79 @@ ## 📋 目录 +- [SimpleBuyParams / SimpleSellParams](#simplebuyparams--simplesellparams) - [TradeBuyParams](#tradebuyparams) - [TradeSellParams](#tradesellparams) - [参数分类](#参数分类) - [重要说明](#重要说明) +## SimpleBuyParams / SimpleSellParams + +新接入优先使用 `SimpleBuyParams` 和 `SimpleSellParams`。这两个结构体描述交易意图,SDK 内部会转换成低层 `TradeBuyParams` / `TradeSellParams`。 + +### SimpleBuyParams + +| 参数 | 类型 | 必需 | 说明 | +|------|------|------|------| +| `dex_type` | `DexType` | ✅ | 使用哪个协议交易,例如 `DexType::PumpFun`。 | +| `pay_with` | `TradeTokenType` | ✅ | 买入时用什么 quote 支付。钱包实际花原生 SOL 就传 `SOL`。PumpFun V2 的 SOL/WSOL quote 池,如果你想用原生 SOL 结算,也仍然传 `SOL`。 | +| `mint` | `Pubkey` | ✅ | 要买入的 token mint。 | +| `amount` | `BuyAmount` | ✅ | 买入数量语义。选择一个枚举,不再组合多个低层数量字段。 | +| `extension_params` | `DexParamEnum` | ✅ | 协议状态参数,来自 parser/RPC 缓存,例如 `DexParamEnum::PumpFun(PumpFunParams::from_trade(...))`。 | +| `recent_blockhash` | `Hash` | ✅,使用 `new` 时 | 非 nonce 交易使用的 recent blockhash。SDK 不会在热路径临时获取。 | +| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | CU price/limit 和 relay tip 配置。 | +| `slippage_basis_points` | `Option` | ❌ | 可选滑点覆盖。`100` 表示 1%。 | +| `account_policy` | `AccountPolicy` | ❌ | ATA 创建/关闭策略。默认 `Auto`。 | +| `address_lookup_table_account` | `Option` | ❌ | 可选 ALT,用于减少交易体积。 | +| `wait_tx_confirmed` | `bool` | ❌ | 是否等链上确认后再返回。默认 `false`。 | +| `wait_for_all_submits` | `bool` | ❌ | fast-submit 模式下,是否等待所有 SWQoS 通道返回并拿到全部签名。 | +| `durable_nonce` | `Option` | ❌ | durable nonce 信息。使用 `SimpleBuyParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 | +| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 | +| `grpc_recv_us` | `Option` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 | + +### SimpleSellParams + +| 参数 | 类型 | 必需 | 说明 | +|------|------|------|------| +| `dex_type` | `DexType` | ✅ | 使用哪个协议交易,例如 `DexType::PumpFun`。 | +| `receive_as` | `TradeTokenType` | ✅ | 卖出后接收什么 quote。想收原生 SOL 就传 `SOL`。 | +| `mint` | `Pubkey` | ✅ | 要卖出的 token mint。 | +| `amount` | `SellAmount` | ✅ | 卖出数量语义。 | +| `extension_params` | `DexParamEnum` | ✅ | 协议状态参数,来自 parser/RPC 缓存。 | +| `recent_blockhash` | `Hash` | ✅,使用 `new` 时 | 非 nonce 交易使用的 recent blockhash。 | +| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | CU price/limit 和 relay tip 配置。 | +| `slippage_basis_points` | `Option` | ❌ | 可选滑点覆盖。`100` 表示 1%。 | +| `account_policy` | `AccountPolicy` | ❌ | ATA 创建/关闭策略。默认 `Auto`。 | +| `address_lookup_table_account` | `Option` | ❌ | 可选 ALT,用于减少交易体积。 | +| `wait_tx_confirmed` | `bool` | ❌ | 是否等链上确认后再返回。默认 `false`。 | +| `wait_for_all_submits` | `bool` | ❌ | fast-submit 模式下,是否等待所有 SWQoS 通道返回并拿到全部签名。 | +| `durable_nonce` | `Option` | ❌ | durable nonce 信息。使用 `SimpleSellParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 | +| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 | +| `with_tip` | `bool` | ❌ | 卖出交易是否带 relay tip。默认 `true`,可通过 `.with_tip(false)` 关闭。 | +| `grpc_recv_us` | `Option` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 | + +### 数量如何选择 + +| 枚举 | 含义 | 底层映射 | +|------|------|----------| +| `BuyAmount::ExactInput(amount)` | 精确花费指定 quote 数量,滑点保护最小买到 token 数量。 | `input_token_amount = amount`,`use_exact_sol_amount = Some(true)` | +| `BuyAmount::WithMaxInput { quote_amount }` | 常规 PumpFun/PumpSwap buy。SDK 估算输出,并把滑点作用在最大 quote 成本上。 | `input_token_amount = quote_amount`,`use_exact_sol_amount = Some(false)` | +| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | 精确买到指定 token 数量,并限制最多花多少 quote。 | `fixed_output_token_amount = Some(output_amount)`,`input_token_amount = max_input_amount` | +| `SellAmount::ExactInput(amount)` | 精确卖出指定 token 数量,滑点保护最少收到 quote 数量。 | `input_token_amount = amount` | +| `SellAmount::ExactOutput { output_amount, max_input_amount }` | 精确收到指定 quote 数量,并限制最多卖出多少 token;取决于 DEX 是否支持。 | `fixed_output_token_amount = Some(output_amount)`,`input_token_amount = max_input_amount` | + +### AccountPolicy + +| 枚举 | 行为 | 适用场景 | +|------|------|----------| +| `Auto` | SDK 按实际路径创建必要 ATA。买入会创建目标 token ATA;卖出接收非 SOL 时会创建输出 ATA。 | 普通应用、手动交易工具。 | +| `HotPathMinimal` | 交易内不创建/关闭 ATA。 | Bot、狙击、套利、对交易体积敏感的路径。 | +| `CreateMissing` | 尽量在交易内创建缺失 ATA。 | 更重视方便,不追求最小交易体积。 | +| `AssumePrepared` | 不创建也不关闭 token account,调用方保证都已准备好。 | 高级确定性流程。 | + ## TradeBuyParams -`TradeBuyParams` 结构体包含在不同 DEX 协议上执行买入订单所需的所有参数。 +`TradeBuyParams` 是高级低层买入 API。新接入建议优先使用 `SimpleBuyParams`,只有需要直接控制单个 ATA flag 时再使用它。 ### 基础交易参数 diff --git a/examples/simple_trading/Cargo.toml b/examples/simple_trading/Cargo.toml new file mode 100644 index 0000000..f75fc60 --- /dev/null +++ b/examples/simple_trading/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "simple_trading" +version = "0.1.0" +edition = "2021" + +[dependencies] +sol-trade-sdk = { path = "../.." } +solana-commitment-config = { version = "3.0.0", features = ["serde"] } +solana-sdk = "3.0.0" +tokio = { version = "1", features = ["full"] } diff --git a/examples/simple_trading/src/main.rs b/examples/simple_trading/src/main.rs new file mode 100644 index 0000000..44b26d9 --- /dev/null +++ b/examples/simple_trading/src/main.rs @@ -0,0 +1,103 @@ +//! Simple high-level trading API example. +//! +//! This example focuses on parameter construction. Replace the placeholder +//! PumpFun params with real params from your parser/RPC cache before sending. + +use std::sync::Arc; + +use sol_trade_sdk::{ + common::{GasFeeStrategy, TradeConfig}, + constants::{TOKEN_PROGRAM_2022, WSOL_TOKEN_ACCOUNT}, + instruction::utils::pumpfun::global_constants, + swqos::SwqosConfig, + trading::{ + core::params::{DexParamEnum, PumpFunParams}, + factory::DexType, + }, + AccountPolicy, BuyAmount, SellAmount, SimpleBuyParams, SimpleSellParams, SolanaTrade, + TradeTokenType, +}; +use solana_commitment_config::CommitmentConfig; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let payer = Keypair::from_base58_string("use_your_payer_keypair_here"); + let rpc_url = std::env::var("RPC_URL") + .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()); + let commitment = CommitmentConfig::processed(); + let swqos_configs = vec![SwqosConfig::Default(rpc_url.clone())]; + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // Default is true. WSOL ATA is prepared on startup, not in the hot trade tx. + .create_wsol_ata_on_startup(true) + .build(); + let client = SolanaTrade::new(Arc::new(payer), trade_config).await; + + let mint = Pubkey::new_unique(); + let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?; + let gas_fee_strategy = GasFeeStrategy::new(); + gas_fee_strategy.set_global_fee_strategy(150_000, 150_000, 500_000, 500_000, 0.001, 0.001); + + // In production, fill these fields from your parser/RPC cache. They are + // protocol state, not user preferences. + let pumpfun_params = DexParamEnum::PumpFun(PumpFunParams::from_trade( + Pubkey::new_unique(), // bonding_curve + Pubkey::new_unique(), // associated_bonding_curve + mint, + // WSOL quote_mint selects PumpFun V2 SOL layout. Users still pay with + // native SOL below by setting `TradeTokenType::SOL`. + WSOL_TOKEN_ACCOUNT, + Pubkey::new_unique(), // creator from event/cache + Pubkey::default(), // creator_vault; SDK derives it if unavailable + 1_073_000_000_000_000, // virtual_token_reserves + 30_000_000_000, // virtual_quote_reserves + 793_100_000_000_000, // real_token_reserves + 0, // real_quote_reserves + None, // close_token_account_when_sell + global_constants::FEE_RECIPIENT, + // If parser/cache does not know the mint owner, PumpFun now defaults to + // Token-2022. Passing it explicitly makes the example easier to read. + TOKEN_PROGRAM_2022, + false, // is_cashback_coin + Some(false), // mayhem_mode + )); + + let buy_params = SimpleBuyParams::new( + DexType::PumpFun, + // For PumpFun V2 SOL-paired coins, keep this as SOL even though V2 + // account metas use the WSOL mint as quote_mint. + TradeTokenType::SOL, + mint, + // Regular PumpFun/PumpSwap buy. The SDK estimates expected output and + // applies slippage to the maximum quote cost. + BuyAmount::WithMaxInput { quote_amount: 100_000 }, + pumpfun_params.clone(), + recent_blockhash, + gas_fee_strategy.clone(), + ) + .slippage_basis_points(300) + // Best for bots: do not create/close ATAs in the hot transaction. + // Use AccountPolicy::Auto for normal integrations. + .account_policy(AccountPolicy::HotPathMinimal); + + // client.buy_simple(buy_params).await?; + let _ = buy_params; + + let sell_params = SimpleSellParams::new( + DexType::PumpFun, + TradeTokenType::SOL, + mint, + SellAmount::ExactInput(1_000_000), + pumpfun_params, + client.infrastructure.rpc.get_latest_blockhash().await?, + gas_fee_strategy, + ) + .slippage_basis_points(300) + .account_policy(AccountPolicy::HotPathMinimal); + + // client.sell_simple(sell_params).await?; + let _ = sell_params; + + println!("Built simple buy/sell params for payer {}", client.payer.pubkey()); + Ok(()) +} diff --git a/src/client/mod.rs b/src/client/mod.rs index eff4330..d1c76e9 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -82,6 +82,399 @@ pub enum TradeTokenType { USDC, } +/// Account lifecycle policy for high-level trade requests. +/// +/// This replaces low-level flags such as `create_input_token_ata`, +/// `close_input_token_ata`, `create_mint_ata`, and `create_output_token_ata`. +/// Use this when calling [`TradingClient::buy_simple`] or [`TradingClient::sell_simple`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AccountPolicy { + /// SDK chooses a practical default for the protocol and token route. + /// + /// Buy: create the target token ATA because the wallet usually needs a place + /// to receive the bought token. + /// + /// Sell: create the output ATA only when receiving an SPL token such as USDC + /// or WSOL. Receiving native SOL does not need an ATA. + Auto, + /// Keep the transaction small for sniping/arbitrage; assume hot accounts are prepared. + /// + /// This avoids ATA create/close instructions in the trade transaction. It is + /// the recommended policy for latency-sensitive bots that pre-create WSOL + /// and token ATAs outside the hot path. + HotPathMinimal, + /// Include ATA create instructions when the route needs them. + /// + /// This is more convenient for normal trading but can make PumpFun V2 and + /// other large transactions exceed Solana packet size limits unless an ALT + /// is supplied or the transaction builder can compact optional instructions. + CreateMissing, + /// Do not create or close token accounts in the trade transaction. + /// + /// Use this when the caller has already prepared all required ATAs and wants + /// deterministic instruction layout. Unlike `HotPathMinimal`, this name is + /// intended for correctness/readability rather than bot latency. + AssumePrepared, +} + +/// High-level buy sizing intent. +/// +/// This replaces `input_token_amount`, `fixed_output_token_amount`, and +/// `use_exact_sol_amount` in [`TradeBuyParams`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BuyAmount { + /// Spend this exact quote amount and apply slippage to minimum output. + /// + /// Example: spend exactly `0.1 SOL` or exactly `100 USDC` worth of quote + /// token; if the received token amount is below the slippage-adjusted + /// minimum, the transaction fails. + ExactInput(u64), + /// Buy an exact token amount, using `max_input_amount` as the quote budget. + /// + /// Example: buy exactly `1_000_000` token base units, but fail if the quote + /// cost would exceed `max_input_amount`. + ExactOutput { output_amount: u64, max_input_amount: u64 }, + /// Regular PumpFun/PumpSwap buy: estimate output from `quote_amount` and apply slippage to max quote cost. + /// + /// This maps to `use_exact_sol_amount = Some(false)` in the old API. It is + /// useful for bots that want to know the intended token output from the SDK + /// calculation while letting the chain fail if the max quote budget is + /// exceeded. + WithMaxInput { quote_amount: u64 }, +} + +/// High-level sell sizing intent. +/// +/// This replaces `input_token_amount` and `fixed_output_token_amount` in +/// [`TradeSellParams`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SellAmount { + /// Sell this exact token amount and apply slippage to minimum output. + /// + /// Example: sell exactly `1_000_000` token base units and fail if the + /// received quote amount is below the slippage-adjusted minimum. + ExactInput(u64), + /// Exact-output sell where supported; `max_input_amount` is the token budget. + /// + /// Example: receive exactly `0.1 SOL` or `100 USDC`, but spend no more than + /// `max_input_amount` token base units. + ExactOutput { output_amount: u64, max_input_amount: u64 }, +} + +/// Simpler buy request that describes trade intent instead of low-level ATA flags. +/// +/// Prefer constructing this with [`SimpleBuyParams::new`] or +/// [`SimpleBuyParams::with_durable_nonce`], then optionally set slippage, +/// account policy, ALT, simulation, or confirmation flags through builder-style +/// methods. +#[derive(Clone)] +pub struct SimpleBuyParams { + /// DEX/protocol to route through, such as `DexType::PumpFun`. + pub dex_type: DexType, + /// Quote token used to pay for the buy: `SOL`, `WSOL`, `USDC`, or `USD1`. + /// + /// For PumpFun V2 SOL-paired coins, use `TradeTokenType::SOL` even when the + /// protocol quote mint account is WSOL. The SDK passes WSOL as the V2 quote + /// account but keeps settlement in native SOL. + pub pay_with: TradeTokenType, + /// Mint address of the token being bought. + pub mint: Pubkey, + /// Buy sizing intent. See [`BuyAmount`]. + pub amount: BuyAmount, + /// Optional slippage in basis points. `100` means 1%. + pub slippage_basis_points: Option, + /// Recent blockhash for non-nonce transactions. + /// + /// The SDK intentionally does not fetch blockhash on the hot path. Use + /// [`SimpleBuyParams::with_durable_nonce`] instead when submitting multiple + /// SWQoS lanes against a pinned nonce. + pub recent_blockhash: Option, + /// Protocol-specific parameters, for example `DexParamEnum::PumpFun(...)`. + pub extension_params: DexParamEnum, + /// Compute unit price/limit and relay tip configuration. + pub gas_fee_strategy: GasFeeStrategy, + /// ATA creation/close behavior. See [`AccountPolicy`]. + pub account_policy: AccountPolicy, + /// Optional Address Lookup Table to reduce transaction size. + pub address_lookup_table_account: Option, + /// Wait until the transaction is confirmed before returning. + pub wait_tx_confirmed: bool, + /// Fast-submit mode only: wait for every SWQoS route's submit response so all + /// signatures can be returned. + pub wait_for_all_submits: bool, + /// Durable nonce info. Mutually exclusive with `recent_blockhash`. + pub durable_nonce: Option, + /// Build and simulate the transaction instead of submitting it. + pub simulate: bool, + /// Optional upstream receive timestamp in microseconds for latency tracing. + pub grpc_recv_us: Option, +} + +/// Simpler sell request that describes trade intent instead of low-level ATA flags. +/// +/// Prefer constructing this with [`SimpleSellParams::new`] or +/// [`SimpleSellParams::with_durable_nonce`]. +#[derive(Clone)] +pub struct SimpleSellParams { + /// DEX/protocol to route through, such as `DexType::PumpFun`. + pub dex_type: DexType, + /// Quote token to receive from the sell: `SOL`, `WSOL`, `USDC`, or `USD1`. + pub receive_as: TradeTokenType, + /// Mint address of the token being sold. + pub mint: Pubkey, + /// Sell sizing intent. See [`SellAmount`]. + pub amount: SellAmount, + /// Optional slippage in basis points. `100` means 1%. + pub slippage_basis_points: Option, + /// Recent blockhash for non-nonce transactions. + pub recent_blockhash: Option, + /// Protocol-specific parameters, for example `DexParamEnum::PumpFun(...)`. + pub extension_params: DexParamEnum, + /// Compute unit price/limit and relay tip configuration. + pub gas_fee_strategy: GasFeeStrategy, + /// ATA creation/close behavior. See [`AccountPolicy`]. + pub account_policy: AccountPolicy, + /// Optional Address Lookup Table to reduce transaction size. + pub address_lookup_table_account: Option, + /// Wait until the transaction is confirmed before returning. + pub wait_tx_confirmed: bool, + /// Fast-submit mode only: wait for every SWQoS route's submit response so all + /// signatures can be returned. + pub wait_for_all_submits: bool, + /// Durable nonce info. Mutually exclusive with `recent_blockhash`. + pub durable_nonce: Option, + /// Build and simulate the transaction instead of submitting it. + pub simulate: bool, + /// Whether to include relay tips for sell transactions. + pub with_tip: bool, + /// Optional upstream receive timestamp in microseconds for latency tracing. + pub grpc_recv_us: Option, +} + +impl SimpleBuyParams { + /// Create a simple buy request using a recent blockhash. + /// + /// Defaults: + /// - `account_policy = AccountPolicy::Auto` + /// - `wait_tx_confirmed = false` + /// - `wait_for_all_submits = false` + /// - `simulate = false` + /// - no ALT + /// - no slippage override + pub fn new( + dex_type: DexType, + pay_with: TradeTokenType, + mint: Pubkey, + amount: BuyAmount, + extension_params: DexParamEnum, + recent_blockhash: Hash, + gas_fee_strategy: GasFeeStrategy, + ) -> Self { + Self { + dex_type, + pay_with, + mint, + amount, + slippage_basis_points: None, + recent_blockhash: Some(recent_blockhash), + extension_params, + gas_fee_strategy, + account_policy: AccountPolicy::Auto, + address_lookup_table_account: None, + wait_tx_confirmed: false, + wait_for_all_submits: false, + durable_nonce: None, + simulate: false, + grpc_recv_us: None, + } + } + + /// Create a simple buy request using a durable nonce instead of a recent blockhash. + /// + /// Use this when sending through multiple SWQoS lanes with the same nonce. + pub fn with_durable_nonce( + dex_type: DexType, + pay_with: TradeTokenType, + mint: Pubkey, + amount: BuyAmount, + extension_params: DexParamEnum, + durable_nonce: DurableNonceInfo, + gas_fee_strategy: GasFeeStrategy, + ) -> Self { + Self { + durable_nonce: Some(durable_nonce), + recent_blockhash: None, + ..Self::new( + dex_type, + pay_with, + mint, + amount, + extension_params, + Hash::default(), + gas_fee_strategy, + ) + } + } + + /// Set slippage in basis points. `100` means 1%. + pub fn slippage_basis_points(mut self, value: u64) -> Self { + self.slippage_basis_points = Some(value); + self + } + + /// Set account lifecycle behavior. Bots usually want `HotPathMinimal`; + /// normal integrations can keep the default `Auto`. + pub fn account_policy(mut self, value: AccountPolicy) -> Self { + self.account_policy = value; + self + } + + /// Attach an Address Lookup Table to reduce transaction size. + pub fn address_lookup_table_account(mut self, value: AddressLookupTableAccount) -> Self { + self.address_lookup_table_account = Some(value); + self + } + + /// Wait for confirmation before returning. + pub fn wait_tx_confirmed(mut self, value: bool) -> Self { + self.wait_tx_confirmed = value; + self + } + + /// In fast-submit mode, wait for all SWQoS submit responses and return every signature. + pub fn wait_for_all_submits(mut self, value: bool) -> Self { + self.wait_for_all_submits = value; + self + } + + /// Simulate instead of submitting. + pub fn simulate(mut self, value: bool) -> Self { + self.simulate = value; + self + } + + /// Attach upstream receive timestamp for latency tracing. + pub fn grpc_recv_us(mut self, value: i64) -> Self { + self.grpc_recv_us = Some(value); + self + } +} + +impl SimpleSellParams { + /// Create a simple sell request using a recent blockhash. + /// + /// Defaults: + /// - `account_policy = AccountPolicy::Auto` + /// - `with_tip = true` + /// - `wait_tx_confirmed = false` + /// - `wait_for_all_submits = false` + /// - `simulate = false` + /// - no ALT + /// - no slippage override + pub fn new( + dex_type: DexType, + receive_as: TradeTokenType, + mint: Pubkey, + amount: SellAmount, + extension_params: DexParamEnum, + recent_blockhash: Hash, + gas_fee_strategy: GasFeeStrategy, + ) -> Self { + Self { + dex_type, + receive_as, + mint, + amount, + slippage_basis_points: None, + recent_blockhash: Some(recent_blockhash), + extension_params, + gas_fee_strategy, + account_policy: AccountPolicy::Auto, + address_lookup_table_account: None, + wait_tx_confirmed: false, + wait_for_all_submits: false, + durable_nonce: None, + simulate: false, + with_tip: true, + grpc_recv_us: None, + } + } + + /// Create a simple sell request using a durable nonce instead of a recent blockhash. + pub fn with_durable_nonce( + dex_type: DexType, + receive_as: TradeTokenType, + mint: Pubkey, + amount: SellAmount, + extension_params: DexParamEnum, + durable_nonce: DurableNonceInfo, + gas_fee_strategy: GasFeeStrategy, + ) -> Self { + Self { + durable_nonce: Some(durable_nonce), + recent_blockhash: None, + ..Self::new( + dex_type, + receive_as, + mint, + amount, + extension_params, + Hash::default(), + gas_fee_strategy, + ) + } + } + + /// Set slippage in basis points. `100` means 1%. + pub fn slippage_basis_points(mut self, value: u64) -> Self { + self.slippage_basis_points = Some(value); + self + } + + /// Set account lifecycle behavior. Bots usually want `HotPathMinimal`; + /// normal integrations can keep the default `Auto`. + pub fn account_policy(mut self, value: AccountPolicy) -> Self { + self.account_policy = value; + self + } + + /// Attach an Address Lookup Table to reduce transaction size. + pub fn address_lookup_table_account(mut self, value: AddressLookupTableAccount) -> Self { + self.address_lookup_table_account = Some(value); + self + } + + /// Wait for confirmation before returning. + pub fn wait_tx_confirmed(mut self, value: bool) -> Self { + self.wait_tx_confirmed = value; + self + } + + /// In fast-submit mode, wait for all SWQoS submit responses and return every signature. + pub fn wait_for_all_submits(mut self, value: bool) -> Self { + self.wait_for_all_submits = value; + self + } + + /// Simulate instead of submitting. + pub fn simulate(mut self, value: bool) -> Self { + self.simulate = value; + self + } + + /// Enable or disable relay tips for sell transactions. + pub fn with_tip(mut self, value: bool) -> Self { + self.with_tip = value; + self + } + + /// Attach upstream receive timestamp for latency tracing. + pub fn grpc_recv_us(mut self, value: i64) -> Self { + self.grpc_recv_us = Some(value); + self + } +} + /// Shared infrastructure components that can be reused across multiple wallets /// /// This struct holds the expensive-to-initialize components (RPC client, SWQOS clients) @@ -453,6 +846,96 @@ pub struct TradeSellParams { pub grpc_recv_us: Option, } +#[inline] +fn buy_account_flags(policy: AccountPolicy) -> (bool, bool, bool) { + match policy { + AccountPolicy::Auto => (false, true, false), + AccountPolicy::HotPathMinimal | AccountPolicy::AssumePrepared => (false, false, false), + AccountPolicy::CreateMissing => (true, true, false), + } +} + +#[inline] +fn sell_account_flags(policy: AccountPolicy, receive_as: &TradeTokenType) -> (bool, bool, bool) { + match policy { + AccountPolicy::Auto => (*receive_as != TradeTokenType::SOL, false, false), + AccountPolicy::HotPathMinimal | AccountPolicy::AssumePrepared => (false, false, false), + AccountPolicy::CreateMissing => (true, false, false), + } +} + +impl From for TradeBuyParams { + fn from(params: SimpleBuyParams) -> Self { + let (input_token_amount, fixed_output_token_amount, use_exact_sol_amount) = + match params.amount { + BuyAmount::ExactInput(amount) => (amount, None, Some(true)), + BuyAmount::ExactOutput { output_amount, max_input_amount } => { + (max_input_amount, Some(output_amount), Some(true)) + } + BuyAmount::WithMaxInput { quote_amount } => (quote_amount, None, Some(false)), + }; + let (create_input_token_ata, create_mint_ata, close_input_token_ata) = + buy_account_flags(params.account_policy); + + TradeBuyParams { + dex_type: params.dex_type, + input_token_type: params.pay_with, + mint: params.mint, + input_token_amount, + slippage_basis_points: params.slippage_basis_points, + recent_blockhash: params.recent_blockhash, + extension_params: params.extension_params, + address_lookup_table_account: params.address_lookup_table_account, + wait_tx_confirmed: params.wait_tx_confirmed, + wait_for_all_submits: params.wait_for_all_submits, + create_input_token_ata, + close_input_token_ata, + create_mint_ata, + durable_nonce: params.durable_nonce, + fixed_output_token_amount, + gas_fee_strategy: params.gas_fee_strategy, + simulate: params.simulate, + use_exact_sol_amount, + grpc_recv_us: params.grpc_recv_us, + } + } +} + +impl From for TradeSellParams { + fn from(params: SimpleSellParams) -> Self { + let (input_token_amount, fixed_output_token_amount) = match params.amount { + SellAmount::ExactInput(amount) => (amount, None), + SellAmount::ExactOutput { output_amount, max_input_amount } => { + (max_input_amount, Some(output_amount)) + } + }; + let (create_output_token_ata, close_output_token_ata, close_mint_token_ata) = + sell_account_flags(params.account_policy, ¶ms.receive_as); + + TradeSellParams { + dex_type: params.dex_type, + output_token_type: params.receive_as, + mint: params.mint, + input_token_amount, + slippage_basis_points: params.slippage_basis_points, + recent_blockhash: params.recent_blockhash, + with_tip: params.with_tip, + extension_params: params.extension_params, + address_lookup_table_account: params.address_lookup_table_account, + wait_tx_confirmed: params.wait_tx_confirmed, + wait_for_all_submits: params.wait_for_all_submits, + create_output_token_ata, + close_output_token_ata, + close_mint_token_ata, + durable_nonce: params.durable_nonce, + fixed_output_token_amount, + gas_fee_strategy: params.gas_fee_strategy, + simulate: params.simulate, + grpc_recv_us: params.grpc_recv_us, + } + } +} + impl TradingClient { /// Create a TradingClient from shared infrastructure (fast path) /// @@ -915,6 +1398,18 @@ impl TradingClient { result } + /// Execute a high-level buy request. + #[inline] + pub async fn buy_simple( + &self, + params: SimpleBuyParams, + ) -> Result< + (bool, Vec, Option, Vec<(crate::swqos::SwqosType, i64)>), + anyhow::Error, + > { + self.buy(params.into()).await + } + /// Execute a sell order for a specified token /// /// 🔧 修复:返回Vec支持多SWQOS并发交易 @@ -1031,6 +1526,18 @@ impl TradingClient { result } + /// Execute a high-level sell request. + #[inline] + pub async fn sell_simple( + &self, + params: SimpleSellParams, + ) -> Result< + (bool, Vec, Option, Vec<(crate::swqos::SwqosType, i64)>), + anyhow::Error, + > { + self.sell(params.into()).await + } + /// Execute a sell order for a percentage of the specified token amount /// /// This is a convenience function that calculates the exact amount to sell based on @@ -1283,7 +1790,23 @@ impl TradingClient { #[cfg(test)] mod tests { use super::*; + use crate::instruction::utils::pumpfun::global_constants; use crate::swqos::SwqosRegion; + use std::sync::Arc; + + fn dummy_pumpfun_params() -> DexParamEnum { + DexParamEnum::PumpFun(PumpFunParams { + bonding_curve: Arc::new(Default::default()), + associated_bonding_curve: Pubkey::default(), + observed_trade_creator: None, + creator_vault: Pubkey::default(), + fee_sharing_creator_vault_if_active: None, + token_program: Pubkey::default(), + close_token_account_when_sell: None, + fee_recipient: global_constants::FEE_RECIPIENT, + quote_mint: Pubkey::default(), + }) + } #[test] fn normalize_swqos_configs_adds_default_rpc_route() { @@ -1303,4 +1826,117 @@ mod tests { assert_eq!(normalized.len(), 1); assert!(matches!(normalized[0].swqos_type(), SwqosType::Default)); } + + #[test] + fn simple_buy_hot_path_maps_to_low_level_params() { + let simple = SimpleBuyParams { + dex_type: DexType::PumpFun, + pay_with: TradeTokenType::SOL, + mint: Pubkey::new_unique(), + amount: BuyAmount::WithMaxInput { quote_amount: 10_000 }, + slippage_basis_points: Some(100), + recent_blockhash: Some(Hash::new_unique()), + extension_params: dummy_pumpfun_params(), + gas_fee_strategy: GasFeeStrategy::new(), + account_policy: AccountPolicy::HotPathMinimal, + address_lookup_table_account: None, + wait_tx_confirmed: false, + wait_for_all_submits: false, + durable_nonce: None, + simulate: false, + grpc_recv_us: None, + }; + + let low: TradeBuyParams = simple.into(); + + assert!(matches!(low.input_token_type, TradeTokenType::SOL)); + assert_eq!(low.input_token_amount, 10_000); + assert_eq!(low.use_exact_sol_amount, Some(false)); + assert_eq!(low.fixed_output_token_amount, None); + assert!(!low.create_input_token_ata); + assert!(!low.close_input_token_ata); + assert!(!low.create_mint_ata); + } + + #[test] + fn simple_buy_auto_creates_target_mint_ata() { + let simple = SimpleBuyParams { + dex_type: DexType::PumpFun, + pay_with: TradeTokenType::SOL, + mint: Pubkey::new_unique(), + amount: BuyAmount::ExactOutput { output_amount: 42, max_input_amount: 10_000 }, + slippage_basis_points: None, + recent_blockhash: Some(Hash::new_unique()), + extension_params: dummy_pumpfun_params(), + gas_fee_strategy: GasFeeStrategy::new(), + account_policy: AccountPolicy::Auto, + address_lookup_table_account: None, + wait_tx_confirmed: false, + wait_for_all_submits: false, + durable_nonce: None, + simulate: false, + grpc_recv_us: None, + }; + + let low: TradeBuyParams = simple.into(); + + assert_eq!(low.input_token_amount, 10_000); + assert_eq!(low.fixed_output_token_amount, Some(42)); + assert_eq!(low.use_exact_sol_amount, Some(true)); + assert!(low.create_mint_ata); + assert!(!low.create_input_token_ata); + } + + #[test] + fn simple_buy_builder_sets_defaults_and_overrides() { + let simple = SimpleBuyParams::new( + DexType::PumpFun, + TradeTokenType::SOL, + Pubkey::new_unique(), + BuyAmount::ExactInput(10_000), + dummy_pumpfun_params(), + Hash::new_unique(), + GasFeeStrategy::new(), + ) + .slippage_basis_points(250) + .account_policy(AccountPolicy::HotPathMinimal); + + let low: TradeBuyParams = simple.into(); + + assert_eq!(low.slippage_basis_points, Some(250)); + assert_eq!(low.use_exact_sol_amount, Some(true)); + assert!(!low.create_input_token_ata); + assert!(!low.create_mint_ata); + assert!(!low.wait_tx_confirmed); + } + + #[test] + fn simple_sell_auto_creates_non_sol_output_ata() { + let simple = SimpleSellParams { + dex_type: DexType::PumpFun, + receive_as: TradeTokenType::USDC, + mint: Pubkey::new_unique(), + amount: SellAmount::ExactInput(50_000), + slippage_basis_points: None, + recent_blockhash: Some(Hash::new_unique()), + extension_params: dummy_pumpfun_params(), + gas_fee_strategy: GasFeeStrategy::new(), + account_policy: AccountPolicy::Auto, + address_lookup_table_account: None, + wait_tx_confirmed: false, + wait_for_all_submits: false, + durable_nonce: None, + simulate: false, + with_tip: true, + grpc_recv_us: None, + }; + + let low: TradeSellParams = simple.into(); + + assert!(matches!(low.output_token_type, TradeTokenType::USDC)); + assert_eq!(low.input_token_amount, 50_000); + assert!(low.create_output_token_ata); + assert!(!low.close_output_token_ata); + assert!(!low.close_mint_token_ata); + } } diff --git a/src/instruction/pumpfun.rs b/src/instruction/pumpfun.rs index da6ece6..772f1d6 100644 --- a/src/instruction/pumpfun.rs +++ b/src/instruction/pumpfun.rs @@ -586,10 +586,9 @@ fn build_buy_unified(params: &SwapParams) -> Result> { } }; - if params.create_input_mint_ata - || (quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT - && params.input_mint == crate::constants::SOL_TOKEN_ACCOUNT) - { + let should_use_native_sol_for_wsol_quote = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT + && params.input_mint == crate::constants::SOL_TOKEN_ACCOUNT; + if params.create_input_mint_ata && !should_use_native_sol_for_wsol_quote { push_create_or_wrap_user_token_account( &mut instructions, ¶ms.payer.pubkey(), @@ -632,7 +631,7 @@ fn build_buy_unified(params: &SwapParams) -> Result> { instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, buy_data.as_slice(), metas)); - if params.close_input_mint_ata { + if params.close_input_mint_ata && !should_use_native_sol_for_wsol_quote { push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), "e_mint); } @@ -1025,9 +1024,11 @@ mod tests { } #[test] - fn pumpfun_v2_regular_buy_wraps_max_quote_budget() { + fn pumpfun_v2_regular_buy_uses_native_sol_for_wsol_quote() { let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM); params.create_output_mint_ata = false; + params.create_input_mint_ata = true; + params.close_input_mint_ata = true; params.use_exact_sol_amount = Some(false); if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params { *protocol_params = @@ -1035,27 +1036,76 @@ mod tests { } let instructions = build_buy(¶ms).unwrap(); - let transfer_ix = &instructions[1]; + assert_eq!(instructions.len(), 1); let buy_ix = instructions.last().unwrap(); - let system_ix = bincode::deserialize::< - solana_system_interface::instruction::SystemInstruction, - >(&transfer_ix.data) - .unwrap(); let expected = crate::utils::calc::common::calculate_with_slippage_buy( params.input_amount.unwrap(), params.slippage_basis_points.unwrap(), ); - match system_ix { - solana_system_interface::instruction::SystemInstruction::Transfer { lamports } => { - assert_eq!(lamports, expected); - } - other => panic!("unexpected system instruction: {:?}", other), - } assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR); + assert_eq!(u64::from_le_bytes(buy_ix.data[16..24].try_into().unwrap()), expected); assert_eq!(buy_ix.accounts.len(), 27); } + #[test] + fn pumpfun_v2_regular_buy_skips_wsol_ata_create_on_hot_path() { + let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM); + params.create_output_mint_ata = false; + params.create_input_mint_ata = true; + params.close_input_mint_ata = true; + params.use_exact_sol_amount = Some(false); + if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params { + *protocol_params = + protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT); + } + + let instructions = build_buy(¶ms).unwrap(); + + assert_eq!(instructions.len(), 1); + assert_eq!( + &instructions.last().unwrap().data[..8], + crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR + ); + } + + #[test] + fn pumpfun_v2_wsol_quote_hot_path_transaction_fits_packet_with_output_ata_create() { + let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM); + params.create_input_mint_ata = true; + params.create_output_mint_ata = true; + params.use_exact_sol_amount = Some(false); + if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params { + *protocol_params = + protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT); + } + + let business_instructions = build_buy(¶ms).unwrap(); + let transaction = crate::trading::common::transaction_builder::build_transaction( + ¶ms.payer, + 150_000, + 500_000, + &business_instructions, + None, + Some(solana_hash::Hash::new_unique()), + None, + "PumpFun", + true, + true, + &Pubkey::new_unique(), + 0.001, + None, + ) + .unwrap(); + let serialized = bincode::serialize(&transaction).unwrap(); + + assert!( + serialized.len() <= 1232, + "serialized transaction too large: {} > 1232", + serialized.len() + ); + } + #[test] fn pumpfun_from_trade_wsol_quote_regular_buy_selects_buy_v2() { let mint = pump_mint(); diff --git a/src/lib.rs b/src/lib.rs index a05fba9..d9d5b02 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ pub mod utils; // Re-export for SwqosConfig (Node1/BlockRazor transport; Astralane submission mode) pub use crate::swqos::{AstralaneTransport, SwqosTransport}; pub use client::{ - find_pool_by_mint, recommended_sender_thread_core_indices, SolanaTrade, TradeBuyParams, - TradeSellParams, TradeTokenType, TradingClient, TradingInfrastructure, + find_pool_by_mint, recommended_sender_thread_core_indices, AccountPolicy, BuyAmount, + SellAmount, SimpleBuyParams, SimpleSellParams, SolanaTrade, TradeBuyParams, TradeSellParams, + TradeTokenType, TradingClient, TradingInfrastructure, }; diff --git a/src/trading/common/transaction_builder.rs b/src/trading/common/transaction_builder.rs index e413b7b..82aecca 100755 --- a/src/trading/common/transaction_builder.rs +++ b/src/trading/common/transaction_builder.rs @@ -17,6 +17,8 @@ use crate::{ }, }; +const PACKET_DATA_SIZE: usize = 1232; + /// Convert SOL amount (f64) to lamports without string allocation (hot path). #[inline(always)] fn sol_f64_to_lamports(sol: f64) -> u64 { @@ -43,6 +45,75 @@ pub fn build_transaction( tip_account: &Pubkey, tip_amount: f64, durable_nonce: Option<&DurableNonceInfo>, +) -> Result { + let transaction = build_transaction_with_compute_budget( + payer, + unit_limit, + unit_price, + business_instructions, + address_lookup_table_account, + recent_blockhash, + middleware_manager, + protocol_name, + is_buy, + with_tip, + tip_account, + tip_amount, + durable_nonce, + )?; + + let serialized_len = bincode::serialized_size(&transaction)? as usize; + if serialized_len <= PACKET_DATA_SIZE + || durable_nonce.is_some() + || !with_tip + || tip_amount <= 0.0 + || (unit_limit == 0 && unit_price == 0) + { + return Ok(transaction); + } + + let compact_transaction = build_transaction_with_compute_budget( + payer, + 0, + 0, + business_instructions, + address_lookup_table_account, + recent_blockhash, + middleware_manager, + protocol_name, + is_buy, + with_tip, + tip_account, + tip_amount, + durable_nonce, + )?; + let compact_len = bincode::serialized_size(&compact_transaction)? as usize; + if compact_len <= PACKET_DATA_SIZE { + return Ok(compact_transaction); + } + + Err(anyhow!( + "transaction too large: {} > {}; compact without compute budget is {} bytes. Use an address lookup table or pre-create token ATAs before submitting", + serialized_len, + PACKET_DATA_SIZE, + compact_len + )) +} + +fn build_transaction_with_compute_budget( + payer: &Arc, + unit_limit: u32, + unit_price: u64, + business_instructions: &[Instruction], + address_lookup_table_account: Option<&AddressLookupTableAccount>, + recent_blockhash: Option, + middleware_manager: Option<&Arc>, + protocol_name: &str, + is_buy: bool, + with_tip: bool, + tip_account: &Pubkey, + tip_amount: f64, + durable_nonce: Option<&DurableNonceInfo>, ) -> Result { let mut instructions = Vec::with_capacity(business_instructions.len() + 5);