Chuyển đến nội dung chính

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.

Trang này được dịch tự động bằng AI. Phiên bản tiếng Anh là bản chính thức.Xem bản tiếng Anh →
PDAs (program-derived addresses) và CPIs (cross-program invocation) là hai primitives làm cho Raydium hoạt động được. PDAs cho phép một chương trình “sở hữu” các địa chỉ xác định mà không cần khóa riêng — đó là cách hoạt động của pool authorities và vaults. CPIs cho phép một chương trình gọi chương trình khác — đó là cách Raydium hoán đổi tokens thông qua chương trình SPL Token và cách các integrators kết hợp Raydium vào luồng công việc của riêng họ. Cả hai đều đáng hiểu trước khi đọc mã nguồn của Raydium.

PDAs: địa chỉ không có khóa

Program-Derived Address là một public key mà:
  • Không nằm trên đường cong ed25519 (không tồn tại khóa riêng cho nó).
  • Được dẫn xuất một cách xác định từ một program ID và một tập hợp seeds.
  • Có thể được ký bởi chỉ chương trình dẫn xuất, thông qua invoke_signed.
Mỗi pool authority của Raydium, mỗi pool state account, mỗi vault, mỗi farm state — tất cả đều là PDAs.

Dẫn xuất

Một PDA được tính bằng cách băm program ID với các seeds, sau đó tìm một byte “bump” để buộc kết quả nằm ngoài đường cong. Byte bump đầu tiên (thường bắt đầu từ 255 và giảm dần) tạo ra một địa chỉ nằm ngoài đường cong sẽ thắng; đây là canonical bump.
import { PublicKey } from "@solana/web3.js";

const [poolAuthority, bump] = PublicKey.findProgramAddressSync(
  [Buffer.from("authority"), poolId.toBuffer()],
  CPMM_PROGRAM_ID,
);
Các seeds có thể là bất cứ thứ gì — chuỗi ký tự, các pubkeys khác, giá trị u64 dưới dạng bytes little-endian. Quy ước của Raydium là một tiền tố có thể đọc được của con người theo sau bởi các định danh duy nhất.

Mẫu PDA của Raydium

PDAs phổ biến trong các chương trình của Raydium:
PDASeedsProgram
AMM authority (AMM v4)[b"amm authority"] + bumpAMM v4
Pool state (CPMM)[b"pool", amm_config, mint_a, mint_b]CPMM
Pool vault (CPMM)[b"pool_vault", pool, mint]CPMM
Authority (CPMM)[b"vault_and_lp_mint_auth_seed"]CPMM
Pool state (CLMM)[b"pool", amm_config, mint_0, mint_1]CLMM
Tick array (CLMM)[b"tick_array", pool, start_tick_index]CLMM
Observation (CLMM)[b"observation", pool]CLMM
Personal position (CLMM)[b"position", position_nft_mint]CLMM
Farm state (Farm v6)[b"pool_farm_state", farm_id]Farm v6
User ledger (Farm v6)[b"user_ledger", farm, user]Farm v6
Người dùng và integrators có thể tính toán những PDAs này mà không cần tìm nạp bất cứ thứ gì — với các input công khai (pool ID, farm ID, user key), PDA là xác định được.

Canonical bump

Mặc dù về nguyên tắc có thể tồn tại nhiều bumps tạo ra các địa chỉ nằm ngoài đường cong, các chương trình của Raydium luôn sử dụng canonical bump (được tìm thấy bằng cách giảm dần từ 255). Điều này được lưu trữ trong dữ liệu account của PDA để các giao dịch tiếp theo có thể chuyển nó vào và bỏ qua vòng lặp dẫn xuất (chi phí compute lớn):
#[account]
pub struct PoolState {
    pub bump: [u8; 1],
    // ... rest of pool state
}
Trong các giao dịch tiếp theo, bump được đọc từ pool state thay vì được tính toán lại.

CPIs: gọi các chương trình khác

Cross-Program Invocation cho phép một chương trình gọi các instructions của chương trình khác theo cách inline trong một giao dịch duy nhất. Raydium sử dụng CPIs rộng rãi:
  • Các swap instructions gọi chương trình SPL Token để di chuyển tokens.
  • CLMM gọi Metaplex để mint position NFT.
  • Pool creation gọi System Program để cấp phát accounts.
  • Farm v6 gọi SPL Token để chuyển phần thưởng.
