import { ComputeBudgetProgram, Connection, Keypair, PublicKey, Transaction, VersionedTransaction } from "@solana/web3.js"; import { EntropyToMnemonic, SVMDeriveChildPrivateKey } from "../walletBip32"; import { VM } from "../vm"; import { ChainAddress, ChainWallet } from "../IChainWallet"; import { Balance, ChainWalletConfig, UserTokenBalance, TokenInfo, TransactionResult, NFT, DiscoveredWallet, WalletDiscoveryOptions, WalletDiscoveryResult, PocketDiscoveryOptions } from "../types"; import { VMValidation, sanitizeError, logSafeError } from "../vm-validation"; import { getSvmNativeBalance, getTokenBalance, getTransferNativeTransaction, getTransferTokenTransaction, signAndSendTransaction, getJupiterQuote, buildJupiterSwapTransaction, executeJupiterSwap, uiAmountToBaseUnits, validateJupiterTokens, JupiterQuoteResponse, getTokenInfo, discoverTokens, signTransaction, sendTransaction, fetchWalletNfts } from "./utils"; import BN from "bn.js"; import nacl from "tweetnacl"; import base58 from "bs58"; import { fetchPrices } from "../price"; import { PriceResponse } from "../price.types"; import { getSVMTransactionHistory, SVMTransactionHistoryItem } from "./transactionParsing"; /** * Create a Solana connection for the provided chain configuration. * * @param config - Chain configuration containing the RPC URL. * @returns Solana `Connection` instance. */ export const createSvmConnection = (config: ChainWalletConfig): Connection => new Connection(config.rpcUrl); /** * Get native SOL balance for an address. * * @param address - Solana public key to query. * @param connection - Solana connection. * @returns Native balance details. */ export const getSvmNativeBalanceForAddress = async ( address: PublicKey, connection: Connection ): Promise => { return await SVMVM.getNativeBalance(address, connection); }; /** * Get SPL token balance for an address. * * @param address - Wallet public key to query. * @param tokenAddress - SPL token mint address. * @param connection - Solana connection. * @returns Token balance details. */ export const getSvmTokenBalanceForAddress = async ( address: PublicKey, tokenAddress: PublicKey, connection: Connection ): Promise => { return await SVMVM.getTokenBalance(address, tokenAddress, connection); }; /** * Discover SPL tokens held by an address. * * @param address - Wallet public key to scan. * @param connection - Solana connection. * @returns Discovered token balances. */ export const discoverSvmTokens = async ( address: PublicKey, connection: Connection ): Promise[]> => { return await discoverTokens(address, connection); }; /** * Discover NFTs held by an address. * * @param address - Wallet public key to scan. * @param connection - Solana connection. * @returns Discovered NFTs. */ export const discoverSvmNFTs = async ( address: PublicKey, connection: Connection ): Promise => { return await fetchWalletNfts(address, connection); }; /** * Get parsed transaction history for an address. * * @param connection - Solana connection. * @param address - Wallet public key to inspect. * @returns Transaction history entries. */ export const getSvmTransactionHistoryForAddress = async ( connection: Connection, address: PublicKey ): Promise => { return await getSVMTransactionHistory(connection, address); }; /** * Fetch token prices for SVM tokens on the configured chain. * * @param config - Chain configuration used for chain ID resolution. * @param tokenAddresses - Token mint addresses. * @returns Price response payload. * @throws Error when the price service reports a failure. */ export const getSvmPricesForTokens = async ( config: ChainWalletConfig, tokenAddresses: string[] ): Promise => { const result = await fetchPrices({ vm: 'SVM', chainId: config.chainId, tokenAddresses, }); if (result.error) { throw new Error(result.error.message); } return result.data as PriceResponse; }; export class SVMVM extends VM { getTokenInfo = getTokenInfo static getTokenInfo = getTokenInfo /** * Validate that an input can be parsed as a Solana public key. * * @param address - Address candidate. * @returns `true` if valid, otherwise `false`. */ static validateAddress(address: PublicKey): boolean { try { new PublicKey(address) return true } catch (error) { return false } } derivationPath = "m/44'/501'/0'/"; // Phantom standard derivation path constructor(seed: string) { super(seed, "SVM"); } /** * Read native SOL balance for a wallet. * * @param address - Wallet public key. * @param connection - Solana connection. * @returns Native balance details. */ static getNativeBalance(address: PublicKey, connection: Connection): Promise { return getSvmNativeBalance(address, connection); } /** * Read SPL token balance for a wallet. * * @param address - Wallet public key. * @param tokenAddress - SPL token mint address. * @param connection - Solana connection. * @returns Token balance details normalized to `Balance`. */ static async getTokenBalance(address: PublicKey, tokenAddress: PublicKey, connection: Connection): Promise { const balance = await getTokenBalance(address, tokenAddress, connection); if (balance === 0) { return { balance: new BN(0), formatted: 0, decimal: 0 }; } return { balance: new BN(balance.amount), formatted: balance.uiAmount || parseInt(balance.amount) / 10 ** balance.decimals, decimal: balance.decimals }; } static generateMnemonicFromPrivateKey(privateKey: Keypair): string { return EntropyToMnemonic(base58.encode(privateKey.secretKey)) } static signAndSendTransaction = signAndSendTransaction static signTransaction = signTransaction static sendTransaction = sendTransaction /** * Convert entropy bytes (as string) into a Solana keypair seed. * * @param entropy - Entropy string. * @returns Derived Solana keypair. */ static convertFromEntropyToPrivateKey = (entropy: string): Keypair => { return Keypair.fromSeed(Buffer.from(entropy)) } /** * Derive a child keypair for a wallet index. * * @param index - Wallet index in derivation path. * @param seed - Optional explicit seed (takes priority over mnemonic). * @param mnemonic - Optional mnemonic used when seed is not provided. * @param derivationPath - Base derivation path prefix. * @returns Derived keypair and index. */ generatePrivateKey(index: number, seed?: string, mnemonic?: string, derivationPath = this.derivationPath) { // Validate inputs VMValidation.validateIndex(index, 'Wallet index'); // VMValidation.validateDerivationPath(derivationPath + index + "'", 'SVM'); let _seed: string; if (seed) { VMValidation.validateSeed(seed); _seed = seed; } else if (mnemonic) { VMValidation.validateMnemonic(mnemonic); _seed = VM.mnemonicToSeed(mnemonic); } else { // Check if VM has been disposed this.checkNotDisposed(); _seed = this.seed; } const privateKey = SVMDeriveChildPrivateKey(_seed, index, derivationPath); return { privateKey, index }; } /** * Create an `SVMVM` instance from a mnemonic phrase. * * @param mnemonic - BIP-39 mnemonic phrase. * @returns Initialized SVM VM instance. */ static fromMnemonic(mnemonic: string): VM { const seed = VM.mnemonicToSeed(mnemonic) return new SVMVM(seed) } /** * Derive a savings account using BIP-44 account index * * Main wallet uses: m/44'/501'/0'/0' (account index 0) * Savings accounts use: m/44'/501'/N'/0' (account index N) * * @param accountIndex - The BIP-44 account index (1 for first savings, 2 for second, etc.) * @returns Object containing privateKey, address, and derivation path */ deriveSavingsAccount(accountIndex: number): { privateKey: Keypair; address: PublicKey; derivationPath: string } { const derivationPath = `m/44'/501'/${accountIndex}'/0'`; const keypair = SVMDeriveChildPrivateKey(this.seed, 0, `m/44'/501'/${accountIndex}'/`); return { privateKey: keypair, address: keypair.publicKey, derivationPath }; } /** * Discover wallets with native SOL balances using BIP-44 derivation * * Scans wallet indices to find wallets containing native SOL tokens. * Follows BIP-44 gap limit standard: stops after 20 consecutive empty wallets. * * @param connection - Solana Connection instance * @param options - Discovery options (gap limit, parallel checking, callbacks, etc.) * @returns Discovery result with found wallets and statistics * * @example * ```typescript * const vm = new SVMVM(seed); * const connection = new Connection('https://api.mainnet-beta.solana.com'); * * // Sequential checking (safer for rate limits) * const result = await vm.discoverWallets(connection, { * gapLimit: 20, * onProgress: (current, total, found) => console.log(`Checked ${current}/${total}, found ${found}`), * onDiscovered: (wallet) => console.log(`Found wallet at index ${wallet.index}`) * }); * * // Parallel checking (faster but more aggressive) * const result = await vm.discoverWallets(connection, { * checkInParallel: true, * batchSize: 10, * gapLimit: 20 * }); * ``` */ async discoverWallets( connection: Connection, options?: WalletDiscoveryOptions ): Promise { const startTime = Date.now(); // Default options - parallel checking for speed const { startIndex = 0, maxIndex = 100, gapLimit = 20, minBalance = BigInt(0), includeZeroBalance = false, includePrivateKeys = false, checkInParallel = true, // Default to parallel for speed batchSize = 10, // Larger batch for better performance checkDelay = checkInParallel ? 200 : 50, onProgress, onDiscovered } = options || {}; const discovered: DiscoveredWallet[] = []; let consecutiveEmpty = 0; let scannedIndices = 0; let stoppedByGapLimit = false; if (checkInParallel) { // Parallel checking with batches for (let i = startIndex; i <= maxIndex; i += batchSize) { const batchEnd = Math.min(i + batchSize, maxIndex + 1); const batchPromises: Promise[] = []; // Create batch of parallel checks for (let j = i; j < batchEnd; j++) { batchPromises.push(this.checkWalletBalance(j, connection)); } // Wait for batch to complete const batchResults = await Promise.all(batchPromises); // Process results for (let k = 0; k < batchResults.length; k++) { const index = i + k; const wallet = batchResults[k]; scannedIndices++; if (wallet) { const hasBalance = wallet.nativeBalance.amount > minBalance; if (hasBalance || includeZeroBalance) { if (!includePrivateKeys) { delete wallet.privateKey; } discovered.push(wallet); consecutiveEmpty = 0; onDiscovered?.(wallet); } else { consecutiveEmpty++; } } else { consecutiveEmpty++; } onProgress?.(scannedIndices, maxIndex - startIndex + 1, discovered.length); // Check gap limit if (consecutiveEmpty >= gapLimit) { stoppedByGapLimit = true; break; } } if (stoppedByGapLimit) { break; } // Delay between batches if (batchEnd <= maxIndex) { await this.sleep(checkDelay); } } } else { // Sequential checking for (let i = startIndex; i <= maxIndex; i++) { const wallet = await this.checkWalletBalance(i, connection); scannedIndices++; if (wallet) { const hasBalance = wallet.nativeBalance.amount > minBalance; if (hasBalance || includeZeroBalance) { if (!includePrivateKeys) { delete wallet.privateKey; } discovered.push(wallet); consecutiveEmpty = 0; onDiscovered?.(wallet); } else { consecutiveEmpty++; } } else { consecutiveEmpty++; } onProgress?.(scannedIndices, maxIndex - startIndex + 1, discovered.length); // Check gap limit if (consecutiveEmpty >= gapLimit) { stoppedByGapLimit = true; break; } // Delay between checks if (i < maxIndex) { await this.sleep(checkDelay); } } } // Calculate total balance const totalBalance = discovered.reduce((sum, wallet) => sum + wallet.nativeBalance.amount, BigInt(0)); const duration = Date.now() - startTime; return { discovered, scannedIndices, highestIndex: startIndex + scannedIndices - 1, totalBalance, stoppedByGapLimit, duration }; } /** * Check balance for a specific wallet index with retry logic * @private */ private async checkWalletBalance( index: number, connection: Connection, maxRetries: number = 3 ): Promise { const derivationPath = `m/44'/501'/${index}'/0'`; // Derive wallet using hardened derivation (required for Solana) const keypair = SVMDeriveChildPrivateKey(this.seed, 0, `m/44'/501'/${index}'/`); const address = keypair.publicKey; // Retry logic with exponential backoff for (let attempt = 0; attempt < maxRetries; attempt++) { try { // Get balance in lamports const balanceLamports = await connection.getBalance(address); // Convert lamports to SOL (9 decimals) const balanceSOL = balanceLamports / 1_000_000_000; return { index, address: address.toString(), derivationPath, nativeBalance: { amount: BigInt(balanceLamports), formatted: balanceSOL, symbol: 'SOL' }, privateKey: base58.encode(keypair.secretKey) }; } catch (error) { if (attempt === maxRetries - 1) { console.error(`Failed to check balance for index ${index} after ${maxRetries} attempts:`, error); return null; } // Exponential backoff: 1s, 2s, 4s const backoffMs = 1000 * Math.pow(2, attempt); await this.sleep(backoffMs); } } return null; } /** * Sleep helper for rate limiting * @private */ private sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Discover savings pockets with native SOL balances using BIP-44 derivation * * Scans pocket account indices to find pockets containing native SOL. * Pockets use derivation path: m/44'/501'/{accountIndex+1}'/0' (all hardened for Solana) * * @param connection - Solana Connection instance * @param options - Discovery options (gap limit, parallel checking, walletIndex, callbacks, etc.) * @returns Discovery result with found pockets and statistics * * @example * ```typescript * const vm = new SVMVM(seed); * const connection = new Connection('https://api.mainnet-beta.solana.com'); * * // Discover pockets for wallet index 0 * const result = await vm.discoverPockets(connection, { * walletIndex: 0, * gapLimit: 20, * onDiscovered: (pocket) => console.log(`Found pocket at account ${pocket.index}`) * }); * * console.log(`Found ${result.discovered.length} pockets with SOL`); * ``` */ async discoverPockets( connection: Connection, options?: PocketDiscoveryOptions ): Promise { const startTime = Date.now(); // Default options - parallel checking for speed const { startIndex = 0, maxIndex = 100, gapLimit = 20, minBalance = BigInt(0), includeZeroBalance = false, includePrivateKeys = false, checkInParallel = true, batchSize = 10, checkDelay = checkInParallel ? 200 : 50, onProgress, onDiscovered, walletIndex = 0, } = options || {}; const discovered: DiscoveredWallet[] = []; let consecutiveEmpty = 0; let scannedIndices = 0; let stoppedByGapLimit = false; if (checkInParallel) { // Parallel checking with batches for (let i = startIndex; i <= maxIndex; i += batchSize) { const batchEnd = Math.min(i + batchSize, maxIndex + 1); const batchPromises: Promise[] = []; // Create batch of parallel checks for (let j = i; j < batchEnd; j++) { batchPromises.push(this.checkPocketBalance(j, walletIndex, connection)); } // Wait for batch to complete const batchResults = await Promise.all(batchPromises); // Process results for (let k = 0; k < batchResults.length; k++) { const index = i + k; const pocket = batchResults[k]; scannedIndices++; if (pocket) { const hasBalance = pocket.nativeBalance.amount > minBalance; if (hasBalance || includeZeroBalance) { if (!includePrivateKeys) { delete pocket.privateKey; } discovered.push(pocket); consecutiveEmpty = 0; onDiscovered?.(pocket); } else { consecutiveEmpty++; } } else { consecutiveEmpty++; } onProgress?.(scannedIndices, maxIndex - startIndex + 1, discovered.length); // Check gap limit if (consecutiveEmpty >= gapLimit) { stoppedByGapLimit = true; break; } } if (stoppedByGapLimit) { break; } // Delay between batches if (batchEnd <= maxIndex) { await this.sleep(checkDelay); } } } else { // Sequential checking for (let i = startIndex; i <= maxIndex; i++) { const pocket = await this.checkPocketBalance(i, walletIndex, connection); scannedIndices++; if (pocket) { const hasBalance = pocket.nativeBalance.amount > minBalance; if (hasBalance || includeZeroBalance) { if (!includePrivateKeys) { delete pocket.privateKey; } discovered.push(pocket); consecutiveEmpty = 0; onDiscovered?.(pocket); } else { consecutiveEmpty++; } } else { consecutiveEmpty++; } onProgress?.(scannedIndices, maxIndex - startIndex + 1, discovered.length); // Check gap limit if (consecutiveEmpty >= gapLimit) { stoppedByGapLimit = true; break; } // Delay between checks if (i < maxIndex) { await this.sleep(checkDelay); } } } // Calculate total balance const totalBalance = discovered.reduce((sum, pocket) => sum + pocket.nativeBalance.amount, BigInt(0)); const duration = Date.now() - startTime; return { discovered, scannedIndices, highestIndex: startIndex + scannedIndices - 1, totalBalance, stoppedByGapLimit, duration }; } /** * Check balance for a specific pocket at account index * @private */ private async checkPocketBalance( accountIndex: number, walletIndex: number, connection: Connection, maxRetries: number = 3 ): Promise { // Pocket derivation: m/44'/501'/{accountIndex+1}'/0' (all hardened for Solana) const pocketIndex = accountIndex + 1; const derivationPath = `m/44'/501'/${pocketIndex}'/0'`; // Derive wallet using hardened derivation (required for Solana) // Note: For pockets, we use pocketIndex for account and walletIndex is not used in derivation // This matches the EVM pocket pattern but adapted for Solana's all-hardened requirement const keypair = SVMDeriveChildPrivateKey(this.seed, 0, `m/44'/501'/${pocketIndex}'/`); const address = keypair.publicKey; // Retry logic with exponential backoff for (let attempt = 0; attempt < maxRetries; attempt++) { try { // Get balance in lamports const balanceLamports = await connection.getBalance(address); // Convert lamports to SOL (9 decimals) const balanceSOL = balanceLamports / 1_000_000_000; return { index: accountIndex, address: address.toString(), derivationPath, nativeBalance: { amount: BigInt(balanceLamports), formatted: balanceSOL, symbol: 'SOL' }, privateKey: base58.encode(keypair.secretKey) }; } catch (error) { if (attempt === maxRetries - 1) { console.error(`Failed to check pocket at account ${accountIndex} after ${maxRetries} attempts:`, error); return null; } // Exponential backoff: 1s, 2s, 4s const backoffMs = 1000 * Math.pow(2, attempt); await this.sleep(backoffMs); } } return null; } } export class SVMChainAddress extends ChainAddress { /** * Create a chain-address wrapper with a managed Solana connection. * * @param config - Chain configuration. * @param address - Wallet public key. * @param index - Optional wallet index. */ constructor(config: ChainWalletConfig, address: PublicKey, index?: number) { const connection = createSvmConnection(config); super(config, address, index); this.connection = connection; } /** * Get native SOL balance for this address. * * @returns Native balance details. */ async getNativeBalance(): Promise { return await getSvmNativeBalanceForAddress(this.address, this.connection!); } /** * Get SPL token balance for this address. * * @param tokenAddress - SPL token mint address. * @returns Token balance details. */ async getTokenBalance(tokenAddress: PublicKey): Promise { return await getSvmTokenBalanceForAddress(this.address, tokenAddress, this.connection!); } /** * Discover SPL tokens held by this address. * * @returns Discovered token balances. */ async discoverToken(): Promise[]> { return await discoverSvmTokens(this.address, this.connection!); } /** * Discover NFTs held by this address. * * @returns Discovered NFTs. */ async discoverNFT(): Promise { return await discoverSvmNFTs(this.address, this.connection!); } /** * Get transaction history for this address. * * @returns Parsed transaction history entries. */ async getTransactionHistory(): Promise { return await getSvmTransactionHistoryForAddress(this.connection!, this.address); } /** * Fetch price data for token mints. * * @param tokenAddresses - Token mint addresses. * @returns Price response. */ async getPrices(tokenAddresses: string[]): Promise { return await getSvmPricesForTokens(this.config, tokenAddresses); } } export class SVMChainWallet extends ChainWallet { /** * Create a chain wallet around a Solana keypair. * * @param config - Chain configuration. * @param privateKey - Wallet keypair. * @param index - Wallet index. */ constructor(config: ChainWalletConfig, privateKey: Keypair, index: number) { const address = privateKey.publicKey; super(config, address, privateKey, index); this.address = privateKey.publicKey; this.privateKey = privateKey; this.connection = createSvmConnection(config); } /** * Get this wallet's public address. * * @returns Solana public key. */ generateAddress() { return this.address; } /** * Convert entropy bytes (as string) into a Solana keypair. * * @param entropy - Entropy string. * @returns Derived keypair. */ convertFromEntropyToPrivateKey = (entropy: string): Keypair => { return Keypair.fromSeed(Buffer.from(entropy)) } /** * Get native SOL balance for this wallet. * * @returns Native balance details. */ async getNativeBalance(): Promise { // Implement native balance retrieval logic here return await getSvmNativeBalanceForAddress(this.address, this.connection!); } /** * Get SPL token balance for this wallet. * * @param tokenAddress - SPL token mint address. * @returns Token balance details. */ async getTokenBalance(tokenAddress: PublicKey): Promise { // Implement token balance retrieval logic here return await getSvmTokenBalanceForAddress(this.address, tokenAddress, this.connection!); } /** * Discover SPL tokens held by this wallet. * * @returns Discovered token balances. */ async discoverToken(): Promise[]> { // Implement token discovery logic here return await discoverSvmTokens(this.address, this.connection!); } /** * Discover NFTs held by this wallet. * * @returns Discovered NFTs. */ async discoverNFT(): Promise { // Implement NFT discovery logic here return await discoverSvmNFTs(this.address, this.connection!); } /** * Transfer native SOL to another address. * * @param to - Recipient public key. * @param amount - Amount of SOL to transfer. * @returns Transaction result containing signature hash. */ async transferNative(to: PublicKey, amount: number): Promise { // Implement native transfer logic here const transaction = await getTransferNativeTransaction( this.privateKey, to, amount, this.connection! ); const hash = await SVMVM.signAndSendTransaction( transaction, this.connection!, this.privateKey ); return { success: true, hash }; } /** * Transfer SPL tokens to another address. * * @param token - Token info to transfer. * @param to - Recipient public key. * @param amount - Amount in UI units. * @returns Transaction result containing signature hash. */ async transferToken(token: TokenInfo, to: PublicKey, amount: number): Promise { // Implement token transfer logic here const transaction = await getTransferTokenTransaction( this.privateKey, new PublicKey(to), token, amount, this.connection! ); const hash = await SVMVM.signAndSendTransaction( transaction, this.connection!, this.privateKey ); return { success: true, hash }; } /** * Sign a transaction with this wallet. * * @param transaction - Legacy or versioned transaction. * @returns Signed transaction. */ async signTransaction(transaction: VersionedTransaction | Transaction) { return await SVMVM.signTransaction(transaction, this.privateKey) } /** * Send a pre-signed transaction to the network. * * @param transaction - Signed legacy or versioned transaction. * @returns Transaction signature hash. */ async sendTransaction(transaction: VersionedTransaction | Transaction) { return await SVMVM.sendTransaction(transaction, this.connection!) } /** * Sign and send a transaction in one step. * * @param transaction - Legacy or versioned transaction. * @returns Transaction signature hash. */ async signAndSendTransaction(transaction: VersionedTransaction | Transaction) { return await SVMVM.signAndSendTransaction(transaction, this.connection!, this.privateKey); } /** * Get transaction history for this wallet. * * @returns Parsed transaction history entries. */ async getTransactionHistory(): Promise { return await getSvmTransactionHistoryForAddress(this.connection!, this.address); } //add gas estimation method here if needed, but note that Solana transactions typically have a fixed fee per signature and do not require gas estimation like EVM chains async estimateGas(transaction: VersionedTransaction | Transaction): Promise { const connection = this.connection!; if (transaction instanceof Transaction) { if (!transaction.feePayer) { transaction.feePayer = this.address; } if (!transaction.recentBlockhash) { const { blockhash } = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; } } const message = transaction instanceof Transaction ? transaction.compileMessage() : transaction.message; const feeResponse = await connection.getFeeForMessage(message); const baseFeeLamports = feeResponse.value ?? 0; const { computeUnitPriceMicroLamports, computeUnitLimit } = this.extractComputeBudgetDetails(transaction); if (computeUnitPriceMicroLamports === 0n) { return baseFeeLamports; } let unitsConsumed: number | undefined; try { const simulation = transaction instanceof Transaction ? await connection.simulateTransaction(transaction) : await connection.simulateTransaction(transaction, { sigVerify: false, replaceRecentBlockhash: true, }); unitsConsumed = simulation.value.unitsConsumed ?? undefined; } catch { // Non-fatal: fall back to declared CU limit or a conservative default. } const effectiveComputeUnits = BigInt( unitsConsumed ?? computeUnitLimit ?? 200_000 ); const priorityFeeLamports = this.ceilDiv( effectiveComputeUnits * computeUnitPriceMicroLamports, 1_000_000n ); const totalFeeLamports = BigInt(baseFeeLamports) + priorityFeeLamports; if (totalFeeLamports > BigInt(Number.MAX_SAFE_INTEGER)) { throw new Error("Estimated fee exceeds JavaScript safe integer range"); } return Number(totalFeeLamports); } private extractComputeBudgetDetails( transaction: VersionedTransaction | Transaction ): { computeUnitPriceMicroLamports: bigint; computeUnitLimit?: number } { let computeUnitPriceMicroLamports = 0n; let computeUnitLimit: number | undefined; if (transaction instanceof Transaction) { for (const instruction of transaction.instructions) { if (!instruction.programId.equals(ComputeBudgetProgram.programId)) { continue; } const data = instruction.data; if (!data || data.length === 0) { continue; } const discriminator = data[0]; if (discriminator === 2 && data.length >= 5) { computeUnitLimit = this.readU32LE(data, 1); } else if (discriminator === 3 && data.length >= 9) { computeUnitPriceMicroLamports = this.readU64LE(data, 1); } } return { computeUnitPriceMicroLamports, computeUnitLimit }; } const message = transaction.message as { compiledInstructions?: Array<{ programIdIndex: number; data: Uint8Array }>; staticAccountKeys?: PublicKey[]; }; const compiledInstructions = message.compiledInstructions ?? []; const staticAccountKeys = message.staticAccountKeys ?? []; for (const instruction of compiledInstructions) { const programId = staticAccountKeys[instruction.programIdIndex]; if (!programId || !programId.equals(ComputeBudgetProgram.programId)) { continue; } const data = instruction.data; if (!data || data.length === 0) { continue; } const discriminator = data[0]; if (discriminator === 2 && data.length >= 5) { computeUnitLimit = this.readU32LE(data, 1); } else if (discriminator === 3 && data.length >= 9) { computeUnitPriceMicroLamports = this.readU64LE(data, 1); } } return { computeUnitPriceMicroLamports, computeUnitLimit }; } private readU32LE(data: Uint8Array, offset: number): number { return ( data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24) ) >>> 0; } private readU64LE(data: Uint8Array, offset: number): bigint { let value = 0n; for (let i = 0; i < 8; i++) { value |= BigInt(data[offset + i]) << BigInt(8 * i); } return value; } private ceilDiv(a: bigint, b: bigint): bigint { return (a + b - 1n) / b; } /** * Get token metadata for an SPL mint. * * @param tokenAddress - SPL token mint address. * @returns Token metadata from `getTokenInfo`. */ async getTokenInfo(tokenAddress: PublicKey) { return await SVMVM.getTokenInfo(tokenAddress, this.connection!) } /** * Fetch prices for provided token mints. * * @param tokenAddresses - Token mint addresses. * @returns Price response. */ async getPrices(tokenAddresses: string[]): Promise { return await getSvmPricesForTokens(this.config, tokenAddresses); } /** * Execute a token swap via Jupiter. * * @param fromToken - Source token info. * @param toToken - Destination token mint. * @param amount - Amount in source token UI units. * @param slippage - Slippage tolerance in basis points. * @returns Transaction result with success/error details. */ async swap(fromToken: TokenInfo, toToken: PublicKey, amount: number, slippage: number = 50): Promise { try { if (amount <= 0) { return { success: false, hash: "", error: "Amount must be greater than 0" }; } if (slippage < 0 || slippage > 5000) { return { success: false, hash: "", error: "Slippage must be between 0 and 5000 basis points (0-50%)" }; } const fromTokenMint = new PublicKey(fromToken.address); const toTokenMint = toToken; // const validation = await validateJupiterTokens( // fromTokenMint.toString(), // toTokenMint.toString() // ); // if (!validation.valid) { // return { // success: false, // hash: "", // error: validation.message || "Token validation failed" // }; // } const baseAmount = uiAmountToBaseUnits(amount, fromToken.decimals); // const balance = await this.getTokenBalance(fromTokenMint); // if (balance.balance.lt(new BN(baseAmount))) { // return { // success: false, // hash: "", // error: "Insufficient balance for swap" // }; // } const swapResult = await executeJupiterSwap( { fromToken: fromTokenMint, toToken: toTokenMint, amount: baseAmount, slippageBps: slippage, userPublicKey: this.address }, this.connection!, this.privateKey ); if (!swapResult.success) { return { success: false, hash: "", error: swapResult.error || "Swap failed" }; } return { success: true, hash: swapResult.hash || "" }; } catch (error) { console.error("Swap error:", error); return { success: false, hash: "", error: error instanceof Error ? error.message : "Unknown swap error occurred" }; } } /** * Fetch a Jupiter quote without sending a transaction. * * @param fromToken - Source token info. * @param toToken - Destination token mint. * @param amount - Amount in source token UI units. * @param slippage - Slippage tolerance in basis points. * @returns Quote payload with input/output values and route data. */ async getSwapQuote(fromToken: TokenInfo, toToken: PublicKey, amount: number, slippage: number = 50): Promise<{ success: boolean; inputAmount?: string; outputAmount?: string; priceImpact?: string; routePlan?: JupiterQuoteResponse['routePlan']; slippageBps?: number; error?: string; }> { try { const fromTokenMint = new PublicKey(fromToken.address); const baseAmount = uiAmountToBaseUnits(amount, fromToken.decimals); const quote = await getJupiterQuote( fromTokenMint.toString(), toToken.toString(), baseAmount, slippage ); return { success: true, inputAmount: quote.inAmount, outputAmount: quote.outAmount, priceImpact: quote.priceImpactPct, routePlan: quote.routePlan, slippageBps: quote.slippageBps }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Failed to get swap quote" }; } } /** * Sign raw message bytes using ed25519 detached signature. * * @param message - Message bytes to sign. * @returns Detached signature bytes. */ signMessage = (message: Uint8Array,) => { const signature = nacl.sign.detached(message, this.privateKey.secretKey); return signature }; }