UNPKG

@shogun-sdk/money-legos

Version:

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

351 lines 15.2 kB
import { Connection, PublicKey } from '@solana/web3.js'; import { createPublicClient, fallback, http, parseEther, parseGwei } from 'viem'; import { isEVMChain, getRpcUrls, SOLANA_CHAIN_ID, isNativeAddress } from '../config/chains.js'; import { arbitrum, avalanche, base, berachain, bsc, mainnet, polygon, sonic } from 'viem/chains'; import { compareAddresses, isWrappedAddress } from './address.js'; import { NATIVE_TOKEN, SOL_NATIVE } from '../config/addresses.js'; import { returnErrorDuringEstimationGas } from './errors.js'; import { convertToIntegerMultiplier } from './eth.js'; import { ethers } from 'ethers'; import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { erc20TokenABI } from './abi.js'; import { HyperEVM } from './viemChains.custom.js'; import { max } from './math.js'; // you can set those differently export const MIN_NATIVE_TOKEN_BALANCE = { [mainnet.id]: 1000000000000000n, // 0.001 ETH [base.id]: 200000000000000n, // 0.0002 ETH [arbitrum.id]: 200000000000000n, // 0.0002 ETH [bsc.id]: 200000000000000n, // 0.0002 ETH [SOLANA_CHAIN_ID]: 5000000n, // 0.005 SOL [berachain.id]: 50000000000000000n, // 0.05 ETH [sonic.id]: 500000000000000000n, // 0.5 SONIC [avalanche.id]: 10000000000000000n, // 0.01 AVAX [polygon.id]: 3000000000000000000n, // 3 POL [HyperEVM.id]: 50000000000000000n, // 0.05 HYPE }; // you can set those differently export const MIN_BRIDGE_FEE_NATIVE_TOKEN = { [mainnet.id]: BigInt(1000000000000000), // 0.001 ETH [base.id]: BigInt(1_000_000_000_000_000), // 0.001 ETH [arbitrum.id]: BigInt(100_000_000_000_000), // 0.0001 ETH [bsc.id]: BigInt(100_000_000_000_000), // 0.0001 ETH [SOLANA_CHAIN_ID]: BigInt(25_000_000), // 0.025 SOL [berachain.id]: BigInt(1_000_000_000_000_000), // 0.001 BERA [sonic.id]: BigInt(500_000_000_000_000_000), // .5 SONIC [avalanche.id]: BigInt(10_000_000_000_000_000), // 0.01 AVAX [polygon.id]: BigInt(11_000_000_000_000_000_000), // 11 POL [HyperEVM.id]: BigInt(1_000_000_000_000_000), // 0.001 HYPE }; export const getEthChainViem = (network) => { const chainMap = { [mainnet.id]: mainnet, [arbitrum.id]: arbitrum, [base.id]: base, [bsc.id]: bsc, [berachain.id]: berachain, [avalanche.id]: avalanche, [sonic.id]: sonic, [polygon.id]: polygon, [HyperEVM.id]: HyperEVM, }; if (network === SOLANA_CHAIN_ID) { throw new Error(`Network "${network}" is not supported in EVM chain configuration.`); } return chainMap[network] ?? base; }; export const getEvmJsonRpcProvider = (network) => { const rpcUrls = getRpcUrls()[network]; const chainViem = getEthChainViem(network); const transports = rpcUrls?.rpc.map((url) => http(url)); return createPublicClient({ chain: chainViem, transport: fallback(transports ?? [], { rank: false, retryCount: 5, retryDelay: 1000, }), }); }; /** * Calculates gasLimit based on estimated transaction gas cost. * Increasing gasLimit to cover unexpected expenses * @param estimatedGas Estimated transaction gas cost */ export const getAdjustedGasLimit = (network, estimatedGas) => { if (network === arbitrum.id) { return BigInt(Math.ceil(Number(estimatedGas) * GAS_LIMIT_MULTIPLIER_ARBITRUM)); } return BigInt(Math.ceil(Number(estimatedGas) * GAS_LIMIT_MULTIPLIER)); }; /** * @deprecated This method uses legacy gas estimation logic and may not reflect * current network conditions accurately. Prefer using `getAdjustedFeesPerGas` * with up-to-date block and pending fee data instead. */ export const getAdjustedFeesPerGasOld = async (provider, userPriorityFeeGwei) => { const fees = await provider.estimateFeesPerGas(); // Minimum tip allowed: 0.1 gwei (100_000_000 wei) const MIN_PRIORITY_FEE = BigInt(100000000); // Use the greater of user-provided tip or minimum allowed const priorityFee = userPriorityFeeGwei >= MIN_PRIORITY_FEE ? userPriorityFeeGwei : MIN_PRIORITY_FEE; // Calculate base fee as: maxFeePerGas (from node) - priorityFee (from node) const baseFee = BigInt(fees.maxFeePerGas) - BigInt(fees.maxPriorityFeePerGas); // Final maxFeePerGas = baseFee + adjusted priority fee const maxFeePerGas = baseFee + priorityFee; return { maxFeePerGas, maxPriorityFeePerGas: priorityFee, }; }; /** * Calculates adjusted `maxFeePerGas` and `maxPriorityFeePerGas` for EIP-1559 transactions * based on a user-defined priority fee in GWEI. * * Ensures the priority fee meets a minimum threshold (0.1 gwei), * and computes `maxFeePerGas` as (baseFee + priorityFee). * * @param provider - An EVM-compatible viem public client (supports `estimateFeesPerGas`) * @param userPriorityFeeGwei - Desired priority fee in wei (e.g., 150_000_000n for 0.15 gwei) * @returns An object containing `maxFeePerGas` and `maxPriorityFeePerGas` in wei */ export const getAdjustedFeesPerGas = async (provider, userPriorityFeeGwei) => { const MIN_PRIORITY_FEE = 100000000n; // 0.1 Gwei in wei // Get estimated EIP-1559 fees const fees = await provider.estimateFeesPerGas(); const rawMaxPriorityFee = BigInt(fees.maxPriorityFeePerGas); // Calculate adjusted priority fee with user-defined boost const adjustedMaxPriorityFeePerGas = max(rawMaxPriorityFee + userPriorityFeeGwei, MIN_PRIORITY_FEE); // Get current base fee from the pending block (fallback to latest if unsupported) let block; try { block = await provider.getBlock({ blockTag: "pending" }); } catch (err) { console.warn("Pending block not supported by this provider, falling back to latest."); block = await provider.getBlock(); } const baseFeePerGas = BigInt(block.baseFeePerGas || 0); // Calculate total max fee per gas const adjustedMaxFeePerGas = baseFeePerGas + adjustedMaxPriorityFeePerGas; return { maxFeePerGas: adjustedMaxFeePerGas, maxPriorityFeePerGas: adjustedMaxPriorityFeePerGas, }; }; /* * Calculates adjusted maxFeePerGas based on estimated maxFeePerGas and gas multiplier * @param maxFeePerGas Estimated maxFeePerGas * @param gasMultiplier Gas price priority multiplier */ export const getAdjustedMaxFeePerGas = (maxFeePerGas, gasMultiplier) => { const integerMultiplier = convertToIntegerMultiplier(gasMultiplier, Number(GAS_PRICE_SCALE)); return (BigInt(maxFeePerGas) * integerMultiplier) / GAS_PRICE_SCALE; }; const GAS_LIMIT_MULTIPLIER = 1.2; // +20% to gas estimation export const GAS_LIMIT_MULTIPLIER_ARBITRUM = 3; // +300% to gas estimation for Arbitrum const GAS_PRICE_SCALE = BigInt(10000000000); export const fetchProviderEstimation = async (provider, quote, from) => { const transaction = quote.calldatas; try { if (quote?.inputAmount?.chainId === sonic.id) { return await provider.estimateGas({ to: transaction.to, data: transaction.data, value: BigInt(transaction.value), account: from, }); } return await provider.estimateGas({ to: transaction.to, data: transaction.data, value: BigInt(transaction.value), account: from, blockTag: 'latest', stateOverride: [ { address: from, balance: parseEther('1000000000'), }, ], }); } catch (error) { const blockNumber = await provider.getBlockNumber(); console.log(`>> fetchProviderEstimation error for block ${blockNumber} with token ${quote.inputAmount?.address} ${quote.inputAmount?.chainId} for token ${quote.outputAmount?.address} ${quote.outputAmount?.chainId} with value ${quote.inputAmount?.value}:`, error.message, error.stack); returnErrorDuringEstimationGas(quote, error); return BigInt(0); } }; export const getQuoteNativeBridgeFee = (quote) => { const isNative = quote?.inputAmount && isNativeAddress(quote.inputAmount?.address); if (isNative && quote.calldatas?.value && BigInt(quote.calldatas.value) > BigInt(0)) { const difference = BigInt(quote.calldatas.value) - BigInt(quote.inputAmount?.value); if (difference > BigInt(0)) { return { bridgeFee: difference, isNativeIncludedInAmountIn: true, isBridgeFeeInValue: false, }; } } // TODO: check if this is correct let bridgeFee = BigInt(0); const checkFee = () => { const isCrossChain = quote?.inputAmount?.chainId !== quote?.outputAmount?.chainId; const fee = isCrossChain ? MIN_BRIDGE_FEE_NATIVE_TOKEN[quote?.inputAmount?.chainId] || BigInt(0) : BigInt(0); return fee; }; if (quote?.bridgeFee) { if (isWrappedAddress?.(quote?.bridgeFee?.token)) { bridgeFee += BigInt(quote?.bridgeFee?.amount); } else if (compareAddresses(quote?.bridgeFee?.token, SOL_NATIVE)) { bridgeFee += BigInt(quote?.bridgeFee?.amount); } else if (compareAddresses(quote?.bridgeFee?.token, NATIVE_TOKEN.ETH)) { bridgeFee += BigInt(quote?.bridgeFee?.amount); } else { bridgeFee = checkFee(); } } else { bridgeFee = checkFee(); } return { bridgeFee, isNativeIncludedInAmountIn: false, isBridgeFeeInValue: !!quote.calldatas?.value && bridgeFee === BigInt(quote.calldatas.value), }; }; export const estimateEvmTransaction = async (network, quote, from, userDefinedFeeGwei) => { const provider = getEvmJsonRpcProvider(network); const transaction = quote.calldatas; const value = BigInt(transaction.value); const userDefinedFeeGweiUnit = parseGwei(String(userDefinedFeeGwei)); // Get gas price and estimated transaction gas cost in parallel const [{ maxFeePerGas, maxPriorityFeePerGas }, gasEstimate] = await Promise.all([ getAdjustedFeesPerGas(provider, userDefinedFeeGweiUnit), // Estimate gas usage for the transaction fetchProviderEstimation(provider, quote, from), ]); const gasLimit = getAdjustedGasLimit(network, gasEstimate); const { bridgeFee, isNativeIncludedInAmountIn, isBridgeFeeInValue } = getQuoteNativeBridgeFee(quote); // Calculate total gas cost in Wei const totalGasCostInWei = BigInt(gasLimit) * maxFeePerGas + (isBridgeFeeInValue ? BigInt(0) : bridgeFee); const minimumNativeBalance = totalGasCostInWei + BigInt(isNativeIncludedInAmountIn ? quote.inputAmount?.value : value); // not adding bridgeFee, because it's already included in the quote.inputAmount.value from dextra // Return the result return { maxFeePerGas, maxPriorityFeePerGas, gasLimit, value: transaction.value ? BigInt(transaction.value) : BigInt(0), estimatedFees: totalGasCostInWei, minimumNativeBalance, provider: provider, bridgeFee, }; }; /** * Creates a EVM connection using ethers with fallback RPC URLs. * Attempts to connect to each RPC URL in order until a working connection is established. * @returns ethers.JsonRpcProvider - a working EVM connection * @throws Error if no RPC URLs are defined or if all connections fail */ export const getEVMEthersProviderWithFallback = async (chainId) => { const rpcUrls = getRpcUrls()[chainId]?.rpc; if (!rpcUrls?.length) { throw new Error(`No RPC URLs defined for chain ${chainId}`); } for (const url of rpcUrls) { const provider = new ethers.JsonRpcProvider(url); try { // Test the provider by requesting the latest block number const blockNumber = await provider.getBlockNumber(); if (typeof blockNumber === 'number') { return provider; } } catch (error) { console.warn(`Failed to connect to RPC URL: ${url}`, error); } } throw new Error('All EVM RPC connections failed'); }; /** * Creates a Solana connection with fallback RPC URLs. * Attempts to connect to each RPC URL in order until a working connection is established. * @returns Promise<Connection> A working Solana connection * @throws Error if no RPC URLs are defined or if all connections fail */ export const getSolanaProviderWithFallback = async () => { const rpcUrls = getRpcUrls()[SOLANA_CHAIN_ID]?.rpc; if (!rpcUrls?.length) { throw new Error('No RPC URLs defined for Solana network'); } const commitment = 'confirmed'; for (const url of rpcUrls) { try { const connection = new Connection(url, { commitment }); await connection.getEpochInfo(); // Verify connection works return connection; } catch (error) { console.warn(`Failed to connect to RPC URL: ${url}`, error); } } throw new Error('All Solana RPC connections failed'); }; export const getSolanaProvider = async () => { return getSolanaProviderWithFallback(); }; export const ZERO_BALANCE = BigInt('0'); export const getTokenBalanceRpc = async (walletAddress, tokenAddress, network) => { try { if (!walletAddress || !tokenAddress) { return ZERO_BALANCE; } if (network === SOLANA_CHAIN_ID) { const ownerPubKey = new PublicKey(walletAddress); const mintPubKey = new PublicKey(tokenAddress); const provider = await getSolanaProvider(); const tokenAccounts = await Promise.all([ provider.getParsedTokenAccountsByOwner(ownerPubKey, { programId: TOKEN_PROGRAM_ID }), provider.getParsedTokenAccountsByOwner(ownerPubKey, { programId: TOKEN_2022_PROGRAM_ID }), ]); const tokenAccount = tokenAccounts .flatMap((accounts) => accounts.value) .find((account) => account.account.data.parsed.info.mint === mintPubKey.toBase58()); if (!tokenAccount) { return ZERO_BALANCE; } else { return BigInt(tokenAccount.account.data.parsed.info.tokenAmount.amount); } } else if (isEVMChain(network)) { const provider = getEvmJsonRpcProvider(network); const balance = (await provider.readContract({ address: tokenAddress, abi: erc20TokenABI, functionName: 'balanceOf', args: [walletAddress], // ...(txReceipt // ? { // blockNumber: (txReceipt as EthTransactionResponse).blockNumber, // } // : {}), })); return balance; } return ZERO_BALANCE; } catch (error) { console.error('Error getting token balance:', error); return ZERO_BALANCE; } }; //# sourceMappingURL=rpc.js.map