@shogun-sdk/money-legos
Version:
Shogun Money Legos: clients and types for quotes, memes, prices, balances, fees, validations, etc.
171 lines (167 loc) • 8.89 kB
JavaScript
import { isEVMChain, SOLANA_CHAIN_ID } from '../config/chains.js';
import { STABLECOINS } from '../config/stablecoins.js';
import { compareAddresses, compareChains } from '../utils/address.js';
import { NATIVE_TOKEN, SOL_NATIVE } from '../config/addresses.js';
import { InsufficientFundsErrorBuilder } from './InsufficientFundsErrorBuilder.js';
export const MAX_AFFILIATE_FEE_PERCENT = 5; // 5% fee
export const TURNKEY_TX_COST_USD = 0.11; // 0.11 USD (because slippage is 1-10%, 10 cents + 1 cent)
export class AffiliateFeeService {
/**
* Calculates minimum allowed USD value for a token to keep total fees under 5%
* @param network The blockchain network
* @param excludeAffiliateFees Whether to exclude affiliate fees from calculation
* @returns Minimum USD value required to keep fees under 5%
*/
static calculateMinAllowedUsdValue(systemFeePercent, chainId, excludeAffiliateFees, isEoaAccount = false, externalServiceTxCostUsd = TURNKEY_TX_COST_USD, isSameChainTransfer = false, // just transfer without swap
additionalAffiliateFee) {
const turnkeyFeeUsd = isEoaAccount
? 0
: isSameChainTransfer
? externalServiceTxCostUsd
: AffiliateFeeService.getChainFee(chainId, externalServiceTxCostUsd); // Fixed chain fee in USD
const affiliateFeePercent = (excludeAffiliateFees ? 0 : systemFeePercent) + additionalAffiliateFee;
// Formula derivation:
// totalFeePercent = affiliateFeePercent + (chainFee/(amount - affiliateFeePercent * amount/100))*100 <= MAX_TOTAL_FEE_PERCENT
// Formula derivation:
// Let x be the minimum amount in USD
// Let f be affiliate fee percent (e.g. 1%)
// Let c be chain fee in USD (fixed)
// Let m be max total fee percent (e.g. 5%)
// Total fee percent = affiliate fee percent + (chain fee / amount after affiliate fee) * 100
// affiliate fee percent + (c / (x * (1 - f/100))) * 100 <= m
// Solving for x:
// (c * 100) / (x * (1 - f/100)) <= m - f
// (c * 100) / (m - f) <= x * (1 - f/100)
// x >= (c * 100) / ((m - f) * (1 - f/100))
// Example calculation:
// For $2.77 input with 1% affiliate fee and $0.11 chain fee:
// 1 + (0.11/(2.77-1*2.77/100)) * 100 = 5%
// This shows that $2.77 is the minimum amount needed to keep total fees under 5%
const maxTotalFeePercent = MAX_AFFILIATE_FEE_PERCENT;
// If affiliate fee equals or exceeds max total fee, no amount is sufficient
if (affiliateFeePercent >= maxTotalFeePercent) {
return 5; // $5 is minimum amount to keep total fees under 5%
}
const denominator = (maxTotalFeePercent - affiliateFeePercent) * (1 - affiliateFeePercent / 100);
const minAmount = (turnkeyFeeUsd * 100) / denominator;
return Math.max(minAmount, 0);
}
/**
* Calculates the minimum required amount in token's smallest unit based on its USD price
*/
/**
* Calculates total fee percentage based on input amount and token price
*/
static calculateTotalFeePercentage(chainId, systemFeePercent, excludeAffiliateFees, amountInTokens, decimals, tokenUsdPrice, isEoaAccount = false, additionalAffiliateFee = 0) {
const serviceFees = excludeAffiliateFees ? 0 : systemFeePercent;
if (!tokenUsdPrice || tokenUsdPrice <= 0) {
return serviceFees;
}
if (isEoaAccount) {
return Number(serviceFees) + additionalAffiliateFee;
}
const amountInUsd = (Number(amountInTokens) * tokenUsdPrice) / 10 ** decimals;
// if amountInUsd less 5$, make affiliate fee 5%
if (amountInUsd < 5) {
return MAX_AFFILIATE_FEE_PERCENT;
}
// Then calculate turnkey fee percentage based on remaining amount
const chainFee = this.getChainFee(chainId);
const turnkeyFeePercent = isEoaAccount ? 0 : (chainFee / amountInUsd) * 100;
const totalFeePercent = (Number(serviceFees) + turnkeyFeePercent + additionalAffiliateFee).toFixed(10);
return +totalFeePercent;
}
static isOverMaxAffiliateFee(totalFeePercent) {
return totalFeePercent > MAX_AFFILIATE_FEE_PERCENT;
}
}
Object.defineProperty(AffiliateFeeService, "getChainFee", {
enumerable: true,
configurable: true,
writable: true,
value: (network, externalServiceTxCostUsd = TURNKEY_TX_COST_USD) => {
if (network === SOLANA_CHAIN_ID) {
return externalServiceTxCostUsd;
}
else if (isEVMChain(network)) {
return externalServiceTxCostUsd * 2; // approve + swap tx cost
}
return 0;
}
});
/**
* Validates transaction size and returns total fee percentage
*/
Object.defineProperty(AffiliateFeeService, "validateAffiliateFeeWithServiceFees", {
enumerable: true,
configurable: true,
writable: true,
value: ({ systemFeePercent, additionalAffiliateFee, balances, excludeAffiliateFee, isTransfer, tokenInAddress, tokeinInDecimals, sourceNetwork, amountBigInt, errorBuilder, isEoaAccount, externalServiceTxCostUsd, }) => {
const { tokenUsdPrice } = AffiliateFeeService.getAffiliateFeeTokenForQuote(balances, tokenInAddress, sourceNetwork) || {};
const totalFeePercent = AffiliateFeeService.calculateTotalFeePercentage(sourceNetwork, systemFeePercent, excludeAffiliateFee, BigInt(amountBigInt), tokeinInDecimals, tokenUsdPrice, isEoaAccount, additionalAffiliateFee);
if (totalFeePercent >= 100 ||
!Number.isFinite(totalFeePercent) ||
AffiliateFeeService.isOverMaxAffiliateFee(totalFeePercent)) {
return {
totalFeePercent: Math.min(totalFeePercent, MAX_AFFILIATE_FEE_PERCENT),
isValid: true, // true, because we don't want to block the transaction and just set affiliate fee as MAX_AFFILIATE_FEE_PERCENT
error: InsufficientFundsErrorBuilder.buildAffiliateFeeErrorMsg(amountBigInt, balances, excludeAffiliateFee, tokenInAddress, sourceNetwork, isTransfer, systemFeePercent, isEoaAccount, externalServiceTxCostUsd, additionalAffiliateFee, errorBuilder),
};
}
return {
isValid: true,
totalFeePercent: Math.min(totalFeePercent, MAX_AFFILIATE_FEE_PERCENT),
};
}
});
/*
Affiliate fee can be taken from amount in or amount out
We should get the token from user balances (in case of having in balances)
In case of `buy` action
- fee is taken directly from token in or from token out after swap to token out
- in case of sell action: same as buy action
In both cases it will be taken directly from token in (from balances) or from token out after swap to token out
We are not searching token from queriedToken or from codex, because we can't be sure 100% that swap price is correct. (for example crosschain swap
USDC base => ETH base => SOL solana => TRUMP solana, but we always have USDC base in balances), so we can take +- fee USDC base amount
It is more simple than calculating fee from token out after swap to token output
*/
Object.defineProperty(AffiliateFeeService, "getAffiliateFeeTokenForQuote", {
enumerable: true,
configurable: true,
writable: true,
value: (balances, tokenAddress, sourceNetwork) => {
// Check if token is a stablecoin - if so return $1 price
for (const chainStablecoins of Object.values(STABLECOINS)) {
for (const stablecoin of Object.values(chainStablecoins)) {
if (compareAddresses(stablecoin.address, tokenAddress)) {
return {
tokenUsdPrice: 1,
decimals: stablecoin.decimals,
symbol: stablecoin.symbol,
tokenAddress: stablecoin.address,
};
}
}
}
if (!balances) {
return undefined;
}
let fromBalances = balances.find((token) => compareAddresses(token.tokenAddress, tokenAddress) && compareChains(token.network, sourceNetwork));
if (!fromBalances && tokenAddress === NATIVE_TOKEN.SOL) {
fromBalances = balances.find((token) => compareAddresses(token.tokenAddress, SOL_NATIVE));
}
if (!fromBalances && tokenAddress === SOL_NATIVE) {
fromBalances = balances.find((token) => compareAddresses(token.tokenAddress, SOL_NATIVE));
}
if (fromBalances) {
return {
tokenUsdPrice: fromBalances.usdPrice,
decimals: fromBalances.decimals,
symbol: fromBalances.symbol,
tokenAddress: fromBalances.tokenAddress,
};
}
return undefined;
}
});
//# sourceMappingURL=AffiliateFeeService.js.map