Integrators cũng sử dụng CPIs để gọi vào Raydium — đó là cách các vault strategies, leveraged-LP protocols, và auto-compounders hoạt động. Xem integration-guides/cpi-integration.

invoke vs invoke_signed

Solana runtime cung cấp hai CPI primitives:
  • invoke: gọi chương trình khác; chương trình được gọi sẽ kế thừa các signers từ giao dịch bên ngoài.
  • invoke_signed: gọi chương trình khác thay mặt cho một PDA; runtime xác minh các seeds của PDA và ủy quyền cho chữ ký.
invoke_signed là phép thuật cho phép các chương trình giữ quyền kiểm soát các accounts mà không cần quản lý các khóa riêng.

Ví dụ: Raydium chuyển từ một pool vault

Một pool vault là một Token Account có authority là một PDA của pool program. Để chuyển tokens ra ngoài trong suốt một swap, pool program phải ký tên như PDA đó:
// Seeds for the pool authority
let authority_seeds: &[&[u8]] = &[
    b"vault_and_lp_mint_auth_seed",
    &[authority_bump],
];
let signer_seeds: &[&[&[u8]]] = &[authority_seeds];

// Build CPI context
let cpi_accounts = Transfer {
    from:      input_vault.to_account_info(),
    to:        user_ata.to_account_info(),
    authority: pool_authority.to_account_info(),
};
let cpi_program = token_program.to_account_info();
let cpi_ctx     = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);

// Execute
token::transfer(cpi_ctx, amount)?;
Runtime thấy invoke_signed được gọi bởi CPMM program, xác minh rằng vault_and_lp_mint_auth_seed + bump dẫn xuất tới địa chỉ của pool_authority khi được băm với program ID của CPMM, và cho phép chữ ký của authority trên token transfer. Không có khóa riêng nào liên quan.

Ví dụ: integrator gọi Raydium CPMM

Một integrator program (ví dụ: một escrow) có thể gọi swap_base_input của Raydium thông qua CPI:
use raydium_cpmm::cpi::{self, accounts::Swap};

let cpi_accounts = Swap {
    payer:                order.to_account_info(),      // PDA, will sign
    authority:            pool_authority_info,
    amm_config:           amm_config_info,
    pool_state:           pool_info,
    input_token_account:  order_input_ata,
    output_token_account: order_output_ata,
    input_vault:          input_vault_info,
    output_vault:         output_vault_info,
    input_token_program:  input_token_program_info,
    output_token_program: output_token_program_info,
    input_token_mint:     input_mint_info,
    output_token_mint:    output_mint_info,
    observation_state:    observation_info,
};

let seeds = &[b"order", user.key.as_ref(), &[order.bump]];
let cpi_ctx = CpiContext::new_with_signer(
    cpmm_program.to_account_info(),
    cpi_accounts,
    &[seeds],
);

cpi::swap_base_input(cpi_ctx, amount_in, min_out)?;
Đây là mẫu tích hợp chính thức — xem integration-guides/cpi-integration để xem ví dụ escrow đầy đủ.

Giới hạn độ sâu CPI

Solana giới hạn độ sâu CPI ở 4 level. Instruction cấp cao nhất của một giao dịch được tính là depth 0; mỗi CPI invocation tăng depth thêm một. Ý nghĩa thực tế: swap của chính Raydium đã sử dụng 1-2 levels của CPI (Raydium → SPL Token). Một integrator gọi Raydium sử dụng 2. Nếu integrator đó được gọi bởi một integrator khác, đó là 3. Level thứ 4 là giới hạn. Hầu hết các compositions vẫn ở dưới giới hạn này một cách dễ dàng, nhưng nesting sâu (aggregator → router → Raydium → hook) có thể vượt qua nó. Hãy thiết kế flat thay vì deep.

Remaining accounts

Khi một Raydium instruction cần một số lượng accounts có thể thay đổi (ví dụ: CLMM swap đi qua một số lượng tick arrays không xác định), các accounts bổ sung được chuyển qua như remaining accounts — được thêm vào danh sách fixed-account, được diễn giải theo vị trí. SwapV2 của CPMM sử dụng remaining accounts cho các transfer-hook programs’ extra required accounts. Clients tìm nạp các accounts cần thiết và thêm chúng:
const swapIx = await raydium.cpmm.swap({
  /* ... */
  // SDK handles remaining accounts automatically
});
Ở mức CPI, integrators phải chuyển tiếp remaining accounts thông qua instruction riêng của họ:
pub struct Swap<'info> {
    // ... fixed accounts
    // Plus Remaining accounts forwarded via ctx.remaining_accounts
}

