@shogun-sdk/money-legos
Version:
Shogun Money Legos: clients and types for quotes, memes, prices, balances, fees, validations, etc.
261 lines • 11.6 kB
JavaScript
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