diff --git a/README.md b/README.md index 02faed6..969cc29 100644 --- a/README.md +++ b/README.md @@ -141,11 +141,14 @@ let swqos_configs: Vec = vec![ ), // QUIC ]; // Create TradeConfig instance -let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); - -// Optional: customize WSOL ATA and seed optimization -// let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment) -// .with_wsol_ata_config(true, true); // create_wsol_ata_on_startup, use_seed_optimize +let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true - check & create WSOL ATA on init + // .use_seed_optimize(true) // default: true - seed optimization for ATA ops + // .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) + .build(); // Create TradingClient let client = TradingClient::new(Arc::new(payer), trade_config).await; diff --git a/README_CN.md b/README_CN.md index 44f8838..d14b414 100755 --- a/README_CN.md +++ b/README_CN.md @@ -141,11 +141,14 @@ let swqos_configs: Vec = vec![ ), // QUIC ]; // 创建 TradeConfig 实例 -let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); - -// 可选:自定义 WSOL ATA 与 Seed 优化 -// let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment) -// .with_wsol_ata_config(true, true); // create_wsol_ata_on_startup, use_seed_optimize +let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // 默认: true - 初始化时检查并创建 WSOL ATA + // .use_seed_optimize(true) // 默认: true - ATA 操作启用 seed 优化 + // .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) + .build(); // 创建 TradingClient let client = TradingClient::new(Arc::new(payer), trade_config).await; diff --git a/examples/address_lookup/src/main.rs b/examples/address_lookup/src/main.rs index 30074c7..1ed9756 100644 --- a/examples/address_lookup/src/main.rs +++ b/examples/address_lookup/src/main.rs @@ -109,7 +109,14 @@ async fn create_solana_trade_client() -> AnyResult { let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; println!("✅ SolanaTrade client initialized successfully!"); Ok(solana_trade) diff --git a/examples/bonk_copy_trading/src/main.rs b/examples/bonk_copy_trading/src/main.rs index 84d5b9e..ec78b81 100644 --- a/examples/bonk_copy_trading/src/main.rs +++ b/examples/bonk_copy_trading/src/main.rs @@ -113,7 +113,14 @@ async fn create_solana_trade_client() -> AnyResult { let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; println!("✅ SolanaTrade client initialized successfully!"); Ok(solana_trade) diff --git a/examples/bonk_sniper_trading/src/main.rs b/examples/bonk_sniper_trading/src/main.rs index 5c3c600..84763c4 100644 --- a/examples/bonk_sniper_trading/src/main.rs +++ b/examples/bonk_sniper_trading/src/main.rs @@ -81,7 +81,14 @@ async fn create_solana_trade_client() -> AnyResult { let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; println!("✅ SolanaTrade client initialized successfully!"); Ok(solana_trade) diff --git a/examples/cli_trading/src/main.rs b/examples/cli_trading/src/main.rs index 42d190c..9b1ee18 100644 --- a/examples/cli_trading/src/main.rs +++ b/examples/cli_trading/src/main.rs @@ -1392,7 +1392,14 @@ async fn initialize_real_client() -> AnyResult { let rpc_url = RPC_URL.to_string(); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); let solana_trade = SolanaTrade::new(payer, trade_config).await; println!("✅ SolanaTrade client initialized successfully!"); Ok(solana_trade) diff --git a/examples/meteora_damm_v2_direct_trading/src/main.rs b/examples/meteora_damm_v2_direct_trading/src/main.rs index 4737a66..1beb6c7 100644 --- a/examples/meteora_damm_v2_direct_trading/src/main.rs +++ b/examples/meteora_damm_v2_direct_trading/src/main.rs @@ -107,7 +107,14 @@ async fn create_solana_trade_client() -> AnyResult { let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; println!("✅ SolanaTrade client initialized successfully!"); Ok(solana_trade) diff --git a/examples/middleware_system/src/main.rs b/examples/middleware_system/src/main.rs index 40ebf5b..c987463 100644 --- a/examples/middleware_system/src/main.rs +++ b/examples/middleware_system/src/main.rs @@ -63,7 +63,14 @@ async fn create_solana_trade_client() -> AnyResult { let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; println!("✅ SolanaTrade client initialized successfully!"); Ok(solana_trade) diff --git a/examples/nonce_cache/src/main.rs b/examples/nonce_cache/src/main.rs index 80a340b..8ea15c5 100644 --- a/examples/nonce_cache/src/main.rs +++ b/examples/nonce_cache/src/main.rs @@ -108,7 +108,14 @@ async fn create_solana_trade_client() -> AnyResult { let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; println!("✅ SolanaTrade client initialized successfully!"); Ok(solana_trade) diff --git a/examples/pumpfun_copy_trading/src/main.rs b/examples/pumpfun_copy_trading/src/main.rs index aa72b22..038f9e4 100644 --- a/examples/pumpfun_copy_trading/src/main.rs +++ b/examples/pumpfun_copy_trading/src/main.rs @@ -114,7 +114,14 @@ async fn create_solana_trade_client() -> AnyResult { .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); Ok(SolanaTrade::new(Arc::new(payer), trade_config).await) } diff --git a/examples/pumpfun_sniper_trading/src/main.rs b/examples/pumpfun_sniper_trading/src/main.rs index 0189597..67ef2d4 100644 --- a/examples/pumpfun_sniper_trading/src/main.rs +++ b/examples/pumpfun_sniper_trading/src/main.rs @@ -104,7 +104,14 @@ async fn create_solana_trade_client() -> AnyResult { .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); Ok(SolanaTrade::new(Arc::new(payer), trade_config).await) } diff --git a/examples/pumpswap_direct_trading/src/main.rs b/examples/pumpswap_direct_trading/src/main.rs index 468b897..1202ae8 100644 --- a/examples/pumpswap_direct_trading/src/main.rs +++ b/examples/pumpswap_direct_trading/src/main.rs @@ -104,7 +104,14 @@ async fn create_solana_trade_client() -> AnyResult { let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; println!("✅ SolanaTrade client initialized successfully!"); Ok(solana_trade) diff --git a/examples/pumpswap_trading/src/main.rs b/examples/pumpswap_trading/src/main.rs index a8a058e..d3e78bf 100644 --- a/examples/pumpswap_trading/src/main.rs +++ b/examples/pumpswap_trading/src/main.rs @@ -133,7 +133,14 @@ async fn create_solana_trade_client() -> AnyResult { let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; println!("✅ SolanaTrade client initialized successfully!"); Ok(solana_trade) diff --git a/examples/raydium_amm_v4_trading/src/main.rs b/examples/raydium_amm_v4_trading/src/main.rs index 18d50ec..b98f737 100644 --- a/examples/raydium_amm_v4_trading/src/main.rs +++ b/examples/raydium_amm_v4_trading/src/main.rs @@ -109,7 +109,14 @@ async fn create_solana_trade_client() -> AnyResult { let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; println!("✅ SolanaTrade client initialized successfully!"); Ok(solana_trade) diff --git a/examples/raydium_cpmm_trading/src/main.rs b/examples/raydium_cpmm_trading/src/main.rs index bbe1bd5..c9f647a 100644 --- a/examples/raydium_cpmm_trading/src/main.rs +++ b/examples/raydium_cpmm_trading/src/main.rs @@ -107,7 +107,14 @@ async fn create_solana_trade_client() -> AnyResult { let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; println!("✅ SolanaTrade client initialized successfully!"); Ok(solana_trade) diff --git a/examples/seed_trading/src/main.rs b/examples/seed_trading/src/main.rs index 8caa956..5223662 100644 --- a/examples/seed_trading/src/main.rs +++ b/examples/seed_trading/src/main.rs @@ -107,7 +107,14 @@ async fn create_solana_trade_client() -> AnyResult { let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; println!("✅ SolanaTrade client initialized successfully!"); Ok(solana_trade) diff --git a/examples/trading_client/src/main.rs b/examples/trading_client/src/main.rs index de85803..ef46d88 100644 --- a/examples/trading_client/src/main.rs +++ b/examples/trading_client/src/main.rs @@ -58,11 +58,10 @@ async fn create_trading_client_simple() -> AnyResult { SwqosConfig::Helius("".to_string(), SwqosRegion::Default, None, Some(true)), ]; - // Optional: Customize WSOL ATA and Seed optimization settings - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment).with_wsol_ata_config( - true, // create_wsol_ata_on_startup: Check and create WSOL ATA on startup - true, // use_seed_optimize: Enable seed optimization for all ATA operations - ); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + .create_wsol_ata_on_startup(true) + .use_seed_optimize(true) + .build(); // Creates new infrastructure internally let client = TradingClient::new(Arc::new(payer), trade_config).await; diff --git a/examples/wsol_wrapper/src/main.rs b/examples/wsol_wrapper/src/main.rs index dfbc8b8..2bcddba 100644 --- a/examples/wsol_wrapper/src/main.rs +++ b/examples/wsol_wrapper/src/main.rs @@ -84,7 +84,14 @@ async fn create_solana_trade_client() -> Result = vec![SwqosConfig::Default(rpc_url.clone())]; - let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); + let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + // .create_wsol_ata_on_startup(true) // default: true + // .use_seed_optimize(true) // default: true + // .log_enabled(true) // default: true + // .check_min_tip(false) // default: false + // .swqos_cores_from_end(false) // default: false + // .mev_protection(false) // default: false + .build(); let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; println!("✅ SolanaTrade client initialized successfully!"); Ok(solana_trade) diff --git a/sol-trade-sdk-nodejs/package.json b/sol-trade-sdk-nodejs/package.json new file mode 100644 index 0000000..e8e3a87 --- /dev/null +++ b/sol-trade-sdk-nodejs/package.json @@ -0,0 +1,43 @@ +{ + "name": "sol-trade-sdk", + "version": "1.0.0", + "description": "Production-grade instruction builders for Solana DEX protocols", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "solana", + "dex", + "pumpfun", + "bonk", + "raydium", + "meteora", + "blockchain", + "crypto" + ], + "author": "", + "license": "MIT", + "dependencies": { + "@solana/web3.js": "^1.95.0", + "@solana/spl-token": "^0.4.0" + }, + "devDependencies": { + "typescript": "^5.0.0", + "@types/node": "^20.0.0" + }, + "files": [ + "dist", + "README.md" + ], + "repository": { + "type": "git", + "url": "https://github.com/sol-trade/sol-trade-sdk-nodejs" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/sol-trade-sdk-nodejs/src/index.ts b/sol-trade-sdk-nodejs/src/index.ts new file mode 100644 index 0000000..fde9e3d --- /dev/null +++ b/sol-trade-sdk-nodejs/src/index.ts @@ -0,0 +1,7 @@ +/** + * Sol Trade SDK - Node.js + * + * Production-grade instruction builders for Solana DEX protocols. + */ + +export * from "./instruction"; diff --git a/sol-trade-sdk-nodejs/src/instruction/bonk_builder.ts b/sol-trade-sdk-nodejs/src/instruction/bonk_builder.ts new file mode 100644 index 0000000..d328d25 --- /dev/null +++ b/sol-trade-sdk-nodejs/src/instruction/bonk_builder.ts @@ -0,0 +1,646 @@ +/** + * Bonk Protocol Instruction Builder + * + * Production-grade instruction builder for Bonk AMM protocol. + * Supports buy and sell operations with WSOL and USD1 pools. + */ + +import { + PublicKey, + Keypair, + AccountMeta, + Instruction, + SystemProgram, +} from "@solana/web3.js"; +import { + getAssociatedTokenAddressSync, + createAssociatedTokenAccountInstruction, + TOKEN_PROGRAM_ID, + createCloseAccountInstruction, + NATIVE_MINT, + createSyncNativeInstruction, + createAccount, + getAccount, + getMint, + Account as TokenAccount, +} from "@solana/spl-token"; + +// ============================================ +// Program IDs and Constants +// ============================================ + +/** Bonk program ID */ +export const BONK_PROGRAM_ID = new PublicKey( + "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj" +); + +/** Authority */ +export const AUTHORITY = new PublicKey( + "WLhv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh" +); + +/** Global Config */ +export const GLOBAL_CONFIG = new PublicKey( + "6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX" +); + +/** USD1 Global Config */ +export const USD1_GLOBAL_CONFIG = new PublicKey( + "EPiZbnrThjyLnoQ6QQzkxeFqyL5uyg9RzNHHAudUPxBz" +); + +/** Event Authority */ +export const EVENT_AUTHORITY = new PublicKey( + "2DPAtwB8L12vrMRExbLuyGnC7n2J5LNoZQSejeQGpwkr" +); + +/** WSOL Token Account (mint) */ +export const WSOL_TOKEN_ACCOUNT = new PublicKey( + "So11111111111111111111111111111111111111112" +); + +/** USD1 Token Account (mint) */ +export const USD1_TOKEN_ACCOUNT = new PublicKey( + "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB" +); + +/** Fee rates */ +export const PLATFORM_FEE_RATE = 100n; // 1% +export const PROTOCOL_FEE_RATE = 25n; // 0.25% +export const SHARE_FEE_RATE = 0n; // 0% + +// ============================================ +// Discriminators +// ============================================ + +/** Buy exact in instruction discriminator */ +export const BUY_EXACT_IN_DISCRIMINATOR: Buffer = Buffer.from([ + 250, 234, 13, 123, 213, 156, 19, 236, +]); + +/** Sell exact in instruction discriminator */ +export const SELL_EXACT_IN_DISCRIMINATOR: Buffer = Buffer.from([ + 149, 39, 222, 155, 211, 124, 152, 26, +]); + +// ============================================ +// Seeds +// ============================================ + +export const POOL_SEED = Buffer.from("pool"); +export const POOL_VAULT_SEED = Buffer.from("pool_vault"); + +// ============================================ +// PDA Derivation Functions +// ============================================ + +/** + * Derive the pool PDA for given base and quote mints + */ +export function getPoolPda(baseMint: PublicKey, quoteMint: PublicKey): PublicKey { + const [pda] = PublicKey.findProgramAddressSync( + [POOL_SEED, baseMint.toBuffer(), quoteMint.toBuffer()], + BONK_PROGRAM_ID + ); + return pda; +} + +/** + * Derive the vault PDA for a pool and mint + */ +export function getVaultPda(poolState: PublicKey, mint: PublicKey): PublicKey { + const [pda] = PublicKey.findProgramAddressSync( + [POOL_VAULT_SEED, poolState.toBuffer(), mint.toBuffer()], + BONK_PROGRAM_ID + ); + return pda; +} + +/** + * Derive platform associated account + */ +export function getPlatformAssociatedAccount(platformConfig: PublicKey): PublicKey { + const [pda] = PublicKey.findProgramAddressSync( + [platformConfig.toBuffer(), WSOL_TOKEN_ACCOUNT.toBuffer()], + BONK_PROGRAM_ID + ); + return pda; +} + +/** + * Derive creator associated account + */ +export function getCreatorAssociatedAccount(creator: PublicKey): PublicKey { + const [pda] = PublicKey.findProgramAddressSync( + [creator.toBuffer(), WSOL_TOKEN_ACCOUNT.toBuffer()], + BONK_PROGRAM_ID + ); + return pda; +} + +// ============================================ +// Helper Functions +// ============================================ + +/** + * Calculate amount in net after fees + */ +export function getAmountInNet( + amountIn: bigint, + protocolFeeRate: bigint, + platformFeeRate: bigint, + shareFeeRate: bigint +): bigint { + const protocolFee = (amountIn * protocolFeeRate) / 10000n; + const platformFee = (amountIn * platformFeeRate) / 10000n; + const shareFee = (amountIn * shareFeeRate) / 10000n; + return amountIn - protocolFee - platformFee - shareFee; +} + +/** + * Calculate amount out for a swap + */ +export function getAmountOut( + amountIn: bigint, + protocolFeeRate: bigint, + platformFeeRate: bigint, + shareFeeRate: bigint, + virtualBase: bigint, + virtualQuote: bigint, + realBase: bigint, + realQuote: bigint, + slippageBasisPoints: bigint +): bigint { + const amountInNet = getAmountInNet( + amountIn, + protocolFeeRate, + platformFeeRate, + shareFeeRate + ); + + const inputReserve = virtualQuote + realQuote; + const outputReserve = virtualBase - realBase; + + const numerator = amountInNet * outputReserve; + const denominator = inputReserve + amountInNet; + let amountOut = numerator / denominator; + + // Apply slippage + amountOut = amountOut - (amountOut * slippageBasisPoints) / 10000n; + + return amountOut; +} + +/** + * Calculate amount in required for a desired output + */ +export function getAmountIn( + amountOut: bigint, + protocolFeeRate: bigint, + platformFeeRate: bigint, + shareFeeRate: bigint, + virtualBase: bigint, + virtualQuote: bigint, + realBase: bigint, + realQuote: bigint, + slippageBasisPoints: bigint +): bigint { + // Consider slippage, actual required output amount is higher + const amountOutWithSlippage = (amountOut * 10000n) / (10000n - slippageBasisPoints); + + const inputReserve = virtualQuote + realQuote; + const outputReserve = virtualBase - realBase; + + // Reverse calculate using AMM formula + const numerator = amountOutWithSlippage * inputReserve; + const denominator = outputReserve - amountOutWithSlippage; + const amountInNet = numerator / denominator; + + // Calculate total fee rate + const totalFeeRate = protocolFeeRate + platformFeeRate + shareFeeRate; + + const amountIn = (amountInNet * 10000n) / (10000n - totalFeeRate); + + return amountIn; +} + +// ============================================ +// WSOL Helper Functions +// ============================================ + +/** + * Create instructions to wrap SOL into WSOL + */ +export function createWsolInstructions( + payer: PublicKey, + amount: bigint +): Instruction[] { + const instructions: Instruction[] = []; + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payer, true); + + // Create WSOL ATA if needed (using create account for simplicity) + // In production, use createAssociatedTokenAccountInstruction + + // Sync native (wrap SOL) + instructions.push( + createSyncNativeInstruction(wsolAta) + ); + + return instructions; +} + +/** + * Create instruction to close WSOL ATA and unwrap to SOL + */ +export function createCloseWsolInstruction(payer: PublicKey): Instruction { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payer, true); + return createCloseAccountInstruction(wsolAta, payer, payer, [], TOKEN_PROGRAM_ID); +} + +/** + * Create WSOL ATA instruction + */ +export function createWsolAtaInstruction(payer: PublicKey): Instruction { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payer, true); + return createAssociatedTokenAccountInstruction( + payer, + wsolAta, + payer, + NATIVE_MINT, + TOKEN_PROGRAM_ID + ); +} + +// ============================================ +// Types +// ============================================ + +export interface BonkParams { + poolState?: PublicKey; + globalConfig: PublicKey; + platformConfig: PublicKey; + platformAssociatedAccount: PublicKey; + creatorAssociatedAccount: PublicKey; + baseVault?: PublicKey; + quoteVault?: PublicKey; + mintTokenProgram: PublicKey; + virtualBase: bigint; + virtualQuote: bigint; + realBase: bigint; + realQuote: bigint; +} + +export interface BuildBuyInstructionsParams { + payer: Keypair | PublicKey; + outputMint: PublicKey; + inputAmount: bigint; + slippageBasisPoints?: bigint; + fixedOutputAmount?: bigint; + createInputMintAta?: boolean; + createOutputMintAta?: boolean; + closeInputMintAta?: boolean; + protocolParams: BonkParams; +} + +export interface BuildSellInstructionsParams { + payer: Keypair | PublicKey; + inputMint: PublicKey; + inputAmount: bigint; + slippageBasisPoints?: bigint; + fixedOutputAmount?: bigint; + createOutputMintAta?: boolean; + closeOutputMintAta?: boolean; + closeInputMintAta?: boolean; + protocolParams: BonkParams; +} + +// ============================================ +// Instruction Builders +// ============================================ + +/** + * Build buy instructions for Bonk protocol + */ +export function buildBuyInstructions( + params: BuildBuyInstructionsParams +): Instruction[] { + const { + payer, + outputMint, + inputAmount, + slippageBasisPoints = 1000n, + fixedOutputAmount, + createInputMintAta = true, + createOutputMintAta = true, + closeInputMintAta = false, + protocolParams, + } = params; + + if (inputAmount === 0n) { + throw new Error("Amount cannot be zero"); + } + + const payerPubkey = payer instanceof Keypair ? payer.publicKey : payer; + const instructions: Instruction[] = []; + + const { + globalConfig, + platformConfig, + platformAssociatedAccount, + creatorAssociatedAccount, + baseVault, + quoteVault, + mintTokenProgram, + virtualBase, + virtualQuote, + realBase, + realQuote, + } = protocolParams; + + // Check if USD1 pool + const isUsd1Pool = globalConfig.equals(USD1_GLOBAL_CONFIG); + + // Determine quote token mint + const quoteTokenMint = isUsd1Pool ? USD1_TOKEN_ACCOUNT : WSOL_TOKEN_ACCOUNT; + + // Derive pool state + const poolState = protocolParams.poolState && !protocolParams.poolState.equals(PublicKey.default) + ? protocolParams.poolState + : getPoolPda(outputMint, quoteTokenMint); + + // Calculate minimum amount out + const minimumAmountOut = fixedOutputAmount + ? fixedOutputAmount + : getAmountOut( + inputAmount, + PROTOCOL_FEE_RATE, + PLATFORM_FEE_RATE, + SHARE_FEE_RATE, + virtualBase, + virtualQuote, + realBase, + realQuote, + slippageBasisPoints + ); + + // Derive user token accounts + const userBaseTokenAccount = getAssociatedTokenAddressSync( + outputMint, + payerPubkey, + true, + mintTokenProgram + ); + const userQuoteTokenAccount = getAssociatedTokenAddressSync( + quoteTokenMint, + payerPubkey, + true, + TOKEN_PROGRAM_ID + ); + + // Derive vault accounts + const baseVaultAccount = baseVault && !baseVault.equals(PublicKey.default) + ? baseVault + : getVaultPda(poolState, outputMint); + const quoteVaultAccount = quoteVault && !quoteVault.equals(PublicKey.default) + ? quoteVault + : getVaultPda(poolState, quoteTokenMint); + + // Handle WSOL wrapping for non-USD1 pools + if (createInputMintAta && !isUsd1Pool) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createAssociatedTokenAccountInstruction( + payerPubkey, + wsolAta, + payerPubkey, + NATIVE_MINT, + TOKEN_PROGRAM_ID + ) + ); + instructions.push(createSyncNativeInstruction(wsolAta)); + } + + // Create output mint ATA if needed + if (createOutputMintAta) { + instructions.push( + createAssociatedTokenAccountInstruction( + payerPubkey, + userBaseTokenAccount, + payerPubkey, + outputMint, + mintTokenProgram + ) + ); + } + + // Build instruction data + const shareFeeRate = 0n; + const data = Buffer.alloc(32); + BUY_EXACT_IN_DISCRIMINATOR.copy(data, 0); + data.writeBigUInt64LE(inputAmount, 8); + data.writeBigUInt64LE(minimumAmountOut, 16); + data.writeBigUInt64LE(shareFeeRate, 24); + + // Build accounts + const accounts: AccountMeta[] = [ + { pubkey: payerPubkey, isSigner: true, isWritable: true }, + { pubkey: AUTHORITY, isSigner: false, isWritable: false }, + { pubkey: globalConfig, isSigner: false, isWritable: false }, + { pubkey: platformConfig, isSigner: false, isWritable: false }, + { pubkey: poolState, isSigner: false, isWritable: true }, + { pubkey: userBaseTokenAccount, isSigner: false, isWritable: true }, + { pubkey: userQuoteTokenAccount, isSigner: false, isWritable: true }, + { pubkey: baseVaultAccount, isSigner: false, isWritable: true }, + { pubkey: quoteVaultAccount, isSigner: false, isWritable: true }, + { pubkey: outputMint, isSigner: false, isWritable: false }, + { pubkey: quoteTokenMint, isSigner: false, isWritable: false }, + { pubkey: mintTokenProgram, isSigner: false, isWritable: false }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: EVENT_AUTHORITY, isSigner: false, isWritable: false }, + { pubkey: BONK_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: platformAssociatedAccount, isSigner: false, isWritable: true }, + { pubkey: creatorAssociatedAccount, isSigner: false, isWritable: true }, + ]; + + instructions.push( + new Instruction({ + keys: accounts, + programId: BONK_PROGRAM_ID, + data, + }) + ); + + // Close WSOL ATA if requested + if (closeInputMintAta && !isUsd1Pool) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createCloseAccountInstruction(wsolAta, payerPubkey, payerPubkey, [], TOKEN_PROGRAM_ID) + ); + } + + return instructions; +} + +/** + * Build sell instructions for Bonk protocol + */ +export function buildSellInstructions( + params: BuildSellInstructionsParams +): Instruction[] { + const { + payer, + inputMint, + inputAmount, + slippageBasisPoints = 1000n, + fixedOutputAmount, + createOutputMintAta = true, + closeOutputMintAta = false, + closeInputMintAta = false, + protocolParams, + } = params; + + if (inputAmount === 0n) { + throw new Error("Amount cannot be zero"); + } + + const payerPubkey = payer instanceof Keypair ? payer.publicKey : payer; + const instructions: Instruction[] = []; + + const { + globalConfig, + platformConfig, + platformAssociatedAccount, + creatorAssociatedAccount, + baseVault, + quoteVault, + mintTokenProgram, + virtualBase, + virtualQuote, + realBase, + realQuote, + } = protocolParams; + + // Check if USD1 pool + const isUsd1Pool = globalConfig.equals(USD1_GLOBAL_CONFIG); + + // Determine quote token mint + const quoteTokenMint = isUsd1Pool ? USD1_TOKEN_ACCOUNT : WSOL_TOKEN_ACCOUNT; + + // Derive pool state + const poolState = protocolParams.poolState && !protocolParams.poolState.equals(PublicKey.default) + ? protocolParams.poolState + : getPoolPda(inputMint, quoteTokenMint); + + // Calculate minimum amount out + const minimumAmountOut = fixedOutputAmount + ? fixedOutputAmount + : getAmountOut( + inputAmount, + PROTOCOL_FEE_RATE, + PLATFORM_FEE_RATE, + SHARE_FEE_RATE, + virtualBase, + virtualQuote, + realBase, + realQuote, + slippageBasisPoints + ); + + // Derive user token accounts + const userBaseTokenAccount = getAssociatedTokenAddressSync( + inputMint, + payerPubkey, + true, + mintTokenProgram + ); + const userQuoteTokenAccount = getAssociatedTokenAddressSync( + quoteTokenMint, + payerPubkey, + true, + TOKEN_PROGRAM_ID + ); + + // Derive vault accounts + const baseVaultAccount = baseVault && !baseVault.equals(PublicKey.default) + ? baseVault + : getVaultPda(poolState, inputMint); + const quoteVaultAccount = quoteVault && !quoteVault.equals(PublicKey.default) + ? quoteVault + : getVaultPda(poolState, quoteTokenMint); + + // Create WSOL ATA for receiving SOL if needed + if (createOutputMintAta && !isUsd1Pool) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createAssociatedTokenAccountInstruction( + payerPubkey, + wsolAta, + payerPubkey, + NATIVE_MINT, + TOKEN_PROGRAM_ID + ) + ); + } + + // Build instruction data + const shareFeeRate = 0n; + const data = Buffer.alloc(32); + SELL_EXACT_IN_DISCRIMINATOR.copy(data, 0); + data.writeBigUInt64LE(inputAmount, 8); + data.writeBigUInt64LE(minimumAmountOut, 16); + data.writeBigUInt64LE(shareFeeRate, 24); + + // Build accounts + const accounts: AccountMeta[] = [ + { pubkey: payerPubkey, isSigner: true, isWritable: true }, + { pubkey: AUTHORITY, isSigner: false, isWritable: false }, + { pubkey: globalConfig, isSigner: false, isWritable: false }, + { pubkey: platformConfig, isSigner: false, isWritable: false }, + { pubkey: poolState, isSigner: false, isWritable: true }, + { pubkey: userBaseTokenAccount, isSigner: false, isWritable: true }, + { pubkey: userQuoteTokenAccount, isSigner: false, isWritable: true }, + { pubkey: baseVaultAccount, isSigner: false, isWritable: true }, + { pubkey: quoteVaultAccount, isSigner: false, isWritable: true }, + { pubkey: inputMint, isSigner: false, isWritable: false }, + { pubkey: quoteTokenMint, isSigner: false, isWritable: false }, + { pubkey: mintTokenProgram, isSigner: false, isWritable: false }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: EVENT_AUTHORITY, isSigner: false, isWritable: false }, + { pubkey: BONK_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: platformAssociatedAccount, isSigner: false, isWritable: true }, + { pubkey: creatorAssociatedAccount, isSigner: false, isWritable: true }, + ]; + + instructions.push( + new Instruction({ + keys: accounts, + programId: BONK_PROGRAM_ID, + data, + }) + ); + + // Close WSOL ATA if requested + if (closeOutputMintAta && !isUsd1Pool) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createCloseAccountInstruction(wsolAta, payerPubkey, payerPubkey, [], TOKEN_PROGRAM_ID) + ); + } + + // Close input token ATA if requested + if (closeInputMintAta) { + instructions.push( + createCloseAccountInstruction( + userBaseTokenAccount, + payerPubkey, + payerPubkey, + [], + mintTokenProgram + ) + ); + } + + return instructions; +} diff --git a/sol-trade-sdk-nodejs/src/instruction/index.ts b/sol-trade-sdk-nodejs/src/instruction/index.ts new file mode 100644 index 0000000..f135428 --- /dev/null +++ b/sol-trade-sdk-nodejs/src/instruction/index.ts @@ -0,0 +1,21 @@ +/** + * Sol Trade SDK - Instruction Builders + * + * Production-grade instruction builders for Solana DEX protocols. + * Supports: PumpFun, Bonk, Raydium CPMM, Raydium AMM V4, Meteora DAMM V2 + */ + +// PumpFun Protocol +export * from "./pumpfun_builder"; + +// Bonk Protocol +export * from "./bonk_builder"; + +// Raydium CPMM Protocol +export * from "./raydium_cpmm_builder"; + +// Raydium AMM V4 Protocol +export * from "./raydium_amm_v4_builder"; + +// Meteora DAMM V2 Protocol +export * from "./meteora_damm_v2_builder"; diff --git a/sol-trade-sdk-nodejs/src/instruction/meteora_damm_v2_builder.ts b/sol-trade-sdk-nodejs/src/instruction/meteora_damm_v2_builder.ts new file mode 100644 index 0000000..fe66293 --- /dev/null +++ b/sol-trade-sdk-nodejs/src/instruction/meteora_damm_v2_builder.ts @@ -0,0 +1,437 @@ +/** + * Meteora DAMM V2 Protocol Instruction Builder + * + * Production-grade instruction builder for Meteora DAMM V2 protocol. + * Supports swap operations with WSOL and USDC pools. + */ + +import { + PublicKey, + Keypair, + AccountMeta, + Instruction, + SystemProgram, +} from "@solana/web3.js"; +import { + getAssociatedTokenAddressSync, + createAssociatedTokenAccountInstruction, + TOKEN_PROGRAM_ID, + createCloseAccountInstruction, + NATIVE_MINT, + createSyncNativeInstruction, +} from "@solana/spl-token"; + +// ============================================ +// Program IDs and Constants +// ============================================ + +/** Meteora DAMM V2 program ID */ +export const METEORA_DAMM_V2_PROGRAM_ID = new PublicKey( + "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG" +); + +/** Authority */ +export const AUTHORITY = new PublicKey( + "HLnpSz9h2S4hiLQ43rnSD9XkcUThA7B8hQMKmDaiTLcC" +); + +/** WSOL Token Account (mint) */ +export const WSOL_TOKEN_ACCOUNT = new PublicKey( + "So11111111111111111111111111111111111111112" +); + +/** USDC Token Account (mint) */ +export const USDC_TOKEN_ACCOUNT = new PublicKey( + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" +); + +// ============================================ +// Discriminators +// ============================================ + +/** Swap instruction discriminator */ +export const SWAP_DISCRIMINATOR: Buffer = Buffer.from([ + 248, 198, 158, 145, 225, 117, 135, 200, +]); + +// ============================================ +// Seeds +// ============================================ + +export const EVENT_AUTHORITY_SEED = Buffer.from("__event_authority"); + +// ============================================ +// PDA Derivation Functions +// ============================================ + +/** + * Derive the event authority PDA + */ +export function getEventAuthorityPda(): PublicKey { + const [pda] = PublicKey.findProgramAddressSync( + [EVENT_AUTHORITY_SEED], + METEORA_DAMM_V2_PROGRAM_ID + ); + return pda; +} + +// ============================================ +// Helper Functions +// ============================================ + +/** + * Create instructions to wrap SOL into WSOL + */ +export function createWsolInstructions( + payer: PublicKey, + amount: bigint +): Instruction[] { + const instructions: Instruction[] = []; + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payer, true); + + // Sync native (wrap SOL) + instructions.push(createSyncNativeInstruction(wsolAta)); + + return instructions; +} + +/** + * Create instruction to close WSOL ATA and unwrap to SOL + */ +export function createCloseWsolInstruction(payer: PublicKey): Instruction { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payer, true); + return createCloseAccountInstruction(wsolAta, payer, payer, [], TOKEN_PROGRAM_ID); +} + +/** + * Create WSOL ATA instruction + */ +export function createWsolAtaInstruction(payer: PublicKey): Instruction { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payer, true); + return createAssociatedTokenAccountInstruction( + payer, + wsolAta, + payer, + NATIVE_MINT, + TOKEN_PROGRAM_ID + ); +} + +// ============================================ +// Types +// ============================================ + +export interface MeteoraDammV2Params { + pool: PublicKey; + tokenAMint: PublicKey; + tokenBMint: PublicKey; + tokenAVault: PublicKey; + tokenBVault: PublicKey; + tokenAProgram: PublicKey; + tokenBProgram: PublicKey; +} + +export interface BuildBuyInstructionsParams { + payer: Keypair | PublicKey; + inputMint: PublicKey; + outputMint: PublicKey; + inputAmount: bigint; + slippageBasisPoints?: bigint; + fixedOutputAmount?: bigint; + createInputMintAta?: boolean; + createOutputMintAta?: boolean; + closeInputMintAta?: boolean; + protocolParams: MeteoraDammV2Params; +} + +export interface BuildSellInstructionsParams { + payer: Keypair | PublicKey; + inputMint: PublicKey; + outputMint: PublicKey; + inputAmount: bigint; + slippageBasisPoints?: bigint; + fixedOutputAmount?: bigint; + createOutputMintAta?: boolean; + closeOutputMintAta?: boolean; + closeInputMintAta?: boolean; + protocolParams: MeteoraDammV2Params; +} + +// ============================================ +// Instruction Builders +// ============================================ + +/** + * Build buy instructions for Meteora DAMM V2 protocol + */ +export function buildBuyInstructions( + params: BuildBuyInstructionsParams +): Instruction[] { + const { + payer, + inputMint, + outputMint, + inputAmount, + fixedOutputAmount, + createInputMintAta = true, + createOutputMintAta = true, + closeInputMintAta = false, + protocolParams, + } = params; + + if (inputAmount === 0n) { + throw new Error("Amount cannot be zero"); + } + + if (!fixedOutputAmount) { + throw new Error("fixedOutputAmount must be set for Meteora DAMM V2 swap"); + } + + const payerPubkey = payer instanceof Keypair ? payer.publicKey : payer; + const instructions: Instruction[] = []; + + const { + pool, + tokenAMint, + tokenBMint, + tokenAVault, + tokenBVault, + tokenAProgram, + tokenBProgram, + } = protocolParams; + + // Check pool type + const isWsol = tokenAMint.equals(WSOL_TOKEN_ACCOUNT) || tokenBMint.equals(WSOL_TOKEN_ACCOUNT); + const isUsdc = tokenAMint.equals(USDC_TOKEN_ACCOUNT) || tokenBMint.equals(USDC_TOKEN_ACCOUNT); + + if (!isWsol && !isUsdc) { + throw new Error("Pool must contain WSOL or USDC"); + } + + // Determine swap direction + const isAIn = tokenAMint.equals(WSOL_TOKEN_ACCOUNT) || tokenAMint.equals(USDC_TOKEN_ACCOUNT); + + // Derive user token accounts + const inputTokenAccount = getAssociatedTokenAddressSync( + inputMint, + payerPubkey, + true, + isAIn ? tokenAProgram : tokenBProgram + ); + const outputTokenAccount = getAssociatedTokenAddressSync( + outputMint, + payerPubkey, + true, + isAIn ? tokenBProgram : tokenAProgram + ); + + // Derive event authority + const eventAuthority = getEventAuthorityPda(); + + // Handle WSOL wrapping + if (createInputMintAta && isWsol) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createAssociatedTokenAccountInstruction( + payerPubkey, + wsolAta, + payerPubkey, + NATIVE_MINT, + TOKEN_PROGRAM_ID + ) + ); + instructions.push(createSyncNativeInstruction(wsolAta)); + } + + // Create output mint ATA if needed + if (createOutputMintAta) { + instructions.push( + createAssociatedTokenAccountInstruction( + payerPubkey, + outputTokenAccount, + payerPubkey, + outputMint, + TOKEN_PROGRAM_ID + ) + ); + } + + // Build instruction data + const data = Buffer.alloc(24); + SWAP_DISCRIMINATOR.copy(data, 0); + data.writeBigUInt64LE(inputAmount, 8); + data.writeBigUInt64LE(fixedOutputAmount, 16); + + // Build accounts + const accounts: AccountMeta[] = [ + { pubkey: AUTHORITY, isSigner: false, isWritable: false }, + { pubkey: pool, isSigner: false, isWritable: true }, + { pubkey: inputTokenAccount, isSigner: false, isWritable: true }, + { pubkey: outputTokenAccount, isSigner: false, isWritable: true }, + { pubkey: tokenAVault, isSigner: false, isWritable: true }, + { pubkey: tokenBVault, isSigner: false, isWritable: true }, + { pubkey: tokenAMint, isSigner: false, isWritable: false }, + { pubkey: tokenBMint, isSigner: false, isWritable: false }, + { pubkey: payerPubkey, isSigner: true, isWritable: true }, + { pubkey: tokenAProgram, isSigner: false, isWritable: false }, + { pubkey: tokenBProgram, isSigner: false, isWritable: false }, + { pubkey: METEORA_DAMM_V2_PROGRAM_ID, isSigner: false, isWritable: false }, // Referral Token Account (placeholder) + { pubkey: eventAuthority, isSigner: false, isWritable: false }, + { pubkey: METEORA_DAMM_V2_PROGRAM_ID, isSigner: false, isWritable: false }, + ]; + + instructions.push( + new Instruction({ + keys: accounts, + programId: METEORA_DAMM_V2_PROGRAM_ID, + data, + }) + ); + + // Close WSOL ATA if requested + if (closeInputMintAta && isWsol) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createCloseAccountInstruction(wsolAta, payerPubkey, payerPubkey, [], TOKEN_PROGRAM_ID) + ); + } + + return instructions; +} + +/** + * Build sell instructions for Meteora DAMM V2 protocol + */ +export function buildSellInstructions( + params: BuildSellInstructionsParams +): Instruction[] { + const { + payer, + inputMint, + outputMint, + inputAmount, + fixedOutputAmount, + createOutputMintAta = true, + closeOutputMintAta = false, + closeInputMintAta = false, + protocolParams, + } = params; + + if (inputAmount === 0n) { + throw new Error("Amount cannot be zero"); + } + + if (!fixedOutputAmount) { + throw new Error("fixedOutputAmount must be set for Meteora DAMM V2 swap"); + } + + const payerPubkey = payer instanceof Keypair ? payer.publicKey : payer; + const instructions: Instruction[] = []; + + const { + pool, + tokenAMint, + tokenBMint, + tokenAVault, + tokenBVault, + tokenAProgram, + tokenBProgram, + } = protocolParams; + + // Check pool type + const isWsol = tokenBMint.equals(WSOL_TOKEN_ACCOUNT) || tokenAMint.equals(WSOL_TOKEN_ACCOUNT); + const isUsdc = tokenBMint.equals(USDC_TOKEN_ACCOUNT) || tokenAMint.equals(USDC_TOKEN_ACCOUNT); + + if (!isWsol && !isUsdc) { + throw new Error("Pool must contain WSOL or USDC"); + } + + // Determine swap direction (selling token for WSOL/USDC) + const isAIn = tokenBMint.equals(WSOL_TOKEN_ACCOUNT) || tokenBMint.equals(USDC_TOKEN_ACCOUNT); + + // Derive user token accounts + const inputTokenAccount = getAssociatedTokenAddressSync( + inputMint, + payerPubkey, + true, + isAIn ? tokenAProgram : tokenBProgram + ); + const outputTokenAccount = getAssociatedTokenAddressSync( + outputMint, + payerPubkey, + true, + isAIn ? tokenBProgram : tokenAProgram + ); + + // Derive event authority + const eventAuthority = getEventAuthorityPda(); + + // Create WSOL ATA for receiving if needed + if (createOutputMintAta && isWsol) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createAssociatedTokenAccountInstruction( + payerPubkey, + wsolAta, + payerPubkey, + NATIVE_MINT, + TOKEN_PROGRAM_ID + ) + ); + } + + // Build instruction data + const data = Buffer.alloc(24); + SWAP_DISCRIMINATOR.copy(data, 0); + data.writeBigUInt64LE(inputAmount, 8); + data.writeBigUInt64LE(fixedOutputAmount, 16); + + // Build accounts + const accounts: AccountMeta[] = [ + { pubkey: AUTHORITY, isSigner: false, isWritable: false }, + { pubkey: pool, isSigner: false, isWritable: true }, + { pubkey: inputTokenAccount, isSigner: false, isWritable: true }, + { pubkey: outputTokenAccount, isSigner: false, isWritable: true }, + { pubkey: tokenAVault, isSigner: false, isWritable: true }, + { pubkey: tokenBVault, isSigner: false, isWritable: true }, + { pubkey: tokenAMint, isSigner: false, isWritable: false }, + { pubkey: tokenBMint, isSigner: false, isWritable: false }, + { pubkey: payerPubkey, isSigner: true, isWritable: true }, + { pubkey: tokenAProgram, isSigner: false, isWritable: false }, + { pubkey: tokenBProgram, isSigner: false, isWritable: false }, + { pubkey: METEORA_DAMM_V2_PROGRAM_ID, isSigner: false, isWritable: false }, // Referral Token Account + { pubkey: eventAuthority, isSigner: false, isWritable: false }, + { pubkey: METEORA_DAMM_V2_PROGRAM_ID, isSigner: false, isWritable: false }, + ]; + + instructions.push( + new Instruction({ + keys: accounts, + programId: METEORA_DAMM_V2_PROGRAM_ID, + data, + }) + ); + + // Close WSOL ATA if requested + if (closeOutputMintAta && isWsol) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createCloseAccountInstruction(wsolAta, payerPubkey, payerPubkey, [], TOKEN_PROGRAM_ID) + ); + } + + // Close input token ATA if requested + if (closeInputMintAta) { + instructions.push( + createCloseAccountInstruction( + inputTokenAccount, + payerPubkey, + payerPubkey, + [], + isAIn ? tokenAProgram : tokenBProgram + ) + ); + } + + return instructions; +} diff --git a/sol-trade-sdk-nodejs/src/instruction/pumpfun_builder.ts b/sol-trade-sdk-nodejs/src/instruction/pumpfun_builder.ts new file mode 100644 index 0000000..f7ffc04 --- /dev/null +++ b/sol-trade-sdk-nodejs/src/instruction/pumpfun_builder.ts @@ -0,0 +1,515 @@ +/** + * PumpFun Protocol Instruction Builder + * + * Production-grade instruction builder for PumpFun bonding curve protocol. + * Supports buy, sell, and cashback claim operations. + */ + +import { + PublicKey, + Keypair, + AccountMeta, + Instruction, + SYSVAR_RENT_PUBKEY, + SystemProgram, +} from "@solana/web3.js"; +import { + getAssociatedTokenAddressSync, + createAssociatedTokenAccountInstruction, + TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + createCloseAccountInstruction, +} from "@solana/spl-token"; + +// ============================================ +// Program IDs and Constants +// ============================================ + +/** PumpFun program ID */ +export const PUMPFUN_PROGRAM_ID = new PublicKey( + "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" +); + +/** MPL Token Metadata program ID */ +export const MPL_TOKEN_METADATA_PROGRAM_ID = new PublicKey( + "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" +); + +/** Event Authority for PumpFun */ +export const EVENT_AUTHORITY = new PublicKey( + "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1" +); + +/** Fee Program */ +export const FEE_PROGRAM = new PublicKey( + "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" +); + +/** Global Volume Accumulator */ +export const GLOBAL_VOLUME_ACCUMULATOR = new PublicKey( + "Hq2wp8uJ9jCPsYgNHex8RtqdvMPfVGoYwjvF1ATiwn2Y" +); + +/** Fee Config */ +export const FEE_CONFIG = new PublicKey( + "8Wf5TiAheLUqBrKXeYg2JtAFFMWtKdG2BSFgqUcPVwTt" +); + +/** Global Account */ +export const GLOBAL_ACCOUNT = new PublicKey( + "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf" +); + +/** Authority */ +export const AUTHORITY = new PublicKey( + "FFWtrEQ4B4PKQoVuHYzZq8FabGkVatYzDpEVHsK5rrhF" +); + +/** Withdraw Authority */ +export const WITHDRAW_AUTHORITY = new PublicKey( + "39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg" +); + +/** Fee Recipient */ +export const FEE_RECIPIENT = new PublicKey( + "62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV" +); + +/** Mayhem Fee Recipients */ +export const MAYHEM_FEE_RECIPIENTS: PublicKey[] = [ + new PublicKey("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"), + new PublicKey("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"), + new PublicKey("8SBKzEQU4nLSzcwF4a74F2iaUDQyTfjGndn6qUWBnrpR"), + new PublicKey("4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH"), + new PublicKey("8sNeir4QsLsJdYpc9RZacohhK1Y5FLU3nC5LXgYB4aa6"), + new PublicKey("Fh9HmeLNUMVCvejxCtCL2DbYaRyBFVJ5xrWkLnMH6fdk"), + new PublicKey("463MEnMeGyJekNZFQSTUABBEbLnvMTALbT6ZmsxAbAdq"), + new PublicKey("6AUH3WEHucYZyC61hqpqYUWVto5qA5hjHuNQ32GNnNxA"), +]; + +// ============================================ +// Discriminators +// ============================================ + +/** Buy instruction discriminator */ +export const BUY_DISCRIMINATOR: Buffer = Buffer.from([ + 102, 6, 61, 18, 1, 218, 235, 234, +]); + +/** Buy exact SOL in discriminator */ +export const BUY_EXACT_SOL_IN_DISCRIMINATOR: Buffer = Buffer.from([ + 56, 252, 116, 8, 158, 223, 205, 95, +]); + +/** Sell instruction discriminator */ +export const SELL_DISCRIMINATOR: Buffer = Buffer.from([ + 51, 230, 133, 164, 1, 127, 131, 173, +]); + +/** Claim cashback discriminator */ +export const CLAIM_CASHBACK_DISCRIMINATOR: Buffer = Buffer.from([ + 37, 58, 35, 126, 190, 53, 228, 197, +]); + +// ============================================ +// Seeds +// ============================================ + +export const BONDING_CURVE_SEED = Buffer.from("bonding-curve"); +export const BONDING_CURVE_V2_SEED = Buffer.from("bonding-curve-v2"); +export const CREATOR_VAULT_SEED = Buffer.from("creator-vault"); +export const USER_VOLUME_ACCUMULATOR_SEED = Buffer.from("user_volume_accumulator"); + +// ============================================ +// PDA Derivation Functions +// ============================================ + +/** + * Derive the bonding curve PDA for a given mint + */ +export function getBondingCurvePda(mint: PublicKey): PublicKey { + const [pda] = PublicKey.findProgramAddressSync( + [BONDING_CURVE_SEED, mint.toBuffer()], + PUMPFUN_PROGRAM_ID + ); + return pda; +} + +/** + * Derive the bonding curve v2 PDA for a given mint + */ +export function getBondingCurveV2Pda(mint: PublicKey): PublicKey { + const [pda] = PublicKey.findProgramAddressSync( + [BONDING_CURVE_V2_SEED, mint.toBuffer()], + PUMPFUN_PROGRAM_ID + ); + return pda; +} + +/** + * Derive the creator vault PDA for a given creator + */ +export function getCreatorVaultPda(creator: PublicKey): PublicKey { + const [pda] = PublicKey.findProgramAddressSync( + [CREATOR_VAULT_SEED, creator.toBuffer()], + PUMPFUN_PROGRAM_ID + ); + return pda; +} + +/** + * Derive the user volume accumulator PDA for a given user + */ +export function getUserVolumeAccumulatorPda(user: PublicKey): PublicKey { + const [pda] = PublicKey.findProgramAddressSync( + [USER_VOLUME_ACCUMULATOR_SEED, user.toBuffer()], + PUMPFUN_PROGRAM_ID + ); + return pda; +} + +/** + * Get a random Mayhem fee recipient + */ +export function getRandomMayhemFeeRecipient(): PublicKey { + const index = Math.floor(Math.random() * MAYHEM_FEE_RECIPIENTS.length); + return MAYHEM_FEE_RECIPIENTS[index]; +} + +// ============================================ +// Helper Functions +// ============================================ + +/** + * Calculate buy amount with slippage protection + */ +export function calculateWithSlippageBuy(amount: bigint, basisPoints: bigint): bigint { + const maxBps = 9999n; + const bps = basisPoints > maxBps ? maxBps : basisPoints; + return amount + (amount * bps) / 10000n; +} + +/** + * Calculate sell amount with slippage protection + */ +export function calculateWithSlippageSell(amount: bigint, basisPoints: bigint): bigint { + if (amount <= basisPoints / 10000n) { + return 1n; + } + return amount - (amount * basisPoints) / 10000n; +} + +// ============================================ +// Types +// ============================================ + +export interface BondingCurve { + account: PublicKey; + virtualTokenReserves: bigint; + virtualSolReserves: bigint; + realTokenReserves: bigint; + isMayhemMode: boolean; + isCashbackCoin: boolean; +} + +export interface PumpFunParams { + bondingCurve: BondingCurve; + creatorVault: PublicKey; + tokenProgram: PublicKey; + associatedBondingCurve?: PublicKey; + closeTokenAccountWhenSell?: boolean; +} + +export interface BuildBuyInstructionsParams { + payer: Keypair | PublicKey; + outputMint: PublicKey; + inputAmount: bigint; + slippageBasisPoints?: bigint; + fixedOutputAmount?: bigint; + createOutputMintAta?: boolean; + protocolParams: PumpFunParams; + useExactSolAmount?: boolean; +} + +export interface BuildSellInstructionsParams { + payer: Keypair | PublicKey; + inputMint: PublicKey; + inputAmount: bigint; + slippageBasisPoints?: bigint; + fixedOutputAmount?: bigint; + closeInputMintAta?: boolean; + protocolParams: PumpFunParams; +} + +// ============================================ +// Instruction Builders +// ============================================ + +/** + * Build buy instructions for PumpFun protocol + */ +export function buildBuyInstructions( + params: BuildBuyInstructionsParams +): Instruction[] { + const { + payer, + outputMint, + inputAmount, + slippageBasisPoints = 1000n, + fixedOutputAmount, + createOutputMintAta = true, + protocolParams, + useExactSolAmount = true, + } = params; + + if (inputAmount === 0n) { + throw new Error("Amount cannot be zero"); + } + + const payerPubkey = payer instanceof Keypair ? payer.publicKey : payer; + const instructions: Instruction[] = []; + + const { bondingCurve, creatorVault, tokenProgram, associatedBondingCurve } = protocolParams; + + // Derive bonding curve address + const bondingCurveAddr = + bondingCurve.account.equals(PublicKey.default) || bondingCurve.account === undefined + ? getBondingCurvePda(outputMint) + : bondingCurve.account; + + // Get token program + const tokenProgramId = tokenProgram || TOKEN_PROGRAM_ID; + + // Derive associated bonding curve + const associatedBondingCurveAddr = + associatedBondingCurve && !associatedBondingCurve.equals(PublicKey.default) + ? associatedBondingCurve + : getAssociatedTokenAddressSync(outputMint, bondingCurveAddr, true, tokenProgramId); + + // Derive user token account + const userTokenAccount = getAssociatedTokenAddressSync( + outputMint, + payerPubkey, + true, + tokenProgramId + ); + + // Derive user volume accumulator + const userVolumeAccumulator = getUserVolumeAccumulatorPda(payerPubkey); + + // Create ATA if needed + if (createOutputMintAta) { + instructions.push( + createAssociatedTokenAccountInstruction( + payerPubkey, + userTokenAccount, + payerPubkey, + outputMint, + tokenProgramId + ) + ); + } + + // Determine fee recipient + const feeRecipient = bondingCurve.isMayhemMode + ? getRandomMayhemFeeRecipient() + : FEE_RECIPIENT; + + // Derive bonding curve v2 + const bondingCurveV2 = getBondingCurveV2Pda(outputMint); + + // Track volume for cashback coins + const trackVolume = bondingCurve.isCashbackCoin + ? Buffer.from([1, 1]) + : Buffer.from([1, 0]); + + // Build instruction data + let data: Buffer; + if (useExactSolAmount) { + // buy_exact_sol_in(spendable_sol_in: u64, min_tokens_out: u64, track_volume) + const minTokensOut = fixedOutputAmount + ? fixedOutputAmount + : calculateWithSlippageSell(inputAmount, slippageBasisPoints); + data = Buffer.alloc(26); + BUY_EXACT_SOL_IN_DISCRIMINATOR.copy(data, 0); + data.writeBigUInt64LE(inputAmount, 8); + data.writeBigUInt64LE(minTokensOut, 16); + trackVolume.copy(data, 24); + } else { + // buy(token_amount: u64, max_sol_cost: u64, track_volume) + const maxSolCost = calculateWithSlippageBuy(inputAmount, slippageBasisPoints); + data = Buffer.alloc(26); + BUY_DISCRIMINATOR.copy(data, 0); + data.writeBigUInt64LE(inputAmount, 8); + data.writeBigUInt64LE(maxSolCost, 16); + trackVolume.copy(data, 24); + } + + // Build accounts + const accounts: AccountMeta[] = [ + { pubkey: GLOBAL_ACCOUNT, isSigner: false, isWritable: false }, + { pubkey: feeRecipient, isSigner: false, isWritable: true }, + { pubkey: outputMint, isSigner: false, isWritable: false }, + { pubkey: bondingCurveAddr, isSigner: false, isWritable: true }, + { pubkey: associatedBondingCurveAddr, isSigner: false, isWritable: true }, + { pubkey: userTokenAccount, isSigner: false, isWritable: true }, + { pubkey: payerPubkey, isSigner: true, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: tokenProgramId, isSigner: false, isWritable: false }, + { pubkey: creatorVault, isSigner: false, isWritable: true }, + { pubkey: EVENT_AUTHORITY, isSigner: false, isWritable: false }, + { pubkey: PUMPFUN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: GLOBAL_VOLUME_ACCUMULATOR, isSigner: false, isWritable: true }, + { pubkey: userVolumeAccumulator, isSigner: false, isWritable: true }, + { pubkey: FEE_CONFIG, isSigner: false, isWritable: false }, + { pubkey: FEE_PROGRAM, isSigner: false, isWritable: false }, + { pubkey: bondingCurveV2, isSigner: false, isWritable: false }, + ]; + + instructions.push( + new Instruction({ + keys: accounts, + programId: PUMPFUN_PROGRAM_ID, + data, + }) + ); + + return instructions; +} + +/** + * Build sell instructions for PumpFun protocol + */ +export function buildSellInstructions( + params: BuildSellInstructionsParams +): Instruction[] { + const { + payer, + inputMint, + inputAmount, + slippageBasisPoints = 1000n, + fixedOutputAmount, + closeInputMintAta = false, + protocolParams, + } = params; + + if (inputAmount === 0n) { + throw new Error("Amount cannot be zero"); + } + + const payerPubkey = payer instanceof Keypair ? payer.publicKey : payer; + const instructions: Instruction[] = []; + + const { bondingCurve, creatorVault, tokenProgram, associatedBondingCurve, closeTokenAccountWhenSell } = protocolParams; + + // Derive bonding curve address + const bondingCurveAddr = + bondingCurve.account.equals(PublicKey.default) || bondingCurve.account === undefined + ? getBondingCurvePda(inputMint) + : bondingCurve.account; + + // Get token program + const tokenProgramId = tokenProgram || TOKEN_PROGRAM_ID; + + // Derive associated bonding curve + const associatedBondingCurveAddr = + associatedBondingCurve && !associatedBondingCurve.equals(PublicKey.default) + ? associatedBondingCurve + : getAssociatedTokenAddressSync(inputMint, bondingCurveAddr, true, tokenProgramId); + + // Derive user token account + const userTokenAccount = getAssociatedTokenAddressSync( + inputMint, + payerPubkey, + true, + tokenProgramId + ); + + // Determine fee recipient + const feeRecipient = bondingCurve.isMayhemMode + ? getRandomMayhemFeeRecipient() + : FEE_RECIPIENT; + + // Derive bonding curve v2 + const bondingCurveV2 = getBondingCurveV2Pda(inputMint); + + // Build instruction data (sell: token_amount, min_sol_output) + const minSolOutput = fixedOutputAmount + ? fixedOutputAmount + : calculateWithSlippageSell(inputAmount, slippageBasisPoints); + const data = Buffer.alloc(24); + SELL_DISCRIMINATOR.copy(data, 0); + data.writeBigUInt64LE(inputAmount, 8); + data.writeBigUInt64LE(minSolOutput, 16); + + // Build accounts + const accounts: AccountMeta[] = [ + { pubkey: GLOBAL_ACCOUNT, isSigner: false, isWritable: false }, + { pubkey: feeRecipient, isSigner: false, isWritable: true }, + { pubkey: inputMint, isSigner: false, isWritable: false }, + { pubkey: bondingCurveAddr, isSigner: false, isWritable: true }, + { pubkey: associatedBondingCurveAddr, isSigner: false, isWritable: true }, + { pubkey: userTokenAccount, isSigner: false, isWritable: true }, + { pubkey: payerPubkey, isSigner: true, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: creatorVault, isSigner: false, isWritable: true }, + { pubkey: tokenProgramId, isSigner: false, isWritable: false }, + { pubkey: EVENT_AUTHORITY, isSigner: false, isWritable: false }, + { pubkey: PUMPFUN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: FEE_CONFIG, isSigner: false, isWritable: false }, + { pubkey: FEE_PROGRAM, isSigner: false, isWritable: false }, + ]; + + // Add user volume accumulator for cashback coins + if (bondingCurve.isCashbackCoin) { + const userVolumeAccumulator = getUserVolumeAccumulatorPda(payerPubkey); + accounts.push({ pubkey: userVolumeAccumulator, isSigner: false, isWritable: true }); + } + + // Add bonding curve v2 + accounts.push({ pubkey: bondingCurveV2, isSigner: false, isWritable: false }); + + instructions.push( + new Instruction({ + keys: accounts, + programId: PUMPFUN_PROGRAM_ID, + data, + }) + ); + + // Close token account if requested + if (closeInputMintAta || closeTokenAccountWhenSell) { + instructions.push( + createCloseAccountInstruction( + userTokenAccount, + payerPubkey, + payerPubkey, + [], + tokenProgramId + ) + ); + } + + return instructions; +} + +/** + * Build claim cashback instruction for PumpFun + */ +export function buildClaimCashbackInstruction(payer: PublicKey): Instruction { + const userVolumeAccumulator = getUserVolumeAccumulatorPda(payer); + + const accounts: AccountMeta[] = [ + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: userVolumeAccumulator, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: EVENT_AUTHORITY, isSigner: false, isWritable: false }, + { pubkey: PUMPFUN_PROGRAM_ID, isSigner: false, isWritable: false }, + ]; + + return new Instruction({ + keys: accounts, + programId: PUMPFUN_PROGRAM_ID, + data: CLAIM_CASHBACK_DISCRIMINATOR, + }); +} diff --git a/sol-trade-sdk-nodejs/src/instruction/raydium_amm_v4_builder.ts b/sol-trade-sdk-nodejs/src/instruction/raydium_amm_v4_builder.ts new file mode 100644 index 0000000..2ca3c0b --- /dev/null +++ b/sol-trade-sdk-nodejs/src/instruction/raydium_amm_v4_builder.ts @@ -0,0 +1,476 @@ +/** + * Raydium AMM V4 Protocol Instruction Builder + * + * Production-grade instruction builder for Raydium AMM V4 protocol. + * Supports swap operations with WSOL and USDC pools. + */ + +import { + PublicKey, + Keypair, + AccountMeta, + Instruction, + SystemProgram, +} from "@solana/web3.js"; +import { + getAssociatedTokenAddressSync, + createAssociatedTokenAccountInstruction, + TOKEN_PROGRAM_ID, + createCloseAccountInstruction, + NATIVE_MINT, + createSyncNativeInstruction, +} from "@solana/spl-token"; + +// ============================================ +// Program IDs and Constants +// ============================================ + +/** Raydium AMM V4 program ID */ +export const RAYDIUM_AMM_V4_PROGRAM_ID = new PublicKey( + "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" +); + +/** Authority */ +export const AUTHORITY = new PublicKey( + "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1" +); + +/** WSOL Token Account (mint) */ +export const WSOL_TOKEN_ACCOUNT = new PublicKey( + "So11111111111111111111111111111111111111112" +); + +/** USDC Token Account (mint) */ +export const USDC_TOKEN_ACCOUNT = new PublicKey( + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" +); + +/** Fee rates */ +export const TRADE_FEE_NUMERATOR = 25n; +export const TRADE_FEE_DENOMINATOR = 10000n; +export const SWAP_FEE_NUMERATOR = 25n; +export const SWAP_FEE_DENOMINATOR = 10000n; + +// ============================================ +// Discriminators +// ============================================ + +/** Swap base in instruction discriminator (single byte) */ +export const SWAP_BASE_IN_DISCRIMINATOR: Buffer = Buffer.from([9]); + +/** Swap base out instruction discriminator (single byte) */ +export const SWAP_BASE_OUT_DISCRIMINATOR: Buffer = Buffer.from([11]); + +// ============================================ +// Seeds +// ============================================ + +export const POOL_SEED = Buffer.from("pool"); + +// ============================================ +// Helper Functions +// ============================================ + +/** + * Compute swap amount for AMM V4 + */ +export function computeSwapAmount( + coinReserve: bigint, + pcReserve: bigint, + isCoinIn: boolean, + amountIn: bigint, + slippageBasisPoints: bigint +): { amountOut: bigint; minAmountOut: bigint } { + // Apply trade fee (0.25%) + const amountInAfterFee = amountIn - (amountIn * TRADE_FEE_NUMERATOR) / TRADE_FEE_DENOMINATOR; + + // Calculate output using constant product formula + let amountOut: bigint; + if (isCoinIn) { + // Selling coin for pc: output = (pcReserve * amountIn) / (coinReserve + amountIn) + const denominator = coinReserve + amountInAfterFee; + amountOut = (pcReserve * amountInAfterFee) / denominator; + } else { + // Selling pc for coin: output = (coinReserve * amountIn) / (pcReserve + amountIn) + const denominator = pcReserve + amountInAfterFee; + amountOut = (coinReserve * amountInAfterFee) / denominator; + } + + // Apply slippage + const minAmountOut = amountOut - (amountOut * slippageBasisPoints) / 10000n; + + return { amountOut, minAmountOut }; +} + +/** + * Create instructions to wrap SOL into WSOL + */ +export function createWsolInstructions( + payer: PublicKey, + amount: bigint +): Instruction[] { + const instructions: Instruction[] = []; + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payer, true); + + // Sync native (wrap SOL) + instructions.push(createSyncNativeInstruction(wsolAta)); + + return instructions; +} + +/** + * Create instruction to close WSOL ATA and unwrap to SOL + */ +export function createCloseWsolInstruction(payer: PublicKey): Instruction { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payer, true); + return createCloseAccountInstruction(wsolAta, payer, payer, [], TOKEN_PROGRAM_ID); +} + +/** + * Create WSOL ATA instruction + */ +export function createWsolAtaInstruction(payer: PublicKey): Instruction { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payer, true); + return createAssociatedTokenAccountInstruction( + payer, + wsolAta, + payer, + NATIVE_MINT, + TOKEN_PROGRAM_ID + ); +} + +// ============================================ +// Types +// ============================================ + +export interface RaydiumAmmV4Params { + amm: PublicKey; + coinMint: PublicKey; + pcMint: PublicKey; + tokenCoin: PublicKey; + tokenPc: PublicKey; + coinReserve: bigint; + pcReserve: bigint; +} + +export interface BuildBuyInstructionsParams { + payer: Keypair | PublicKey; + outputMint: PublicKey; + inputAmount: bigint; + slippageBasisPoints?: bigint; + fixedOutputAmount?: bigint; + createInputMintAta?: boolean; + createOutputMintAta?: boolean; + closeInputMintAta?: boolean; + protocolParams: RaydiumAmmV4Params; +} + +export interface BuildSellInstructionsParams { + payer: Keypair | PublicKey; + inputMint: PublicKey; + inputAmount: bigint; + slippageBasisPoints?: bigint; + fixedOutputAmount?: bigint; + createOutputMintAta?: boolean; + closeOutputMintAta?: boolean; + closeInputMintAta?: boolean; + protocolParams: RaydiumAmmV4Params; +} + +// ============================================ +// Instruction Builders +// ============================================ + +/** + * Build buy instructions for Raydium AMM V4 protocol + */ +export function buildBuyInstructions( + params: BuildBuyInstructionsParams +): Instruction[] { + const { + payer, + outputMint, + inputAmount, + slippageBasisPoints = 1000n, + fixedOutputAmount, + createInputMintAta = true, + createOutputMintAta = true, + closeInputMintAta = false, + protocolParams, + } = params; + + if (inputAmount === 0n) { + throw new Error("Amount cannot be zero"); + } + + const payerPubkey = payer instanceof Keypair ? payer.publicKey : payer; + const instructions: Instruction[] = []; + + const { + amm, + coinMint, + pcMint, + tokenCoin, + tokenPc, + coinReserve, + pcReserve, + } = protocolParams; + + // Check pool type + const isWsol = coinMint.equals(WSOL_TOKEN_ACCOUNT) || pcMint.equals(WSOL_TOKEN_ACCOUNT); + const isUsdc = coinMint.equals(USDC_TOKEN_ACCOUNT) || pcMint.equals(USDC_TOKEN_ACCOUNT); + + if (!isWsol && !isUsdc) { + throw new Error("Pool must contain WSOL or USDC"); + } + + // Determine swap direction + const isBaseIn = coinMint.equals(WSOL_TOKEN_ACCOUNT) || coinMint.equals(USDC_TOKEN_ACCOUNT); + + // Calculate output + const swapResult = computeSwapAmount( + coinReserve, + pcReserve, + isBaseIn, + inputAmount, + slippageBasisPoints + ); + const minimumAmountOut = fixedOutputAmount || swapResult.minAmountOut; + + // Determine input/output mints + const inputMint = isWsol ? WSOL_TOKEN_ACCOUNT : USDC_TOKEN_ACCOUNT; + + // Derive user token accounts + const userSourceTokenAccount = getAssociatedTokenAddressSync( + inputMint, + payerPubkey, + true, + TOKEN_PROGRAM_ID + ); + const userDestinationTokenAccount = getAssociatedTokenAddressSync( + outputMint, + payerPubkey, + true, + TOKEN_PROGRAM_ID + ); + + // Handle WSOL wrapping + if (createInputMintAta && isWsol) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createAssociatedTokenAccountInstruction( + payerPubkey, + wsolAta, + payerPubkey, + NATIVE_MINT, + TOKEN_PROGRAM_ID + ) + ); + instructions.push(createSyncNativeInstruction(wsolAta)); + } + + // Create output mint ATA if needed + if (createOutputMintAta) { + instructions.push( + createAssociatedTokenAccountInstruction( + payerPubkey, + userDestinationTokenAccount, + payerPubkey, + outputMint, + TOKEN_PROGRAM_ID + ) + ); + } + + // Build instruction data (1 byte discriminator + 8 bytes amountIn + 8 bytes minAmountOut) + const data = Buffer.alloc(17); + SWAP_BASE_IN_DISCRIMINATOR.copy(data, 0); + data.writeBigUInt64LE(inputAmount, 1); + data.writeBigUInt64LE(minimumAmountOut, 9); + + // Build accounts (Raydium AMM V4 has a specific account order) + const accounts: AccountMeta[] = [ + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: amm, isSigner: false, isWritable: true }, + { pubkey: AUTHORITY, isSigner: false, isWritable: false }, + { pubkey: amm, isSigner: false, isWritable: true }, // Amm Open Orders (same as amm for simplicity) + { pubkey: tokenCoin, isSigner: false, isWritable: true }, // Pool Coin Token Account + { pubkey: tokenPc, isSigner: false, isWritable: true }, // Pool Pc Token Account + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Program (placeholder) + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Market (placeholder) + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Bids (placeholder) + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Asks (placeholder) + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Event Queue (placeholder) + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Coin Vault Account (placeholder) + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Pc Vault Account (placeholder) + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Vault Signer (placeholder) + { pubkey: userSourceTokenAccount, isSigner: false, isWritable: true }, + { pubkey: userDestinationTokenAccount, isSigner: false, isWritable: true }, + { pubkey: payerPubkey, isSigner: true, isWritable: true }, + ]; + + instructions.push( + new Instruction({ + keys: accounts, + programId: RAYDIUM_AMM_V4_PROGRAM_ID, + data, + }) + ); + + // Close WSOL ATA if requested + if (closeInputMintAta && isWsol) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createCloseAccountInstruction(wsolAta, payerPubkey, payerPubkey, [], TOKEN_PROGRAM_ID) + ); + } + + return instructions; +} + +/** + * Build sell instructions for Raydium AMM V4 protocol + */ +export function buildSellInstructions( + params: BuildSellInstructionsParams +): Instruction[] { + const { + payer, + inputMint, + inputAmount, + slippageBasisPoints = 1000n, + fixedOutputAmount, + createOutputMintAta = true, + closeOutputMintAta = false, + closeInputMintAta = false, + protocolParams, + } = params; + + if (inputAmount === 0n) { + throw new Error("Amount cannot be zero"); + } + + const payerPubkey = payer instanceof Keypair ? payer.publicKey : payer; + const instructions: Instruction[] = []; + + const { + amm, + coinMint, + pcMint, + tokenCoin, + tokenPc, + coinReserve, + pcReserve, + } = protocolParams; + + // Check pool type + const isWsol = coinMint.equals(WSOL_TOKEN_ACCOUNT) || pcMint.equals(WSOL_TOKEN_ACCOUNT); + const isUsdc = coinMint.equals(USDC_TOKEN_ACCOUNT) || pcMint.equals(USDC_TOKEN_ACCOUNT); + + if (!isWsol && !isUsdc) { + throw new Error("Pool must contain WSOL or USDC"); + } + + // Determine swap direction (selling token for WSOL/USDC means pc is output) + const isBaseIn = pcMint.equals(WSOL_TOKEN_ACCOUNT) || pcMint.equals(USDC_TOKEN_ACCOUNT); + + // Calculate output + const swapResult = computeSwapAmount( + coinReserve, + pcReserve, + isBaseIn, + inputAmount, + slippageBasisPoints + ); + const minimumAmountOut = fixedOutputAmount || swapResult.minAmountOut; + + // Determine output mint + const outputMint = isWsol ? WSOL_TOKEN_ACCOUNT : USDC_TOKEN_ACCOUNT; + + // Derive user token accounts + const userSourceTokenAccount = getAssociatedTokenAddressSync( + inputMint, + payerPubkey, + true, + TOKEN_PROGRAM_ID + ); + const userDestinationTokenAccount = getAssociatedTokenAddressSync( + outputMint, + payerPubkey, + true, + TOKEN_PROGRAM_ID + ); + + // Create WSOL ATA for receiving if needed + if (createOutputMintAta && isWsol) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createAssociatedTokenAccountInstruction( + payerPubkey, + wsolAta, + payerPubkey, + NATIVE_MINT, + TOKEN_PROGRAM_ID + ) + ); + } + + // Build instruction data + const data = Buffer.alloc(17); + SWAP_BASE_IN_DISCRIMINATOR.copy(data, 0); + data.writeBigUInt64LE(inputAmount, 1); + data.writeBigUInt64LE(minimumAmountOut, 9); + + // Build accounts + const accounts: AccountMeta[] = [ + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: amm, isSigner: false, isWritable: true }, + { pubkey: AUTHORITY, isSigner: false, isWritable: false }, + { pubkey: amm, isSigner: false, isWritable: true }, // Amm Open Orders + { pubkey: tokenCoin, isSigner: false, isWritable: true }, // Pool Coin Token Account + { pubkey: tokenPc, isSigner: false, isWritable: true }, // Pool Pc Token Account + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Program + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Market + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Bids + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Asks + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Event Queue + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Coin Vault Account + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Pc Vault Account + { pubkey: amm, isSigner: false, isWritable: false }, // Serum Vault Signer + { pubkey: userSourceTokenAccount, isSigner: false, isWritable: true }, + { pubkey: userDestinationTokenAccount, isSigner: false, isWritable: true }, + { pubkey: payerPubkey, isSigner: true, isWritable: true }, + ]; + + instructions.push( + new Instruction({ + keys: accounts, + programId: RAYDIUM_AMM_V4_PROGRAM_ID, + data, + }) + ); + + // Close WSOL ATA if requested + if (closeOutputMintAta && isWsol) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createCloseAccountInstruction(wsolAta, payerPubkey, payerPubkey, [], TOKEN_PROGRAM_ID) + ); + } + + // Close input token ATA if requested + if (closeInputMintAta) { + instructions.push( + createCloseAccountInstruction( + userSourceTokenAccount, + payerPubkey, + payerPubkey, + [], + TOKEN_PROGRAM_ID + ) + ); + } + + return instructions; +} diff --git a/sol-trade-sdk-nodejs/src/instruction/raydium_cpmm_builder.ts b/sol-trade-sdk-nodejs/src/instruction/raydium_cpmm_builder.ts new file mode 100644 index 0000000..6d511c8 --- /dev/null +++ b/sol-trade-sdk-nodejs/src/instruction/raydium_cpmm_builder.ts @@ -0,0 +1,556 @@ +/** + * Raydium CPMM (Concentrated Pool Market Maker) Protocol Instruction Builder + * + * Production-grade instruction builder for Raydium CPMM protocol. + * Supports swap operations with WSOL and USDC pools. + */ + +import { + PublicKey, + Keypair, + AccountMeta, + Instruction, + SystemProgram, +} from "@solana/web3.js"; +import { + getAssociatedTokenAddressSync, + createAssociatedTokenAccountInstruction, + TOKEN_PROGRAM_ID, + createCloseAccountInstruction, + NATIVE_MINT, + createSyncNativeInstruction, +} from "@solana/spl-token"; + +// ============================================ +// Program IDs and Constants +// ============================================ + +/** Raydium CPMM program ID */ +export const RAYDIUM_CPMM_PROGRAM_ID = new PublicKey( + "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C" +); + +/** Authority */ +export const AUTHORITY = new PublicKey( + "GpMZbSM2GgvTKHJirzeGfMFoaZ8UR2X7F4v8vHTvxFbL" +); + +/** WSOL Token Account (mint) */ +export const WSOL_TOKEN_ACCOUNT = new PublicKey( + "So11111111111111111111111111111111111111112" +); + +/** USDC Token Account (mint) */ +export const USDC_TOKEN_ACCOUNT = new PublicKey( + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" +); + +/** Fee rates */ +export const FEE_RATE_DENOMINATOR_VALUE = 1_000_000n; +export const TRADE_FEE_RATE = 2500n; +export const CREATOR_FEE_RATE = 0n; +export const PROTOCOL_FEE_RATE = 120000n; +export const FUND_FEE_RATE = 40000n; + +// ============================================ +// Discriminators +// ============================================ + +/** Swap base in instruction discriminator */ +export const SWAP_BASE_IN_DISCRIMINATOR: Buffer = Buffer.from([ + 143, 190, 90, 218, 196, 30, 51, 222, +]); + +/** Swap base out instruction discriminator */ +export const SWAP_BASE_OUT_DISCRIMINATOR: Buffer = Buffer.from([ + 55, 217, 98, 86, 163, 74, 180, 173, +]); + +// ============================================ +// Seeds +// ============================================ + +export const POOL_SEED = Buffer.from("pool"); +export const POOL_VAULT_SEED = Buffer.from("pool_vault"); +export const OBSERVATION_STATE_SEED = Buffer.from("observation"); + +// ============================================ +// PDA Derivation Functions +// ============================================ + +/** + * Derive the pool PDA for given config and mints + */ +export function getPoolPda( + ammConfig: PublicKey, + mint1: PublicKey, + mint2: PublicKey +): PublicKey { + const [pda] = PublicKey.findProgramAddressSync( + [POOL_SEED, ammConfig.toBuffer(), mint1.toBuffer(), mint2.toBuffer()], + RAYDIUM_CPMM_PROGRAM_ID + ); + return pda; +} + +/** + * Derive the vault PDA for a pool and mint + */ +export function getVaultPda(poolState: PublicKey, mint: PublicKey): PublicKey { + const [pda] = PublicKey.findProgramAddressSync( + [POOL_VAULT_SEED, poolState.toBuffer(), mint.toBuffer()], + RAYDIUM_CPMM_PROGRAM_ID + ); + return pda; +} + +/** + * Derive the observation state PDA for a pool + */ +export function getObservationStatePda(poolState: PublicKey): PublicKey { + const [pda] = PublicKey.findProgramAddressSync( + [OBSERVATION_STATE_SEED, poolState.toBuffer()], + RAYDIUM_CPMM_PROGRAM_ID + ); + return pda; +} + +// ============================================ +// Helper Functions +// ============================================ + +/** + * Compute swap amount for CPMM + */ +export function computeSwapAmount( + baseReserve: bigint, + quoteReserve: bigint, + isBaseIn: boolean, + amountIn: bigint, + slippageBasisPoints: bigint +): { amountOut: bigint; minAmountOut: bigint } { + // Apply trade fee (0.25%) + const feeRate = TRADE_FEE_RATE; + const feeDenominator = FEE_RATE_DENOMINATOR_VALUE; + const amountInAfterFee = amountIn - (amountIn * feeRate) / feeDenominator; + + // Calculate output using constant product formula + let amountOut: bigint; + if (isBaseIn) { + // Selling base for quote: output = (quoteReserve * amountIn) / (baseReserve + amountIn) + const denominator = baseReserve + amountInAfterFee; + amountOut = (quoteReserve * amountInAfterFee) / denominator; + } else { + // Selling quote for base: output = (baseReserve * amountIn) / (quoteReserve + amountIn) + const denominator = quoteReserve + amountInAfterFee; + amountOut = (baseReserve * amountInAfterFee) / denominator; + } + + // Apply slippage + const minAmountOut = amountOut - (amountOut * slippageBasisPoints) / 10000n; + + return { amountOut, minAmountOut }; +} + +/** + * Create instructions to wrap SOL into WSOL + */ +export function createWsolInstructions( + payer: PublicKey, + amount: bigint +): Instruction[] { + const instructions: Instruction[] = []; + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payer, true); + + // Sync native (wrap SOL) + instructions.push(createSyncNativeInstruction(wsolAta)); + + return instructions; +} + +/** + * Create instruction to close WSOL ATA and unwrap to SOL + */ +export function createCloseWsolInstruction(payer: PublicKey): Instruction { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payer, true); + return createCloseAccountInstruction(wsolAta, payer, payer, [], TOKEN_PROGRAM_ID); +} + +/** + * Create WSOL ATA instruction + */ +export function createWsolAtaInstruction(payer: PublicKey): Instruction { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payer, true); + return createAssociatedTokenAccountInstruction( + payer, + wsolAta, + payer, + NATIVE_MINT, + TOKEN_PROGRAM_ID + ); +} + +// ============================================ +// Types +// ============================================ + +export interface RaydiumCpmmParams { + poolState?: PublicKey; + ammConfig: PublicKey; + baseMint: PublicKey; + quoteMint: PublicKey; + baseTokenProgram: PublicKey; + quoteTokenProgram: PublicKey; + baseVault?: PublicKey; + quoteVault?: PublicKey; + baseReserve: bigint; + quoteReserve: bigint; + observationState?: PublicKey; +} + +export interface BuildBuyInstructionsParams { + payer: Keypair | PublicKey; + outputMint: PublicKey; + inputAmount: bigint; + slippageBasisPoints?: bigint; + fixedOutputAmount?: bigint; + createInputMintAta?: boolean; + createOutputMintAta?: boolean; + closeInputMintAta?: boolean; + protocolParams: RaydiumCpmmParams; +} + +export interface BuildSellInstructionsParams { + payer: Keypair | PublicKey; + inputMint: PublicKey; + inputAmount: bigint; + slippageBasisPoints?: bigint; + fixedOutputAmount?: bigint; + createOutputMintAta?: boolean; + closeOutputMintAta?: boolean; + closeInputMintAta?: boolean; + protocolParams: RaydiumCpmmParams; +} + +// ============================================ +// Instruction Builders +// ============================================ + +/** + * Build buy instructions for Raydium CPMM protocol + */ +export function buildBuyInstructions( + params: BuildBuyInstructionsParams +): Instruction[] { + const { + payer, + outputMint, + inputAmount, + slippageBasisPoints = 1000n, + fixedOutputAmount, + createInputMintAta = true, + createOutputMintAta = true, + closeInputMintAta = false, + protocolParams, + } = params; + + if (inputAmount === 0n) { + throw new Error("Amount cannot be zero"); + } + + const payerPubkey = payer instanceof Keypair ? payer.publicKey : payer; + const instructions: Instruction[] = []; + + const { + ammConfig, + baseMint, + quoteMint, + baseTokenProgram, + quoteTokenProgram, + baseVault, + quoteVault, + baseReserve, + quoteReserve, + observationState, + } = protocolParams; + + // Check pool type + const isWsol = baseMint.equals(WSOL_TOKEN_ACCOUNT) || quoteMint.equals(WSOL_TOKEN_ACCOUNT); + const isUsdc = baseMint.equals(USDC_TOKEN_ACCOUNT) || quoteMint.equals(USDC_TOKEN_ACCOUNT); + + if (!isWsol && !isUsdc) { + throw new Error("Pool must contain WSOL or USDC"); + } + + // Determine swap direction + const isBaseIn = baseMint.equals(WSOL_TOKEN_ACCOUNT) || baseMint.equals(USDC_TOKEN_ACCOUNT); + const mintTokenProgram = isBaseIn ? quoteTokenProgram : baseTokenProgram; + + // Derive pool state + const poolState = protocolParams.poolState && !protocolParams.poolState.equals(PublicKey.default) + ? protocolParams.poolState + : getPoolPda(ammConfig, baseMint, quoteMint); + + // Calculate output + const swapResult = computeSwapAmount( + baseReserve, + quoteReserve, + isBaseIn, + inputAmount, + slippageBasisPoints + ); + const minimumAmountOut = fixedOutputAmount || swapResult.minAmountOut; + + // Determine input/output mints + const inputMint = isWsol ? WSOL_TOKEN_ACCOUNT : USDC_TOKEN_ACCOUNT; + + // Derive user token accounts + const inputTokenAccount = getAssociatedTokenAddressSync( + inputMint, + payerPubkey, + true, + TOKEN_PROGRAM_ID + ); + const outputTokenAccount = getAssociatedTokenAddressSync( + outputMint, + payerPubkey, + true, + mintTokenProgram + ); + + // Derive vault accounts + const inputVaultAccount = getVaultPda(poolState, inputMint); + const outputVaultAccount = getVaultPda(poolState, outputMint); + + // Derive observation state + const observationStateAccount = observationState && !observationState.equals(PublicKey.default) + ? observationState + : getObservationStatePda(poolState); + + // Handle WSOL wrapping + if (createInputMintAta && isWsol) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createAssociatedTokenAccountInstruction( + payerPubkey, + wsolAta, + payerPubkey, + NATIVE_MINT, + TOKEN_PROGRAM_ID + ) + ); + instructions.push(createSyncNativeInstruction(wsolAta)); + } + + // Create output mint ATA if needed + if (createOutputMintAta) { + instructions.push( + createAssociatedTokenAccountInstruction( + payerPubkey, + outputTokenAccount, + payerPubkey, + outputMint, + mintTokenProgram + ) + ); + } + + // Build instruction data + const data = Buffer.alloc(24); + SWAP_BASE_IN_DISCRIMINATOR.copy(data, 0); + data.writeBigUInt64LE(inputAmount, 8); + data.writeBigUInt64LE(minimumAmountOut, 16); + + // Build accounts + const accounts: AccountMeta[] = [ + { pubkey: payerPubkey, isSigner: true, isWritable: true }, + { pubkey: AUTHORITY, isSigner: false, isWritable: false }, + { pubkey: ammConfig, isSigner: false, isWritable: false }, + { pubkey: poolState, isSigner: false, isWritable: true }, + { pubkey: inputTokenAccount, isSigner: false, isWritable: true }, + { pubkey: outputTokenAccount, isSigner: false, isWritable: true }, + { pubkey: inputVaultAccount, isSigner: false, isWritable: true }, + { pubkey: outputVaultAccount, isSigner: false, isWritable: true }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: mintTokenProgram, isSigner: false, isWritable: false }, + { pubkey: inputMint, isSigner: false, isWritable: false }, + { pubkey: outputMint, isSigner: false, isWritable: false }, + { pubkey: observationStateAccount, isSigner: false, isWritable: true }, + ]; + + instructions.push( + new Instruction({ + keys: accounts, + programId: RAYDIUM_CPMM_PROGRAM_ID, + data, + }) + ); + + // Close WSOL ATA if requested + if (closeInputMintAta && isWsol) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createCloseAccountInstruction(wsolAta, payerPubkey, payerPubkey, [], TOKEN_PROGRAM_ID) + ); + } + + return instructions; +} + +/** + * Build sell instructions for Raydium CPMM protocol + */ +export function buildSellInstructions( + params: BuildSellInstructionsParams +): Instruction[] { + const { + payer, + inputMint, + inputAmount, + slippageBasisPoints = 1000n, + fixedOutputAmount, + createOutputMintAta = true, + closeOutputMintAta = false, + closeInputMintAta = false, + protocolParams, + } = params; + + if (inputAmount === 0n) { + throw new Error("Amount cannot be zero"); + } + + const payerPubkey = payer instanceof Keypair ? payer.publicKey : payer; + const instructions: Instruction[] = []; + + const { + ammConfig, + baseMint, + quoteMint, + baseTokenProgram, + quoteTokenProgram, + baseReserve, + quoteReserve, + observationState, + } = protocolParams; + + // Check pool type + const isWsol = baseMint.equals(WSOL_TOKEN_ACCOUNT) || quoteMint.equals(WSOL_TOKEN_ACCOUNT); + const isUsdc = baseMint.equals(USDC_TOKEN_ACCOUNT) || quoteMint.equals(USDC_TOKEN_ACCOUNT); + + if (!isWsol && !isUsdc) { + throw new Error("Pool must contain WSOL or USDC"); + } + + // Determine swap direction + const isQuoteOut = quoteMint.equals(WSOL_TOKEN_ACCOUNT) || quoteMint.equals(USDC_TOKEN_ACCOUNT); + const mintTokenProgram = isQuoteOut ? baseTokenProgram : quoteTokenProgram; + + // Derive pool state + const poolState = protocolParams.poolState && !protocolParams.poolState.equals(PublicKey.default) + ? protocolParams.poolState + : getPoolPda(ammConfig, baseMint, quoteMint); + + // Calculate output + const swapResult = computeSwapAmount( + baseReserve, + quoteReserve, + isQuoteOut, + inputAmount, + slippageBasisPoints + ); + const minimumAmountOut = fixedOutputAmount || swapResult.minAmountOut; + + // Determine output mint + const outputMint = isWsol ? WSOL_TOKEN_ACCOUNT : USDC_TOKEN_ACCOUNT; + + // Derive user token accounts + const inputTokenAccount = getAssociatedTokenAddressSync( + inputMint, + payerPubkey, + true, + mintTokenProgram + ); + const outputTokenAccount = getAssociatedTokenAddressSync( + outputMint, + payerPubkey, + true, + TOKEN_PROGRAM_ID + ); + + // Derive vault accounts + const inputVaultAccount = getVaultPda(poolState, inputMint); + const outputVaultAccount = getVaultPda(poolState, outputMint); + + // Derive observation state + const observationStateAccount = observationState && !observationState.equals(PublicKey.default) + ? observationState + : getObservationStatePda(poolState); + + // Create WSOL ATA for receiving if needed + if (createOutputMintAta && isWsol) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createAssociatedTokenAccountInstruction( + payerPubkey, + wsolAta, + payerPubkey, + NATIVE_MINT, + TOKEN_PROGRAM_ID + ) + ); + } + + // Build instruction data + const data = Buffer.alloc(24); + SWAP_BASE_IN_DISCRIMINATOR.copy(data, 0); + data.writeBigUInt64LE(inputAmount, 8); + data.writeBigUInt64LE(minimumAmountOut, 16); + + // Build accounts + const accounts: AccountMeta[] = [ + { pubkey: payerPubkey, isSigner: true, isWritable: true }, + { pubkey: AUTHORITY, isSigner: false, isWritable: false }, + { pubkey: ammConfig, isSigner: false, isWritable: false }, + { pubkey: poolState, isSigner: false, isWritable: true }, + { pubkey: inputTokenAccount, isSigner: false, isWritable: true }, + { pubkey: outputTokenAccount, isSigner: false, isWritable: true }, + { pubkey: inputVaultAccount, isSigner: false, isWritable: true }, + { pubkey: outputVaultAccount, isSigner: false, isWritable: true }, + { pubkey: mintTokenProgram, isSigner: false, isWritable: false }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: inputMint, isSigner: false, isWritable: false }, + { pubkey: outputMint, isSigner: false, isWritable: false }, + { pubkey: observationStateAccount, isSigner: false, isWritable: true }, + ]; + + instructions.push( + new Instruction({ + keys: accounts, + programId: RAYDIUM_CPMM_PROGRAM_ID, + data, + }) + ); + + // Close WSOL ATA if requested + if (closeOutputMintAta && isWsol) { + const wsolAta = getAssociatedTokenAddressSync(NATIVE_MINT, payerPubkey, true); + instructions.push( + createCloseAccountInstruction(wsolAta, payerPubkey, payerPubkey, [], TOKEN_PROGRAM_ID) + ); + } + + // Close input token ATA if requested + if (closeInputMintAta) { + instructions.push( + createCloseAccountInstruction( + inputTokenAccount, + payerPubkey, + payerPubkey, + [], + mintTokenProgram + ) + ); + } + + return instructions; +} diff --git a/sol-trade-sdk-nodejs/tsconfig.json b/sol-trade-sdk-nodejs/tsconfig.json new file mode 100644 index 0000000..04044ae --- /dev/null +++ b/sol-trade-sdk-nodejs/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "outDir": "./dist", + "rootDir": "./src", + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/sol-trade-sdk-python/README.md b/sol-trade-sdk-python/README.md new file mode 100644 index 0000000..3ffe7b4 --- /dev/null +++ b/sol-trade-sdk-python/README.md @@ -0,0 +1,80 @@ +# Solana Trading SDK - Python + +Production-grade instruction builders for various Solana DEX protocols. + +## Supported Protocols + +- **PumpFun** - Bonding curve trading +- **PumpSwap** - Pump AMM trading +- **Bonk** - Bonk DEX trading +- **Raydium CPMM** - Raydium Concentrated Product Market Maker +- **Raydium AMM V4** - Raydium Automated Market Maker V4 +- **Meteora DAMM V2** - Meteora Dynamic AMM V2 + +## Installation + +```bash +pip install sol-trade-sdk +``` + +## Quick Start + +```python +from solders.pubkey import Pubkey +from sol_trade_sdk.instruction import ( + PumpFunParams, + build_pumpfun_buy_instructions, + get_bonding_curve_pda, +) + +# Set up parameters +payer = Pubkey.from_string("YOUR_WALLET_ADDRESS") +output_mint = Pubkey.from_string("TOKEN_MINT_ADDRESS") + +# Build parameters +params = PumpFunParams( + virtual_token_reserves=1000000000000, + virtual_sol_reserves=30000000000, + real_token_reserves=793000000000000, + creator_vault=Pubkey.from_string("CREATOR_VAULT_ADDRESS"), + token_program=Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), +) + +# Build buy instructions +instructions = build_pumpfun_buy_instructions( + payer=payer, + output_mint=output_mint, + input_amount=1000000000, # 1 SOL + params=params, + slippage_bps=500, # 5% +) +``` + +## Features + +- All program IDs and constants from Rust implementations +- Correct discriminators from Rust (not placeholder values) +- PDA derivation functions using solders.pubkey +- `build_buy_instructions` function for each protocol +- `build_sell_instructions` function for each protocol +- WSOL handling (wrap/close) +- ATA creation support +- Slippage calculation + +## Development + +```bash +# Install dev dependencies +pip install -e ".[dev]" + +# Run tests +pytest + +# Format code +black src/ +isort src/ +``` + +## License + +MIT License diff --git a/sol-trade-sdk-python/pyproject.toml b/sol-trade-sdk-python/pyproject.toml new file mode 100644 index 0000000..7c23aed --- /dev/null +++ b/sol-trade-sdk-python/pyproject.toml @@ -0,0 +1,53 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "sol-trade-sdk" +version = "1.0.0" +description = "Production-grade instruction builders for Solana DEX protocols" +readme = "README.md" +requires-python = ">=3.8" +license = "MIT" +authors = [ + { name = "Sol Trade SDK Team" } +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "solders>=0.21.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", + "black>=23.0.0", + "isort>=5.0.0", + "mypy>=1.0.0", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/sol_trade_sdk"] + +[tool.black] +line-length = 100 +target-version = ["py38", "py39", "py310", "py311", "py312"] + +[tool.isort] +profile = "black" +line_length = 100 + +[tool.mypy] +python_version = "3.8" +warn_return_any = true +warn_unused_configs = true diff --git a/sol-trade-sdk-python/src/__init__.py b/sol-trade-sdk-python/src/__init__.py new file mode 100644 index 0000000..ffd0ee4 --- /dev/null +++ b/sol-trade-sdk-python/src/__init__.py @@ -0,0 +1,9 @@ +""" +Solana Trading SDK Python + +Production-grade instruction builders for various Solana DEX protocols. +""" + +from .instruction import * + +__version__ = "1.0.0" diff --git a/sol-trade-sdk-python/src/instruction/__init__.py b/sol-trade-sdk-python/src/instruction/__init__.py new file mode 100644 index 0000000..141a43a --- /dev/null +++ b/sol-trade-sdk-python/src/instruction/__init__.py @@ -0,0 +1,366 @@ +""" +Solana Trading SDK - Instruction Builders + +Production-grade instruction builders for various Solana DEX protocols: +- PumpFun +- PumpSwap +- Bonk +- Raydium CPMM +- Raydium AMM V4 +- Meteora DAMM V2 + +Each module provides: +- Program IDs and constants +- Instruction discriminators +- PDA derivation functions +- build_buy_instructions function +- build_sell_instructions function +- Protocol-specific parameter dataclasses +""" + +from .common import ( + # Program IDs + SYSTEM_PROGRAM, + TOKEN_PROGRAM, + TOKEN_PROGRAM_2022, + ASSOCIATED_TOKEN_PROGRAM, + # Token Mints + SOL_TOKEN_ACCOUNT, + WSOL_TOKEN_ACCOUNT, + USDC_TOKEN_ACCOUNT, + USD1_TOKEN_ACCOUNT, + # Constants + DEFAULT_SLIPPAGE, + # Utility Functions + get_associated_token_address, + create_associated_token_account_idempotent_instruction, + create_wsol_account_instruction, + close_wsol_account_instruction, + close_token_account_instruction, + handle_wsol, + create_wsol_ata, + close_wsol, + # Calculation Functions + calculate_with_slippage_buy, + calculate_with_slippage_sell, +) + +from .pumpfun_builder import ( + # Program IDs and Constants + PUMPFUN_PROGRAM_ID, + FEE_RECIPIENT as PUMPFUN_FEE_RECIPIENT, + GLOBAL_ACCOUNT as PUMPFUN_GLOBAL_ACCOUNT, + EVENT_AUTHORITY as PUMPFUN_EVENT_AUTHORITY, + AUTHORITY as PUMPFUN_AUTHORITY, + FEE_PROGRAM as PUMPFUN_FEE_PROGRAM, + GLOBAL_VOLUME_ACCUMULATOR as PUMPFUN_GLOBAL_VOLUME_ACCUMULATOR, + FEE_CONFIG as PUMPFUN_FEE_CONFIG, + MAYHEM_FEE_RECIPIENTS as PUMPFUN_MAYHEM_FEE_RECIPIENTS, + # Discriminators + BUY_DISCRIMINATOR as PUMPFUN_BUY_DISCRIMINATOR, + BUY_EXACT_SOL_IN_DISCRIMINATOR as PUMPFUN_BUY_EXACT_SOL_IN_DISCRIMINATOR, + SELL_DISCRIMINATOR as PUMPFUN_SELL_DISCRIMINATOR, + CLAIM_CASHBACK_DISCRIMINATOR as PUMPFUN_CLAIM_CASHBACK_DISCRIMINATOR, + # PDA Functions + get_bonding_curve_pda, + get_bonding_curve_v2_pda, + get_creator_vault_pda, + get_user_volume_accumulator_pda as get_pumpfun_user_volume_accumulator_pda, + get_creator, + get_mayhem_fee_recipient_random, + # Params + PumpFunParams, + # Calculation Functions + get_buy_token_amount_from_sol_amount, + get_sell_sol_amount_from_token_amount, + # Instruction Builders + build_buy_instructions as build_pumpfun_buy_instructions, + build_sell_instructions as build_pumpfun_sell_instructions, + claim_cashback_pumpfun_instruction, +) + +from .pumpswap_builder import ( + # Program IDs and Constants + PUMPSWAP_PROGRAM_ID, + FEE_RECIPIENT as PUMPSWAP_FEE_RECIPIENT, + GLOBAL_ACCOUNT as PUMPSWAP_GLOBAL_ACCOUNT, + EVENT_AUTHORITY as PUMPSWAP_EVENT_AUTHORITY, + ASSOCIATED_TOKEN_PROGRAM as PUMPSWAP_ASSOCIATED_TOKEN_PROGRAM, + PUMP_PROGRAM_ID, + FEE_PROGRAM as PUMPSWAP_FEE_PROGRAM, + GLOBAL_VOLUME_ACCUMULATOR as PUMPSWAP_GLOBAL_VOLUME_ACCUMULATOR, + FEE_CONFIG as PUMPSWAP_FEE_CONFIG, + DEFAULT_COIN_CREATOR_VAULT_AUTHORITY, + MAYHEM_FEE_RECIPIENTS as PUMPSWAP_MAYHEM_FEE_RECIPIENTS, + LP_FEE_BASIS_POINTS, + PROTOCOL_FEE_BASIS_POINTS, + COIN_CREATOR_FEE_BASIS_POINTS, + # Discriminators + BUY_DISCRIMINATOR as PUMPSWAP_BUY_DISCRIMINATOR, + BUY_EXACT_QUOTE_IN_DISCRIMINATOR, + SELL_DISCRIMINATOR as PUMPSWAP_SELL_DISCRIMINATOR, + CLAIM_CASHBACK_DISCRIMINATOR as PUMPSWAP_CLAIM_CASHBACK_DISCRIMINATOR, + # PDA Functions + get_pool_v2_pda, + get_pump_pool_authority_pda, + get_canonical_pool_pda, + get_user_volume_accumulator_pda as get_pumpswap_user_volume_accumulator_pda, + get_user_volume_accumulator_wsol_ata, + get_user_volume_accumulator_quote_ata, + coin_creator_vault_authority, + coin_creator_vault_ata, + fee_recipient_ata, + get_mayhem_fee_recipient_random as get_pumpswap_mayhem_fee_recipient_random, + # Params + PumpSwapParams, + # Calculation Functions + buy_quote_input_internal, + sell_base_input_internal, + # Instruction Builders + build_buy_instructions as build_pumpswap_buy_instructions, + build_sell_instructions as build_pumpswap_sell_instructions, + claim_cashback_pumpswap_instruction, +) + +from .bonk_builder import ( + # Program IDs and Constants + BONK_PROGRAM_ID, + AUTHORITY as BONK_AUTHORITY, + GLOBAL_CONFIG, + USD1_GLOBAL_CONFIG, + EVENT_AUTHORITY as BONK_EVENT_AUTHORITY, + PLATFORM_FEE_RATE, + PROTOCOL_FEE_RATE as BONK_PROTOCOL_FEE_RATE, + SHARE_FEE_RATE, + # Discriminators + BUY_EXACT_IN_DISCRIMINATOR, + SELL_EXACT_IN_DISCRIMINATOR, + # PDA Functions + get_pool_pda as get_bonk_pool_pda, + get_vault_pda as get_bonk_vault_pda, + get_platform_associated_account, + get_creator_associated_account, + # Params + BonkParams, + # Calculation Functions + get_amount_in_net, + get_amount_out as get_bonk_amount_out, + get_buy_token_amount_from_sol_amount as get_bonk_buy_token_amount_from_sol_amount, + get_sell_sol_amount_from_token_amount as get_bonk_sell_sol_amount_from_token_amount, + # Instruction Builders + build_buy_instructions as build_bonk_buy_instructions, + build_sell_instructions as build_bonk_sell_instructions, +) + +from .raydium_cpmm_builder import ( + # Program IDs and Constants + RAYDIUM_CPMM_PROGRAM_ID, + AUTHORITY as RAYDIUM_CPMM_AUTHORITY, + FEE_RATE_DENOMINATOR_VALUE, + TRADE_FEE_RATE, + CREATOR_FEE_RATE, + PROTOCOL_FEE_RATE as RAYDIUM_CPMM_PROTOCOL_FEE_RATE, + FUND_FEE_RATE, + # Discriminators + SWAP_BASE_IN_DISCRIMINATOR as RAYDIUM_CPMM_SWAP_BASE_IN_DISCRIMINATOR, + SWAP_BASE_OUT_DISCRIMINATOR as RAYDIUM_CPMM_SWAP_BASE_OUT_DISCRIMINATOR, + # PDA Functions + get_pool_pda as get_raydium_cpmm_pool_pda, + get_vault_pda as get_raydium_cpmm_vault_pda, + get_observation_state_pda, + # Params + RaydiumCpmmParams, + # Calculation Functions + compute_swap_amount as compute_raydium_cpmm_swap_amount, + # Instruction Builders + build_buy_instructions as build_raydium_cpmm_buy_instructions, + build_sell_instructions as build_raydium_cpmm_sell_instructions, +) + +from .raydium_amm_v4_builder import ( + # Program IDs and Constants + RAYDIUM_AMM_V4_PROGRAM_ID, + AUTHORITY as RAYDIUM_AMM_V4_AUTHORITY, + TRADE_FEE_NUMERATOR, + TRADE_FEE_DENOMINATOR, + SWAP_FEE_NUMERATOR, + SWAP_FEE_DENOMINATOR, + # Discriminators + SWAP_BASE_IN_DISCRIMINATOR as RAYDIUM_AMM_V4_SWAP_BASE_IN_DISCRIMINATOR, + SWAP_BASE_OUT_DISCRIMINATOR as RAYDIUM_AMM_V4_SWAP_BASE_OUT_DISCRIMINATOR, + # Params + RaydiumAmmV4Params, + # Calculation Functions + compute_swap_amount as compute_raydium_amm_v4_swap_amount, + # Instruction Builders + build_buy_instructions as build_raydium_amm_v4_buy_instructions, + build_sell_instructions as build_raydium_amm_v4_sell_instructions, +) + +from .meteora_damm_v2_builder import ( + # Program IDs and Constants + METEORA_DAMM_V2_PROGRAM_ID, + AUTHORITY as METEORA_DAMM_V2_AUTHORITY, + # Discriminators + SWAP_DISCRIMINATOR as METEORA_DAMM_V2_SWAP_DISCRIMINATOR, + # PDA Functions + get_event_authority_pda, + # Params + MeteoraDammV2Params, + # Instruction Builders + build_buy_instructions as build_meteora_damm_v2_buy_instructions, + build_sell_instructions as build_meteora_damm_v2_sell_instructions, +) + +# Version +__version__ = "1.0.0" + +__all__ = [ + # Common + "SYSTEM_PROGRAM", + "TOKEN_PROGRAM", + "TOKEN_PROGRAM_2022", + "ASSOCIATED_TOKEN_PROGRAM", + "SOL_TOKEN_ACCOUNT", + "WSOL_TOKEN_ACCOUNT", + "USDC_TOKEN_ACCOUNT", + "USD1_TOKEN_ACCOUNT", + "DEFAULT_SLIPPAGE", + "get_associated_token_address", + "create_associated_token_account_idempotent_instruction", + "create_wsol_account_instruction", + "close_wsol_account_instruction", + "close_token_account_instruction", + "handle_wsol", + "create_wsol_ata", + "close_wsol", + "calculate_with_slippage_buy", + "calculate_with_slippage_sell", + + # PumpFun + "PUMPFUN_PROGRAM_ID", + "PUMPFUN_FEE_RECIPIENT", + "PUMPFUN_GLOBAL_ACCOUNT", + "PUMPFUN_EVENT_AUTHORITY", + "PUMPFUN_AUTHORITY", + "PUMPFUN_FEE_PROGRAM", + "PUMPFUN_GLOBAL_VOLUME_ACCUMULATOR", + "PUMPFUN_FEE_CONFIG", + "PUMPFUN_MAYHEM_FEE_RECIPIENTS", + "PUMPFUN_BUY_DISCRIMINATOR", + "PUMPFUN_BUY_EXACT_SOL_IN_DISCRIMINATOR", + "PUMPFUN_SELL_DISCRIMINATOR", + "PUMPFUN_CLAIM_CASHBACK_DISCRIMINATOR", + "get_bonding_curve_pda", + "get_bonding_curve_v2_pda", + "get_creator_vault_pda", + "get_pumpfun_user_volume_accumulator_pda", + "get_creator", + "get_mayhem_fee_recipient_random", + "PumpFunParams", + "get_buy_token_amount_from_sol_amount", + "get_sell_sol_amount_from_token_amount", + "build_pumpfun_buy_instructions", + "build_pumpfun_sell_instructions", + "claim_cashback_pumpfun_instruction", + + # PumpSwap + "PUMPSWAP_PROGRAM_ID", + "PUMPSWAP_FEE_RECIPIENT", + "PUMPSWAP_GLOBAL_ACCOUNT", + "PUMPSWAP_EVENT_AUTHORITY", + "PUMPSWAP_ASSOCIATED_TOKEN_PROGRAM", + "PUMP_PROGRAM_ID", + "PUMPSWAP_FEE_PROGRAM", + "PUMPSWAP_GLOBAL_VOLUME_ACCUMULATOR", + "PUMPSWAP_FEE_CONFIG", + "DEFAULT_COIN_CREATOR_VAULT_AUTHORITY", + "PUMPSWAP_MAYHEM_FEE_RECIPIENTS", + "LP_FEE_BASIS_POINTS", + "PROTOCOL_FEE_BASIS_POINTS", + "COIN_CREATOR_FEE_BASIS_POINTS", + "PUMPSWAP_BUY_DISCRIMINATOR", + "BUY_EXACT_QUOTE_IN_DISCRIMINATOR", + "PUMPSWAP_SELL_DISCRIMINATOR", + "PUMPSWAP_CLAIM_CASHBACK_DISCRIMINATOR", + "get_pool_v2_pda", + "get_pump_pool_authority_pda", + "get_canonical_pool_pda", + "get_pumpswap_user_volume_accumulator_pda", + "get_user_volume_accumulator_wsol_ata", + "get_user_volume_accumulator_quote_ata", + "coin_creator_vault_authority", + "coin_creator_vault_ata", + "fee_recipient_ata", + "get_pumpswap_mayhem_fee_recipient_random", + "PumpSwapParams", + "buy_quote_input_internal", + "sell_base_input_internal", + "build_pumpswap_buy_instructions", + "build_pumpswap_sell_instructions", + "claim_cashback_pumpswap_instruction", + + # Bonk + "BONK_PROGRAM_ID", + "BONK_AUTHORITY", + "GLOBAL_CONFIG", + "USD1_GLOBAL_CONFIG", + "BONK_EVENT_AUTHORITY", + "PLATFORM_FEE_RATE", + "BONK_PROTOCOL_FEE_RATE", + "SHARE_FEE_RATE", + "BUY_EXACT_IN_DISCRIMINATOR", + "SELL_EXACT_IN_DISCRIMINATOR", + "get_bonk_pool_pda", + "get_bonk_vault_pda", + "get_platform_associated_account", + "get_creator_associated_account", + "BonkParams", + "get_amount_in_net", + "get_bonk_amount_out", + "get_bonk_buy_token_amount_from_sol_amount", + "get_bonk_sell_sol_amount_from_token_amount", + "build_bonk_buy_instructions", + "build_bonk_sell_instructions", + + # Raydium CPMM + "RAYDIUM_CPMM_PROGRAM_ID", + "RAYDIUM_CPMM_AUTHORITY", + "FEE_RATE_DENOMINATOR_VALUE", + "TRADE_FEE_RATE", + "CREATOR_FEE_RATE", + "RAYDIUM_CPMM_PROTOCOL_FEE_RATE", + "FUND_FEE_RATE", + "RAYDIUM_CPMM_SWAP_BASE_IN_DISCRIMINATOR", + "RAYDIUM_CPMM_SWAP_BASE_OUT_DISCRIMINATOR", + "get_raydium_cpmm_pool_pda", + "get_raydium_cpmm_vault_pda", + "get_observation_state_pda", + "RaydiumCpmmParams", + "compute_raydium_cpmm_swap_amount", + "build_raydium_cpmm_buy_instructions", + "build_raydium_cpmm_sell_instructions", + + # Raydium AMM V4 + "RAYDIUM_AMM_V4_PROGRAM_ID", + "RAYDIUM_AMM_V4_AUTHORITY", + "TRADE_FEE_NUMERATOR", + "TRADE_FEE_DENOMINATOR", + "SWAP_FEE_NUMERATOR", + "SWAP_FEE_DENOMINATOR", + "RAYDIUM_AMM_V4_SWAP_BASE_IN_DISCRIMINATOR", + "RAYDIUM_AMM_V4_SWAP_BASE_OUT_DISCRIMINATOR", + "RaydiumAmmV4Params", + "compute_raydium_amm_v4_swap_amount", + "build_raydium_amm_v4_buy_instructions", + "build_raydium_amm_v4_sell_instructions", + + # Meteora DAMM V2 + "METEORA_DAMM_V2_PROGRAM_ID", + "METEORA_DAMM_V2_AUTHORITY", + "METEORA_DAMM_V2_SWAP_DISCRIMINATOR", + "get_event_authority_pda", + "MeteoraDammV2Params", + "build_meteora_damm_v2_buy_instructions", + "build_meteora_damm_v2_sell_instructions", +] diff --git a/sol-trade-sdk-python/src/instruction/bonk_builder.py b/sol-trade-sdk-python/src/instruction/bonk_builder.py new file mode 100644 index 0000000..8444384 --- /dev/null +++ b/sol-trade-sdk-python/src/instruction/bonk_builder.py @@ -0,0 +1,533 @@ +""" +Bonk instruction builder for Solana trading SDK. +Production-grade implementation with all constants, discriminators, and PDA derivation functions. +""" + +from typing import List, Optional, Tuple +from dataclasses import dataclass +from solders.pubkey import Pubkey +from solders.instruction import Instruction, AccountMeta +import struct + +from .common import ( + SYSTEM_PROGRAM, + TOKEN_PROGRAM, + WSOL_TOKEN_ACCOUNT, + USD1_TOKEN_ACCOUNT, + USDC_TOKEN_ACCOUNT, + DEFAULT_SLIPPAGE, + get_associated_token_address, + create_associated_token_account_idempotent_instruction, + handle_wsol, + close_wsol, + close_token_account_instruction, + calculate_with_slippage_sell, +) + +# ============================================ +# Bonk Program ID +# ============================================ + +BONK_PROGRAM_ID: Pubkey = Pubkey.from_string("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj") + +# ============================================ +# Bonk Constants +# ============================================ + +# Authority +AUTHORITY: Pubkey = Pubkey.from_string("WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh") + +# Global Config (SOL pools) +GLOBAL_CONFIG: Pubkey = Pubkey.from_string("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX") + +# USD1 Global Config +USD1_GLOBAL_CONFIG: Pubkey = Pubkey.from_string("EPiZbnrThjyLnoQ6QQzkxeFqyL5uyg9RzNHHAudUPxBz") + +# Event Authority +EVENT_AUTHORITY: Pubkey = Pubkey.from_string("2DPAtwB8L12vrMRExbLuyGnC7n2J5LNoZQSejeQGpwkr") + +# Fee Rates +PLATFORM_FEE_RATE: int = 100 # 1% +PROTOCOL_FEE_RATE: int = 25 # 0.25% +SHARE_FEE_RATE: int = 0 # 0% + +# ============================================ +# Instruction Discriminators +# ============================================ + +BUY_EXACT_IN_DISCRIMINATOR: bytes = bytes([250, 234, 13, 123, 213, 156, 19, 236]) +SELL_EXACT_IN_DISCRIMINATOR: bytes = bytes([149, 39, 222, 155, 211, 124, 152, 26]) + +# ============================================ +# Seeds +# ============================================ + +POOL_SEED = b"pool" +POOL_VAULT_SEED = b"pool_vault" + +# ============================================ +# PDA Derivation Functions +# ============================================ + +def get_pool_pda(base_mint: Pubkey, quote_mint: Pubkey) -> Pubkey: + """ + Derive the pool PDA for a given base and quote mint. + Seeds: ["pool", base_mint, quote_mint] + """ + seeds = [POOL_SEED, bytes(base_mint), bytes(quote_mint)] + (pda, _) = Pubkey.find_program_address(seeds, BONK_PROGRAM_ID) + return pda + + +def get_vault_pda(pool_state: Pubkey, mint: Pubkey) -> Pubkey: + """ + Derive the vault PDA for a given pool and mint. + Seeds: ["pool_vault", pool_state, mint] + """ + seeds = [POOL_VAULT_SEED, bytes(pool_state), bytes(mint)] + (pda, _) = Pubkey.find_program_address(seeds, BONK_PROGRAM_ID) + return pda + + +def get_platform_associated_account(platform_config: Pubkey) -> Pubkey: + """ + Derive the platform associated account PDA. + Seeds: [platform_config, WSOL_TOKEN_ACCOUNT] + """ + seeds = [bytes(platform_config), bytes(WSOL_TOKEN_ACCOUNT)] + (pda, _) = Pubkey.find_program_address(seeds, BONK_PROGRAM_ID) + return pda + + +def get_creator_associated_account(creator: Pubkey) -> Pubkey: + """ + Derive the creator associated account PDA. + Seeds: [creator, WSOL_TOKEN_ACCOUNT] + """ + seeds = [bytes(creator), bytes(WSOL_TOKEN_ACCOUNT)] + (pda, _) = Pubkey.find_program_address(seeds, BONK_PROGRAM_ID) + return pda + + +# ============================================ +# Bonk Parameters Dataclass +# ============================================ + +@dataclass +class BonkParams: + """Parameters for Bonk protocol trading.""" + virtual_base: int = 0 + virtual_quote: int = 0 + real_base: int = 0 + real_quote: int = 0 + pool_state: Optional[Pubkey] = None # If None, will derive from mints + base_vault: Optional[Pubkey] = None + quote_vault: Optional[Pubkey] = None + mint_token_program: Pubkey = TOKEN_PROGRAM + platform_config: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + platform_associated_account: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + creator_associated_account: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + global_config: Pubkey = GLOBAL_CONFIG # Use USD1_GLOBAL_CONFIG for USD1 pools + + @property + def is_usd1_pool(self) -> bool: + """Check if this is a USD1 pool.""" + return self.global_config == USD1_GLOBAL_CONFIG + + +# ============================================ +# Bonk Calculation Functions +# ============================================ + +def get_amount_in_net( + amount_in: int, + protocol_fee_rate: int, + platform_fee_rate: int, + share_fee_rate: int, +) -> int: + """Calculate net input amount after fees.""" + protocol_fee = (amount_in * protocol_fee_rate) // 10000 + platform_fee = (amount_in * platform_fee_rate) // 10000 + share_fee = (amount_in * share_fee_rate) // 10000 + return amount_in - protocol_fee - platform_fee - share_fee + + +def get_amount_out( + amount_in: int, + protocol_fee_rate: int, + platform_fee_rate: int, + share_fee_rate: int, + virtual_base: int, + virtual_quote: int, + real_base: int, + real_quote: int, + slippage_bps: int, +) -> int: + """ + Calculate output amount for a given input amount on Bonk. + """ + amount_in_net = get_amount_in_net( + amount_in, protocol_fee_rate, platform_fee_rate, share_fee_rate + ) + + input_reserve = virtual_quote + real_quote + output_reserve = virtual_base - real_base + + numerator = amount_in_net * output_reserve + denominator = input_reserve + amount_in_net + amount_out = numerator // denominator + + # Apply slippage + amount_out = amount_out - (amount_out * slippage_bps) // 10000 + return amount_out + + +def get_buy_token_amount_from_sol_amount( + amount_in: int, + virtual_base: int, + virtual_quote: int, + real_base: int, + real_quote: int, + slippage_bps: int, +) -> int: + """ + Calculate the token amount received for a given SOL amount on Bonk. + """ + return get_amount_out( + amount_in, + PROTOCOL_FEE_RATE, + PLATFORM_FEE_RATE, + SHARE_FEE_RATE, + virtual_base, + virtual_quote, + real_base, + real_quote, + slippage_bps, + ) + + +def get_sell_sol_amount_from_token_amount( + amount_in: int, + virtual_base: int, + virtual_quote: int, + real_base: int, + real_quote: int, + slippage_bps: int, +) -> int: + """ + Calculate the SOL amount received for a given token amount on Bonk. + For sell, we swap base -> quote. + """ + # For sell: base is input, quote is output + # So we swap virtual_base with virtual_quote roles + return get_amount_out( + amount_in, + PROTOCOL_FEE_RATE, + PLATFORM_FEE_RATE, + SHARE_FEE_RATE, + virtual_quote, # Swapped + virtual_base, # Swapped + real_quote, # Swapped + real_base, # Swapped + slippage_bps, + ) + + +# ============================================ +# Build Buy Instructions +# ============================================ + +def build_buy_instructions( + payer: Pubkey, + output_mint: Pubkey, + input_amount: int, + params: BonkParams, + slippage_bps: int = DEFAULT_SLIPPAGE, + create_input_ata: bool = True, + create_output_ata: bool = True, + close_input_ata: bool = False, + fixed_output_amount: Optional[int] = None, +) -> List[Instruction]: + """ + Build Bonk buy instructions. + + Args: + payer: The wallet paying for the swap + output_mint: The token mint to buy + input_amount: Amount of SOL/quote to spend + params: Bonk protocol parameters + slippage_bps: Slippage tolerance in basis points + create_input_ata: Whether to create WSOL ATA if needed + create_output_ata: Whether to create output token ATA if needed + close_input_ata: Whether to close WSOL ATA after swap + fixed_output_amount: If set, use this as exact output amount + + Returns: + List of instructions for the buy operation + """ + if input_amount == 0: + raise ValueError("Amount cannot be zero") + + instructions = [] + is_usd1_pool = params.is_usd1_pool + + # Determine quote mint (WSOL or USD1) + quote_mint = USD1_TOKEN_ACCOUNT if is_usd1_pool else WSOL_TOKEN_ACCOUNT + quote_token_mint_meta = AccountMeta(quote_mint, False, False) + + # Get pool state + pool_state = params.pool_state + if pool_state is None: + pool_state = get_pool_pda(output_mint, quote_mint) + + # Get global config + global_config = USD1_GLOBAL_CONFIG if is_usd1_pool else GLOBAL_CONFIG + global_config_meta = AccountMeta(global_config, False, False) + + # Calculate minimum output amount + if fixed_output_amount is not None: + minimum_amount_out = fixed_output_amount + else: + minimum_amount_out = get_buy_token_amount_from_sol_amount( + input_amount, + params.virtual_base, + params.virtual_quote, + params.real_base, + params.real_quote, + slippage_bps, + ) + + share_fee_rate = 0 + + # Get user token accounts + user_base_token_account = get_associated_token_address( + payer, output_mint, params.mint_token_program + ) + user_quote_token_account = get_associated_token_address( + payer, quote_mint, TOKEN_PROGRAM + ) + + # Get vaults + base_vault = params.base_vault + if base_vault is None: + base_vault = get_vault_pda(pool_state, output_mint) + + quote_vault = params.quote_vault + if quote_vault is None: + quote_vault = get_vault_pda(pool_state, quote_mint) + + # Handle WSOL for non-USD1 pools + if create_input_ata and not is_usd1_pool: + instructions.extend(handle_wsol(payer, input_amount)) + + # Create output ATA if needed + if create_output_ata: + instructions.append( + create_associated_token_account_idempotent_instruction( + payer, payer, output_mint, params.mint_token_program + ) + ) + + # Build instruction data + data = BUY_EXACT_IN_DISCRIMINATOR + struct.pack(" List[Instruction]: + """ + Build Bonk sell instructions. + + Args: + payer: The wallet paying for the swap + input_mint: The token mint to sell + input_amount: Amount of tokens to sell + params: Bonk protocol parameters + slippage_bps: Slippage tolerance in basis points + create_output_ata: Whether to create WSOL ATA for receiving SOL + close_output_ata: Whether to close WSOL ATA after swap + close_input_ata: Whether to close token ATA after swap + fixed_output_amount: If set, use this as exact output amount + + Returns: + List of instructions for the sell operation + """ + if input_amount == 0: + raise ValueError("Amount cannot be zero") + + instructions = [] + is_usd1_pool = params.is_usd1_pool + + # Determine quote mint (WSOL or USD1) + quote_mint = USD1_TOKEN_ACCOUNT if is_usd1_pool else WSOL_TOKEN_ACCOUNT + quote_token_mint_meta = AccountMeta(quote_mint, False, False) + + # Get pool state + pool_state = params.pool_state + if pool_state is None: + pool_state = get_pool_pda(input_mint, quote_mint) + + # Get global config + global_config = USD1_GLOBAL_CONFIG if is_usd1_pool else GLOBAL_CONFIG + global_config_meta = AccountMeta(global_config, False, False) + + # Calculate minimum output amount + share_fee_rate = 0 + if fixed_output_amount is not None: + minimum_amount_out = fixed_output_amount + else: + minimum_amount_out = get_sell_sol_amount_from_token_amount( + input_amount, + params.virtual_base, + params.virtual_quote, + params.real_base, + params.real_quote, + slippage_bps, + ) + + # Get user token accounts + user_base_token_account = get_associated_token_address( + payer, input_mint, params.mint_token_program + ) + user_quote_token_account = get_associated_token_address( + payer, quote_mint, TOKEN_PROGRAM + ) + + # Get vaults + base_vault = params.base_vault + if base_vault is None: + base_vault = get_vault_pda(pool_state, input_mint) + + quote_vault = params.quote_vault + if quote_vault is None: + quote_vault = get_vault_pda(pool_state, quote_mint) + + # Create WSOL ATA if needed for receiving SOL + if create_output_ata and not is_usd1_pool: + wsol_ata = get_associated_token_address(payer, WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM) + instructions.append( + create_associated_token_account_idempotent_instruction( + payer, payer, WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM + ) + ) + + # Build instruction data + data = SELL_EXACT_IN_DISCRIMINATOR + struct.pack(" Pubkey: + """ + Derive the associated token account address for a given owner and mint. + """ + from solders.pubkey import Pubkey as SolderPubkey + + seeds = [ + bytes(owner), + bytes(token_program), + bytes(mint), + ] + (ata, _) = SolderPubkey.find_program_address(seeds, ASSOCIATED_TOKEN_PROGRAM) + return ata + + +def create_associated_token_account_idempotent_instruction( + payer: Pubkey, + owner: Pubkey, + mint: Pubkey, + token_program: Pubkey = TOKEN_PROGRAM, +) -> Instruction: + """ + Create an idempotent instruction to create an associated token account. + This instruction will succeed even if the account already exists. + """ + ata = get_associated_token_address(owner, mint, token_program) + + # Discriminator for create_idempotent (Anchor IDL) + data = b"\x01" # create_idempotent discriminator (different from create) + + accounts = [ + AccountMeta(payer, True, True), # payer (signer, writable) + AccountMeta(ata, False, True), # ata (writable) + AccountMeta(owner, False, False), # owner (readonly) + AccountMeta(mint, False, False), # mint (readonly) + AccountMeta(SYSTEM_PROGRAM, False, False), # system_program (readonly) + AccountMeta(token_program, False, False), # token_program (readonly) + ] + + return Instruction(ASSOCIATED_TOKEN_PROGRAM, data, accounts) + + +def create_wsol_account_instruction( + payer: Pubkey, + amount: int, +) -> List[Instruction]: + """ + Create instructions to: + 1. Create WSOL account if needed (via ATA idempotent) + 2. Transfer SOL to the WSOL account + 3. Sync the WSOL account (close will be done separately) + + Returns a list of instructions for handling WSOL. + """ + instructions = [] + + wsol_ata = get_associated_token_address(payer, WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM) + + # Create idempotent WSOL ATA + instructions.append( + create_associated_token_account_idempotent_instruction( + payer, payer, WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM + ) + ) + + # Transfer SOL to WSOL ATA (system transfer) + # The amount is transferred to the WSOL account + from solders.system_program import transfer, TransferParams + + transfer_ix = transfer( + TransferParams( + from_pubkey=payer, + to_pubkey=wsol_ata, + lamports=amount, + ) + ) + instructions.append(transfer_ix) + + # Sync native token account (needed for wrapped SOL) + # Sync instruction: 17, 2, 218, 95, 237, 188, 186, 205 (sync_native discriminator) + sync_data = bytes([17, 2, 218, 95, 237, 188, 186, 205]) + sync_accounts = [AccountMeta(wsol_ata, False, True)] + sync_ix = Instruction(TOKEN_PROGRAM, sync_data, sync_accounts) + instructions.append(sync_ix) + + return instructions + + +def close_wsol_account_instruction( + payer: Pubkey, +) -> Instruction: + """ + Create instruction to close WSOL account and reclaim rent. + """ + wsol_ata = get_associated_token_address(payer, WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM) + + # Close account discriminator + close_data = bytes([153, 228, 76, 56, 218, 79, 98, 4]) # close_account + + accounts = [ + AccountMeta(wsol_ata, False, True), # account to close (writable) + AccountMeta(payer, False, True), # destination (writable) + AccountMeta(payer, True, False), # owner (signer) + ] + + return Instruction(TOKEN_PROGRAM, close_data, accounts) + + +def close_token_account_instruction( + token_program: Pubkey, + account: Pubkey, + destination: Pubkey, + owner: Pubkey, +) -> Instruction: + """ + Create instruction to close a token account and reclaim rent. + """ + close_data = bytes([153, 228, 76, 56, 218, 79, 98, 4]) # close_account + + accounts = [ + AccountMeta(account, False, True), # account to close (writable) + AccountMeta(destination, False, True), # destination (writable) + AccountMeta(owner, True, False), # owner (signer) + ] + + return Instruction(token_program, close_data, accounts) + + +def handle_wsol(payer: Pubkey, amount: int) -> List[Instruction]: + """ + Handle WSOL operations: create ATA, transfer SOL, sync. + This is the Python equivalent of the Rust handle_wsol function. + """ + return create_wsol_account_instruction(payer, amount) + + +def create_wsol_ata(payer: Pubkey) -> List[Instruction]: + """ + Create WSOL ATA without funding (for receiving WSOL). + """ + return [ + create_associated_token_account_idempotent_instruction( + payer, payer, WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM + ) + ] + + +def close_wsol(payer: Pubkey) -> List[Instruction]: + """ + Close WSOL ATA and reclaim rent. + """ + return [close_wsol_account_instruction(payer)] + + +# ============================================ +# Calculation Functions +# ============================================ + +def calculate_with_slippage_buy(amount: int, slippage_bps: int) -> int: + """ + Calculate maximum input amount with slippage for buy operations. + Returns amount * (10000 + slippage_bps) / 10000 + """ + return amount * (10000 + slippage_bps) // 10000 + + +def calculate_with_slippage_sell(amount: int, slippage_bps: int) -> int: + """ + Calculate minimum output amount with slippage for sell operations. + Returns amount * (10000 - slippage_bps) / 10000 + """ + return amount * (10000 - slippage_bps) // 10000 diff --git a/sol-trade-sdk-python/src/instruction/meteora_damm_v2_builder.py b/sol-trade-sdk-python/src/instruction/meteora_damm_v2_builder.py new file mode 100644 index 0000000..cd5ef6a --- /dev/null +++ b/sol-trade-sdk-python/src/instruction/meteora_damm_v2_builder.py @@ -0,0 +1,325 @@ +""" +Meteora DAMM V2 instruction builder for Solana trading SDK. +Production-grade implementation with all constants, discriminators, and PDA derivation functions. +""" + +from typing import List, Optional +from dataclasses import dataclass +from solders.pubkey import Pubkey +from solders.instruction import Instruction, AccountMeta +import struct + +from .common import ( + TOKEN_PROGRAM, + WSOL_TOKEN_ACCOUNT, + USDC_TOKEN_ACCOUNT, + DEFAULT_SLIPPAGE, + get_associated_token_address, + create_associated_token_account_idempotent_instruction, + handle_wsol, + close_wsol, + close_token_account_instruction, +) + +# ============================================ +# Meteora DAMM V2 Program ID +# ============================================ + +METEORA_DAMM_V2_PROGRAM_ID: Pubkey = Pubkey.from_string("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG") + +# ============================================ +# Meteora DAMM V2 Constants +# ============================================ + +# Pool Authority +AUTHORITY: Pubkey = Pubkey.from_string("HLnpSz9h2S4hiLQ43rnSD9XkcUThA7B8hQMKmDaiTLcC") + +# ============================================ +# Instruction Discriminators +# ============================================ + +SWAP_DISCRIMINATOR: bytes = bytes([248, 198, 158, 145, 225, 117, 135, 200]) + +# ============================================ +# Seeds +# ============================================ + +EVENT_AUTHORITY_SEED = b"__event_authority" + + +# ============================================ +# PDA Derivation Functions +# ============================================ + +def get_event_authority_pda() -> Pubkey: + """ + Derive the event authority PDA. + Seeds: ["__event_authority"] + """ + seeds = [EVENT_AUTHORITY_SEED] + (pda, _) = Pubkey.find_program_address(seeds, METEORA_DAMM_V2_PROGRAM_ID) + return pda + + +# ============================================ +# Meteora DAMM V2 Parameters Dataclass +# ============================================ + +@dataclass +class MeteoraDammV2Params: + """Parameters for Meteora DAMM V2 protocol trading.""" + pool: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + token_a_vault: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + token_b_vault: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + token_a_mint: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + token_b_mint: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + token_a_program: Pubkey = TOKEN_PROGRAM + token_b_program: Pubkey = TOKEN_PROGRAM + + @property + def is_wsol(self) -> bool: + """Check if the pool contains WSOL.""" + return self.token_a_mint == WSOL_TOKEN_ACCOUNT or self.token_b_mint == WSOL_TOKEN_ACCOUNT + + @property + def is_usdc(self) -> bool: + """Check if the pool contains USDC.""" + return self.token_a_mint == USDC_TOKEN_ACCOUNT or self.token_b_mint == USDC_TOKEN_ACCOUNT + + +# ============================================ +# Build Buy Instructions +# ============================================ + +def build_buy_instructions( + payer: Pubkey, + output_mint: Pubkey, + input_amount: int, + params: MeteoraDammV2Params, + slippage_bps: int = DEFAULT_SLIPPAGE, + create_input_ata: bool = True, + create_output_ata: bool = True, + close_input_ata: bool = False, + fixed_output_amount: Optional[int] = None, +) -> List[Instruction]: + """ + Build Meteora DAMM V2 buy instructions. + + Args: + payer: The wallet paying for the swap + output_mint: The token mint to buy + input_amount: Amount of SOL/USDC to spend + params: Meteora DAMM V2 protocol parameters + slippage_bps: Slippage tolerance in basis points + create_input_ata: Whether to create WSOL ATA if needed + create_output_ata: Whether to create output token ATA if needed + close_input_ata: Whether to close WSOL ATA after swap + fixed_output_amount: MUST be set for Meteora DAMM V2 swaps + + Returns: + List of instructions for the buy operation + """ + if input_amount == 0: + raise ValueError("Amount cannot be zero") + + instructions = [] + + # Validate pool contains WSOL or USDC + if not params.is_wsol and not params.is_usdc: + raise ValueError("Pool must contain WSOL or USDC") + + # Determine if token A is input (WSOL/USDC) + is_a_in = params.token_a_mint == WSOL_TOKEN_ACCOUNT or params.token_a_mint == USDC_TOKEN_ACCOUNT + + # Meteora DAMM V2 requires fixed_output_amount + if fixed_output_amount is None: + raise ValueError("fixed_output_amount must be set for MeteoraDammV2 swap") + + minimum_amount_out = fixed_output_amount + + # Determine input/output mints and programs + input_mint = WSOL_TOKEN_ACCOUNT if params.is_wsol else USDC_TOKEN_ACCOUNT + + input_token_program = params.token_a_program if is_a_in else params.token_b_program + output_token_program = params.token_b_program if is_a_in else params.token_a_program + + # Get user token accounts + input_token_account = get_associated_token_address(payer, input_mint, TOKEN_PROGRAM) + output_token_account = get_associated_token_address(payer, output_mint, TOKEN_PROGRAM) + + # Get event authority PDA + event_authority = get_event_authority_pda() + + # Handle WSOL if needed + if create_input_ata and params.is_wsol: + instructions.extend(handle_wsol(payer, input_amount)) + + # Create output ATA if needed + if create_output_ata: + instructions.append( + create_associated_token_account_idempotent_instruction( + payer, payer, output_mint, TOKEN_PROGRAM + ) + ) + + # Build instruction data + data = SWAP_DISCRIMINATOR + struct.pack(" List[Instruction]: + """ + Build Meteora DAMM V2 sell instructions. + + Args: + payer: The wallet paying for the swap + input_mint: The token mint to sell + input_amount: Amount of tokens to sell + params: Meteora DAMM V2 protocol parameters + slippage_bps: Slippage tolerance in basis points + create_output_ata: Whether to create WSOL ATA for receiving SOL + close_output_ata: Whether to close WSOL ATA after swap + close_input_ata: Whether to close token ATA after swap + fixed_output_amount: MUST be set for Meteora DAMM V2 swaps + + Returns: + List of instructions for the sell operation + """ + if input_amount == 0: + raise ValueError("Amount cannot be zero") + + instructions = [] + + # Validate pool contains WSOL or USDC + if not params.is_wsol and not params.is_usdc: + raise ValueError("Pool must contain WSOL or USDC") + + # Determine if token B is output (WSOL/USDC) + is_a_in = params.token_b_mint == WSOL_TOKEN_ACCOUNT or params.token_b_mint == USDC_TOKEN_ACCOUNT + + # Meteora DAMM V2 requires fixed_output_amount + if fixed_output_amount is None: + raise ValueError("fixed_output_amount must be set for MeteoraDammV2 swap") + + minimum_amount_out = fixed_output_amount + + # Determine output mint (WSOL or USDC) + output_mint = WSOL_TOKEN_ACCOUNT if params.is_wsol else USDC_TOKEN_ACCOUNT + + # Get token programs based on direction + input_token_program = params.token_a_program if is_a_in else params.token_b_program + output_token_program = params.token_b_program if is_a_in else params.token_a_program + + # Get user token accounts + input_token_account = get_associated_token_address(payer, input_mint, input_token_program) + output_token_account = get_associated_token_address(payer, output_mint, TOKEN_PROGRAM) + + # Get event authority PDA + event_authority = get_event_authority_pda() + + # Create WSOL ATA if needed for receiving SOL + if create_output_ata and params.is_wsol: + instructions.append( + create_associated_token_account_idempotent_instruction( + payer, payer, WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM + ) + ) + + # Build instruction data + data = SWAP_DISCRIMINATOR + struct.pack(" Pubkey: + """ + Derive the bonding curve PDA for a given mint. + Seeds: ["bonding-curve", mint] + """ + seeds = [BONDING_CURVE_SEED, bytes(mint)] + (pda, _) = Pubkey.find_program_address(seeds, PUMPFUN_PROGRAM_ID) + return pda + + +def get_bonding_curve_v2_pda(mint: Pubkey) -> Pubkey: + """ + Derive the bonding curve v2 PDA for a given mint. + Seeds: ["bonding-curve-v2", mint] + """ + seeds = [BONDING_CURVE_V2_SEED, bytes(mint)] + (pda, _) = Pubkey.find_program_address(seeds, PUMPFUN_PROGRAM_ID) + return pda + + +def get_creator_vault_pda(creator: Pubkey) -> Pubkey: + """ + Derive the creator vault PDA for a given creator. + Seeds: ["creator-vault", creator] + """ + seeds = [CREATOR_VAULT_SEED, bytes(creator)] + (pda, _) = Pubkey.find_program_address(seeds, PUMPFUN_PROGRAM_ID) + return pda + + +def get_user_volume_accumulator_pda(user: Pubkey) -> Pubkey: + """ + Derive the user volume accumulator PDA for a given user. + Seeds: ["user_volume_accumulator", user] + """ + seeds = [USER_VOLUME_ACCUMULATOR_SEED, bytes(user)] + (pda, _) = Pubkey.find_program_address(seeds, PUMPFUN_PROGRAM_ID) + return pda + + +def get_creator(creator_vault_pda: Pubkey) -> Pubkey: + """ + Get the creator pubkey from the creator vault PDA. + Returns default pubkey if creator_vault_pda is default. + """ + default_pubkey = Pubkey.from_string("11111111111111111111111111111111") + if creator_vault_pda == default_pubkey: + return default_pubkey + return creator_vault_pda + + +def get_mayhem_fee_recipient_random() -> Pubkey: + """ + Get a random Mayhem fee recipient. + """ + return random.choice(MAYHEM_FEE_RECIPIENTS) + + +# ============================================ +# PumpFun Parameters Dataclass +# ============================================ + +@dataclass +class PumpFunParams: + """Parameters for PumpFun protocol trading.""" + bonding_curve_account: Optional[Pubkey] = None # If None, will derive from mint + virtual_token_reserves: int = 0 + virtual_sol_reserves: int = 0 + real_token_reserves: int = 0 + real_sol_reserves: int = 0 + token_total_supply: int = 0 + complete: bool = False + creator: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + is_mayhem_mode: bool = False + is_cashback_coin: bool = False + associated_bonding_curve: Optional[Pubkey] = None + creator_vault: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + token_program: Pubkey = TOKEN_PROGRAM + close_token_account_when_sell: bool = False + + +# ============================================ +# PumpFun Calculation Functions +# ============================================ + +def get_buy_token_amount_from_sol_amount( + virtual_token_reserves: int, + virtual_sol_reserves: int, + real_token_reserves: int, + creator: Pubkey, + sol_amount: int, +) -> int: + """ + Calculate the token amount received for a given SOL amount on PumpFun. + Uses the bonding curve formula. + """ + if sol_amount == 0: + return 0 + + default_pubkey = Pubkey.from_string("11111111111111111111111111111111") + is_non_zero_creator = creator != default_pubkey + + # Calculate using AMM formula + n = virtual_sol_reserves * virtual_token_reserves + i = virtual_sol_reserves + sol_amount + r = n // i + 1 + s = virtual_token_reserves - r + + # Apply creator fee if applicable + if is_non_zero_creator: + s = s - (s * 30) // 10000 # 0.30% creator fee + + return min(s, real_token_reserves) + + +def get_sell_sol_amount_from_token_amount( + virtual_token_reserves: int, + virtual_sol_reserves: int, + creator: Pubkey, + token_amount: int, +) -> int: + """ + Calculate the SOL amount received for a given token amount on PumpFun. + """ + if token_amount == 0: + return 0 + + default_pubkey = Pubkey.from_string("11111111111111111111111111111111") + is_non_zero_creator = creator != default_pubkey + + # Calculate using AMM formula + n = virtual_sol_reserves * virtual_token_reserves + i = virtual_token_reserves + token_amount + r = n // i + 1 + sol_amount = virtual_sol_reserves - r + + # Apply creator fee if applicable + if is_non_zero_creator: + sol_amount = sol_amount - (sol_amount * 30) // 10000 # 0.30% creator fee + + return sol_amount + + +# ============================================ +# Build Buy Instructions +# ============================================ + +def build_buy_instructions( + payer: Pubkey, + output_mint: Pubkey, + input_amount: int, + params: PumpFunParams, + slippage_bps: int = DEFAULT_SLIPPAGE, + create_output_ata: bool = True, + close_input_ata: bool = False, + fixed_output_amount: Optional[int] = None, + use_exact_sol_amount: bool = True, +) -> List[Instruction]: + """ + Build PumpFun buy instructions. + + Args: + payer: The wallet paying for the swap + output_mint: The token mint to buy + input_amount: Amount of SOL to spend + params: PumpFun protocol parameters + slippage_bps: Slippage tolerance in basis points + create_output_ata: Whether to create output token ATA if needed + close_input_ata: Whether to close WSOL ATA after swap + fixed_output_amount: If set, use this as exact output amount + use_exact_sol_amount: If True, use buy_exact_sol_in instruction + + Returns: + List of instructions for the buy operation + """ + if input_amount == 0: + raise ValueError("Amount cannot be zero") + + instructions = [] + + # Get bonding curve address + bonding_curve_addr = params.bonding_curve_account + if bonding_curve_addr is None: + bonding_curve_addr = get_bonding_curve_pda(output_mint) + + # Get creator from creator_vault + creator = get_creator(params.creator_vault) + + # Calculate token amount + if fixed_output_amount is not None: + buy_token_amount = fixed_output_amount + else: + buy_token_amount = get_buy_token_amount_from_sol_amount( + params.virtual_token_reserves, + params.virtual_sol_reserves, + params.real_token_reserves, + creator, + input_amount, + ) + + # Calculate max SOL cost with slippage + max_sol_cost = calculate_with_slippage_buy(input_amount, slippage_bps) + + # Get associated bonding curve + associated_bonding_curve = params.associated_bonding_curve + if associated_bonding_curve is None: + associated_bonding_curve = get_associated_token_address( + bonding_curve_addr, output_mint, params.token_program + ) + + # Get user token account + user_token_account = get_associated_token_address( + payer, output_mint, params.token_program + ) + + # Create ATA if needed + if create_output_ata: + instructions.append( + create_associated_token_account_idempotent_instruction( + payer, payer, output_mint, params.token_program + ) + ) + + # Get user volume accumulator + user_volume_accumulator = get_user_volume_accumulator_pda(payer) + + # Get bonding curve v2 + bonding_curve_v2 = get_bonding_curve_v2_pda(output_mint) + + # Determine fee recipient + if params.is_mayhem_mode: + fee_recipient = get_mayhem_fee_recipient_random() + else: + fee_recipient = FEE_RECIPIENT + + # Build instruction data + track_volume = bytes([1, 1]) if params.is_cashback_coin else bytes([1, 0]) + + if use_exact_sol_amount: + # buy_exact_sol_in(spendable_sol_in: u64, min_tokens_out: u64, track_volume) + min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bps) + data = BUY_EXACT_SOL_IN_DISCRIMINATOR + struct.pack(" List[Instruction]: + """ + Build PumpFun sell instructions. + + Args: + payer: The wallet paying for the swap + input_mint: The token mint to sell + input_amount: Amount of tokens to sell + params: PumpFun protocol parameters + slippage_bps: Slippage tolerance in basis points + create_output_ata: Whether to create WSOL ATA for receiving SOL + close_output_ata: Whether to close WSOL ATA after swap + close_input_ata: Whether to close token ATA after swap + fixed_output_amount: If set, use this as exact output amount + + Returns: + List of instructions for the sell operation + """ + if input_amount == 0: + raise ValueError("Amount cannot be zero") + + instructions = [] + + # Get bonding curve address + bonding_curve_addr = params.bonding_curve_account + if bonding_curve_addr is None: + bonding_curve_addr = get_bonding_curve_pda(input_mint) + + # Get creator from creator_vault + creator = get_creator(params.creator_vault) + + # Calculate SOL amount + sol_amount = get_sell_sol_amount_from_token_amount( + params.virtual_token_reserves, + params.virtual_sol_reserves, + creator, + input_amount, + ) + + # Calculate min SOL output with slippage + if fixed_output_amount is not None: + min_sol_output = fixed_output_amount + else: + min_sol_output = calculate_with_slippage_sell(sol_amount, slippage_bps) + + # Get associated bonding curve + associated_bonding_curve = params.associated_bonding_curve + if associated_bonding_curve is None: + associated_bonding_curve = get_associated_token_address( + bonding_curve_addr, input_mint, params.token_program + ) + + # Get user token account + user_token_account = get_associated_token_address( + payer, input_mint, params.token_program + ) + + # Create WSOL ATA if needed for receiving SOL + if create_output_ata or close_output_ata: + instructions.extend( + create_associated_token_account_idempotent_instruction( + payer, payer, WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM + ) + ) + + # Determine fee recipient + if params.is_mayhem_mode: + fee_recipient = get_mayhem_fee_recipient_random() + else: + fee_recipient = FEE_RECIPIENT + + # Build instruction data + data = SELL_DISCRIMINATOR + struct.pack(" Instruction: + """ + Build instruction to claim cashback for PumpFun bonding curve. + Transfers native lamports from UserVolumeAccumulator to user. + """ + user_volume_accumulator = get_user_volume_accumulator_pda(payer) + + accounts = [ + AccountMeta(payer, True, True), # user (signer, writable) + AccountMeta(user_volume_accumulator, False, True), # user_volume_accumulator (writable) + AccountMeta(SYSTEM_PROGRAM, False, False), # system_program + AccountMeta(EVENT_AUTHORITY, False, False), # event_authority + AccountMeta(PUMPFUN_PROGRAM_ID, False, False), # program + ] + + return Instruction(PUMPFUN_PROGRAM_ID, CLAIM_CASHBACK_DISCRIMINATOR, accounts) + + +# ============================================ +# Exports +# ============================================ + +__all__ = [ + # Program IDs and Constants + "PUMPFUN_PROGRAM_ID", + "FEE_RECIPIENT", + "GLOBAL_ACCOUNT", + "EVENT_AUTHORITY", + "AUTHORITY", + "FEE_PROGRAM", + "GLOBAL_VOLUME_ACCUMULATOR", + "FEE_CONFIG", + "MAYHEM_FEE_RECIPIENTS", + # Discriminators + "BUY_DISCRIMINATOR", + "BUY_EXACT_SOL_IN_DISCRIMINATOR", + "SELL_DISCRIMINATOR", + "CLAIM_CASHBACK_DISCRIMINATOR", + # PDA Functions + "get_bonding_curve_pda", + "get_bonding_curve_v2_pda", + "get_creator_vault_pda", + "get_user_volume_accumulator_pda", + "get_creator", + "get_mayhem_fee_recipient_random", + # Params + "PumpFunParams", + # Calculation Functions + "get_buy_token_amount_from_sol_amount", + "get_sell_sol_amount_from_token_amount", + # Instruction Builders + "build_buy_instructions", + "build_sell_instructions", + "claim_cashback_pumpfun_instruction", +] diff --git a/sol-trade-sdk-python/src/instruction/pumpswap_builder.py b/sol-trade-sdk-python/src/instruction/pumpswap_builder.py new file mode 100644 index 0000000..7705df3 --- /dev/null +++ b/sol-trade-sdk-python/src/instruction/pumpswap_builder.py @@ -0,0 +1,760 @@ +""" +PumpSwap instruction builder for Solana trading SDK. +Production-grade implementation with all constants, discriminators, and PDA derivation functions. +""" + +from typing import List, Optional, Tuple +from dataclasses import dataclass +from solders.pubkey import Pubkey +from solders.instruction import Instruction, AccountMeta +import struct +import random + +from .common import ( + SYSTEM_PROGRAM, + TOKEN_PROGRAM, + TOKEN_PROGRAM_2022, + WSOL_TOKEN_ACCOUNT, + USDC_TOKEN_ACCOUNT, + DEFAULT_SLIPPAGE, + get_associated_token_address, + create_associated_token_account_idempotent_instruction, + handle_wsol, + close_wsol, + close_token_account_instruction, + calculate_with_slippage_sell, +) + +# ============================================ +# PumpSwap Program ID +# ============================================ + +PUMPSWAP_PROGRAM_ID: Pubkey = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") + +# ============================================ +# PumpSwap Constants +# ============================================ + +# Fee Recipient +FEE_RECIPIENT: Pubkey = Pubkey.from_string("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV") + +# Global Account +GLOBAL_ACCOUNT: Pubkey = Pubkey.from_string("ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw") + +# Event Authority +EVENT_AUTHORITY: Pubkey = Pubkey.from_string("GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR") + +# Associated Token Program +ASSOCIATED_TOKEN_PROGRAM: Pubkey = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") + +# Protocol Fee Recipient +PROTOCOL_FEE_RECIPIENT: Pubkey = Pubkey.from_string("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV") + +# Pump Program ID (Bonding Curve) +PUMP_PROGRAM_ID: Pubkey = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") + +# Fee Program +FEE_PROGRAM: Pubkey = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ") + +# Global Volume Accumulator +GLOBAL_VOLUME_ACCUMULATOR: Pubkey = Pubkey.from_string("C2aFPdENg4A2HQsmrd5rTw5TaYBX5Ku887cWjbFKtZpw") + +# Fee Config +FEE_CONFIG: Pubkey = Pubkey.from_string("5PHirr8joyTMp9JMm6N7hNDVyEYdkzDqazxPD7RaTjx") + +# Default Coin Creator Vault Authority +DEFAULT_COIN_CREATOR_VAULT_AUTHORITY: Pubkey = Pubkey.from_string("8N3GDaZ2iwN65oxVatKTLPNooAVUJTbfiVJ1ahyqwjSk") + +# Mayhem Fee Recipients +MAYHEM_FEE_RECIPIENTS: List[Pubkey] = [ + Pubkey.from_string("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"), + Pubkey.from_string("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"), + Pubkey.from_string("8SBKzEQU4nLSzcwF4a74F2iaUDQyTfjGndn6qUWBnrpR"), + Pubkey.from_string("4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH"), + Pubkey.from_string("8sNeir4QsLsJdYpc9RZacohhK1Y5FLU3nC5LXgYB4aa6"), + Pubkey.from_string("Fh9HmeLNUMVCvejxCtCL2DbYaRyBFVJ5xrWkLnMH6fdk"), + Pubkey.from_string("463MEnMeGyJekNZFQSTUABBEbLnvMTALbT6ZmsxAbAdq"), + Pubkey.from_string("6AUH3WEHucYZyC61hqpqYUWVto5qA5hjHuNQ32GNnNxA"), +] + +# Fee Basis Points +LP_FEE_BASIS_POINTS: int = 25 +PROTOCOL_FEE_BASIS_POINTS: int = 5 +COIN_CREATOR_FEE_BASIS_POINTS: int = 5 + +# ============================================ +# Instruction Discriminators +# ============================================ + +BUY_DISCRIMINATOR: bytes = bytes([102, 6, 61, 18, 1, 218, 235, 234]) +BUY_EXACT_QUOTE_IN_DISCRIMINATOR: bytes = bytes([198, 46, 21, 82, 180, 217, 232, 112]) +SELL_DISCRIMINATOR: bytes = bytes([51, 230, 133, 164, 1, 127, 131, 173]) +CLAIM_CASHBACK_DISCRIMINATOR: bytes = bytes([37, 58, 35, 126, 190, 53, 228, 197]) + +# ============================================ +# Seeds +# ============================================ + +GLOBAL_SEED = b"global" +MINT_AUTHORITY_SEED = b"mint-authority" +BONDING_CURVE_SEED = b"bonding-curve" +METADATA_SEED = b"metadata" +USER_VOLUME_ACCUMULATOR_SEED = b"user_volume_accumulator" +GLOBAL_VOLUME_ACCUMULATOR_SEED = b"global_volume_accumulator" +FEE_CONFIG_SEED = b"fee_config" +POOL_V2_SEED = b"pool-v2" +POOL_SEED = b"pool" +POOL_AUTHORITY_SEED = b"pool-authority" + +# ============================================ +# PDA Derivation Functions +# ============================================ + +def get_pool_v2_pda(base_mint: Pubkey) -> Pubkey: + """ + Derive the pool v2 PDA for a given base mint. + Seeds: ["pool-v2", base_mint] + """ + seeds = [POOL_V2_SEED, bytes(base_mint)] + (pda, _) = Pubkey.find_program_address(seeds, PUMPSWAP_PROGRAM_ID) + return pda + + +def get_pump_pool_authority_pda(mint: Pubkey) -> Pubkey: + """ + Derive the pump pool authority PDA for canonical pool. + Seeds: ["pool-authority", mint] + """ + seeds = [POOL_AUTHORITY_SEED, bytes(mint)] + (pda, _) = Pubkey.find_program_address(seeds, PUMP_PROGRAM_ID) + return pda + + +def get_canonical_pool_pda(mint: Pubkey) -> Pubkey: + """ + Derive the canonical pump pool PDA. + Seeds: ["pool", index=0, authority, mint, WSOL] + """ + index = 0 + authority = get_pump_pool_authority_pda(mint) + seeds = [ + POOL_SEED, + struct.pack(" Pubkey: + """ + Derive the user volume accumulator PDA for a given user. + Seeds: ["user_volume_accumulator", user] + """ + seeds = [USER_VOLUME_ACCUMULATOR_SEED, bytes(user)] + (pda, _) = Pubkey.find_program_address(seeds, PUMPSWAP_PROGRAM_ID) + return pda + + +def get_user_volume_accumulator_wsol_ata(user: Pubkey) -> Pubkey: + """ + Get the WSOL ATA of UserVolumeAccumulator for PumpSwap AMM. + """ + accumulator = get_user_volume_accumulator_pda(user) + return get_associated_token_address(accumulator, WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM) + + +def get_user_volume_accumulator_quote_ata( + user: Pubkey, + quote_mint: Pubkey, + quote_token_program: Pubkey, +) -> Pubkey: + """ + Get the quote-mint ATA of UserVolumeAccumulator (used for sell cashback). + """ + accumulator = get_user_volume_accumulator_pda(user) + return get_associated_token_address(accumulator, quote_mint, quote_token_program) + + +def coin_creator_vault_authority(coin_creator: Pubkey) -> Pubkey: + """ + Derive the coin creator vault authority PDA. + Seeds: ["creator_vault", coin_creator] + """ + seeds = [b"creator_vault", bytes(coin_creator)] + (pda, _) = Pubkey.find_program_address(seeds, PUMPSWAP_PROGRAM_ID) + return pda + + +def coin_creator_vault_ata(coin_creator: Pubkey, quote_mint: Pubkey) -> Pubkey: + """ + Get the coin creator vault ATA. + """ + authority = coin_creator_vault_authority(coin_creator) + return get_associated_token_address(authority, quote_mint, TOKEN_PROGRAM) + + +def fee_recipient_ata(fee_recipient: Pubkey, quote_mint: Pubkey) -> Pubkey: + """ + Get the fee recipient ATA. + """ + return get_associated_token_address(fee_recipient, quote_mint, TOKEN_PROGRAM) + + +def get_mayhem_fee_recipient_random() -> Tuple[Pubkey, AccountMeta]: + """ + Get a random Mayhem fee recipient and its AccountMeta. + """ + recipient = random.choice(MAYHEM_FEE_RECIPIENTS) + meta = AccountMeta(recipient, False, False) + return (recipient, meta) + + +# ============================================ +# PumpSwap Parameters Dataclass +# ============================================ + +@dataclass +class PumpSwapParams: + """Parameters for PumpSwap protocol trading.""" + pool: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + base_mint: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + quote_mint: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + pool_base_token_account: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + pool_quote_token_account: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + pool_base_token_reserves: int = 0 + pool_quote_token_reserves: int = 0 + coin_creator_vault_ata: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + coin_creator_vault_authority: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + base_token_program: Pubkey = TOKEN_PROGRAM + quote_token_program: Pubkey = TOKEN_PROGRAM + is_mayhem_mode: bool = False + is_cashback_coin: bool = False + + @property + def is_wsol(self) -> bool: + """Check if the pool contains WSOL.""" + return ( + (self.base_mint == WSOL_TOKEN_ACCOUNT and self.quote_mint != USDC_TOKEN_ACCOUNT) + or (self.quote_mint == WSOL_TOKEN_ACCOUNT and self.base_mint != USDC_TOKEN_ACCOUNT) + ) + + @property + def is_usdc(self) -> bool: + """Check if the pool contains USDC.""" + return ( + (self.base_mint == USDC_TOKEN_ACCOUNT and self.quote_mint != WSOL_TOKEN_ACCOUNT) + or (self.quote_mint == USDC_TOKEN_ACCOUNT and self.base_mint != WSOL_TOKEN_ACCOUNT) + ) + + +# ============================================ +# PumpSwap Calculation Functions +# ============================================ + +def buy_quote_input_internal( + quote_amount: int, + slippage_bps: int, + pool_base_reserves: int, + pool_quote_reserves: int, + creator: Pubkey, +) -> Tuple[int, int]: + """ + Calculate buy amounts for PumpSwap when quote is input. + Returns (base_amount_out, max_quote_amount_in). + """ + default_pubkey = Pubkey.from_string("11111111111111111111111111111111") + is_non_zero_creator = creator != default_pubkey + + # Apply LP fee + quote_amount_after_fee = quote_amount - (quote_amount * LP_FEE_BASIS_POINTS) // 10000 + + # Apply protocol fee + quote_amount_after_fee = quote_amount_after_fee - (quote_amount * PROTOCOL_FEE_BASIS_POINTS) // 10000 + + # Apply creator fee if applicable + if is_non_zero_creator: + quote_amount_after_fee = quote_amount_after_fee - (quote_amount * COIN_CREATOR_FEE_BASIS_POINTS) // 10000 + + # Calculate output using AMM formula + # out = (quote_in * base_reserves) / (quote_reserves + quote_in) + numerator = quote_amount_after_fee * pool_base_reserves + denominator = pool_quote_reserves + quote_amount_after_fee + base_amount_out = numerator // denominator + + # Calculate max quote with slippage + max_quote = int(quote_amount * (10000 + slippage_bps) / 10000) + + return (base_amount_out, max_quote) + + +def sell_base_input_internal( + base_amount: int, + slippage_bps: int, + pool_base_reserves: int, + pool_quote_reserves: int, + creator: Pubkey, +) -> Tuple[int, int]: + """ + Calculate sell amounts for PumpSwap when base is input. + Returns (min_quote_amount_out, base_amount_in). + """ + default_pubkey = Pubkey.from_string("11111111111111111111111111111111") + is_non_zero_creator = creator != default_pubkey + + # Apply LP fee + base_amount_after_fee = base_amount - (base_amount * LP_FEE_BASIS_POINTS) // 10000 + + # Apply protocol fee + base_amount_after_fee = base_amount_after_fee - (base_amount * PROTOCOL_FEE_BASIS_POINTS) // 10000 + + # Apply creator fee if applicable + if is_non_zero_creator: + base_amount_after_fee = base_amount_after_fee - (base_amount * COIN_CREATOR_FEE_BASIS_POINTS) // 10000 + + # Calculate output using AMM formula + # out = (base_in * quote_reserves) / (base_reserves + base_in) + numerator = base_amount_after_fee * pool_quote_reserves + denominator = pool_base_reserves + base_amount_after_fee + quote_amount_out = numerator // denominator + + # Apply slippage + min_quote = calculate_with_slippage_sell(quote_amount_out, slippage_bps) + + return (min_quote, base_amount) + + +# ============================================ +# Build Buy Instructions +# ============================================ + +def build_buy_instructions( + payer: Pubkey, + output_mint: Pubkey, + input_amount: int, + params: PumpSwapParams, + slippage_bps: int = DEFAULT_SLIPPAGE, + create_input_ata: bool = True, + create_output_ata: bool = True, + close_input_ata: bool = False, + fixed_output_amount: Optional[int] = None, + use_exact_quote_amount: bool = True, +) -> List[Instruction]: + """ + Build PumpSwap buy instructions. + + Args: + payer: The wallet paying for the swap + output_mint: The token mint to buy (base_mint) + input_amount: Amount of SOL/USDC to spend + params: PumpSwap protocol parameters + slippage_bps: Slippage tolerance in basis points + create_input_ata: Whether to create WSOL ATA if needed + create_output_ata: Whether to create output token ATA if needed + close_input_ata: Whether to close WSOL ATA after swap + fixed_output_amount: If set, use this as exact output amount + use_exact_quote_amount: If True, use buy_exact_quote_in instruction + + Returns: + List of instructions for the buy operation + """ + if input_amount == 0: + raise ValueError("Amount cannot be zero") + + instructions = [] + + # Validate pool contains WSOL or USDC + if not params.is_wsol and not params.is_usdc: + raise ValueError("Pool must contain WSOL or USDC") + + # Determine if quote is WSOL/USDC + quote_is_wsol_or_usdc = ( + params.quote_mint == WSOL_TOKEN_ACCOUNT or params.quote_mint == USDC_TOKEN_ACCOUNT + ) + + # Get creator + creator = Pubkey.from_string("11111111111111111111111111111111") + if params.coin_creator_vault_authority != DEFAULT_COIN_CREATOR_VAULT_AUTHORITY: + creator = params.coin_creator_vault_authority + + # Calculate amounts + if quote_is_wsol_or_usdc: + result = buy_quote_input_internal( + input_amount, + slippage_bps, + params.pool_base_token_reserves, + params.pool_quote_token_reserves, + creator, + ) + token_amount, sol_amount = result[0], result[1] + else: + result = sell_base_input_internal( + input_amount, + slippage_bps, + params.pool_base_token_reserves, + params.pool_quote_token_reserves, + creator, + ) + token_amount, sol_amount = result[1], result[0] + + if fixed_output_amount is not None: + token_amount = fixed_output_amount + + # Get user token accounts + user_base_token_account = get_associated_token_address( + payer, params.base_mint, params.base_token_program + ) + user_quote_token_account = get_associated_token_address( + payer, params.quote_mint, params.quote_token_program + ) + + # Determine fee recipient + if params.is_mayhem_mode: + fee_recipient, fee_recipient_meta = get_mayhem_fee_recipient_random() + else: + fee_recipient = FEE_RECIPIENT + fee_recipient_meta = AccountMeta(FEE_RECIPIENT, False, False) + + # Get fee recipient ATA + if params.is_mayhem_mode: + fee_recipient_ata_addr = fee_recipient_ata(fee_recipient, WSOL_TOKEN_ACCOUNT) + else: + fee_recipient_ata_addr = fee_recipient_ata(fee_recipient, params.quote_mint) + + # Handle WSOL if needed + if create_input_ata: + wrap_amount = input_amount if quote_is_wsol_or_usdc and use_exact_quote_amount else sol_amount + if params.is_wsol: + instructions.extend(handle_wsol(payer, wrap_amount)) + + # Create output ATA if needed + if create_output_ata: + output_mint_to_create = params.base_mint if quote_is_wsol_or_usdc else params.quote_mint + output_program = params.base_token_program if quote_is_wsol_or_usdc else params.quote_token_program + instructions.append( + create_associated_token_account_idempotent_instruction( + payer, payer, output_mint_to_create, output_program + ) + ) + + # Build accounts list + accounts = [ + AccountMeta(params.pool, False, True), # pool_id (writable) + AccountMeta(payer, True, True), # user (signer, writable) + AccountMeta(GLOBAL_ACCOUNT, False, False), # global (readonly) + AccountMeta(params.base_mint, False, False), # base_mint (readonly) + AccountMeta(params.quote_mint, False, False), # quote_mint (readonly) + AccountMeta(user_base_token_account, False, True), # user_base_token_account (writable) + AccountMeta(user_quote_token_account, False, True), # user_quote_token_account (writable) + AccountMeta(params.pool_base_token_account, False, True), # pool_base_token_account (writable) + AccountMeta(params.pool_quote_token_account, False, True), # pool_quote_token_account (writable) + fee_recipient_meta, # fee_recipient (readonly) + AccountMeta(fee_recipient_ata_addr, False, True), # fee_recipient_ata (writable) + AccountMeta(params.base_token_program, False, False), # base_token_program (readonly) + AccountMeta(params.quote_token_program, False, False), # quote_token_program (readonly) + AccountMeta(SYSTEM_PROGRAM, False, False), # system_program (readonly) + AccountMeta(ASSOCIATED_TOKEN_PROGRAM, False, False), # associated_token_program (readonly) + AccountMeta(EVENT_AUTHORITY, False, False), # event_authority (readonly) + AccountMeta(PUMPSWAP_PROGRAM_ID, False, False), # program (readonly) + AccountMeta(params.coin_creator_vault_ata, False, True), # coin_creator_vault_ata (writable) + AccountMeta(params.coin_creator_vault_authority, False, False), # coin_creator_vault_authority (readonly) + ] + + # Add volume accumulator accounts for WSOL/USDC pools + if quote_is_wsol_or_usdc: + accounts.append(AccountMeta(GLOBAL_VOLUME_ACCUMULATOR, False, True)) + uva = get_user_volume_accumulator_pda(payer) + accounts.append(AccountMeta(uva, False, True)) + + # Add fee accounts + accounts.append(AccountMeta(FEE_CONFIG, False, False)) + accounts.append(AccountMeta(FEE_PROGRAM, False, False)) + + # Add cashback WSOL ATA + if params.is_cashback_coin: + wsol_ata = get_user_volume_accumulator_wsol_ata(payer) + accounts.append(AccountMeta(wsol_ata, False, True)) + + # Add pool v2 PDA + pool_v2 = get_pool_v2_pda(params.base_mint) + accounts.append(AccountMeta(pool_v2, False, False)) + + # Build instruction data + track_volume = bytes([1, 1]) if params.is_cashback_coin else bytes([1, 0]) + + if quote_is_wsol_or_usdc: + if use_exact_quote_amount: + min_base_out = calculate_with_slippage_sell(token_amount, slippage_bps) + data = BUY_EXACT_QUOTE_IN_DISCRIMINATOR + struct.pack(" List[Instruction]: + """ + Build PumpSwap sell instructions. + + Args: + payer: The wallet paying for the swap + input_mint: The token mint to sell (base_mint) + input_amount: Amount of tokens to sell + params: PumpSwap protocol parameters + slippage_bps: Slippage tolerance in basis points + create_output_ata: Whether to create WSOL ATA for receiving SOL + close_output_ata: Whether to close WSOL ATA after swap + close_input_ata: Whether to close token ATA after swap + fixed_output_amount: If set, use this as exact output amount + + Returns: + List of instructions for the sell operation + """ + if input_amount == 0: + raise ValueError("Amount cannot be zero") + + instructions = [] + + # Validate pool contains WSOL or USDC + if not params.is_wsol and not params.is_usdc: + raise ValueError("Pool must contain WSOL or USDC") + + # Determine if quote is WSOL/USDC + quote_is_wsol_or_usdc = ( + params.quote_mint == WSOL_TOKEN_ACCOUNT or params.quote_mint == USDC_TOKEN_ACCOUNT + ) + + # Get creator + creator = Pubkey.from_string("11111111111111111111111111111111") + if params.coin_creator_vault_authority != DEFAULT_COIN_CREATOR_VAULT_AUTHORITY: + creator = params.coin_creator_vault_authority + + # Calculate amounts + if quote_is_wsol_or_usdc: + result = sell_base_input_internal( + input_amount, + slippage_bps, + params.pool_base_token_reserves, + params.pool_quote_token_reserves, + creator, + ) + token_amount, sol_amount = input_amount, result[0] + else: + result = buy_quote_input_internal( + input_amount, + slippage_bps, + params.pool_base_token_reserves, + params.pool_quote_token_reserves, + creator, + ) + token_amount, sol_amount = result[1], result[0] + + if fixed_output_amount is not None: + sol_amount = fixed_output_amount + + # Get user token accounts + user_base_token_account = get_associated_token_address( + payer, params.base_mint, params.base_token_program + ) + user_quote_token_account = get_associated_token_address( + payer, params.quote_mint, params.quote_token_program + ) + + # Determine fee recipient + if params.is_mayhem_mode: + fee_recipient, fee_recipient_meta = get_mayhem_fee_recipient_random() + else: + fee_recipient = FEE_RECIPIENT + fee_recipient_meta = AccountMeta(FEE_RECIPIENT, False, False) + + # Get fee recipient ATA + if params.is_mayhem_mode: + fee_recipient_ata_addr = fee_recipient_ata(fee_recipient, WSOL_TOKEN_ACCOUNT) + else: + fee_recipient_ata_addr = fee_recipient_ata(fee_recipient, params.quote_mint) + + # Create WSOL ATA if needed + if create_output_ata and params.is_wsol: + instructions.append( + create_associated_token_account_idempotent_instruction( + payer, payer, WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM + ) + ) + + # Build accounts list + accounts = [ + AccountMeta(params.pool, False, True), # pool_id (writable) + AccountMeta(payer, True, True), # user (signer, writable) + AccountMeta(GLOBAL_ACCOUNT, False, False), # global (readonly) + AccountMeta(params.base_mint, False, False), # base_mint (readonly) + AccountMeta(params.quote_mint, False, False), # quote_mint (readonly) + AccountMeta(user_base_token_account, False, True), # user_base_token_account (writable) + AccountMeta(user_quote_token_account, False, True), # user_quote_token_account (writable) + AccountMeta(params.pool_base_token_account, False, True), # pool_base_token_account (writable) + AccountMeta(params.pool_quote_token_account, False, True), # pool_quote_token_account (writable) + fee_recipient_meta, # fee_recipient (readonly) + AccountMeta(fee_recipient_ata_addr, False, True), # fee_recipient_ata (writable) + AccountMeta(params.base_token_program, False, False), # base_token_program (readonly) + AccountMeta(params.quote_token_program, False, False), # quote_token_program (readonly) + AccountMeta(SYSTEM_PROGRAM, False, False), # system_program (readonly) + AccountMeta(ASSOCIATED_TOKEN_PROGRAM, False, False), # associated_token_program (readonly) + AccountMeta(EVENT_AUTHORITY, False, False), # event_authority (readonly) + AccountMeta(PUMPSWAP_PROGRAM_ID, False, False), # program (readonly) + AccountMeta(params.coin_creator_vault_ata, False, True), # coin_creator_vault_ata (writable) + AccountMeta(params.coin_creator_vault_authority, False, False), # coin_creator_vault_authority (readonly) + ] + + # Add volume accumulator accounts for non-WSOL/USDC pools + if not quote_is_wsol_or_usdc: + accounts.append(AccountMeta(GLOBAL_VOLUME_ACCUMULATOR, False, True)) + uva = get_user_volume_accumulator_pda(payer) + accounts.append(AccountMeta(uva, False, True)) + + # Add fee accounts + accounts.append(AccountMeta(FEE_CONFIG, False, False)) + accounts.append(AccountMeta(FEE_PROGRAM, False, False)) + + # Add cashback accounts for sell + if params.is_cashback_coin: + quote_ata = get_user_volume_accumulator_quote_ata( + payer, params.quote_mint, params.quote_token_program + ) + accumulator = get_user_volume_accumulator_pda(payer) + accounts.append(AccountMeta(quote_ata, False, True)) + accounts.append(AccountMeta(accumulator, False, True)) + + # Add pool v2 PDA + pool_v2 = get_pool_v2_pda(params.base_mint) + accounts.append(AccountMeta(pool_v2, False, False)) + + # Build instruction data + if quote_is_wsol_or_usdc: + data = SELL_DISCRIMINATOR + struct.pack(" Instruction: + """ + Build instruction to claim cashback for PumpSwap AMM. + Transfers WSOL from UserVolumeAccumulator's WSOL ATA to user's WSOL ATA. + """ + user_volume_accumulator = get_user_volume_accumulator_pda(payer) + user_volume_accumulator_wsol_ata = get_user_volume_accumulator_wsol_ata(payer) + user_wsol_ata = get_associated_token_address(payer, quote_mint, quote_token_program) + + accounts = [ + AccountMeta(payer, True, True), # user (signer, writable) + AccountMeta(user_volume_accumulator, False, True), # user_volume_accumulator (writable) + AccountMeta(quote_mint, False, False), # quote_mint (readonly) + AccountMeta(quote_token_program, False, False), # quote_token_program (readonly) + AccountMeta(user_volume_accumulator_wsol_ata, False, True), # user_volume_accumulator_wsol_token_account (writable) + AccountMeta(user_wsol_ata, False, True), # user_wsol_token_account (writable) + AccountMeta(SYSTEM_PROGRAM, False, False), # system_program + AccountMeta(EVENT_AUTHORITY, False, False), # event_authority + AccountMeta(PUMPSWAP_PROGRAM_ID, False, False), # program + ] + + return Instruction(PUMPSWAP_PROGRAM_ID, CLAIM_CASHBACK_DISCRIMINATOR, accounts) + + +# ============================================ +# Exports +# ============================================ + +__all__ = [ + # Program IDs and Constants + "PUMPSWAP_PROGRAM_ID", + "FEE_RECIPIENT", + "GLOBAL_ACCOUNT", + "EVENT_AUTHORITY", + "ASSOCIATED_TOKEN_PROGRAM", + "PROTOCOL_FEE_RECIPIENT", + "PUMP_PROGRAM_ID", + "FEE_PROGRAM", + "GLOBAL_VOLUME_ACCUMULATOR", + "FEE_CONFIG", + "DEFAULT_COIN_CREATOR_VAULT_AUTHORITY", + "MAYHEM_FEE_RECIPIENTS", + "LP_FEE_BASIS_POINTS", + "PROTOCOL_FEE_BASIS_POINTS", + "COIN_CREATOR_FEE_BASIS_POINTS", + # Discriminators + "BUY_DISCRIMINATOR", + "BUY_EXACT_QUOTE_IN_DISCRIMINATOR", + "SELL_DISCRIMINATOR", + "CLAIM_CASHBACK_DISCRIMINATOR", + # PDA Functions + "get_pool_v2_pda", + "get_pump_pool_authority_pda", + "get_canonical_pool_pda", + "get_user_volume_accumulator_pda", + "get_user_volume_accumulator_wsol_ata", + "get_user_volume_accumulator_quote_ata", + "coin_creator_vault_authority", + "coin_creator_vault_ata", + "fee_recipient_ata", + "get_mayhem_fee_recipient_random", + # Params + "PumpSwapParams", + # Calculation Functions + "buy_quote_input_internal", + "sell_base_input_internal", + # Instruction Builders + "build_buy_instructions", + "build_sell_instructions", + "claim_cashback_pumpswap_instruction", +] diff --git a/sol-trade-sdk-python/src/instruction/raydium_amm_v4_builder.py b/sol-trade-sdk-python/src/instruction/raydium_amm_v4_builder.py new file mode 100644 index 0000000..20c2ef3 --- /dev/null +++ b/sol-trade-sdk-python/src/instruction/raydium_amm_v4_builder.py @@ -0,0 +1,374 @@ +""" +Raydium AMM V4 instruction builder for Solana trading SDK. +Production-grade implementation with all constants, discriminators, and PDA derivation functions. +""" + +from typing import List, Optional +from dataclasses import dataclass +from solders.pubkey import Pubkey +from solders.instruction import Instruction, AccountMeta +import struct + +from .common import ( + TOKEN_PROGRAM, + WSOL_TOKEN_ACCOUNT, + USDC_TOKEN_ACCOUNT, + DEFAULT_SLIPPAGE, + get_associated_token_address, + create_associated_token_account_idempotent_instruction, + handle_wsol, + close_wsol, + close_token_account_instruction, + calculate_with_slippage_sell, +) + +# ============================================ +# Raydium AMM V4 Program ID +# ============================================ + +RAYDIUM_AMM_V4_PROGRAM_ID: Pubkey = Pubkey.from_string("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8") + +# ============================================ +# Raydium AMM V4 Constants +# ============================================ + +# Authority +AUTHORITY: Pubkey = Pubkey.from_string("5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1") + +# Fee Rates +TRADE_FEE_NUMERATOR: int = 25 +TRADE_FEE_DENOMINATOR: int = 10000 +SWAP_FEE_NUMERATOR: int = 25 +SWAP_FEE_DENOMINATOR: int = 10000 + +# ============================================ +# Instruction Discriminators +# ============================================ + +# Note: Raydium AMM V4 uses single-byte discriminators +SWAP_BASE_IN_DISCRIMINATOR: bytes = bytes([9]) +SWAP_BASE_OUT_DISCRIMINATOR: bytes = bytes([11]) + +# ============================================ +# Seeds +# ============================================ + +POOL_SEED = b"pool" + + +# ============================================ +# Raydium AMM V4 Parameters Dataclass +# ============================================ + +@dataclass +class RaydiumAmmV4Params: + """Parameters for Raydium AMM V4 protocol trading.""" + amm: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + coin_mint: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + pc_mint: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + token_coin: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + token_pc: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + coin_reserve: int = 0 + pc_reserve: int = 0 + + @property + def is_wsol(self) -> bool: + """Check if the pool contains WSOL.""" + return self.coin_mint == WSOL_TOKEN_ACCOUNT or self.pc_mint == WSOL_TOKEN_ACCOUNT + + @property + def is_usdc(self) -> bool: + """Check if the pool contains USDC.""" + return self.coin_mint == USDC_TOKEN_ACCOUNT or self.pc_mint == USDC_TOKEN_ACCOUNT + + +# ============================================ +# Raydium AMM V4 Calculation Functions +# ============================================ + +def compute_swap_amount( + coin_reserve: int, + pc_reserve: int, + is_coin_in: bool, + amount_in: int, + slippage_bps: int, +) -> tuple: + """ + Compute swap output amount for Raydium AMM V4. + Returns (amount_out, min_amount_out). + """ + # Apply trade fee (0.25%) + amount_in_after_fee = amount_in - (amount_in * TRADE_FEE_NUMERATOR) // TRADE_FEE_DENOMINATOR + + if is_coin_in: + # Coin -> PC + # out = (amount_in * pc_reserve) / (coin_reserve + amount_in) + numerator = amount_in_after_fee * pc_reserve + denominator = coin_reserve + amount_in_after_fee + amount_out = numerator // denominator + else: + # PC -> Coin + # out = (amount_in * coin_reserve) / (pc_reserve + amount_in) + numerator = amount_in_after_fee * coin_reserve + denominator = pc_reserve + amount_in_after_fee + amount_out = numerator // denominator + + # Apply slippage + min_amount_out = calculate_with_slippage_sell(amount_out, slippage_bps) + + return (amount_out, min_amount_out) + + +# ============================================ +# Build Buy Instructions +# ============================================ + +def build_buy_instructions( + payer: Pubkey, + output_mint: Pubkey, + input_amount: int, + params: RaydiumAmmV4Params, + slippage_bps: int = DEFAULT_SLIPPAGE, + create_input_ata: bool = True, + create_output_ata: bool = True, + close_input_ata: bool = False, + fixed_output_amount: Optional[int] = None, +) -> List[Instruction]: + """ + Build Raydium AMM V4 buy instructions. + + Args: + payer: The wallet paying for the swap + output_mint: The token mint to buy + input_amount: Amount of SOL/USDC to spend + params: Raydium AMM V4 protocol parameters + slippage_bps: Slippage tolerance in basis points + create_input_ata: Whether to create WSOL ATA if needed + create_output_ata: Whether to create output token ATA if needed + close_input_ata: Whether to close WSOL ATA after swap + fixed_output_amount: If set, use this as exact output amount + + Returns: + List of instructions for the buy operation + """ + if input_amount == 0: + raise ValueError("Amount cannot be zero") + + instructions = [] + + # Validate pool contains WSOL or USDC + if not params.is_wsol and not params.is_usdc: + raise ValueError("Pool must contain WSOL or USDC") + + # Determine if coin is input (WSOL/USDC) + is_coin_in = params.coin_mint == WSOL_TOKEN_ACCOUNT or params.coin_mint == USDC_TOKEN_ACCOUNT + + # Calculate swap amount + _, min_amount_out = compute_swap_amount( + params.coin_reserve, + params.pc_reserve, + is_coin_in, + input_amount, + slippage_bps, + ) + + if fixed_output_amount is not None: + minimum_amount_out = fixed_output_amount + else: + minimum_amount_out = min_amount_out + + # Determine input mint (WSOL or USDC) + input_mint = WSOL_TOKEN_ACCOUNT if params.is_wsol else USDC_TOKEN_ACCOUNT + + # Get user token accounts + user_source_token_account = get_associated_token_address(payer, input_mint, TOKEN_PROGRAM) + user_destination_token_account = get_associated_token_address(payer, output_mint, TOKEN_PROGRAM) + + # Handle WSOL if needed + if create_input_ata and params.is_wsol: + instructions.extend(handle_wsol(payer, input_amount)) + + # Create output ATA if needed + if create_output_ata: + instructions.append( + create_associated_token_account_idempotent_instruction( + payer, payer, output_mint, TOKEN_PROGRAM + ) + ) + + # Build instruction data (1 byte discriminator + 8 bytes amount_in + 8 bytes min_out) + data = SWAP_BASE_IN_DISCRIMINATOR + struct.pack(" List[Instruction]: + """ + Build Raydium AMM V4 sell instructions. + + Args: + payer: The wallet paying for the swap + input_mint: The token mint to sell + input_amount: Amount of tokens to sell + params: Raydium AMM V4 protocol parameters + slippage_bps: Slippage tolerance in basis points + create_output_ata: Whether to create WSOL ATA for receiving SOL + close_output_ata: Whether to close WSOL ATA after swap + close_input_ata: Whether to close token ATA after swap + fixed_output_amount: If set, use this as exact output amount + + Returns: + List of instructions for the sell operation + """ + if input_amount == 0: + raise ValueError("Amount cannot be zero") + + instructions = [] + + # Validate pool contains WSOL or USDC + if not params.is_wsol and not params.is_usdc: + raise ValueError("Pool must contain WSOL or USDC") + + # Determine if pc is output (WSOL/USDC) + # For sell: is_base_in = True means we're selling PC to get Coin + # is_base_in = False means we're selling Coin to get PC + is_pc_out = params.pc_mint == WSOL_TOKEN_ACCOUNT or params.pc_mint == USDC_TOKEN_ACCOUNT + + # Calculate swap amount (is_pc_out reversed because we're selling to get WSOL/USDC) + _, min_amount_out = compute_swap_amount( + params.coin_reserve, + params.pc_reserve, + not is_pc_out, # Reversed for sell + input_amount, + slippage_bps, + ) + + if fixed_output_amount is not None: + minimum_amount_out = fixed_output_amount + else: + minimum_amount_out = min_amount_out + + # Determine output mint (WSOL or USDC) + output_mint = WSOL_TOKEN_ACCOUNT if params.is_wsol else USDC_TOKEN_ACCOUNT + + # Get user token accounts + user_source_token_account = get_associated_token_address(payer, input_mint, TOKEN_PROGRAM) + user_destination_token_account = get_associated_token_address(payer, output_mint, TOKEN_PROGRAM) + + # Create WSOL ATA if needed for receiving SOL + if create_output_ata and params.is_wsol: + instructions.append( + create_associated_token_account_idempotent_instruction( + payer, payer, WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM + ) + ) + + # Build instruction data (1 byte discriminator + 8 bytes amount_in + 8 bytes min_out) + data = SWAP_BASE_IN_DISCRIMINATOR + struct.pack(" Pubkey: + """ + Derive the pool PDA for a given amm_config and two mints. + Seeds: ["pool", amm_config, mint1, mint2] + """ + seeds = [POOL_SEED, bytes(amm_config), bytes(mint1), bytes(mint2)] + (pda, _) = Pubkey.find_program_address(seeds, RAYDIUM_CPMM_PROGRAM_ID) + return pda + + +def get_vault_pda(pool_state: Pubkey, mint: Pubkey) -> Pubkey: + """ + Derive the vault PDA for a given pool and mint. + Seeds: ["pool_vault", pool_state, mint] + """ + seeds = [POOL_VAULT_SEED, bytes(pool_state), bytes(mint)] + (pda, _) = Pubkey.find_program_address(seeds, RAYDIUM_CPMM_PROGRAM_ID) + return pda + + +def get_observation_state_pda(pool_state: Pubkey) -> Pubkey: + """ + Derive the observation state PDA for a given pool. + Seeds: ["observation", pool_state] + """ + seeds = [OBSERVATION_STATE_SEED, bytes(pool_state)] + (pda, _) = Pubkey.find_program_address(seeds, RAYDIUM_CPMM_PROGRAM_ID) + return pda + + +# ============================================ +# Raydium CPMM Parameters Dataclass +# ============================================ + +@dataclass +class RaydiumCpmmParams: + """Parameters for Raydium CPMM protocol trading.""" + pool_state: Optional[Pubkey] = None # If None, will derive from mints and amm_config + amm_config: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + base_mint: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + quote_mint: Pubkey = Pubkey.from_string("11111111111111111111111111111111") + base_reserve: int = 0 + quote_reserve: int = 0 + base_vault: Optional[Pubkey] = None + quote_vault: Optional[Pubkey] = None + base_token_program: Pubkey = TOKEN_PROGRAM + quote_token_program: Pubkey = TOKEN_PROGRAM + observation_state: Optional[Pubkey] = None + + @property + def is_wsol(self) -> bool: + """Check if the pool contains WSOL.""" + return self.base_mint == WSOL_TOKEN_ACCOUNT or self.quote_mint == WSOL_TOKEN_ACCOUNT + + @property + def is_usdc(self) -> bool: + """Check if the pool contains USDC.""" + return self.base_mint == USDC_TOKEN_ACCOUNT or self.quote_mint == USDC_TOKEN_ACCOUNT + + +# ============================================ +# Raydium CPMM Calculation Functions +# ============================================ + +def compute_swap_amount( + base_reserve: int, + quote_reserve: int, + is_base_in: bool, + amount_in: int, + slippage_bps: int, +) -> tuple: + """ + Compute swap output amount for Raydium CPMM. + Returns (amount_out, min_amount_out). + """ + # Apply trade fee (0.25%) + fee_rate = TRADE_FEE_RATE / FEE_RATE_DENOMINATOR_VALUE + amount_in_after_fee = int(amount_in * (1 - fee_rate)) + + if is_base_in: + # Base -> Quote + # out = (amount_in * quote_reserve) / (base_reserve + amount_in) + numerator = amount_in_after_fee * quote_reserve + denominator = base_reserve + amount_in_after_fee + amount_out = numerator // denominator + else: + # Quote -> Base + # out = (amount_in * base_reserve) / (quote_reserve + amount_in) + numerator = amount_in_after_fee * base_reserve + denominator = quote_reserve + amount_in_after_fee + amount_out = numerator // denominator + + # Apply slippage + min_amount_out = calculate_with_slippage_sell(amount_out, slippage_bps) + + return (amount_out, min_amount_out) + + +# ============================================ +# Build Buy Instructions +# ============================================ + +def build_buy_instructions( + payer: Pubkey, + output_mint: Pubkey, + input_amount: int, + params: RaydiumCpmmParams, + slippage_bps: int = DEFAULT_SLIPPAGE, + create_input_ata: bool = True, + create_output_ata: bool = True, + close_input_ata: bool = False, + fixed_output_amount: Optional[int] = None, +) -> List[Instruction]: + """ + Build Raydium CPMM buy instructions. + + Args: + payer: The wallet paying for the swap + output_mint: The token mint to buy + input_amount: Amount of SOL/USDC to spend + params: Raydium CPMM protocol parameters + slippage_bps: Slippage tolerance in basis points + create_input_ata: Whether to create WSOL ATA if needed + create_output_ata: Whether to create output token ATA if needed + close_input_ata: Whether to close WSOL ATA after swap + fixed_output_amount: If set, use this as exact output amount + + Returns: + List of instructions for the buy operation + """ + if input_amount == 0: + raise ValueError("Amount cannot be zero") + + instructions = [] + + # Validate pool contains WSOL or USDC + if not params.is_wsol and not params.is_usdc: + raise ValueError("Pool must contain WSOL or USDC") + + # Get pool state + pool_state = params.pool_state + if pool_state is None: + pool_state = get_pool_pda(params.amm_config, params.base_mint, params.quote_mint) + + # Determine if base is input (WSOL/USDC) + is_base_in = params.base_mint == WSOL_TOKEN_ACCOUNT or params.base_mint == USDC_TOKEN_ACCOUNT + + # Get output token program + mint_token_program = params.quote_token_program if is_base_in else params.base_token_program + + # Calculate swap amount + _, min_amount_out = compute_swap_amount( + params.base_reserve, + params.quote_reserve, + is_base_in, + input_amount, + slippage_bps, + ) + + if fixed_output_amount is not None: + minimum_amount_out = fixed_output_amount + else: + minimum_amount_out = min_amount_out + + # Determine input mint (WSOL or USDC) + input_mint = WSOL_TOKEN_ACCOUNT if params.is_wsol else USDC_TOKEN_ACCOUNT + + # Get user token accounts + input_token_account = get_associated_token_address(payer, input_mint, TOKEN_PROGRAM) + output_token_account = get_associated_token_address(payer, output_mint, mint_token_program) + + # Get vaults + input_vault = params.base_vault if is_base_in else params.quote_vault + if input_vault is None: + input_vault = get_vault_pda(pool_state, input_mint) + + output_vault = params.quote_vault if is_base_in else params.base_vault + if output_vault is None: + output_vault = get_vault_pda(pool_state, output_mint) + + # Get observation state + observation_state = params.observation_state + if observation_state is None: + observation_state = get_observation_state_pda(pool_state) + + # Handle WSOL if needed + if create_input_ata and params.is_wsol: + instructions.extend(handle_wsol(payer, input_amount)) + + # Create output ATA if needed + if create_output_ata: + instructions.append( + create_associated_token_account_idempotent_instruction( + payer, payer, output_mint, mint_token_program + ) + ) + + # Build instruction data + data = SWAP_BASE_IN_DISCRIMINATOR + struct.pack(" List[Instruction]: + """ + Build Raydium CPMM sell instructions. + + Args: + payer: The wallet paying for the swap + input_mint: The token mint to sell + input_amount: Amount of tokens to sell + params: Raydium CPMM protocol parameters + slippage_bps: Slippage tolerance in basis points + create_output_ata: Whether to create WSOL ATA for receiving SOL + close_output_ata: Whether to close WSOL ATA after swap + close_input_ata: Whether to close token ATA after swap + fixed_output_amount: If set, use this as exact output amount + + Returns: + List of instructions for the sell operation + """ + if input_amount == 0: + raise ValueError("Amount cannot be zero") + + instructions = [] + + # Validate pool contains WSOL or USDC + if not params.is_wsol and not params.is_usdc: + raise ValueError("Pool must contain WSOL or USDC") + + # Get pool state + pool_state = params.pool_state + if pool_state is None: + pool_state = get_pool_pda(params.amm_config, params.base_mint, params.quote_mint) + + # Determine if quote is output (WSOL/USDC) + is_quote_out = params.quote_mint == WSOL_TOKEN_ACCOUNT or params.quote_mint == USDC_TOKEN_ACCOUNT + + # Get input token program + mint_token_program = params.base_token_program if is_quote_out else params.quote_token_program + + # Calculate swap amount + _, min_amount_out = compute_swap_amount( + params.base_reserve, + params.quote_reserve, + is_quote_out, # Swap direction is reversed for sell + input_amount, + slippage_bps, + ) + + if fixed_output_amount is not None: + minimum_amount_out = fixed_output_amount + else: + minimum_amount_out = min_amount_out + + # Determine output mint (WSOL or USDC) + output_mint = WSOL_TOKEN_ACCOUNT if params.is_wsol else USDC_TOKEN_ACCOUNT + + # Get user token accounts + output_token_account = get_associated_token_address(payer, output_mint, TOKEN_PROGRAM) + input_token_account = get_associated_token_address(payer, input_mint, mint_token_program) + + # Get vaults + output_vault = params.quote_vault if is_quote_out else params.base_vault + if output_vault is None: + output_vault = get_vault_pda(pool_state, output_mint) + + input_vault = params.base_vault if is_quote_out else params.quote_vault + if input_vault is None: + input_vault = get_vault_pda(pool_state, input_mint) + + # Get observation state + observation_state = params.observation_state + if observation_state is None: + observation_state = get_observation_state_pda(pool_state) + + # Create WSOL ATA if needed for receiving SOL + if create_output_ata and params.is_wsol: + instructions.append( + create_associated_token_account_idempotent_instruction( + payer, payer, WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM + ) + ) + + # Build instruction data + data = SWAP_BASE_IN_DISCRIMINATOR + struct.pack(", + commitment: CommitmentConfig, + ) -> TradeConfigBuilder { + TradeConfigBuilder::new(rpc_url, swqos_configs, commitment) + } + + /// Shortcut: create a TradeConfig with all defaults. Equivalent to `builder(...).build()`. pub fn new( rpc_url: String, swqos_configs: Vec, commitment: CommitmentConfig, ) -> Self { - if crate::common::sdk_log::sdk_log_enabled() { - println!("🔧 TradeConfig create_wsol_ata_on_startup default: true"); - println!("🔧 TradeConfig use_seed_optimize default: true"); - } + Self::builder(rpc_url, swqos_configs, commitment).build() + } +} + +/// Builder for [`TradeConfig`]. Created via [`TradeConfig::builder`]. +/// +/// All fields are optional and pre-filled with sensible defaults. +/// Call `.build()` to produce the final [`TradeConfig`]. +#[derive(Debug, Clone)] +pub struct TradeConfigBuilder { + rpc_url: String, + swqos_configs: Vec, + commitment: CommitmentConfig, + create_wsol_ata_on_startup: bool, + use_seed_optimize: bool, + log_enabled: bool, + check_min_tip: bool, + swqos_cores_from_end: bool, + mev_protection: bool, +} + +impl TradeConfigBuilder { + fn new(rpc_url: String, swqos_configs: Vec, commitment: CommitmentConfig) -> Self { Self { rpc_url, swqos_configs, commitment, - create_wsol_ata_on_startup: true, // default: check and create on startup - use_seed_optimize: true, // default: use seed optimization - log_enabled: true, // default: enable all SDK logs - check_min_tip: false, // default: skip min tip check to reduce latency + create_wsol_ata_on_startup: true, + use_seed_optimize: true, + log_enabled: true, + check_min_tip: false, swqos_cores_from_end: false, + mev_protection: false, } } - /// Create a TradeConfig with custom WSOL ATA settings - pub fn with_wsol_ata_config( - mut self, - create_wsol_ata_on_startup: bool, - use_seed_optimize: bool, - ) -> Self { - self.create_wsol_ata_on_startup = create_wsol_ata_on_startup; - self.use_seed_optimize = use_seed_optimize; + /// Check and create WSOL ATA on SDK initialization. Default: `true`. + pub fn create_wsol_ata_on_startup(mut self, v: bool) -> Self { + self.create_wsol_ata_on_startup = v; self } - /// Set whether to check minimum tip per SWQOS (filter out configs below min). Default false for lower latency. - pub fn with_check_min_tip(mut self, check_min_tip: bool) -> Self { - self.check_min_tip = check_min_tip; + /// Enable seed optimization for all ATA operations. Default: `true`. + pub fn use_seed_optimize(mut self, v: bool) -> Self { + self.use_seed_optimize = v; self } - /// Use the *last* N cores for SWQOS (instead of the first N). Call this when the main thread or tokio workers use low-numbered cores to avoid binding SWQOS to busy cores. Default false. - pub fn with_swqos_cores_from_end(mut self, from_end: bool) -> Self { - self.swqos_cores_from_end = from_end; + /// Enable SDK logs (timing, SWQOS submit/confirm, WSOL, blacklist, etc.). Default: `true`. + pub fn log_enabled(mut self, v: bool) -> Self { + self.log_enabled = v; self } + + /// Filter out SWQOS providers whose tip is below their minimum requirement. + /// Adds a small check on the hot path; disable for lowest latency. Default: `false`. + pub fn check_min_tip(mut self, v: bool) -> Self { + self.check_min_tip = v; + self + } + + /// Bind SWQOS sender threads to the *last* N CPU cores instead of the first N. + /// Useful when main thread / tokio workers occupy low-numbered cores. Default: `false`. + pub fn swqos_cores_from_end(mut self, v: bool) -> Self { + self.swqos_cores_from_end = v; + self + } + + /// Enable global MEV protection. When `true`: + /// - **Astralane QUIC** uses port `9000` (MEV-protected endpoint) + /// - **BlockRazor** uses `mode=sandwichMitigation` (skips blacklisted Leader slots) + /// + /// May reduce landing speed. Default: `false`. + pub fn mev_protection(mut self, v: bool) -> Self { + self.mev_protection = v; + self + } + + /// Consume the builder and produce a [`TradeConfig`]. + pub fn build(self) -> TradeConfig { + TradeConfig { + rpc_url: self.rpc_url, + swqos_configs: self.swqos_configs, + commitment: self.commitment, + create_wsol_ata_on_startup: self.create_wsol_ata_on_startup, + use_seed_optimize: self.use_seed_optimize, + log_enabled: self.log_enabled, + check_min_tip: self.check_min_tip, + swqos_cores_from_end: self.swqos_cores_from_end, + mev_protection: self.mev_protection, + } + } } pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient; diff --git a/src/constants/swqos.rs b/src/constants/swqos.rs index 3473c24..a71b44f 100755 --- a/src/constants/swqos.rs +++ b/src/constants/swqos.rs @@ -323,6 +323,19 @@ pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 8] = [ "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_STELLIUM: [&str; 8] = [ "http://ewr1.flashrpc.com", "http://fra1.flashrpc.com", diff --git a/src/lib.rs b/src/lib.rs index c3f0bfb..e7e9b88 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -152,6 +152,7 @@ impl TradingInfrastructure { config.rpc_url.clone(), config.commitment.clone(), swqos.clone(), + config.mev_protection, ), ) .await diff --git a/src/swqos/astralane_quic.rs b/src/swqos/astralane_quic.rs index c5f080e..2f7c956 100644 --- a/src/swqos/astralane_quic.rs +++ b/src/swqos/astralane_quic.rs @@ -52,32 +52,32 @@ impl AstralaneQuicClient { fn astralane_quic_ip_candidates(host: &str, port: u16) -> Vec { // Official recommended direct-IP list (faster/more stable than DNS-only for QUIC). // We intentionally avoid fr2/ams2 per prior guidance. + // Both port 7000 (standard) and port 9000 (MEV-protected) use the same IPs. match host { "fr.gateway.astralane.io" => vec![ - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(185, 191, 117, 97)), 7000), - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(45, 139, 132, 160)), 7000), + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(185, 191, 117, 97)), port), + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(45, 139, 132, 160)), port), ], "ny.gateway.astralane.io" => { - vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 45, 19)), 7000)] + vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 45, 19)), port)] } "ams.gateway.astralane.io" => vec![ - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 43, 43)), 7000), - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 186, 73)), 7000), + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 43, 43)), port), + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 186, 73)), port), ], "la.gateway.astralane.io" => { - vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(74, 118, 142, 151)), 7000)] + vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(74, 118, 142, 151)), port)] } "lim.gateway.astralane.io" => { - vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(162, 19, 222, 232)), 7000)] + vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(162, 19, 222, 232)), port)] } "sg.gateway.astralane.io" => { - vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(67, 209, 54, 176)), 7000)] + vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(67, 209, 54, 176)), port)] } "lit.gateway.astralane.io" => { - vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 97, 47)), 7000)] + vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 97, 47)), port)] } _ => { - let _ = port; // keep signature consistent; fall back to DNS below. Vec::new() } } diff --git a/src/swqos/blockrazor.rs b/src/swqos/blockrazor.rs index 87d4a59..16f4a44 100644 --- a/src/swqos/blockrazor.rs +++ b/src/swqos/blockrazor.rs @@ -89,6 +89,8 @@ pub enum BlockRazorBackend { grpc_client: Arc>, ping_handle: Arc>>>, stop_ping: Arc, + /// When true, gRPC send_transaction sets revert_protection=true for MEV protection. + mev_protection: bool, }, Http { endpoint: String, @@ -96,6 +98,8 @@ pub enum BlockRazorBackend { http_client: Client, ping_handle: Arc>>>, stop_ping: Arc, + /// When true, HTTP request adds revertProtection=true query param for MEV protection. + mev_protection: bool, }, } @@ -144,10 +148,10 @@ impl SwqosClientTrait for BlockRazorClient { impl BlockRazorClient { pub async fn new(rpc_url: String, endpoint: String, auth_token: String) -> Result { // 默认使用 HTTP 模式,避免 gRPC FRAME_SIZE_ERROR - Ok(Self::new_http(rpc_url, endpoint, auth_token)) + Ok(Self::new_http(rpc_url, endpoint, auth_token, false)) } - pub async fn new_grpc(rpc_url: String, endpoint: String, auth_token: String) -> Result { + pub async fn new_grpc(rpc_url: String, endpoint: String, auth_token: String, mev_protection: bool) -> Result { let rpc_client = SolanaRpcClient::new(rpc_url); // 配置 Channel,增加连接超时 @@ -173,6 +177,7 @@ impl BlockRazorClient { grpc_client, ping_handle, stop_ping, + mev_protection, }, }; @@ -184,7 +189,7 @@ impl BlockRazorClient { Ok(client) } - pub fn new_http(rpc_url: String, endpoint: String, auth_token: String) -> Self { + pub fn new_http(rpc_url: String, endpoint: String, auth_token: String, mev_protection: bool) -> Self { let rpc_client = SolanaRpcClient::new(rpc_url); let http_client = default_http_client_builder().user_agent("").build().unwrap(); let ping_handle = Arc::new(tokio::sync::Mutex::new(None)); @@ -198,6 +203,7 @@ impl BlockRazorClient { http_client, ping_handle, stop_ping, + mev_protection, }, }; @@ -217,6 +223,7 @@ impl BlockRazorClient { stop_ping, endpoint, auth_token, + .. } => { let grpc_client = grpc_client.clone(); let ping_handle = ping_handle.clone(); @@ -293,6 +300,7 @@ impl BlockRazorClient { http_client, ping_handle, stop_ping, + .. } => { let endpoint = endpoint.clone(); let auth_token = auth_token.clone(); @@ -374,6 +382,7 @@ impl BlockRazorClient { match &self.backend { BlockRazorBackend::Grpc { grpc_client, + mev_protection, .. } => { let (content, _signature) = @@ -383,7 +392,9 @@ impl BlockRazorClient { let client = grpc_client.load(); let signature = client.send_transaction( content, - "fast".to_string(), + // mev_protection=true: sandwichMitigation mode skips blacklisted Leader slots (MEV protection). + // revert_protection is unrelated to MEV; keep false. + if *mev_protection { "sandwichMitigation".to_string() } else { "fast".to_string() }, None, false, ).await; @@ -412,14 +423,22 @@ impl BlockRazorClient { endpoint, auth_token, http_client, + mev_protection, .. } => { let (content, _signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?; + let mut 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. + ("mode", if *mev_protection { "sandwichMitigation" } else { "fast" }), + ]; + let response = http_client .post(endpoint) - .query(&[("auth", auth_token.as_str())]) + .query(&query_params) .header("Content-Type", "text/plain") .body(content) .send() diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index 9aae951..6b530a1 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -29,7 +29,8 @@ use anyhow::Result; use crate::{ common::SolanaRpcClient, constants::swqos::{ - SWQOS_ENDPOINTS_ASTRALANE, SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_BLOCKRAZOR, + SWQOS_ENDPOINTS_ASTRALANE, 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, @@ -303,6 +304,7 @@ impl SwqosConfig { region: SwqosRegion, url: Option, transport: Option, + mev_protection: bool, ) -> String { if let Some(custom_url) = url { return custom_url; @@ -329,12 +331,17 @@ impl SwqosConfig { SwqosType::Astralane => { let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic); if use_quic { - SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string() + // 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, url), + _ => Self::get_endpoint(swqos_type, region, None), } } @@ -342,6 +349,7 @@ impl SwqosConfig { rpc_url: String, commitment: CommitmentConfig, swqos_config: SwqosConfig, + mev_protection: bool, ) -> Result> { match swqos_config { SwqosConfig::Jito(auth_token, region, url) => { @@ -398,15 +406,15 @@ impl SwqosConfig { SwqosConfig::BlockRazor(auth_token, region, url, transport) => { // BlockRazor: transport=None 或 transport=Grpc 时使用 gRPC,transport=Http 时使用 HTTP let use_http = transport.map_or(false, |t| t == SwqosTransport::Http); - let endpoint = SwqosConfig::get_endpoint_with_transport(SwqosType::BlockRazor, region, url, transport); + let endpoint = SwqosConfig::get_endpoint_with_transport(SwqosType::BlockRazor, region, url, transport, mev_protection); if use_http { let blockrazor_client = - BlockRazorClient::new_http(rpc_url.clone(), endpoint.to_string(), auth_token); + BlockRazorClient::new_http(rpc_url.clone(), endpoint.to_string(), auth_token, mev_protection); Ok(Arc::new(blockrazor_client)) } else { // 使用 gRPC 模式(默认或用户明确指定了 gRPC) let blockrazor_client = - BlockRazorClient::new_grpc(rpc_url.clone(), endpoint.to_string(), auth_token).await?; + BlockRazorClient::new_grpc(rpc_url.clone(), endpoint.to_string(), auth_token, mev_protection).await?; Ok(Arc::new(blockrazor_client)) } } @@ -414,7 +422,12 @@ impl SwqosConfig { let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic); if use_quic { let quic_endpoint = url.unwrap_or_else(|| { - SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string() + // 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)