UNPKG

@shogun-sdk/money-legos

Version:

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

188 lines (161 loc) 6.7 kB
import { Connection, VersionedTransaction } from '@solana/web3.js'; import { isNativeAddress, SOLANA_CHAIN_ID } from '../config/chains.js'; import type { EVMPublicClient } from '../types/client.js'; import type { Calldatas, QuoteTypes } from '../types/index.js'; import { isWrappedAddress } from '../utils/address.js'; import { getInsufficientGasWebAppError } from '../utils/errors.js'; import { estimateEvmTransaction, getQuoteNativeBridgeFee, getSolanaProvider } from '../utils/rpc.js'; import { getNativeBalanceRpc } from '../utils/getNativeBalance.js'; import { DEFAULT_JITO_TIP } from '../lib/jito.js'; export interface ICollectedFees { totalTxCost: bigint; gasLimit?: bigint; maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint; estimatedFees?: bigint; bridgeFee?: bigint; } // TODO: make dynamic calulation of fee const TODO_BRIDGE_FEE = BigInt(30000000); // 0.03 SOL in lamports export interface ISolanaFeeEstimation { fees: bigint; preBalances?: Record<string, bigint>; postBalances?: Record<string, bigint>; } // 150000 lamports is typical minimum fee for swap const MIN_SOLANA_FEE = 150000; const decodeSolanaTransactions = (calldatas: Calldatas[]): VersionedTransaction[] => { return calldatas?.map((calldata) => { const messageBuffer = Buffer.from(calldata.data, 'base64'); return VersionedTransaction.deserialize(messageBuffer); }); }; export class FeeService { public async estimateSolanaTransactionFees( provider: Connection, transactions: VersionedTransaction[], ): Promise<ISolanaFeeEstimation> { try { // Estimate fees using existing method const fees = await Promise.all( transactions.map(async (tx) => { try { const feeResponse = await provider.getFeeForMessage(tx.message, 'processed'); if (!feeResponse?.value) { return BigInt(MIN_SOLANA_FEE); } return BigInt(feeResponse.value); } catch (err) { console.error('Error getting fee for message:', err); return BigInt(MIN_SOLANA_FEE); } }), ); const totalFee = fees.reduce((acc, fee) => acc + fee, BigInt(0)); // Not possible to simulate bundle for solana https://intensitylabsgroup.slack.com/archives/C06MTL2GR42/p1739272343867779?thread_ts=1739266229.767679&cid=C06MTL2GR42 /* // Simulate bundle to get balance changes try { const simulationResult = await simulateBundle({ encodedTransactions: }); // Extract pre and post balances from simulation if (simulationResult) { // Return both fees and balance changes return { fees: totalFee, preBalances: simulationResult.preBalances, postBalances: simulationResult.postBalances, }; } } catch (err) { console.warn('Failed to simulate bundle for balance changes:', err); } */ // Return just fees if simulation fails return { fees: totalFee }; } catch (err) { console.error('Failed to estimate Solana transaction fees:', err); // Return minimum fee * number of transactions as fallback return { fees: BigInt(MIN_SOLANA_FEE * transactions.length), }; } } // TODO https://intensitylabsgroup.slack.com/archives/C06MTL2GR42/p1739266229767679 // TODO: make dynamic calulation of fee public static getConstSolanaBridgeCostFee = (quote: QuoteTypes) => { const hasBridgeCallata = (quote.calldatas as Calldatas[]).find((tx) => tx.label === 'bridge'); return hasBridgeCallata ? TODO_BRIDGE_FEE : BigInt(0); }; async checkHasSufficientBalanceForGas( senderAddress: string, srcNetwork: number, quote: QuoteTypes, setSuggestedSlippage: (slippage: number) => void, priorityFee: bigint = DEFAULT_JITO_TIP, getErrorFallback: (estimatedFees: bigint, network: number) => string = getInsufficientGasWebAppError, userDefinedFeeGwei: number = 0, ): Promise<{ fees: ICollectedFees; error?: string; provider: EVMPublicClient | Connection | null; }> { const nativeBalance = await getNativeBalanceRpc(senderAddress, srcNetwork); try { if (srcNetwork === SOLANA_CHAIN_ID) { const provider = await getSolanaProvider(); const transactions = decodeSolanaTransactions(quote.calldatas as Calldatas[]); // TODO https://intensitylabsgroup.slack.com/archives/C06MTL2GR42/p1739266229767679 const hasBridgeCalldata = (quote.calldatas as Calldatas[]).find((tx) => tx.label === 'bridge'); const { bridgeFee } = getQuoteNativeBridgeFee(quote); const estimatedFees = (await this.estimateSolanaTransactionFees(provider, transactions)).fees + (BigInt(quote.jitoTipsTotal || BigInt(priorityFee)) + (hasBridgeCalldata ? bridgeFee : BigInt(0))); const isWrapped = isWrappedAddress(quote.inputAmount?.address); const isNative = isNativeAddress(quote.inputAmount?.address); const nativeBalanceUsed = isWrapped || isNative ? BigInt(quote.inputAmount?.value) : BigInt(0); const minimumNativeBalance = estimatedFees + nativeBalanceUsed; // not adding bridgeFee, because it's already included in the quote.inputAmount.value from dextra const isEnoughBalance = BigInt(nativeBalance) >= minimumNativeBalance; return { fees: { totalTxCost: minimumNativeBalance, estimatedFees: estimatedFees, bridgeFee, }, error: isEnoughBalance ? undefined : getErrorFallback(estimatedFees + bridgeFee, srcNetwork), provider: provider as unknown as Connection, }; } else { const { maxFeePerGas, maxPriorityFeePerGas, gasLimit, estimatedFees, minimumNativeBalance, provider, bridgeFee, } = await estimateEvmTransaction(srcNetwork, quote, senderAddress, userDefinedFeeGwei); const isEnoughBalance = BigInt(nativeBalance) >= minimumNativeBalance; return { fees: { maxPriorityFeePerGas, maxFeePerGas, gasLimit, totalTxCost: minimumNativeBalance, estimatedFees, bridgeFee, }, provider, error: isEnoughBalance ? undefined : getErrorFallback(estimatedFees + bridgeFee, srcNetwork), }; } } catch (error: any) { setSuggestedSlippage?.(error.suggestedSlippageValue); return { fees: { totalTxCost: BigInt(0), }, provider: null, error: error.message, }; } } }