UNPKG

@shogun-sdk/money-legos

Version:

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

237 lines 11.1 kB
import { AddressLookupTableAccount, TransactionExpiredBlockheightExceededError, TransactionMessage as Web3TransactionMessage, } from '@solana/web3.js'; import { createRecentSignatureConfirmationPromiseFactory, createBlockHeightExceedencePromiseFactory, waitForRecentTransactionConfirmation, } from '@solana/transaction-confirmation'; import { createSolanaRpc, createSolanaRpcSubscriptions, setTransactionMessageLifetimeUsingBlockhash, pipe, } from '@solana/kit'; import axios from 'axios'; import { getRpcUrls, SOLANA_CHAIN_ID } from '@shogun-sdk/money-legos'; import { fromVersionedTransaction } from '@solana/compat'; // import { default as bs58 } from 'bs58'; export const wait = (time) => new Promise((resolve) => setTimeout(resolve, time)); const SEND_OPTIONS = { skipPreflight: true }; const MAX_RETRIES = 20; const RETRY_DELAY = 200; // 200ms // Add new constants for error handling const RPC_ERROR_CODES = { SERVICE_UNAVAILABLE: 503, RATE_LIMITED: 429, BAD_GATEWAY: 502, }; const isRetryableError = (error) => { if (error instanceof Error) { // Check for specific error codes in the error message const statusCode = error.message.match(/(\d{3})/)?.[1]; if (statusCode) { return [RPC_ERROR_CODES.SERVICE_UNAVAILABLE, RPC_ERROR_CODES.RATE_LIMITED, RPC_ERROR_CODES.BAD_GATEWAY].includes(Number(statusCode)); } // Check for common network error messages return (error.message.includes('Service Unavailable') || error.message.includes('Failed to fetch') || error.message.includes('network error') || error.message.includes('timeout')); } return false; }; // for transfers only export async function transactionSenderAndConfirmationWaiter({ connection, versionedTransaction, }) { for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { try { const serializedTransaction = Buffer.from(versionedTransaction.serialize()); const txid = await connection.sendRawTransaction(serializedTransaction, SEND_OPTIONS); // Verify transaction confirmation and return txid if successful const transactionConfirmed = await checkTransactionConfirmation(connection, versionedTransaction, txid); if (transactionConfirmed) return txid; console.error('Failed to get transaction after multiple attempts'); return null; } catch (e) { if (e instanceof TransactionExpiredBlockheightExceededError) { console.warn(`Transaction expired on attempt ${attempt + 1}. Retrying...`); if (attempt < MAX_RETRIES - 1) { await wait(RETRY_DELAY); continue; } console.error('Max retries reached. Transaction repeatedly expired.'); return null; } // Handle retryable RPC errors if (isRetryableError(e)) { const backoffDelay = Math.min(RETRY_DELAY * Math.pow(2, attempt), 5000); // Exponential backoff capped at 5s console.warn(`RPC error on attempt ${attempt + 1}, retrying in ${backoffDelay}ms:`, e); if (attempt < MAX_RETRIES - 1) { await wait(backoffDelay); continue; } } console.error('Error confirming transaction:', e); } } return null; } export const sendBundleUsingJitoApi = async (transactionBase64Array) => { const endpoints = [ 'https://mainnet.block-engine.jito.wtf/api/v1/bundles', 'https://amsterdam.mainnet.block-engine.jito.wtf/api/v1/bundles', 'https://frankfurt.mainnet.block-engine.jito.wtf/api/v1/bundles', 'https://ny.mainnet.block-engine.jito.wtf/api/v1/bundles', 'https://tokyo.mainnet.block-engine.jito.wtf/api/v1/bundles', ]; const requests = endpoints.map((url) => axios.post(url, { jsonrpc: '2.0', id: 1, method: 'sendBundle', params: [transactionBase64Array], }, { headers: { 'Content-Type': 'application/json', 'x-jito-auth': process.env.JITO_AUTH_KEY, }, })); const results = await Promise.all(requests.map((p) => p.catch((e) => e))); const successfulResults = results.filter((result) => !(result instanceof Error) && result.data && !result.data.error); if (successfulResults.length === 0) { console.error('All Jito sendBundle requests failed'); return null; } // Return the result from the first successful request return successfulResults[0].data.result; }; export const sendTransactionUsingJitoApi = async (transactionBase64) => { try { const endpoints = [ 'https://mainnet.block-engine.jito.wtf/api/v1/transactions', 'https://amsterdam.mainnet.block-engine.jito.wtf/api/v1/transactions', 'https://frankfurt.mainnet.block-engine.jito.wtf/api/v1/transactions', 'https://ny.mainnet.block-engine.jito.wtf/api/v1/transactions', 'https://tokyo.mainnet.block-engine.jito.wtf/api/v1/transactions', ]; // Create a promise for each endpoint that resolves on success or rejects on error const requests = endpoints.map((url) => new Promise((resolve, reject) => { axios .post(url, { jsonrpc: '2.0', id: 1, method: 'sendTransaction', params: [transactionBase64], }, { headers: { 'Content-Type': 'application/json', 'x-jito-auth': process.env.JITO_AUTH_KEY, }, }) .then((response) => { if (response.data && !response.data.error) { resolve(response.data.result); } else { console.error('>>> JITO ERROR', response.data.error); reject(new Error(`Request failed for ${url}`)); } return response; }) .catch((e) => { console.error('>>> JITO ERROR', e); reject(new Error('All Jito sendTransaction requests failed')); }); })); // Wait for the first successful response const result = await Promise.any(requests).catch(() => { return null; }); if (!result) { console.error('All Jito sendTransaction requests failed'); return ''; } return result; } catch (error) { console.error('Error in sendTransactionUsingJito:', error); throw error; } }; export async function getBundleStatuses(bundleID) { const { data } = await axios.post('https://mainnet.block-engine.jito.wtf/api/v1/bundles', { jsonrpc: '2.0', id: 1, method: 'getBundleStatuses', params: [[bundleID]], // Allows multiple IDs }, { headers: { 'Content-Type': 'application/json', 'x-jito-auth': process.env.JITO_AUTH_KEY, }, }); const result = data.result.value[0] ?? {}; return { status: result.confirmation_status, transactions: result.transactions, }; } export async function checkTransactionConfirmation(connection, versionedTx, signature) { try { // Get RPC endpoint from connection const rpcEndpoint = getRpcUrls()[SOLANA_CHAIN_ID]?.rpc[0]; const rpcWsEndpoint = rpcEndpoint?.replace('https://', 'wss://').replace('http://', 'ws://'); if (!rpcEndpoint || !rpcWsEndpoint) { throw new Error('RPC endpoint or RPC WebSocket endpoint not found'); } // Create RPC clients for the new Solana web3.js const rpc = createSolanaRpc(rpcEndpoint); const rpcSubscriptions = createSolanaRpcSubscriptions(rpcWsEndpoint); // Create promise factories const getRecentSignatureConfirmationPromise = createRecentSignatureConfirmationPromiseFactory({ rpc, rpcSubscriptions, }); const getBlockHeightExceedencePromise = createBlockHeightExceedencePromiseFactory({ rpc, rpcSubscriptions, }); // Get the latest blockhash for block height tracking const { lastValidBlockHeight, blockhash } = await connection.getLatestBlockhash(); if (versionedTx.message.addressTableLookups.length > 0) { // Resolve address table lookups first const addressLookupTableAccounts = await Promise.all(versionedTx.message.addressTableLookups.map(async (lookup) => { const accountInfo = await connection.getAccountInfo(lookup.accountKey); if (!accountInfo) { console.error(`Failed to fetch account info for ${lookup.accountKey.toString()}`); return null; } const state = AddressLookupTableAccount.deserialize(accountInfo.data); return new AddressLookupTableAccount({ key: lookup.accountKey, state }); })); // Filter out null values const validAddressLookupTableAccounts = addressLookupTableAccounts.filter((account) => account !== null); // Now decompile with resolved lookup tables const transactionMessage = Web3TransactionMessage.decompile(versionedTx.message, { addressLookupTableAccounts: validAddressLookupTableAccounts, }); versionedTx.message = transactionMessage.compileToLegacyMessage(); } // Error confirming transaction: Failed to get account keys because address table lookups were not resolved Error: Failed to get account keys because address table lookups were not resolved const modernTransaction = fromVersionedTransaction(versionedTx); const transactionMessage = pipe(modernTransaction, (tx) => setTransactionMessageLifetimeUsingBlockhash({ blockhash: blockhash, lastValidBlockHeight: BigInt(lastValidBlockHeight), }, tx)); // Wait for confirmation using websockets await waitForRecentTransactionConfirmation({ commitment: 'confirmed', transaction: transactionMessage, getBlockHeightExceedencePromise: ({ abortSignal }) => getBlockHeightExceedencePromise({ abortSignal, lastValidBlockHeight: BigInt(lastValidBlockHeight), }), getRecentSignatureConfirmationPromise: ({ abortSignal }) => getRecentSignatureConfirmationPromise({ abortSignal, commitment: 'confirmed', signature: signature, }), }); return true; } catch (error) { console.error('Error confirming transaction:', error?.message, error?.stack); return false; } } //# sourceMappingURL=transactionSenderSol.js.map