UNPKG

@shogun-sdk/money-legos

Version:

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

261 lines 11.6 kB
import axios from 'axios'; import { MIN_NATIVE_TOKEN_BALANCE } from '../utils/rpc.js'; import { getNativeBalanceRpc } from '../utils/getNativeBalance.js'; import { CHAIN_MAP } from '../config/chains.js'; import { compareAddresses } from '../utils/address.js'; import { fromWei } from '../utils/eth.js'; import { AffiliateFeeService, MAX_AFFILIATE_FEE_PERCENT } from './AffiliateFeeService.js'; export class GasStationCalculator { constructor(errorBuilder, balancesClient) { Object.defineProperty(this, "errorBuilder", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "balancesClient", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.errorBuilder = errorBuilder; this.balancesClient = balancesClient; } async getNativeDestinationNetworkPrice(network, tokens) { const nativeToken = tokens.find((token) => token.network === network && token.nativeToken); if (nativeToken) { return nativeToken.usdPrice; } const tokenAddress = CHAIN_MAP[network]?.wrapped; if (!tokenAddress) { return 0; } const coingeckoData = await this.balancesClient.getTokenUSDPrice(tokenAddress, network); return coingeckoData?.priceUsd || 0; } getMinNativeBalance(network) { return MIN_NATIVE_TOKEN_BALANCE[network]; } calculateRefuelFeePercent(refuelAmountUsd, amountInUsd) { if (amountInUsd <= 0) { return 0; } return Number((refuelAmountUsd / amountInUsd) * 110); } async calculateRefuelAmount(srcNetwork, destNetwork, tokenInput, tokens, destinationNativeBalance, swapParams, systemFeePercent, isEoaAccount, externalServiceTxCostUsd) { const minDestinationBalance = this.getMinNativeBalance(destNetwork); const DEFAULT_STATE = { additionalAffiliateFeePercent: 0, refuelAmount: BigInt(0), affiliateFeeTotalValidation: { isValid: true, error: '', totalFeePercent: MAX_AFFILIATE_FEE_PERCENT, }, }; if (!minDestinationBalance) { return DEFAULT_STATE; } const refuelDestinationAmount = minDestinationBalance - destinationNativeBalance; // Calculate required amount of ETH to reach MIN_NATIVE_TOKEN_BALANCE const refuelAmount = refuelDestinationAmount > 0 ? refuelDestinationAmount : BigInt(0); if (refuelAmount <= 0) { return DEFAULT_STATE; } const weiAmount = swapParams.weiAmount; // amount of src token in USD const amountInUsd = weiAmount ? Number(fromWei(weiAmount.toString(), tokenInput.decimals)) * tokenInput.usdPrice : 0; if (!amountInUsd) { return DEFAULT_STATE; } const nativeDestinationNetworkPrice = await this.getNativeDestinationNetworkPrice(destNetwork, tokens); if (!CHAIN_MAP[destNetwork]?.decimals) { return DEFAULT_STATE; } const refuelDestinationAmountUsd = Number(fromWei(refuelAmount.toString(), CHAIN_MAP[destNetwork]?.decimals)) * nativeDestinationNetworkPrice; const additionalAffiliateFeePercent = this.calculateRefuelFeePercent(refuelDestinationAmountUsd, amountInUsd); const excludeFee = false; /*excludeAffiliateFee({ srcToken: tokenInput.tokenAddress, destToken: swapParams.destToken, srcChainId: srcNetwork, destChainId: destNetwork, });*/ // We are using AffiliateFeeService just to valdiate possible affiliate fee for gas refuel // If it is not valid, we will not refuel const affiliateFeeTotalValidation = await AffiliateFeeService.validateAffiliateFeeWithServiceFees({ excludeAffiliateFee: excludeFee, isTransfer: false, tokenInAddress: tokenInput.tokenAddress, tokeinInDecimals: tokenInput.decimals, sourceNetwork: srcNetwork, amountBigInt: weiAmount.toString(), errorBuilder: this.errorBuilder, systemFeePercent, balances: tokens, isEoaAccount, additionalAffiliateFee: additionalAffiliateFeePercent, externalServiceTxCostUsd, }); // no refuel if affiliate fee is not valid or max affiliate fee if (!affiliateFeeTotalValidation?.isValid || affiliateFeeTotalValidation.totalFeePercent === MAX_AFFILIATE_FEE_PERCENT // handle case when affiliate fee is max, we don't want to block the transaction but we will skip the refuel ) { console.log('[GasStationService] Affiliate fee is not valid or max affiliate fee', affiliateFeeTotalValidation?.isValid, affiliateFeeTotalValidation?.totalFeePercent); return { ...DEFAULT_STATE, affiliateFeeTotalValidation: affiliateFeeTotalValidation, gasRefuelWarning: destinationNativeBalance === 0n && this.isCrossChainRefuel(srcNetwork, destNetwork) ? 'Select a native token on destination network to have a gas for future swaps' : undefined, }; } return { additionalAffiliateFeePercent, affiliateFeeTotalValidation, refuelAmount }; } isCrossChainRefuel(srcNetwork, destNetwork) { return srcNetwork !== destNetwork; } findTokenIn(tokens, srcTokenAddress, srcNetwork, walletAddress) { return tokens?.find((token) => compareAddresses(token.tokenAddress, srcTokenAddress) && token.network === srcNetwork && compareAddresses(token.walletAddress, walletAddress)); } async ignoreGasRefuel(tokenOut, walletAddress, network) { if (tokenOut && this.isTokenOutNative(tokenOut.tokenAddress, network)) { return true; } const balance = await getNativeBalanceRpc(walletAddress, network); const minBalance = this.getMinNativeBalance(network); return !minBalance || balance >= minBalance; } isTokenOutNative(address, destinationNetwork) { const chain = CHAIN_MAP[destinationNetwork]; if (!chain?.tokenAddress) { return false; } return compareAddresses(address, chain.tokenAddress); } async calculateGasRefuelAmount(walletAddress, destinationWalletAddress, balances, swapParams, systemFeePercent, isEoaAccount, externalServiceTxCostUsd, findTokenFallback) { const destinationNetwork = swapParams.destNetwork; const DEFAULT_STATE = { success: false, additionalAffiliateFeePercent: 0, refuelAmount: BigInt(0), tokenIn: undefined, destinationWalletAddress: undefined, gasRefuelWarning: undefined, affiliateFeeTotalValidation: { isValid: true, error: '', totalFeePercent: MAX_AFFILIATE_FEE_PERCENT, }, }; if (!destinationWalletAddress) { return DEFAULT_STATE; } const tokenIn = findTokenFallback ? findTokenFallback(balances ?? [], swapParams.srcToken, swapParams.srcNetwork, walletAddress) : this.findTokenIn(balances ?? [], swapParams.srcToken, swapParams.srcNetwork, walletAddress); if (!tokenIn) { return DEFAULT_STATE; } const tokenOut = findTokenFallback ? findTokenFallback(balances ?? [], swapParams.destToken, swapParams.destNetwork, destinationWalletAddress) : this.findTokenIn(balances ?? [], swapParams.destToken, swapParams.destNetwork, destinationWalletAddress); // Check if destination wallet already has enough balance if (await this.ignoreGasRefuel(tokenOut, destinationWalletAddress, destinationNetwork)) { return DEFAULT_STATE; } const balance = await getNativeBalanceRpc(destinationWalletAddress, destinationNetwork); const { additionalAffiliateFeePercent, refuelAmount, gasRefuelWarning, affiliateFeeTotalValidation } = await this.calculateRefuelAmount(swapParams.srcNetwork, swapParams.destNetwork, tokenIn, balances ?? [], balance, swapParams, systemFeePercent, isEoaAccount, externalServiceTxCostUsd); return { success: refuelAmount > BigInt(0), refuelAmount, tokenIn, destinationWalletAddress, additionalAffiliateFeePercent: additionalAffiliateFeePercent, gasRefuelWarning, affiliateFeeTotalValidation, }; } } export class GasStationService extends GasStationCalculator { constructor(apiKey, apiUrl, errorHandler, errorBuilder, balancesClient) { super(errorBuilder, balancesClient); Object.defineProperty(this, "apiKey", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "apiUrl", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "errorHandler", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.apiKey = apiKey; this.apiUrl = apiUrl; this.errorHandler = errorHandler; } async makeGasStationApiRequest(url, payload, headers) { try { await axios.post(url, payload, { headers }); return true; } catch (error) { const errorMessage = `Error sending to gas station: ${error instanceof Error ? error.message : 'Unknown error'}`; this.errorHandler(errorMessage); console.error(errorMessage); return false; } } /** * Sends a request to the Gas Station API to refuel a wallet with native tokens * * Example curl command to test locally: * ``` curl -X POST http://localhost:3000 \ -H "Content-Type: application/json" \ -H "x-api-key: your_api_key" \ -d '{ "chainId": 7565164, "amount": "10000", "recipient": "HxQpBRpH7sRHpidTWZ55xAsMxddhjHBE8BRkkj2ewHa1" }' * ``` */ async sendGasStationRequest(amount, network, walletAddress) { if (!amount || amount === BigInt(0)) { return { success: true }; } // Double-check balance before sending request if (await this.ignoreGasRefuel(undefined, walletAddress, network)) { return { success: false, error: 'Wallet already has sufficient balance' }; } if (!CHAIN_MAP[network]?.id) { return { success: false, error: `Unsupported network ID: ${network}` }; } const url = `${this.apiUrl}/api/refuel`; const payload = { chainId: CHAIN_MAP[network].id, amount: amount.toString(), recipient: walletAddress, }; const headers = { 'Content-Type': 'application/json', 'x-api-key': this.apiKey, }; const success = await this.makeGasStationApiRequest(url, payload, headers); return { success, error: success ? undefined : 'Failed to send gas station request' }; } } //# sourceMappingURL=GasStationService.js.map