Files
sol-trade-sdk/sol-trade-sdk-nodejs/src/instruction/raydium_amm_v4_builder.ts
T
0xfnzero 971ef41fad feat: add global MEV protection and refactor TradeConfig to builder pattern
- Add `mev_protection: bool` to `TradeConfig` and `InfrastructureConfig` (default: false)
  - Astralane QUIC: switches to port 9000 (MEV-protected endpoint) when enabled
  - BlockRazor HTTP: uses `mode=sandwichMitigation` query param when enabled
  - BlockRazor gRPC: uses `mode=sandwichMitigation` when enabled
- Add `SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV` constants (port 9000) to `constants/swqos.rs`
- Fix `astralane_quic.rs` IP candidates to use the actual port from the address (supports both 7000 and 9000)
- Refactor `TradeConfig` to builder pattern via `TradeConfig::builder()`
  - Introduce `TradeConfigBuilder` with all optional fields and clear defaults
  - `TradeConfig::new()` kept as a shortcut (calls `builder().build()`) for backward compatibility
  - Remove old `with_wsol_ata_config` / `with_check_min_tip` / `with_swqos_cores_from_end` / `with_mev_protection` chain methods
- Update all 16 examples to use `TradeConfig::builder()` with commented-out options so users can discover all available settings at a glance
- Update README.md and README_CN.md code snippets to use builder pattern

🤖 Generated with [Qoder][https://qoder.com]
2026-04-08 02:13:37 +08:00

477 lines
14 KiB
TypeScript

/**
* 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;
}