Merge pull request #77 from 0xfnzero/feature/pump-cashback

feat: PumpFun & PumpSwap Cashback support
This commit is contained in:
Wood
2026-02-19 00:44:47 +08:00
committed by GitHub
11 changed files with 16352 additions and 5 deletions
+74
View File
@@ -0,0 +1,74 @@
# Pump Cashback 集成说明
本 SDK 已支持 [Pump Cashback Rewards](https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CASHBACK_README.md):在启用 cashback 的币种上交易时,用户可获得手续费返还而非支付给创作者。
## 行为概览
- **Bonding Curve (Pump)**
- **Buy**:无需改指令,若币种启用 cashback 会自动累计。
- **Sell**:当 `bonding_curve.is_cashback_coin == true` 时,SDK 会在指令中追加 `UserVolumeAccumulator` PDAremaining account),用于累计可领取的 cashback。
- **Pump Swap**
- **Buy**:当 `PumpSwapParams.is_cashback_coin == true` 时,会追加 UserVolumeAccumulator 的 **WSOL ATA** 作为 remaining account。
- **Sell**:当 `is_cashback_coin == true` 时,会追加 **WSOL ATA**0th)和 **UserVolumeAccumulator PDA**1st)作为 remaining accounts。
`PumpFunParams` 通过 `from_mint_by_rpc` 拉取 bonding curve 时会解析链上 `is_cashback_coin``PumpSwapParams` 通过 `from_pool_address_by_rpc` / `from_mint_by_rpc` 拉取 pool 时会解析 `is_cashback_coin`
## 领取 Cashback
### 推荐:使用 TradingClient 一键领取
若已有 `TradingClient`(例如用于买卖的同一个客户端),可直接调用以下方法,内部会完成构建交易、签名与发送:
```rust
// 领取 Pump 曲线产生的 Cashback(到账为 native SOL
let sig = client.claim_cashback_pumpfun().await?;
// 领取 PumpSwap 产生的 Cashback(到账为 WSOL,自动确保用户 WSOL ATA 存在)
let sig = client.claim_cashback_pumpswap().await?;
```
- **`claim_cashback_pumpfun()`**:领取 Bonding Curve (Pump) 的返还,到账为钱包 SOL。
- **`claim_cashback_pumpswap()`**:领取 PumpSwap (AMM) 的返还,到账为用户的 WSOL ATA;若用户尚无 WSOL ATA 会先自动创建再领取。
### 仅构建指令(自行组交易时使用)
#### Bonding Curve (Pump)
将 native lamports 从 UserVolumeAccumulator 转到用户钱包:
```rust
use sol_trade_sdk::instruction::pumpfun;
let ix = pumpfun::claim_cashback_pumpfun_instruction(&payer.pubkey());
// 将 ix 放入交易并发送
```
#### Pump Swap (AMM)
将 WSOL 从 UserVolumeAccumulator 的 WSOL ATA 转到用户的 WSOL ATA。**调用前需确保用户 WSOL ATA 已存在**(或使用上面的 `claim_cashback_pumpswap()` 会自动处理):
```rust
use sol_trade_sdk::instruction::pumpswap;
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
use sol_trade_sdk::constants::TOKEN_PROGRAM;
let ix = pumpswap::claim_cashback_pumpswap_instruction(
&payer.pubkey(),
WSOL_TOKEN_ACCOUNT,
TOKEN_PROGRAM,
);
```
## 读取未领取金额
- **Pump (Bonding Curve)**:读 Pump 程序的 `UserVolumeAccumulator` PDA 的 lamports,减去维持账户所需的 rent-exempt 金额,即为未领取 cashbacklamports)。
- **Pump Swap**:读 Pump AMM 程序的 UserVolumeAccumulator 的 **WSOL ATA** 的 token balance,即为未领取 cashbackWSOL 数量)。
PDA 推导(本 SDK 已实现):
- Pump`instruction::utils::pumpfun::get_user_volume_accumulator_pda(user)`
- Pump AMM`instruction::utils::pumpswap::get_user_volume_accumulator_pda(user)`WSOL ATA`instruction::utils::pumpswap::get_user_volume_accumulator_wsol_ata(user)`
## IDL 来源
根目录 `idl/` 下的 `pump.json``pump_amm.json``pump_fees.json` 从 [pump-fun/pump-public-docs](https://github.com/pump-fun/pump-public-docs) 的 `idl` 目录同步,便于与官方 IDL 对照和后续升级。
+7098
View File
File diff suppressed because it is too large Load Diff
+6268
View File
File diff suppressed because it is too large Load Diff
+2761
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -60,6 +60,8 @@ pub struct BondingCurveAccount {
pub creator: Pubkey,
/// Whether this is a mayhem mode token (Token2022)
pub is_mayhem_mode: bool,
/// Whether this coin has cashback enabled (creator fee redirected to users)
pub is_cashback_coin: bool,
}
impl BondingCurveAccount {
@@ -87,6 +89,7 @@ impl BondingCurveAccount {
complete: false,
creator: creator,
is_mayhem_mode: is_mayhem_mode,
is_cashback_coin: false,
}
}
@@ -116,6 +119,7 @@ impl BondingCurveAccount {
complete: false,
creator: creator,
is_mayhem_mode: is_mayhem_mode,
is_cashback_coin: false,
}
}
+27 -2
View File
@@ -263,7 +263,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
global_constants::FEE_RECIPIENT_META
};
let accounts: [AccountMeta; 14] = [
let mut accounts: Vec<AccountMeta> = vec![
global_constants::GLOBAL_ACCOUNT_META,
fee_recipient_meta,
AccountMeta::new_readonly(params.input_mint, false),
@@ -280,10 +280,17 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
accounts::FEE_PROGRAM_META,
];
// Cashback: Bonding Curve Sell expects UserVolumeAccumulator PDA at 0th remaining account (writable)
if bonding_curve.is_cashback_coin {
let user_volume_accumulator =
get_user_volume_accumulator_pda(&params.payer.pubkey()).unwrap();
accounts.push(AccountMeta::new(user_volume_accumulator, false));
}
instructions.push(Instruction::new_with_bytes(
accounts::PUMPFUN,
&sell_data,
accounts.to_vec(),
accounts,
));
// Optional: Close token account
@@ -302,3 +309,21 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
Ok(instructions)
}
}
/// Claim cashback for Bonding Curve (Pump program). Transfers native lamports from UserVolumeAccumulator to user.
pub fn claim_cashback_pumpfun_instruction(payer: &Pubkey) -> Option<Instruction> {
const CLAIM_CASHBACK_DISCRIMINATOR: [u8; 8] = [37, 58, 35, 126, 190, 53, 228, 197];
let user_volume_accumulator = get_user_volume_accumulator_pda(payer)?;
let accounts = vec![
AccountMeta::new(*payer, true), // user (signer, writable)
AccountMeta::new(user_volume_accumulator, false), // user_volume_accumulator (writable, not signer)
crate::constants::SYSTEM_PROGRAM_META,
accounts::EVENT_AUTHORITY_META,
accounts::PUMPFUN_META,
];
Some(Instruction::new_with_bytes(
accounts::PUMPFUN,
&CLAIM_CASHBACK_DISCRIMINATOR,
accounts,
))
}
+53 -2
View File
@@ -1,7 +1,8 @@
use crate::{
constants::trade::trade::DEFAULT_SLIPPAGE,
instruction::utils::pumpswap::{
accounts, fee_recipient_ata, get_user_volume_accumulator_pda, BUY_DISCRIMINATOR,
accounts, fee_recipient_ata, get_user_volume_accumulator_pda,
get_user_volume_accumulator_wsol_ata, BUY_DISCRIMINATOR,
BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
},
trading::{
@@ -183,6 +184,12 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
}
accounts.push(accounts::FEE_CONFIG_META);
accounts.push(accounts::FEE_PROGRAM_META);
// Cashback: remaining_accounts[0] = WSOL ATA of UserVolumeAccumulator (after named accounts per IDL)
if protocol_params.is_cashback_coin {
if let Some(wsol_ata) = get_user_volume_accumulator_wsol_ata(&params.payer.pubkey()) {
accounts.push(AccountMeta::new(wsol_ata, false));
}
}
// Create instruction data
let mut data = [0u8; 24];
@@ -373,9 +380,18 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
false,
));
}
accounts.push(accounts::FEE_CONFIG_META);
accounts.push(accounts::FEE_PROGRAM_META);
// Cashback: remaining_accounts[0] = WSOL ATA of UserVolumeAccumulator, remaining_accounts[1] = UserVolumeAccumulator PDA
if protocol_params.is_cashback_coin {
if let (Some(wsol_ata), Some(accumulator)) = (
get_user_volume_accumulator_wsol_ata(&params.payer.pubkey()),
get_user_volume_accumulator_pda(&params.payer.pubkey()),
) {
accounts.push(AccountMeta::new(wsol_ata, false));
accounts.push(AccountMeta::new(accumulator, false));
}
}
// Create instruction data
let mut data = [0u8; 24];
@@ -420,3 +436,38 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
Ok(instructions)
}
}
/// Claim cashback for PumpSwap (AMM). Transfers WSOL from UserVolumeAccumulator's WSOL ATA to user's WSOL ATA.
/// Caller should ensure user's WSOL ATA exists (e.g. create idempotent ATA instruction) before this instruction.
pub fn claim_cashback_pumpswap_instruction(
payer: &Pubkey,
quote_mint: Pubkey,
quote_token_program: Pubkey,
) -> Option<solana_sdk::instruction::Instruction> {
const CLAIM_CASHBACK_DISCRIMINATOR: [u8; 8] = [37, 58, 35, 126, 190, 53, 228, 197];
let user_volume_accumulator = get_user_volume_accumulator_pda(payer)?;
let user_volume_accumulator_wsol_ata = get_user_volume_accumulator_wsol_ata(payer)?;
let user_wsol_ata = crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
payer,
&quote_mint,
&quote_token_program,
);
// IDL order: user, user_volume_accumulator, quote_mint, quote_token_program,
// user_volume_accumulator_wsol_token_account, user_wsol_token_account, system_program, event_authority, program
let accounts = vec![
AccountMeta::new(*payer, true), // user (signer, writable)
AccountMeta::new(user_volume_accumulator, false), // user_volume_accumulator (writable)
AccountMeta::new_readonly(quote_mint, false),
AccountMeta::new_readonly(quote_token_program, false),
AccountMeta::new(user_volume_accumulator_wsol_ata, false), // writable
AccountMeta::new(user_wsol_ata, false), // writable
crate::constants::SYSTEM_PROGRAM_META,
accounts::EVENT_AUTHORITY_META,
accounts::AMM_PROGRAM_META,
];
Some(solana_sdk::instruction::Instruction::new_with_bytes(
accounts::AMM_PROGRAM,
&CLAIM_CASHBACK_DISCRIMINATOR,
accounts,
))
}
+10
View File
@@ -185,6 +185,16 @@ pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
)
}
/// WSOL ATA of UserVolumeAccumulator for Pump AMM (used for cashback remaining accounts).
pub fn get_user_volume_accumulator_wsol_ata(user: &Pubkey) -> Option<Pubkey> {
let accumulator = get_user_volume_accumulator_pda(user)?;
Some(crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
&accumulator,
&crate::constants::WSOL_TOKEN_ACCOUNT,
&crate::constants::TOKEN_PROGRAM,
))
}
pub fn get_global_volume_accumulator_pda() -> Option<Pubkey> {
let seeds: &[&[u8]; 1] = &[&seeds::GLOBAL_VOLUME_ACCUMULATOR_SEED];
let program_id: &Pubkey = &&accounts::AMM_PROGRAM;
+3 -1
View File
@@ -15,9 +15,11 @@ pub struct Pool {
pub lp_supply: u64,
pub coin_creator: Pubkey,
pub is_mayhem_mode: bool,
/// Whether this pool's coin has cashback enabled
pub is_cashback_coin: bool,
}
pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1;
pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1;
pub fn pool_decode(data: &[u8]) -> Option<Pool> {
if data.len() < POOL_SIZE {
+49
View File
@@ -902,4 +902,53 @@ impl TradingClient {
let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
Ok(signature.to_string())
}
/// Claim Bonding Curve (Pump) cashback.
///
/// Transfers native SOL from the user's UserVolumeAccumulator to the wallet.
/// If there is nothing to claim, the transaction may still succeed with no SOL transferred.
///
/// # Returns
/// * `Ok(String)` - Transaction signature
/// * `Err(anyhow::Error)` - Build or send failure (e.g. invalid PDA)
pub async fn claim_cashback_pumpfun(&self) -> Result<String, anyhow::Error> {
use solana_sdk::transaction::Transaction;
let ix = crate::instruction::pumpfun::claim_cashback_pumpfun_instruction(&self.payer.pubkey())
.ok_or_else(|| anyhow::anyhow!("Failed to build PumpFun claim_cashback instruction"))?;
let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
let mut transaction = Transaction::new_with_payer(&[ix], Some(&self.payer.pubkey()));
transaction.sign(&[&*self.payer], recent_blockhash);
let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
Ok(signature.to_string())
}
/// Claim PumpSwap (AMM) cashback.
///
/// Transfers WSOL from the UserVolumeAccumulator to the user's WSOL ATA.
/// Creates the user's WSOL ATA idempotently if it does not exist, then claims.
///
/// # Returns
/// * `Ok(String)` - Transaction signature
/// * `Err(anyhow::Error)` - Build or send failure
pub async fn claim_cashback_pumpswap(&self) -> Result<String, anyhow::Error> {
use solana_sdk::transaction::Transaction;
let mut instructions = crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
&self.payer.pubkey(),
&self.payer.pubkey(),
&WSOL_TOKEN_ACCOUNT,
&crate::constants::TOKEN_PROGRAM,
self.use_seed_optimize,
);
let ix = crate::instruction::pumpswap::claim_cashback_pumpswap_instruction(
&self.payer.pubkey(),
WSOL_TOKEN_ACCOUNT,
crate::constants::TOKEN_PROGRAM,
).ok_or_else(|| anyhow::anyhow!("Failed to build PumpSwap claim_cashback instruction"))?;
instructions.push(ix);
let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
let mut transaction = Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
transaction.sign(&[&*self.payer], recent_blockhash);
let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
Ok(signature.to_string())
}
}
+5
View File
@@ -189,6 +189,7 @@ impl PumpFunParams {
complete: account.0.complete,
creator: account.0.creator,
is_mayhem_mode: account.0.is_mayhem_mode,
is_cashback_coin: account.0.is_cashback_coin,
};
let associated_bonding_curve = get_associated_token_address_with_program_id(
&bonding_curve.account,
@@ -243,6 +244,8 @@ pub struct PumpSwapParams {
pub quote_token_program: Pubkey,
/// Whether the pool is in mayhem mode
pub is_mayhem_mode: bool,
/// Whether the pool's coin has cashback enabled
pub is_cashback_coin: bool,
}
impl PumpSwapParams {
@@ -274,6 +277,7 @@ impl PumpSwapParams {
base_token_program,
quote_token_program,
is_mayhem_mode,
is_cashback_coin: false,
}
}
@@ -335,6 +339,7 @@ impl PumpSwapParams {
} else {
crate::constants::TOKEN_PROGRAM_2022
},
is_cashback_coin: pool_data.is_cashback_coin,
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
crate::constants::TOKEN_PROGRAM
} else {