@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
text/typescript
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;
});
}