UNPKG

@shogun-sdk/money-legos

Version:

Shogun Money Legos: clients and types for quotes, memes, prices, balances, fees, validations, etc.

371 lines (325 loc) 11.7 kB
import { AddressLookupTableAccount, Connection, PublicKey, type TransactionError, TransactionMessage, VersionedTransaction, } from '@solana/web3.js'; import axios, { type AxiosRequestConfig } from 'axios'; import bs58 from 'bs58'; import type { SimulateBundleOptions, SimulateBundleResponse } from '../types/index.js'; import { compareAddresses } from '../utils/address.js'; import { getSolanaProvider } from '../utils/rpc.js'; import { getRpcUrls, SOLANA_CHAIN_ID } from '../config/chains.js'; export const SLIPPAGE_ERROR_CODE = 6001; type SolanaError = | { InstructionError: [number, { Custom?: number }] } // | string | TransactionError | null; type ConfirmResult = { success: boolean; error?: string; isTimeout?: boolean; slot?: number; transactionResponse?: { transactionHash: string; slot: number; }; }; export const sendTransactionUsingJito = async (transactionBase64: string): Promise<string> => { try { const response = await axios.post(`/api/jito/transaction`, { transaction: transactionBase64, }); if (!response.data || response.data.error) { console.error('>>> JITO ERROR', response.data?.error); throw new Error('Jito sendTransaction request failed'); } console.log('>>> JITO RESULT', response.data.result); return response.data.result; } catch (error) { console.error('Error in sendTransactionUsingJito:', error); throw error; } }; export const sendBundleUsingJito = async (transactionBase64Array: string[]) => { try { console.log('Sending bundle to Jito via backend proxy...'); const response = await axios.post(`/api/jito/bundle`, { transactions: transactionBase64Array, }); if (!response.data || response.data.error) { throw new Error('Jito sendBundle request failed'); } return response.data.result; } catch (error) { console.error('Error in sendBundleUsingJito:', error); throw error; } }; export function isValidSolanaSignature(signature: string): boolean { try { // Check if the signature is a string if (typeof signature !== 'string') { return false; } // Decode the base58 string const decoded = bs58.decode(signature); // Solana signatures are always 64 bytes (512 bits) long return decoded.length === 64; } catch (error: unknown) { console.log({ error }); return false; } } export const confirmTransaction = async ( transactionHash: string, options: { maxRetries?: number; commitment?: 'processed' | 'confirmed' | 'finalized'; checkInterval?: number; } = {}, ): Promise<ConfirmResult> => { const { maxRetries = 10, commitment = 'confirmed', checkInterval = 200 } = options; for (let attempt = 0; attempt < maxRetries; attempt++) { try { if (attempt === 0) { await new Promise((resolve) => setTimeout(resolve, 2000)); } const solanaProvider = await getSolanaProvider(); const status = await solanaProvider.getSignatureStatus(transactionHash); if (!status?.value) { console.log('Transaction status check: Transaction not found, retrying...'); await new Promise((resolve) => setTimeout(resolve, checkInterval)); continue; } if (status.value.err) { const errorDetails = status.value.err; return { success: false, error: parseInstructionError(errorDetails as SolanaError), slot: status.context.slot, }; } const confirmationLevel = status.value.confirmationStatus ?? 'unknown'; if (!['processed', 'confirmed', 'finalized'].includes(confirmationLevel)) { console.warn(`Unknown confirmation level: ${confirmationLevel}`); continue; } const isConfirmed = (commitment === 'processed' && confirmationLevel !== 'unknown') || (commitment === 'confirmed' && ['confirmed', 'finalized'].includes(confirmationLevel)) || (commitment === 'finalized' && confirmationLevel === 'finalized'); if (isConfirmed) { return { success: true, transactionResponse: { transactionHash, slot: status.context.slot, }, }; } console.warn(`Attempt ${attempt + 1} waiting for ${commitment} confirmation`); const backoffTime = Math.min(1000 * Math.pow(2, attempt), 10000); await new Promise((resolve) => setTimeout(resolve, backoffTime)); } catch (error) { console.warn(`Attempt ${attempt + 1} failed:`, error); if (attempt >= maxRetries - 1) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error', }; } } } return { success: false, error: 'Max retries exceeded', isTimeout: true, }; }; export const parseInstructionError = (err: SolanaError): string => { try { if (err && typeof err === 'object' && 'InstructionError' in err && Array.isArray(err.InstructionError)) { const [index, details] = err.InstructionError; // Check for slippage tolerance error if (index === 1 && details?.Custom === SLIPPAGE_ERROR_CODE) { return 'Slippage tolerance exceeded'; } } return JSON.stringify(err); } catch (error) { return 'Unknown instruction error'; } }; export const SOL_NATIVE_DEXTRA = '11111111111111111111111111111111'; export const WRAPPED_SOL_MINT = 'So11111111111111111111111111111111111111112'; export const NATIVE_SOL_MINT = 'So11111111111111111111111111111111111111111'; export const getNormalizedSolanaToken = (address: string): string => { return compareAddresses(address, NATIVE_SOL_MINT) || compareAddresses(address, SOL_NATIVE_DEXTRA) ? WRAPPED_SOL_MINT : address; }; export interface SimulationResult { preBalances?: Record<string, bigint>; postBalances?: Record<string, bigint>; success: boolean; } export async function simulateBundle({ encodedTransactions }: SimulateBundleOptions): Promise<SimulationResult | null> { const endpoint = getRpcUrls()[SOLANA_CHAIN_ID]?.rpc[0]; if (!endpoint) { throw new Error('JITO_RPC Missing for simulateBundle'); } const config: AxiosRequestConfig = { headers: { 'Content-Type': 'application/json', }, }; const data = { jsonrpc: '2.0', id: 1, method: 'simulateBundle', params: [ { encodedTransactions, config: { sigVerify: false, replaceRecentBlockhash: true, commitment: 'confirmed', }, }, ], }; try { const response = await axios.post<SimulateBundleResponse>(endpoint, data, config); console.log('response', response.data); if (!response.data?.result?.value) { return null; } const result: any = response.data.result.value; // Extract pre and post balances from accounts const preBalances: Record<string, bigint> = {}; const postBalances: Record<string, bigint> = {}; if (result.preTokenBalances) { result.preTokenBalances.forEach((balance: any) => { if (balance?.account && balance?.uiTokenAmount?.amount) { preBalances[balance.account] = BigInt(balance.uiTokenAmount.amount); } }); } if (result.postTokenBalances) { result.postTokenBalances.forEach((balance: any) => { if (balance?.account && balance?.uiTokenAmount?.amount) { postBalances[balance.account] = BigInt(balance.uiTokenAmount.amount); } }); } // Also include SOL balances if available if (result.preBalances) { Object.entries(result.preBalances).forEach(([address, balance]: [string, any]) => { preBalances[address] = BigInt(balance); }); } if (result.postBalances) { Object.entries(result.postBalances).forEach(([address, balance]: [string, any]) => { postBalances[address] = BigInt(balance); }); } return { success: result.summary === 'succeeded', preBalances, postBalances, }; } catch (error) { console.error('Error simulating bundle:', error); return null; } } export async function processTransactionsWithFeePayer( input: { data: string }[] | VersionedTransaction, connection: Connection, senderAddressString: string, feePayerAddressString: string, ): Promise<VersionedTransaction[]> { const senderAddress = new PublicKey(senderAddressString); const feePayerAddress = new PublicKey(feePayerAddressString); const transactions: VersionedTransaction[] = []; async function processSingleTransaction(tx: VersionedTransaction): Promise<VersionedTransaction> { // Resolve any address lookup tables. const addressTableLookups = tx.message.addressTableLookups; const lookupTableAccounts: AddressLookupTableAccount[] = addressTableLookups.length > 0 ? await Promise.all( addressTableLookups.map(async (lookup) => { const response = await connection.getAccountInfo(lookup.accountKey); if (response === null) { throw new Error(`Address lookup table account not found: ${lookup.accountKey.toBase58()}`); } return new AddressLookupTableAccount({ key: lookup.accountKey, state: AddressLookupTableAccount.deserialize(response.data), }); }), ) : []; // Decompile the transaction message. const message = TransactionMessage.decompile(tx.message, { addressLookupTableAccounts: lookupTableAccounts, }); // Override fee payer instructions. overrideFeePayerInMsgInstructions(message, senderAddress, feePayerAddress); // Set the payer key to fee payer. message.payerKey = feePayerAddress; // Update the blockhash. const { blockhash } = await connection.getLatestBlockhash(); message.recentBlockhash = blockhash; // Recompile the message. const compiledMessage = message.compileToV0Message(lookupTableAccounts); return new VersionedTransaction(compiledMessage); } if (Array.isArray(input)) { // Process an array of calldatas. for (const calldata of input) { const messageBuffer = Buffer.from(calldata.data, 'base64'); const originalTransaction = VersionedTransaction.deserialize(messageBuffer); const newTx = await processSingleTransaction(originalTransaction); transactions.push(newTx); } } else { // Input is a single VersionedTransaction. const newTx = await processSingleTransaction(input); transactions.push(newTx); } return transactions; } function overrideFeePayerInMsgInstructions( message: TransactionMessage, senderAddress: PublicKey, feePayerAddress: PublicKey, ): void { const SYSTEM_PROGRAM_ID = new PublicKey('11111111111111111111111111111111'); const ATA_PROGRAM_ID = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'); message.instructions = message.instructions.map((instruction) => { // For System Program instructions (e.g. Jito tip) if (instruction.programId.equals(SYSTEM_PROGRAM_ID)) { instruction.keys = instruction.keys.map((key, index) => { if (index === 0 && key.isSigner && key.pubkey.equals(senderAddress)) { return { ...key, pubkey: feePayerAddress }; } return key; }); } // For ATA creation instructions if (instruction.programId.equals(ATA_PROGRAM_ID)) { instruction.keys = instruction.keys.map((key, index) => { if (index === 0 && key.isSigner && key.pubkey.equals(senderAddress)) { return { ...key, pubkey: feePayerAddress }; } return key; }); } return instruction; }); }