Zum Hauptinhalt springen

Documentation Index

Fetch the complete documentation index at: https://docs.raydium.io/llms.txt

Use this file to discover all available pages before exploring further.

Diese Seite wurde mit KI automatisch übersetzt. Maßgeblich ist stets die englische Version.Englische Version ansehen →
sdk-api/rust-cpi behandelt die Low-Level-Mechanik beim Aufrufen einzelner Raydium-Programme. Diese Seite ist der höherwertige Begleiter: warum Sie Raydium in Ihr eigenes Programm integrieren würden, welches Muster zu Ihrem Use-Case passt, und welche vollständige Infrastruktur Sie benötigen.

Wann CPI das richtige Werkzeug ist

Ein benutzerdefiniertes Programm macht Sinn, wenn der Trade atomar mit anderen On-Chain-Zustandsänderungen stattfinden muss, die nur Ihr Programm vornehmen kann. Häufige Fälle:
  • Escrow- / Limit-Order-Programme — der Benutzer deponiert eine Mint in Ihrem Escrow, Ihr Programm überwacht eine Preisbedingung, und wenn sie ausgelöst wird, tauscht Ihr Programm atomar über Raydium und guthschreibt das Benutzerkonto.
  • Aggregator-Proxys — eine einzelne Anweisung, die einen Swap über Raydium und eine oder mehrere andere DEXes leitet, alle Hops unter einer einzigen Slippage-Prüfung, die Ihr Programm besitzt.
  • Auto-Compounding-Vaults — LP oder Farm-Stake in Ihrem Vault deponieren, Vault sammelt Rewards nach Zeitplan, versorgt Liquidität neu, gibt Share-Token aus.
  • Strategy-Vaults — gehebelte LP-Positionen, die sich durch Swaps über CLMM rebalancieren; Liquidatoren, die Positionen schließen und Sicherheiten in einer Transaktion tauschen.
  • Token-Startplattformen mit benutzerdefiniertem Vesting — Ihr Programm hält Vesting-Token und gibt sie nach Zeitplan in einen Raydium-Pool frei.
Wenn Sie nur einen Swap aus Off-Chain-Code senden möchten, ist CPI eine Überkomplizierung — verwenden Sie das SDK. CPI lohnt sich nur dann, wenn Atomarität mit Ihrem eigenen Zustand erforderlich ist.

Kompositionsmuster

Muster 1: Schlanker Proxy

Ihr Programm stellt eine einzelne Anweisung bereit, die eine Richtlinie überprüft (z. B. Whitelisting von Mint-Paaren, Gebührenrabatt für verifizierte Benutzer) und leitet dann an Raydium weiter.
┌──────────────┐   user tx    ┌────────────────┐  CPI  ┌──────────┐
│ user         │─────────────▶│ your program   │──────▶│ Raydium  │
└──────────────┘              │  (validate)    │       │  (CPMM)  │
                              └────────────────┘       └──────────┘
Der Zustand lebt in den ATAs des Benutzers. Ihr Programm besitzt keine Token. Minimale Vertrauensverpflichtung.

Muster 2: Escrow

Ihr Programm besitzt eine PDA, die die Input-Mint des Benutzers hält. Bei Auslösung signiert die PDA einen CPI zu Raydium, um ihren eigenen Saldo zu tauschen.
           deposit                   trigger
   user ───────────▶  PDA vault  ───────────────▶  Raydium swap
                     (your prog)                    (signed by PDA)


                                                    PDA vault (output mint)

                                                     withdraw ▼
                                                         user
Kritisches Detail: die PDA signiert über CpiContext::new_with_signer. Siehe Signer Seeds.

Muster 3: Zusammengesetzter Multi-Hop

Ihr Programm gibt mehrere CPIs in einer Anweisung aus und erzwingt eine einzige Slippage-Grenze über alle hinweg. Die Raydium-Swap-Anweisungen haben jeweils ihre eigene minimum_amount_out, aber Sie setzen diese auf 0 (oder eine sehr lockere Untergrenze) und erzwingen eine strikte endgültige Untergrenze nach dem letzten Hop selbst.
instruction:
  CPI swap: tokenA → tokenB   (raydium, loose min)
  CPI swap: tokenB → tokenC   (raydium / third-party, loose min)
  CPI swap: tokenC → tokenD   (raydium, loose min)
  require(user.tokenD_ata.amount - pre_balance >= user_min_out)
Dies gibt Ihnen ein einzelnes Rückgängig-machen-Tor für die gesamte Route. Verwenden Sie dieses Muster nur, wenn Sie jedem Hop vertrauen, dass er slippage-sicher ist; andernfalls lassen Sie jeden Hop sein eigenes Minimum erzwingen.

Muster 4: Vault / Strategy

Ihr Programm hält LP-Token oder Farm-Stake in einer PDA. Ein Keeper (oder der Benutzer) ruft compound() auf, das:
  1. Rewards aus der Farm sammelt.
  2. Rewards für Pool-Token tauscht (CPI in CPMM oder CLMM).
  3. Die Erlöse wieder in den LP einzahlt (ein weiterer CPI).
  4. Den neuen LP einsetzt (ein weiterer CPI).
Alles in einer Transaktion, damit sich der Vault-NAV atomar bewegt. Das Compute-Budget beträgt typischerweise 600k–1M CU; Adressensuchtabellen sind obligatorisch.

Kontolisten-Konstruktion

Die Accounts-Struktur des aufrufenden Programms spiegelt die Kontenreihenfolge des Raydium-Programms wider, aber die meisten Raydium-seitigen Konten sind UncheckedAccount, weil Raydium sie selbst überprüft. Sie fügen nur Einschränkungen auf Konten hinzu, die Sie besitzen:
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};

#[derive(Accounts)]
pub struct EscrowSwap<'info> {
    /// The escrow PDA; holds input mint and signs the CPI.
    #[account(
        mut,
        seeds = [b"escrow", user.key().as_ref()],
        bump = escrow.bump,
    )]
    pub escrow: Account<'info, Escrow>,

    #[account(mut)]
    pub user: Signer<'info>,

    // ----- Raydium-side accounts, mostly unchecked -----

    /// CHECK: validated by CPMM
    #[account(mut)] pub pool_state: UncheckedAccount<'info>,
    /// CHECK: validated by CPMM
    pub amm_config: UncheckedAccount<'info>,
    /// CHECK: validated by CPMM
    pub pool_authority: UncheckedAccount<'info>,
    #[account(mut)] pub input_vault:  Account<'info, TokenAccount>,
    #[account(mut)] pub output_vault: Account<'info, TokenAccount>,
    /// CHECK: validated by CPMM
    #[account(mut)] pub observation_state: UncheckedAccount<'info>,

    /// Escrow's input ATA — owned by the escrow PDA.
    #[account(
        mut,
        associated_token::mint = input_mint,
        associated_token::authority = escrow,
    )]
    pub escrow_input_ata: Account<'info, TokenAccount>,

    /// Escrow's output ATA.
    #[account(
        mut,
        associated_token::mint = output_mint,
        associated_token::authority = escrow,
    )]
    pub escrow_output_ata: Account<'info, TokenAccount>,

    pub input_mint:  Account<'info, anchor_spl::token::Mint>,
    pub output_mint: Account<'info, anchor_spl::token::Mint>,

    pub cpmm_program:       Program<'info, raydium_cp_swap::program::RaydiumCpSwap>,
    pub token_program:      Program<'info, Token>,
    pub token_program_2022: Program<'info, anchor_spl::token_2022::Token2022>,
}
Die Asymmetrie — strikte Überprüfung Ihrer Konten, UncheckedAccount für die Raydium-Seite — ist keine Faulheit. Der Empfänger überprüft die Seinen; Doppelüberprüfung beim Aufrufer verschwendet nur CU und riskiert Synchronisierungsverlust, wenn Raydium ein neues Struct-Layout-Feld ausliefert.

Der CPI-Aufruf selbst

use raydium_cp_swap::cpi::{self, accounts::Swap as CpmmSwap};