// Forward remaining_accounts into the CPI
cpi::swap_base_input(
    cpi_ctx.with_remaining_accounts(ctx.remaining_accounts.to_vec()),
    amount_in,
    min_out,
)?;

Những cạm bẫy của PDA

Seeds sai → địa chỉ sai

Một lỗi trong đó các seeds ở thứ tự sai, mã hóa sai, hoặc bao gồm/loại trừ một byte thêm sẽ im lặng tạo ra một PDA khác. Giao dịch sẽ thất bại một cách mơ hồ (chương trình cố gắng đọc một account không tồn tại). Luôn test đơn vị dẫn xuất seed so với các giá trị golden đã biết.

Không lưu trữ bump

Nếu bạn tái dẫn xuất bump trên mỗi giao dịch, bạn sẽ phải trả compute cho vòng lặp dẫn xuất. Lưu trữ canonical bump trong dữ liệu của PDA và đọc nó từ đó.

Nhầm lẫn canonical vs non-canonical bump

Non-canonical bumps (nếu ai đó tìm thấy một cái tạo ra off-curve) được cho phép bởi invoke_signed nhưng bị từ chối bởi các chương trình của Raydium thông qua assert_eq!(bump, canonical_bump). Nếu ai đó cố gắng khẳng định một PDA với non-canonical bump, tx sẽ thất bại.

Chuyển một PDA dưới dạng signer khi bạn không phải là chương trình sở hữu

Chỉ chương trình có ID trong dẫn xuất PDA của PDA có thể invoke_signed với các seeds của nó. Nếu bạn cố gắng, runtime sẽ từ chối.

Những cạm bẫy của CPI

Quên chuyển tiếp remaining_accounts

Nếu instruction bên ngoài của bạn chuyển transfer-hook accounts trong remaining_accounts nhưng CPI vào Raydium không chuyển tiếp chúng, Raydium sẽ thất bại vì nó không thể tìm thấy các hook accounts. Luôn bao gồm with_remaining_accounts trong các CPIs cần chúng.

Writable flags mismatch

Một account mà instruction bên ngoài đánh dấu writable cũng phải writable trong lệnh gọi CPI nếu chương trình được gọi dự định viết vào nó. Mismatch → runtime rejection.

Không tính đến rent

CPI tới một chương trình tạo một account (ví dụ: ATA creation) yêu cầu payer có đủ SOL cho rent. Failed rent checks xuất hiện dưới dạng các lỗi mơ hồ.

Ví dụ đã làm việc: tính toán PDAs CPMM của Raydium

import { PublicKey } from "@solana/web3.js";

const CPMM_PROGRAM_ID = new PublicKey("CPMMoo8L3F4NbTegBCKVNunggL7H1Zpdmwpwh8KMoZ0F");

function computeCpmmPdas(ammConfig, mintA, mintB) {
  const [poolState, poolBump] = PublicKey.findProgramAddressSync(
    [
      Buffer.from("pool"),
      ammConfig.toBuffer(),
      mintA.toBuffer(),
      mintB.toBuffer(),
    ],
    CPMM_PROGRAM_ID,
  );

  const [authority] = PublicKey.findProgramAddressSync(
    [Buffer.from("vault_and_lp_mint_auth_seed")],
    CPMM_PROGRAM_ID,
  );

  const [vaultA] = PublicKey.findProgramAddressSync(
    [Buffer.from("pool_vault"), poolState.toBuffer(), mintA.toBuffer()],
    CPMM_PROGRAM_ID,
  );

  const [vaultB] = PublicKey.findProgramAddressSync(
    [Buffer.from("pool_vault"), poolState.toBuffer(), mintB.toBuffer()],
    CPMM_PROGRAM_ID,
  );

  const [observation] = PublicKey.findProgramAddressSync(
    [Buffer.from("observation"), poolState.toBuffer()],
    CPMM_PROGRAM_ID,
  );

  return { poolState, authority, vaultA, vaultB, observation, poolBump };
}
Đây chính xác là những gì Raydium SDK làm dưới nắp khi bạn gọi getPoolInfoFromRpc({ poolId }) — nó dẫn xuất các PDAs liên quan mà không cần một round-trip.

Các con trỏ

Nguồn: