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