pub fn escrow_swap(
    ctx: Context<EscrowSwap>,
    amount_in: u64,
    minimum_amount_out: u64,
) -> Result<()> {
    let user_key = ctx.accounts.user.key();
    let bump     = ctx.accounts.escrow.bump;
    let seeds: &[&[u8]] = &[b"escrow", user_key.as_ref(), &[bump]];
    let signer: &[&[&[u8]]] = &[seeds];

    let cpi_accounts = CpmmSwap {
        payer:                ctx.accounts.user.to_account_info(),
        authority:            ctx.accounts.escrow.to_account_info(),
        amm_config:           ctx.accounts.amm_config.to_account_info(),
        pool_state:           ctx.accounts.pool_state.to_account_info(),
        input_token_account:  ctx.accounts.escrow_input_ata.to_account_info(),
        output_token_account: ctx.accounts.escrow_output_ata.to_account_info(),
        input_vault:          ctx.accounts.input_vault.to_account_info(),
        output_vault:         ctx.accounts.output_vault.to_account_info(),
        input_token_program:  ctx.accounts.token_program.to_account_info(),
        output_token_program: ctx.accounts.token_program.to_account_info(),
        input_token_mint:     ctx.accounts.input_mint.to_account_info(),
        output_token_mint:    ctx.accounts.output_mint.to_account_info(),
        observation_state:    ctx.accounts.observation_state.to_account_info(),
    };

    let cpi_ctx = CpiContext::new_with_signer(
        ctx.accounts.cpmm_program.to_account_info(),
        cpi_accounts,
        signer,
    );

    cpi::swap_base_input(cpi_ctx, amount_in, minimum_amount_out)?;
    Ok(())
}

PDA-Signer-Seeds

Der CPI gelingt nur, wenn die als authority übergebene PDA mit der Herleitung übereinstimmt, die der Aufrufer beansprucht. Die beiden müssen sich einigen auf:
  1. Die Seed-Byte-Sequenz (hier [b"escrow", user.key().as_ref()]).
  2. Der Bump.
  3. Die aufrufende Programm-ID (Ihr Programm, nicht Raydium).
Raydium kümmert sich nicht darum, wer die Authority ist — es kümmert sich nur darum, dass die authority-Signatur die Transaktion abdeckt und dass die Input-ATA dieser Authority gehört. Die Überprüfung erfolgt in anchor_spl::token::transfer: Das Feld authority der ATA muss dem Signer entsprechen. Häufiger Fehler: user als Authority übergeben (und Übertrag von escrow_input_ata, die der Escrow-PDA gehört). Das SPL-Token-Programm lehnt mit owner mismatch ab. Machen Sie das Feld authority immer zum ATA-Besitzer.

Verbleibende Konten

Mehrere Raydium-Anweisungen verwenden eine Variable-Längenliste von Konten, die nach den festen angehängt werden — verbleibende Konten.
  • CLMM SwapV2: 1–8 TickArrayState-Konten für die Tick-Arrays, die der Swap durchqueren kann, in Swap-Richtung.
  • Farm v6 Deposit / Harvest / Withdraw: (reward_vault, user_reward_ata)-Paare, ein Paar pro aktivem Reward-Slot.
  • Token-2022 Transfer-Hook-Mints: das Transfer-Hook-Programm plus alle Konten, die der Hook benötigt.
Die Anchor-CPI-Helfer überprüfen verbleibende Konten nicht typgerecht. Geben Sie sie durch:
let cpi_ctx = CpiContext::new_with_signer(program, accounts, signer)
    .with_remaining_accounts(ctx.remaining_accounts.to_vec());
Reihenfolge ist wichtig. Für CLMM:
remaining = [
    tick_array_in_direction_0,    // first one crossed
    tick_array_in_direction_1,
    ...,
]
Für Farm v6 Harvest:
remaining = [
    reward_vault_0, user_reward_ata_0,
    reward_vault_1, user_reward_ata_1,
    // omit any slot whose reward_state is Uninitialized
]
Ihr aufrufendes Programm muss die verbleibenden Konten, die es vom Client erhält, unverändert durchleiten. Versuchen Sie nicht, sie zu filtern oder neu zu ordnen.

Compute-Budget für zusammengesetzte Aufrufe

