@shogun-sdk/money-legos
Version:
Shogun Money Legos: clients and types for quotes, memes, prices, balances, fees, validations, etc.
388 lines (337 loc) • 13.2 kB
text/typescript
import { SHOGUN_MULTICALL_V1_ABI } from '../config/abis/shogunMulticallV1.abi.js';
import { encodeFunctionData } from 'viem';
import { ethers } from 'ethers';
import { FourMeme, FourMemeTokenInfo } from '../services/index.js';
import { QuoteTypes } from '../types/index.js';
import { compareAddresses } from '../utils/address.js';
import { NATIVE_TOKEN } from '../config/addresses.js';
import { QuoteParams } from '../utils/quote.js';
import { getQuote, getTokenData } from '../lib/index.js';
import { ExternalCallMode } from '../types/externalCall.js';
import { ExternalCallType } from '../types/externalCall.js';
import {
SHOGUN_MULTICALL_V1_ADDRESS,
FOUR_MEME_V2_BUYER_ADDRESS,
ZERO_ADDRESS,
ZERO_HASH,
} from '../constants/index.js';
import { EvmExternalCall } from '../types/externalCall.js';
import {
BSC_CHAIN_ID,
FOUR_MEME_TOKEN_MANAGER_2_LITE_ABI,
FOUR_MEME_V2_BUYER_ABI,
SOLANA_CHAIN_ID,
} from '../config/index.js';
import { collectAffiliateFeeBeforeSwap, formatTokenAmount } from '../utils/index.js';
import { calculateAffiliateFeeAndUpdateAmountIn } from '../utils/index.js';
import { CallStruct } from '../types/multicall.js';
export async function tryHandleFourMemeQuote(
apiKey: string,
apiUrl: string,
params: QuoteParams,
): Promise<{ isHandled: boolean; quote?: QuoteTypes; error?: string }> {
let fourMemeClient: FourMeme;
try {
fourMemeClient = await FourMeme.create();
} catch {
return { isHandled: true, error: 'Could not fetch quote. All BSC RPC connections failed.' };
}
const [isSrcTokenFourMeme, isDestTokenFourMeme] = await Promise.all([
fourMemeClient.tokenManagerHelperV3.getTokenInfo(params.srcToken),
fourMemeClient.tokenManagerHelperV3.getTokenInfo(params.destToken),
]);
// if none of the tokens is a FourMeme token then its not a FourMeme swap
if (!isSrcTokenFourMeme?.version && !isDestTokenFourMeme?.version) {
return { isHandled: false };
}
if (isSrcTokenFourMeme?.version && isDestTokenFourMeme?.version) {
return { isHandled: true, error: 'Could not fetch quote. Both source and destination are FourMeme tokens.' };
}
const { fourMemeTokenInfo, fourMemeToken } = (
isSrcTokenFourMeme?.version
? { fourMemeTokenInfo: isSrcTokenFourMeme, fourMemeToken: params.srcToken }
: { fourMemeTokenInfo: isDestTokenFourMeme, fourMemeToken: params.destToken }
) as { fourMemeTokenInfo: FourMemeTokenInfo; fourMemeToken: string };
const tokenValidationResult = await validateFourMemeToken(fourMemeTokenInfo);
if (!tokenValidationResult.isValid) {
return tokenValidationResult.result;
}
return handleFourMemeQuote(apiKey, apiUrl, params, fourMemeClient, fourMemeToken, fourMemeTokenInfo);
}
async function validateFourMemeToken(
tokenInfo: FourMemeTokenInfo,
): Promise<{ isValid: true } | { isValid: false; result: { isHandled: boolean; error?: string } }> {
// if the token is already listed on PancakeSwap, we fetch the quote from the Shogun API
if (tokenInfo.liquidityAdded) {
return { isValid: false, result: { isHandled: false } };
}
if (tokenInfo.quote !== ethers.ZeroAddress) {
return { isValid: false, result: { isHandled: true, error: 'Could not fetch quote. Quote token is not BNB.' } };
}
// V1 tokens are not supported (created before September 5, 2024)
if (tokenInfo.version !== BigInt(2)) {
return {
isValid: false,
result: {
isHandled: true,
error: 'Could not fetch quote. V1 Four.meme tokens are not supported. Please use V2 tokens.',
},
};
}
return { isValid: true };
}
async function handleFourMemeQuote(
apiKey: string,
apiUrl: string,
params: QuoteParams,
fourMemeClient: FourMeme,
fourMemeToken: string,
fourMemeTokenInfo: FourMemeTokenInfo,
): Promise<{ isHandled: boolean; quote?: QuoteTypes; error?: string }> {
if (compareAddresses(params.destToken, fourMemeToken)) {
return handleFourMemeBuyQuote(apiKey, apiUrl, params, fourMemeClient, fourMemeToken, fourMemeTokenInfo);
} else {
return handleFourMemeSellQuote(params, fourMemeClient, fourMemeToken, fourMemeTokenInfo);
}
}
async function handleFourMemeBuyQuote(
apiKey: string,
apiUrl: string,
params: QuoteParams,
fourMemeClient: FourMeme,
tokenAddress: string,
tokenInfo: FourMemeTokenInfo,
): Promise<{ isHandled: boolean; quote?: QuoteTypes; error?: string }> {
if (params.srcChain === BSC_CHAIN_ID && compareAddresses(params.srcToken, NATIVE_TOKEN.ETH)) {
return handleFourMemeBuyQuoteWithBNBSrcToken(params, fourMemeClient, tokenAddress, tokenInfo);
} else {
return handleFourMemeBuyQuoteWithNonBNBSrcToken(apiKey, apiUrl, params, fourMemeClient, tokenAddress, tokenInfo);
}
}
async function handleFourMemeBuyQuoteWithBNBSrcToken(
params: QuoteParams,
fourMemeClient: FourMeme,
tokenAddress: string,
tokenInfo: FourMemeTokenInfo,
): Promise<{ isHandled: boolean; quote?: QuoteTypes; error?: string }> {
const tokenManager = new ethers.Contract(tokenInfo.tokenManager, FOUR_MEME_TOKEN_MANAGER_2_LITE_ABI);
const calldataArray: CallStruct[] = [];
let affiliateFeeData;
const isAffiliateFeeDataProvided = params.affiliateFee && params.affiliateWallet;
if (isAffiliateFeeDataProvided) {
affiliateFeeData = calculateAffiliateFeeAndUpdateAmountIn(params);
collectAffiliateFeeBeforeSwap(calldataArray, params, affiliateFeeData.formattedAffiliateFeeData);
}
const tryBuyResult = await fourMemeClient.tokenManagerHelperV3.tryBuy({
tokenAddress,
nativeAmount: affiliateFeeData?.updatedAmountIn ?? BigInt(params.amount),
});
if (!tryBuyResult) {
return { isHandled: true, error: 'Could not fetch quote. Error estimating amount out.' };
}
const estimatedTokenAmount = tryBuyResult.estimatedAmount;
const minTokenAmountToReceive = (estimatedTokenAmount * BigInt(10000 - params.slippage * 100)) / BigInt(10000);
const BNBSwapAmountWithoutFee = affiliateFeeData ? affiliateFeeData.updatedAmountIn : BigInt(params.amount);
// BNB -> FM swap calldata
const swapCalldata: CallStruct = {
params: ZERO_HASH,
target: tokenManager.target as `0x${string}`,
msgValue: BNBSwapAmountWithoutFee,
data: tokenManager.interface.encodeFunctionData('buyTokenAMAP(address,address,uint256,uint256)', [
tokenAddress,
params.senderAddress,
BNBSwapAmountWithoutFee,
minTokenAmountToReceive,
]) as `0x${string}`,
};
calldataArray.push(swapCalldata);
const inputTokenData = await getTokenData(params.srcToken, params.srcChain);
const outputTokenData = await getTokenData(tokenAddress, params.destChain);
const quote: QuoteTypes = {
routes: [],
inputAmount: {
address: params.srcToken,
decimals: inputTokenData.decimals ?? 18,
name: inputTokenData.name ?? '',
symbol: inputTokenData.symbol ?? '',
value: params.amount,
chainId: params.srcChain,
},
outputAmount: {
address: tokenAddress,
decimals: outputTokenData.decimals ?? 18,
name: outputTokenData.name ?? '',
symbol: outputTokenData.symbol ?? '',
value: estimatedTokenAmount.toString(),
chainId: params.destChain,
},
calldatas: {
chainId: params.srcChain,
from: params.senderAddress,
value: params.amount,
data: encodeFunctionData({
abi: SHOGUN_MULTICALL_V1_ABI,
functionName: 'multicall',
args: [calldataArray, ZERO_ADDRESS, params.senderAddress as `0x${string}`, BigInt(0)],
}),
to: SHOGUN_MULTICALL_V1_ADDRESS,
},
...(affiliateFeeData?.formattedAffiliateFeeData ?? { affiliateFee: {} }),
};
return { isHandled: true, quote };
}
async function handleFourMemeBuyQuoteWithNonBNBSrcToken(
apiKey: string,
apiUrl: string,
params: QuoteParams,
fourMemeClient: FourMeme,
tokenAddress: string,
tokenInfo: FourMemeTokenInfo,
): Promise<{ isHandled: boolean; quote?: QuoteTypes; error?: string }> {
if (params.srcChain === SOLANA_CHAIN_ID) {
return { isHandled: true, error: 'Could not fetch quote. Solana is not supported as source chain.' };
}
const intermediateQuoteBody = {
...params,
destToken: NATIVE_TOKEN.ETH,
};
let intermediateQuote: QuoteTypes | undefined;
try {
intermediateQuote = await getQuote(
{
key: apiKey,
url: apiUrl,
},
intermediateQuoteBody,
);
if (!intermediateQuote) {
return { isHandled: true, error: 'Could not fetch quote. Error fetching quote.' };
}
if (intermediateQuote.error) {
return {
isHandled: true,
error: `Could not fetch quote. ${intermediateQuote.error}`,
};
}
} catch (error) {
return { isHandled: true, error: `Could not fetch quote. ${error}` };
}
const expectedBnbAmount = BigInt(intermediateQuote.outputAmount.value);
const tryBuyResult = await fourMemeClient.tokenManagerHelperV3.tryBuy({
tokenAddress,
nativeAmount: expectedBnbAmount,
});
if (!tryBuyResult) {
return { isHandled: true, error: 'Could not fetch quote. Error estimating amount out.' };
}
const expectedAmountOut = tryBuyResult.estimatedAmount;
const expectedAmountOutMin = (expectedAmountOut * (BigInt(10_000) - BigInt(params.slippage * 100))) / BigInt(10_000);
const v2Buyer = new ethers.Contract(FOUR_MEME_V2_BUYER_ADDRESS, FOUR_MEME_V2_BUYER_ABI);
const externalCall: EvmExternalCall = {
type: ExternalCallType.EVM,
to: v2Buyer.target as `0x${string}`,
data: v2Buyer.interface.encodeFunctionData('buyTokenWithBNB', [
tokenInfo.tokenManager,
tokenAddress,
params.senderAddress,
expectedAmountOutMin,
]) as `0x${string}`,
allowFailure: params.srcChain !== params.destChain,
fallbackAddress: params.senderAddress as `0x${string}`,
mode: ExternalCallMode.ApproveAndCall,
};
try {
const finalQuote = await getQuote(
{
key: apiKey,
url: apiUrl,
},
{
...intermediateQuoteBody,
externalCall: JSON.stringify(externalCall),
destinationAddress: v2Buyer.target as `0x${string}`,
},
);
if (!finalQuote) {
return {
isHandled: true,
error: `Could not fetch quote. Error fetching quote.`,
};
}
if (finalQuote.error) {
return {
isHandled: true,
error: `Could not fetch quote. ${finalQuote.error}`,
};
}
return { isHandled: true, quote: finalQuote };
} catch (error) {
return { isHandled: true, error: `Could not fetch quote. ${error}` };
}
}
async function handleFourMemeSellQuote(
params: QuoteParams,
fourMemeClient: FourMeme,
tokenAddress: string,
tokenInfo: FourMemeTokenInfo,
): Promise<{ isHandled: boolean; quote?: QuoteTypes; error?: string }> {
if (!compareAddresses(params.destToken, NATIVE_TOKEN.ETH) || params.destChain !== BSC_CHAIN_ID) {
return { isHandled: true, error: 'Could not fetch quote. Destination token must be BNB.' };
}
const tokenManager = new ethers.Contract(tokenInfo.tokenManager, FOUR_MEME_TOKEN_MANAGER_2_LITE_ABI);
params.amount = formatTokenAmount(params.amount);
const trySellResult = await fourMemeClient.tokenManagerHelperV3.trySell({
tokenAddress,
amount: BigInt(params.amount),
});
if (!trySellResult) {
return { isHandled: true, error: 'Could not fetch quote. Error estimating amount out.' };
}
const expectedAmountOut = trySellResult.funds;
const expectedAmountOutMin = (expectedAmountOut * BigInt(10000 - params.slippage * 100)) / BigInt(10000);
const isAffiliateFeeDataProvided = params.affiliateFee && params.affiliateWallet;
const data = isAffiliateFeeDataProvided
? (tokenManager.interface.encodeFunctionData('sellToken(uint256, address, uint256, uint256, uint256, address)', [
0,
tokenAddress,
params.amount,
expectedAmountOutMin,
Number(params.affiliateFee) * 100,
params.affiliateWallet,
]) as `0x${string}`)
: (tokenManager.interface.encodeFunctionData('sellToken(uint256, address, uint256, uint256)', [
0,
tokenAddress,
params.amount,
expectedAmountOutMin,
]) as `0x${string}`);
const inputTokenData = await getTokenData(tokenAddress, params.srcChain);
const outputTokenData = await getTokenData(NATIVE_TOKEN.ETH, params.destChain);
const quote: QuoteTypes = {
routes: [],
inputAmount: {
address: tokenAddress,
decimals: inputTokenData.decimals ?? 18,
name: inputTokenData.name ?? '',
symbol: inputTokenData.symbol ?? '',
value: params.amount,
chainId: params.srcChain,
},
outputAmount: {
address: NATIVE_TOKEN.ETH,
decimals: outputTokenData.decimals ?? 18,
name: outputTokenData.name ?? '',
symbol: outputTokenData.symbol ?? '',
value: expectedAmountOut.toString(),
chainId: params.destChain,
},
calldatas: {
chainId: params.srcChain,
from: params.senderAddress,
value: '0',
data,
to: tokenManager.target as `0x${string}`,
},
};
return { isHandled: true, quote };
}