@shogun-sdk/money-legos
Version:
Shogun Money Legos: clients and types for quotes, memes, prices, balances, fees, validations, etc.
237 lines • 11.1 kB
JavaScript
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