Ein CPI kostet ~1.500 CU für den Aufruframmen selbst; die eigene CU-Nutzung des Aufgerufs stapelt sich darauf. Ungefähres Budget pro Raydium-CPI:
AufrufCU (SPL Token)CU (Token-2022)
CPMM swap_base_input~150,000~200,000
CLMM swap_v2 (single tick array)~180,000~230,000
CLMM swap_v2 (crosses 2 ticks)~220,000~270,000
Farm v6 deposit~120,000~150,000
Farm v6 harvest (per reward slot)+30,000+40,000
AMM v4 swap_base_in~140,000n/a
Addieren Sie ~1.500 für jeden CPI-Frame und ~20.000 für Ihre Program-Overhead. Ein Auto-Compounder, der harvest → swap A → swap B → deposit LP → stake LP ausführt, erreicht leicht 700k CU. Setzen Sie immer ein explizites ComputeBudgetProgram::set_compute_unit_limit:
import { ComputeBudgetProgram } from "@solana/web3.js";

const tx = new Transaction().add(
  ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000 }),
  ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priorityFeeMicroLamports }),
  yourInstruction,
);
Die Standard-200k-CU-Obergrenze wird still ausgeschöpft, lange bevor ein zusammengesetzter Aufruf abgeschlossen ist.

Fehlerausbreitung

Raydium-Programme geben Anchor-Fehler mit stabilen Fehlercodes zurück. Ihr aufrufendes Programm sieht sie als Err(ProgramError::Custom(code)). Leiten Sie sie standardmäßig weiter:
cpi::swap_base_input(cpi_ctx, amount_in, minimum_amount_out)?;
Oder abfangen für bestimmte Codes:
use raydium_cp_swap::error::ErrorCode as CpmmErr;

match cpi::swap_base_input(cpi_ctx, amount_in, minimum_amount_out) {
    Ok(_) => {},
    Err(err) if is_err(err, CpmmErr::ExceededSlippage) => {
        // Your program might want to retry at a larger slippage, or unwind state.
        return err!(YourErr::PoolTooVolatile);
    }
    Err(err) => return Err(err),
}
Die Fehlercod-zu-Bedeutungs-Zuordnung ist stabil gemäß der IDL-Richtlinie (sdk-api/anchor-idl); neue Codes werden am Ende angehängt, bestehende Codes ändern nie ihre Bedeutung.

Vollständiges praktisches Beispiel: Limit-Order-Escrow

Ablauf:
  1. open_order — Benutzer hinterlegt amount_in von input_mint in Escrow-PDA; zeichnet Ziel min_amount_out und Ablauf auf.
  2. execute_order — Jeder (Keeper) ruft mit den aktuellen Pool-Konten auf. Programm überprüft das aktuelle Angebot ≥ min_amount_out, dann CPI-Raydium-Swap und behält den Output im Escrow.
  3. claim — Benutzer zieht die Output-Mint aus dem Escrow.
#[account]
pub struct LimitOrder {
    pub user:          Pubkey,
    pub input_mint:    Pubkey,
    pub output_mint:   Pubkey,
    pub amount_in:     u64,
    pub min_out:       u64,
    pub expiry_unix:   i64,
    pub state:         u8,    // 0 open, 1 filled, 2 cancelled, 3 expired
    pub bump:          u8,
}

#[program]
pub mod limit_orders {
    use super::*;

