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]
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Sol Trade SDK - Node.js
|
||||
*
|
||||
* Production-grade instruction builders for Solana DEX protocols.
|
||||
*/
|
||||
|
||||
export * from "./instruction";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user