    pub fn execute_order(
        ctx: Context<ExecuteOrder>,
    ) -> Result<()> {
        let order = &ctx.accounts.order;
        require!(order.state == 0, OrderErr::NotOpen);
        require!(Clock::get()?.unix_timestamp < order.expiry_unix, OrderErr::Expired);

        let user_key = order.user;
        let seeds: &[&[u8]] = &[b"order", user_key.as_ref(), &[order.bump]];
        let signer: &[&[&[u8]]] = &[seeds];

        let pre_out_balance = ctx.accounts.escrow_output_ata.amount;

        let cpi_accounts = CpmmSwap {
            payer:                ctx.accounts.keeper.to_account_info(),
            authority:            ctx.accounts.order.to_account_info(),
            amm_config:           ctx.accounts.amm_config.to_account_info(),
            pool_state:           ctx.accounts.pool_state.to_account_info(),
            input_token_account:  ctx.accounts.escrow_input_ata.to_account_info(),
            output_token_account: ctx.accounts.escrow_output_ata.to_account_info(),
            input_vault:          ctx.accounts.input_vault.to_account_info(),
            output_vault:         ctx.accounts.output_vault.to_account_info(),
            input_token_program:  ctx.accounts.token_program.to_account_info(),
            output_token_program: ctx.accounts.token_program.to_account_info(),
            input_token_mint:     ctx.accounts.input_mint.to_account_info(),
            output_token_mint:    ctx.accounts.output_mint.to_account_info(),
            observation_state:    ctx.accounts.observation_state.to_account_info(),
        };

        let cpi_ctx = CpiContext::new_with_signer(
            ctx.accounts.cpmm_program.to_account_info(),
            cpi_accounts,
            signer,
        );

        // Let the escrow enforce the minimum — we trust Raydium's slippage, but we
        // also re-check our own post-swap delta in case a future change ever relaxes it.
        cpi::swap_base_input(cpi_ctx, order.amount_in, order.min_out)?;

        ctx.accounts.escrow_output_ata.reload()?;
        let delta = ctx.accounts.escrow_output_ata.amount
            .checked_sub(pre_out_balance)
            .ok_or(error!(OrderErr::AccountingError))?;
        require!(delta >= order.min_out, OrderErr::InsufficientOutput);

        let order = &mut ctx.accounts.order;
        order.state = 1;
        Ok(())
    }
}
Der Keeper bezahlt die Transaktionsgebühr (sie erhalten anderswo eine Keeper-Gebühr — nicht gezeigt). Die Escrow-PDA signiert den CPI. Sowohl die Raydium-seitige Slippage-Prüfung als auch die eigene Delta-Prüfung des Escrows erzwingen die Untergrenze — Sicherheit nach allen Seiten.

Tests

Raydium-Programme in einen lokalen Validator für Integrationstests ziehen (aus Anchor.toml):
[test.validator]
clone = [
  { address = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK" }, # CPMM
  { address = "CLMM...." },                                     # CLMM
  { address = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" }, # AMM v4
  { address = "FarmqiPv5eAj3j1GMdMCMUGXqPUvmquZtMy86QH6rzhG" }, # Farm v6
]
Klonen Sie auch die Pool-Zustandskonten, damit Ihre Tests tatsächlich Swaps ausführen können; anchor test ruft sie beim Start von Mainnet ab. Siehe sdk-api/rust-cpi.

Fallstricke spezifisch für Komposition

Reentrancy

Solana hat keine echte Reentrancy — ein CPI kann nicht in der gleichen Invokation in das ursprüngliche Programm zurückrufen. Aber Sie können sich trotzdem selbst in eine logische Reentrancy bauen: ein CPI, das Ihren Zustand liest, dann liest Ihr Code ihn wieder an und nimmt an, der CPI habe ihn nicht geändert. Für Raydium berühren die CPIs Ihren Zustand nicht, daher ist dies weniger ein Problem als z. B. in Flash-Loan-Kontexten. Aber wenn Sie Raydium mit einem Kreditprotokoll zusammensetzen, seien Sie sich bewusst.

Kontomutability-Drift

Wenn Ihr Programm ein Konto als mut übergibt, aber Raydium erwartet es schreibgeschützt (oder umgekehrt), lehnt die Runtime den Aufruf mit InvalidAccountData ab. Prüfen Sie Raydiums Anweisungs-erwartete Mutability immer in der IDL; anchor_cp_swap::cpi::accounts::Swap erzwingt es über seine Feldtypen.

Token-2022-Programmfeld

Input- und Output-Mints können sich unter verschiedenen Token-Programmen befinden — eines SPL Token, eines Token-2022. Der CPI hat getrennte Felder input_token_program und output_token_program aus diesem Grund. Prüfen Sie immer das Feld owner jeder Mint und leiten Sie das korrekte Programm in jeden Slot.

Versionierte Transaktionen

Eine zusammengesetzte Tx mit 2+ Raydium-CPIs plus ATA-Erstellung passt selten in eine Legacy (v0-ohne-LUT)-Transaktion. Verwenden Sie V0 mit Adressensuchtabellen; rufen Sie Raydiums öffentliche LUTs über raydium.getRaydiumLutAddresses() ab.

Verweise

Quellen: